import { useState, useRef, useEffect } from 'react'; import { Dialog, DialogContent, DialogDescription, DialogTitle, } from '@/components/ui/dialog'; import { Terminal as XTerm } from 'xterm'; import { FitAddon } from 'xterm-addon-fit'; import 'xterm/css/xterm.css'; import { Maximize2, Minimize2, X } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useUserStore } from '@/store'; interface TerminalDialogProps { open: boolean; onOpenChange: (open: boolean) => void; serverId: number | null; serverName: string; serverIp: string; } /** * 终端 Dialog — WebSocket SSH 终端 // Logan.Li * 打开时自动连接,关闭时自动断开 */ export function TerminalDialog({ open, onOpenChange, serverId, serverName, serverIp, }: TerminalDialogProps) { const containerRef = useRef(null); const termRef = useRef(null); const wsRef = useRef(null); const fitRef = useRef(null); const [connected, setConnected] = useState(false); const [fullscreen, setFullscreen] = useState(false); // 连接终端 useEffect(() => { if (!open || !serverId) return; // 创建终端 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); termRef.current = term; fitRef.current = fit; // WebSocket 连接 const proto = location.protocol === 'https:' ? 'wss' : 'ws'; const token = useUserStore.getState().token || ''; const wsUrl = `${proto}://${location.host}/ws/terminal?id=${serverId}&cols=80&rows=24&token=${encodeURIComponent(token)}`; const ws = new WebSocket(wsUrl); wsRef.current = ws; term.onData((data) => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ status: 0, data })); } }); ws.onmessage = (event) => { try { const msg = JSON.parse(event.data); if (msg.status === 0 && typeof msg.data === 'string') { term.write(msg.data); } } catch { term.write(typeof event.data === 'string' ? event.data : ''); } }; ws.onopen = () => { setConnected(true); // 挂载终端到容器 if (containerRef.current) { term.open(containerRef.current); requestAnimationFrame(() => { try { fit.fit(); term.focus(); } catch { /* noop */ } }); } }; ws.onclose = () => { setConnected(false); term.write('\r\n\x1b[31m[连接已关闭]\x1b[0m\r\n'); }; ws.onerror = () => { term.write('\r\n\x1b[31m[连接发生错误]\x1b[0m\r\n'); }; // resize 监听 const handleResize = () => { if (fitRef.current && termRef.current) { try { fit.fit(); const { cols, rows } = termRef.current; if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ status: 1, cols, rows })); } } catch { /* noop */ } } }; window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); try { ws.close(); term.dispose(); } catch { /* noop */ } termRef.current = null; wsRef.current = null; setConnected(false); }; }, [open, serverId]); // fullscreen 切换时重新 fit useEffect(() => { if (open && fitRef.current) { requestAnimationFrame(() => { try { fitRef.current?.fit(); termRef.current?.focus(); } catch { /* noop */ } }); } }, [fullscreen, open]); return ( 终端 - {serverName} SSH 终端连接 {/* 终端头部 */}
{serverName} ({serverIp})
{/* 终端容器 */}
); }