v1.3.0: 主题系统+代码分割+设置重构+命令片段库+命令面板+连接面板 | 19 files +2141/-257 | tsc零错误 构建成功

This commit is contained in:
OpsXShell Dev committed 2026-04-18 19:25:45 +08:00
1 parent caba5f4c0d
commit 1f2de032a4
19 files changed
+2142 -258

No files matched your search

+335
View File
@@ -0,0 +1,335 @@
/**
* 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<ConnectionState, string> = {
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 (
<span
className={`inline-block w-2 h-2 rounded-full ${STATUS_DOT_COLORS[state] ?? 'bg-gray-500'}`}
/>
)
}
// ============== Status Label ==============
const STATUS_LABELS: Record<ConnectionState, string> = {
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 (
<div className="bg-dark-900 rounded-lg p-3 space-y-2.5">
{/* Header: host + status */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 min-w-0">
<StatusDot state={conn.state} />
<span className="text-sm text-gray-200 truncate font-medium">
{conn.config.username}@{conn.config.host}
</span>
</div>
<span
className={`text-xs px-1.5 py-0.5 rounded ${
conn.state === 'connected'
? 'bg-green-400/10 text-green-400'
: conn.state === 'connecting' || conn.state === 'reconnecting'
? 'bg-yellow-400/10 text-yellow-400'
: conn.state === 'background'
? 'bg-green-400/10 text-green-400 opacity-70'
: conn.state === 'suspended'
? 'bg-gray-400/10 text-gray-400'
: conn.state === 'error'
? 'bg-red-400/10 text-red-400'
: 'bg-gray-500/10 text-gray-500'
}`}
>
{STATUS_LABELS[conn.state]}
</span>
</div>
{/* Details */}
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-gray-400">
{/* Port */}
<span className="flex items-center gap-1">
<Activity className="w-3 h-3" />
:{conn.config.port ?? 22}
</span>
{/* Duration */}
<span className="flex items-center gap-1">
<Clock className="w-3 h-3" />
{formatDuration(durationMs)}
</span>
{/* Transfer */}
{conn.stats.bytesTransferred > 0 && (
<span className="flex items-center gap-1">
<HardDrive className="w-3 h-3" />
{formatBytes(conn.stats.bytesTransferred)}
</span>
)}
{/* Sessions */}
{sessionCount > 0 && (
<span>
{conn.shellIds.size} shell{conn.sftpIds.size > 0 ? ` / ${conn.sftpIds.size} sftp` : ''}
</span>
)}
{/* Ref count */}
{conn.refCount > 1 && <span>×{conn.refCount} 引用</span>}
</div>
{/* Actions */}
<div className="flex items-center gap-2 pt-1">
{canReconnect && (
<button
type="button"
onClick={() => onReconnect(conn.connectionId)}
disabled={isReconnecting}
className="flex items-center gap-1 px-2 py-1 rounded text-xs
bg-accent-blue/10 text-accent-blue hover:bg-accent-blue/20
disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<RefreshCw className={`w-3 h-3 ${isReconnecting ? 'animate-spin' : ''}`} />
{isReconnecting ? '重连中...' : '重连'}
</button>
)}
{canDisconnect && (
<button
type="button"
onClick={() => onDisconnect(conn.connectionId)}
className="flex items-center gap-1 px-2 py-1 rounded text-xs
bg-red-400/10 text-red-400 hover:bg-red-400/20 transition-colors"
>
<WifiOff className="w-3 h-3" />
断开
</button>
)}
</div>
{/* Error message */}
{conn.error && (
<p className="text-xs text-red-400 truncate" title={conn.error}>
{conn.error}
</p>
)}
</div>
)
}
// ============== 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 (
<div className="grid grid-cols-4 gap-2">
{items.map((item) => (
<div key={item.label} className="bg-dark-900 rounded-lg p-2 text-center">
<div className={`text-lg font-semibold ${item.color}`}>{item.value}</div>
<div className="text-[10px] text-gray-500">{item.label}</div>
</div>
))}
</div>
)
}
// ============== 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<string | null>(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 */}
<div
className="fixed inset-0 bg-black/30 z-40"
onClick={close}
/>
{/* Panel */}
<div
className="fixed top-0 right-0 bottom-0 w-[360px] z-50
bg-dark-800/95 backdrop-blur border-l border-dark-700
flex flex-col shadow-xl"
>
{/* Header */}
<div className="flex items-center justify-between px-4 h-12 border-b border-dark-700 shrink-0">
<h2 className="text-sm font-semibold text-gray-200 flex items-center gap-2">
<Wifi className="w-4 h-4 text-accent-blue" />
连接管理
</h2>
<button
type="button"
onClick={close}
className="p-1 rounded hover:bg-dark-700 text-gray-400 hover:text-gray-200 transition-colors"
>
<X className="w-4 h-4" />
</button>
</div>
{/* Stats Overview */}
<div className="px-4 py-3 border-b border-dark-700 shrink-0">
<StatsOverview
total={stats.total}
connected={stats.connected}
background={stats.background}
error={stats.error + stats.suspended}
/>
</div>
{/* Connection List */}
<div className="flex-1 overflow-y-auto px-4 py-3 space-y-2">
{connList.length === 0 ? (
<EmptyState
icon={WifiOff}
title="暂无连接"
description="当前没有任何活跃连接,连接到服务器后将在此显示。"
/>
) : (
connList.map((conn) => (
<ConnectionCard
key={conn.connectionId}
conn={conn}
onReconnect={handleReconnect}
onDisconnect={handleDisconnect}
reconnectingId={reconnectingId}
/>
))
)}
</div>
</div>
</>
)
}