重写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 { 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}
|
||||||
|
|||||||
@ -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" />
|
||||||
|
|||||||
@ -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',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
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;
|
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);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 双击标题栏最大化
|
// 双击标题栏最大化
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user