deploy-ease-platform/frontend/src/components/Terminal/TerminalToolbar.tsx
2025-12-07 01:05:25 +08:00

281 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Terminal 工具栏组件
*/
import React from 'react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Search, Trash2, Copy, ZoomIn, ZoomOut, RotateCcw, Loader2, XCircle, Palette, SplitSquareVertical, SplitSquareHorizontal, Plus, ChevronDown, FolderOpen } 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[];
compact?: boolean; // 紧凑模式
onSearch?: () => void;
onClear?: () => void;
onCopy?: () => void;
onZoomIn?: () => void;
onZoomOut?: () => void;
onReconnect?: () => void;
onThemeChange?: (themeName: string) => void;
onFileManager?: () => void;
// 分屏操作
onSplitUp?: () => void;
onSplitDown?: () => void;
onSplitLeft?: () => void;
onSplitRight?: () => void;
onSplitInGroup?: () => void;
}
export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({
config,
connectionStatus,
fontSize,
currentTheme,
themes = [],
compact = false,
onSearch,
onClear,
onCopy,
onZoomIn,
onZoomOut,
onReconnect,
onThemeChange,
onFileManager,
onSplitUp,
onSplitDown,
onSplitLeft,
onSplitRight,
onSplitInGroup,
}) => {
if (!config.show) return null;
const getStatusBadge = () => {
// 紧凑模式:只显示状态圆点
if (compact) {
switch (connectionStatus) {
case 'connecting':
return <div className="h-2 w-2 rounded-full bg-yellow-500 animate-pulse" title="连接中" />;
case 'connected':
return <div className="h-2 w-2 rounded-full bg-emerald-500 animate-pulse" title="已连接" />;
case 'reconnecting':
return <div className="h-2 w-2 rounded-full bg-orange-500 animate-pulse" title="重连中" />;
case 'error':
return <div className="h-2 w-2 rounded-full bg-red-500" title="连接失败" />;
case 'disconnected':
return <div className="h-2 w-2 rounded-full bg-gray-400" title="已断开" />;
}
}
// 正常模式显示带文字的Badge
switch (connectionStatus) {
case 'connecting':
return <Badge variant="outline" className="bg-yellow-100 text-yellow-700"><Loader2 className="mr-1 h-3 w-3 animate-spin" /></Badge>;
case 'connected':
return <Badge variant="outline" className="bg-emerald-100 text-emerald-700"><div className="mr-1 h-2 w-2 rounded-full bg-emerald-500 animate-pulse" /></Badge>;
case 'reconnecting':
return <Badge variant="outline" className="bg-orange-100 text-orange-700"><Loader2 className="mr-1 h-3 w-3 animate-spin" /></Badge>;
case 'error':
return <Badge variant="outline" className="bg-red-100 text-red-700"><XCircle className="mr-1 h-3 w-3" /></Badge>;
case 'disconnected':
return <Badge variant="outline" className="bg-gray-100 text-gray-700"><XCircle className="mr-1 h-3 w-3" /></Badge>;
}
};
return (
<div className={styles.toolbar}>
{/* 左侧:状态指示器 */}
<div className={styles.left}>
{config.showStatus && getStatusBadge()}
{config.showFontSizeLabel && (
<span className={compact ? "text-[12px] text-gray-500 dark:text-gray-400" : "text-xs text-gray-500 dark:text-gray-400"}>
{compact ? `${fontSize}px` : `字体 ${fontSize}px`}
</span>
)}
</div>
{/* 右侧:所有操作按钮 */}
<div className={styles.right}>
{/* 文件管理 */}
{onFileManager && (
<Button
size="sm"
variant="ghost"
className="h-7 px-2"
onClick={onFileManager}
title="文件管理"
>
<FolderOpen className="h-3.5 w-3.5" />
</Button>
)}
{config.showSearch && (
<Button
size="sm"
variant="ghost"
className="h-7 px-2"
onClick={onSearch}
title="搜索 (Ctrl+F)"
>
<Search className="h-3.5 w-3.5" />
</Button>
)}
{config.showClear && (
<Button
size="sm"
variant="ghost"
className="h-7 px-2"
onClick={onClear}
title="清屏 (Ctrl+L)"
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
)}
{config.showCopy && (
<Button
size="sm"
variant="ghost"
className="h-7 px-2"
onClick={onCopy}
title="复制选中 (Ctrl+C)"
>
<Copy className="h-3.5 w-3.5" />
</Button>
)}
{config.showFontSize && (
<>
<div className="h-4 w-px bg-gray-300 dark:bg-gray-600 mx-1" />
<Button
size="sm"
variant="ghost"
className="h-7 px-2"
onClick={onZoomOut}
title="缩小字体"
>
<ZoomOut className="h-3.5 w-3.5" />
</Button>
<Button
size="sm"
variant="ghost"
className="h-7 px-2"
onClick={onZoomIn}
title="放大字体"
>
<ZoomIn className="h-3.5 w-3.5" />
</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={compact ? "h-7 w-[120px] text-xs" : "h-7 w-[200px] text-xs"}>
<Palette className="h-3.5 w-3.5 mr-1" />
<SelectValue placeholder="主题" />
</SelectTrigger>
<SelectContent className="z-[9999]">
{themes.map((theme) => {
// 紧凑模式:只显示英文名(去掉括号和中文)
const displayLabel = compact
? theme.label.split('')[0].split('(')[0].trim()
: theme.label;
return (
<SelectItem key={theme.name} value={theme.name}>
{displayLabel}
</SelectItem>
);
})}
</SelectContent>
</Select>
</>
)}
{/* 分屏菜单 */}
{(onSplitUp || onSplitDown || onSplitLeft || onSplitRight || onSplitInGroup) && (
<>
<div className="h-4 w-px bg-gray-300 dark:bg-gray-600 mx-1" />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-7 px-2 relative z-50"
title="分屏"
>
<SplitSquareVertical className="h-3.5 w-3.5 mr-1" />
<ChevronDown className="h-3 w-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="z-[9999]">
{onSplitUp && (
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onSplitUp(); }}>
<SplitSquareHorizontal className="h-4 w-4 mr-2" />
</DropdownMenuItem>
)}
{onSplitDown && (
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onSplitDown(); }}>
<SplitSquareHorizontal className="h-4 w-4 mr-2" />
</DropdownMenuItem>
)}
{(onSplitUp || onSplitDown) && (onSplitLeft || onSplitRight) && (
<DropdownMenuSeparator />
)}
{onSplitLeft && (
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onSplitLeft(); }}>
<SplitSquareVertical className="h-4 w-4 mr-2" />
</DropdownMenuItem>
)}
{onSplitRight && (
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onSplitRight(); }}>
<SplitSquareVertical className="h-4 w-4 mr-2" />
</DropdownMenuItem>
)}
{onSplitInGroup && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onSplitInGroup(); }}>
<Plus className="h-4 w-4 mr-2" />
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</>
)}
{(connectionStatus === 'disconnected' || connectionStatus === 'error') && (
<>
<div className="h-4 w-px bg-gray-300 dark:bg-gray-600 mx-1" />
<Button
size="sm"
variant="outline"
className="h-7"
onClick={onReconnect}
>
<RotateCcw className="h-3.5 w-3.5 mr-1" />
</Button>
</>
)}
{config.extraActions}
</div>
</div>
);
};