import { useRef, useEffect, useState } 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, FolderOpen } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useUserStore } from '@/store'; import { pipelineApi } from '@/api'; interface WorkspaceTermDialogProps { open: boolean; onOpenChange: (open: boolean) => void; runId: number | null; buildNumber: number; } /** * Workspace 终端 Dialog — 进入流水线构建目录查看文件 */ export function WorkspaceTermDialog({ open, onOpenChange, runId, buildNumber, }: WorkspaceTermDialogProps) { 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 [workspace, setWorkspace] = useState(''); // 获取 workspace 路径 useEffect(() => { if (!open || !runId) return; pipelineApi.getWorkspace(runId) .then(( data: any) => setWorkspace(data?.workspace_path || '')) .catch(() => {}); }, [open, runId]); // 连接终端 useEffect(() => { if (!open || !runId) 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; const proto = location.protocol === 'https:' ? 'wss' : 'ws'; const token = useUserStore.getState().token || ''; const wsUrl = `${proto}://${location.host}/ws/pipeline/${runId}/workspace-shell?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'); }; return () => { try { ws.close(); term.dispose(); } catch { /* noop */ } termRef.current = null; wsRef.current = null; setConnected(false); }; }, [open, runId]); // ResizeObserver for fit useEffect(() => { if (!open) return; const container = containerRef.current; if (!container) return; let timer: ReturnType; const ro = new ResizeObserver(() => { clearTimeout(timer); timer = setTimeout(() => { if (fitRef.current && termRef.current) { try { fitRef.current.fit(); termRef.current.focus(); if (wsRef.current?.readyState === WebSocket.OPEN) { const { cols, rows } = termRef.current; wsRef.current.send(JSON.stringify({ status: 1, cols, rows })); } } catch { /* noop */ } } }, 60); }); ro.observe(container); return () => { clearTimeout(timer); ro.disconnect(); }; }, [open]); return ( Workspace 终端 #{buildNumber} 流水线构建目录终端
#{buildNumber} 构建目录 {workspace && ( {workspace} )}
); }