/** * ConnectionPanel - 连接状态管理面板 * * 右侧滑出面板,实时监控所有连接状态、流量统计、支持重连/断开操作。 */ import { useState, useCallback, useMemo } from 'react' import { Wifi, WifiOff, RefreshCw, X, Clock, HardDrive, Activity, } from 'lucide-react' import { useConnectionStore } from '../stores/connection' import { usePoolStore } from '../stores/connection/poolStore' import { useConnectionPanelStore } from '../stores/connectionPanelStore' import type { PooledConnection, ConnectionState } from '../stores/connection/types' import EmptyState from './common/EmptyState' // ============== Helpers ============== /** 格式化字节 */ function formatBytes(bytes: number): string { if (bytes === 0) return '0 B' const units = ['B', 'KB', 'MB', 'GB'] const k = 1024 const i = Math.floor(Math.log(bytes) / Math.log(k)) const value = bytes / Math.pow(k, i) return `${value.toFixed(i === 0 ? 0 : 1)} ${units[i]}` } /** 格式化连接时长 (ms → readable) */ function formatDuration(ms: number): string { const seconds = Math.floor(ms / 1000) if (seconds < 60) return `${seconds}s` const minutes = Math.floor(seconds / 60) if (minutes < 60) return `${minutes}m ${seconds % 60}s` const hours = Math.floor(minutes / 60) return `${hours}h ${minutes % 60}m` } // ============== Status Dot ============== interface StatusDotProps { state: ConnectionState } const STATUS_DOT_COLORS: Record = { connected: 'bg-green-400', connecting: 'bg-yellow-400 animate-pulse', background: 'bg-green-400 opacity-60', suspended: 'bg-gray-400', error: 'bg-red-400', disconnected: 'bg-gray-500', reconnecting: 'bg-yellow-400 animate-pulse', } function StatusDot({ state }: StatusDotProps) { return ( ) } // ============== Status Label ============== const STATUS_LABELS: Record = { connected: '已连接', connecting: '连接中', background: '后台', suspended: '已挂起', error: '错误', disconnected: '已断开', reconnecting: '重连中', } // ============== Connection Card ============== interface ConnectionCardProps { conn: PooledConnection onReconnect: (id: string) => void onDisconnect: (id: string) => void reconnectingId: string | null } function ConnectionCard({ conn, onReconnect, onDisconnect, reconnectingId }: ConnectionCardProps) { const isReconnecting = reconnectingId === conn.connectionId const canReconnect = conn.state === 'suspended' || conn.state === 'error' || conn.state === 'disconnected' const canDisconnect = conn.state !== 'disconnected' && conn.state !== 'error' const durationMs = Date.now() - conn.stats.connectedAt const sessionCount = conn.shellIds.size + conn.sftpIds.size return (
{/* Header: host + status */}
{conn.config.username}@{conn.config.host}
{STATUS_LABELS[conn.state]}
{/* Details */}
{/* Port */} :{conn.config.port ?? 22} {/* Duration */} {formatDuration(durationMs)} {/* Transfer */} {conn.stats.bytesTransferred > 0 && ( {formatBytes(conn.stats.bytesTransferred)} )} {/* Sessions */} {sessionCount > 0 && ( {conn.shellIds.size} shell{conn.sftpIds.size > 0 ? ` / ${conn.sftpIds.size} sftp` : ''} )} {/* Ref count */} {conn.refCount > 1 && ×{conn.refCount} 引用}
{/* Actions */}
{canReconnect && ( )} {canDisconnect && ( )}
{/* Error message */} {conn.error && (

{conn.error}

)}
) } // ============== Stats Overview ============== interface StatsBarProps { total: number connected: number background: number error: number } function StatsOverview({ total, connected, background, error }: StatsBarProps) { const items = [ { label: '总计', value: total, color: 'text-gray-300' }, { label: '活跃', value: connected, color: 'text-green-400' }, { label: '后台', value: background, color: 'text-yellow-400' }, { label: '断开/错误', value: error, color: 'text-red-400' }, ] return (
{items.map((item) => (
{item.value}
{item.label}
))}
) } // ============== Main Panel ============== export default function ConnectionPanel() { const isOpen = useConnectionPanelStore((s) => s.isOpen) const close = useConnectionPanelStore((s) => s.close) const connections = usePoolStore((s) => s.connections) const { resumeConnection, disconnectConnection, getStats, } = useConnectionStore() const [reconnectingId, setReconnectingId] = useState(null) // Derive connections list and stats const { connList, stats } = useMemo(() => { const list = Array.from(connections.values()) const s = getStats() return { connList: list, stats: s } }, [connections, getStats]) // Handlers const handleReconnect = useCallback( async (connectionId: string) => { setReconnectingId(connectionId) try { await resumeConnection(connectionId) } finally { setReconnectingId(null) } }, [resumeConnection], ) const handleDisconnect = useCallback( async (connectionId: string) => { await disconnectConnection(connectionId) }, [disconnectConnection], ) if (!isOpen) return null return ( <> {/* Backdrop */}
{/* Panel */}
{/* Header */}

连接管理

{/* Stats Overview */}
{/* Connection List */}
{connList.length === 0 ? ( ) : ( connList.map((conn) => ( )) )}
) }