重写ssh前端组件,通用化
This commit is contained in:
parent
83c36866cf
commit
e768188ca5
@ -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}
|
||||
|
||||
@ -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 cursor = orientation === 'horizontal' ? 'col-resize' : 'row-resize';
|
||||
document.body.style.cursor = cursor;
|
||||
document.body.style.userSelect = 'none';
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
const delta = orientation === 'horizontal'
|
||||
? e.clientX - startPosRef.current
|
||||
: e.clientY - startPosRef.current;
|
||||
e.preventDefault();
|
||||
|
||||
onResize(delta);
|
||||
startPosRef.current = orientation === 'horizontal' ? e.clientX : e.clientY;
|
||||
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'
|
||||
}
|
||||
`}
|
||||
/>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -217,6 +217,8 @@ export interface TerminalProps {
|
||||
toolbar?: TerminalToolbarConfig;
|
||||
/** 是否为激活状态(用于Tab切换时重新focus) */
|
||||
isActive?: boolean;
|
||||
/** 紧凑模式(多分屏时自动启用) */
|
||||
compact?: boolean;
|
||||
/** 连接状态变化回调 */
|
||||
onStatusChange?: (status: ConnectionStatus) => void;
|
||||
/** 关闭就绪回调 */
|
||||
|
||||
@ -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) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user