diff --git a/frontend/src/components/Terminal/Terminal.tsx b/frontend/src/components/Terminal/Terminal.tsx index 5dea3c70..a3915144 100644 --- a/frontend/src/components/Terminal/Terminal.tsx +++ b/frontend/src/components/Terminal/Terminal.tsx @@ -8,8 +8,10 @@ import styles from './index.module.less'; import { useTerminal } from './useTerminal'; import { TerminalToolbar } from './TerminalToolbar'; import type { TerminalProps, TerminalToolbarConfig } from './types'; -import { Loader2, XCircle } from 'lucide-react'; +import { TERMINAL_THEMES, getThemeByName } from './themes'; +import { Loader2, XCircle, ChevronUp, ChevronDown, X } from 'lucide-react'; import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; import { message } from 'antd'; export const Terminal: React.FC = ({ @@ -24,6 +26,8 @@ export const Terminal: React.FC = ({ }) => { const [fontSize, setFontSize] = useState(display?.fontSize ?? 14); const [showSearch, setShowSearch] = useState(false); + const [currentTheme, setCurrentTheme] = useState('dark'); + const [auditShown, setAuditShown] = useState(false); const { terminalRef, @@ -39,7 +43,11 @@ export const Terminal: React.FC = ({ cleanup, } = useTerminal({ connection, - display: { ...display, fontSize }, + display: { + ...display, + fontSize, + theme: getThemeByName(currentTheme).theme, + }, onStatusChange, onError, }); @@ -82,9 +90,27 @@ export const Terminal: React.FC = ({ }; }, [id]); - // 显示审计警告 + // 监听窗口大小变化,自动调整终端尺寸 useEffect(() => { - if (connectionStatus === 'connected' && audit?.enabled && terminalInstance) { + const handleResize = () => { + if (fitAddon) { + // 延迟执行以确保窗口大小已经更新 + setTimeout(() => { + fitAddon.fit(); + }, 100); + } + }; + + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, [fitAddon]); + + // 显示审计警告(只显示一次) + useEffect(() => { + if (connectionStatus === 'connected' && audit?.enabled && terminalInstance && !auditShown) { const companyName = audit.companyName || ''; const customMessage = audit.message; @@ -100,15 +126,18 @@ export const Terminal: React.FC = ({ terminalInstance.writeln('\x1b[33m└─────────────────────────────────────────────────────────────\x1b[0m\r\n'); } + setAuditShown(true); + setTimeout(() => { fitAddon?.fit(); }, 100); } - }, [connectionStatus, audit, terminalInstance, fitAddon]); + }, [connectionStatus, audit, terminalInstance, fitAddon, auditShown]); // 重连处理 const handleReconnect = useCallback(() => { cleanup(); + setAuditShown(false); // 重置审计警告标记,重连后重新显示 setTimeout(() => { initializeTerminal(); connectWebSocket(); @@ -195,6 +224,16 @@ export const Terminal: React.FC = ({ } }, [fontSize, terminalInstance, fitAddon]); + // 切换主题 + const handleThemeChange = useCallback((themeName: string) => { + setCurrentTheme(themeName); + if (terminalInstance) { + const theme = getThemeByName(themeName); + terminalInstance.options.theme = theme.theme; + message.success(`已切换到 ${theme.label} 主题`); + } + }, [terminalInstance]); + return (
{/* 工具栏 */} @@ -202,16 +241,80 @@ export const Terminal: React.FC = ({ config={toolbarConfig} connectionStatus={connectionStatus} fontSize={fontSize} + currentTheme={currentTheme} + themes={TERMINAL_THEMES} onSearch={handleSearch} onClear={handleClear} onCopy={handleCopy} onZoomIn={handleZoomIn} onZoomOut={handleZoomOut} onReconnect={handleReconnect} + onThemeChange={handleThemeChange} /> {/* 终端区域 */}
+ {/* 搜索框 */} + {showSearch && ( +
+ handleSearchChange(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + if (e.shiftKey) { + handleSearchPrev(); + } else { + handleSearchNext(); + } + } else if (e.key === 'Escape') { + handleCloseSearch(); + } + }} + className="terminal-search-input" + style={{ + background: 'transparent', + border: 'none', + outline: 'none', + color: '#fff', + width: '200px', + fontSize: '0.875rem', + }} + /> +
+ + +
+ +
+ )} +
void; onClear?: () => void; onCopy?: () => void; onZoomIn?: () => void; onZoomOut?: () => void; onReconnect?: () => void; + onThemeChange?: (themeName: string) => void; } export const TerminalToolbar: React.FC = ({ config, connectionStatus, fontSize, + currentTheme, + themes = [], onSearch, onClear, onCopy, onZoomIn, onZoomOut, onReconnect, + onThemeChange, }) => { if (!config.show) return null; @@ -118,6 +126,25 @@ export const TerminalToolbar: React.FC = ({ )} + {/* 主题选择器 */} + {themes.length > 0 && onThemeChange && ( + <> +
+ + + )} {(connectionStatus === 'disconnected' || connectionStatus === 'error') && ( <>
diff --git a/frontend/src/components/Terminal/TerminalWindowManager.tsx b/frontend/src/components/Terminal/TerminalWindowManager.tsx index 5983ab2c..12767a0d 100644 --- a/frontend/src/components/Terminal/TerminalWindowManager.tsx +++ b/frontend/src/components/Terminal/TerminalWindowManager.tsx @@ -58,12 +58,18 @@ export function TerminalWindowManager({ const windowId = `${type}-${resourceId}-${Date.now()}`; const offset = windows.length * 30; + // 计算居中位置 + const windowWidth = 1200; + const windowHeight = 700; + const centerX = (window.innerWidth - windowWidth) / 2 + offset; + const centerY = (window.innerHeight - windowHeight) / 2 + offset; + const newWindow: TerminalWindow = { id: windowId, resource, isMinimized: false, - position: { x: 100 + offset, y: 100 + offset }, - size: { width: 1200, height: 700 }, + position: { x: Math.max(0, centerX), y: Math.max(0, centerY) }, + size: { width: windowWidth, height: windowHeight }, connectionStatus: 'connecting', }; diff --git a/frontend/src/components/Terminal/index.module.less b/frontend/src/components/Terminal/index.module.less index bb53bcaa..ec5da153 100644 --- a/frontend/src/components/Terminal/index.module.less +++ b/frontend/src/components/Terminal/index.module.less @@ -44,19 +44,40 @@ .content { width: 100%; height: 100%; - padding: 0.5rem; + padding: 0; } /* xterm.js 滚动条样式优化 */ .content :global(.xterm) { height: 100%; - padding: 0; + padding: 0 0 0.5rem 0.5rem; /* 下边距 左边距 */ } .content :global(.xterm-screen) { padding: 0; } +/* 搜索高亮样式 - 浅黄色背景 */ +.content :global(.xterm-decoration-overview-ruler) { + background-color: rgba(255, 235, 59, 0.4) !important; +} + +.content :global(.xterm .xterm-decoration-overview-ruler) { + background-color: rgba(255, 235, 59, 0.4) !important; +} + +/* 当前搜索匹配项 - 深黄色 */ +.content :global(.xterm .xterm-rows .xterm-decoration) { + background-color: rgba(255, 235, 59, 0.6) !important; +} + +/* 搜索结果背景高亮 */ +.content :global(.xterm .xterm-rows > :not(.xterm-decoration-top-layer) .xterm-decoration) { + background-color: rgba(255, 235, 59, 0.4) !important; + color: #000 !important; + font-weight: bold; +} + .content :global(.xterm-viewport) { scrollbar-width: thin; scrollbar-color: rgba(255, 255, 255, 0.3) transparent; diff --git a/frontend/src/components/Terminal/index.ts b/frontend/src/components/Terminal/index.ts index 768f4576..256cf984 100644 --- a/frontend/src/components/Terminal/index.ts +++ b/frontend/src/components/Terminal/index.ts @@ -6,7 +6,9 @@ export { Terminal } from './Terminal'; export { useTerminal } from './useTerminal'; export { TerminalWindowManager } from './TerminalWindowManager'; +export { TERMINAL_THEMES, getThemeByName } from './themes'; export type { TerminalWindow } from './TerminalWindowManager'; +export type { TerminalTheme } from './themes'; export type { TerminalType, ConnectionStatus, diff --git a/frontend/src/components/Terminal/themes.ts b/frontend/src/components/Terminal/themes.ts new file mode 100644 index 00000000..93703e75 --- /dev/null +++ b/frontend/src/components/Terminal/themes.ts @@ -0,0 +1,324 @@ +/** + * Terminal 主题配置 + */ + +export interface TerminalTheme { + name: string; + label: string; + theme: { + background: string; + foreground: string; + cursor: string; + black: string; + red: string; + green: string; + yellow: string; + blue: string; + magenta: string; + cyan: string; + white: string; + brightBlack: string; + brightRed: string; + brightGreen: string; + brightYellow: string; + brightBlue: string; + brightMagenta: string; + brightCyan: string; + brightWhite: string; + selectionBackground?: string; + selectionForeground?: string; + }; +} + +export const TERMINAL_THEMES: TerminalTheme[] = [ + { + name: 'dark', + label: '暗色(默认)', + theme: { + background: '#1e1e1e', + foreground: '#d4d4d4', + cursor: '#d4d4d4', + black: '#000000', + red: '#cd3131', + green: '#0dbc79', + yellow: '#e5e510', + blue: '#2472c8', + magenta: '#bc3fbc', + cyan: '#11a8cd', + white: '#e5e5e5', + brightBlack: '#666666', + brightRed: '#f14c4c', + brightGreen: '#23d18b', + brightYellow: '#f5f543', + brightBlue: '#3b8eea', + brightMagenta: '#d670d6', + brightCyan: '#29b8db', + brightWhite: '#e5e5e5', + selectionBackground: 'rgba(255, 255, 255, 0.3)', + }, + }, + { + name: 'monokai', + label: 'Monokai', + theme: { + background: '#272822', + foreground: '#f8f8f2', + cursor: '#f8f8f0', + black: '#272822', + red: '#f92672', + green: '#a6e22e', + yellow: '#f4bf75', + blue: '#66d9ef', + magenta: '#ae81ff', + cyan: '#a1efe4', + white: '#f8f8f2', + brightBlack: '#75715e', + brightRed: '#f92672', + brightGreen: '#a6e22e', + brightYellow: '#f4bf75', + brightBlue: '#66d9ef', + brightMagenta: '#ae81ff', + brightCyan: '#a1efe4', + brightWhite: '#f9f8f5', + selectionBackground: 'rgba(255, 255, 255, 0.3)', + }, + }, + { + name: 'dracula', + label: 'Dracula', + theme: { + background: '#282a36', + foreground: '#f8f8f2', + cursor: '#f8f8f2', + black: '#21222c', + red: '#ff5555', + green: '#50fa7b', + yellow: '#f1fa8c', + blue: '#bd93f9', + magenta: '#ff79c6', + cyan: '#8be9fd', + white: '#f8f8f2', + brightBlack: '#6272a4', + brightRed: '#ff6e6e', + brightGreen: '#69ff94', + brightYellow: '#ffffa5', + brightBlue: '#d6acff', + brightMagenta: '#ff92df', + brightCyan: '#a4ffff', + brightWhite: '#ffffff', + selectionBackground: 'rgba(68, 71, 90, 0.5)', + }, + }, + { + name: 'solarized-dark', + label: 'Solarized Dark', + theme: { + background: '#002b36', + foreground: '#839496', + cursor: '#839496', + black: '#073642', + red: '#dc322f', + green: '#859900', + yellow: '#b58900', + blue: '#268bd2', + magenta: '#d33682', + cyan: '#2aa198', + white: '#eee8d5', + brightBlack: '#002b36', + brightRed: '#cb4b16', + brightGreen: '#586e75', + brightYellow: '#657b83', + brightBlue: '#839496', + brightMagenta: '#6c71c4', + brightCyan: '#93a1a1', + brightWhite: '#fdf6e3', + selectionBackground: 'rgba(7, 54, 66, 0.5)', + }, + }, + { + name: 'nord', + label: 'Nord', + theme: { + background: '#2e3440', + foreground: '#d8dee9', + cursor: '#d8dee9', + black: '#3b4252', + red: '#bf616a', + green: '#a3be8c', + yellow: '#ebcb8b', + blue: '#81a1c1', + magenta: '#b48ead', + cyan: '#88c0d0', + white: '#e5e9f0', + brightBlack: '#4c566a', + brightRed: '#bf616a', + brightGreen: '#a3be8c', + brightYellow: '#ebcb8b', + brightBlue: '#81a1c1', + brightMagenta: '#b48ead', + brightCyan: '#8fbcbb', + brightWhite: '#eceff4', + selectionBackground: 'rgba(76, 86, 106, 0.5)', + }, + }, + { + name: 'solarized-light', + label: 'Solarized Light', + theme: { + background: '#fdf6e3', + foreground: '#657b83', + cursor: '#657b83', + black: '#073642', + red: '#dc322f', + green: '#859900', + yellow: '#b58900', + blue: '#268bd2', + magenta: '#d33682', + cyan: '#2aa198', + white: '#eee8d5', + brightBlack: '#002b36', + brightRed: '#cb4b16', + brightGreen: '#586e75', + brightYellow: '#657b83', + brightBlue: '#839496', + brightMagenta: '#6c71c4', + brightCyan: '#93a1a1', + brightWhite: '#fdf6e3', + selectionBackground: 'rgba(7, 54, 66, 0.2)', + }, + }, + { + name: 'gruvbox-dark', + label: 'Gruvbox Dark', + theme: { + background: '#282828', + foreground: '#ebdbb2', + cursor: '#ebdbb2', + black: '#282828', + red: '#cc241d', + green: '#98971a', + yellow: '#d79921', + blue: '#458588', + magenta: '#b16286', + cyan: '#689d6a', + white: '#a89984', + brightBlack: '#928374', + brightRed: '#fb4934', + brightGreen: '#b8bb26', + brightYellow: '#fabd2f', + brightBlue: '#83a598', + brightMagenta: '#d3869b', + brightCyan: '#8ec07c', + brightWhite: '#ebdbb2', + selectionBackground: 'rgba(235, 219, 178, 0.3)', + }, + }, + { + name: 'one-dark', + label: 'One Dark', + theme: { + background: '#282c34', + foreground: '#abb2bf', + cursor: '#528bff', + black: '#282c34', + red: '#e06c75', + green: '#98c379', + yellow: '#e5c07b', + blue: '#61afef', + magenta: '#c678dd', + cyan: '#56b6c2', + white: '#abb2bf', + brightBlack: '#5c6370', + brightRed: '#e06c75', + brightGreen: '#98c379', + brightYellow: '#e5c07b', + brightBlue: '#61afef', + brightMagenta: '#c678dd', + brightCyan: '#56b6c2', + brightWhite: '#ffffff', + selectionBackground: 'rgba(171, 178, 191, 0.3)', + }, + }, + { + name: 'material', + label: 'Material', + theme: { + background: '#263238', + foreground: '#eeffff', + cursor: '#ffcc00', + black: '#000000', + red: '#e53935', + green: '#91b859', + yellow: '#ffb62c', + blue: '#6182b8', + magenta: '#7c4dff', + cyan: '#39adb5', + white: '#ffffff', + brightBlack: '#000000', + brightRed: '#ff5370', + brightGreen: '#c3e88d', + brightYellow: '#ffcb6b', + brightBlue: '#82aaff', + brightMagenta: '#c792ea', + brightCyan: '#89ddff', + brightWhite: '#ffffff', + selectionBackground: 'rgba(128, 203, 196, 0.3)', + }, + }, + { + name: 'github', + label: 'GitHub', + theme: { + background: '#ffffff', + foreground: '#24292e', + cursor: '#044289', + black: '#24292e', + red: '#d73a49', + green: '#22863a', + yellow: '#b08800', + blue: '#0366d6', + magenta: '#6f42c1', + cyan: '#1b7c83', + white: '#6a737d', + brightBlack: '#586069', + brightRed: '#cb2431', + brightGreen: '#22863a', + brightYellow: '#b08800', + brightBlue: '#005cc5', + brightMagenta: '#5a32a3', + brightCyan: '#3192aa', + brightWhite: '#959da5', + selectionBackground: 'rgba(3, 102, 214, 0.2)', + }, + }, + { + name: 'tokyo-night', + label: 'Tokyo Night', + theme: { + background: '#1a1b26', + foreground: '#c0caf5', + cursor: '#c0caf5', + black: '#15161e', + red: '#f7768e', + green: '#9ece6a', + yellow: '#e0af68', + blue: '#7aa2f7', + magenta: '#bb9af7', + cyan: '#7dcfff', + white: '#a9b1d6', + brightBlack: '#414868', + brightRed: '#f7768e', + brightGreen: '#9ece6a', + brightYellow: '#e0af68', + brightBlue: '#7aa2f7', + brightMagenta: '#bb9af7', + brightCyan: '#7dcfff', + brightWhite: '#c0caf5', + selectionBackground: 'rgba(192, 202, 245, 0.3)', + }, + }, +]; + +export const getThemeByName = (name: string): TerminalTheme => { + return TERMINAL_THEMES.find(t => t.name === name) || TERMINAL_THEMES[0]; +}; diff --git a/frontend/src/components/ui/draggable-window.tsx b/frontend/src/components/ui/draggable-window.tsx index ce08338f..0649d368 100644 --- a/frontend/src/components/ui/draggable-window.tsx +++ b/frontend/src/components/ui/draggable-window.tsx @@ -53,9 +53,16 @@ export const DraggableWindow: React.FC = ({ if (!isDragging) return; const handleMouseMove = (e: MouseEvent) => { + const newX = e.clientX - dragStartRef.current.x; + const newY = e.clientY - dragStartRef.current.y; + + // 限制在可见区域内 + const maxX = window.innerWidth - size.width; + const maxY = window.innerHeight - size.height; + setPosition({ - x: e.clientX - dragStartRef.current.x, - y: e.clientY - dragStartRef.current.y, + x: Math.max(0, Math.min(newX, maxX)), + y: Math.max(0, Math.min(newY, maxY)), }); }; @@ -70,7 +77,7 @@ export const DraggableWindow: React.FC = ({ document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; - }, [isDragging]); + }, [isDragging, size.width, size.height]); // 调整大小开始 const handleResizeStart = (e: React.MouseEvent) => { @@ -95,14 +102,24 @@ export const DraggableWindow: React.FC = ({ const deltaX = e.clientX - resizeStartRef.current.x; const deltaY = e.clientY - resizeStartRef.current.y; + // 限制窗口大小不超出屏幕 + const newWidth = Math.max(600, resizeStartRef.current.width + deltaX); + const newHeight = Math.max(400, resizeStartRef.current.height + deltaY); + const maxWidth = window.innerWidth - position.x; + const maxHeight = window.innerHeight - position.y; + setSize({ - width: Math.max(600, resizeStartRef.current.width + deltaX), - height: Math.max(400, resizeStartRef.current.height + deltaY), + width: Math.min(newWidth, maxWidth), + height: Math.min(newHeight, maxHeight), }); }; const handleMouseUp = () => { setIsResizing(false); + // 触发 resize 事件通知终端调整大小 + setTimeout(() => { + window.dispatchEvent(new Event('resize')); + }, 50); }; document.addEventListener('mousemove', handleMouseMove); @@ -112,19 +129,24 @@ export const DraggableWindow: React.FC = ({ document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; - }, [isResizing]); + }, [isResizing, position.x, position.y]); // 最大化/还原 const toggleMaximize = () => { - if (isMaximized) { - setPosition(savedState.position); - setSize(savedState.size); - } else { + if (!isMaximized) { setSavedState({ position, size }); setPosition({ x: 0, y: 0 }); setSize({ width: window.innerWidth, height: window.innerHeight }); + } else { + setPosition(savedState.position); + setSize(savedState.size); } setIsMaximized(!isMaximized); + + // 触发 resize 事件通知终端调整大小 + setTimeout(() => { + window.dispatchEvent(new Event('resize')); + }, 250); }; // 双击标题栏最大化