💥 破坏性更新 咱不推荐更新

This commit is contained in:
7836246 2024-03-25 06:55:06 +08:00
parent 9bb0fbd9a6
commit 410dc1ac58
52 changed files with 3645 additions and 1337 deletions

4
.npmrc Normal file
View File

@ -0,0 +1,4 @@
public-hoist-pattern[]=@css-render/vue3-ssr
public-hoist-pattern[]=vueuc
public-hoist-pattern[]=naive-ui
shamefully-hoist=true

View File

@ -10,7 +10,13 @@ Nuxt-Whois 是一个基于 Nuxt3、Tailwind CSS 和 Xep-Whois 构建的Whois查
- **Dns查询**支持Dns查询方便用户查看域名的Dns信息。
- **自定义后缀**支持自定义Whois服务器后缀方便用户查询不同后缀的域名。
## 更新说明
- 2024.3.25 抛弃原来 NuxtUi 改用 NaiveUi 重构中 后台增加中 当前版本无法上线使用
- 2024.3.18 重构V2版本 预计三天内完成。
### 内容修改
大部分多语言文字都在lang文件夹下或者.env文件可以自行修改。
### 环境要求
@ -59,4 +65,5 @@ pnpm dev
```
# 免责声明
本项目开源仅供学习使用,不得用于任何违法用途,否则后果自负,与本人无关。使用请保留项目地址谢谢。
本项目开源仅供学习使用,不得用于任何违法用途,否则后果自负,与本人无关。使用请保留项目地址谢谢。

47
app.vue
View File

@ -1,7 +1,44 @@
<template>
<NuxtLoadingIndicator />
<NuxtLayout>
<UNotifications />
<NuxtPage />
</NuxtLayout>
<n-config-provider>
<n-modal-provider>
<n-message-provider>
<NuxtLayout>
<NuxtLoadingIndicator/>
<NuxtPage/>
<!-- <CommonLayoutSetting v-if="isAdminRoute" class="fixed right-12 top-1/2 z-999" />-->
</NuxtLayout>
</n-message-provider>
</n-modal-provider>
</n-config-provider>
</template>
<script setup lang="ts">
const whoisStore = useWhoisStore()
const dnsStore = useDnsStore()
const domainStore = useDomainStore()
whoisStore.newWhoisList()
dnsStore.newDnsList()
domainStore.newDomainList()
</script>
<style>
body {
background-color: #fff;
color: rgba(0, 0, 0, 0.8);
}
.dark-mode body {
background-color: #091a28;
color: #18181c;
}
.sepia-mode body {
background-color: #f1e7d0;
color: #433422;
}
.light-mode body {
background-color: #F1F3F4;
color: #433422;
}
</style>

BIN
assets/images/login-pic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@ -0,0 +1,13 @@
<script lang="ts" setup>
const localePath = useLocalePath()
const router = useRouter();
</script>
<template>
<div
class="cursor-pointer flex h-10 w-10 items-center justify-center rounded-lg bg-gray-100 dark:bg-gray-700"
@click="router.push(localePath('/settings/api'))"
>
<Icon name="i-eos-icons:api-outlined" class=" text-lg dark:text-white" />
</div>
</template>

View File

@ -0,0 +1,27 @@
<script setup lang="ts">
defineProps({
full: {
type: Boolean,
default: false,
},
showFooter: {
type: Boolean,
default: false,
},
})
</script>
<template>
<main class="cus-scroll h-full flex-col flex-1 bg-#f5f6fb dark:bg-#121212">
<transition name="fade-slide" mode="out-in" appear>
<main :class="{ 'flex-1': full }" class="m-12"><slot /></main>
</transition>
<slot v-if="$slots.footer" name="footer" />
<CommonTheFooter v-else-if="showFooter" class="mb-12 mt-auto" />
<n-back-top :bottom="20" />
</main>
</template>
<style scoped>
</style>

View File

@ -15,16 +15,17 @@ const {t} = useI18n()
<Icon name="mdi:about-circle-outline" class=" text-lg dark:text-white" />
</div>
</div>
<USlideover
v-model="isOpen"
side="left"
<NDrawer
v-model:show="isOpen"
placement="left"
:default-width="502"
resizable
>
<button>
<Icon name="lets-icons:close-ring-light" class="absolute top-2 right-2 text-gray-500 cursor-pointer" @click="isOpen = false" />
</button>
<div class="p-4 flex-1">
<div>{{t('index.support')}}</div>
<div class="flex flex-wrap mt-2 p-5 overflow-y-auto max-h-[95vh]">
<NDrawerContent
:title="t('index.support')"
closable
>
<div class="flex flex-wrap mt-2 ">
<span
v-for="item in SupportedTLDs"
:key="item"
@ -33,8 +34,8 @@ const {t} = useI18n()
{{ item }}
</span>
</div>
</div>
</USlideover>
</NDrawerContent>
</NDrawer>
</div>
</template>

View File

@ -1,16 +1,8 @@
<script setup lang="ts">
import {useStyleStore} from "~/stores/style";
const isOpen = ref(false)
const styleStore = useStyleStore()
const {t} = useI18n()
const slideoverConfig = {
//
width: 'w-screen max-w-2xl', //
// ...
};
</script>
@ -25,18 +17,20 @@ const slideoverConfig = {
</div>
</div>
<USlideover
v-model="isOpen"
side="right"
:ui="slideoverConfig"
<NDrawer
v-model:show="isOpen"
placement="right"
:default-width="602"
resizable
>
<button>
<Icon name="lets-icons:close-ring-light" class="absolute top-2 right-2 text-gray-500 cursor-pointer" @click="isOpen = false" />
</button>
<div class="w-full min-h-screen bg-gray-100 p-5 overflow-y-auto max-h-[95vh]">
<NDrawerContent
:title="t('history.title')"
class="w-full min-h-screen bg-gray-100 overflow-y-auto"
closable
>
<div class="max-w-6xl mx-auto">
<h1 class="text-2xl font-bold text-gray-800 mb-5 flex items-center justify-between">
{{ t('history.title') }}
<span class="text-sm text-gray-500 bg-gray-100 py-1 px-3 rounded-full">
{{ t('history.tips', { length: styleStore.getHistory.length }) }}
</span>
@ -44,7 +38,6 @@ const slideoverConfig = {
<div class="bg-white shadow-md rounded-lg">
<!-- 条件渲染如果有历史记录则显示表格否则显示提示 -->
<div v-if="styleStore.getHistory.length">
<table class="min-w-full leading-normal">
<!-- 表格头部和内容 -->
<table class="min-w-full leading-normal">
<thead>
@ -76,23 +69,22 @@ const slideoverConfig = {
{{ item.date }}
</td>
<td class="px-5 py-5 text-sm bg-white">
<UButton
<NButton
@click="styleStore.deleteHistory(item.id)"
color="sky"
>{{t('common.actions.delete')}}</UButton>
>{{t('common.actions.delete')}}</NButton>
</td>
</tr>
</tbody>
</table>
</table>
</div>
<div v-else class="text-center py-5">
<p class="text-gray-500">{{ t('history.empty') }}</p>
</div>
</div>
</div>
</div>
</USlideover>
</NDrawerContent>
</NDrawer>
</div>
</template>

View File

@ -0,0 +1,98 @@
<script setup lang="ts">
// import { MeModal } from '@/components'
const [modalRef] = useModal()
</script>
<template>
123
<div>
<n-tooltip trigger="hover" placement="left">
<template #trigger>
<IconSettings size="32" @click="modalRef.open()" filled class="cursor-pointer text-32 color-primary" />
</template>
布局设置
</n-tooltip>
<MeModal
ref="modalRef"
title="布局设置"
:show-footer="false"
width="600px"
:modal-style="{ opacity: 0.85 }"
>
<n-space justify="space-between">
<div class="flex-col cursor-pointer justify-center" @click="appStore.setLayout('simple')">
<div class="flex">
<n-skeleton :width="20" :height="60" />
<div class="ml-4">
<n-skeleton :width="80" :height="60" />
</div>
</div>
<n-button
class="mt-12"
size="small"
:type="appStore.layout === 'simple' ? 'primary' : ''"
ghost
>
简约
</n-button>
</div>
<div class="flex-col cursor-pointer justify-center" @click="appStore.setLayout('normal')">
<div class="flex">
<n-skeleton :width="20" :height="60" />
<div class="ml-4">
<n-skeleton :width="80" :height="10" />
<n-skeleton class="mt-4" :width="80" :height="46" />
</div>
</div>
<n-button
class="mt-12"
size="small"
:type="appStore.layout === 'normal' ? 'primary' : ''"
ghost
>
通用
</n-button>
</div>
<div class="flex-col cursor-pointer justify-center" @click="appStore.setLayout('full')">
<div class="flex">
<n-skeleton :width="20" :height="60" />
<div class="ml-4">
<n-skeleton :width="80" :height="6" />
<n-skeleton class="mt-4" :width="80" :height="4" />
<n-skeleton class="mt-4" :width="80" :height="42" />
</div>
</div>
<n-button
class="mt-12"
size="small"
:type="appStore.layout === 'full' ? 'primary' : ''"
ghost
>
全面
</n-button>
</div>
<div class="flex-col cursor-pointer justify-center" @click="appStore.setLayout('empty')">
<div class="flex">
<n-skeleton :width="104" :height="60" />
</div>
<n-button
class="mt-12"
size="small"
:type="appStore.layout === 'empty' ? 'primary' : ''"
ghost
>
空白
</n-button>
</div>
</n-space>
<p class="mt-16 opacity-50">
: 此设置仅对未设置layout或者设置成跟随系统的页面有效菜单设置的layout优先级最高
</p>
</MeModal>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,23 @@
<script setup lang="ts">
</script>
<template>
<footer class="f-c-c text-14 text-gray-500">
<p>
Copyright © 2023
<a
href="https://github.com/zclzone"
target="__blank"
class="transition"
hover="decoration-underline color-primary"
>
Ronnie Zhang(大脸怪)
</a>
</p>
</footer>
</template>
<style scoped>
</style>

View File

@ -1,29 +1,24 @@
<script lang="ts" setup>
import {useTimeStore} from "~/stores/time";
const switchLocalePath = useSwitchLocalePath()
const timeStore = useTimeStore()
const settingsStore = useSettingsStore()
const availableDns = [
{ iso: '', name: '本地 DNS', flag: 'material-symbols:dns-outline' },
{ iso: 'google', name: 'Google', flag: 'flat-color-icons:google' },
{ iso: 'aliyun', name: 'AliYun', flag: 'ant-design:aliyun-outlined' },
{ iso: 'tencent', name: 'Tencent', flag: 'emojione:cloud' },
{ iso: 'cloudflare', name: 'CloudFlare', flag: 'skill-icons:cloudflare-light' },
// ...
]
const dnsStore = useDnsStore()
const handlePost = async (iso: string) => {
timeStore.setDnsServer(iso)
const {newsDnsArr} = storeToRefs(dnsStore)
const handlePost = async (name: string) => {
dnsStore.moveToTop(name)
//
await refreshNuxtData('dns')
}
</script>
<template>
<div>
<HeadlessListbox
v-model="timeStore.getDnsServer"
v-model="dnsStore.newsDnsArr"
as="div"
class="relative flex items-center"
>
@ -34,29 +29,33 @@ const handlePost = async (iso: string) => {
<div
class="flex h-10 w-10 items-center justify-center rounded-lg bg-gray-100 dark:bg-gray-700"
>
<Icon name="gg:select-o" class=" text-lg dark:text-white" size="20" />
<Icon name="gg:select-o" class=" text-lg dark:text-white" size="20"/>
</div>
</HeadlessListboxButton>
<HeadlessListboxOptions
class="absolute top-full right-0 z-[999] mt-2 w-40 overflow-hidden rounded-lg bg-white text-sm font-semibold text-gray-700 shadow-lg shadow-gray-300 outline-none dark:bg-gray-800 dark:text-white dark:shadow-gray-500 dark:ring-0"
>
<div
v-for="lang in availableDns"
:key="lang.iso"
@click="handlePost(lang.iso)"
class="flex w-full cursor-pointer items-center justify-between py-2 px-3"
v-for="lang in newsDnsArr"
:key="lang.name"
@click="handlePost(lang.name)"
:class="{
'text-white-500 bg-gray-200 dark:bg-gray-500/50':
timeStore.getDnsServer === lang.iso,
'hover:bg-gray-200 dark:hover:bg-gray-700/30':
timeStore.getDnsServer !== lang.iso,
'flex w-full cursor-pointer items-center justify-between py-2 px-3 text-white-500 bg-gray-200 dark:bg-gray-500/50':
dnsStore.getFirstNewDnsShown.name === lang.name && lang.show,
'flex w-full cursor-pointer items-center justify-between py-2 px-3 hover:bg-gray-200 dark:hover:bg-gray-700/30':
dnsStore.getFirstNewDnsShown.name !== lang.name && lang.show,
}"
>
<span class="truncate">
{{ lang.name }}
<span
v-if="lang.show"
class="truncate">
{{ lang.iName }}
</span>
<span class="flex items-center justify-center text-sm">
<Icon :name="lang.flag" class="text-base" size="20" />
<span
v-if="lang.show"
class="flex items-center justify-center text-sm">
<Icon :name="lang.flag" class="text-base" size="20"/>
</span>
</div>
</HeadlessListboxOptions>

View File

@ -1,47 +1,89 @@
<script setup lang="ts">
const timeStore = useTimeStore()
const settingsStore = useSettingsStore()
const emit = defineEmits(['action'])
const handleActionFromDnsList = (urlParam:string) => {
emit('action', urlParam)
}
const {t} = useI18n()
</script>
<template>
<ClientOnly>
<div class="flex justify-between w-full">
<div class="space-x-2">
<div class="flex space-x-2">
<!-- 左边的新元素 -->
<UTooltip :text="t('popper.support')" :popper="{ placement: 'top' }">
<CommonDomainList />
</UTooltip>
<n-tooltip
v-if="settingsStore.isDomainList"
trigger="hover" placement="top">
<template #trigger>
<CommonDomainList/>
</template>
<span>{{ t('popper.support') }}</span>
</n-tooltip>
<UTooltip
<n-tooltip
v-if="settingsStore.getHistory"
:text="t('popper.history')" :popper="{ placement: 'top' }">
<CommonHistory />
</UTooltip>
trigger="hover" placement="top">
<template #trigger>
<CommonHistory/>
</template>
<span>{{ t('popper.history') }}</span>
</n-tooltip>
<!-- <n-tooltip-->
<!-- v-if="settingsStore.getHistory"-->
<!-- trigger="hover" placement="top" >-->
<!-- <template #trigger>-->
<!-- <CommonDnsList @action="handleActionFromDnsList" />-->
<!-- </template>-->
<!-- <span>{{t('popper.dns')}}</span>-->
<!-- </n-tooltip>-->
<UTooltip :text="t('popper.dns')" :popper="{ placement: 'top' }">
<CommonDnsList @action="handleActionFromDnsList" />
</UTooltip>
</div>
<div class="flex space-x-2">
<!-- 右边的现有元素 -->
<UTooltip :text="t('popper.setting')" :popper="{ placement: 'top' }">
<CommonSettingsChange />
</UTooltip>
<UTooltip :text="timeStore.timeZones" :popper="{ placement: 'top' }">
<CommonTimeZonesChange />
</UTooltip>
<UTooltip :text="t('popper.theme')" :popper="{ placement: 'top' }">
<CommonColorChange />
</UTooltip>
<UTooltip :text="t('popper.language')" :popper="{ placement: 'top' }">
<CommonLanguageChange />
</UTooltip>
<n-tooltip
trigger="hover"
placement="top">
<template #trigger>
<CommonSettingsChange/>
</template>
<span>{{ t('popper.setting') }}</span>
</n-tooltip>
<n-tooltip
trigger="hover"
placement="top">
<template #trigger>
<CommonApiChange/>
</template>
<span>第三方APi</span>
</n-tooltip>
<n-tooltip
trigger="hover"
placement="top">
<template #trigger>
<CommonTimeZonesChange/>
</template>
<span>{{ timeStore.timeZones }}</span>
</n-tooltip>
<n-tooltip
trigger="hover"
placement="top">
<template #trigger>
<CommonColorChange/>
</template>
<span>{{ t('popper.theme') }}</span>
</n-tooltip>
<n-tooltip
trigger="hover"
placement="top">
<template #trigger>
<CommonLanguageChange/>
</template>
<span>{{ t('popper.language') }}</span>
</n-tooltip>
</div>
</div>
</ClientOnly>

View File

@ -0,0 +1,43 @@
export const trimDomain = (domain: string): string => {
return domain.trim().toLowerCase(); // 确保域名为小写
};
export const splitDomain = (domain: string): string[] => {
return domain.split('.');
};
//
// const SupportedTLDs = new Set(Object.keys(domainStore.SupportedTLDs));
//
// export const updateDomainForTLD = (parts: string[]): string => {
// const potentialTLD = parts.slice(-2).join('.').toLowerCase(); // 确保为小写
// let domainToKeep: string;
// if (SupportedTLDs.has(potentialTLD)) {
// domainToKeep = parts.length > 2 ? parts.slice(-3).join('.') : parts.join('.');
// } else {
// domainToKeep = parts.slice(-2).join('.');
// }
// return domainToKeep;
// };
//
//
// export const validateDomain = (parts: string[]): boolean => {
// if (parts.length < 2) {
// const message = useMessage()
// message.warning('域名格式不正确')
// return false;
// }
// return true;
// };
//
//
// const isTLDValid = (parts: string[]): boolean => {
// const lastPart = parts[parts.length - 1].toLowerCase(); // 获取最后一部分,并确保为小写
// const potentialTLD = parts.slice(-2).join('.').toLowerCase(); // 获取可能的多部分TLD并确保为小写
//
// if (!SupportedTLDs.has(lastPart) && !SupportedTLDs.has(potentialTLD)) {
// const message = useMessage()
// message.warning('域名后缀不合法')
// return false;
// }
// return true;
// };

View File

@ -0,0 +1,122 @@
<script setup lang="ts">
import { RefreshCcw,XCircle,ArrowRightLeft,ArrowLeftFromLine,ArrowRightFromLine } from 'lucide-vue-next';
import {useTabStore} from "~/stores/admin/tab";
const props = defineProps({
show: {
type: Boolean,
default: false,
},
currentPath: {
type: String,
default: '',
},
x: {
type: Number,
default: 0,
},
y: {
type: Number,
default: 0,
},
})
const emit = defineEmits(['update:show'])
const tabStore = useTabStore()
const options = computed(() => [
{
label: '重新加载',
key: 'reload',
disabled: props.currentPath !== tabStore.activeTab,
icon: () => h(RefreshCcw),
},
{
label: '关闭',
key: 'close',
disabled: tabStore.tabs.length <= 1,
icon: () => h(XCircle),
},
{
label: '关闭其他',
key: 'close-other',
disabled: tabStore.tabs.length <= 1,
icon: () => h(ArrowRightLeft),
},
{
label: '关闭左侧',
key: 'close-left',
disabled: tabStore.tabs.length <= 1 || props.currentPath === tabStore.tabs[0].path,
icon: () => h(ArrowLeftFromLine),
},
{
label: '关闭右侧',
key: 'close-right',
disabled:
tabStore.tabs.length <= 1 ||
props.currentPath === tabStore.tabs[tabStore.tabs.length - 1].path,
icon: () => h(ArrowRightFromLine),
},
])
const route = useRoute()
const actionMap = new Map([
[
'reload',
() => {
tabStore.reloadTab(route.fullPath)
},
],
[
'close',
() => {
tabStore.removeTab(props.currentPath)
},
],
[
'close-other',
() => {
tabStore.removeOther(props.currentPath)
},
],
[
'close-left',
() => {
tabStore.removeLeft(props.currentPath)
},
],
[
'close-right',
() => {
tabStore.removeRight(props.currentPath)
},
],
])
function handleHideDropdown() {
emit('update:show', false)
}
function handleSelect(key:any) {
const actionFn = actionMap.get(key)
actionFn && actionFn()
handleHideDropdown()
}
</script>
<template>
<n-dropdown
:show="show"
:options="options"
:x="x"
:y="y"
placement="bottom-start"
@clickoutside="handleHideDropdown"
@select="handleSelect"
/>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,95 @@
<script setup lang="ts">
import ContextMenu from './ContextMenu.vue'
import {useTabStore} from "~/stores/admin/tab";
const router = useRouter()
// const appStore = useAppStore()
const tabStore = useTabStore()
const contextMenuOption = reactive({
show: false,
x: 0,
y: 0,
currentPath: '',
})
const handleItemClick = (path:any) => {
tabStore.setActiveTab(path)
router.push(path)
}
function showContextMenu() {
contextMenuOption.show = true
}
function hideContextMenu() {
contextMenuOption.show = false
}
function setContextMenu(x:any, y:any, currentPath:any) {
Object.assign(contextMenuOption, { x, y, currentPath })
}
//
async function handleContextMenu(e:any, tagItem:any) {
const { clientX, clientY } = e
hideContextMenu()
setContextMenu(clientX, clientY, tagItem.path)
await nextTick()
showContextMenu()
}
</script>
<template>
<div>
<n-tabs
:value="tabStore.activeTab"
:closable="tabStore.tabs.length > 1"
:style="`--selected-bg: ${appStore.isDark ? '#1b2429' : '#eaf0f1'}`"
type="card"
@close="(path) => tabStore.removeTab(path)"
>
<n-tab
v-for="item in tabStore.tabs"
:key="item.path"
:name="item.path"
@click="handleItemClick(item.path)"
@contextmenu.prevent="handleContextMenu($event, item)"
>
{{ item.title }}
</n-tab>
</n-tabs>
<ContextMenu
v-if="contextMenuOption.show"
v-model:show="contextMenuOption.show"
:current-path="contextMenuOption.currentPath"
:x="contextMenuOption.x"
:y="contextMenuOption.y"
/>
</div>
</template>
<style scoped lang="scss">
:deep(.n-tabs) {
.n-tabs-tab {
padding-left: 16px;
height: 36px;
background: transparent !important;
border-radius: 4px !important;
margin-right: 4px;
&:hover {
border: 1px solid var(--primary-color) !important;
}
}
.n-tabs-tab--active {
border: 1px solid var(--primary-color) !important;
background-color: var(--selected-bg) !important;
}
.n-tabs-pad,
.n-tabs-tab-pad,
.n-tabs-scroll-padding {
border: none !important;
}
}
</style>

View File

@ -1,89 +1,60 @@
<script setup lang="ts">
import {useStyleStore} from "~/stores/style";
import {useApisStore} from "~/stores/api";
const { t } = useI18n()
const state = reactive({
domain: '',
})
const toast = useToast();
const {t} = useI18n()
const router = useRouter();
const runtimeConfig = useRuntimeConfig()
const localePath = useLocalePath()
const settingsStore = useSettingsStore()
const domainStore = useDomainStore()
const SupportedTLDs = new Set(Object.keys(domainStore.SupportedTLDs));
const handleAction = async (url: any) => {
if (!state.domain) return toast.add({ title: '请输入域名' })
let domain = trimDomain(state.domain);
const parts = splitDomain(domain);
if (!validateDomain(parts) || !isTLDValid(parts)) return;
domain = updateDomainForTLD(parts);
state.domain = domain;
const isLink = ref({})
isLink.value = settingsStore.linkOpenType != 'currentWindow'
await router.push(localePath(`/${url}/${state.domain.replace(/\./g, '_')}.html`))
}
const trimDomain = (domain: string): string => {
return domain.trim().toLowerCase(); //
};
const splitDomain = (domain: string): string[] => {
return domain.split('.');
};
const validateDomain = (parts: string[]): boolean => {
if (parts.length < 2) {
toast.add({ title: '域名格式不正确' });
return false;
}
return true;
};
const isTLDValid = (parts: string[]): boolean => {
const lastPart = parts[parts.length - 1].toLowerCase(); //
const potentialTLD = parts.slice(-2).join('.').toLowerCase(); // TLD
if (!SupportedTLDs.has(lastPart) && !SupportedTLDs.has(potentialTLD)) {
toast.add({ title: '域名后缀不合法' });
return false;
}
return true;
};
const updateDomainForTLD = (parts: string[]): string => {
const potentialTLD = parts.slice(-2).join('.').toLowerCase(); //
let domainToKeep: string;
if (SupportedTLDs.has(potentialTLD)) {
domainToKeep = parts.length > 2 ? parts.slice(-3).join('.') : parts.join('.');
} else {
domainToKeep = parts.slice(-2).join('.');
}
return domainToKeep;
};
const styleStore = useStyleStore()
const clientMounted = ref(false);
const apisStore = useApisStore()
const message = useMessage()
const handleAction = async (url: any) => {
if (!settingsStore.getDomain) return message.error('请输入域名')
//
const domainPattern = /^(?!:\/\/)([a-zA-Z0-9-_]+\.)+[a-zA-Z0-9]{2,11}?$/;
// IPv4
const ipPattern = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
// state.domainIP
if (!domainPattern.test(settingsStore.getDomain) && !ipPattern.test(settingsStore.getDomain)) {
message.error('请输入正确的域名或IP地址')
return;
}
await router.push(localePath(`/${url}/${settingsStore.getDomain.replace(/\./g, '_')}.html`))
}
onMounted(() => {
clientMounted.value = true;
});
const selectOptions = ref([
{
label: 'Whois',
value: 'whois'
}, {
label: 'Dns',
value: 'dns'
}, {
label: 'Domain',
value: 'domain'
}
])
const {selectedOption} = storeToRefs(settingsStore)
const handleSelectOptions = (value: any) => {
settingsStore.setSelectedOption(value);
console.log(selectedOption.value)
}
</script>
<template>
<div
class="w-full text-xs bg-[#F1F3F4] dark:bg-transparent"
class="w-full text-xs dark:bg-transparent"
:class="{ 'h-[90vh]': !styleStore.getIsPage && clientMounted }"
>
<div
@ -99,34 +70,51 @@ onMounted(() => {
</NuxtLink>
</nav>
<div class="mt-6">
<UForm :state="state"
class="flex items-center space-x-2 mb-3 dark:text-white"
@submit="handleAction('whois')">
<div class="flex items-center space-x-2 mb-3 dark:text-white"
>
<!-- 容器div用于水平布局 -->
<div class="flex-grow">
<UInput
v-model="state.domain"
:placeholder="t('index.placeholder')"
color="sky"
size="xl"
class="w-full " />
<NInputGroup>
<n-select
:style="{ width: '20%' }"
size="large"
v-model:value="selectedOption"
:options="selectOptions"
@update:value="handleSelectOptions"
/>
<NInput
v-model:value="settingsStore.domainSearch"
@keyup.enter="handleAction(settingsStore.selectedOption)"
type="text"
:placeholder="t('index.placeholder')"
size="large"
clearable
autofocus
class="w-full "/>
</NInputGroup>
</div>
<!-- 使用v-if或v-show基于state.domain的值来控制按钮的显示 -->
<UButton type="submit" color="sky" size="xl" v-if="state.domain">
<!-- 使用v-if基于state.domain的值来控制按钮的显示 -->
<NButton type="primary"
size="large"
@click="handleAction(settingsStore.selectedOption)"
v-if="settingsStore.domainSearch">
{{ t('index.onSubmit') }}
</UButton>
</UForm>
</NButton>
</div>
</div>
<CommonBulletin
v-if="!styleStore.isPage && clientMounted"
:text="`➡️ ${t('index.tips') }`"
/>
<TabList @action="handleAction" />
<slot />
<ClientOnly>
<CommonBulletin
v-if="settingsStore.isBulletin && !styleStore.isPage"
:text="`➡️ ${t('index.tips') }`"
/>
</ClientOnly>
<TabList @action="handleAction"/>
<slot/>
</div>
</div>
<CommonFooter />
<CommonFooter/>
</template>
<style scoped>

View File

@ -1,15 +0,0 @@
<script setup lang="ts">
</script>
<template>
<div class="w-full h-[90vh] text-xs bg-[#F1F3F4] dark:bg-transparent">
<div class="max-w-screen-lg mx-auto pt-[15vh] px-[1em] pb-[10vh] ">
<slot />
</div>
</div>
<CommonFooter />
</template>
<style scoped>
</style>

14
layouts/empty/index.vue Normal file
View File

@ -0,0 +1,14 @@
<script setup lang="ts">
</script>
<template>
<div class="w-full text-xs bg-[#F1F3F4] dark:bg-transparent">
<div class="max-w-screen-lg mx-auto pt-[5vh] px-[1em] pb-[10vh] ">
<slot />
</div>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,56 @@
<script setup lang="ts">
import { useDark, useToggle, useFullscreen } from '@vueuse/core'
import {useAppStore} from "~/stores/admin/app";
const appStore = useAppStore()
const isDark = useDark()
const toggleDark = () => {
appStore.toggleDark()
useToggle(isDark)()
}
const { isFullscreen, toggle } = useFullscreen()
function handleLinkClick(link:string) {
window.open(link)
}
</script>
<template>
<CommonAppCard class="flex items-center px-12" border-b="1px solid light_border dark:dark_border">
<MenuCollapse />
<BreadCrumb />
<div class="ml-auto flex flex-shrink-0 items-center px-12 text-18" >
<n-popover trigger="hover">
<template #trigger>
<div class="mr-16 f-c-c cursor-pointer rounded-4 p-6 text-22 transition-all-300 auto-bg-hover" @click="toggleDark">
<!-- 根据 isDark 条件动态切换图标 -->
<IconMoon v-if="isDark" />
<IconSun v-else />
</div>
</template>
<!-- 根据 isDark 条件动态切换提示文本 -->
<span>{{ isDark ? '夜间模式' : '日间模式' }}</span>
</n-popover>
<n-popover trigger="hover">
<template #trigger>
<div class="mr-16 f-c-c cursor-pointer rounded-4 p-6 text-22 transition-all-300 auto-bg-hover" @click="toggle">
<IconMinimize v-if="isFullscreen" />
<IconMaximize v-else />
</div>
</template>
<span>{{ isFullscreen ? '退出全屏' : '全屏模式' }}</span>
</n-popover>
<UserAvatar />
</div>
</CommonAppCard>
</template>
<style scoped>
</style>

34
layouts/full/index.vue Normal file
View File

@ -0,0 +1,34 @@
<script setup lang="ts">
import AppTab from '@/layouts/components/tab/index'
import SideBar from './sidebar/index.vue'
import AppHeader from './header/index.vue'
import {useAppStore} from "~/stores/admin/app";
const appStore = useAppStore()
</script>
<template>
<div class="wh-full flex">
<aside
class="flex-col flex-shrink-0 transition-width-300"
:class="appStore.collapsed ? 'w-64' : 'w-220'"
border-r="1px solid light_border dark:dark_border"
>
<SideBar />
</aside>
<article class="w-0 flex-col flex-1">
<AppHeader class="h-60 flex-shrink-0" />
<div class="p-12" border-b="1px solid light_border dark:dark_border">
<AppTab class="flex-shrink-0" />
</div>
<slot />
</article>
</div>
</template>
<style scoped>
.collapsed {
width: 64px;
}
</style>

View File

@ -0,0 +1,12 @@
<script setup lang="ts">
</script>
<template>
<SideLogo border-b="1px solid light_border dark:dark_border" />
<SideMenu class="cus-scroll-y mt-4 h-0 flex-1" />
</template>
<style scoped>
</style>

View File

@ -1,14 +1,20 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
devtools: { enabled: true },
routeRules:{
'/admin/**':{ ssr : false }
},
modules: [
'@nuxt/devtools',
'@nuxt/ui',
'@nuxtjs/i18n',
'nuxt-headlessui',
'@pinia/nuxt', // needed
'@pinia-plugin-persistedstate/nuxt',
'@nuxt/devtools', // Devtools开发工具
'@nuxtjs/i18n', // 多语言
'@pinia/nuxt', // Pinia 持久化状态管理
'@pinia-plugin-persistedstate/nuxt', // Pinia 持久化状态管理插件
'nuxt-simple-robots',
'nuxt-headlessui', // 组件库
'@bg-dev/nuxt-naiveui', // 组件库
'@nuxtjs/tailwindcss', // 组件库
'nuxt-icon',
'@nuxtjs/color-mode',
],
features:{
inlineStyles: true,
@ -47,4 +53,14 @@ export default defineNuxtConfig({
headlessui: {
prefix: 'Headless'
},
naiveui:{
},
colorMode: {
preference: 'system', // default value of $colorMode.preference
fallback: 'light', // fallback value if not system preference found
classPrefix: '',
classSuffix: '-mode',
storageKey: 'nuxt-color-mode'
}
})

View File

@ -11,18 +11,25 @@
},
"dependencies": {
"@nuxt/ui": "^2.14.2",
"@nuxtjs/tailwindcss": "^6.11.4",
"@pinia/nuxt": "^0.5.1",
"nuxt": "^3.11.0",
"lucide-vue-next": "^0.363.0",
"nuxt": "^3.11.1",
"socks": "^2.8.1",
"vue": "^3.4.21",
"vue-router": "^4.3.0"
"vue-router": "^4.3.0",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@bg-dev/nuxt-naiveui": "^1.10.4",
"@nuxtjs/color-mode": "^3.3.3",
"@nuxtjs/i18n": "^8.2.0",
"@nuxtjs/tailwindcss": "^6.11.4",
"@pinia-plugin-persistedstate/nuxt": "^1.2.0",
"less": "^4.2.0",
"nuxt-headlessui": "^1.1.5",
"nuxt-simple-robots": "4.0.0-rc.14",
"typescript": "5.4.2"
"nuxt-icon": "^0.6.10",
"nuxt-simple-robots": "4.0.0-rc.15",
"sass": "^1.72.0",
"typescript": "5.4.3"
}
}

View File

@ -0,0 +1,179 @@
<script setup lang="ts">
definePageMeta({
layout: 'full',
})
</script>
<template>
<CommonAppPage show-footer>
<div class="flex">
<n-card class="min-w-200 w-30%">
<div class="flex items-center">
<!-- <n-avatar round :size="60" :src="userStore.avatar" class="flex-shrink-0" />-->
<div class="ml-20 flex-col">
<span class="text-20 opacity-80">
<!-- Hello, {{ userStore.nickName ?? userStore.username }}-->
</span>
<!-- <span class="mt-4 opacity-50">当前角色{{ userStore.currentRole?.name }}</span>-->
</div>
</div>
<p class="mt-28 text-14 opacity-60">一个人几乎可以在任何他怀有无限热忱的事情上成功</p>
<p class="mt-12 text-right text-12 opacity-40"> 查尔斯·史考伯</p>
</n-card>
<n-card class="ml-12 w-70%" title="✨ 欢迎使用 Vue Naive Admin 2.0">
<template #header-extra>
<a
class="text-14 text-primary text-highlight hover:underline hover:opacity-80"
href="https://isme.top"
target="_blank"
@click.prevent="useMessage()?.info('官网正在火速开发中...')"
>
isme.top
</a>
</template>
<p class="opacity-60">
这是一款极简风格的后台管理模板包含前后端解决方案前端使用 Vite + Vue3 + Pinia +
Unocss后端使用 Nestjs + TypeOrm +
MySql简单易用赏心悦目历经十几次重构和细节打磨诚意满满
</p>
<footer class="mt-12 flex items-center justify-end">
<n-button
type="primary"
ghost
tag="a"
href="https://docs.isme.top/web/#/624306705/188522224"
target="__blank"
>
开发文档
</n-button>
<n-button
type="primary"
class="ml-12"
tag="a"
href="https://github.com/zclzone/vue-naive-admin/tree/2.x"
target="__blank"
>
代码仓库
</n-button>
</footer>
</n-card>
</div>
<div class="mt-12 flex">
<n-card class="w-50%" title="💯 特性" segmented>
<template #header-extra>
<span class="opacity-90 text-highlight">👏 历经十几次重构和细节打磨</span>
</template>
<ul class="opacity-90">
<li class="py-4">
🆒 使用
<b>Vue3</b>
主流技术栈:
<span class="text-highlight">Vite + Vue3 + Pinia</span>
</li>
<li class="py-4">
🍇 使用
<b>原子CSS</b>
框架:
<span class="text-highlight">Unocss</span>
优雅轻量易用
</li>
<li class="py-4">
🤹 使用主流的
<span class="text-highlight">iconify + unocss</span>
图标方案支持自定义图标支持动态渲染
</li>
<li class="py-4">
🎨 使用 Naive UI
<span class="text-highlight">极致简洁的代码风格和清爽的页面设计</span>
审美在线主题轻松定制
</li>
<li class="py-4">
👏 先进且易于理解的文件结构设计多个模块之间
<b>零耦合</b>
单个业务模块删除不影响其他模块
</li>
<li class="py-4">
🚀
<span class="text-highlight">扁平化路由</span>
设计每一个组件都可以是一个页面告别多级路由 KeepAlive 难实现问题
</li>
<li class="py-4">
🍒
<span class="text-highlight">基于权限动态生成路由</span>
无需额外定义路由
<span class="text-highlight">403和404可区分</span>
而不是无权限也跳404
</li>
<li class="py-4">
🔐 基于Redis集成
<span class="text-highlight">无感刷新</span>
用户登录态可控安全与体验缺一不可
</li>
<li class="py-4">
基于 Naive UI 封装
<span class="text-highlight">message</span>
全局工具方法支持批量提醒支持跨页面共享实例
</li>
<li class="py-4">
基于 Naive UI 封装常用的业务组件包含
<span class="text-highlight">Page</span>
组件
<span class="text-highlight">CRUD</span>
表格组件及
<span class="text-highlight">Modal</span>
组件减少大量重复性工作
</li>
</ul>
<n-divider class="mb-0! mt-12!">
<p class="text-14 opacity-60">
👉点击
<b class="mx-2 transition hover:text-primary">
<a href="https://isme.top" target="_blank">更多</a>
</b>
查看更多实用功能持续开发中...
</p>
</n-divider>
</n-card>
<n-card class="ml-12 w-50%" title="🛠️ 技术栈" segmented>
<!-- <VChart :option="skillOption" autoresize />-->
</n-card>
</div>
<n-card class="mt-12" title="⚡️ 趋势" segmented>
<!-- <VChart :option="trendOption" :init-options="{ height: 400 }" autoresize />-->
</n-card>
</CommonAppPage>
</template>
<style lang="less" scoped>
.viewMount {
width: 100%;
height: 300px;
box-shadow: 0 1px 2px -2px rgba(0, 0, 0, 0.08), 0 3px 6px 0 rgba(0, 0, 0, 0.06),
0 5px 12px 4px rgba(0, 0, 0, 0.04);
border: 1px solid #efeff5;
.chart-header {
padding: 0 10px;
border-bottom: 1px solid #e5e6e7;
display: flex;
justify-content: space-between;
align-items: center;
height: 40px;
.left-title {
display: flex;
color: #3a4446;
}
.right-unit {
color: #fff;
padding: 1px 6px;
border-radius: 2px;
}
}
}
</style>

104
pages/admin/user/Login.vue Normal file
View File

@ -0,0 +1,104 @@
<template>
<div class="login-container flex items-center justify-center p-[20px]">
<div class="login-box w-[960px] h-[560px] md:w-[100%] md:px-[50px] px-[80px] py-[30px]">
<div class="flex items-center justify-center md:hidden">
<img :src="LoginPic" alt="login-pic" />
</div>
<div class="flex justify-center w-[360px] md:w-[100%] flex-col items-center space-y-8">
<div class="space-y-2">
<div class="flex justify-center items-center space-x-[10px]">
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="45" height="45">
<path
d="M228.364788 380.401625c0-20.948412 11.370167-40.019574 29.20405-49.277834l235.061667-136.228678 2.346563-1.237279c14.079381-6.762369 30.292002-6.719705 46.525955 1.322608l235.168329 137.103307c18.559184 9.044936 30.398664 28.500081 30.398664 49.832476l0.426648 272.905337c0 21.033742-11.476829 40.232898-28.436084 48.851186l-242.165354 135.290053a50.643107 50.643107 0 0 1-42.792786 1.621262l-3.178527-1.535933L99.239798 624.572225C80.445957 615.697948 68.222494 596.072144 68.222494 574.249104l0.959958-294.643048c0-21.417725 11.903477-40.894202 28.436084-48.851186L492.395848 5.058124c14.100713-6.869031 30.377331-6.869031 46.611285 1.130617l387.609628 223.499509c18.559184 9.023603 30.441329 28.500081 30.441328 49.917806v452.374781c0 21.183069-11.647488 40.467554-28.286756 48.765856l-392.004102 223.563506a50.643107 50.643107 0 0 1-43.048774 1.215946l-2.858541-1.407938-237.57889-130.767585-100.262259 89.638727c-21.076407 20.905748-54.397609 20.052452-74.450061-1.919916A56.424186 56.424186 0 0 1 64.020012 923.033772V754.145196c0-20.137781 15.593981-36.435732 34.835802-36.435732s34.835802 16.29795 34.835802 36.435732v130.340937l81.063104-72.48748a51.005758 51.005758 0 0 1 56.530848-10.900855l2.837209 1.386606 239.754794 131.962199 373.530247-213.025302V290.165591L514.666869 75.284371 138.811391 290.165591l-0.89596 273.331985 90.449357 49.51249V380.401625z m86.716188 173.752362h47.54991l73.511435 93.585219a42.046152 42.046152 0 0 0 60.648001 6.399719c6.954361-5.866409 11.946142-13.823392 14.33537-22.804331l46.077974-173.795027 57.87479 96.635752h95.398473c17.215243 0 31.16663-14.506029 31.16663-32.339911 0-17.876548-13.951387-32.339912-31.145298-32.339912h-60.775995l-62.78124-104.827392a43.304763 43.304763 0 0 0-25.193559-19.519142c-22.953658-6.549045-46.675282 7.466338-52.989671 31.273292l-47.848563 180.472067-68.690314-87.398825H315.102309c-17.215243 0-31.16663 14.463364-31.16663 32.339912 0 17.855215 13.951387 32.318579 31.145297 32.318579z"
></path>
</svg>
<n-gradient-text :gradient="gradient" class="text-[24px]">Admin Pro</n-gradient-text>
</div>
<div class="text-[#5a6f7e]">Admin Pro 中后台前端/设计解决方案</div>
</div>
<n-form
ref="formRef"
:model="formModel"
:rules="formRules"
label-placement="left"
size="large"
class="w-[100%]"
:show-require-mark="false"
:show-label="false"
>
<n-form-item path="username">
<n-input v-model:value="formModel.username" round placeholder="请输入用户名">
<template #prefix>
<Icon name="mdi:user-outline" />
</template>
</n-input>
</n-form-item>
<n-form-item path="password">
<n-input v-model:value="formModel.password" round placeholder="请输入密码">
<template #prefix>
<Icon name="material-symbols:lock-outline" />
</template>
</n-input>
</n-form-item>
<n-form-item>
<div class="flex justify-between w-[100%] px-[2px]">
<div class="flex-initial">
<n-checkbox v-model:checked="autoLogin">自动登录</n-checkbox>
</div>
<div class="flex-initial order-last">
<n-button text type="primary">忘记密码</n-button>
</div>
</div>
</n-form-item>
<n-form-item>
<n-button type="primary" size="large" round @click="handleSubmitForm" :loading="submitLoading" block>登录</n-button>
</n-form-item>
</n-form>
</div>
</div>
</div>
</template>
<script setup>
definePageMeta({
layout: "empty",
})
import LoginPic from "@/assets/images/login-pic.png";
const router = useRouter();
const message = useMessage();
const autoLogin = ref(false);
const submitLoading = ref(false);
const formRef = ref(null);
const formModel = reactive({
username: "",
password: "",
});
const formRules = {
username: { required: true, message: "请输入用户名", trigger: "blur" },
password: { required: true, message: "请输入密码", trigger: "blur" },
};
const gradient = {
deg: 92.06,
from: "#33c2ff 0%",
// to: `${appStore.appTheme} 100%`,
};
const handleSubmitForm = (e) => {
};
</script>
<style lang="less" scoped>
.login-container {
background: linear-gradient(-135deg, #d765cf, #495fd1);
min-height: 100%;
.login-box {
@apply shadow-md rounded-xl bg-white flex justify-between;
}
}
</style>

View File

@ -1,20 +1,18 @@
<script setup lang="ts">
import {useStyleStore} from "~/stores/style";
import {AdjustTimeToUTCOffset} from "~/utils/utc";
import {useTimeStore} from "~/stores/time";
import DnsInfo from "~/components/dns/InfoList.vue";
const {t} = useI18n()
const localePath = useLocalePath()
const route = useRoute();
const {domain} = route.params;
const domainData = domain.replace(/_/g, '.')
const domainData = typeof domain === "string" ? domain?.replace(/_/g, '.') : "";
const timeStore = useTimeStore()
const styleStore = useStyleStore()
const localePath = useLocalePath()
const settingsStore = useSettingsStore()
const dnsStore = useDnsStore()
styleStore.setIsPage(true)
const {data, pending, error, refresh} = await useAsyncData(
@ -22,8 +20,9 @@ const {data, pending, error, refresh} = await useAsyncData(
() => $fetch('/api/dns', {
method: 'POST',
body: {
domain: domainData,
dnsServer:timeStore.getDnsServer
domain: domainData,
dnsServer: dnsStore.getFirstNewDnsShown?.name,
flag: dnsStore.getHasShownItems
}
})
)
@ -40,15 +39,16 @@ if (!error.value && settingsStore.getHistory) {
)
}
useHead({
title: `${domainData} - ${t('dns.title')}`,
meta: [
{
name: 'description',
content: t('dns.description', { domain: domainData })
},{
content: t('dns.description', {domain: domainData})
}, {
name: 'keywords',
content: t('dns.keywords', { domain: domainData })
content: t('dns.keywords', {domain: domainData})
}
]
})
@ -58,40 +58,60 @@ useHead({
<div class="mt-5">
<div class="bg-white shadow-lg rounded-lg overflow-hidden">
<div class="p-6">
<div class="flex justify-between items-center mb-6">
<div class="flex justify-between items-center mb-6"
v-if="dnsStore.getHasShownItems"
>
<h2 class="text-2xl font-bold text-gray-800"> {{ t('dns.dnsResult') }}
<span class="text-gray-300 text-sm font-normal ml-2">{{ timeStore.getDnsServer }}</span>
<ClientOnly>
<span
class="text-gray-300 text-sm font-normal ml-2">
{{ dnsStore.getFirstNewDnsShown.iName }}
</span>
</ClientOnly>
</h2>
<ClientOnly>
<UTooltip :text="t('popper.dnsChange')" :popper="{ placement: 'top' }">
<DnsApiChanges />
</UTooltip>
<NTooltip
placement="top">
<template #trigger>
<DnsApiChanges
/>
</template>
{{ t('popper.dnsChange') }}
</NTooltip>
</ClientOnly>
</div>
<DnsDefaultList v-if="timeStore.getDnsServer == ''"
:data="data" />
<div
v-else
class=" "> <!-- 使用 min-h-screen 确保占满至少一个屏幕高度 -->
<div
class="p-8 ">
<!-- 增加内边距使用更大的最大宽度更大的圆角和阴影 -->
<h2 class="mb-6 text-xl font-bold text-gray-900 dark:text-white w-full">提示</h2> <!-- 增大标题文字和下边距 -->
<p class="text-center my-2 text-lg text-gray-700 dark:text-gray-400 w-full">当前没有可用的 DNS 服务器</p>
<!-- 增大正文文字尺寸并添加更多说明 -->
<p class="text-center my-2 text-lg text-gray-700 dark:text-gray-400 w-full">请检查您的Dns设置或稍后再试</p>
<!-- 增大正文文字尺寸并添加更多说明 -->
</div>
</div>
<DnsInfoList
v-if="timeStore.getDnsServer != 'cloudflare' && timeStore.getDnsServer != ''"
v-if="dnsStore.getHasShownItems"
:data="data"
/>
<DnsCloudflareList
v-if="timeStore.getDnsServer == 'cloudflare'"
:data="data"
/>
<!-- <DnsCloudflareList-->
<!-- v-if="timeStore.getDnsServer == 'cloudflare'"-->
<!-- :data="data"-->
<!-- />-->
</div>
</div>
</div>
<!-- 公告部分 -->
<CommonBulletin v-if="error" class="mt-5" >
<CommonBulletin v-if="error && apisStore.getHasShownItems" class="mt-5">
<template #text>
<Icon name="bx:error" size="16px" color="red" />
<Icon name="bx:error" size="16px" color="red"/>
{{ t('error.notFound') }}
</template>
</CommonBulletin>

View File

@ -0,0 +1,94 @@
<script setup lang="ts">
import {AdjustTimeToUTCOffset} from "~/utils/utc";
const {t} = useI18n()
const localePath = useLocalePath()
const timeStore = useTimeStore()
const styleStore = useStyleStore()
const settingsStore = useSettingsStore()
// api
const domainStore = useDomainStore()
styleStore.setIsPage(true)
const route = useRoute();
const {domain} = route.params;
const domainData = typeof domain === "string" ? domain?.replace(/_/g, '.') : "";
const {data: domainInfo, pending, error, refresh} = await useAsyncData(
'domain',
() => $fetch('/api/domain', {
method: 'POST',
body: {
domain: domainData,
domainServer: domainStore.getFirstNewDomainShown.name,
flag: domainStore.getHasDomainShown
}
})
)
if (!error.value && settingsStore.getHistory) {
styleStore.addOrUpdateHistory(
{
id: domainData,
type: 'domain',
domain: domainData,
path: localePath(`/domain/${domain}.html`),
date: AdjustTimeToUTCOffset(new Date().toString(), timeStore.timeZones)
}
)
}
</script>
<template>
<div class="mt-5 mx-auto mb-5">
<div
v-if="domainStore.getHasDomainShown"
class="bg-white dark:bg-gray-900 shadow rounded-lg overflow-hidden">
<div class="p-6 space-y-4">
<div
class="flex justify-between items-center">
<n-tag type="info" size="medium">域名信息</n-tag>
<n-tag v-if="domainStore.getHasDomainShown" type="success" size="medium">
Api来源{{ domainStore.getFirstNewDomainShown?.name }}
</n-tag>
</div>
<div class="grid grid-cols-2 gap-4 text-sm">
<p class="text-gray-800 dark:text-gray-200">域名: <span class="font-medium">{{ domainInfo.domain }}</span></p>
<p class="text-gray-800 dark:text-gray-200">货币: <span class="font-medium">{{
domainInfo.currency
}} ({{ domainInfo.currency_symbol }})</span></p>
<p class="text-gray-800 dark:text-gray-200">新注册价格: <span
class="font-medium">{{ domainInfo.currency_symbol }}{{ domainInfo.new }}</span>
</p>
<p class="text-gray-800 dark:text-gray-200">续费价格: <span class="font-medium">{{
domainInfo.currency_symbol
}}{{ domainInfo.renew }}</span></p>
<p v-if="domainInfo.premium" class="text-gray-800 dark:text-gray-200">溢价<span
class="font-medium">{{ domainInfo.premium ? '支持' : '不支持' }}</span></p>
<p v-else class="text-gray-800 dark:text-gray-200">溢价功能<span
class="font-medium">{{ domainInfo.premium ? '支持' : '不支持' }}</span></p>
</div>
</div>
</div>
<div
v-else
class="bg-white shadow-lg rounded-lg overflow-hidden"> <!-- 使用 min-h-screen 确保占满至少一个屏幕高度 -->
<div
class="p-8 ">
<!-- 增加内边距使用更大的最大宽度更大的圆角和阴影 -->
<h2 class="mb-6 text-xl font-bold text-gray-900 dark:text-white w-full">提示</h2> <!-- 增大标题文字和下边距 -->
<p class="text-center my-2 text-lg text-gray-700 dark:text-gray-400 w-full">当前没有可用的 Domain 服务器</p>
<!-- 增大正文文字尺寸并添加更多说明 -->
<p class="text-center my-2 text-lg text-gray-700 dark:text-gray-400 w-full">请检查您的Domain设置或稍后再试</p>
<!-- 增大正文文字尺寸并添加更多说明 -->
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,298 @@
<script setup lang="ts">
import draggable from "vuedraggable";
const styleStore = useStyleStore()
const settingsStore = useSettingsStore();
const whoisStore = useWhoisStore()
const dnsStore = useDnsStore()
const domainStore = useDomainStore()
styleStore.setIsPage(true)
const {
newsDnsArr,
} = storeToRefs(dnsStore);
const {
newsDomainArr,
} = storeToRefs(domainStore);
const {
newsWhoisArr,
} = storeToRefs(whoisStore);
const message = useMessage();
//
const restoreDefault = () => {
newsWhoisArr.value = newsWhoisArr.value.sort((a, b) => a.order - b.order);
message.success("恢复默认Whois榜单排序成功");
};
const restoreDnsDefault = () => {
newsDnsArr.value = newsDnsArr.value.sort((a, b) => a.order - b.order);
message.success("恢复Dns Api榜单排序成功");
};
const restoreDomainDefault = () => {
newsDomainArr.value = newsDomainArr.value.sort((a, b) => a.order - b.order);
message.success("恢复Dns Api榜单排序成功");
};
//
const saveSoreData = (name = null, open = false) => {
message.success(
name ? `${name}Whois榜单已${open ? "开启" : "关闭"}` : "Whois榜单排序成功"
);
whoisStore.checkNewsWhoisUpdate()
};
const saveDnsSoreData = (name = null, open = false) => {
message.success(
name ? `${name}DnsServer已${open ? "开启" : "关闭"}` : "DnsServer排序成功"
);
dnsStore.checkNewsDnsUpdate()
};
const saveSoreDomainData = (name = null, open = false) => {
message.success(
name ? `${name}DomainServer已${open ? "开启" : "关闭"}` : "DomainServer排序成功"
);
domainStore.checkNewsDomainUpdate()
};
</script>
<template>
<ClientOnly>
<div class="setting mt-5">
<n-card class="set-item">
<div class="top">
<div class="name">
<n-text class="text">Whois第三方API</n-text>
<n-text class="tip" depth="3">
拖拽以排序开关用以控制在页面中的显示状态
</n-text>
</div>
<n-popconfirm @positive-click="restoreDefault">
<template #trigger>
<n-button class="control" size="small"> 恢复默认</n-button>
</template>
确认将排序恢复到默认状态
</n-popconfirm>
</div>
<draggable
:list="newsWhoisArr"
:animation="200"
class="mews-group"
item-key="order"
@end="saveSoreData()"
>
<template #item="{ element }">
<n-card
class="item"
embedded
:content-style="{ display: 'flex', alignItems: 'center' }"
>
<div class="desc" :style="{ opacity: element.show ? null : 0.6 }">
<img class="logo" :src="`/logo/${element.name}.png`" alt="logo"/>
<n-text class="news-name" v-html="element.label"/>
</div>
<n-switch
class="switch"
:round="false"
:disabled="element.disabled"
v-model:value="element.show"
@update:value="saveSoreData(element.label, element.show)"
/>
</n-card>
</template>
</draggable>
</n-card>
<n-card class="set-item">
<div class="top">
<div class="name">
<n-text class="text">Dns第三方API</n-text>
<n-text class="tip" depth="3">
拖拽以排序开关用以控制在页面中的显示状态
</n-text>
</div>
<n-popconfirm
@positive-click="restoreDnsDefault"
negative-text="取消"
positive-text="确认"
>
<template #trigger>
<n-button class="control" size="small"> 恢复默认</n-button>
</template>
确认将Dns排序恢复到默认状态
</n-popconfirm>
</div>
<draggable
:list="newsDnsArr"
:animation="200"
class="mews-group"
item-key="order"
@end="saveDnsSoreData()"
>
<template #item="{ element }">
<n-card
class="item"
embedded
:content-style="{ display: 'flex', alignItems: 'center' }"
>
<div class="desc" :style="{ opacity: element.show ? null : 0.6 }">
<img class="logo" :src="`/logo/${element.name}.png`" alt="logo"/>
<n-text class="news-name" v-html="element.label"/>
</div>
<n-switch
class="switch"
:disabled="element.disabled"
:round="false"
v-model:value="element.show"
@update:value="saveDnsSoreData(element.label, element.show)"
/>
</n-card>
</template>
</draggable>
</n-card>
<n-card class="set-item">
<div class="top">
<div class="name">
<n-text class="text">域名第三方API</n-text>
<n-text class="tip" depth="3">
拖拽以排序开关用以控制在页面中的显示状态
</n-text>
</div>
<n-popconfirm @positive-click="restoreDomainDefault">
<template #trigger>
<n-button class="control" size="small"> 恢复默认</n-button>
</template>
确认将排序恢复到默认状态
</n-popconfirm>
</div>
<draggable
:list="newsDomainArr"
:animation="200"
class="mews-group"
item-key="order"
@end="saveSoreDomainData()"
>
<template #item="{ element }">
<n-card
class="item"
embedded
:content-style="{ display: 'flex', alignItems: 'center' }"
>
<div class="desc" :style="{ opacity: element.show ? null : 0.6 }">
<img class="logo" :src="`/logo/${element.name}.png`" alt="logo"/>
<n-text class="news-name" v-html="element.label"/>
</div>
<n-switch
class="switch"
:round="false"
:disabled="element.disabled"
v-model:value="element.show"
@update:value="saveSoreDomainData(element.label, element.show)"
/>
</n-card>
</template>
</draggable>
</n-card>
</div>
</ClientOnly>
</template>
<style lang="scss" scoped>
.setting {
.title {
margin-top: 30px;
margin-bottom: 20px;
font-size: 40px;
font-weight: bold;
}
.n-h {
padding-left: 16px;
font-size: 20px;
margin-left: 4px;
}
.set-item {
width: 100%;
border-radius: 8px;
margin-bottom: 12px;
.top {
display: flex;
align-items: center;
justify-content: space-between;
.name {
font-size: 18px;
display: flex;
flex-direction: column;
.tip {
font-size: 12px;
border-radius: 8px;
}
}
.set {
max-width: 200px;
}
}
.mews-group {
margin-top: 16px;
display: grid;
grid-template-columns: repeat(5, minmax(0px, 1fr));
gap: 24px;
@media (max-width: 1666px) {
grid-template-columns: repeat(4, minmax(0px, 1fr));
}
@media (max-width: 1200px) {
grid-template-columns: repeat(3, minmax(0px, 1fr));
}
@media (max-width: 890px) {
grid-template-columns: repeat(2, minmax(0px, 1fr));
}
@media (max-width: 620px) {
grid-template-columns: repeat(1, minmax(0px, 1fr));
}
.item {
cursor: pointer;
.desc {
display: flex;
align-items: center;
width: 100%;
transition: all 0.3s;
.logo {
width: 40px;
height: 40px;
margin-right: 12px;
}
.news-name {
font-size: 16px;
}
}
.switch {
margin-left: auto;
}
}
}
}
}
</style>

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import {useStyleStore} from "~/stores/style";
import {useSettingsStore} from "~/stores/settings";
import {useDomainStore} from "~/stores/domain";
import {useModal} from 'naive-ui';
const styleStore = useStyleStore()
const settingsStore = useSettingsStore()
@ -9,7 +9,7 @@ const {t} = useI18n()
const timeStore = useTimeStore()
styleStore.setIsPage(true)
const {isHistory} = storeToRefs(settingsStore)
const {isHistory, isBulletin, isDomainList} = storeToRefs(settingsStore)
const isOpen = ref(false)
const isEditDomainOpen = ref(false)
@ -27,136 +27,259 @@ const handleReset = async () => {
</script>
<template>
<div class="setting">
<div class="text-2xl font-bold mt-[30px] mb-[20px]">{{ t('settings.title') }}</div>
<UCard>
<div class="flex justify-between items-center">
<div class="text-base ">{{ t('settings.history') }}</div>
<div>
<UToggle v-model="isHistory" />
</div>
<div class="setting mt-5 settings-grid">
<n-h6 prefix="bar"> 基础设置</n-h6>
<n-card class="set-item">
<div class="top grid grid-cols-2 gap-4">
<div class="name">
<n-text class="text">{{ t('settings.title') }}</n-text>
<n-text class="tip" depth="3">{{ t('settings.history') }}</n-text>
</div>
</UCard>
</div>
<n-switch v-model:value="isHistory" :round="false"/>
<div class="setting">
<div class="text-2xl font-bold mt-[30px] mb-[20px]"> {{ t('settings.suffixSetting') }} </div>
<u-card class="set-item">
<div class="flex justify-between items-center">
<div class="text-base"> {{ t('settings.customSuffix') }} </div>
<div class="text-sm " >
{{ t('settings.suffixDesc') }}
<div class="name">
<n-text class="text">公告设置</n-text>
<n-text class="tip" depth="3">是否开启首页公告功能</n-text>
</div>
<div>
<u-button type="warning"
@click="isEditDomainOpen = true"
> {{ t('settings.manage') }} </u-button>
<n-switch v-model:value="isBulletin" :round="false"/>
<div class="name">
<n-text class="text">支持列表</n-text>
<n-text class="tip" depth="3">是否开启支持列表功能</n-text>
</div>
<n-switch v-model:value="isDomainList" :round="false"/>
</div>
</n-card>
<UModal
v-model="isEditDomainOpen"
>
<UCard
:ui="{
base: 'h-full flex flex-col',
rounded: '10',
divide: 'divide-y divide-gray-100 dark:divide-gray-800',
body: {
base: 'grow'
}
}"
<n-h6 prefix="bar"> 杂项设置</n-h6>
<n-card class="set-item">
<div class="top">
<div class="name">
<n-text class="text">重置所有数据</n-text>
<n-text class="tip" depth="3">
重置所有数据你的自定义设置都将会丢失
</n-text>
</div>
<n-popconfirm
@positive-click="handleReset"
:negative-text="t('settings.cancel')"
:positive-text="t('settings.confirm')"
>
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
{{ t('settings.suffixManage') }}
</h3>
<UButton
color="gray"
variant="ghost"
icon="i-heroicons-x-mark-20-solid"
class="-my-1"
type="button"
@click="isEditDomainOpen = false" />
</div>
<template #trigger>
<n-button type="warning"> {{ t('common.actions.reset') }}</n-button>
</template>
<div class="mx-auto">
<DomainEditor />
</div>
</UCard>
</UModal>
</u-card>
</div>
<div class="setting">
<div class="text-2xl font-bold mt-[30px] mb-[20px]">{{ t('settings.linkOpenType') }}</div>
<UCard>
<div class="flex justify-between items-center">
<div class="text-base "> {{ t('settings.linkOpenTypeDesc') }} </div>
<div>
<ClientOnly>
<CommonLinkChange />
</ClientOnly>
</div>
确认重置所有数据你的自定义设置都将会丢失
</n-popconfirm>
</div>
</UCard>
</n-card>
</div>
<!-- <div class="setting">-->
<!-- <div class="text-2xl font-bold mt-[30px] mb-[20px]"> {{ t('settings.suffixSetting') }} </div>-->
<!-- <u-card class="set-item">-->
<!-- <div class="flex justify-between items-center">-->
<!-- <div class="text-base"> {{ t('settings.customSuffix') }} </div>-->
<!-- <div class="text-sm " >-->
<!-- {{ t('settings.suffixDesc') }}-->
<!-- </div>-->
<!-- <div>-->
<!-- <u-button type="warning"-->
<!-- @click="isEditDomainOpen = true"-->
<!-- > {{ t('settings.manage') }} </u-button>-->
<!-- </div>-->
<!-- </div>-->
<div class="setting">
<div class="text-2xl font-bold mt-[30px] mb-[20px]"> {{ t('settings.miscellaneous') }} </div>
<u-card class="set-item">
<div class="flex justify-between items-center">
<div class="text-base">{{ t('settings.reset') }} </div>
<div class="text-sm " >
{{ t('settings.resetDesc') }}
</div>
<div>
<u-button type="warning"
@click="isOpen = true"
> {{ t('common.actions.reset') }} </u-button>
</div>
</div>
<!-- <UModal-->
<!-- v-model="isEditDomainOpen"-->
<!-- >-->
<!-- <UCard-->
<!-- :ui="{-->
<!-- base: 'h-full flex flex-col',-->
<!-- rounded: '10',-->
<!-- divide: 'divide-y divide-gray-100 dark:divide-gray-800',-->
<!-- body: {-->
<!-- base: 'grow'-->
<!-- }-->
<!-- }"-->
<!-- >-->
<!-- <template #header>-->
<!-- <div class="flex items-center justify-between">-->
<!-- <h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">-->
<!-- {{ t('settings.suffixManage') }}-->
<!-- </h3>-->
<!-- <UButton-->
<!-- color="gray"-->
<!-- variant="ghost"-->
<!-- icon="i-heroicons-x-mark-20-solid"-->
<!-- class="-my-1"-->
<!-- type="button"-->
<!-- @click="isEditDomainOpen = false" />-->
<!-- </div>-->
<!-- </template>-->
<!-- <div class="mx-auto">-->
<!-- <DomainEditor />-->
<!-- </div>-->
<!-- </UCard>-->
<!-- </UModal>-->
<!-- </u-card>-->
<!-- </div>-->
<UModal
v-model="isOpen"
>
<UCard
:ui="{
base: 'h-full flex flex-col',
rounded: '',
divide: 'divide-y divide-gray-100 dark:divide-gray-800',
body: {
base: 'grow'
}
}"
>
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
Modal
</h3>
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="isOpen = false" />
</div>
</template>
<!-- <div class="setting">-->
<!-- <div class="text-2xl font-bold mt-[30px] mb-[20px]">{{ t('settings.linkOpenType') }}</div>-->
<!-- <UCard>-->
<!-- <div class="flex justify-between items-center">-->
<!-- <div class="text-base "> {{ t('settings.linkOpenTypeDesc') }} </div>-->
<!-- <div>-->
<!-- <ClientOnly>-->
<!-- <CommonLinkChange />-->
<!-- </ClientOnly>-->
<!-- </div>-->
<!-- </div>-->
<!-- </UCard>-->
<!-- </div>-->
<div class="p-4 m-auto text-center">
{{ t('settings.resetConfirm') }}
</div>
<div class="flex justify-end">
<UButton
@click="handleReset"
class="my-1">
{{ t('common.actions.confirm') }}
</UButton>
</div>
</UCard>
</UModal>
</u-card>
</div>
<!-- <div class="setting">-->
<!-- <div class="text-2xl font-bold mt-[30px] mb-[20px]"> {{ t('settings.miscellaneous') }} </div>-->
<!-- <u-card class="set-item">-->
<!-- <div class="flex justify-between items-center">-->
<!-- <div class="text-base">{{ t('settings.reset') }} </div>-->
<!-- <div class="text-sm " >-->
<!-- {{ t('settings.resetDesc') }}-->
<!-- </div>-->
<!-- <div>-->
<!-- <u-button type="warning"-->
<!-- @click="isOpen = true"-->
<!-- > {{ t('common.actions.reset') }} </u-button>-->
<!-- </div>-->
<!-- </div>-->
<!-- <UModal-->
<!-- v-model="isOpen"-->
<!-- >-->
<!-- <UCard-->
<!-- :ui="{-->
<!-- base: 'h-full flex flex-col',-->
<!-- rounded: '',-->
<!-- divide: 'divide-y divide-gray-100 dark:divide-gray-800',-->
<!-- body: {-->
<!-- base: 'grow'-->
<!-- }-->
<!-- }"-->
<!-- >-->
<!-- <template #header>-->
<!-- <div class="flex items-center justify-between">-->
<!-- <h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">-->
<!-- Modal-->
<!-- </h3>-->
<!-- <UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="isOpen = false" />-->
<!-- </div>-->
<!-- </template>-->
<!-- <div class="p-4 m-auto text-center">-->
<!-- {{ t('settings.resetConfirm') }}-->
<!-- </div>-->
<!-- <div class="flex justify-end">-->
<!-- <UButton-->
<!-- @click="handleReset"-->
<!-- class="my-1">-->
<!-- {{ t('common.actions.confirm') }}-->
<!-- </UButton>-->
<!-- </div>-->
<!-- </UCard>-->
<!-- </UModal>-->
<!-- </u-card>-->
<!-- </div>-->
</template>
<style scoped>
.setting {
.title {
margin-top: 30px;
margin-bottom: 20px;
font-size: 40px;
font-weight: bold;
}
.n-h {
padding-left: 16px;
font-size: 20px;
margin-left: 4px;
}
.set-item {
width: 100%;
border-radius: 8px;
margin-bottom: 12px;
.top {
display: flex;
align-items: center;
justify-content: space-between;
.name {
font-size: 18px;
display: flex;
flex-direction: column;
.tip {
font-size: 12px;
border-radius: 8px;
}
}
.set {
max-width: 200px;
}
}
.mews-group {
margin-top: 16px;
display: grid;
grid-template-columns: repeat(5, minmax(0px, 1fr));
gap: 24px;
@media (max-width: 1666px) {
grid-template-columns: repeat(4, minmax(0px, 1fr));
}
@media (max-width: 1200px) {
grid-template-columns: repeat(3, minmax(0px, 1fr));
}
@media (max-width: 890px) {
grid-template-columns: repeat(2, minmax(0px, 1fr));
}
@media (max-width: 620px) {
grid-template-columns: repeat(1, minmax(0px, 1fr));
}
.item {
cursor: pointer;
.desc {
display: flex;
align-items: center;
width: 100%;
transition: all 0.3s;
.logo {
width: 40px;
height: 40px;
margin-right: 12px;
}
.news-name {
font-size: 16px;
}
}
.switch {
margin-left: auto;
}
}
}
}
}
</style>

View File

@ -1,8 +1,6 @@
<script setup lang="ts">
import {ParseWhois} from "~/utils/whoisToJson";
import {AdjustTimeToUTCOffset} from "~/utils/utc";
import {useTimeStore} from "~/stores/time";
import {useStyleStore} from "~/stores/style";
const route = useRoute();
const {domain} = route.params;
@ -133,7 +131,7 @@ useHead({
class="hover:bg-gray-100 text-gray-900 dark:hover:bg-gray-700 text-gray-200">
<th class="p-4 text-left font-semibold text-gray-900 dark:text-gray-200">{{ t('result.rawData') }}</th>
<td class="p-4 text-gray-900 dark:text-gray-200">
<UToggle color="sky" v-model="showRawData"/>
<NSwitch v-model:value="showRawData"/>
</td>
</tr>
</tbody>

1936
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

BIN
public/logo/aliyun.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 B

BIN
public/logo/cloudflare.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
public/logo/google.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
public/logo/iamwawa.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
public/logo/nuxt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 B

BIN
public/logo/tencent.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
public/logo/tianhu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 B

BIN
public/logo/whocx.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -1,11 +1,10 @@
import dns from 'node:dns/promises';
// 定义 DNS 服务器配置
const dnsServers:any = {
const dnsServers: any = {
google: 'https://dns.google/resolve',
cloudflare: 'http://1.1.1.1/dns-query',
aliyun: 'https://223.5.5.5/resolve',
tencent: 'https://doh.pub/dns-query',
nuxt: '/api/resolve',
};
interface Resp {
@ -23,57 +22,75 @@ interface soaRecord {
expire: number;
minttl: number;
}
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const domain = body.domain;
const flag = body.flag;
const dnsServerKey = body.dnsServer;
//判断是否开启DNS
if (!flag) {
return {
status: 200,
body: 'DNS is not open'
}
}
switch (dnsServerKey) {
case 'google':
return await $fetch(dnsServers.google, {
return await $fetch(dnsServers.google, {
params: {
name: domain,
type: 'A',
}
});
case 'tencent':
return await $fetch(dnsServers.tencent, {
return await $fetch(dnsServers.tencent, {
params: {
name: domain,
type: 'A',
}
});
case 'cloudflare':
const resp = await $fetch(dnsServers.cloudflare, {
const resp = await $fetch(dnsServers.cloudflare, {
method: 'GET',
params: {
name: domain,
},
},
headers: {
"Accept": "application/dns-json", // 设置期望的响应数据类型
}
}).then((resp:any) => {
}).then((resp: any) => {
return resp.text()
})
})
return JSON.parse(resp);
case 'aliyun':
return await $fetch(dnsServers.aliyun, {
return await $fetch(dnsServers.aliyun, {
params: {
name: domain,
type: '1',
}
});
case 'nuxt':
return await $fetch(dnsServers.nuxt, {
method: 'POST',
body: {
name: domain,
type: '1',
}
});
default:
const resolver = new dns.Resolver();
const aRecords = await resolver.resolve(domain, 'A');
const nsRecords = await resolver.resolve(domain, 'NS');
const soaRecord = await resolver.resolveSoa(domain);
return {
aRecords: aRecords,
nsRecords: nsRecords,
soaRecord: soaRecord,
} as Resp;
// const resolver = new dns.Resolver();
//
// const aRecords = await resolver.resolve(domain, 'A');
// const nsRecords = await resolver.resolve(domain, 'NS');
// const soaRecord = await resolver.resolveSoa(domain);
// return {
// aRecords: aRecords,
// nsRecords: nsRecords,
// soaRecord: soaRecord,
// } as Resp;
return null
}
});

55
server/api/domain.post.ts Normal file
View File

@ -0,0 +1,55 @@
// 定义 DNS 服务器配置
const doMainServers: any = {
whocx: 'https://who.cx/api/price',
};
interface DomainInfoResponse {
code: number;
currency: string;
currency_symbol: string;
domain: string;
new: string;
renew: string;
premium: boolean;
}
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const domain = body.domain;
const flag = body.flag;
const domainServerKey = body.domainServer;
//判断是否开启DNS
console.log(flag)
if (!flag) {
return {
status: 200,
data: {
status: 'success',
}
}
}
switch (domainServerKey) {
case 'whocx':
const res: any = await $fetch(doMainServers.whocx, {
method: "GET",
params: {
domain: domain,
}
});
return {
code: 200,
currency: res.currency,
currency_symbol: res.currency_symbol,
domain: res.domain,
new: res.new,
renew: res.renew,
premium: false,
} as DomainInfoResponse
default:
return null
}
});

View File

@ -0,0 +1,61 @@
// server/api/dns-query.ts
import {resolve4} from 'dns/promises';
interface DNSQueryResponse {
Status: number;
TC: boolean;
RD: boolean;
RA: boolean;
AD: boolean;
CD: boolean;
Question: {
name: string;
type: number;
};
Answer?: Array<{
name: string;
TTL: number;
type: number;
data: string;
}>;
message?: string;
}
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const queryName: string = body.name // 如果请求体中没有提供域名,默认查询 'baidu.com'
try {
const addresses: string[] = await resolve4(queryName);
const answers = addresses.map((address) => ({
name: `${queryName}.`,
TTL: 269, // 示例中的 TTL 值是硬编码的
type: 1, // 表示 A 记录
data: address,
}));
const response: DNSQueryResponse = {
Status: 0,
TC: false,
RD: true,
RA: true,
AD: false,
CD: false,
Question: {
name: `${queryName}.`,
type: 1,
},
Answer: answers,
};
return response;
} catch (error) {
console.error(error);
const response: DNSQueryResponse = {
AD: false, CD: false, Question: {name: "", type: 0}, RA: false, RD: false, TC: false,
Status: 2, // 使用 2 表示错误状态
message: 'DNS query failed'
};
return response;
}
});

View File

@ -5,44 +5,7 @@ export default defineEventHandler(async (event) => {
try {
const res = await whois(body.domain)
return res._raw
}catch (e) {
return `No match for "${body.domain}".
` +
`>>> Last update of whois database: ${new Date()} <<<
` +
'\n' +
'NOTICE: The expiration date displayed in this record is the date the\n' +
'registrar\'s sponsorship of the domain name registration in the registry is\n' +
'currently set to expire. This date does not necessarily reflect the expiration\n' +
'date of the domain name registrant\'s agreement with the sponsoring\n' +
'registrar. Users may consult the sponsoring registrar\'s Whois database to\n' +
'view the registrar\'s reported date of expiration for this registration.\n' +
'\n' +
'TERMS OF USE: You are not authorized to access or query our Whois\n' +
'database through the use of electronic processes that are high-volume and\n' +
'automated except as reasonably necessary to register domain names or\n' +
'modify existing registrations; the Data in VeriSign Global Registry\n' +
'Services\' ("VeriSign") Whois database is provided by VeriSign for\n' +
'information purposes only, and to assist persons in obtaining information\n' +
'about or related to a domain name registration record. VeriSign does not\n' +
'guarantee its accuracy. By submitting a Whois query, you agree to abide\n' +
'by the following terms of use: You agree that you may use this Data only\n' +
'for lawful purposes and that under no circumstances will you use this Data\n' +
'to: (1) allow, enable, or otherwise support the transmission of mass\n' +
'unsolicited, commercial advertising or solicitations via e-mail, telephone,\n' +
'or facsimile; or (2) enable high volume, automated, electronic processes\n' +
'that apply to VeriSign (or its computer systems). The compilation,\n' +
'repackaging, dissemination or other use of this Data is expressly\n' +
'prohibited without the prior written consent of VeriSign. You agree not to\n' +
'use electronic processes that are automated and high-volume to access or\n' +
'query the Whois database except as reasonably necessary to register\n' +
'domain names or modify existing registrations. VeriSign reserves the right\n' +
'to restrict your access to the Whois database in its sole discretion to ensure\n' +
'operational stability. VeriSign may restrict or terminate your access to the\n' +
'Whois database for failure to abide by these terms of use. VeriSign\n' +
'reserves the right to modify these terms at any time.\n' +
'\n' +
'The Registry database contains ONLY .COM, .NET, .EDU domains and\n' +
'Registrars.'
} catch (e) {
return e
}
})

54
settings/settings.ts Normal file
View File

@ -0,0 +1,54 @@
export const defaultLayout = 'normal'
export const naiveThemeOverrides = {
common: {
primaryColor: '#316C72FF',
primaryColorHover: '#316C72E3',
primaryColorPressed: '#2B4C59FF',
primaryColorSuppl: '#316C72E3',
infoColor: '#2080F0FF',
infoColorHover: '#4098FCFF',
infoColorPressed: '#1060C9FF',
infoColorSuppl: '#4098FCFF',
successColor: '#18A058FF',
successColorHover: '#36AD6AFF',
successColorPressed: '#0C7A43FF',
successColorSuppl: '#36AD6AFF',
warningColor: '#F0A020FF',
warningColorHover: '#FCB040FF',
warningColorPressed: '#C97C10FF',
warningColorSuppl: '#FCB040FF',
errorColor: '#D03050FF',
errorColorHover: '#DE576DFF',
errorColorPressed: '#AB1F3FFF',
errorColorSuppl: '#DE576DFF',
},
}
export const basePermissions = [
{
code: 'ExternalLink',
name: '外链',
type: 'MENU',
icon: 'i-fe:external-link',
order: 98,
enable: true,
show: true,
children: [
{
code: 'MyBlog',
name: '博客-掘金',
type: 'MENU',
path: 'https://juejin.cn/user/1961184475483255',
icon: 'i-simple-icons:juejin',
order: 1,
enable: true,
show: true,
},
],
},
]

27
stores/admin/app.ts Normal file
View File

@ -0,0 +1,27 @@
import { defineStore } from 'pinia'
import { defaultLayout, naiveThemeOverrides } from '@/settings/settings'
import { useDark } from '@vueuse/core'
export const useAppStore = defineStore('app', {
state: () => ({
collapsed: false,
// 直接在状态初始化时判断是否为暗模式
isDark: useDark(),
layout: defaultLayout,
naiveThemeOverrides,
}),
actions: {
switchCollapsed() {
this.collapsed = !this.collapsed
},
setCollapsed(b:any) {
this.collapsed = b
},
toggleDark() {
this.isDark = !this.isDark
},
setLayout(v:any) {
this.layout = v
},
},
persist: true,
})

73
stores/admin/tab.ts Normal file
View File

@ -0,0 +1,73 @@
import { defineStore } from 'pinia'
interface Tab {
icon: any
path: any
name: any
title: any
}
export const useTabStore = defineStore('tab', {
state: () => ({
tabs: [] as any,
activeTab: '',
reloading: false,
}),
actions: {
setActiveTab(path:string) {
this.activeTab = path
},
setTabs(tabs:any) {
this.tabs = tabs
},
addTab(tab: Tab) {
const findIndex = this.tabs.findIndex((item:any) => item.path === tab.path);
if (findIndex !== -1) {
// Replace the existing tab with the new one
this.tabs.splice(findIndex, 1, tab);
} else {
// Add the new tab
this.tabs.push(tab);
}
// Assuming `setActiveTab` is correctly typed to accept a string
this.setActiveTab(tab.path);
},
async removeTab(path:string) {
this.setTabs(this.tabs.filter((tab:any) => tab.path !== path))
if (path === this.activeTab) {
const router = useRouter();
await router.push(this.tabs[this.tabs.length - 1].path)
}
},
async removeOther(curPath: string) {
this.setTabs(this.tabs.filter((tab: any) => tab.path === curPath));
if (curPath !== this.activeTab) {
const router = useRouter();
await router.push(this.tabs[this.tabs.length - 1].path);
}
},
async removeLeft(curPath: string) {
const curIndex = this.tabs.findIndex((item: any) => item.path === curPath);
const filterTabs = this.tabs.filter((_: any, index: any) => index >= curIndex);
this.setTabs(filterTabs);
if (!filterTabs.find((item: any) => item.path === this.activeTab)) {
const router = useRouter();
await router.push(filterTabs[filterTabs.length - 1].path);
}
},
async removeRight(curPath: string) {
const curIndex = this.tabs.findIndex((item: any) => item.path === curPath);
const filterTabs = this.tabs.filter((_: any, index: any) => index <= curIndex);
this.setTabs(filterTabs);
if (!filterTabs.find((item: any) => item.path === this.activeTab)) {
const router = useRouter();
await router.push(filterTabs[filterTabs.length - 1].path);
}
},
async reloadTab(path: string) {
const findItem = this.tabs.find((item:any) => item.path === path);
if (!findItem) return;
await refreshNuxtData()
},
},
persist:true
})

245
stores/api.ts Normal file
View File

@ -0,0 +1,245 @@
import {defineStore} from 'pinia'
export const useApisStore = defineStore('apis', {
state: () => {
const {t} = useI18n()
return {
defaultWhoisArr: [
{
label: "本地接口",
name: "nuxt",
order: 0,
show: true,
disabled: false,
},
{
label: "WHO.CX",
name: "whocx",
order: 1,
show: true,
disabled: false,
},
{
label: "TIAN.HU",
name: "tianhu",
order: 2,
show: true,
disabled: false,
},
{
label: "蛙蛙工具",
name: "iamwawa",
order: 3,
show: true,
disabled: false,
},
],
defaultDnsArr: [
{
label: "本地接口",
name: "nuxt",
order: 0,
show: true,
disabled: false,
iName: "本地 DNS",
flag: 'material-symbols:dns-outline'
},
{
label: "Google",
name: "google",
order: 1,
show: true,
disabled: false,
iName: 'Google',
flag: 'flat-color-icons:google'
},
{
label: "AliYun",
name: "aliyun",
order: 2,
show: true,
disabled: false,
iName: 'AliYun',
flag: 'ant-design:aliyun-outlined'
},
{
label: "Tencent",
name: "tencent",
order: 3,
show: true,
disabled: false,
iName: 'Tencent',
flag: 'emojione:cloud'
},
{
label: "Cloudflare",
name: "cloudflare",
order: 4,
show: false,
disabled: true,
iName: 'CloudFlare',
flag: 'skill-icons:cloudflare-light'
}
],
defaultDomainArr: [
{
label: "本地接口",
name: "nuxt",
order: 0,
show: false,
disabled: true,
iName: "本地 DNS",
flag: 'material-symbols:dns-outline'
},
{
label: "WHO.CX",
name: "whocx",
order: 1,
show: true,
disabled: false,
},
],
newsWhoisArr: [] as any,
newsDnsArr: [] as any,
newsDomainArr: [] as any,
}
},
actions: {
newWhoisList() {
this.newsWhoisArr = this.defaultWhoisArr;
},
// 检查更新
checkNewsWhoisUpdate() {
const mainData = this.newsWhoisArr;
let updatedNum = 0;
if (!mainData) return false;
// console.log("列表尝试更新", this.defaultWhoisArr, this.newsWhoisArr);
// 执行比较并迁移
if (this.newsWhoisArr.length > 0) {
for (const newItem of this.defaultWhoisArr) {
const exists = this.newsWhoisArr.some(
(news: any) =>
newItem.label === news.label && newItem.name === news.name
);
if (!exists) {
// console.log("列表有更新:", newItem);
updatedNum++;
this.newsWhoisArr.push(newItem);
}
}
if (updatedNum) useMessage().success(`成功更新 ${updatedNum} 个Whois数据`);
} else {
// console.log("列表无内容,写入默认");
this.newsWhoisArr = this.defaultWhoisArr;
}
},
newDnsList() {
this.newsDnsArr = this.defaultDnsArr;
},
checkNewsDnsUpdate() {
const mainData = this.newsDnsArr;
let updatedNum = 0;
if (!mainData) return false;
// console.log("列表尝试更新", this.defaultWhoisArr, this.newsWhoisArr);
// 执行比较并迁移
if (this.newsDnsArr.length > 0) {
for (const newItem of this.defaultDnsArr) {
const exists = this.newsDnsArr.some(
(news: any) =>
newItem.label === news.label && newItem.name === news.name
);
if (!exists) {
// console.log("列表有更新:", newItem);
updatedNum++;
this.newsDnsArr.push(newItem);
}
}
if (updatedNum) useMessage().success(`成功更新 ${updatedNum} 个Dns数据`);
} else {
// console.log("列表无内容,写入默认");
this.newsDnsArr = this.defaultDnsArr;
}
},
newListAdd() {
this.newsDomainArr = this.defaultDomainArr;
},
checkNewsDomainUpdate() {
// this.newsDomainArr = this.defaultDomainArr;
const mainData = this.newsDomainArr;
let updatedNum = 0;
if (!mainData) return false;
// console.log("列表尝试更新", this.defaultWhoisArr, this.newsWhoisArr);
// 执行比较并迁移
if (this.newsDomainArr.length > 0) {
for (const newItem of this.defaultDomainArr) {
const exists = this.newsDomainArr.some(
(news: any) =>
newItem.label === news.label && newItem.name === news.name
);
if (!exists) {
// console.log("列表有更新:", newItem);
updatedNum++;
this.newsDomainArr.push(newItem);
}
}
if (updatedNum) useMessage().success(`成功更新 ${updatedNum} 个Domain数据`);
} else {
// console.log("列表无内容,写入默认");
this.newsDomainArr = this.defaultDomainArr;
}
},
moveToTop(name: string) {
// 找到对应元素的索引
const index = this.newsDnsArr.findIndex((item: any) => item.name === name);
if (index === -1) return; // 如果没找到,直接返回
// 获取该元素
const itemToMove = this.newsDnsArr.splice(index, 1)[0];
// 将该元素移动到数组的开头
this.newsDnsArr.unshift(itemToMove);
// 可选如果您想同时更新所有元素的order属性以反映新的顺序
this.newsDnsArr.forEach((item: any, idx: any) => {
item.order = idx;
});
},
moveDomainToTop(name: string) {
// 找到对应元素的索引
const index = this.newsDomainArr.findIndex((item: any) => item.name === name);
if (index === -1) return; // 如果没找到,直接返回
// 获取该元素
const itemToMove = this.newsDomainArr.splice(index, 1)[0];
// 将该元素移动到数组的开头
this.newsDomainArr.unshift(itemToMove);
// 可选如果您想同时更新所有元素的order属性以反映新的顺序
this.newsDomainArr.forEach((item: any, idx: any) => {
item.order = idx;
});
},
},
getters: {
// 获取所有的 Whois 服务器
getNewDnsArr: (state: any) => state.newsDnsArr,
// 获取第一个展示的 Dns 服务器
getFirstNewDnsShown: (state: any) => state.newsDnsArr.find((item: any) => item.show),
//判断是否有开启的 Dns 服务器
getHasShownItems(state: any) {
return state.newsDnsArr.some((item: any) => item.show);
},
// 获取第一个展示的 Domain 服务器
getFirstNewDomainShown: (state: any) => state.newsDomainArr.find((item: any) => item.show),
// 判断是否有开启的 Domain 服务器
getHasDomainShown(state: any) {
return state.newsDomainArr.some((item: any) => item.show);
},
},
persist: {
storage: persistedState.cookiesWithOptions({
sameSite: 'strict',
}),
},
})

116
stores/dnsData.ts Normal file
View File

@ -0,0 +1,116 @@
import {defineStore} from 'pinia'
export const useDnsStore = defineStore('useDnsStore', {
state: () => {
const {t} = useI18n()
return {
defaultDnsArr: [
{
label: "本地接口",
name: "nuxt",
order: 0,
show: true,
disabled: false,
iName: "本地 DNS",
flag: 'material-symbols:dns-outline'
},
{
label: "Google",
name: "google",
order: 1,
show: true,
disabled: false,
iName: 'Google',
flag: 'flat-color-icons:google'
},
{
label: "AliYun",
name: "aliyun",
order: 2,
show: true,
disabled: false,
iName: 'AliYun',
flag: 'ant-design:aliyun-outlined'
},
{
label: "Tencent",
name: "tencent",
order: 3,
show: true,
disabled: false,
iName: 'Tencent',
flag: 'emojione:cloud'
},
{
label: "Cloudflare",
name: "cloudflare",
order: 4,
show: false,
disabled: true,
iName: 'CloudFlare',
flag: 'skill-icons:cloudflare-light'
}
],
newsDnsArr: [] as any,
}
},
actions: {
newDnsList() {
this.newsDnsArr = this.defaultDnsArr;
},
checkNewsDnsUpdate() {
const mainData = this.newsDnsArr;
let updatedNum = 0;
if (!mainData) return false;
// console.log("列表尝试更新", this.defaultWhoisArr, this.newsWhoisArr);
// 执行比较并迁移
if (this.newsDnsArr.length > 0) {
for (const newItem of this.defaultDnsArr) {
const exists = this.newsDnsArr.some(
(news: any) =>
newItem.label === news.label && newItem.name === news.name
);
if (!exists) {
// console.log("列表有更新:", newItem);
updatedNum++;
this.newsDnsArr.push(newItem);
}
}
if (updatedNum) useMessage().success(`成功更新 ${updatedNum} 个Dns数据`);
} else {
// console.log("列表无内容,写入默认");
this.newsDnsArr = this.defaultDnsArr;
}
},
moveToTop(name: string) {
// 找到对应元素的索引
const index = this.newsDnsArr.findIndex((item: any) => item.name === name);
if (index === -1) return; // 如果没找到,直接返回
// 获取该元素
const itemToMove = this.newsDnsArr.splice(index, 1)[0];
// 将该元素移动到数组的开头
this.newsDnsArr.unshift(itemToMove);
// 可选如果您想同时更新所有元素的order属性以反映新的顺序
this.newsDnsArr.forEach((item: any, idx: any) => {
item.order = idx;
});
},
},
getters: {
getNewDnsArr: (state: any) => state.newsDnsArr,
// 获取第一个展示的 Dns 服务器
getFirstNewDnsShown: (state: any) => state.newsDnsArr.find((item: any) => item.show),
//判断是否有开启的 Dns 服务器
getHasShownItems(state: any) {
return state.newsDnsArr.some((item: any) => item.show);
},
},
persist: {
storage: persistedState.cookiesWithOptions({
sameSite: 'strict',
}),
},
})

73
stores/domainData.ts Normal file
View File

@ -0,0 +1,73 @@
import {defineStore} from 'pinia'
export const useDomainStore = defineStore('useDomainStore', {
state: () => {
const {t} = useI18n()
return {
defaultDomainArr: [
{
label: "本地接口",
name: "nuxt",
order: 0,
show: false,
disabled: true,
iName: "本地 DNS",
flag: 'material-symbols:dns-outline'
},
{
label: "WHO.CX",
name: "whocx",
order: 1,
show: true,
disabled: false,
},
],
newsDomainArr: [] as any,
}
},
actions: {
newDomainList() {
this.newsDomainArr = this.defaultDomainArr;
},
checkNewsDomainUpdate() {
// this.newsDomainArr = this.defaultDomainArr;
const mainData = this.newsDomainArr;
let updatedNum = 0;
if (!mainData) return false;
// console.log("列表尝试更新", this.defaultWhoisArr, this.newsWhoisArr);
// 执行比较并迁移
if (this.newsDomainArr.length > 0) {
for (const newItem of this.defaultDomainArr) {
const exists = this.newsDomainArr.some(
(news: any) =>
newItem.label === news.label && newItem.name === news.name
);
if (!exists) {
// console.log("列表有更新:", newItem);
updatedNum++;
this.newsDomainArr.push(newItem);
}
}
if (updatedNum) useMessage().success(`成功更新 ${updatedNum} 个Domain数据`);
} else {
// console.log("列表无内容,写入默认");
this.newsDomainArr = this.defaultDomainArr;
}
},
},
getters: {
// 获取第一个展示的 Domain 服务器
getFirstNewDomainShown: (state: any) => state.newsDomainArr.find((item: any) => item.show),
// 判断是否有开启的 Domain 服务器
getHasDomainShown(state: any) {
return state.newsDomainArr.some((item: any) => item.show);
},
},
persist: {
storage: persistedState.cookiesWithOptions({
sameSite: 'strict',
}),
},
})

View File

@ -1,4 +1,4 @@
import { defineStore } from 'pinia'
import {defineStore} from 'pinia'
export const useSettingsStore = defineStore('settings', {
@ -6,19 +6,29 @@ export const useSettingsStore = defineStore('settings', {
const {t} = useI18n()
return {
isHistory: true,
isBulletin: true,
isDomainList: true,
linkOpenType: 'currentWindow',
selectedOption: 'whois',
domainSearch: '',
}
},
actions: {
setHistory(value: boolean) {
this.isHistory = value
},
setLinkOpenType(value: string ) {
setLinkOpenType(value: string) {
this.linkOpenType = value
},
setSelectedOption(name: string) {
this.selectedOption = name;
}
},
getters: {
getHistory: (state) => state.isHistory,
getHistory: (state: any) => state.isHistory,
getDomain(state: any) {
return state.domainSearch;
}
},
persist: {
storage: persistedState.cookiesWithOptions({

80
stores/whoisData.ts Normal file
View File

@ -0,0 +1,80 @@
import {defineStore} from 'pinia'
export const useWhoisStore = defineStore('useWhoisStore', {
state: () => {
const {t} = useI18n()
return {
defaultWhoisArr: [
{
label: "本地接口",
name: "nuxt",
order: 0,
show: true,
disabled: false,
},
{
label: "WHO.CX",
name: "whocx",
order: 1,
show: true,
disabled: false,
},
{
label: "TIAN.HU",
name: "tianhu",
order: 2,
show: true,
disabled: false,
},
{
label: "蛙蛙工具",
name: "iamwawa",
order: 3,
show: true,
disabled: false,
},
],
newsWhoisArr: [] as any,
}
},
actions: {
newWhoisList() {
this.newsWhoisArr = this.defaultWhoisArr;
},
// 检查更新
checkNewsWhoisUpdate() {
const mainData = this.newsWhoisArr;
let updatedNum = 0;
if (!mainData) return false;
// console.log("列表尝试更新", this.defaultWhoisArr, this.newsWhoisArr);
// 执行比较并迁移
if (this.newsWhoisArr.length > 0) {
for (const newItem of this.defaultWhoisArr) {
const exists = this.newsWhoisArr.some(
(news: any) =>
newItem.label === news.label && newItem.name === news.name
);
if (!exists) {
// console.log("列表有更新:", newItem);
updatedNum++;
this.newsWhoisArr.push(newItem);
}
}
if (updatedNum) useMessage().success(`成功更新 ${updatedNum} 个Whois数据`);
} else {
// console.log("列表无内容,写入默认");
this.newsWhoisArr = this.defaultWhoisArr;
}
},
},
getters: {
// 获取所有的 Whois 服务器
getNewDnsArr: (state: any) => state.newsDnsArr,
},
persist: {
storage: persistedState.cookiesWithOptions({
sameSite: 'strict',
}),
},
})

View File

@ -27,4 +27,5 @@ export default {
corePlugins: {
preflight: true,
},
darkMode: 'class',
}