diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..0a2a58db9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,23 @@ +.git +.github +.VSCodeCounter +.claude +.vscode + +node_modules +dist +logs +localtest +package-lock.json + +.DS_Store +.env +*.log +npm-debug.log* +yarn-debug.log* +pnpm-debug.log* + +common/maxmind-db/.maxmind-update-*.json +common/maxmind-db/.maxmind-update.lock +common/maxmind-db/*.bak +common/maxmind-db/*.next diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..93b4a7d10 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +name: CI + +on: + push: + branches: [main, dev] + pull_request: + branches: [main, dev] + +# Least-privilege GITHUB_TOKEN: tests + build only need to read the repo. +permissions: + contents: read + +jobs: + check: + name: Test + Build + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version: 24 + # Intentionally no `cache: npm` — that requires a package-lock.json, and this + # repo gitignores the lockfile by design. `npm install` below runs cold every + # run; dependency install on this project takes ~30s, which is acceptable + # versus the drift risk of committing a lockfile we don't otherwise use. + + - name: Install dependencies + run: npm install --no-audit --no-fund + + - name: Run tests + run: npm test + + - name: Build + run: npm run build diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 88d53e70e..2f4e83c8d 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -9,6 +9,15 @@ on: description: 'Version tag' required: true +# Least-privilege GITHUB_TOKEN: +# - contents: read → checkout the source +# - packages: write → push the built image to ghcr.io/${owner}/myip +# Docker Hub push uses DOCKER_HUB_ACCESS_TOKEN (a repo secret), not GITHUB_TOKEN, +# so no extra scopes are needed for that half of the job. +permissions: + contents: read + packages: write + jobs: build-and-push: runs-on: ubuntu-latest @@ -16,19 +25,19 @@ jobs: steps: - name: Check Out Repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Login to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Login to GitHub Packages - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -47,7 +56,7 @@ jobs: echo "RELEASE_NOTES=Manually triggered build" >> $GITHUB_ENV - name: Build and Push Docker Image - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v7 with: push: true tags: | diff --git a/.github/workflows/dockerhub-readme.yml b/.github/workflows/dockerhub-readme.yml new file mode 100644 index 000000000..3e5a06646 --- /dev/null +++ b/.github/workflows/dockerhub-readme.yml @@ -0,0 +1,37 @@ +name: Sync README to Docker Hub + +# Keeps the Docker Hub overview page in sync with README.md in this repo. +# Triggers on any push to main that touches README.md (or this workflow file +# itself), so documentation edits propagate without waiting for the next +# release or a manual step. Also exposes `workflow_dispatch` for a manual +# one-shot push (useful the first time this workflow is enabled, or to force +# a resync after rotating the Docker Hub token). + +on: + push: + branches: [main] + paths: + - 'README.md' + - '.github/workflows/dockerhub-readme.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + sync: + name: Push README to Docker Hub + runs-on: ubuntu-latest + environment: production + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Update Docker Hub description + uses: peter-evans/dockerhub-description@v5 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + repository: jason5ng32/myip + readme-filepath: ./README.md diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml index eed18fff7..38871af21 100644 --- a/.github/workflows/sync.yml +++ b/.github/workflows/sync.yml @@ -5,7 +5,7 @@ permissions: on: schedule: - - cron: "0 0 * * *" # 每天 UTC 时间 0 点 + - cron: "0 0 * * *" # every day at 00:00 UTC workflow_dispatch: jobs: @@ -15,11 +15,11 @@ jobs: if: ${{ github.event.repository.fork }} steps: - # 步骤 1: 运行标准的签出动作 + # step 1: checkout target repo - name: Checkout target repo - uses: actions/checkout@v3 + uses: actions/checkout@v6 - # 步骤 2: 运行同步动作 + # step 2: sync upstream changes - name: Sync upstream changes id: sync uses: aormsby/Fork-Sync-With-Upstream-action@v3.4 @@ -27,15 +27,14 @@ jobs: upstream_sync_repo: jason5ng32/MyIP upstream_sync_branch: main target_sync_branch: main - target_repo_token: ${{ secrets.GITHUB_TOKEN }} # 自动生成,无需设置 + target_repo_token: ${{ secrets.GITHUB_TOKEN }} # auto-generated, no need to set - # 将 test_mode 设置为 true 来运行测试而不是真正的动作! + # set test_mode to true to run tests instead of the real action! test_mode: false - # 出错检查 + # error check - name: Sync check if: failure() run: | - echo "[Error] 由于上游仓库的 workflow 文件变更,导致 GitHub 自动暂停了本次自动更新,你需要手动 Sync Fork 一次。" - echo "[Error] Due to a change in the workflow file of the upstream repository, GitHub has automatically suspended the scheduled automatic update. You need to manually sync your fork." + echo "[Error] Due to a change in the workflow file of the upstream repository, GitHub has automatically suspended the scheduled automatic update. You need to manually sync your fork again." exit 1 diff --git a/.gitignore b/.gitignore index ef13726e0..db2a47a90 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* +.claude node_modules dist @@ -34,3 +35,5 @@ common/maxmind-db/.maxmind-update-*.json common/maxmind-db/.maxmind-update.lock common/maxmind-db/*.bak common/maxmind-db/*.next +common/maxmind-db/*.mmdb +.learnings/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..3ffa8c117 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,111 @@ +# AGENTS.md + +Single source of truth for anyone — human or AI — contributing to MyIP. + +For area-specific details, see: + +- Frontend (Vue 3 SPA): @frontend/AGENTS.md +- Backend API (Express): @api/AGENTS.md + +## Overview + +**MyIP** (IPCheck.ing) is an open-source IP toolbox: IP lookup, connectivity tests, WebRTC / DNS-leak detection, speed test, MTR, Whois, security checklist, browser fingerprint, anonymity checks, and more. + +Single repo, two halves: a Vue 3 SPA front-end and an Express 5 back-end API, served side by side. + +## Stack + +| Layer | Technology | +|---|---| +| Framework | Vue 3 (` diff --git a/frontend/components/Achievements.vue b/frontend/components/Achievements.vue index 62001e6d2..1f22e47e9 100644 --- a/frontend/components/Achievements.vue +++ b/frontend/components/Achievements.vue @@ -1,244 +1,183 @@ diff --git a/frontend/components/Additional.vue b/frontend/components/Additional.vue index d7ccfa318..50b0c9770 100644 --- a/frontend/components/Additional.vue +++ b/frontend/components/Additional.vue @@ -1,128 +1,114 @@ \ No newline at end of file + diff --git a/frontend/components/Advanced.vue b/frontend/components/Advanced.vue index 73df4edfa..5e3f09afa 100644 --- a/frontend/components/Advanced.vue +++ b/frontend/components/Advanced.vue @@ -1,66 +1,82 @@ \ No newline at end of file + diff --git a/frontend/components/ConnectivityTest.vue b/frontend/components/ConnectivityTest.vue index 53fa53919..e8213c31d 100644 --- a/frontend/components/ConnectivityTest.vue +++ b/frontend/components/ConnectivityTest.vue @@ -1,48 +1,56 @@ - diff --git a/frontend/components/DnsLeaksTest.vue b/frontend/components/DnsLeaksTest.vue index 9a2cb659f..c205f4c42 100644 --- a/frontend/components/DnsLeaksTest.vue +++ b/frontend/components/DnsLeaksTest.vue @@ -1,60 +1,90 @@ @@ -64,16 +94,45 @@ import { useMainStore } from '@/store'; import { useI18n } from 'vue-i18n'; import { trackEvent } from '@/utils/use-analytics'; import countryLookup from 'country-code-lookup'; -import getCountryName from '@/utils/country-name.js'; +import { JnTooltip } from '@/components/ui/tooltip'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import getCountryName from '@/data/country-name.js'; +import { useStatusTone } from '@/composables/use-status-tone.js'; +import { EthernetPort, ChevronRight, HeartPulse, MapPin, RotateCw } from 'lucide-vue-next'; +import { Icon } from '@iconify/vue'; const { t } = useI18n(); - const store = useMainStore(); -const isDarkMode = computed(() => store.isDarkMode); -const isMobile = computed(() => store.isMobile); const lang = computed(() => store.lang); +const isStarted = ref(false); + +const { dotClass, textClass } = useStatusTone(); + +// Business status → 4 tone levels +const toneOf = (leak) => { + if (leak.ip === t('dnsleaktest.StatusWait')) return 'wait'; + if (leak.ip === t('dnsleaktest.StatusError')) return 'fail'; + if (leak.ip.includes('.') || leak.ip.includes(':')) return 'ok-fast'; + return 'wait'; +}; + + +// Status +const isFieldPending = (value) => { + return !value || value === t('dnsleaktest.StatusWait') || value === t('dnsleaktest.StatusError'); +}; +const isResolved = (leak) => toneOf(leak) === 'ok-fast'; + +// Ensure full line display, without line breaks due to IPv6 +const fitOneLineClass = (text) => { + const len = typeof text === 'string' ? text.length : 0; + if (len <= 15) return 'text-base'; + if (len <= 26) return 'text-sm'; + return 'text-sm md:text-xs'; +}; const createDefaultCard = () => ({ name: t('dnsleaktest.Name'), @@ -84,32 +143,28 @@ const createDefaultCard = () => ({ }); const leakTest = reactive([ - { ...createDefaultCard(), id: "ipapi1" }, - { ...createDefaultCard(), id: "ipapi2" }, - { ...createDefaultCard(), id: "sfshark1" }, - { ...createDefaultCard(), id: "sfshark2" }, + { ...createDefaultCard(), id: 'ipapi1' }, + { ...createDefaultCard(), id: 'ipapi2' }, + { ...createDefaultCard(), id: 'sfshark1' }, + { ...createDefaultCard(), id: 'sfshark2' }, ]); -const isStarted = ref(false); - -// 生成 32 位随机字符串 +// Generate 32-digit random string const generate32DigitString = () => { const unixTime = Date.now().toString(); - const fixedString = "jason5ng32"; + const fixedString = 'jason5ng32'; const randomString = Math.random().toString(36).substring(2, 11); - return unixTime + fixedString + randomString; }; -// 生成 14 位随机字符串 +// Generate 14-digit random string const generate14DigitString = () => { - const fixedString = "jn32"; + const fixedString = 'jn32'; const randomString = Math.random().toString(36).substring(2, 11); - return fixedString + randomString; }; -// DNS 泄露测试 1 +// DNS leak test 1 const fetchLeakTestIpApiCom = (index) => { return new Promise((resolve, reject) => { const urlString = generate32DigitString(); @@ -117,26 +172,24 @@ const fetchLeakTestIpApiCom = (index) => { fetch(url) .then((response) => { - if (!response.ok) { - throw new Error("Network response was not ok"); - } + if (!response.ok) throw new Error('Network response was not ok'); return response.json(); }) .then((data) => { - if (data.dns && "geo" in data.dns && "ip" in data.dns) { - const geoSplit = data.dns.geo.split(" - "); + if (data.dns && 'geo' in data.dns && 'ip' in data.dns) { + const geoSplit = data.dns.geo.split(' - '); leakTest[index].country_code = countryLookup.byCountry(geoSplit[0]).iso2; leakTest[index].country = getCountryName(leakTest[index].country_code, lang.value); leakTest[index].org = geoSplit[1] || ''; leakTest[index].ip = data.dns.ip; resolve(); } else { - console.error("Unexpected data structure:", data); - reject(new Error("Unexpected data structure")); + console.error('Unexpected data structure:', data); + reject(new Error('Unexpected data structure')); } }) .catch((error) => { - console.error("Error fetching leak test data:", error); + console.error('Error fetching leak test data:', error); leakTest[index].country = t('dnsleaktest.StatusError'); leakTest[index].ip = t('dnsleaktest.StatusError'); leakTest[index].country_code = ''; @@ -146,7 +199,7 @@ const fetchLeakTestIpApiCom = (index) => { }); }; -// DNS 泄露测试 2 +// DNS leak test 2 const fetchLeakTestSfSharkCom = (index, key) => { return new Promise((resolve, reject) => { const urlString = generate14DigitString(); @@ -154,9 +207,7 @@ const fetchLeakTestSfSharkCom = (index, key) => { fetch(url) .then((response) => { - if (!response.ok) { - throw new Error("Network response was not ok"); - } + if (!response.ok) throw new Error('Network response was not ok'); return response.json(); }) .then((data) => { @@ -170,12 +221,12 @@ const fetchLeakTestSfSharkCom = (index, key) => { leakTest[index].ip = keyEntry.IP; resolve(); } else { - console.error("Unexpected data structure:", data); - reject(new Error("Unexpected data structure")); + console.error('Unexpected data structure:', data); + reject(new Error('Unexpected data structure')); } }) .catch((error) => { - console.error("Error fetching leak test data:", error); + console.error('Error fetching leak test data:', error); leakTest[index].ip = t('dnsleaktest.StatusError'); leakTest[index].country = t('dnsleaktest.StatusError'); leakTest[index].country_code = ''; @@ -185,7 +236,7 @@ const fetchLeakTestSfSharkCom = (index, key) => { }); }; -// 检查所有 DNS 泄露测试 +// Check all const checkAllDNSLeakTest = async (isRefresh) => { isStarted.value = true; if (isRefresh) { @@ -199,48 +250,33 @@ const checkAllDNSLeakTest = async (isRefresh) => { }); } - // 设置延迟请求函数 - const delayedFetch = (fetchFunction, index, key, delay) => { - return new Promise((resolve) => { - setTimeout(() => { - fetchFunction(index, key).then(resolve).catch(resolve); // 无论成功或失败都会解决 - }, delay); - }); - }; + const delayedFetch = (fetchFunction, index, key, delay) => new Promise((resolve) => { + setTimeout(() => { + fetchFunction(index, key).then(resolve).catch(resolve); + }, delay); + }); - // 批量请求 const promises = [ delayedFetch(fetchLeakTestIpApiCom, 0, null, 100), delayedFetch(fetchLeakTestIpApiCom, 1, null, 1000), delayedFetch(fetchLeakTestSfSharkCom, 2, 0, 100), - delayedFetch(fetchLeakTestSfSharkCom, 3, 0, 1000) + delayedFetch(fetchLeakTestSfSharkCom, 3, 0, 1000), ]; - // 最长等待 6 秒 const allSettledPromise = Promise.allSettled(promises); const timeoutPromise = new Promise((resolve) => setTimeout(resolve, 6000)); - // 等待所有请求完成或超时 return Promise.race([allSettledPromise, timeoutPromise]).then(() => { store.setLoadingStatus('dnsleaktest', true); }); }; - onMounted(() => { store.setMountingStatus('dnsleaktest', true); }); defineExpose({ checkAllDNSLeakTest, - leakTest + leakTest, }); - - diff --git a/frontend/components/Footer.vue b/frontend/components/Footer.vue index 9ed8819f7..41b6d76a4 100644 --- a/frontend/components/Footer.vue +++ b/frontend/components/Footer.vue @@ -1,226 +1,216 @@ - - diff --git a/frontend/components/IpInfos.vue b/frontend/components/IpInfos.vue index b2a3e8f5a..7135aa9ff 100644 --- a/frontend/components/IpInfos.vue +++ b/frontend/components/IpInfos.vue @@ -1,22 +1,27 @@ @@ -42,22 +47,10 @@ const configs = computed(() => store.configs); const userPreferences = computed(() => store.userPreferences); const lang = computed(() => store.lang); -// 页面的动态配置 -const isMapShown = computed(() => userPreferences.value.showMap); +// Dynamic configuration of the page const isCardsCollapsed = computed(() => userPreferences.value.simpleMode); -// 创建样式 -const colClass = computed(() => { - const numCards = ipCardsToShow.value; - if (numCards > 0) { - // 保证每行不超过三个卡片 - const colSize = numCards > 3 ? 4 : Math.floor(12 / numCards); - return `col-xl-${colSize} col-md-${colSize} mb-4`; - } - return 'col-xl-4 col-lg-6 col-md-6 mb-4'; // 默认情况 -}); - -// 默认卡片数据 +// Default card data const createDefaultCard = () => ({ ip: "", country_name: "", @@ -72,13 +65,10 @@ const createDefaultCard = () => ({ mapUrl_dark: '/res/defaultMap_dark.webp', }); -// IP 数据卡片 +// IP data cards +// Order: v4, v6, CF-v4, CF-v6, CN, v64 +// First 2 / 4 / 6 slice is meaningful at every count the user can pick. const ipDataCards = reactive([ - { - ...createDefaultCard(), - id: "ipchecking_v64", - source: "IPCheck.ing IPv6/4", - }, { ...createDefaultCard(), id: "ipchecking_v4", @@ -89,11 +79,6 @@ const ipDataCards = reactive([ id: "ipchecking_v6", source: "IPCheck.ing IPv6", }, - { - ...createDefaultCard(), - id: "cnsource", - source: "CN Source", - }, { ...createDefaultCard(), id: "cloudflare_v4", @@ -104,16 +89,26 @@ const ipDataCards = reactive([ id: "cloudflare_v6", source: "Cloudflare IPv6", }, + { + ...createDefaultCard(), + id: "cnsource", + source: "CN Source", + }, + { + ...createDefaultCard(), + id: "ipchecking_v64", + source: "IPCheck.ing IPv6/4", + }, ]); -// 默认 ASN 信息 +// Default ASN information const asnInfos = ref({ "AS888888": { "asnName": "Google", "asnOrgName": "GOGL-ARIN", "estimatedUsers": "888888", "IPv4_Pct": "95.35", "IPv6_Pct": "4.65", "HTTP_Pct": "3.16", "HTTPS_Pct": "96.84", "Desktop_Pct": "58.88", "Mobile_Pct": "41.12", "Bot_Pct": "98.46", "Human_Pct": "1.54" } }); -// 其它数据 +// Other data const ipCardsToShow = ref(userPreferences.value.ipCardsToShow); const copiedStatus = ref({}); const IPArray = ref([]); @@ -121,11 +116,11 @@ const ipGeoSource = ref(userPreferences.value.ipGeoSource); const usingSource = ref(userPreferences.value.ipGeoSource); const fetchStatus = reactive([]); -// 中间件 +// Middleware let pendingIPDetailsRequests = new Map(); let ipDataCache = new Map(); -// 公共获取 IP 地址方法 +// Shared method to get IP address const fetchIP = async (cardID, getFromSource) => { const { ip, source } = await getFromSource(configs.value.originalSite); let fetchingStatus = false; @@ -134,18 +129,20 @@ const fetchIP = async (cardID, getFromSource) => { ipDataCards[cardID].source = source; IPArray.value = [...IPArray.value, ip]; await fetchIPDetails(cardID, ip); - } else if (cardID === 2 || cardID === 5) { + } else if (cardID === 1 || cardID === 3) { + // v6 cards in the new order: ipchecking_v6 (1), cloudflare_v6 (3) ipDataCards[cardID].ip = t('ipInfos.IPv6Error'); } else { ipDataCards[cardID].ip = t('ipInfos.IPv4Error'); } - // 总是返回 true,即使获取 IP 失败,以便在 trackFetchStatus 中记录 + // Always return true, even if fetching IP fails + // for tracking fetch status fetchingStatus = true; fetchStatus[cardID] = { [cardID]: fetchingStatus }; trackFetchStatus(fetchStatus); }; -// 上报数据获取状态,并发送到 store +// Report data fetch status, and send to store const trackFetchStatus = (status) => { let allHasFetched = true; for (let i = 0; i < ipCardsToShow.value; i++) { @@ -160,18 +157,18 @@ const trackFetchStatus = (status) => { } }; -// 检查所有 IP 地址 +// Check all IP addresses const checkAllIPs = async () => { const ipFunctions = [ - () => fetchIP(0, getIPFromIPChecking64), - () => fetchIP(1, getIPFromIPChecking4), - () => fetchIP(2, getIPFromIPChecking6), - () => fetchIP(3, getIPFromIPIP), - () => fetchIP(4, getIPFromCloudflare_V4), - () => fetchIP(5, getIPFromCloudflare_V6), + () => fetchIP(0, getIPFromIPChecking4), + () => fetchIP(1, getIPFromIPChecking6), + () => fetchIP(2, getIPFromCloudflare_V4), + () => fetchIP(3, getIPFromCloudflare_V6), + () => fetchIP(4, getIPFromIPIP), + () => fetchIP(5, getIPFromIPChecking64), ]; - // 限制执行的函数数量为 ipCardsToShow 的长度 + // Limit the number of functions to execute to the length of ipCardsToShow const maxIndex = ipCardsToShow.value; let index = 0; @@ -185,7 +182,7 @@ const checkAllIPs = async () => { }, 500); }; -// 从 IP 地址获取 IP 详细信息 +// Get IP details from IP address const fetchIPDetails = async (cardIndex, ip, sourceID = null) => { sourceID = sourceID || ipGeoSource.value; const card = ipDataCards[cardIndex]; @@ -195,14 +192,14 @@ const fetchIPDetails = async (cardIndex, ip, sourceID = null) => { setLang = 'zh-CN'; } - // 检查缓存中是否已有该 IP 的数据 + // Check if the IP data is already in the cache if (ipDataCache.has(ip)) { const cachedData = ipDataCache.get(ip); Object.assign(card, cachedData); return; } - // 检查是否有正在进行的查询,如果有,则等待该查询完成 + // Check if there is a query in progress, if so, wait for it to complete if (pendingIPDetailsRequests.has(ip)) { await pendingIPDetailsRequests.get(ip); const cachedData = ipDataCache.get(ip); @@ -244,7 +241,7 @@ const fetchIPDetails = async (cardIndex, ip, sourceID = null) => { throw new Error("All sources failed to fetch IP details for IP: " + ip); })(); - // 将此 Promise 存储在 pendingIPDetailsRequests 中,以避免重复查询 + // Store this Promise in pendingIPDetailsRequests to avoid duplicate queries pendingIPDetailsRequests.set(ip, fetchPromise); try { @@ -253,14 +250,14 @@ const fetchIPDetails = async (cardIndex, ip, sourceID = null) => { console.error(error); throw error; } finally { - // 完成后,从 pendingIPDetailsRequests 中移除 + // After completion, remove from pendingIPDetailsRequests pendingIPDetailsRequests.delete(ip); } }; -// 在重新选择 IP 数据库源时,更新 IP 地理数据 +// When the IP database source is reselected, update the IP geographic data const selectIPGeoSource = () => { - // 清空部分数据 + // Clear partial data ipDataCards.forEach((card) => { const { ip, mapUrl, mapUrl_dark } = card; Object.assign(card, createDefaultCard(), { ip, mapUrl, mapUrl_dark }); @@ -268,10 +265,10 @@ const selectIPGeoSource = () => { ipDataCache.clear(); - // 尝试更新一次,成功后再获取其他 IP 数据 + // Try to update once, then get other IP data let runningSource = fetchIPDetails(0, ipDataCards[0].ip, ipGeoSource.value); - // 重新获取 IP 数据 + // Re-fetch IP data let index = 1; const interval = setInterval(() => { if (index < ipDataCards.length) { @@ -286,27 +283,27 @@ const selectIPGeoSource = () => { }, 500); }; -// 刷新某张卡片 +// Refresh a card const refreshCard = (card, index) => { clearCardData(card); switch (index) { case 0: - fetchIP(0, getIPFromIPChecking64); + fetchIP(0, getIPFromIPChecking4); break; case 1: - fetchIP(1, getIPFromIPChecking4); + fetchIP(1, getIPFromIPChecking6); break; case 2: - fetchIP(2, getIPFromIPChecking6); + fetchIP(2, getIPFromCloudflare_V4); break; case 3: - fetchIP(3, getIPFromIPIP); + fetchIP(3, getIPFromCloudflare_V6); break; case 4: - fetchIP(4, getIPFromCloudflare_V4); + fetchIP(4, getIPFromIPIP); break; case 5: - fetchIP(5, getIPFromCloudflare_V6); + fetchIP(5, getIPFromIPChecking64); break; default: console.error("Undefind Source:"); @@ -314,7 +311,7 @@ const refreshCard = (card, index) => { trackEvent('IPCheck', 'RefreshClick', 'IPInfos'); }; -// 清空卡片数据 +// Clear card data const clearCardData = (card) => { Object.assign(card, createDefaultCard()); }; @@ -343,5 +340,4 @@ defineExpose({ - \ No newline at end of file + \ No newline at end of file diff --git a/frontend/components/Nav.vue b/frontend/components/Nav.vue index 49b993a7b..87d8bc170 100644 --- a/frontend/components/Nav.vue +++ b/frontend/components/Nav.vue @@ -1,134 +1,171 @@