# GHCS v0.2 · terminal/page-v2.tsx · 修复版 · 直接import xterm 覆盖文件:`~/guanghu-dev/ghcs-mono/frontend/src/app/console/terminal/page.tsx` **在 VS Code 里全选(⌘+A)旧代码 → 粘贴下面代码 → ⌘+S** ```tsx "use client"; import { useEffect, useRef, useState, useCallback } from "react"; import { Terminal } from "@xterm/xterm"; import { FitAddon } from "@xterm/addon-fit"; import "@xterm/xterm/css/xterm.css"; import { CommandLineIcon, ComputerDesktopIcon, ArrowPathIcon, SignalIcon, SignalSlashIcon, } from "@heroicons/react/24/outline"; type ServerInfo = { key: string; name: string; host: string; }; export default function TerminalPage() { const termRef = useRef(null); const term = useRef(null); const fitAddon = useRef(null); const wsRef = useRef(null); const [servers, setServers] = useState([]); const [selectedServer, setSelectedServer] = useState(""); const [connected, setConnected] = useState(false); const [connecting, setConnecting] = useState(false); const [error, setError] = useState(""); const [termReady, setTermReady] = useState(false); const API_BASE = process.env.NEXT_PUBLIC_CONSOLE_API || ""; // 加载服务器列表 useEffect(() => { fetch(`${API_BASE}/ws/terminal/servers`) .then((r) => r.json()) .then((data) => { setServers(data.servers || []); if (data.servers?.length > 0 && !selectedServer) { setSelectedServer(data.servers[0].key); } }) .catch(() => setError("无法加载服务器列表")); }, [API_BASE]); // 初始化终端(页面加载时) useEffect(() => { if (!termRef.current) return; const t = new Terminal({ cursorBlink: true, cursorStyle: "bar", fontSize: 14, fontFamily: '"JetBrains Mono", "Fira Code", Menlo, Monaco, monospace', theme: { background: "#0d1117", foreground: "#c9d1d9", cursor: "#58a6ff", selectionBackground: "#264f78", black: "#484f58", red: "#ff7b72", green: "#3fb950", yellow: "#d29922", blue: "#58a6ff", magenta: "#bc8cff", cyan: "#39c5cf", white: "#b1bac4", brightBlack: "#6e7681", brightRed: "#ffa198", brightGreen: "#56d364", brightYellow: "#e3b341", brightBlue: "#79c0ff", brightMagenta: "#d2a8ff", brightCyan: "#56d4dd", brightWhite: "#f0f6fc", }, }); const fit = new FitAddon(); t.loadAddon(fit); t.open(termRef.current); fit.fit(); term.current = t; fitAddon.current = fit; setTermReady(true); return () => { t.dispose(); }; }, []); // 连接 const connect = useCallback(() => { if (!selectedServer || !term.current) return; setConnecting(true); setError(""); // 先清理旧连接 if (wsRef.current) { wsRef.current.close(); wsRef.current = null; } // 清屏 term.current.clear(); term.current.writeln("\x1b[32m正在连接到 " + selectedServer + "...\x1b[0m"); const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const apiHost = API_BASE ? API_BASE.replace(/^https?:/, "") : `//${window.location.host}`; const wsUrl = `${wsProtocol}${apiHost}/ws/terminal/${selectedServer}`; const ws = new WebSocket(wsUrl); ws.binaryType = "arraybuffer"; wsRef.current = ws; ws.onopen = () => { setConnected(true); setConnecting(false); term.current?.focus(); }; ws.onmessage = (event) => { if (event.data instanceof ArrayBuffer) { term.current?.write(new Uint8Array(event.data)); } else { try { const msg = JSON.parse(event.data); if (msg.type === "error") { setError(msg.message); term.current?.writeln("\r\n\x1b[31m[错误] " + msg.message + "\x1b[0m"); } } catch { term.current?.write(event.data); } } }; ws.onclose = () => { setConnected(false); term.current?.writeln("\r\n\x1b[33m[连接已断开]\x1b[0m"); }; ws.onerror = () => { setError("WebSocket 连接失败"); setConnecting(false); }; // 键盘输入 term.current.onData((data) => { if (ws.readyState === WebSocket.OPEN) { ws.send(data); } }); // 窗口大小调整 const observer = new ResizeObserver(() => { fitAddon.current?.fit(); if (term.current && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: "resize", cols: term.current.cols, rows: term.current.rows, })); } }); if (termRef.current) observer.observe(termRef.current); }, [selectedServer, API_BASE]); // 断开 const disconnect = useCallback(() => { if (wsRef.current) { wsRef.current.close(); wsRef.current = null; } setConnected(false); term.current?.writeln("\r\n\x1b[33m已断开连接\x1b[0m"); }, []); return (

SSH Web 终端

{connected ? ( <> 已连接 ) : ( <> 未连接 )}
{error && (
{error}
)}
💡 提示:点一下黑色终端区域,然后就可以输入命令了。这是一个真实的 SSH 会话。
); } ``` --- ## 🔑 与上一版的区别 - 用 `import { Terminal } from "@xterm/xterm"` 直接导入,不再动态加载 - 用 `import "@xterm/xterm/css/xterm.css"` 直接导入 CSS,不再加载 CDN - 终端在页面一打开就初始化好,不需要等"加载中" - 去掉了 `display: none/block` 切换,更简单稳定 - 去掉了 `(window as any)` 全局变量存储 --- ## 部署 1. VS Code 打开 `page.tsx`(terminal 文件夹下的) 2. ⌘+A 全选 → 粘贴上面代码 → ⌘+S 3. 浏览器刷新 `http://localhost:3000/console/terminal` 4. 点连接 → 终端变黑 → 点一下黑框 → 敲 `whoami`