 82c68471df
			
		
	
	82c68471df
	
	
	
		
			
			- 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.
		
			
				
	
	
		
			262 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			262 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| 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("Please enter a URL");
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const seq = ++seqRef.current;
 | |
|     let targetUrl = url.trim();
 | |
| 
 | |
|     // If no protocol prefix, default to 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 : "Request failed";
 | |
| 
 | |
|       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("Please enter a URL");
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     setRunning(true);
 | |
|     setResults([]);
 | |
|     setStats({
 | |
|       sent: 0,
 | |
|       received: 0,
 | |
|       lost: 0,
 | |
|       min: 0,
 | |
|       max: 0,
 | |
|       avg: 0,
 | |
|     });
 | |
|     seqRef.current = 0;
 | |
| 
 | |
|     // Execute first ping immediately
 | |
|     ping();
 | |
| 
 | |
|     // Then execute every second
 | |
|     intervalRef.current = window.setInterval(ping, 1000);
 | |
|   };
 | |
| 
 | |
|   const stopPing = () => {
 | |
|     setRunning(false);
 | |
|     if (intervalRef.current) {
 | |
|       clearInterval(intervalRef.current);
 | |
|       intervalRef.current = null;
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   useEffect(() => {
 | |
|     // Auto-scroll to bottom
 | |
|     if (resultsContainerRef.current) {
 | |
|       resultsContainerRef.current.scrollTop =
 | |
|         resultsContainerRef.current.scrollHeight;
 | |
|     }
 | |
|   }, [results]);
 | |
| 
 | |
|   useEffect(() => {
 | |
|     // Cleanup timer
 | |
|     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">Target URL or IP</label>
 | |
|           <Input
 | |
|             placeholder="e.g. example.com or 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">
 | |
|               Start Ping
 | |
|             </Button>
 | |
|           ) : (
 | |
|             <Button onClick={stopPing} variant="destructive" className="flex-1">
 | |
|               Stop
 | |
|             </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">Statistics</div>
 | |
|           <div className="grid grid-cols-2 gap-2 text-sm">
 | |
|             <div className="text-muted-foreground">Sent:</div>
 | |
|             <div>{stats.sent} packets</div>
 | |
|             <div className="text-muted-foreground">Received:</div>
 | |
|             <div>{stats.received} packets</div>
 | |
|             <div className="text-muted-foreground">Lost:</div>
 | |
|             <div>
 | |
|               {stats.lost} packets ({lossRate}%)
 | |
|             </div>
 | |
|             {stats.received > 0 && (
 | |
|               <>
 | |
|                 <div className="text-muted-foreground">Min Latency:</div>
 | |
|                 <div>{stats.min.toFixed(2)} ms</div>
 | |
|                 <div className="text-muted-foreground">Max Latency:</div>
 | |
|                 <div>{stats.max.toFixed(2)} ms</div>
 | |
|                 <div className="text-muted-foreground">Avg Latency:</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 Results:</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} Request timeout
 | |
|                     {result.error && ` (${result.error})`}
 | |
|                   </>
 | |
|                 )}
 | |
|               </div>
 | |
|             ))}
 | |
|             {running && (
 | |
|               <div className="flex items-center gap-2 text-muted-foreground">
 | |
|                 <Loader2 className="size-3 animate-spin" />
 | |
|                 Running...
 | |
|               </div>
 | |
|             )}
 | |
|           </div>
 | |
|         </div>
 | |
|       )}
 | |
|     </div>
 | |
|   );
 | |
| };
 | |
| 
 | |
| export default Tool;
 | |
| 
 |