import { useState, useRef, useEffect } from 'react'; import { cn } from '@/lib/utils'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog'; import { Terminal, Plus, X, Server, Maximize2, Minimize2, } from 'lucide-react'; import { Terminal as XTerm } from 'xterm'; import { FitAddon } from 'xterm-addon-fit'; import 'xterm/css/xterm.css'; import { serverApi } from '@/services/api'; import { useUserStore } from '@/store'; interface TerminalSession { id: string; serverId: string; serverName: string; serverIp: string; connected: boolean; } interface TermEntry { term: XTerm; ws: WebSocket; fit: FitAddon; opened: boolean; } export function WebShell() { const [servers, setServers] = useState([]); const [sessions, setSessions] = useState([]); const [activeSession, setActiveSession] = useState(null); const [selectedServer, setSelectedServer] = useState(''); const [isFullscreen, setIsFullscreen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false); // 每个会话的终端实例:sessionId -> { term, ws, fit, opened } const termsRef = useRef>(new Map()); // 每个会话的容器 DOM const containerRef = useRef>(new Map()); const loadServers = async () => { try { const res: any = await serverApi.getServers(); setServers(res?.list || []); } catch (e) { console.error('加载服务器列表失败:', e); setServers([]); } }; useEffect(() => { loadServers(); }, []); // 卸载时关闭所有连接 useEffect(() => { return () => { termsRef.current.forEach(({ ws, term }) => { try { ws.close(); term.dispose(); } catch { /* noop */ } }); termsRef.current.clear(); }; }, []); const runningServers = servers.filter((s) => s.status === 'running'); const createSession = () => { if (!selectedServer) return; const server = servers.find((s) => s.id === selectedServer); if (!server) return; const sessionId = `session-${Date.now()}`; const cols = 80; const rows = 24; const term = new XTerm({ cursorBlink: true, fontSize: 13, fontFamily: 'Menlo, Monaco, "Courier New", monospace', allowProposedApi: true, theme: { background: '#0a0e14', foreground: '#d6d6d6', cursor: '#d6d6d6', selectionBackground: 'rgba(255,255,255,0.2)', }, }); const fit = new FitAddon(); term.loadAddon(fit); const proto = typeof location !== 'undefined' && location.protocol === 'https:' ? 'wss' : 'ws'; const token = useUserStore.getState().token || ''; const wsUrl = `${proto}://${location.host}/ws/terminal?id=${server.id}&cols=${cols}&rows=${rows}&token=${encodeURIComponent(token)}`; const ws = new WebSocket(wsUrl); // 终端输入 -> WebSocket term.onData((data) => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ status: 0, data })); } }); // WebSocket 输出 -> 终端 ws.onmessage = (event) => { try { const msg = JSON.parse(event.data); if (msg.status === 0 && typeof msg.data === 'string') { term.write(msg.data); } } catch { // 非 JSON,直接写入原始数据 term.write(typeof event.data === 'string' ? event.data : ''); } }; ws.onopen = () => { term.write(`\x1b[32m正在连接 ${server.hostname} (${server.ip})...\x1b[0m\r\n`); }; ws.onclose = () => { term.write('\r\n\x1b[31m[连接已关闭]\x1b[0m\r\n'); setSessions((prev) => prev.map((s) => (s.id === sessionId ? { ...s, connected: false } : s)) ); }; ws.onerror = () => { term.write('\r\n\x1b[31m[连接发生错误]\x1b[0m\r\n'); }; termsRef.current.set(sessionId, { term, ws, fit, opened: false }); const newSession: TerminalSession = { id: sessionId, serverId: server.id, serverName: server.hostname, serverIp: server.ip, connected: true, }; setSessions((prev) => [...prev, newSession]); setActiveSession(sessionId); setSelectedServer(''); setDialogOpen(false); }; const closeSession = (sessionId: string) => { const entry = termsRef.current.get(sessionId); if (entry) { try { if (entry.ws.readyState === WebSocket.OPEN || entry.ws.readyState === WebSocket.CONNECTING) { entry.ws.close(); } entry.term.dispose(); } catch { /* noop */ } termsRef.current.delete(sessionId); } containerRef.current.delete(sessionId); const newSessions = sessions.filter((s) => s.id !== sessionId); setSessions(newSessions); if (activeSession === sessionId) { setActiveSession(newSessions.length > 0 ? newSessions[0].id : null); } }; // 窗口 resize 时适配当前激活的终端 useEffect(() => { const handleResize = () => { if (!activeSession) return; const entry = termsRef.current.get(activeSession); if (!entry || !entry.opened) return; try { entry.fit.fit(); const { cols, rows } = entry.term; entry.term.resize(cols, rows); if (entry.ws.readyState === WebSocket.OPEN) { entry.ws.send(JSON.stringify({ status: 1, cols, rows })); } } catch { /* noop */ } }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, [activeSession, isFullscreen]); // 激活会话切换后重新 fit useEffect(() => { if (!activeSession) return; const entry = termsRef.current.get(activeSession); if (entry && entry.opened) { requestAnimationFrame(() => { try { entry.fit.fit(); entry.term.focus(); } catch { /* noop */ } }); } }, [activeSession, isFullscreen]); // 快捷命令:直接发送到当前激活的 WebSocket const sendCommand = (cmd: string) => { if (!activeSession) return; const entry = termsRef.current.get(activeSession); if (entry && entry.ws.readyState === WebSocket.OPEN) { entry.ws.send(JSON.stringify({ status: 0, data: cmd + '\n' })); entry.term.focus(); } }; // 注册容器并首次挂载终端 const registerContainer = (sessionId: string, el: HTMLDivElement | null) => { if (el) { containerRef.current.set(sessionId, el); const entry = termsRef.current.get(sessionId); if (entry && !entry.opened) { try { entry.term.open(el); entry.opened = true; requestAnimationFrame(() => { try { entry.fit.fit(); entry.term.focus(); } catch { /* noop */ } }); } catch (e) { console.error('打开终端失败:', e); } } } else { containerRef.current.delete(sessionId); } }; const activeSessionData = sessions.find((s) => s.id === activeSession); const terminalHeight = isFullscreen ? 'calc(100vh - 140px)' : '500px'; return (
{/* Page Header */} {!isFullscreen && (

WebShell

远程服务器终端管理

新建终端连接 选择要连接的服务器
)} {/* Terminal Container */}
{sessions.length > 0 ? (
{sessions.map((session) => ( ))}
) : ( 暂无连接 )}
{activeSessionData && ( {activeSessionData.serverIp} )}
{sessions.length > 0 ? (
{sessions.map((session) => (
registerContainer(session.id, el)} className={cn( 'absolute inset-0 p-1', activeSession === session.id ? 'block' : 'hidden' )} /> ))}
) : (

暂无活动连接

点击“新建连接”开始远程会话

)} {/* Quick Commands */} {activeSessionData && ( 快捷命令
{['ls -la', 'pwd', 'df -h', 'free -m', 'ps aux', 'top', 'netstat -tlnp', 'systemctl status'].map( (cmd) => ( ) )}
)}
); }