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.
This commit is contained in:
261
src/components/tool/network/ping.tsx
Normal file
261
src/components/tool/network/ping.tsx
Normal file
@@ -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<string>("");
|
||||
const [running, setRunning] = useState<boolean>(false);
|
||||
const [results, setResults] = useState<PingResult[]>([]);
|
||||
const [stats, setStats] = useState<PingStats>({
|
||||
sent: 0,
|
||||
received: 0,
|
||||
lost: 0,
|
||||
min: 0,
|
||||
max: 0,
|
||||
avg: 0,
|
||||
});
|
||||
const intervalRef = useRef<number | null>(null);
|
||||
const seqRef = useRef<number>(0);
|
||||
const resultsContainerRef = useRef<HTMLDivElement>(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 (
|
||||
<div className="flex flex-col gap-4 h-full">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">目标 URL 或 IP</label>
|
||||
<Input
|
||||
placeholder="例如: example.com 或 https://example.com"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
disabled={running}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{!running ? (
|
||||
<Button onClick={startPing} className="flex-1">
|
||||
开始 Ping
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={stopPing} variant="destructive" className="flex-1">
|
||||
停止
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{stats.sent > 0 && (
|
||||
<div className="border rounded-md p-3 bg-card text-card-foreground">
|
||||
<div className="text-sm font-medium mb-2">统计信息</div>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div className="text-muted-foreground">已发送:</div>
|
||||
<div>{stats.sent} 包</div>
|
||||
<div className="text-muted-foreground">已接收:</div>
|
||||
<div>{stats.received} 包</div>
|
||||
<div className="text-muted-foreground">丢失:</div>
|
||||
<div>
|
||||
{stats.lost} 包 ({lossRate}%)
|
||||
</div>
|
||||
{stats.received > 0 && (
|
||||
<>
|
||||
<div className="text-muted-foreground">最小延迟:</div>
|
||||
<div>{stats.min.toFixed(2)} ms</div>
|
||||
<div className="text-muted-foreground">最大延迟:</div>
|
||||
<div>{stats.max.toFixed(2)} ms</div>
|
||||
<div className="text-muted-foreground">平均延迟:</div>
|
||||
<div>{stats.avg.toFixed(2)} ms</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{results.length > 0 && (
|
||||
<div className="flex flex-col gap-2 flex-1 overflow-hidden">
|
||||
<div className="text-sm font-medium">Ping 结果:</div>
|
||||
<div
|
||||
ref={resultsContainerRef}
|
||||
className="flex-1 overflow-auto space-y-1 font-mono text-sm border rounded-md p-3 bg-card"
|
||||
>
|
||||
{results.map((result) => (
|
||||
<div
|
||||
key={result.seq}
|
||||
className={result.success ? "text-green-500" : "text-red-500"}
|
||||
>
|
||||
{result.success ? (
|
||||
<>
|
||||
seq={result.seq} time={result.time.toFixed(2)}ms
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
seq={result.seq} 请求超时
|
||||
{result.error && ` (${result.error})`}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{running && (
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Loader2 className="size-3 animate-spin" />
|
||||
运行中...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tool;
|
||||
|
||||
Reference in New Issue
Block a user