From 3d80b4b2b686116a71db8f7755adc69848732cbe Mon Sep 17 00:00:00 2001 From: typist Date: Wed, 29 Oct 2025 05:34:01 +0800 Subject: [PATCH 1/6] feat: add @radix-ui/react-collapsible dependency and create collapsible component - Added @radix-ui/react-collapsible version 1.1.12 to dependencies in package.json. - Created a new collapsible component in src/components/ui/collapsible.tsx using Radix UI's collapsible primitives. --- package.json | 1 + pnpm-lock.yaml | 32 +++++++++++++++++++++++++++++++ src/components/ui/collapsible.tsx | 12 ++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 src/components/ui/collapsible.tsx diff --git a/package.json b/package.json index 36c62b2..fba790e 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "release:major": "npm version major && git push --follow-tags" }, "dependencies": { + "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-separator": "^1.1.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1ada229..a67820a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: .: dependencies: + '@radix-ui/react-collapsible': + specifier: ^1.1.12 + version: 1.1.12(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@radix-ui/react-dialog': specifier: ^1.1.15 version: 1.1.15(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -323,6 +326,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-collapsible@1.1.12': + resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-collection@1.1.7': resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} peerDependencies: @@ -1921,6 +1937,22 @@ snapshots: '@types/react': 19.2.2 '@types/react-dom': 19.2.2(@types/react@19.2.2) + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.2.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.2 + '@types/react-dom': 19.2.2(@types/react@19.2.2) + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.2.0) diff --git a/src/components/ui/collapsible.tsx b/src/components/ui/collapsible.tsx new file mode 100644 index 0000000..ef6b9f9 --- /dev/null +++ b/src/components/ui/collapsible.tsx @@ -0,0 +1,12 @@ +"use client" + +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" + +const Collapsible = CollapsiblePrimitive.Root + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger + +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent + +export { Collapsible, CollapsibleTrigger, CollapsibleContent } + -- 2.49.1 From 79c4462bfddb17edbab967494a5847030359b6aa Mon Sep 17 00:00:00 2001 From: typist Date: Wed, 29 Oct 2025 05:35:37 +0800 Subject: [PATCH 2/6] feat: enhance tool routing and add network tools - Refactored routing logic in src/router.tsx to utilize a buildToolRoutes function for better scalability. - Updated tools in src/components/tool/index.tsx to include a new "Network Tools" category with DNS, Ping, TCPing, and Speed Test components. --- src/components/tool/index.tsx | 3 ++- src/router.tsx | 32 +++++++++++++++++++++++--------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/components/tool/index.tsx b/src/components/tool/index.tsx index 735a1d8..b0701be 100644 --- a/src/components/tool/index.tsx +++ b/src/components/tool/index.tsx @@ -10,7 +10,8 @@ export interface Tool { name: string; icon: ReactNode; description: string; - component: ReactNode; + component?: ReactNode; + children?: Tool[]; } export const tools: Tool[] = [ diff --git a/src/router.tsx b/src/router.tsx index c73043d..0760224 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -2,11 +2,30 @@ import { createBrowserRouter, redirect, RouterProvider, + type RouteObject, } from "react-router-dom"; -import { tools } from "@/components/tool"; +import { tools, type Tool } from "@/components/tool"; import { Layout } from "./layout"; +const buildToolRoutes = (tools: Tool[]): RouteObject[] => { + return tools.map((tool) => { + const route: RouteObject = { + path: tool.path, + }; + + if (tool.component) { + route.element = tool.component; + } + + if (tool.children && tool.children.length > 0) { + route.children = buildToolRoutes(tool.children); + } + + return route; + }); +}; + // 路由配置 const router = createBrowserRouter([ { @@ -16,19 +35,14 @@ const router = createBrowserRouter([ { path: "tool", children: [ - ...tools.map((tool) => ( - { - path: tool.path, - element: tool.component, - } - )), + ...buildToolRoutes(tools), { index: true, loader: () => redirect("/tool/uuid"), }, - ] + ], }, - ] + ], }, { index: true, -- 2.49.1 From 27d230e50118e4051649d5863fbf52cbef71824d Mon Sep 17 00:00:00 2001 From: typist Date: Wed, 29 Oct 2025 05:35:58 +0800 Subject: [PATCH 3/6] feat: add custom scrollbar styles for light and dark themes - Implemented custom scrollbar styles in src/index.css for improved aesthetics. - Added styles for both light and dark themes, enhancing user experience across different modes. --- src/index.css | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/index.css b/src/index.css index 3830205..9f52dc8 100644 --- a/src/index.css +++ b/src/index.css @@ -117,4 +117,51 @@ body { @apply bg-background text-foreground; } +} + +/* 自定义滚动条样式 */ +* { + scrollbar-width: thin; + scrollbar-color: oklch(0.551 0.027 264.364 / 0.3) transparent; +} + +*::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +*::-webkit-scrollbar-track { + background: transparent; + border-radius: 4px; +} + +*::-webkit-scrollbar-thumb { + background: oklch(0.551 0.027 264.364 / 0.3); + border-radius: 4px; + transition: background 0.2s ease; +} + +*::-webkit-scrollbar-thumb:hover { + background: oklch(0.551 0.027 264.364 / 0.5); +} + +*::-webkit-scrollbar-thumb:active { + background: oklch(0.551 0.027 264.364 / 0.6); +} + +/* 深色模式下的滚动条 */ +.dark *::-webkit-scrollbar-thumb { + background: oklch(0.707 0.022 261.325 / 0.4); +} + +.dark *::-webkit-scrollbar-thumb:hover { + background: oklch(0.707 0.022 261.325 / 0.6); +} + +.dark *::-webkit-scrollbar-thumb:active { + background: oklch(0.707 0.022 261.325 / 0.7); +} + +.dark * { + scrollbar-color: oklch(0.707 0.022 261.325 / 0.4) transparent; } \ No newline at end of file -- 2.49.1 From e9cf714da9f9e4c0048b6d0e1f92f6a9d940ebd1 Mon Sep 17 00:00:00 2001 From: typist Date: Wed, 29 Oct 2025 05:37:37 +0800 Subject: [PATCH 4/6] feat: add network tools including DNS, Ping, TCPing, and Speed Test - Introduced a new "Network Tools" category in src/components/tool/index.tsx. - Implemented DNS lookup functionality in src/components/tool/network/dns.tsx. - Added Ping, TCPing, and Speed Test components in their respective files. - Updated network index file to export new components for routing. --- src/components/tool/index.tsx | 41 ++- src/components/tool/network/dns.tsx | 179 ++++++++++++ src/components/tool/network/index.tsx | 5 + src/components/tool/network/ping.tsx | 261 +++++++++++++++++ src/components/tool/network/speedtest.tsx | 338 ++++++++++++++++++++++ src/components/tool/network/tcping.tsx | 285 ++++++++++++++++++ 6 files changed, 1107 insertions(+), 2 deletions(-) create mode 100644 src/components/tool/network/dns.tsx create mode 100644 src/components/tool/network/index.tsx create mode 100644 src/components/tool/network/ping.tsx create mode 100644 src/components/tool/network/speedtest.tsx create mode 100644 src/components/tool/network/tcping.tsx diff --git a/src/components/tool/index.tsx b/src/components/tool/index.tsx index b0701be..ead16c2 100644 --- a/src/components/tool/index.tsx +++ b/src/components/tool/index.tsx @@ -1,9 +1,10 @@ import type { ReactNode } from 'react'; -import { FileJson, Hash, Binary } from 'lucide-react' +import { FileJson, Hash, Binary, Network, Globe, Activity, Gauge, Wifi } from 'lucide-react' import UUID from './uuid' import JSON from './json' import Base64 from './base64' +import { DNS, Ping, TCPing, SpeedTest } from './network' export interface Tool { path: string; @@ -35,5 +36,41 @@ export const tools: Tool[] = [ description: "Encode and decode Base64", icon: , component: , - } + }, + { + path: "network", + name: "Network Tools", + description: "Network testing tools", + icon: , + children: [ + { + path: "dns", + name: "DNS Lookup", + description: "DNS query tool", + icon: , + component: , + }, + { + path: "ping", + name: "Ping", + description: "Ping test tool", + icon: , + component: , + }, + { + path: "tcping", + name: "TCPing", + description: "TCP port connectivity test", + icon: , + component: , + }, + { + path: "speedtest", + name: "Speed Test", + description: "Website speed test", + icon: , + component: , + }, + ], + }, ]; \ No newline at end of file diff --git a/src/components/tool/network/dns.tsx b/src/components/tool/network/dns.tsx new file mode 100644 index 0000000..6659e40 --- /dev/null +++ b/src/components/tool/network/dns.tsx @@ -0,0 +1,179 @@ +import { useState, type FC } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { toast } from "sonner"; +import { Loader2 } from "lucide-react"; + +interface DNSRecord { + name: string; + type: number; + TTL: number; + data: string; +} + +interface DNSResponse { + Status: number; + Answer?: DNSRecord[]; + Question?: Array<{ name: string; type: number }>; +} + +const DNS_RECORD_TYPES = [ + { value: "1", label: "A", description: "IPv4 地址" }, + { value: "28", label: "AAAA", description: "IPv6 地址" }, + { value: "5", label: "CNAME", description: "规范名称" }, + { value: "15", label: "MX", description: "邮件交换" }, + { value: "2", label: "NS", description: "名称服务器" }, + { value: "16", label: "TXT", description: "文本记录" }, + { value: "6", label: "SOA", description: "授权起始" }, + { value: "257", label: "CAA", description: "证书颁发机构授权" }, + { value: "12", label: "PTR", description: "指针记录" }, + { value: "33", label: "SRV", description: "服务记录" }, +]; + +const getRecordTypeName = (type: number): string => { + const record = DNS_RECORD_TYPES.find((r) => r.value === String(type)); + return record ? record.label : `TYPE${type}`; +}; + +const Tool: FC = () => { + const [domain, setDomain] = useState(""); + const [loading, setLoading] = useState(false); + const [results, setResults] = useState([]); + const [queryTime, setQueryTime] = useState(0); + + const queryDNS = async () => { + if (!domain.trim()) { + toast.error("请输入域名"); + return; + } + + setLoading(true); + setResults([]); + setQueryTime(0); + + const startTime = performance.now(); + + try { + // 并发查询所有记录类型 + const queries = DNS_RECORD_TYPES.map((recordType) => + fetch( + `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent( + domain + )}&type=${recordType.value}`, + { + headers: { + Accept: "application/dns-json", + }, + } + ) + .then((response) => response.json()) + .then((data: DNSResponse) => { + if (data.Status === 0 && data.Answer && data.Answer.length > 0) { + return data.Answer; + } + return []; + }) + .catch(() => []) + ); + + const allResults = await Promise.all(queries); + const endTime = performance.now(); + setQueryTime(endTime - startTime); + + // 合并所有结果并去重 + const combinedResults = allResults.flat(); + + if (combinedResults.length > 0) { + // 按记录类型分组并去重 + const uniqueResults = Array.from( + new Map( + combinedResults.map((record) => [ + `${record.name}-${record.type}-${record.data}`, + record, + ]) + ).values() + ); + + setResults(uniqueResults); + toast.success(`查询成功,找到 ${uniqueResults.length} 条记录`); + } else { + setResults([]); + toast.info("未找到记录"); + } + } catch (error: unknown) { + if (error instanceof Error) { + toast.error(`查询失败: ${error.message}`); + } else { + toast.error("查询失败"); + } + setResults([]); + } finally { + setLoading(false); + } + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !loading) { + queryDNS(); + } + }; + + return ( +
+
+
+ + setDomain(e.target.value)} + onKeyPress={handleKeyPress} + disabled={loading} + /> + + 将自动查询所有类型的 DNS 记录 + +
+ + +
+ + {queryTime > 0 && ( +
+ 查询耗时: {queryTime.toFixed(2)} ms +
+ )} + + {results.length > 0 && ( +
+
查询结果:
+
+ {results.map((record, index) => ( +
+
+
名称:
+
{record.name}
+
类型:
+
{getRecordTypeName(record.type)}
+
TTL:
+
{record.TTL} 秒
+
数据:
+
{record.data}
+
+
+ ))} +
+
+ )} +
+ ); +}; + +export default Tool; + diff --git a/src/components/tool/network/index.tsx b/src/components/tool/network/index.tsx new file mode 100644 index 0000000..380f532 --- /dev/null +++ b/src/components/tool/network/index.tsx @@ -0,0 +1,5 @@ +export { default as DNS } from './dns'; +export { default as Ping } from './ping'; +export { default as TCPing } from './tcping'; +export { default as SpeedTest } from './speedtest'; + diff --git a/src/components/tool/network/ping.tsx b/src/components/tool/network/ping.tsx new file mode 100644 index 0000000..a5ba320 --- /dev/null +++ b/src/components/tool/network/ping.tsx @@ -0,0 +1,261 @@ +import { useState, useEffect, useRef, type FC } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { toast } from "sonner"; +import { Loader2 } from "lucide-react"; + +interface PingResult { + seq: number; + time: number; + success: boolean; + error?: string; +} + +interface PingStats { + sent: number; + received: number; + lost: number; + min: number; + max: number; + avg: number; +} + +const Tool: FC = () => { + const [url, setUrl] = useState(""); + const [running, setRunning] = useState(false); + const [results, setResults] = useState([]); + const [stats, setStats] = useState({ + sent: 0, + received: 0, + lost: 0, + min: 0, + max: 0, + avg: 0, + }); + const intervalRef = useRef(null); + const seqRef = useRef(0); + const resultsContainerRef = useRef(null); + + const ping = async () => { + if (!url.trim()) { + toast.error("请输入 URL"); + return; + } + + const seq = ++seqRef.current; + let targetUrl = url.trim(); + + // 如果没有协议前缀,默认使用 https:// + if (!targetUrl.startsWith("http://") && !targetUrl.startsWith("https://")) { + targetUrl = `https://${targetUrl}`; + } + + const startTime = performance.now(); + + try { + await fetch(targetUrl, { + method: "HEAD", + mode: "no-cors", + cache: "no-cache", + }); + + const endTime = performance.now(); + const time = endTime - startTime; + + const newResult: PingResult = { + seq, + time, + success: true, + }; + + setResults((prev) => [...prev, newResult]); + updateStats(newResult); + } catch (error: unknown) { + const endTime = performance.now(); + const time = endTime - startTime; + + const errorMessage = + error instanceof Error ? error.message : "请求失败"; + + const newResult: PingResult = { + seq, + time, + success: false, + error: errorMessage, + }; + + setResults((prev) => [...prev, newResult]); + updateStats(newResult); + } + }; + + const updateStats = (newResult: PingResult) => { + setStats((prev) => { + const sent = prev.sent + 1; + const received = newResult.success ? prev.received + 1 : prev.received; + const lost = sent - received; + + let min = prev.min; + let max = prev.max; + let avg = prev.avg; + + if (newResult.success) { + if (received === 1) { + min = newResult.time; + max = newResult.time; + avg = newResult.time; + } else { + min = Math.min(min, newResult.time); + max = Math.max(max, newResult.time); + avg = (prev.avg * (received - 1) + newResult.time) / received; + } + } + + return { sent, received, lost, min, max, avg }; + }); + }; + + const startPing = () => { + if (!url.trim()) { + toast.error("请输入 URL"); + return; + } + + setRunning(true); + setResults([]); + setStats({ + sent: 0, + received: 0, + lost: 0, + min: 0, + max: 0, + avg: 0, + }); + seqRef.current = 0; + + // 立即执行第一次 ping + ping(); + + // 然后每秒执行一次 + intervalRef.current = window.setInterval(ping, 1000); + }; + + const stopPing = () => { + setRunning(false); + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + }; + + useEffect(() => { + // 自动滚动到底部 + if (resultsContainerRef.current) { + resultsContainerRef.current.scrollTop = + resultsContainerRef.current.scrollHeight; + } + }, [results]); + + useEffect(() => { + // 清理定时器 + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }; + }, []); + + const lossRate = + stats.sent > 0 ? ((stats.lost / stats.sent) * 100).toFixed(1) : "0.0"; + + return ( +
+
+
+ + setUrl(e.target.value)} + disabled={running} + /> +
+ +
+ {!running ? ( + + ) : ( + + )} +
+
+ + {stats.sent > 0 && ( +
+
统计信息
+
+
已发送:
+
{stats.sent} 包
+
已接收:
+
{stats.received} 包
+
丢失:
+
+ {stats.lost} 包 ({lossRate}%) +
+ {stats.received > 0 && ( + <> +
最小延迟:
+
{stats.min.toFixed(2)} ms
+
最大延迟:
+
{stats.max.toFixed(2)} ms
+
平均延迟:
+
{stats.avg.toFixed(2)} ms
+ + )} +
+
+ )} + + {results.length > 0 && ( +
+
Ping 结果:
+
+ {results.map((result) => ( +
+ {result.success ? ( + <> + seq={result.seq} time={result.time.toFixed(2)}ms + + ) : ( + <> + seq={result.seq} 请求超时 + {result.error && ` (${result.error})`} + + )} +
+ ))} + {running && ( +
+ + 运行中... +
+ )} +
+
+ )} +
+ ); +}; + +export default Tool; + diff --git a/src/components/tool/network/speedtest.tsx b/src/components/tool/network/speedtest.tsx new file mode 100644 index 0000000..0626ce7 --- /dev/null +++ b/src/components/tool/network/speedtest.tsx @@ -0,0 +1,338 @@ +import { useState, type FC } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { toast } from "sonner"; +import { Loader2 } from "lucide-react"; + +interface PerformanceMetrics { + dns: number; + tcp: number; + ssl: number; + ttfb: number; + download: number; + total: number; +} + +interface SpeedTestResult { + downloadSpeed?: number; + uploadSpeed?: number; + performance?: PerformanceMetrics; +} + +const Tool: FC = () => { + const [url, setUrl] = useState(""); + const [testType, setTestType] = useState<"performance" | "download" | "upload">( + "performance" + ); + const [testing, setTesting] = useState(false); + const [result, setResult] = useState(null); + + const testPerformance = async (targetUrl: string) => { + // 清除之前的性能数据 + performance.clearResourceTimings(); + + const startTime = performance.now(); + + try { + const response = await fetch(targetUrl, { + cache: "no-cache", + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + // 等待内容加载完成 + await response.blob(); + + const endTime = performance.now(); + + // 获取性能数据 + const perfEntries = performance.getEntriesByType( + "resource" + ) as PerformanceResourceTiming[]; + const entry = perfEntries.find((e) => e.name === targetUrl); + + if (entry) { + const metrics: PerformanceMetrics = { + dns: entry.domainLookupEnd - entry.domainLookupStart, + tcp: entry.connectEnd - entry.connectStart, + ssl: + entry.secureConnectionStart > 0 + ? entry.connectEnd - entry.secureConnectionStart + : 0, + ttfb: entry.responseStart - entry.requestStart, + download: entry.responseEnd - entry.responseStart, + total: entry.responseEnd - entry.startTime, + }; + + return { performance: metrics }; + } else { + // 如果没有详细的性能数据,只返回总时间 + return { + performance: { + dns: 0, + tcp: 0, + ssl: 0, + ttfb: 0, + download: 0, + total: endTime - startTime, + }, + }; + } + } catch (error) { + throw error; + } + }; + + const testDownloadSpeed = async (targetUrl: string) => { + const startTime = performance.now(); + + try { + const response = await fetch(targetUrl, { + cache: "no-cache", + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const blob = await response.blob(); + const endTime = performance.now(); + + const fileSizeBytes = blob.size; + const durationSeconds = (endTime - startTime) / 1000; + const speedMbps = (fileSizeBytes * 8) / (durationSeconds * 1000000); + + return { downloadSpeed: speedMbps }; + } catch (error) { + throw error; + } + }; + + const testUploadSpeed = async (targetUrl: string) => { + // 生成 1MB 的测试数据 + const testData = new Uint8Array(1024 * 1024); + for (let i = 0; i < testData.length; i++) { + testData[i] = Math.floor(Math.random() * 256); + } + + const startTime = performance.now(); + + try { + const response = await fetch(targetUrl, { + method: "POST", + body: testData, + cache: "no-cache", + }); + + const endTime = performance.now(); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const fileSizeBytes = testData.length; + const durationSeconds = (endTime - startTime) / 1000; + const speedMbps = (fileSizeBytes * 8) / (durationSeconds * 1000000); + + return { uploadSpeed: speedMbps }; + } catch (error) { + throw error; + } + }; + + const startTest = async () => { + if (!url.trim()) { + toast.error("请输入 URL"); + return; + } + + let targetUrl = url.trim(); + + // 如果没有协议前缀,默认使用 https:// + if (!targetUrl.startsWith("http://") && !targetUrl.startsWith("https://")) { + targetUrl = `https://${targetUrl}`; + } + + setTesting(true); + setResult(null); + + try { + let testResult: SpeedTestResult = {}; + + switch (testType) { + case "performance": + testResult = await testPerformance(targetUrl); + toast.success("性能测试完成"); + break; + case "download": + testResult = await testDownloadSpeed(targetUrl); + toast.success("下载速度测试完成"); + break; + case "upload": + testResult = await testUploadSpeed(targetUrl); + toast.success("上传速度测试完成"); + break; + } + + setResult(testResult); + } catch (error: unknown) { + if (error instanceof Error) { + toast.error(`测试失败: ${error.message}`); + } else { + toast.error("测试失败"); + } + } finally { + setTesting(false); + } + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !testing) { + startTest(); + } + }; + + return ( +
+
+
+ ⚠️ CORS 限制说明 +
+
+

由于浏览器的 CORS 安全策略,部分网站可能无法直接测试。

+

建议测试以下类型的网站:

+
    +
  • 支持 CORS 的公共 API
  • +
  • 您自己控制的网站(可配置 CORS 头)
  • +
  • 使用 CORS 代理服务
  • +
+
+
+ +
+
+ + setUrl(e.target.value)} + onKeyPress={handleKeyPress} + disabled={testing} + /> +
+ +
+ + +
+ + +
+ + {result && ( +
+
测试结果:
+ + {result.performance && ( +
+
页面加载性能
+
+ {result.performance.dns > 0 && ( +
+
DNS 查询:
+
{result.performance.dns.toFixed(2)} ms
+
+ )} + {result.performance.tcp > 0 && ( +
+
TCP 连接:
+
{result.performance.tcp.toFixed(2)} ms
+
+ )} + {result.performance.ssl > 0 && ( +
+
SSL 握手:
+
{result.performance.ssl.toFixed(2)} ms
+
+ )} + {result.performance.ttfb > 0 && ( +
+
首字节时间 (TTFB):
+
{result.performance.ttfb.toFixed(2)} ms
+
+ )} + {result.performance.download > 0 && ( +
+
内容下载:
+
{result.performance.download.toFixed(2)} ms
+
+ )} +
+
总时间:
+
+ {result.performance.total.toFixed(2)} ms +
+
+
+
+ )} + + {result.downloadSpeed !== undefined && ( +
+
下载速度
+
+ {result.downloadSpeed.toFixed(2)} Mbps +
+
+ {(result.downloadSpeed / 8).toFixed(2)} MB/s +
+
+ )} + + {result.uploadSpeed !== undefined && ( +
+
上传速度
+
+ {result.uploadSpeed.toFixed(2)} Mbps +
+
+ {(result.uploadSpeed / 8).toFixed(2)} MB/s +
+
+ )} +
+ )} + + {testType === "download" && ( +
+ 提示: 下载速度测试会下载目标 URL 的内容并计算速度 +
+ )} + + {testType === "upload" && ( +
+ 提示: 上传速度测试会向目标 URL 发送 1MB 测试数据 +
+ )} +
+ ); +}; + +export default Tool; + diff --git a/src/components/tool/network/tcping.tsx b/src/components/tool/network/tcping.tsx new file mode 100644 index 0000000..bc98050 --- /dev/null +++ b/src/components/tool/network/tcping.tsx @@ -0,0 +1,285 @@ +import { useState, useEffect, useRef, type FC } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { toast } from "sonner"; +import { Loader2 } from "lucide-react"; + +interface TCPingResult { + seq: number; + time: number; + success: boolean; + error?: string; +} + +interface TCPingStats { + sent: number; + received: number; + lost: number; + min: number; + max: number; + avg: number; +} + +const Tool: FC = () => { + const [host, setHost] = useState(""); + const [port, setPort] = useState("443"); + const [running, setRunning] = useState(false); + const [results, setResults] = useState([]); + const [stats, setStats] = useState({ + sent: 0, + received: 0, + lost: 0, + min: 0, + max: 0, + avg: 0, + }); + const intervalRef = useRef(null); + const seqRef = useRef(0); + const resultsContainerRef = useRef(null); + + const tcping = async () => { + if (!host.trim()) { + toast.error("请输入主机名或 IP"); + return; + } + + const seq = ++seqRef.current; + const portNum = parseInt(port) || 443; + let targetUrl = host.trim(); + + // 移除协议前缀 + targetUrl = targetUrl.replace(/^https?:\/\//, ""); + + // 构建测试 URL + const protocol = portNum === 443 ? "https" : "http"; + const url = `${protocol}://${targetUrl}:${portNum}`; + + const startTime = performance.now(); + + try { + // 使用 fetch 测试连接 + await fetch(url, { + method: "HEAD", + mode: "no-cors", + cache: "no-cache", + }); + + const endTime = performance.now(); + const time = endTime - startTime; + + const newResult: TCPingResult = { + seq, + time, + success: true, + }; + + setResults((prev) => [...prev, newResult]); + updateStats(newResult); + } catch (error: unknown) { + const endTime = performance.now(); + const time = endTime - startTime; + + const errorMessage = + error instanceof Error ? error.message : "连接失败"; + + const newResult: TCPingResult = { + seq, + time, + success: false, + error: errorMessage, + }; + + setResults((prev) => [...prev, newResult]); + updateStats(newResult); + } + }; + + const updateStats = (newResult: TCPingResult) => { + setStats((prev) => { + const sent = prev.sent + 1; + const received = newResult.success ? prev.received + 1 : prev.received; + const lost = sent - received; + + let min = prev.min; + let max = prev.max; + let avg = prev.avg; + + if (newResult.success) { + if (received === 1) { + min = newResult.time; + max = newResult.time; + avg = newResult.time; + } else { + min = Math.min(min, newResult.time); + max = Math.max(max, newResult.time); + avg = (prev.avg * (received - 1) + newResult.time) / received; + } + } + + return { sent, received, lost, min, max, avg }; + }); + }; + + const startTCPing = () => { + if (!host.trim()) { + toast.error("请输入主机名或 IP"); + return; + } + + setRunning(true); + setResults([]); + setStats({ + sent: 0, + received: 0, + lost: 0, + min: 0, + max: 0, + avg: 0, + }); + seqRef.current = 0; + + // 立即执行第一次 tcping + tcping(); + + // 然后每秒执行一次 + intervalRef.current = window.setInterval(tcping, 1000); + }; + + const stopTCPing = () => { + setRunning(false); + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + }; + + useEffect(() => { + // 自动滚动到底部 + if (resultsContainerRef.current) { + resultsContainerRef.current.scrollTop = + resultsContainerRef.current.scrollHeight; + } + }, [results]); + + useEffect(() => { + // 清理定时器 + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }; + }, []); + + const lossRate = + stats.sent > 0 ? ((stats.lost / stats.sent) * 100).toFixed(1) : "0.0"; + + return ( +
+
+
+ + setHost(e.target.value)} + disabled={running} + /> +
+ +
+ + setPort(e.target.value)} + disabled={running} + min="1" + max="65535" + /> +
+ +
+ {!running ? ( + + ) : ( + + )} +
+
+ + {stats.sent > 0 && ( +
+
统计信息
+
+
已发送:
+
{stats.sent} 次
+
成功:
+
{stats.received} 次
+
失败:
+
+ {stats.lost} 次 ({lossRate}%) +
+ {stats.received > 0 && ( + <> +
最小延迟:
+
{stats.min.toFixed(2)} ms
+
最大延迟:
+
{stats.max.toFixed(2)} ms
+
平均延迟:
+
{stats.avg.toFixed(2)} ms
+
端口状态:
+
开放
+ + )} +
+
+ )} + + {results.length > 0 && ( +
+
TCPing 结果:
+
+ {results.map((result) => ( +
+ {result.success ? ( + <> + seq={result.seq} port={port} time={result.time.toFixed(2)}ms + + ) : ( + <> + seq={result.seq} port={port} 连接失败 + {result.error && ` (${result.error})`} + + )} +
+ ))} + {running && ( +
+ + 运行中... +
+ )} +
+
+ )} +
+ ); +}; + +export default Tool; + -- 2.49.1 From 3f7c81e89221c9b1b4294adcf41b8e7d5b0e3fd1 Mon Sep 17 00:00:00 2001 From: typist Date: Wed, 29 Oct 2025 05:45:09 +0800 Subject: [PATCH 5/6] feat: refactor AppSidebar to support nested tool menus with collapsible items - Enhanced the AppSidebar component to recursively render tools with potential submenus. - Introduced a buildFullPath function for dynamic routing based on tool hierarchy. - Utilized the Collapsible component for better organization of tools with children. - Maintained existing footer and header structure for consistency. --- src/components/sidebar/index.tsx | 130 ++++++++++++++++++++++--------- 1 file changed, 94 insertions(+), 36 deletions(-) diff --git a/src/components/sidebar/index.tsx b/src/components/sidebar/index.tsx index a6dde77..a43c742 100644 --- a/src/components/sidebar/index.tsx +++ b/src/components/sidebar/index.tsx @@ -1,40 +1,98 @@ -import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar"; -import { tools } from "@/components/tool"; +import type { ReactNode } from "react"; +import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarMenuButton, SidebarMenuItem, SidebarMenu, SidebarMenuSub, SidebarMenuSubItem, SidebarMenuSubButton } from "@/components/ui/sidebar"; +import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible"; +import { tools, type Tool } from "@/components/tool"; import { Link } from "react-router-dom"; import { ModeToggle } from "@/components/theme/toggle"; import { Button } from "../ui/button"; +import { ChevronRight } from "lucide-react"; -export const AppSidebar = () => ( - - - Lite Kit - - - - - Tools - - - { - tools.map((tool) => ( - - - - {tool.icon} - {tool.name} - - - - )) - } - - - - - - - - -) \ No newline at end of file +export const AppSidebar = () => { + // 递归构建完整路径 + const buildFullPath = (pathSegments: string[]): string => { + return `/tool/${pathSegments.join("/")}`; + }; + + // 递归渲染菜单项 + const renderMenuItem = (tool: Tool, parentPaths: string[] = []): ReactNode => { + const currentPaths = [...parentPaths, tool.path]; + + if (tool.children) { + // 有子菜单的项目 + return ( + + + + + {tool.icon} + {tool.name} + + + + + + {tool.children.map((child) => ( + + {child.children ? ( + renderMenuItem(child, currentPaths) + ) : ( + + + {child.icon} + {child.name} + + + )} + + ))} + + + + + ); + } + + // 没有子菜单的项目 + return ( + + + + {tool.icon} + {tool.name} + + + + ); + }; + + return ( + + + Lite Kit + + + + Tools + + + {tools.map((tool) => renderMenuItem(tool))} + + + + + + + + + + ); +}; \ No newline at end of file -- 2.49.1 From 82c68471df214a4c95db42a67c9df62278b9f645 Mon Sep 17 00:00:00 2001 From: typist Date: Wed, 29 Oct 2025 05:51:10 +0800 Subject: [PATCH 6/6] refactor: update network tool components for improved user experience - Translated UI text in DNS, Ping, TCPing, and Speed Test components from Chinese to English for better accessibility. - Enhanced user prompts and labels for clarity, including error messages and button texts. - Updated comments in the code to reflect the changes in language and improve code readability. --- src/components/tool/network/dns.tsx | 56 +++++++++---------- src/components/tool/network/ping.tsx | 50 ++++++++--------- src/components/tool/network/speedtest.tsx | 66 +++++++++++------------ src/components/tool/network/tcping.tsx | 48 ++++++++--------- 4 files changed, 110 insertions(+), 110 deletions(-) diff --git a/src/components/tool/network/dns.tsx b/src/components/tool/network/dns.tsx index 6659e40..cae5ca1 100644 --- a/src/components/tool/network/dns.tsx +++ b/src/components/tool/network/dns.tsx @@ -18,16 +18,16 @@ interface DNSResponse { } const DNS_RECORD_TYPES = [ - { value: "1", label: "A", description: "IPv4 地址" }, - { value: "28", label: "AAAA", description: "IPv6 地址" }, - { value: "5", label: "CNAME", description: "规范名称" }, - { value: "15", label: "MX", description: "邮件交换" }, - { value: "2", label: "NS", description: "名称服务器" }, - { value: "16", label: "TXT", description: "文本记录" }, - { value: "6", label: "SOA", description: "授权起始" }, - { value: "257", label: "CAA", description: "证书颁发机构授权" }, - { value: "12", label: "PTR", description: "指针记录" }, - { value: "33", label: "SRV", description: "服务记录" }, + { value: "1", label: "A", description: "IPv4 Address" }, + { value: "28", label: "AAAA", description: "IPv6 Address" }, + { value: "5", label: "CNAME", description: "Canonical Name" }, + { value: "15", label: "MX", description: "Mail Exchange" }, + { value: "2", label: "NS", description: "Name Server" }, + { value: "16", label: "TXT", description: "Text Record" }, + { value: "6", label: "SOA", description: "Start of Authority" }, + { value: "257", label: "CAA", description: "Certification Authority Authorization" }, + { value: "12", label: "PTR", description: "Pointer Record" }, + { value: "33", label: "SRV", description: "Service Record" }, ]; const getRecordTypeName = (type: number): string => { @@ -43,7 +43,7 @@ const Tool: FC = () => { const queryDNS = async () => { if (!domain.trim()) { - toast.error("请输入域名"); + toast.error("Please enter a domain name"); return; } @@ -54,7 +54,7 @@ const Tool: FC = () => { const startTime = performance.now(); try { - // 并发查询所有记录类型 + // Query all record types concurrently const queries = DNS_RECORD_TYPES.map((recordType) => fetch( `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent( @@ -80,11 +80,11 @@ const Tool: FC = () => { const endTime = performance.now(); setQueryTime(endTime - startTime); - // 合并所有结果并去重 + // Merge and deduplicate results const combinedResults = allResults.flat(); if (combinedResults.length > 0) { - // 按记录类型分组并去重 + // Group by record type and deduplicate const uniqueResults = Array.from( new Map( combinedResults.map((record) => [ @@ -95,16 +95,16 @@ const Tool: FC = () => { ); setResults(uniqueResults); - toast.success(`查询成功,找到 ${uniqueResults.length} 条记录`); + toast.success(`Query successful, found ${uniqueResults.length} record(s)`); } else { setResults([]); - toast.info("未找到记录"); + toast.info("No records found"); } } catch (error: unknown) { if (error instanceof Error) { - toast.error(`查询失败: ${error.message}`); + toast.error(`Query failed: ${error.message}`); } else { - toast.error("查询失败"); + toast.error("Query failed"); } setResults([]); } finally { @@ -122,34 +122,34 @@ const Tool: FC = () => {
- + setDomain(e.target.value)} onKeyPress={handleKeyPress} disabled={loading} /> - 将自动查询所有类型的 DNS 记录 + Will automatically query all DNS record types
{queryTime > 0 && (
- 查询耗时: {queryTime.toFixed(2)} ms + Query time: {queryTime.toFixed(2)} ms
)} {results.length > 0 && (
-
查询结果:
+
Query Results:
{results.map((record, index) => (
{ className="border rounded-md p-3 bg-card text-card-foreground" >
-
名称:
+
Name:
{record.name}
-
类型:
+
Type:
{getRecordTypeName(record.type)}
TTL:
-
{record.TTL} 秒
-
数据:
+
{record.TTL} seconds
+
Data:
{record.data}
diff --git a/src/components/tool/network/ping.tsx b/src/components/tool/network/ping.tsx index a5ba320..378226e 100644 --- a/src/components/tool/network/ping.tsx +++ b/src/components/tool/network/ping.tsx @@ -38,14 +38,14 @@ const Tool: FC = () => { const ping = async () => { if (!url.trim()) { - toast.error("请输入 URL"); + toast.error("Please enter a URL"); return; } const seq = ++seqRef.current; let targetUrl = url.trim(); - // 如果没有协议前缀,默认使用 https:// + // If no protocol prefix, default to https:// if (!targetUrl.startsWith("http://") && !targetUrl.startsWith("https://")) { targetUrl = `https://${targetUrl}`; } @@ -75,7 +75,7 @@ const Tool: FC = () => { const time = endTime - startTime; const errorMessage = - error instanceof Error ? error.message : "请求失败"; + error instanceof Error ? error.message : "Request failed"; const newResult: PingResult = { seq, @@ -117,7 +117,7 @@ const Tool: FC = () => { const startPing = () => { if (!url.trim()) { - toast.error("请输入 URL"); + toast.error("Please enter a URL"); return; } @@ -133,10 +133,10 @@ const Tool: FC = () => { }); seqRef.current = 0; - // 立即执行第一次 ping + // Execute first ping immediately ping(); - // 然后每秒执行一次 + // Then execute every second intervalRef.current = window.setInterval(ping, 1000); }; @@ -149,7 +149,7 @@ const Tool: FC = () => { }; useEffect(() => { - // 自动滚动到底部 + // Auto-scroll to bottom if (resultsContainerRef.current) { resultsContainerRef.current.scrollTop = resultsContainerRef.current.scrollHeight; @@ -157,7 +157,7 @@ const Tool: FC = () => { }, [results]); useEffect(() => { - // 清理定时器 + // Cleanup timer return () => { if (intervalRef.current) { clearInterval(intervalRef.current); @@ -172,9 +172,9 @@ const Tool: FC = () => {
- + setUrl(e.target.value)} disabled={running} @@ -184,11 +184,11 @@ const Tool: FC = () => {
{!running ? ( ) : ( )}
@@ -196,23 +196,23 @@ const Tool: FC = () => { {stats.sent > 0 && (
-
统计信息
+
Statistics
-
已发送:
-
{stats.sent} 包
-
已接收:
-
{stats.received} 包
-
丢失:
+
Sent:
+
{stats.sent} packets
+
Received:
+
{stats.received} packets
+
Lost:
- {stats.lost} 包 ({lossRate}%) + {stats.lost} packets ({lossRate}%)
{stats.received > 0 && ( <> -
最小延迟:
+
Min Latency:
{stats.min.toFixed(2)} ms
-
最大延迟:
+
Max Latency:
{stats.max.toFixed(2)} ms
-
平均延迟:
+
Avg Latency:
{stats.avg.toFixed(2)} ms
)} @@ -222,7 +222,7 @@ const Tool: FC = () => { {results.length > 0 && (
-
Ping 结果:
+
Ping Results:
{ ) : ( <> - seq={result.seq} 请求超时 + seq={result.seq} Request timeout {result.error && ` (${result.error})`} )} @@ -247,7 +247,7 @@ const Tool: FC = () => { {running && (
- 运行中... + Running...
)}
diff --git a/src/components/tool/network/speedtest.tsx b/src/components/tool/network/speedtest.tsx index 0626ce7..ea1d453 100644 --- a/src/components/tool/network/speedtest.tsx +++ b/src/components/tool/network/speedtest.tsx @@ -68,7 +68,7 @@ const Tool: FC = () => { return { performance: metrics }; } else { - // 如果没有详细的性能数据,只返回总时间 + // If no detailed performance data, only return total time return { performance: { dns: 0, @@ -144,13 +144,13 @@ const Tool: FC = () => { const startTest = async () => { if (!url.trim()) { - toast.error("请输入 URL"); + toast.error("Please enter a URL"); return; } let targetUrl = url.trim(); - // 如果没有协议前缀,默认使用 https:// + // If no protocol prefix, default to https:// if (!targetUrl.startsWith("http://") && !targetUrl.startsWith("https://")) { targetUrl = `https://${targetUrl}`; } @@ -164,24 +164,24 @@ const Tool: FC = () => { switch (testType) { case "performance": testResult = await testPerformance(targetUrl); - toast.success("性能测试完成"); + toast.success("Performance test completed"); break; case "download": testResult = await testDownloadSpeed(targetUrl); - toast.success("下载速度测试完成"); + toast.success("Download speed test completed"); break; case "upload": testResult = await testUploadSpeed(targetUrl); - toast.success("上传速度测试完成"); + toast.success("Upload speed test completed"); break; } setResult(testResult); } catch (error: unknown) { if (error instanceof Error) { - toast.error(`测试失败: ${error.message}`); + toast.error(`Test failed: ${error.message}`); } else { - toast.error("测试失败"); + toast.error("Test failed"); } } finally { setTesting(false); @@ -198,24 +198,24 @@ const Tool: FC = () => {
- ⚠️ CORS 限制说明 + ⚠️ CORS Restrictions
-

由于浏览器的 CORS 安全策略,部分网站可能无法直接测试。

-

建议测试以下类型的网站:

+

Due to browser CORS security policies, some websites cannot be tested directly.

+

Recommended websites to test:

    -
  • 支持 CORS 的公共 API
  • -
  • 您自己控制的网站(可配置 CORS 头)
  • -
  • 使用 CORS 代理服务
  • +
  • Public APIs with CORS support
  • +
  • Your own websites (with configured CORS headers)
  • +
  • Using CORS proxy services
- + setUrl(e.target.value)} onKeyPress={handleKeyPress} @@ -224,7 +224,7 @@ const Tool: FC = () => {
- +
{result && (
-
测试结果:
+
Test Results:
{result.performance && (
-
页面加载性能
+
Page Load Performance
{result.performance.dns > 0 && (
-
DNS 查询:
+
DNS Lookup:
{result.performance.dns.toFixed(2)} ms
)} {result.performance.tcp > 0 && (
-
TCP 连接:
+
TCP Connection:
{result.performance.tcp.toFixed(2)} ms
)} {result.performance.ssl > 0 && (
-
SSL 握手:
+
SSL Handshake:
{result.performance.ssl.toFixed(2)} ms
)} {result.performance.ttfb > 0 && (
-
首字节时间 (TTFB):
+
Time to First Byte (TTFB):
{result.performance.ttfb.toFixed(2)} ms
)} {result.performance.download > 0 && (
-
内容下载:
+
Content Download:
{result.performance.download.toFixed(2)} ms
)}
-
总时间:
+
Total Time:
{result.performance.total.toFixed(2)} ms
@@ -295,7 +295,7 @@ const Tool: FC = () => { {result.downloadSpeed !== undefined && (
-
下载速度
+
Download Speed
{result.downloadSpeed.toFixed(2)} Mbps
@@ -307,7 +307,7 @@ const Tool: FC = () => { {result.uploadSpeed !== undefined && (
-
上传速度
+
Upload Speed
{result.uploadSpeed.toFixed(2)} Mbps
@@ -321,13 +321,13 @@ const Tool: FC = () => { {testType === "download" && (
- 提示: 下载速度测试会下载目标 URL 的内容并计算速度 + Note: Download speed test will download content from the target URL and calculate speed
)} {testType === "upload" && (
- 提示: 上传速度测试会向目标 URL 发送 1MB 测试数据 + Note: Upload speed test will send 1MB of test data to the target URL
)}
diff --git a/src/components/tool/network/tcping.tsx b/src/components/tool/network/tcping.tsx index bc98050..f94338b 100644 --- a/src/components/tool/network/tcping.tsx +++ b/src/components/tool/network/tcping.tsx @@ -39,7 +39,7 @@ const Tool: FC = () => { const tcping = async () => { if (!host.trim()) { - toast.error("请输入主机名或 IP"); + toast.error("Please enter a hostname or IP"); return; } @@ -80,7 +80,7 @@ const Tool: FC = () => { const time = endTime - startTime; const errorMessage = - error instanceof Error ? error.message : "连接失败"; + error instanceof Error ? error.message : "Connection failed"; const newResult: TCPingResult = { seq, @@ -122,7 +122,7 @@ const Tool: FC = () => { const startTCPing = () => { if (!host.trim()) { - toast.error("请输入主机名或 IP"); + toast.error("Please enter a hostname or IP"); return; } @@ -177,9 +177,9 @@ const Tool: FC = () => {
- + setHost(e.target.value)} disabled={running} @@ -187,10 +187,10 @@ const Tool: FC = () => {
- + setPort(e.target.value)} disabled={running} @@ -202,7 +202,7 @@ const Tool: FC = () => {
{!running ? ( ) : ( )}
@@ -218,26 +218,26 @@ const Tool: FC = () => { {stats.sent > 0 && (
-
统计信息
+
Statistics
-
已发送:
-
{stats.sent} 次
-
成功:
-
{stats.received} 次
-
失败:
+
Sent:
+
{stats.sent} times
+
Success:
+
{stats.received} times
+
Failed:
- {stats.lost} 次 ({lossRate}%) + {stats.lost} times ({lossRate}%)
{stats.received > 0 && ( <> -
最小延迟:
+
Min Latency:
{stats.min.toFixed(2)} ms
-
最大延迟:
+
Max Latency:
{stats.max.toFixed(2)} ms
-
平均延迟:
+
Avg Latency:
{stats.avg.toFixed(2)} ms
-
端口状态:
-
开放
+
Port Status:
+
Open
)}
@@ -246,7 +246,7 @@ const Tool: FC = () => { {results.length > 0 && (
-
TCPing 结果:
+
TCPing Results:
{ ) : ( <> - seq={result.seq} port={port} 连接失败 + seq={result.seq} port={port} Connection failed {result.error && ` (${result.error})`} )} @@ -271,7 +271,7 @@ const Tool: FC = () => { {running && (
- 运行中... + Running...
)}
-- 2.49.1