From ebb78433c07b0cf2b169a1239b8b95ce448e7287 Mon Sep 17 00:00:00 2001 From: Test User Date: Thu, 23 Apr 2026 11:19:16 +0800 Subject: [PATCH 01/13] feat: add usePageMetadata hook for document title and html lang sync --- .../src/hooks/usePageMetadata.ts | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/dev-tools-collection-web/src/hooks/usePageMetadata.ts diff --git a/src/dev-tools-collection-web/src/hooks/usePageMetadata.ts b/src/dev-tools-collection-web/src/hooks/usePageMetadata.ts new file mode 100644 index 0000000..08cca61 --- /dev/null +++ b/src/dev-tools-collection-web/src/hooks/usePageMetadata.ts @@ -0,0 +1,49 @@ +import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; + +interface UsePageMetadataOptions { + /** 页面主标题,通常是工具名;为空时使用站点名兜底 */ + title?: string; +} + +const SUPPORTED_LANGS = ['zh', 'en'] as const; +type SupportedLang = (typeof SUPPORTED_LANGS)[number]; + +function normalizeLang(raw: string | undefined): SupportedLang { + if (!raw) return 'en'; + const lower = raw.toLowerCase(); + for (const lang of SUPPORTED_LANGS) { + if (lower === lang || lower.startsWith(`${lang}-`)) { + return lang; + } + } + return 'en'; +} + +/** + * 统一同步 document.title 与 。 + * - 不阻塞渲染,失败时静默回退。 + * - 标题格式:工具页 ` | `,无工具名时使用站点名。 + */ +export function usePageMetadata(options: UsePageMetadataOptions = {}): void { + const { title } = options; + const { i18n, t } = useTranslation(); + const language = i18n.resolvedLanguage ?? i18n.language; + + useEffect(() => { + const lang = normalizeLang(language); + const root = document.documentElement; + if (root.lang !== lang) { + root.lang = lang; + } + + const siteName = t('common.toolbox') || 'Toolbox'; + const pageTitle = title?.trim(); + const finalTitle = pageTitle ? `${pageTitle} | ${siteName}` : siteName; + if (document.title !== finalTitle) { + document.title = finalTitle; + } + }, [language, title, t]); +} + +export default usePageMetadata; From 2ffd51812daed33aaafcaed822c3734620470e88 Mon Sep 17 00:00:00 2001 From: Test User Date: Thu, 23 Apr 2026 11:29:05 +0800 Subject: [PATCH 02/13] feat: mount next-themes ThemeProvider and gate router devtools to DEV --- src/dev-tools-collection-web/src/main.tsx | 10 +++++++++- src/dev-tools-collection-web/src/routes/__root.tsx | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/dev-tools-collection-web/src/main.tsx b/src/dev-tools-collection-web/src/main.tsx index d8599bd..cc0a888 100644 --- a/src/dev-tools-collection-web/src/main.tsx +++ b/src/dev-tools-collection-web/src/main.tsx @@ -2,6 +2,7 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import './index.css'; import { createRouter, RouterProvider } from '@tanstack/react-router'; +import { ThemeProvider } from 'next-themes'; import { routeTree } from './routeTree.gen'; import '@/localization/i18n'; @@ -17,6 +18,13 @@ declare module '@tanstack/react-router' { createRoot(document.getElementById('root')!).render( - + + + ); diff --git a/src/dev-tools-collection-web/src/routes/__root.tsx b/src/dev-tools-collection-web/src/routes/__root.tsx index 2e80563..72f8df3 100644 --- a/src/dev-tools-collection-web/src/routes/__root.tsx +++ b/src/dev-tools-collection-web/src/routes/__root.tsx @@ -7,7 +7,7 @@ export const Route = createRootRoute({ <> - + {import.meta.env.DEV && } ) }); From fee54e0b5b111f83224272e5b69d51b02c0545e2 Mon Sep 17 00:00:00 2001 From: Test User Date: Thu, 23 Apr 2026 11:34:29 +0800 Subject: [PATCH 03/13] refactor: wire page metadata through usePageMetadata and drop dead state --- src/dev-tools-collection-web/index.html | 2 +- .../src/components/ToolPageLayout.tsx | 4 ++-- src/dev-tools-collection-web/src/routes/index.tsx | 14 ++++---------- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/dev-tools-collection-web/index.html b/src/dev-tools-collection-web/index.html index 00d97b3..fc543b3 100644 --- a/src/dev-tools-collection-web/index.html +++ b/src/dev-tools-collection-web/index.html @@ -4,7 +4,7 @@ - 在线工具箱 + Toolbox
diff --git a/src/dev-tools-collection-web/src/components/ToolPageLayout.tsx b/src/dev-tools-collection-web/src/components/ToolPageLayout.tsx index ee2bd60..2577382 100644 --- a/src/dev-tools-collection-web/src/components/ToolPageLayout.tsx +++ b/src/dev-tools-collection-web/src/components/ToolPageLayout.tsx @@ -1,10 +1,10 @@ import type { ReactElement, ReactNode } from 'react'; import Footer from '@/components/Footer.tsx'; import ToolPageHeader from '@/components/ToolPageHeader.tsx'; -import { useDocumentTitle } from '@uidotdev/usehooks'; import { useLocation } from '@tanstack/react-router'; import { toolsData } from '@/data/tools.ts'; import { useTranslation } from 'react-i18next'; +import { usePageMetadata } from '@/hooks/usePageMetadata'; function ToolPageLayout(props: ToolPageLayoutProps): ReactElement { const { children } = props; @@ -14,7 +14,7 @@ function ToolPageLayout(props: ToolPageLayoutProps): ReactElement { const toolId = toolsData.find(tool => tool.url === location.pathname)?.id; const toolName = toolId ? t(`tools.${toolId}`) : t('common.tool'); - useDocumentTitle(toolName); + usePageMetadata({ title: toolName }); return (
diff --git a/src/dev-tools-collection-web/src/routes/index.tsx b/src/dev-tools-collection-web/src/routes/index.tsx index 0066405..04cf697 100644 --- a/src/dev-tools-collection-web/src/routes/index.tsx +++ b/src/dev-tools-collection-web/src/routes/index.tsx @@ -1,10 +1,11 @@ import { createFileRoute } from '@tanstack/react-router'; -import { useEffect, useState } from 'react'; -import { getPopularTools, type Tool, toolsData } from '@/data/tools.ts'; +import { useState } from 'react'; +import { type Tool, toolsData } from '@/data/tools.ts'; import SearchBar from '@/components/SearchBar.tsx'; import ToolCard from '@/components/ToolCard.tsx'; import Footer from '@/components/Footer.tsx'; import { useTranslation } from 'react-i18next'; +import { usePageMetadata } from '@/hooks/usePageMetadata'; export const Route = createFileRoute('/')({ component: Index @@ -13,13 +14,8 @@ export const Route = createFileRoute('/')({ function Index() { const { t } = useTranslation(); const [searchResults, setSearchResults] = useState([]); - const [, setPopularTools] = useState([]); - useEffect(() => { - // Initialize popular tools data - const popular = getPopularTools(); - setPopularTools(popular); - }, []); + usePageMetadata(); const handleSearchResults = (results: Tool[]) => { setSearchResults(results); @@ -42,9 +38,7 @@ function Index() {
- {/* Tools Content */}
- {/* Search Results */} {searchResults.length > 0 ? (

From 4e9afeef4c13027972784f6c7ba864161bf421bc Mon Sep 17 00:00:00 2001 From: Test User Date: Thu, 23 Apr 2026 11:37:00 +0800 Subject: [PATCH 04/13] refactor: simplify LanguageSwitcher with single source of truth --- .../src/components/LanguageSwitcher.tsx | 34 +++++++------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/src/dev-tools-collection-web/src/components/LanguageSwitcher.tsx b/src/dev-tools-collection-web/src/components/LanguageSwitcher.tsx index 7aef205..eb013c2 100644 --- a/src/dev-tools-collection-web/src/components/LanguageSwitcher.tsx +++ b/src/dev-tools-collection-web/src/components/LanguageSwitcher.tsx @@ -1,36 +1,28 @@ import { useTranslation } from 'react-i18next'; import { Button } from '@/components/ui/button'; -import { useEffect, useState } from 'react'; -import { useToggle } from '@uidotdev/usehooks'; + +const LANG_LABELS: Record = { + en: '中文', + zh: 'English' +}; const LanguageSwitcher = () => { const { i18n } = useTranslation(); - const [currentLanguage, setCurrentLanguage] = useState(i18n.language); - const [on, toggle] = useToggle(i18n.language === 'en'); - - useEffect(() => { - setCurrentLanguage(i18n.language); - }, [i18n.language]); - - useEffect(() => { - let newLanguage = 'zh'; - if (on) { - newLanguage = 'en'; - } else { - newLanguage = 'zh'; - } + const current = i18n.resolvedLanguage === 'en' ? 'en' : 'zh'; + const next = current === 'en' ? 'zh' : 'en'; - void i18n.changeLanguage(newLanguage); - }, [i18n, on]); + const handleClick = () => { + void i18n.changeLanguage(next); + }; return ( ); }; From cd64661c39fe222c6250ab66f37ac68564871034 Mon Sep 17 00:00:00 2001 From: Test User Date: Thu, 23 Apr 2026 11:44:47 +0800 Subject: [PATCH 05/13] feat(web): add ThemeSwitcher with next-themes integration --- .../src/components/ThemeSwitcher.tsx | 51 +++++++++++++++++++ .../src/localization/en.ts | 42 +++------------ .../src/localization/zh.ts | 41 +++------------ 3 files changed, 63 insertions(+), 71 deletions(-) create mode 100644 src/dev-tools-collection-web/src/components/ThemeSwitcher.tsx diff --git a/src/dev-tools-collection-web/src/components/ThemeSwitcher.tsx b/src/dev-tools-collection-web/src/components/ThemeSwitcher.tsx new file mode 100644 index 0000000..462b3e2 --- /dev/null +++ b/src/dev-tools-collection-web/src/components/ThemeSwitcher.tsx @@ -0,0 +1,51 @@ +import { useTheme } from 'next-themes'; +import { useTranslation } from 'react-i18next'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from '@/components/ui/select'; + +type ThemeValue = 'light' | 'dark' | 'system'; + +const VALUES: readonly ThemeValue[] = ['light', 'dark', 'system'] as const; + +function normalizeTheme(raw: string | undefined): ThemeValue { + return (VALUES as readonly string[]).includes(raw ?? '') + ? (raw as ThemeValue) + : 'system'; +} + +const ThemeSwitcher = () => { + const { t } = useTranslation(); + const { theme, setTheme } = useTheme(); + + const labels: Record = { + light: t('common.themeLight'), + dark: t('common.themeDark'), + system: t('common.themeSystem') + }; + + return ( + + ); +}; + +export default ThemeSwitcher; diff --git a/src/dev-tools-collection-web/src/localization/en.ts b/src/dev-tools-collection-web/src/localization/en.ts index bb15380..5cfd7dc 100644 --- a/src/dev-tools-collection-web/src/localization/en.ts +++ b/src/dev-tools-collection-web/src/localization/en.ts @@ -9,7 +9,12 @@ export default { home: 'Home', copyright: '© {{year}} Toolbox. All rights reserved.', searchPlaceholder: - 'Search tools, e.g.: JSON Formatter, Timestamp Converter...' + 'Search tools, e.g.: JSON Formatter, Timestamp Converter...', + opensInNewTab: 'opens in a new tab', + theme: 'Theme', + themeLight: 'Light', + themeDark: 'Dark', + themeSystem: 'System' }, tools: { 'json-formatter': 'JSON Formatter', @@ -280,40 +285,5 @@ export default { charRangeDesc: 'Character range, matches any one character in the specified range', escapeDesc: 'Escape character, used to match special characters themselves' - }, - ipInfo: { - title: 'IP Information Tool', - description: - 'View detailed information about an IP address, including ASN, geolocation, company information, etc.', - inputIp: 'Enter IP Address', - ipPlaceholder: 'Example: 8.8.8.8', - search: 'Search', - searching: 'Searching...', - searchMyIp: 'Search My IP', - // Information sections - basicInfo: 'Basic Information', - locationInfo: 'Location Information', - privacyNetworkInfo: 'Privacy and Network Information', - // Basic information fields - ipAddress: 'IP Address', - asn: 'ASN', - ipRange: 'IP Range', - company: 'Company', - asnType: 'ASN Type', - abuseContact: 'Abuse Contact', - // Location information fields - city: 'City', - state: 'State/Province', - country: 'Country', - coordinates: 'Coordinates', - localTime: 'Local Time', - timezone: 'Timezone', - // Privacy information fields - privateIp: 'Private IP', - anycast: 'Anycast', - privateIpDesc: 'Whether this IP is using VPN or other methods to hide', - anycastDesc: 'Anycast address is an address shared by multiple systems', - yes: 'Yes', - no: 'No' } }; diff --git a/src/dev-tools-collection-web/src/localization/zh.ts b/src/dev-tools-collection-web/src/localization/zh.ts index 2b957e3..3140947 100644 --- a/src/dev-tools-collection-web/src/localization/zh.ts +++ b/src/dev-tools-collection-web/src/localization/zh.ts @@ -7,7 +7,12 @@ export default { tool: '工具', home: '首页', copyright: '© {{year}} 工具箱. 保留所有权利。', - searchPlaceholder: '搜索工具,如:JSON 格式化、时间戳转换...' + searchPlaceholder: '搜索工具,如:JSON 格式化、时间戳转换...', + opensInNewTab: '在新标签页打开', + theme: '主题', + themeLight: '浅色', + themeDark: '深色', + themeSystem: '跟随系统' }, tools: { 'json-formatter': 'JSON 格式化', @@ -271,39 +276,5 @@ export default { negatedCharSetDesc: '否定字符集,匹配不在括号内的任意一个字符', charRangeDesc: '字符范围,匹配指定范围内的任意一个字符', escapeDesc: '转义字符,用于匹配特殊字符本身' - }, - ipInfo: { - title: 'IP 信息查看工具', - description: '查看 IP 地址的详细信息,包括 ASN、地理位置、公司信息等。', - inputIp: '输入 IP 地址', - ipPlaceholder: '例如: 8.8.8.8', - search: '查询', - searching: '查询中...', - searchMyIp: '查询本机IP', - // Information sections - basicInfo: '基本信息', - locationInfo: '位置信息', - privacyNetworkInfo: '隐私和网络信息', - // Basic information fields - ipAddress: 'IP 地址', - asn: 'ASN', - ipRange: 'IP 范围', - company: '公司', - asnType: 'ASN 类型', - abuseContact: '滥用联系方式', - // Location information fields - city: '城市', - state: '州/省', - country: '国家', - coordinates: '坐标', - localTime: '本地时间', - timezone: '时区', - // Privacy information fields - privateIp: '私密IP', - anycast: 'Anycast', - privateIpDesc: '此 IP 是否使用 VPN 或其他方法隐藏', - anycastDesc: '任播(Anycast)地址是多个系统共享的地址', - yes: '是', - no: '否' } }; From 513b1791ff9b5a794e486c1e3596dc3c89b5e839 Mon Sep 17 00:00:00 2001 From: Test User Date: Thu, 23 Apr 2026 11:50:32 +0800 Subject: [PATCH 06/13] feat(web): make ip-info tool an external link card --- .../src/components/ToolCard.tsx | 51 ++++- .../src/data/tools.ts | 5 +- .../src/hooks/useGetIpInfo.ts | 131 ----------- .../src/routeTree.gen.ts | 21 -- .../src/routes/tools/ip-info.tsx | 211 ------------------ 5 files changed, 43 insertions(+), 376 deletions(-) delete mode 100644 src/dev-tools-collection-web/src/hooks/useGetIpInfo.ts delete mode 100644 src/dev-tools-collection-web/src/routes/tools/ip-info.tsx diff --git a/src/dev-tools-collection-web/src/components/ToolCard.tsx b/src/dev-tools-collection-web/src/components/ToolCard.tsx index 119323d..ab05117 100644 --- a/src/dev-tools-collection-web/src/components/ToolCard.tsx +++ b/src/dev-tools-collection-web/src/components/ToolCard.tsx @@ -2,6 +2,7 @@ import { Card, CardContent } from '@/components/ui/card'; import type { Tool } from '../data/tools'; import { Link } from '@tanstack/react-router'; import { useTranslation } from 'react-i18next'; +import { ExternalLink } from 'lucide-react'; interface ToolCardProps { tool: Tool; @@ -9,21 +10,47 @@ interface ToolCardProps { const ToolCard = ({ tool }: ToolCardProps) => { const { t } = useTranslation(); + const label = t(`tools.${tool.id}`); + const isExternal = Boolean(tool.externalUrl); + + const cardContent = ( + + {isExternal && ( + + ); + + if (isExternal) { + return ( + + {cardContent} + + ); + } return ( - - -
-
- -
-

- {t(`tools.${tool.id}`)} -

-
-
-
+ {cardContent} ); }; diff --git a/src/dev-tools-collection-web/src/data/tools.ts b/src/dev-tools-collection-web/src/data/tools.ts index 23a800f..63332ca 100644 --- a/src/dev-tools-collection-web/src/data/tools.ts +++ b/src/dev-tools-collection-web/src/data/tools.ts @@ -21,6 +21,8 @@ export interface Tool { >; url: string; popular?: boolean; + // 外链工具:存在时点击卡片会在新标签页打开该 URL,不走站内路由 + externalUrl?: string; } export const toolsData: Tool[] = [ @@ -91,7 +93,8 @@ export const toolsData: Tool[] = [ id: 'ip-info', name: 'IP 信息查看', icon: Globe, - url: '/tools/ip-info', + url: 'https://ipinfo.io/what-is-my-ip', + externalUrl: 'https://ipinfo.io/what-is-my-ip', popular: true } ]; diff --git a/src/dev-tools-collection-web/src/hooks/useGetIpInfo.ts b/src/dev-tools-collection-web/src/hooks/useGetIpInfo.ts deleted file mode 100644 index 5655ce0..0000000 --- a/src/dev-tools-collection-web/src/hooks/useGetIpInfo.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { useState } from 'react'; - -const fetchIpInfo = async (ip?: string): Promise => { - try { - const finalIp = ip || 'what-is-my-ip'; - const response = await fetch(`https://ipinfo.io/${finalIp}`); - const htmlString = await response.text(); - const parser = new DOMParser(); - const doc = parser.parseFromString(htmlString, 'text/html'); - - // Actual IP - const actualIp = doc.querySelector('h1.h1-data-pages')?.textContent; - - const ipInfoTableRows = doc.querySelectorAll( - 'table.table-striped tbody tr' - ); - //ASN - const firstRow = ipInfoTableRows?.[0]; - const asn = firstRow?.querySelector('td:nth-child(2)')?.textContent; - //Range - const thirdRow = ipInfoTableRows?.[2]; - const range = thirdRow?.querySelector('td:nth-child(2)')?.textContent; - //Company - const fourthRow = ipInfoTableRows?.[3]; - const company = fourthRow?.querySelector('td:nth-child(2)')?.textContent; - //Privacy - const sixthRow = ipInfoTableRows?.[5]; - const privacy = - sixthRow?.querySelector('td:nth-child(2)')?.textContent?.trim() === - 'True'; - //Anycast - const seventhRow = ipInfoTableRows?.[6]; - const anycast = seventhRow?.querySelector('td:nth-child(2)')?.textContent; - //ASN Type - const eighthRow = ipInfoTableRows?.[7]; - const asnType = eighthRow?.querySelector('td:nth-child(2)')?.textContent; - //Abuse contact - const ninthRow = ipInfoTableRows?.[8]; - const abuseContact = - ninthRow?.querySelector('td:nth-child(2)')?.textContent?.trim() || - '未提供'; - - // Location - const locationTableRows = doc.querySelectorAll('table.geo-table tbody tr'); - // City - const cityRow = locationTableRows?.[0]; - const city = cityRow?.querySelector('td:nth-child(2)')?.textContent; - // State - const stateRow = locationTableRows?.[1]; - const state = stateRow?.querySelector('td:nth-child(2)')?.textContent; - // Country - const countryRow = locationTableRows?.[2]; - const country = countryRow?.querySelector('td:nth-child(2)')?.textContent; - // Local Time - const localTimeRow = locationTableRows?.[4]; - const localTime = - localTimeRow?.querySelector('td:nth-child(2)')?.textContent; - // Timezone - const timezoneRow = locationTableRows?.[5]; - const timezone = timezoneRow?.querySelector('td:nth-child(2)')?.textContent; - // Coordinates - const coordinatesRow = locationTableRows?.[3]; - const coordinates = - coordinatesRow?.querySelector('td:nth-child(2)')?.textContent; - - return { - asn: asn || '未知', - abuseContact: abuseContact || '未提供', - anycast: anycast?.trim() === 'True', - asnType: asnType || '未知', - company: company || '未知', - privacy: privacy, - range: range || '未知', - time: { - localTime: localTime || '未知', - timezone: timezone || '未知' - }, - location: { - city: city || '未知', - coordinates: coordinates || '未知', - country: country || '未知', - state: state || '未知' - }, - ip: actualIp || '未知' - }; - } catch (error) { - console.error('Error fetching IP info:', error); - return null; - } -}; - -export default function useGetIpInfo() { - const [loading, setLoading] = useState(false); - - const getIpInfo = async (ip?: string): Promise => { - setLoading(true); - const info = await fetchIpInfo(ip); - setLoading(false); - - return info; - }; - - return { - loading, - getIpInfo - }; -} - -// Types -interface IPInfo { - ip: string; - asn: string; - range: string; - company: string; - privacy: boolean; - anycast: boolean; - asnType: string; - abuseContact: string; - location: { - city: string; - state: string; - country: string; - coordinates: string; - }; - time: { - localTime: string; - timezone: string; - }; -} - -export type { IPInfo }; diff --git a/src/dev-tools-collection-web/src/routeTree.gen.ts b/src/dev-tools-collection-web/src/routeTree.gen.ts index e60b3e3..a207078 100644 --- a/src/dev-tools-collection-web/src/routeTree.gen.ts +++ b/src/dev-tools-collection-web/src/routeTree.gen.ts @@ -16,7 +16,6 @@ import { Route as ToolsTimestampRouteImport } from './routes/tools/timestamp' import { Route as ToolsRegexTesterRouteImport } from './routes/tools/regex-tester' import { Route as ToolsQrcodeGeneratorRouteImport } from './routes/tools/qrcode-generator' import { Route as ToolsJsonFormatterRouteImport } from './routes/tools/json-formatter' -import { Route as ToolsIpInfoRouteImport } from './routes/tools/ip-info' import { Route as ToolsHtmlPreviewRouteImport } from './routes/tools/html-preview' import { Route as ToolsHashEncoderRouteImport } from './routes/tools/hash-encoder' import { Route as ToolsBase64CodecRouteImport } from './routes/tools/base64-codec' @@ -56,11 +55,6 @@ const ToolsJsonFormatterRoute = ToolsJsonFormatterRouteImport.update({ path: '/tools/json-formatter', getParentRoute: () => rootRouteImport, } as any) -const ToolsIpInfoRoute = ToolsIpInfoRouteImport.update({ - id: '/tools/ip-info', - path: '/tools/ip-info', - getParentRoute: () => rootRouteImport, -} as any) const ToolsHtmlPreviewRoute = ToolsHtmlPreviewRouteImport.update({ id: '/tools/html-preview', path: '/tools/html-preview', @@ -82,7 +76,6 @@ export interface FileRoutesByFullPath { '/tools/base64-codec': typeof ToolsBase64CodecRoute '/tools/hash-encoder': typeof ToolsHashEncoderRoute '/tools/html-preview': typeof ToolsHtmlPreviewRoute - '/tools/ip-info': typeof ToolsIpInfoRoute '/tools/json-formatter': typeof ToolsJsonFormatterRoute '/tools/qrcode-generator': typeof ToolsQrcodeGeneratorRoute '/tools/regex-tester': typeof ToolsRegexTesterRoute @@ -95,7 +88,6 @@ export interface FileRoutesByTo { '/tools/base64-codec': typeof ToolsBase64CodecRoute '/tools/hash-encoder': typeof ToolsHashEncoderRoute '/tools/html-preview': typeof ToolsHtmlPreviewRoute - '/tools/ip-info': typeof ToolsIpInfoRoute '/tools/json-formatter': typeof ToolsJsonFormatterRoute '/tools/qrcode-generator': typeof ToolsQrcodeGeneratorRoute '/tools/regex-tester': typeof ToolsRegexTesterRoute @@ -109,7 +101,6 @@ export interface FileRoutesById { '/tools/base64-codec': typeof ToolsBase64CodecRoute '/tools/hash-encoder': typeof ToolsHashEncoderRoute '/tools/html-preview': typeof ToolsHtmlPreviewRoute - '/tools/ip-info': typeof ToolsIpInfoRoute '/tools/json-formatter': typeof ToolsJsonFormatterRoute '/tools/qrcode-generator': typeof ToolsQrcodeGeneratorRoute '/tools/regex-tester': typeof ToolsRegexTesterRoute @@ -124,7 +115,6 @@ export interface FileRouteTypes { | '/tools/base64-codec' | '/tools/hash-encoder' | '/tools/html-preview' - | '/tools/ip-info' | '/tools/json-formatter' | '/tools/qrcode-generator' | '/tools/regex-tester' @@ -137,7 +127,6 @@ export interface FileRouteTypes { | '/tools/base64-codec' | '/tools/hash-encoder' | '/tools/html-preview' - | '/tools/ip-info' | '/tools/json-formatter' | '/tools/qrcode-generator' | '/tools/regex-tester' @@ -150,7 +139,6 @@ export interface FileRouteTypes { | '/tools/base64-codec' | '/tools/hash-encoder' | '/tools/html-preview' - | '/tools/ip-info' | '/tools/json-formatter' | '/tools/qrcode-generator' | '/tools/regex-tester' @@ -164,7 +152,6 @@ export interface RootRouteChildren { ToolsBase64CodecRoute: typeof ToolsBase64CodecRoute ToolsHashEncoderRoute: typeof ToolsHashEncoderRoute ToolsHtmlPreviewRoute: typeof ToolsHtmlPreviewRoute - ToolsIpInfoRoute: typeof ToolsIpInfoRoute ToolsJsonFormatterRoute: typeof ToolsJsonFormatterRoute ToolsQrcodeGeneratorRoute: typeof ToolsQrcodeGeneratorRoute ToolsRegexTesterRoute: typeof ToolsRegexTesterRoute @@ -224,13 +211,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ToolsJsonFormatterRouteImport parentRoute: typeof rootRouteImport } - '/tools/ip-info': { - id: '/tools/ip-info' - path: '/tools/ip-info' - fullPath: '/tools/ip-info' - preLoaderRoute: typeof ToolsIpInfoRouteImport - parentRoute: typeof rootRouteImport - } '/tools/html-preview': { id: '/tools/html-preview' path: '/tools/html-preview' @@ -260,7 +240,6 @@ const rootRouteChildren: RootRouteChildren = { ToolsBase64CodecRoute: ToolsBase64CodecRoute, ToolsHashEncoderRoute: ToolsHashEncoderRoute, ToolsHtmlPreviewRoute: ToolsHtmlPreviewRoute, - ToolsIpInfoRoute: ToolsIpInfoRoute, ToolsJsonFormatterRoute: ToolsJsonFormatterRoute, ToolsQrcodeGeneratorRoute: ToolsQrcodeGeneratorRoute, ToolsRegexTesterRoute: ToolsRegexTesterRoute, diff --git a/src/dev-tools-collection-web/src/routes/tools/ip-info.tsx b/src/dev-tools-collection-web/src/routes/tools/ip-info.tsx deleted file mode 100644 index 1d1fb06..0000000 --- a/src/dev-tools-collection-web/src/routes/tools/ip-info.tsx +++ /dev/null @@ -1,211 +0,0 @@ -import { createFileRoute } from '@tanstack/react-router'; -import { type ChangeEvent, type KeyboardEvent, useState } from 'react'; -import ToolPageLayout from '@/components/ToolPageLayout.tsx'; -import { Button } from '@/components/ui/button'; -import { Card } from '@/components/ui/card.tsx'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label.tsx'; -import { - AlertTriangle, - Globe, - Info, - MapPin, - Search, - Shield -} from 'lucide-react'; -import useFetchIpInfoPage, { type IPInfo } from '@/hooks/useGetIpInfo.ts'; -import { useTranslation } from 'react-i18next'; - -export const Route = createFileRoute('/tools/ip-info')({ - component: IPInfoViewer -}); - -function IPInfoViewer() { - const { t } = useTranslation(); - const [ipAddress, setIpAddress] = useState(''); - const { loading: isLoading, getIpInfo } = useFetchIpInfoPage(); - - // This is just for UI demonstration, in a real implementation this would come from an API - const [ipInfo, setIpInfo] = useState(null); - - const handleInputChange = (e: ChangeEvent) => { - setIpAddress(e.target.value); - }; - - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Enter' && ipAddress) { - handleSearch(); - } - }; - - const handleSearch = async (getMyIp?: boolean) => { - if (!ipAddress && !getMyIp) return; - - const ipInfo = await getIpInfo(getMyIp ? undefined : ipAddress); - - setIpInfo(ipInfo); - }; - - return ( - -
- -
-

{t('ipInfo.title')}

-

{t('ipInfo.description')}

-
- -
-
- - -
-
- - -
-
- - {ipInfo && ( -
-
- {/* 基本信息 */} -
-

- - 基本信息 -

- -
- - - - - - -
-
- - {/* 位置信息 */} -
-

- - 位置信息 -

- -
- - - - - - -
-
- - {/* 隐私和网络信息 */} -
-

- - 隐私和网络信息 -

- -
- - -
-
-
-
- )} -
-
-
- ); -} - -// Helper components -const InfoItem = ({ - label, - value, - description -}: { - label: string; - value: string; - description?: string; -}) => ( -
-
- {label}: - {value} -
- {description && ( - {description} - )} -
-); - -const PrivacyItem = ({ - label, - value, - description -}: { - label: string; - value: boolean; - description: string; -}) => ( -
-
- {!value ? ( - - ) : ( - - )} -
- {label} - - {value ? '是' : '否'} - - - {description} - -
-); From 3cc2e5741c736af9f564ec5fb87a5b5a47721f67 Mon Sep 17 00:00:00 2001 From: Test User Date: Thu, 23 Apr 2026 11:52:42 +0800 Subject: [PATCH 07/13] feat(web): host ThemeSwitcher in footer and tokenize footer colors --- src/dev-tools-collection-web/src/components/Footer.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/dev-tools-collection-web/src/components/Footer.tsx b/src/dev-tools-collection-web/src/components/Footer.tsx index 7379e1d..8d3028a 100644 --- a/src/dev-tools-collection-web/src/components/Footer.tsx +++ b/src/dev-tools-collection-web/src/components/Footer.tsx @@ -1,16 +1,20 @@ import { useTranslation } from 'react-i18next'; import LanguageSwitcher from './LanguageSwitcher'; +import ThemeSwitcher from './ThemeSwitcher'; const Footer = () => { const { t } = useTranslation(); const currentYear = new Date().getFullYear(); return ( -