重写ssh前端组件,通用化

This commit is contained in:
dengqichen 2025-12-07 00:06:02 +08:00
parent 7025eebd51
commit 1cddee8d42
6 changed files with 47 additions and 48 deletions

View File

@ -33,6 +33,7 @@ export const Terminal: React.FC<TerminalProps> = ({
onSplitInGroup, onSplitInGroup,
}) => { }) => {
const terminalRef = useRef<HTMLDivElement>(null); const terminalRef = useRef<HTMLDivElement>(null);
const wrapperRef = useRef<HTMLDivElement>(null); // 最外层容器
const instanceRef = useRef<ReturnType<TerminalInstanceManager['getOrCreate']> | null>(null); const instanceRef = useRef<ReturnType<TerminalInstanceManager['getOrCreate']> | null>(null);
const [fontSize, setFontSize] = useState(display?.fontSize ?? 14); const [fontSize, setFontSize] = useState(display?.fontSize ?? 14);
@ -121,12 +122,23 @@ export const Terminal: React.FC<TerminalProps> = ({
setTimeout(() => { setTimeout(() => {
const fitAddon = instanceRef.current?.getFitAddon(); const fitAddon = instanceRef.current?.getFitAddon();
if (fitAddon && instanceRef.current) { if (fitAddon && instanceRef.current) {
// 检查各层容器尺寸
if (wrapperRef.current && terminalRef.current) {
console.log(`[Terminal ${id}] 📦 Wrapper尺寸: ${wrapperRef.current.clientWidth}px × ${wrapperRef.current.clientHeight}px`);
console.log(`[Terminal ${id}] 📦 Content尺寸: ${terminalRef.current.clientWidth}px × ${terminalRef.current.clientHeight}px`);
}
fitAddon.fit(); fitAddon.fit();
// 发送新的尺寸给后端 // 发送新的尺寸给后端
const cols = instanceRef.current.getXTerm().cols; const xterm = instanceRef.current.getXTerm();
const rows = instanceRef.current.getXTerm().rows; const cols = xterm.cols;
const rows = xterm.rows;
console.log(`[Terminal ${id}] FitAddon计算结果: ${cols} cols x ${rows} rows`);
instanceRef.current.sendResize(cols, rows); instanceRef.current.sendResize(cols, rows);
console.log(`[Terminal ${id}] Tab activated, resized and notified: ${cols}x${rows}`); // 重要让XTerm获取焦点否则无法输入
xterm.focus();
const hasFocus = xterm.textarea?.matches(':focus');
console.log(`[Terminal ${id}] Tab activated, resized: ${cols}x${rows}, focused: ${hasFocus}`);
} }
}, 100); }, 100);
} }
@ -135,10 +147,20 @@ export const Terminal: React.FC<TerminalProps> = ({
// 监听窗口大小变化,自动调整终端尺寸 // 监听窗口大小变化,自动调整终端尺寸
useEffect(() => { useEffect(() => {
const handleResize = () => { const handleResize = () => {
console.log(`[Terminal ${id}] 🔄 Window resize event`);
if (wrapperRef.current && terminalRef.current) {
console.log(`[Terminal ${id}] 📦 Resize - Wrapper: ${wrapperRef.current.clientWidth}px × ${wrapperRef.current.clientHeight}px`);
console.log(`[Terminal ${id}] 📦 Resize - Content: ${terminalRef.current.clientWidth}px × ${terminalRef.current.clientHeight}px`);
}
const fitAddon = instanceRef.current?.getFitAddon(); const fitAddon = instanceRef.current?.getFitAddon();
if (fitAddon) { if (fitAddon) {
setTimeout(() => { setTimeout(() => {
fitAddon.fit(); fitAddon.fit();
if (instanceRef.current) {
const xterm = instanceRef.current.getXTerm();
console.log(`[Terminal ${id}] 🔄 Resize后: ${xterm.cols} cols × ${xterm.rows} rows`);
}
}, 100); }, 100);
} }
}; };
@ -269,7 +291,7 @@ export const Terminal: React.FC<TerminalProps> = ({
}, []); }, []);
return ( return (
<div className={styles.terminalWrapper}> <div ref={wrapperRef} className={styles.terminalWrapper}>
{/* 工具栏 */} {/* 工具栏 */}
<TerminalToolbar <TerminalToolbar
config={toolbarConfig} config={toolbarConfig}

View File

@ -57,7 +57,6 @@ export const TerminalSplitDivider: React.FC<TerminalSplitDividerProps> = ({ orie
? (orientation === 'horizontal' ? container.clientWidth : container.clientHeight) ? (orientation === 'horizontal' ? container.clientWidth : container.clientHeight)
: undefined; : undefined;
console.log(`[TerminalSplitDivider] onResize, orientation: ${orientation}, delta: ${delta}, containerSize: ${containerSize}`);
onResize(delta, containerSize); onResize(delta, containerSize);
lastMousePosRef.current = currentMousePos; lastMousePosRef.current = currentMousePos;
}; };
@ -81,7 +80,6 @@ export const TerminalSplitDivider: React.FC<TerminalSplitDividerProps> = ({ orie
const handleMouseDown = (e: React.MouseEvent) => { const handleMouseDown = (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
console.log(`[TerminalSplitDivider] mouseDown, orientation: ${orientation}`);
const initialPos = orientation === 'horizontal' ? e.clientX : e.clientY; const initialPos = orientation === 'horizontal' ? e.clientX : e.clientY;
startPosRef.current = initialPos; startPosRef.current = initialPos;
lastMousePosRef.current = initialPos; lastMousePosRef.current = initialPos;

View File

@ -168,7 +168,7 @@ export const TerminalToolbar: React.FC<TerminalToolbarProps> = ({
<> <>
<div className="h-4 w-px bg-gray-300 dark:bg-gray-600 mx-1" /> <div className="h-4 w-px bg-gray-300 dark:bg-gray-600 mx-1" />
<Select value={currentTheme} onValueChange={onThemeChange}> <Select value={currentTheme} onValueChange={onThemeChange}>
<SelectTrigger className="h-7 w-auto min-w-[100px] text-xs"> <SelectTrigger className={compact ? "h-7 w-[160px] text-xs" : "h-7 w-[200px] text-xs"}>
<Palette className="h-3.5 w-3.5 mr-1" /> <Palette className="h-3.5 w-3.5 mr-1" />
<SelectValue placeholder="选择主题" /> <SelectValue placeholder="选择主题" />
</SelectTrigger> </SelectTrigger>

View File

@ -358,7 +358,7 @@ export class TerminalInstance {
// 监听尺寸变化 // 监听尺寸变化
this.xterm.onResize((size) => { this.xterm.onResize((size) => {
this.sendResize(size.rows, size.cols); this.sendResize(size.cols, size.rows);
}); });
} }

View File

@ -59,7 +59,7 @@ export const TERMINAL_THEMES: TerminalTheme[] = [
}, },
{ {
name: 'monokai', name: 'monokai',
label: 'Monokai', label: 'Monokai(摩诺凯)',
theme: { theme: {
background: '#272822', background: '#272822',
foreground: '#f8f8f2', foreground: '#f8f8f2',
@ -85,7 +85,7 @@ export const TERMINAL_THEMES: TerminalTheme[] = [
}, },
{ {
name: 'dracula', name: 'dracula',
label: 'Dracula', label: 'Dracula(德古拉)',
theme: { theme: {
background: '#282a36', background: '#282a36',
foreground: '#f8f8f2', foreground: '#f8f8f2',
@ -111,7 +111,7 @@ export const TERMINAL_THEMES: TerminalTheme[] = [
}, },
{ {
name: 'solarized-dark', name: 'solarized-dark',
label: 'Solarized Dark', label: 'Solarized Dark(太阳化暗色)',
theme: { theme: {
background: '#002b36', background: '#002b36',
foreground: '#839496', foreground: '#839496',
@ -137,7 +137,7 @@ export const TERMINAL_THEMES: TerminalTheme[] = [
}, },
{ {
name: 'nord', name: 'nord',
label: 'Nord', label: 'Nord(北欧)',
theme: { theme: {
background: '#2e3440', background: '#2e3440',
foreground: '#d8dee9', foreground: '#d8dee9',
@ -163,7 +163,7 @@ export const TERMINAL_THEMES: TerminalTheme[] = [
}, },
{ {
name: 'solarized-light', name: 'solarized-light',
label: 'Solarized Light', label: 'Solarized Light(太阳化亮色)',
theme: { theme: {
background: '#fdf6e3', background: '#fdf6e3',
foreground: '#657b83', foreground: '#657b83',
@ -189,7 +189,7 @@ export const TERMINAL_THEMES: TerminalTheme[] = [
}, },
{ {
name: 'gruvbox-dark', name: 'gruvbox-dark',
label: 'Gruvbox Dark', label: 'Gruvbox Dark(复古暗色)',
theme: { theme: {
background: '#282828', background: '#282828',
foreground: '#ebdbb2', foreground: '#ebdbb2',
@ -215,7 +215,7 @@ export const TERMINAL_THEMES: TerminalTheme[] = [
}, },
{ {
name: 'one-dark', name: 'one-dark',
label: 'One Dark', label: 'One Dark(单色暗)',
theme: { theme: {
background: '#282c34', background: '#282c34',
foreground: '#abb2bf', foreground: '#abb2bf',
@ -241,7 +241,7 @@ export const TERMINAL_THEMES: TerminalTheme[] = [
}, },
{ {
name: 'material', name: 'material',
label: 'Material', label: 'Material(质感)',
theme: { theme: {
background: '#263238', background: '#263238',
foreground: '#eeffff', foreground: '#eeffff',
@ -267,7 +267,7 @@ export const TERMINAL_THEMES: TerminalTheme[] = [
}, },
{ {
name: 'github', name: 'github',
label: 'GitHub', label: 'GitHubGitHub',
theme: { theme: {
background: '#ffffff', background: '#ffffff',
foreground: '#24292e', foreground: '#24292e',
@ -293,7 +293,7 @@ export const TERMINAL_THEMES: TerminalTheme[] = [
}, },
{ {
name: 'tokyo-night', name: 'tokyo-night',
label: 'Tokyo Night', label: 'Tokyo Night(东京夜)',
theme: { theme: {
background: '#1a1b26', background: '#1a1b26',
foreground: '#c0caf5', foreground: '#c0caf5',

View File

@ -5,22 +5,7 @@
import { useState, useCallback } from 'react'; import { useState, useCallback } from 'react';
import type { EditorGroup, TerminalTab, SplitDirection, SplitLayout, SplitNode, SplitContainer, LayoutOrientation } from './types'; import type { EditorGroup, TerminalTab, SplitDirection, SplitLayout, SplitNode, SplitContainer, LayoutOrientation } from './types';
import { TerminalInstanceManager } from './core/TerminalInstanceManager'; import { TerminalInstanceManager } from './core/TerminalInstanceManager';
import { getMinSplitSize, getMinSplitPercent } from './splitUtils';
/**
*
* @param containerSize px
* @param orientation
* @returns px
*/
const getMinSize = (containerSize: number, orientation: LayoutOrientation): number => {
if (orientation === 'horizontal') {
// 左右分屏最小宽度520px
return 520;
} else {
// 上下分屏根据容器高度计算最小200px最多占40%
return Math.max(200, Math.min(containerSize * 0.4, 300));
}
};
interface UseSplitViewOptions { interface UseSplitViewOptions {
initialTab: TerminalTab; initialTab: TerminalTab;
@ -386,19 +371,12 @@ export const useSplitView = ({ initialTab, onWindowClose }: UseSplitViewOptions)
// 调整分屏大小 // 调整分屏大小
const resizeGroups = useCallback((nodeId: string, delta: number, containerSize?: number) => { const resizeGroups = useCallback((nodeId: string, delta: number, containerSize?: number) => {
console.log(`[useSplitView.resizeGroups] nodeId: ${nodeId}, delta: ${delta}, containerSize: ${containerSize}`);
setLayout(prev => { setLayout(prev => {
const result = findParent(prev.root, nodeId); const result = findParent(prev.root, nodeId);
if (!result || !result.parent) { if (!result || !result.parent) return prev;
console.log(`[useSplitView.resizeGroups] findParent failed`);
return prev;
}
const { parent, index } = result; const { parent, index } = result;
if (index >= parent.children.length - 1) { if (index >= parent.children.length - 1) return prev;
console.log(`[useSplitView.resizeGroups] index check failed: ${index} >= ${parent.children.length - 1}`);
return prev;
}
const current = parent.children[index]; const current = parent.children[index];
const next = parent.children[index + 1]; const next = parent.children[index + 1];
@ -408,14 +386,15 @@ export const useSplitView = ({ initialTab, onWindowClose }: UseSplitViewOptions)
// delta是像素值需要转换为百分比(delta / containerSize) * totalSize // delta是像素值需要转换为百分比(delta / containerSize) * totalSize
const deltaPercent = containerSize ? (delta / containerSize) * totalSize : delta; const deltaPercent = containerSize ? (delta / containerSize) * totalSize : delta;
// 计算最小尺寸百分比使用totalSize的20%作为最小值 // 计算最小尺寸百分比:根据方向和容器尺寸计算
// 这样每个分屏至少占两个分屏总和的20%例如总100%时最小20% let minSizePercent = totalSize * 0.2; // 默认20%
const minSizePercent = totalSize * 0.2; if (containerSize && parent.type === 'container') {
const minPixels = getMinSplitSize(containerSize, parent.orientation);
minSizePercent = getMinSplitPercent(minPixels, containerSize, totalSize);
}
const newCurrentSize = Math.max(minSizePercent, Math.min(totalSize - minSizePercent, current.size + deltaPercent)); const newCurrentSize = Math.max(minSizePercent, Math.min(totalSize - minSizePercent, current.size + deltaPercent));
const newNextSize = totalSize - newCurrentSize; const newNextSize = totalSize - newCurrentSize;
console.log(`[useSplitView.resizeGroups] current.size: ${current.size}, deltaPercent: ${deltaPercent}, newCurrentSize: ${newCurrentSize}, newNextSize: ${newNextSize}`);
const newChildren = parent.children.map((child, i) => { const newChildren = parent.children.map((child, i) => {
if (i === index) return { ...child, size: newCurrentSize }; if (i === index) return { ...child, size: newCurrentSize };