import { ConnectionActionMenu } from '@/components/connections/connection-action-menu' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle, } from '@/components/ui/card' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { formatBytes, formatNanoDelay, formatPPS, formatSpeed, } from '@/lib/format' import type { RemoteLinkItem } from '@/types/api' import { ArrowDown, ArrowUp, Check, ChevronsLeftRightEllipsis, Copy, LayoutGrid, List, Network, Plus, Search, Plug, Unplug } from "lucide-react" import { useMemo, useState } from 'react' interface ConnectionsPageProps { links: RemoteLinkItem[] isConnected: boolean onAddLink: (address: string) => Promise<{ success: boolean; message?: string }> onRemoveLink: (address: string) => Promise<{ success: boolean; message?: string }> onLinkAction: ( address: string, action: 'reconnect' | 'disconnect' | 'remove' ) => Promise<{ success: boolean; message?: string }> } export function ConnectionsPage({ links, onAddLink, onRemoveLink, onLinkAction, }: ConnectionsPageProps) { const [viewMode, setViewMode] = useState<'grid' | 'table'>('grid') const [searchQuery, setSearchQuery] = useState('') const [statusFilter, setStatusFilter] = useState<'all' | 'up' | 'down'>('all') // Add Dialog State const [isAddOpen, setIsAddOpen] = useState(false) const [newAddress, setNewAddress] = useState('') const [addError, setAddError] = useState(null) const [isSubmitting, setIsSubmitting] = useState(false) // Copy Feedback state const [copiedAddress, setCopiedAddress] = useState(null) const copyText = (text: string) => { navigator.clipboard.writeText(text) setCopiedAddress(text) setTimeout(() => setCopiedAddress(null), 2000) } // Summary calculations const stats = useMemo(() => { const total = links.length const up = links.filter((l) => l.stateCode === 2 || l.state.toLowerCase().includes('up') || l.state === 'ESTABLISHED').length const down = total - up const totalUpSpeed = links.reduce((acc, l) => acc + (l.upSpeed || 0), 0) const totalDownSpeed = links.reduce((acc, l) => acc + (l.downSpeed || 0), 0) return { total, up, down, totalUpSpeed, totalDownSpeed } }, [links]) // Filtered links const filteredLinks = useMemo(() => { return links.filter((link) => { const matchSearch = searchQuery === '' || link.name.toLowerCase().includes(searchQuery.toLowerCase()) || (link.socketAddress && link.socketAddress.toLowerCase().includes(searchQuery.toLowerCase())) || (link.vaddr && link.vaddr.toLowerCase().includes(searchQuery.toLowerCase())) const isLinkUp = link.stateCode === 2 || link.state.toLowerCase().includes('up') || link.state === 'ESTABLISHED' if (statusFilter === 'up') return matchSearch && isLinkUp if (statusFilter === 'down') return matchSearch && !isLinkUp return matchSearch }) }, [links, searchQuery, statusFilter]) const handleAddSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!newAddress.trim()) { setAddError('请输入远程连接 Socket 地址') return } setIsSubmitting(true) setAddError(null) const res = await onAddLink(newAddress.trim()) setIsSubmitting(false) if (res.success) { setNewAddress('') setIsAddOpen(false) } else { setAddError(res.message || '添加连接失败') } } const getProtocolBadge = (address?: string) => { if (!address) return TCP const lower = address.toLowerCase() if (lower.startsWith('udp://')) { return UDP } if (lower.startsWith('kltp://')) { return KLTP } return TCP } return (
{/* 1. Top Summary Banner */}
全部连接 {stats.total}
已建立连接 {stats.up}
异常/重试中 {stats.down}
实时吞吐
↑ {formatSpeed(stats.totalUpSpeed, 1)} ↓ {formatSpeed(stats.totalDownSpeed, 1)}
{/* 2. Control Toolbar */}
{/* Search bar */}
setSearchQuery(e.target.value)} />
{/* Status filter toggle */}
{/* Action buttons & View mode switch */}
{/* Add Connection Dialog */} 添加连接 } />
添加远程连接 输入远端主机的 Socket 协议地址建立隧道
setNewAddress(e.target.value)} autoFocus /> 格式示例:tcp://ip:portudp://ip:portkltp://ip:port
{addError && (
{addError}
)}
{/* 3. Main Content: Grid or Table View */} {filteredLinks.length === 0 ? (
未发现匹配的连接

当前没有满足条件的远程连接。您可以点击右上角的「添加连接」来新建与远端节点的链路。

) : viewMode === "grid" ? (
{filteredLinks.map((link) => { const isUp = link.stateCode === 2 || link.state.toLowerCase().includes("up") || link.state === "ESTABLISHED" const isUnstable = link.stateCode === 1 || link.state.toLowerCase().includes("unstable") return (
{getProtocolBadge(link.socketAddress || link.name)} {link.socketAddress || link.name}
{/* Actions dropdown component */}
{/* Remote Virtual IPv6 Address */}
远端 SID:
{link.vaddr ? (
{link.vaddr}
) : ( 未协商 )}
{/* Realtime Throughput & PPS */}
发送 {formatPPS(link.upPPS)}
{formatSpeed(link.upSpeed)}
接收 {formatPPS(link.downPPS)}
{formatSpeed(link.downSpeed)}
{/* Delay & Jitter */}
单向时延 (上/下): {isUp ? `${formatNanoDelay(link.upDelay)} / ${formatNanoDelay(link.downDelay)}` : "NaN / NaN"}
网络抖动 (Jitter): {isUp ? formatNanoDelay(link.upJitter) : "NaN"}
累计流量 (发/收): {formatBytes(link.upTraffic, 1)} /{" "} {formatBytes(link.downTraffic, 1)}
) })}
) : ( /* Table View */ 状态 Socket 地址 远端 SID (IPv6) 实时发送 (↑) 实时接收 (↓) 单向时延 累计总流量 操作 {filteredLinks.map((link) => { const isUp = link.stateCode === 2 || link.state.toLowerCase().includes("up") || link.state === "ESTABLISHED" return (
{getProtocolBadge(link.socketAddress || link.name)} {link.socketAddress || link.name}
{link.vaddr || "--"} {formatSpeed(link.upSpeed)} ({formatPPS(link.upPPS)}) {formatSpeed(link.downSpeed)} ({formatPPS(link.downPPS)}) {isUp ? formatNanoDelay(link.upDelay) : "NaN"} {formatBytes( (link.upTraffic || 0) + (link.downTraffic || 0) )}
) })}
)}
) }