From e768188ca5a099f02e24f73eef61bdd69c00c5c2 Mon Sep 17 00:00:00 2001 From: dengqichen Date: Sat, 6 Dec 2025 23:10:37 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E5=86=99ssh=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=EF=BC=8C=E9=80=9A=E7=94=A8=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/Terminal/Terminal.tsx | 2 + .../Terminal/TerminalSplitDivider.tsx | 68 +++++++++++++++---- .../components/Terminal/TerminalSplitNode.tsx | 8 ++- .../components/Terminal/TerminalSplitView.tsx | 10 ++- .../components/Terminal/TerminalToolbar.tsx | 23 ++++++- frontend/src/components/Terminal/types.ts | 2 + .../src/components/Terminal/useSplitView.ts | 12 +++- 7 files changed, 106 insertions(+), 19 deletions(-) diff --git a/frontend/src/components/Terminal/Terminal.tsx b/frontend/src/components/Terminal/Terminal.tsx index 25bd404d..dbb13685 100644 --- a/frontend/src/components/Terminal/Terminal.tsx +++ b/frontend/src/components/Terminal/Terminal.tsx @@ -22,6 +22,7 @@ export const Terminal: React.FC = ({ audit, toolbar, isActive, + compact = false, onStatusChange, onCloseReady, onError, @@ -276,6 +277,7 @@ export const Terminal: React.FC = ({ fontSize={fontSize} currentTheme={currentTheme} themes={TERMINAL_THEMES} + compact={compact} onSearch={handleSearch} onClear={handleClear} onCopy={handleCopy} diff --git a/frontend/src/components/Terminal/TerminalSplitDivider.tsx b/frontend/src/components/Terminal/TerminalSplitDivider.tsx index d77a5339..44a28a1f 100644 --- a/frontend/src/components/Terminal/TerminalSplitDivider.tsx +++ b/frontend/src/components/Terminal/TerminalSplitDivider.tsx @@ -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 = ({ orientation, onResize }) => { const [isDragging, setIsDragging] = useState(false); const startPosRef = useRef(0); + const dividerRef = useRef(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 = ({ 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 (
+ {/* 中心可见线 */} +
+ {/* 拖动热区 - 增加可拖动区域 */}
diff --git a/frontend/src/components/Terminal/TerminalSplitNode.tsx b/frontend/src/components/Terminal/TerminalSplitNode.tsx index 837e72e5..9c4816ea 100644 --- a/frontend/src/components/Terminal/TerminalSplitNode.tsx +++ b/frontend/src/components/Terminal/TerminalSplitNode.tsx @@ -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 = ({ getConnectionConfig, getAuditConfig, getToolbarConfig, + hasMultipleGroups = false, }) => { // 渲染 Group 节点(包含 Tabs 和 Terminal) if (node.type === 'group') { @@ -126,6 +128,7 @@ const TerminalSplitNodeComponent: React.FC = ({ audit={auditConfig} toolbar={toolbarConfig} isActive={tab.isActive} + compact={hasMultipleGroups} onSplitUp={onSplitUp} onSplitDown={onSplitDown} onSplitLeft={onSplitLeft} @@ -168,6 +171,7 @@ const TerminalSplitNodeComponent: React.FC = ({ getConnectionConfig={getConnectionConfig} getAuditConfig={getAuditConfig} getToolbarConfig={getToolbarConfig} + hasMultipleGroups={hasMultipleGroups} />
@@ -175,7 +179,7 @@ const TerminalSplitNodeComponent: React.FC = ({ {index < node.children.length - 1 && ( onResize(child.id, delta)} + onResize={(delta, containerSize) => onResize(child.id, delta, containerSize)} /> )} diff --git a/frontend/src/components/Terminal/TerminalSplitView.tsx b/frontend/src/components/Terminal/TerminalSplitView.tsx index e9897563..83431cd9 100644 --- a/frontend/src/components/Terminal/TerminalSplitView.tsx +++ b/frontend/src/components/Terminal/TerminalSplitView.tsx @@ -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 = ({ 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 (
= ({ getConnectionConfig={getConnectionConfig} getAuditConfig={getAuditConfig} getToolbarConfig={getToolbarConfig} + hasMultipleGroups={hasMultipleGroups} />
); diff --git a/frontend/src/components/Terminal/TerminalToolbar.tsx b/frontend/src/components/Terminal/TerminalToolbar.tsx index 49f72ec0..89b66d37 100644 --- a/frontend/src/components/Terminal/TerminalToolbar.tsx +++ b/frontend/src/components/Terminal/TerminalToolbar.tsx @@ -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 = ({ fontSize, currentTheme, themes = [], + compact = false, onSearch, onClear, onCopy, @@ -60,6 +62,23 @@ export const TerminalToolbar: React.FC = ({ if (!config.show) return null; const getStatusBadge = () => { + // 紧凑模式:只显示状态圆点 + if (compact) { + switch (connectionStatus) { + case 'connecting': + return
; + case 'connected': + return
; + case 'reconnecting': + return
; + case 'error': + return
; + case 'disconnected': + return
; + } + } + + // 正常模式:显示带文字的Badge switch (connectionStatus) { case 'connecting': return 连接中; @@ -80,8 +99,8 @@ export const TerminalToolbar: React.FC = ({
{config.showStatus && getStatusBadge()} {config.showFontSizeLabel && ( - - 字体 {fontSize}px + + {compact ? `${fontSize}px` : `字体 ${fontSize}px`} )}
diff --git a/frontend/src/components/Terminal/types.ts b/frontend/src/components/Terminal/types.ts index 510a3b33..90b04ad8 100644 --- a/frontend/src/components/Terminal/types.ts +++ b/frontend/src/components/Terminal/types.ts @@ -217,6 +217,8 @@ export interface TerminalProps { toolbar?: TerminalToolbarConfig; /** 是否为激活状态(用于Tab切换时重新focus) */ isActive?: boolean; + /** 紧凑模式(多分屏时自动启用) */ + compact?: boolean; /** 连接状态变化回调 */ onStatusChange?: (status: ConnectionStatus) => void; /** 关闭就绪回调 */ diff --git a/frontend/src/components/Terminal/useSplitView.ts b/frontend/src/components/Terminal/useSplitView.ts index d101ca6e..72079bcb 100644 --- a/frontend/src/components/Terminal/useSplitView.ts +++ b/frontend/src/components/Terminal/useSplitView.ts @@ -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) => {