重写ssh前端组件,通用化

This commit is contained in:
dengqichen 2025-12-06 23:10:37 +08:00
parent 83c36866cf
commit e768188ca5
7 changed files with 106 additions and 19 deletions

View File

@ -22,6 +22,7 @@ export const Terminal: React.FC<TerminalProps> = ({
audit,
toolbar,
isActive,
compact = false,
onStatusChange,
onCloseReady,
onError,
@ -276,6 +277,7 @@ export const Terminal: React.FC<TerminalProps> = ({
fontSize={fontSize}
currentTheme={currentTheme}
themes={TERMINAL_THEMES}
compact={compact}
onSearch={handleSearch}
onClear={handleClear}
onCopy={handleCopy}

View File

@ -7,7 +7,7 @@ import type { LayoutOrientation } from './types';
interface TerminalSplitDividerProps {
orientation: LayoutOrientation;
onResize: (delta: number) => void;
onResize: (delta: number, containerSize?: number) => void;
}
/**
@ -18,21 +18,52 @@ interface TerminalSplitDividerProps {
export const TerminalSplitDivider: React.FC<TerminalSplitDividerProps> = ({ orientation, onResize }) => {
const [isDragging, setIsDragging] = useState(false);
const startPosRef = useRef(0);
const dividerRef = useRef<HTMLDivElement>(null);
const lastUpdateRef = useRef(0);
const lastMousePosRef = useRef(0); // 记录上一次鼠标位置
useEffect(() => {
if (!isDragging) return;
const handleMouseMove = (e: MouseEvent) => {
const delta = orientation === 'horizontal'
? e.clientX - startPosRef.current
: e.clientY - startPosRef.current;
// 设置拖动时的鼠标样式
const cursor = orientation === 'horizontal' ? 'col-resize' : 'row-resize';
document.body.style.cursor = cursor;
document.body.style.userSelect = 'none';
onResize(delta);
startPosRef.current = orientation === 'horizontal' ? e.clientX : e.clientY;
const handleMouseMove = (e: MouseEvent) => {
e.preventDefault();
const currentMousePos = orientation === 'horizontal' ? e.clientX : e.clientY;
// 节流限制更新频率为16ms约60fps
const now = Date.now();
if (now - lastUpdateRef.current < 16) {
return;
}
lastUpdateRef.current = now;
// 计算相对于上一帧的增量
const delta = currentMousePos - lastMousePosRef.current;
// 如果没有移动,跳过
if (delta === 0) {
return;
}
// 获取父容器的尺寸
const container = dividerRef.current?.parentElement;
const containerSize = container
? (orientation === 'horizontal' ? container.clientWidth : container.clientHeight)
: undefined;
onResize(delta, containerSize);
lastMousePosRef.current = currentMousePos;
};
const handleMouseUp = () => {
setIsDragging(false);
document.body.style.cursor = '';
document.body.style.userSelect = '';
};
document.addEventListener('mousemove', handleMouseMove);
@ -41,33 +72,46 @@ export const TerminalSplitDivider: React.FC<TerminalSplitDividerProps> = ({ orie
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
document.body.style.cursor = '';
document.body.style.userSelect = '';
};
}, [isDragging, orientation, onResize]);
const handleMouseDown = (e: React.MouseEvent) => {
e.preventDefault();
const initialPos = orientation === 'horizontal' ? e.clientX : e.clientY;
startPosRef.current = initialPos;
lastMousePosRef.current = initialPos;
setIsDragging(true);
startPosRef.current = orientation === 'horizontal' ? e.clientX : e.clientY;
};
return (
<div
ref={dividerRef}
className={`
${orientation === 'horizontal' ? 'w-1 cursor-col-resize' : 'h-1 cursor-row-resize'}
${isDragging ? 'bg-blue-500' : 'bg-gray-300 dark:bg-gray-700 hover:bg-blue-400'}
transition-colors
${orientation === 'horizontal' ? 'w-[2px] cursor-col-resize' : 'h-[2px] cursor-row-resize'}
${isDragging ? 'bg-blue-500' : 'bg-gray-400 dark:bg-gray-600 hover:bg-blue-500'}
transition-all duration-150
relative
group
flex-shrink-0
`}
onMouseDown={handleMouseDown}
>
{/* 中心可见线 */}
<div className={`
absolute inset-0
${orientation === 'horizontal' ? 'w-[1px] left-1/2 -translate-x-1/2' : 'h-[1px] top-1/2 -translate-y-1/2'}
${isDragging ? 'bg-blue-600' : 'bg-gray-500 dark:bg-gray-500'}
`} />
{/* 拖动热区 - 增加可拖动区域 */}
<div
className={`
absolute
${orientation === 'horizontal'
? '-left-1 -right-1 top-0 bottom-0'
: 'left-0 right-0 -top-1 -bottom-1'
? 'w-3 -left-1.5 top-0 bottom-0'
: 'h-3 left-0 right-0 -top-1.5'
}
`}
/>

View File

@ -22,10 +22,11 @@ export interface TerminalSplitNodeProps {
onSplitLeft: () => void;
onSplitRight: () => void;
onFocus: (groupId: string) => void;
onResize: (nodeId: string, delta: number) => void;
onResize: (nodeId: string, delta: number, containerSize?: number) => void;
getConnectionConfig: (tab: TerminalTab) => TerminalConnectionConfig;
getAuditConfig: () => TerminalAuditConfig;
getToolbarConfig: () => TerminalToolbarConfig;
hasMultipleGroups?: boolean; // 是否有多个分屏组
}
/**
@ -48,6 +49,7 @@ const TerminalSplitNodeComponent: React.FC<TerminalSplitNodeProps> = ({
getConnectionConfig,
getAuditConfig,
getToolbarConfig,
hasMultipleGroups = false,
}) => {
// 渲染 Group 节点(包含 Tabs 和 Terminal
if (node.type === 'group') {
@ -126,6 +128,7 @@ const TerminalSplitNodeComponent: React.FC<TerminalSplitNodeProps> = ({
audit={auditConfig}
toolbar={toolbarConfig}
isActive={tab.isActive}
compact={hasMultipleGroups}
onSplitUp={onSplitUp}
onSplitDown={onSplitDown}
onSplitLeft={onSplitLeft}
@ -168,6 +171,7 @@ const TerminalSplitNodeComponent: React.FC<TerminalSplitNodeProps> = ({
getConnectionConfig={getConnectionConfig}
getAuditConfig={getAuditConfig}
getToolbarConfig={getToolbarConfig}
hasMultipleGroups={hasMultipleGroups}
/>
</div>
@ -175,7 +179,7 @@ const TerminalSplitNodeComponent: React.FC<TerminalSplitNodeProps> = ({
{index < node.children.length - 1 && (
<TerminalSplitDivider
orientation={node.orientation}
onResize={(delta) => onResize(child.id, delta)}
onResize={(delta, containerSize) => onResize(child.id, delta, containerSize)}
/>
)}
</React.Fragment>

View File

@ -5,7 +5,7 @@
import React, { useEffect } from 'react';
import { useSplitView } from './useSplitView';
import { TerminalSplitNode } from './TerminalSplitNode';
import type { TerminalTab, TerminalConnectionConfig, TerminalAuditConfig, TerminalToolbarConfig } from './types';
import type { TerminalTab, TerminalConnectionConfig, TerminalAuditConfig, TerminalToolbarConfig, SplitNode } from './types';
// 对外暴露的主组件Props
export interface TerminalSplitViewProps {
@ -85,6 +85,13 @@ export const TerminalSplitView: React.FC<TerminalSplitViewProps> = ({
return () => window.removeEventListener('keydown', handleKeyDown);
}, [splitUp, splitDown, splitLeft, splitRight]);
// 计算是否有多个分屏组
const countGroups = (node: SplitNode): number => {
if (node.type === 'group') return 1;
return node.children.reduce((sum, child) => sum + countGroups(child), 0);
};
const hasMultipleGroups = countGroups(layout.root) > 1;
return (
<div className="terminal-split-container h-full w-full">
<TerminalSplitNode
@ -102,6 +109,7 @@ export const TerminalSplitView: React.FC<TerminalSplitViewProps> = ({
getConnectionConfig={getConnectionConfig}
getAuditConfig={getAuditConfig}
getToolbarConfig={getToolbarConfig}
hasMultipleGroups={hasMultipleGroups}
/>
</div>
);

View File

@ -23,6 +23,7 @@ interface TerminalToolbarProps {
fontSize: number;
currentTheme?: string;
themes?: TerminalTheme[];
compact?: boolean; // 紧凑模式
onSearch?: () => void;
onClear?: () => void;
onCopy?: () => void;
@ -44,6 +45,7 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({
fontSize,
currentTheme,
themes = [],
compact = false,
onSearch,
onClear,
onCopy,
@ -60,6 +62,23 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({
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>;
@ -80,8 +99,8 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({
<div className={styles.left}>
{config.showStatus && getStatusBadge()}
{config.showFontSizeLabel && (
<span className="text-xs text-gray-500 dark:text-gray-400">
{fontSize}px
<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>

View File

@ -217,6 +217,8 @@ export interface TerminalProps {
toolbar?: TerminalToolbarConfig;
/** 是否为激活状态用于Tab切换时重新focus */
isActive?: boolean;
/** 紧凑模式(多分屏时自动启用) */
compact?: boolean;
/** 连接状态变化回调 */
onStatusChange?: (status: ConnectionStatus) => void;
/** 关闭就绪回调 */

View File

@ -369,7 +369,7 @@ export const useSplitView = ({ initialTab, onWindowClose }: UseSplitViewOptions)
};
// 调整分屏大小
const resizeGroups = useCallback((nodeId: string, delta: number) => {
const resizeGroups = useCallback((nodeId: string, delta: number, containerSize?: number) => {
setLayout(prev => {
const result = findParent(prev.root, nodeId);
if (!result || !result.parent) return prev;
@ -381,7 +381,15 @@ export const useSplitView = ({ initialTab, onWindowClose }: UseSplitViewOptions)
const next = parent.children[index + 1];
const totalSize = current.size + next.size;
const newCurrentSize = Math.max(20, Math.min(totalSize - 20, current.size + delta));
// 将像素delta转换为百分比delta
// delta是像素值需要转换为百分比(delta / containerSize) * totalSize
const deltaPercent = containerSize ? (delta / containerSize) * totalSize : delta;
// 计算最小尺寸百分比520px / 容器尺寸 * 100
// 如果没有容器尺寸使用默认20%作为兜底
const minSizePercent = containerSize ? Math.min(50, (520 / containerSize) * 100) : 20;
const newCurrentSize = Math.max(minSizePercent, Math.min(totalSize - minSizePercent, current.size + deltaPercent));
const newNextSize = totalSize - newCurrentSize;
const newChildren = parent.children.map((child, i) => {