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; +