import { useState, useRef, useEffect } from 'react'; import { Dialog, DialogContent, DialogDescription, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Terminal as XTerm } from 'xterm'; import { FitAddon } from 'xterm-addon-fit'; import 'xterm/css/xterm.css'; import { Terminal, Maximize2, Minimize2, X, Loader2 } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useUserStore } from '@/store'; interface KubectlTerminalProps { open: boolean; onOpenChange: (open: boolean) => void; clusterId: number | string; clusterName: string; } export function KubectlTerminal({ open, onOpenChange, clusterId, clusterName }: KubectlTerminalProps) { 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); const [error, setError] = useState(''); // 重置全屏状态 useEffect(() => { if (!open) setFullscreen(false); }, [open]); // 连接终端 useEffect(() => { if (!open || !clusterId) return; const term = new XTerm({ cursorBlink: true, fontSize: 14, fontFamily: 'Menlo, Monaco, "Courier New", monospace', allowProposedApi: true, theme: { background: '#0f172a', foreground: '#e2e8f0', cursor: '#38bdf8', selectionBackground: '#1e40af', black: '#1e293b', red: '#ef4444', green: '#10b981', yellow: '#f59e0b', blue: '#3b82f6', magenta: '#a855f7', cyan: '#06b6d4', white: '#e2e8f0', brightBlack: '#475569', brightRed: '#f87171', brightGreen: '#34d399', brightYellow: '#fbbf24', brightBlue: '#60a5fa', brightMagenta: '#c084fc', brightCyan: '#22d3ee', brightWhite: '#f8fafc', }, }); const fit = new FitAddon(); term.loadAddon(fit); termRef.current = term; fitRef.current = fit; // WebSocket const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const baseUrl = import.meta.env.VITE_API_BASE_URL || `${proto}//${window.location.host}`; const token = useUserStore.getState().token || ''; const wsUrl = `${baseUrl.replace(/^http/, 'ws').replace('/api/v1', '')}/ws/kubectl?token=${encodeURIComponent(token)}&id=${clusterId}`; 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 { if (typeof event.data === 'string') term.write(event.data); } }; ws.onopen = () => { setConnected(true); setError(''); if (containerRef.current) { term.open(containerRef.current); requestAnimationFrame(() => { try { fit.fit(); } catch { /* noop */ } }); // 延迟 focus:确保 Dialog 渲染完成后再聚焦终端 setTimeout(() => { try { term.focus(); } catch { /* noop */ } }, 150); } }; ws.onclose = () => { setConnected(false); term.write('\r\n\x1b[31m[连接已断开]\x1b[0m\r\n'); }; ws.onerror = () => { setError('WebSocket 连接失败'); term.write('\r\n\x1b[31m[连接发生错误]\x1b[0m\r\n'); }; return () => { try { if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) { ws.close(); } term.dispose(); } catch { /* noop */ } termRef.current = null; wsRef.current = null; setConnected(false); }; }, [open, clusterId]); // ResizeObserver 监听容器变化自动 fit useEffect(() => { if (!open) return; const container = containerRef.current; if (!container) return; let resizeTimer: ReturnType; const ro = new ResizeObserver(() => { clearTimeout(resizeTimer); resizeTimer = setTimeout(() => { if (fitRef.current && termRef.current && wsRef.current) { try { fitRef.current.fit(); termRef.current.focus(); const { cols, rows } = termRef.current; if (wsRef.current.readyState === WebSocket.OPEN) { wsRef.current.send(JSON.stringify({ status: 1, cols, rows })); } } catch { /* noop */ } } }, 60); }); ro.observe(container); return () => { clearTimeout(resizeTimer); ro.disconnect(); }; }, [open]); return ( e.preventDefault()} className={cn( 'bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] p-0 gap-0', '!duration-0 !transition-none', fullscreen ? '!max-w-none !w-screen !h-screen !rounded-none !translate-x-0 !translate-y-0 !left-0 !top-0' : '' )} style={fullscreen ? { maxWidth: '100vw', width: '100vw', height: '100vh' } : { maxWidth: '900px', width: '90vw' }} > kubectl — {clusterName} Kubernetes 集群命令行终端 {/* 头部 */}
kubectl — {clusterName} {!connected && !error && }
{/* 终端容器 */}
); }