重写ssh前端组件,通用化
This commit is contained in:
parent
980c827feb
commit
f6abb48333
@ -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<TerminalProps> = ({
|
||||
@ -24,6 +26,8 @@ export const Terminal: React.FC<TerminalProps> = ({
|
||||
}) => {
|
||||
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<TerminalProps> = ({
|
||||
cleanup,
|
||||
} = useTerminal({
|
||||
connection,
|
||||
display: { ...display, fontSize },
|
||||
display: {
|
||||
...display,
|
||||
fontSize,
|
||||
theme: getThemeByName(currentTheme).theme,
|
||||
},
|
||||
onStatusChange,
|
||||
onError,
|
||||
});
|
||||
@ -82,9 +90,27 @@ export const Terminal: React.FC<TerminalProps> = ({
|
||||
};
|
||||
}, [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<TerminalProps> = ({
|
||||
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<TerminalProps> = ({
|
||||
}
|
||||
}, [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 (
|
||||
<div className={styles.terminalWrapper}>
|
||||
{/* 工具栏 */}
|
||||
@ -202,16 +241,80 @@ export const Terminal: React.FC<TerminalProps> = ({
|
||||
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}
|
||||
/>
|
||||
|
||||
{/* 终端区域 */}
|
||||
<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
|
||||
ref={terminalRef}
|
||||
className={styles.content}
|
||||
|
||||
@ -4,32 +4,40 @@
|
||||
import React from 'react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
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 { TerminalTheme } from './themes';
|
||||
import styles from './index.module.less';
|
||||
|
||||
interface TerminalToolbarProps {
|
||||
config: TerminalToolbarConfig;
|
||||
connectionStatus: ConnectionStatus;
|
||||
fontSize: number;
|
||||
currentTheme?: string;
|
||||
themes?: TerminalTheme[];
|
||||
onSearch?: () => void;
|
||||
onClear?: () => void;
|
||||
onCopy?: () => void;
|
||||
onZoomIn?: () => void;
|
||||
onZoomOut?: () => void;
|
||||
onReconnect?: () => void;
|
||||
onThemeChange?: (themeName: string) => void;
|
||||
}
|
||||
|
||||
export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({
|
||||
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<TerminalToolbarProps> = ({
|
||||
</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') && (
|
||||
<>
|
||||
<div className="h-4 w-px bg-gray-300 dark:bg-gray-600 mx-1" />
|
||||
|
||||
@ -58,12 +58,18 @@ export function TerminalWindowManager<TResource = any>({
|
||||
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<TResource> = {
|
||||
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',
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
324
frontend/src/components/Terminal/themes.ts
Normal file
324
frontend/src/components/Terminal/themes.ts
Normal 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];
|
||||
};
|
||||
@ -53,9 +53,16 @@ export const DraggableWindow: React.FC<DraggableWindowProps> = ({
|
||||
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<DraggableWindowProps> = ({
|
||||
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<DraggableWindowProps> = ({
|
||||
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<DraggableWindowProps> = ({
|
||||
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);
|
||||
};
|
||||
|
||||
// 双击标题栏最大化
|
||||
|
||||
Loading…
Reference in New Issue
Block a user