重写ssh前端组件,通用化

This commit is contained in:
dengqichen 2025-12-06 19:14:25 +08:00
parent 980c827feb
commit f6abb48333
7 changed files with 525 additions and 20 deletions

View File

@ -8,8 +8,10 @@ import styles from './index.module.less';
import { useTerminal } from './useTerminal'; import { useTerminal } from './useTerminal';
import { TerminalToolbar } from './TerminalToolbar'; import { TerminalToolbar } from './TerminalToolbar';
import type { TerminalProps, TerminalToolbarConfig } from './types'; 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 { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { message } from 'antd'; import { message } from 'antd';
export const Terminal: React.FC<TerminalProps> = ({ export const Terminal: React.FC<TerminalProps> = ({
@ -24,6 +26,8 @@ export const Terminal: React.FC<TerminalProps> = ({
}) => { }) => {
const [fontSize, setFontSize] = useState(display?.fontSize ?? 14); const [fontSize, setFontSize] = useState(display?.fontSize ?? 14);
const [showSearch, setShowSearch] = useState(false); const [showSearch, setShowSearch] = useState(false);
const [currentTheme, setCurrentTheme] = useState('dark');
const [auditShown, setAuditShown] = useState(false);
const { const {
terminalRef, terminalRef,
@ -39,7 +43,11 @@ export const Terminal: React.FC<TerminalProps> = ({
cleanup, cleanup,
} = useTerminal({ } = useTerminal({
connection, connection,
display: { ...display, fontSize }, display: {
...display,
fontSize,
theme: getThemeByName(currentTheme).theme,
},
onStatusChange, onStatusChange,
onError, onError,
}); });
@ -82,9 +90,27 @@ export const Terminal: React.FC<TerminalProps> = ({
}; };
}, [id]); }, [id]);
// 显示审计警告 // 监听窗口大小变化,自动调整终端尺寸
useEffect(() => { 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 companyName = audit.companyName || '';
const customMessage = audit.message; const customMessage = audit.message;
@ -100,15 +126,18 @@ export const Terminal: React.FC<TerminalProps> = ({
terminalInstance.writeln('\x1b[33m└─────────────────────────────────────────────────────────────\x1b[0m\r\n'); terminalInstance.writeln('\x1b[33m└─────────────────────────────────────────────────────────────\x1b[0m\r\n');
} }
setAuditShown(true);
setTimeout(() => { setTimeout(() => {
fitAddon?.fit(); fitAddon?.fit();
}, 100); }, 100);
} }
}, [connectionStatus, audit, terminalInstance, fitAddon]); }, [connectionStatus, audit, terminalInstance, fitAddon, auditShown]);
// 重连处理 // 重连处理
const handleReconnect = useCallback(() => { const handleReconnect = useCallback(() => {
cleanup(); cleanup();
setAuditShown(false); // 重置审计警告标记,重连后重新显示
setTimeout(() => { setTimeout(() => {
initializeTerminal(); initializeTerminal();
connectWebSocket(); connectWebSocket();
@ -195,6 +224,16 @@ export const Terminal: React.FC<TerminalProps> = ({
} }
}, [fontSize, terminalInstance, fitAddon]); }, [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 ( return (
<div className={styles.terminalWrapper}> <div className={styles.terminalWrapper}>
{/* 工具栏 */} {/* 工具栏 */}
@ -202,16 +241,80 @@ export const Terminal: React.FC<TerminalProps> = ({
config={toolbarConfig} config={toolbarConfig}
connectionStatus={connectionStatus} connectionStatus={connectionStatus}
fontSize={fontSize} fontSize={fontSize}
currentTheme={currentTheme}
themes={TERMINAL_THEMES}
onSearch={handleSearch} onSearch={handleSearch}
onClear={handleClear} onClear={handleClear}
onCopy={handleCopy} onCopy={handleCopy}
onZoomIn={handleZoomIn} onZoomIn={handleZoomIn}
onZoomOut={handleZoomOut} onZoomOut={handleZoomOut}
onReconnect={handleReconnect} onReconnect={handleReconnect}
onThemeChange={handleThemeChange}
/> />
{/* 终端区域 */} {/* 终端区域 */}
<div className={styles.container}> <div className={styles.container}>
{/* 搜索框 */}
{showSearch && (
<div className={styles.searchBar}>
<Input
type="text"
placeholder="搜索..."
value={searchQuery}
onChange={(e) => 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',
}}
/>
<div className="flex items-center gap-0.5 ml-1">
<Button
size="sm"
variant="ghost"
className="h-7 px-2"
onClick={handleSearchPrev}
title="上一个 (Shift+Enter)"
>
<ChevronUp className="h-3.5 w-3.5" />
</Button>
<Button
size="sm"
variant="ghost"
className="h-7 px-2"
onClick={handleSearchNext}
title="下一个 (Enter)"
>
<ChevronDown className="h-3.5 w-3.5" />
</Button>
</div>
<Button
size="sm"
variant="ghost"
className="h-7 px-2"
onClick={handleCloseSearch}
title="关闭 (Esc)"
>
<X className="h-3.5 w-3.5" />
</Button>
</div>
)}
<div <div
ref={terminalRef} ref={terminalRef}
className={styles.content} className={styles.content}

View File

@ -4,32 +4,40 @@
import React from 'react'; import React from 'react';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Search, Trash2, Copy, ZoomIn, ZoomOut, RotateCcw, Loader2, XCircle } from 'lucide-react'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Search, Trash2, Copy, ZoomIn, ZoomOut, RotateCcw, Loader2, XCircle, Palette } from 'lucide-react';
import type { ConnectionStatus, TerminalToolbarConfig } from './types'; import type { ConnectionStatus, TerminalToolbarConfig } from './types';
import type { TerminalTheme } from './themes';
import styles from './index.module.less'; import styles from './index.module.less';
interface TerminalToolbarProps { interface TerminalToolbarProps {
config: TerminalToolbarConfig; config: TerminalToolbarConfig;
connectionStatus: ConnectionStatus; connectionStatus: ConnectionStatus;
fontSize: number; fontSize: number;
currentTheme?: string;
themes?: TerminalTheme[];
onSearch?: () => void; onSearch?: () => void;
onClear?: () => void; onClear?: () => void;
onCopy?: () => void; onCopy?: () => void;
onZoomIn?: () => void; onZoomIn?: () => void;
onZoomOut?: () => void; onZoomOut?: () => void;
onReconnect?: () => void; onReconnect?: () => void;
onThemeChange?: (themeName: string) => void;
} }
export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({
config, config,
connectionStatus, connectionStatus,
fontSize, fontSize,
currentTheme,
themes = [],
onSearch, onSearch,
onClear, onClear,
onCopy, onCopy,
onZoomIn, onZoomIn,
onZoomOut, onZoomOut,
onReconnect, onReconnect,
onThemeChange,
}) => { }) => {
if (!config.show) return null; if (!config.show) return null;
@ -118,6 +126,25 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({
</Button> </Button>
</> </>
)} )}
{/* 主题选择器 */}
{themes.length > 0 && onThemeChange && (
<>
<div className="h-4 w-px bg-gray-300 dark:bg-gray-600 mx-1" />
<Select value={currentTheme} onValueChange={onThemeChange}>
<SelectTrigger className="h-7 w-auto min-w-[100px] text-xs">
<Palette className="h-3.5 w-3.5 mr-1" />
<SelectValue placeholder="选择主题" />
</SelectTrigger>
<SelectContent className="z-[9999]">
{themes.map((theme) => (
<SelectItem key={theme.name} value={theme.name}>
{theme.label}
</SelectItem>
))}
</SelectContent>
</Select>
</>
)}
{(connectionStatus === 'disconnected' || connectionStatus === 'error') && ( {(connectionStatus === 'disconnected' || connectionStatus === 'error') && (
<> <>
<div className="h-4 w-px bg-gray-300 dark:bg-gray-600 mx-1" /> <div className="h-4 w-px bg-gray-300 dark:bg-gray-600 mx-1" />

View File

@ -58,12 +58,18 @@ export function TerminalWindowManager<TResource = any>({
const windowId = `${type}-${resourceId}-${Date.now()}`; const windowId = `${type}-${resourceId}-${Date.now()}`;
const offset = windows.length * 30; 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<TResource> = { const newWindow: TerminalWindow<TResource> = {
id: windowId, id: windowId,
resource, resource,
isMinimized: false, isMinimized: false,
position: { x: 100 + offset, y: 100 + offset }, position: { x: Math.max(0, centerX), y: Math.max(0, centerY) },
size: { width: 1200, height: 700 }, size: { width: windowWidth, height: windowHeight },
connectionStatus: 'connecting', connectionStatus: 'connecting',
}; };

View File

@ -44,19 +44,40 @@
.content { .content {
width: 100%; width: 100%;
height: 100%; height: 100%;
padding: 0.5rem; padding: 0;
} }
/* xterm.js 滚动条样式优化 */ /* xterm.js 滚动条样式优化 */
.content :global(.xterm) { .content :global(.xterm) {
height: 100%; height: 100%;
padding: 0; padding: 0 0 0.5rem 0.5rem; /* 下边距 左边距 */
} }
.content :global(.xterm-screen) { .content :global(.xterm-screen) {
padding: 0; 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) { .content :global(.xterm-viewport) {
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.3) transparent; scrollbar-color: rgba(255, 255, 255, 0.3) transparent;

View File

@ -6,7 +6,9 @@
export { Terminal } from './Terminal'; export { Terminal } from './Terminal';
export { useTerminal } from './useTerminal'; export { useTerminal } from './useTerminal';
export { TerminalWindowManager } from './TerminalWindowManager'; export { TerminalWindowManager } from './TerminalWindowManager';
export { TERMINAL_THEMES, getThemeByName } from './themes';
export type { TerminalWindow } from './TerminalWindowManager'; export type { TerminalWindow } from './TerminalWindowManager';
export type { TerminalTheme } from './themes';
export type { export type {
TerminalType, TerminalType,
ConnectionStatus, ConnectionStatus,

View File

@ -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];
};

View File

@ -53,9 +53,16 @@ export const DraggableWindow: React.FC<DraggableWindowProps> = ({
if (!isDragging) return; if (!isDragging) return;
const handleMouseMove = (e: MouseEvent) => { 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({ setPosition({
x: e.clientX - dragStartRef.current.x, x: Math.max(0, Math.min(newX, maxX)),
y: e.clientY - dragStartRef.current.y, y: Math.max(0, Math.min(newY, maxY)),
}); });
}; };
@ -70,7 +77,7 @@ export const DraggableWindow: React.FC<DraggableWindowProps> = ({
document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp); document.removeEventListener('mouseup', handleMouseUp);
}; };
}, [isDragging]); }, [isDragging, size.width, size.height]);
// 调整大小开始 // 调整大小开始
const handleResizeStart = (e: React.MouseEvent) => { const handleResizeStart = (e: React.MouseEvent) => {
@ -95,14 +102,24 @@ export const DraggableWindow: React.FC<DraggableWindowProps> = ({
const deltaX = e.clientX - resizeStartRef.current.x; const deltaX = e.clientX - resizeStartRef.current.x;
const deltaY = e.clientY - resizeStartRef.current.y; 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({ setSize({
width: Math.max(600, resizeStartRef.current.width + deltaX), width: Math.min(newWidth, maxWidth),
height: Math.max(400, resizeStartRef.current.height + deltaY), height: Math.min(newHeight, maxHeight),
}); });
}; };
const handleMouseUp = () => { const handleMouseUp = () => {
setIsResizing(false); setIsResizing(false);
// 触发 resize 事件通知终端调整大小
setTimeout(() => {
window.dispatchEvent(new Event('resize'));
}, 50);
}; };
document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mousemove', handleMouseMove);
@ -112,19 +129,24 @@ export const DraggableWindow: React.FC<DraggableWindowProps> = ({
document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp); document.removeEventListener('mouseup', handleMouseUp);
}; };
}, [isResizing]); }, [isResizing, position.x, position.y]);
// 最大化/还原 // 最大化/还原
const toggleMaximize = () => { const toggleMaximize = () => {
if (isMaximized) { if (!isMaximized) {
setPosition(savedState.position);
setSize(savedState.size);
} else {
setSavedState({ position, size }); setSavedState({ position, size });
setPosition({ x: 0, y: 0 }); setPosition({ x: 0, y: 0 });
setSize({ width: window.innerWidth, height: window.innerHeight }); setSize({ width: window.innerWidth, height: window.innerHeight });
} else {
setPosition(savedState.position);
setSize(savedState.size);
} }
setIsMaximized(!isMaximized); setIsMaximized(!isMaximized);
// 触发 resize 事件通知终端调整大小
setTimeout(() => {
window.dispatchEvent(new Event('resize'));
}, 250);
}; };
// 双击标题栏最大化 // 双击标题栏最大化