重写ssh前端组件,通用化
This commit is contained in:
parent
83c36866cf
commit
e768188ca5
@ -22,6 +22,7 @@ export const Terminal: React.FC<TerminalProps> = ({
|
|||||||
audit,
|
audit,
|
||||||
toolbar,
|
toolbar,
|
||||||
isActive,
|
isActive,
|
||||||
|
compact = false,
|
||||||
onStatusChange,
|
onStatusChange,
|
||||||
onCloseReady,
|
onCloseReady,
|
||||||
onError,
|
onError,
|
||||||
@ -276,6 +277,7 @@ export const Terminal: React.FC<TerminalProps> = ({
|
|||||||
fontSize={fontSize}
|
fontSize={fontSize}
|
||||||
currentTheme={currentTheme}
|
currentTheme={currentTheme}
|
||||||
themes={TERMINAL_THEMES}
|
themes={TERMINAL_THEMES}
|
||||||
|
compact={compact}
|
||||||
onSearch={handleSearch}
|
onSearch={handleSearch}
|
||||||
onClear={handleClear}
|
onClear={handleClear}
|
||||||
onCopy={handleCopy}
|
onCopy={handleCopy}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import type { LayoutOrientation } from './types';
|
|||||||
|
|
||||||
interface TerminalSplitDividerProps {
|
interface TerminalSplitDividerProps {
|
||||||
orientation: LayoutOrientation;
|
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 }) => {
|
export const TerminalSplitDivider: React.FC<TerminalSplitDividerProps> = ({ orientation, onResize }) => {
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const startPosRef = useRef(0);
|
const startPosRef = useRef(0);
|
||||||
|
const dividerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const lastUpdateRef = useRef(0);
|
||||||
|
const lastMousePosRef = useRef(0); // 记录上一次鼠标位置
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isDragging) return;
|
if (!isDragging) return;
|
||||||
|
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
// 设置拖动时的鼠标样式
|
||||||
const delta = orientation === 'horizontal'
|
const cursor = orientation === 'horizontal' ? 'col-resize' : 'row-resize';
|
||||||
? e.clientX - startPosRef.current
|
document.body.style.cursor = cursor;
|
||||||
: e.clientY - startPosRef.current;
|
document.body.style.userSelect = 'none';
|
||||||
|
|
||||||
onResize(delta);
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
startPosRef.current = orientation === 'horizontal' ? e.clientX : e.clientY;
|
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 = () => {
|
const handleMouseUp = () => {
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
|
document.body.style.cursor = '';
|
||||||
|
document.body.style.userSelect = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('mousemove', handleMouseMove);
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
@ -41,33 +72,46 @@ export const TerminalSplitDivider: React.FC<TerminalSplitDividerProps> = ({ orie
|
|||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('mousemove', handleMouseMove);
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
document.removeEventListener('mouseup', handleMouseUp);
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
document.body.style.cursor = '';
|
||||||
|
document.body.style.userSelect = '';
|
||||||
};
|
};
|
||||||
}, [isDragging, orientation, onResize]);
|
}, [isDragging, orientation, onResize]);
|
||||||
|
|
||||||
const handleMouseDown = (e: React.MouseEvent) => {
|
const handleMouseDown = (e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
const initialPos = orientation === 'horizontal' ? e.clientX : e.clientY;
|
||||||
|
startPosRef.current = initialPos;
|
||||||
|
lastMousePosRef.current = initialPos;
|
||||||
setIsDragging(true);
|
setIsDragging(true);
|
||||||
startPosRef.current = orientation === 'horizontal' ? e.clientX : e.clientY;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={dividerRef}
|
||||||
className={`
|
className={`
|
||||||
${orientation === 'horizontal' ? 'w-1 cursor-col-resize' : 'h-1 cursor-row-resize'}
|
${orientation === 'horizontal' ? 'w-[2px] cursor-col-resize' : 'h-[2px] cursor-row-resize'}
|
||||||
${isDragging ? 'bg-blue-500' : 'bg-gray-300 dark:bg-gray-700 hover:bg-blue-400'}
|
${isDragging ? 'bg-blue-500' : 'bg-gray-400 dark:bg-gray-600 hover:bg-blue-500'}
|
||||||
transition-colors
|
transition-all duration-150
|
||||||
relative
|
relative
|
||||||
group
|
group
|
||||||
|
flex-shrink-0
|
||||||
`}
|
`}
|
||||||
onMouseDown={handleMouseDown}
|
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
|
<div
|
||||||
className={`
|
className={`
|
||||||
absolute
|
absolute
|
||||||
${orientation === 'horizontal'
|
${orientation === 'horizontal'
|
||||||
? '-left-1 -right-1 top-0 bottom-0'
|
? 'w-3 -left-1.5 top-0 bottom-0'
|
||||||
: 'left-0 right-0 -top-1 -bottom-1'
|
: 'h-3 left-0 right-0 -top-1.5'
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -22,10 +22,11 @@ export interface TerminalSplitNodeProps {
|
|||||||
onSplitLeft: () => void;
|
onSplitLeft: () => void;
|
||||||
onSplitRight: () => void;
|
onSplitRight: () => void;
|
||||||
onFocus: (groupId: string) => void;
|
onFocus: (groupId: string) => void;
|
||||||
onResize: (nodeId: string, delta: number) => void;
|
onResize: (nodeId: string, delta: number, containerSize?: number) => void;
|
||||||
getConnectionConfig: (tab: TerminalTab) => TerminalConnectionConfig;
|
getConnectionConfig: (tab: TerminalTab) => TerminalConnectionConfig;
|
||||||
getAuditConfig: () => TerminalAuditConfig;
|
getAuditConfig: () => TerminalAuditConfig;
|
||||||
getToolbarConfig: () => TerminalToolbarConfig;
|
getToolbarConfig: () => TerminalToolbarConfig;
|
||||||
|
hasMultipleGroups?: boolean; // 是否有多个分屏组
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -48,6 +49,7 @@ const TerminalSplitNodeComponent: React.FC<TerminalSplitNodeProps> = ({
|
|||||||
getConnectionConfig,
|
getConnectionConfig,
|
||||||
getAuditConfig,
|
getAuditConfig,
|
||||||
getToolbarConfig,
|
getToolbarConfig,
|
||||||
|
hasMultipleGroups = false,
|
||||||
}) => {
|
}) => {
|
||||||
// 渲染 Group 节点(包含 Tabs 和 Terminal)
|
// 渲染 Group 节点(包含 Tabs 和 Terminal)
|
||||||
if (node.type === 'group') {
|
if (node.type === 'group') {
|
||||||
@ -126,6 +128,7 @@ const TerminalSplitNodeComponent: React.FC<TerminalSplitNodeProps> = ({
|
|||||||
audit={auditConfig}
|
audit={auditConfig}
|
||||||
toolbar={toolbarConfig}
|
toolbar={toolbarConfig}
|
||||||
isActive={tab.isActive}
|
isActive={tab.isActive}
|
||||||
|
compact={hasMultipleGroups}
|
||||||
onSplitUp={onSplitUp}
|
onSplitUp={onSplitUp}
|
||||||
onSplitDown={onSplitDown}
|
onSplitDown={onSplitDown}
|
||||||
onSplitLeft={onSplitLeft}
|
onSplitLeft={onSplitLeft}
|
||||||
@ -168,6 +171,7 @@ const TerminalSplitNodeComponent: React.FC<TerminalSplitNodeProps> = ({
|
|||||||
getConnectionConfig={getConnectionConfig}
|
getConnectionConfig={getConnectionConfig}
|
||||||
getAuditConfig={getAuditConfig}
|
getAuditConfig={getAuditConfig}
|
||||||
getToolbarConfig={getToolbarConfig}
|
getToolbarConfig={getToolbarConfig}
|
||||||
|
hasMultipleGroups={hasMultipleGroups}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -175,7 +179,7 @@ const TerminalSplitNodeComponent: React.FC<TerminalSplitNodeProps> = ({
|
|||||||
{index < node.children.length - 1 && (
|
{index < node.children.length - 1 && (
|
||||||
<TerminalSplitDivider
|
<TerminalSplitDivider
|
||||||
orientation={node.orientation}
|
orientation={node.orientation}
|
||||||
onResize={(delta) => onResize(child.id, delta)}
|
onResize={(delta, containerSize) => onResize(child.id, delta, containerSize)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useSplitView } from './useSplitView';
|
import { useSplitView } from './useSplitView';
|
||||||
import { TerminalSplitNode } from './TerminalSplitNode';
|
import { TerminalSplitNode } from './TerminalSplitNode';
|
||||||
import type { TerminalTab, TerminalConnectionConfig, TerminalAuditConfig, TerminalToolbarConfig } from './types';
|
import type { TerminalTab, TerminalConnectionConfig, TerminalAuditConfig, TerminalToolbarConfig, SplitNode } from './types';
|
||||||
|
|
||||||
// 对外暴露的主组件Props
|
// 对外暴露的主组件Props
|
||||||
export interface TerminalSplitViewProps {
|
export interface TerminalSplitViewProps {
|
||||||
@ -85,6 +85,13 @@ export const TerminalSplitView: React.FC<TerminalSplitViewProps> = ({
|
|||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
}, [splitUp, splitDown, splitLeft, splitRight]);
|
}, [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 (
|
return (
|
||||||
<div className="terminal-split-container h-full w-full">
|
<div className="terminal-split-container h-full w-full">
|
||||||
<TerminalSplitNode
|
<TerminalSplitNode
|
||||||
@ -102,6 +109,7 @@ export const TerminalSplitView: React.FC<TerminalSplitViewProps> = ({
|
|||||||
getConnectionConfig={getConnectionConfig}
|
getConnectionConfig={getConnectionConfig}
|
||||||
getAuditConfig={getAuditConfig}
|
getAuditConfig={getAuditConfig}
|
||||||
getToolbarConfig={getToolbarConfig}
|
getToolbarConfig={getToolbarConfig}
|
||||||
|
hasMultipleGroups={hasMultipleGroups}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -23,6 +23,7 @@ interface TerminalToolbarProps {
|
|||||||
fontSize: number;
|
fontSize: number;
|
||||||
currentTheme?: string;
|
currentTheme?: string;
|
||||||
themes?: TerminalTheme[];
|
themes?: TerminalTheme[];
|
||||||
|
compact?: boolean; // 紧凑模式
|
||||||
onSearch?: () => void;
|
onSearch?: () => void;
|
||||||
onClear?: () => void;
|
onClear?: () => void;
|
||||||
onCopy?: () => void;
|
onCopy?: () => void;
|
||||||
@ -44,6 +45,7 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({
|
|||||||
fontSize,
|
fontSize,
|
||||||
currentTheme,
|
currentTheme,
|
||||||
themes = [],
|
themes = [],
|
||||||
|
compact = false,
|
||||||
onSearch,
|
onSearch,
|
||||||
onClear,
|
onClear,
|
||||||
onCopy,
|
onCopy,
|
||||||
@ -60,6 +62,23 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({
|
|||||||
if (!config.show) return null;
|
if (!config.show) return null;
|
||||||
|
|
||||||
const getStatusBadge = () => {
|
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) {
|
switch (connectionStatus) {
|
||||||
case 'connecting':
|
case 'connecting':
|
||||||
return <Badge variant="outline" className="bg-yellow-100 text-yellow-700"><Loader2 className="mr-1 h-3 w-3 animate-spin" />连接中</Badge>;
|
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}>
|
<div className={styles.left}>
|
||||||
{config.showStatus && getStatusBadge()}
|
{config.showStatus && getStatusBadge()}
|
||||||
{config.showFontSizeLabel && (
|
{config.showFontSizeLabel && (
|
||||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
<span className={compact ? "text-[12px] text-gray-500 dark:text-gray-400" : "text-xs text-gray-500 dark:text-gray-400"}>
|
||||||
字体 {fontSize}px
|
{compact ? `${fontSize}px` : `字体 ${fontSize}px`}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -217,6 +217,8 @@ export interface TerminalProps {
|
|||||||
toolbar?: TerminalToolbarConfig;
|
toolbar?: TerminalToolbarConfig;
|
||||||
/** 是否为激活状态(用于Tab切换时重新focus) */
|
/** 是否为激活状态(用于Tab切换时重新focus) */
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
|
/** 紧凑模式(多分屏时自动启用) */
|
||||||
|
compact?: boolean;
|
||||||
/** 连接状态变化回调 */
|
/** 连接状态变化回调 */
|
||||||
onStatusChange?: (status: ConnectionStatus) => void;
|
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 => {
|
setLayout(prev => {
|
||||||
const result = findParent(prev.root, nodeId);
|
const result = findParent(prev.root, nodeId);
|
||||||
if (!result || !result.parent) return prev;
|
if (!result || !result.parent) return prev;
|
||||||
@ -381,7 +381,15 @@ export const useSplitView = ({ initialTab, onWindowClose }: UseSplitViewOptions)
|
|||||||
const next = parent.children[index + 1];
|
const next = parent.children[index + 1];
|
||||||
const totalSize = current.size + next.size;
|
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 newNextSize = totalSize - newCurrentSize;
|
||||||
|
|
||||||
const newChildren = parent.children.map((child, i) => {
|
const newChildren = parent.children.map((child, i) => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user