🚩 增加 Dns 切换功能

This commit is contained in:
7836246 2024-03-03 13:37:23 +08:00
parent 999d8b7076
commit 9a64049c60
10 changed files with 341 additions and 82 deletions

View File

@ -0,0 +1,65 @@
<script lang="ts" setup>
import {useTimeStore} from "~/stores/time";
const switchLocalePath = useSwitchLocalePath()
const timeStore = useTimeStore()
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 handlePost = async (iso: string) => {
timeStore.setDnsServer(iso)
//
await refreshNuxtData('dns')
}
</script>
<template>
<div>
<HeadlessListbox
v-model="timeStore.getDnsServer"
as="div"
class="relative flex items-center"
>
<HeadlessListboxLabel class="sr-only">
Change Language
</HeadlessListboxLabel>
<HeadlessListboxButton type="button" title="Change Language">
<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" />
</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"
: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,
}"
>
<span class="truncate">
{{ lang.name }}
</span>
<span class="flex items-center justify-center text-sm">
<Icon :name="lang.flag" class="text-base" size="20" />
</span>
</div>
</HeadlessListboxOptions>
</HeadlessListbox>
</div>
</template>

View File

@ -0,0 +1,45 @@
<script setup lang="ts">
const props = defineProps({
data:{
type: Object,
required: true
}
});
const {t} = useI18n()
</script>
<template>
<div class="flex bg-gray-100 p-8">
<div class="w-full mx-auto">
<div class="bg-white shadow-lg rounded-lg p-6 mb-4" v-for="item in data.Answer">
<h2 class="text-xl font-bold">{{ item.name }}</h2>
<div class="grid grid-cols-2 gap-4">
<div>
<p class="font-semibold">Type</p>
<p>{{ item.type }}</p>
</div>
<div>
<p class="font-semibold">TTL</p>
<p>{{ item.TTL }}</p>
</div>
<div class="col-span-2">
<p class="font-semibold">Data</p>
<p>{{ item.data }}</p>
</div>
</div>
<div class="flex space-x-2 mt-4">
<span class="bg-green-200 text-green-800 px-2 py-1 rounded">RD: {{ data.RD }}</span>
<span class="bg-green-200 text-green-800 px-2 py-1 rounded">RA: {{ data.RA }}</span>
<span class="bg-red-200 text-red-800 px-2 py-1 rounded">TC: {{ data.TC }}</span>
<span class="bg-red-200 text-red-800 px-2 py-1 rounded">AD: {{ data.AD }}</span>
<span class="bg-red-200 text-red-800 px-2 py-1 rounded">CD: {{ data.CD }}</span>
</div>
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,61 @@
<script setup lang="ts">
defineProps({
data: {
type: Object,
required: true
}
})
const {t} = useI18n()
</script>
<template>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div v-if="data.aRecords">
<h3 class="font-semibold text-lg text-blue-600 mb-2">{{ t('dns.aRecord') }}</h3>
<div class="border rounded-lg p-4 bg-blue-50">
<ul
class="list-none space-y-2">
<li v-for="(record, index) in data.aRecords" :key="'a-record-' + index" class="flex justify-between items-center">
<span class="font-medium text-gray-700">IP:</span>
<span class="font-normal text-gray-600">{{ record }}</span>
</li>
</ul>
</div>
</div>
<div v-if="data.nsRecords">
<h3 class="font-semibold text-lg text-green-600 mb-2">{{ t('dns.nsRecord') }}</h3>
<div class="border rounded-lg p-4 bg-green-50">
<ul
class="list-none space-y-2">
<li v-for="(record, index) in data.nsRecords" :key="'ns-record-' + index" class="flex justify-between items-center">
<span class="font-normal text-gray-600">{{ record }}</span>
</li>
</ul>
</div>
</div>
<div
v-if="data.soaRecord"
class="md:col-span-2">
<h3 class="font-semibold text-lg text-purple-600 mb-2">{{ t('dns.soaRecord') }}</h3>
<div class="border rounded-lg p-4 bg-purple-50">
<ul class="list-none space-y-2">
<li
><span class="font-medium text-gray-700">nsname:</span> <span class="font-normal text-gray-600">{{ data.soaRecord.nsname }}</span></li>
<li><span class="font-medium text-gray-700">hostmaster:</span> <span class="font-normal text-gray-600">{{ data.soaRecord.hostmaster }}</span></li>
<li><span class="font-medium text-gray-700">serial:</span> <span class="font-normal text-gray-600">{{ data.soaRecord.serial }}</span></li>
<li><span class="font-medium text-gray-700">refresh:</span> <span class="font-normal text-gray-600">{{ data.soaRecord.refresh }}</span></li>
<li><span class="font-medium text-gray-700">retry:</span> <span class="font-normal text-gray-600">{{ data.soaRecord.retry }}</span></li>
<li><span class="font-medium text-gray-700">expire TTL:</span> <span class="font-normal text-gray-600">{{ data.soaRecord.expire }}</span></li>
<li><span class="font-medium text-gray-700">minttl TTL:</span> <span class="font-normal text-gray-600">{{ data.soaRecord.minttl }}</span></li>
</ul>
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,43 @@
<script setup lang="ts">
defineProps({
data: {
type: Object,
required: true
}
})
</script>
<template>
<div class="flex bg-gray-100 p-8">
<div class="w-full mx-auto">
<div class="bg-white shadow-lg rounded-lg p-6 mb-4" v-for="item in data.Answer">
<h2 class="text-xl font-bold">{{ item.name }}</h2>
<div class="grid grid-cols-2 gap-4">
<div>
<p class="font-semibold">Type</p>
<p>{{ item.type }}</p>
</div>
<div>
<p class="font-semibold">TTL</p>
<p>{{ item.TTL }}</p>
</div>
<div class="col-span-2">
<p class="font-semibold">Data</p>
<p>{{ item.data }}</p>
</div>
</div>
<div class="flex space-x-2 mt-4">
<span class="bg-green-200 text-green-800 px-2 py-1 rounded">RD: {{ data.RD }}</span>
<span class="bg-green-200 text-green-800 px-2 py-1 rounded">RA: {{ data.RA }}</span>
<span class="bg-red-200 text-red-800 px-2 py-1 rounded">TC: {{ data.TC }}</span>
<span class="bg-red-200 text-red-800 px-2 py-1 rounded">AD: {{ data.AD }}</span>
<span class="bg-red-200 text-red-800 px-2 py-1 rounded">CD: {{ data.CD }}</span>
</div>
</div>
</div>
</div>
</template>
<style scoped>
</style>

View File

@ -14,7 +14,6 @@
"@nuxtjs/tailwindcss": "^6.11.4", "@nuxtjs/tailwindcss": "^6.11.4",
"@pinia/nuxt": "^0.5.1", "@pinia/nuxt": "^0.5.1",
"nuxt": "^3.10.3", "nuxt": "^3.10.3",
"socks": "^2.8.1",
"vue": "^3.4.21", "vue": "^3.4.21",
"vue-router": "^4.3.0" "vue-router": "^4.3.0"
}, },

View File

@ -2,6 +2,7 @@
import {useStyleStore} from "~/stores/style"; import {useStyleStore} from "~/stores/style";
import {AdjustTimeToUTCOffset} from "~/utils/utc"; import {AdjustTimeToUTCOffset} from "~/utils/utc";
import {useTimeStore} from "~/stores/time"; import {useTimeStore} from "~/stores/time";
import DnsInfo from "~/components/dns/InfoList.vue";
const {t} = useI18n() const {t} = useI18n()
@ -9,21 +10,24 @@ const route = useRoute();
const {domain} = route.params; const {domain} = route.params;
const domainData = domain.replace(/_/g, '.') const domainData = domain.replace(/_/g, '.')
const timeStore = useTimeStore()
const styleStore = useStyleStore()
const localePath = useLocalePath()
styleStore.setIsPage(true)
const {data, pending, error, refresh} = await useAsyncData( const {data, pending, error, refresh} = await useAsyncData(
'dns', 'dns',
() => $fetch('/api/dns', { () => $fetch('/api/dns', {
method: 'POST', method: 'POST',
body: JSON.stringify({domain: 'xukangr.com'}) body: {
domain: domainData,
dnsServer:timeStore.getDnsServer
}
}) })
) )
const timeStore = useTimeStore()
const styleStore = useStyleStore()
styleStore.setIsPage(true)
const localePath = useLocalePath()
if (!error.value) { if (!error.value) {
styleStore.addOrUpdateHistory( styleStore.addOrUpdateHistory(
{ {
@ -54,50 +58,30 @@ useHead({
<div class="mt-5"> <div class="mt-5">
<div class="bg-white shadow-lg rounded-lg overflow-hidden"> <div class="bg-white shadow-lg rounded-lg overflow-hidden">
<div class="p-6"> <div class="p-6">
<h2 class="text-2xl font-bold text-gray-800 mb-6">{{ t('dns.dnsResult') }}</h2> <div class="flex justify-between items-center mb-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <h2 class="text-2xl font-bold text-gray-800"> {{ t('dns.dnsResult') }}
<div> <span class="text-gray-300 text-sm font-normal ml-2">{{ timeStore.getDnsServer }}</span>
<h3 class="font-semibold text-lg text-blue-600 mb-2">{{ t('dns.aRecord') }}</h3> </h2>
<div class="border rounded-lg p-4 bg-blue-50"> <ClientOnly>
<ul <DnsApiChanges />
v-if="data.A.length > 0" </ClientOnly>
class="list-none space-y-2">
<li v-for="(record, index) in data.A" :key="'a-record-' + index" class="flex justify-between items-center">
<span class="font-medium text-gray-700">IP:</span>
<span class="font-normal text-gray-600">{{ record.Record }}</span>
<span class="text-sm text-gray-500">TTL: {{ record.TTL }}</span>
</li>
</ul>
</div>
</div>
<div>
<h3 class="font-semibold text-lg text-green-600 mb-2">{{ t('dns.nsRecord') }}</h3>
<div class="border rounded-lg p-4 bg-green-50">
<ul
v-if="data.NS.length > 0"
class="list-none space-y-2">
<li v-for="(record, index) in data.NS" :key="'ns-record-' + index" class="flex justify-between items-center">
<span class="font-normal text-gray-600">{{ record.Record }}</span>
<span class="text-sm text-gray-500">TTL: {{ record.TTL }}</span>
</li>
</ul>
</div>
</div>
<div class="md:col-span-2">
<h3 class="font-semibold text-lg text-purple-600 mb-2">{{ t('dns.soaRecord') }}</h3>
<div class="border rounded-lg p-4 bg-purple-50">
<ul class="list-none space-y-2">
<li><span class="font-medium text-gray-700">MName:</span> <span class="font-normal text-gray-600">{{ data.SOA.MName }}</span></li>
<li><span class="font-medium text-gray-700">Email:</span> <span class="font-normal text-gray-600">{{ data.SOA.Email }}</span></li>
<li><span class="font-medium text-gray-700">Serial:</span> <span class="font-normal text-gray-600">{{ data.SOA.Serial }}</span></li>
<li><span class="font-medium text-gray-700">Refresh:</span> <span class="font-normal text-gray-600">{{ data.SOA.Refresh }}</span></li>
<li><span class="font-medium text-gray-700">Retry:</span> <span class="font-normal text-gray-600">{{ data.SOA.Retry }}</span></li>
<li><span class="font-medium text-gray-700">Expire:</span> <span class="font-normal text-gray-600">{{ data.SOA.Expire }}</span></li>
<li><span class="font-medium text-gray-700">Minimum TTL:</span> <span class="font-normal text-gray-600">{{ data.SOA.MinimumTTL }}</span></li>
</ul>
</div>
</div>
</div> </div>
<DnsDefaultList v-if="timeStore.getDnsServer == ''"
:data="data" />
<DnsInfoList
v-if="timeStore.getDnsServer != 'cloudflare' && timeStore.getDnsServer != ''"
:data="data"
/>
<DnsCloudflareList
v-if="timeStore.getDnsServer == 'cloudflare'"
:data="data"
/>
</div> </div>
</div> </div>
</div> </div>

View File

@ -18,7 +18,7 @@ const styleStore = useStyleStore()
const localePath = useLocalePath() const localePath = useLocalePath()
const {data, pending, error, refresh} = await useAsyncData( const {data, pending, error, refresh} = await useAsyncData(
'mountains', 'whois',
() => $fetch('/api/whois', { () => $fetch('/api/whois', {
method: 'POST', method: 'POST',
body: JSON.stringify({domain: domainData}) body: JSON.stringify({domain: domainData})

3
pnpm-lock.yaml generated
View File

@ -17,9 +17,6 @@ dependencies:
nuxt: nuxt:
specifier: ^3.10.3 specifier: ^3.10.3
version: 3.10.3(rollup@4.12.0)(typescript@5.3.3)(vite@5.1.4) version: 3.10.3(rollup@4.12.0)(typescript@5.3.3)(vite@5.1.4)
socks:
specifier: ^2.8.1
version: 2.8.1
vue: vue:
specifier: ^3.4.21 specifier: ^3.4.21
version: 3.4.21(typescript@5.3.3) version: 3.4.21(typescript@5.3.3)

View File

@ -1,31 +1,79 @@
import dns from 'node:dns/promises'; import dns from 'node:dns/promises';
// 定义 DNS 服务器配置
const dnsServers:any = {
google: 'https://dns.google/resolve',
cloudflare: 'http://cloudflare-dns.com/dns-query',
aliyun: 'https://223.5.5.5/resolve',
tencent: 'https://doh.pub/dns-query',
};
interface Resp {
aRecords: string[];
nsRecords: string[];
soaRecord: soaRecord;
}
interface soaRecord {
nsname: string;
hostmaster: string;
serial: number;
refresh: number;
retry: number;
expire: number;
minttl: number;
}
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const body = await readBody(event) const body = await readBody(event);
const domain = body.domain; const domain = body.domain;
if (!domain) {
return { error: 'Missing domain' };
}
try {
const aRecords = await dns.resolve(domain, 'A');
const nsRecords = await dns.resolve(domain, 'NS');
const soaRecord = await dns.resolveSoa(domain);
return { const dnsServerKey = body.dnsServer;
A: aRecords.map(ip => ({ TTL: '600', Record: ip })), // 示例中的TTL是假设的
NS: nsRecords.map(ns => ({ TTL: '21600', Record: ns })), // 示例中的TTL是假设的 switch (dnsServerKey) {
SOA: { case 'google':
MName: soaRecord.nsname, return await $fetch(dnsServers.google, {
Email: soaRecord.hostmaster, params: {
Serial: soaRecord.serial, name: domain,
Refresh: soaRecord.refresh, type: 'A',
Retry: soaRecord.retry, }
Expire: soaRecord.expire, });
MinimumTTL: soaRecord.minttl, case 'tencent':
} return await $fetch(dnsServers.tencent, {
}; params: {
} catch (error) { name: domain,
console.error(`Error fetching DNS records for ${domain}:`, error); type: 'A',
return { error: 'Failed to fetch DNS records' }; }
});
case 'cloudflare':
const resp = await $fetch('http://1.1.1.1/dns-query', {
method: 'GET',
params: {
name: domain,
},
headers: {
"Accept": "application/dns-json", // 设置期望的响应数据类型
}
}).then((resp:any) => {
return resp.text()
})
return JSON.parse(resp);
case 'aliyun':
return await $fetch(dnsServers.aliyun, {
params: {
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;
} }
}) });

View File

@ -4,8 +4,25 @@ export const useTimeStore = defineStore('timeZones', {
state: () => { state: () => {
return { return {
timeZones: 'UTC+8', timeZones: 'UTC+8',
dnsServer: '',
} }
}, },
actions: {
setTimeZones(timeZones: string) {
this.timeZones = timeZones
},
setDnsServer(dnsServer: string) {
this.dnsServer = dnsServer
},
},
getters: {
getTimeZones(state) {
return state.timeZones
},
getDnsServer(state) {
return state.dnsServer
},
},
persist: { persist: {
storage: persistedState.cookiesWithOptions({ storage: persistedState.cookiesWithOptions({
sameSite: 'strict', sameSite: 'strict',