mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
Merge pull request #91 from Chiron-Brahm/fix/terminal-state-persistence
fix: 终端和页面切换时保持状态
This commit is contained in:
+101
-97
@@ -52,21 +52,22 @@ const MainContent = styled.main`
|
||||
min-height: 0;
|
||||
`;
|
||||
|
||||
const PageWrapper = styled.div`
|
||||
const PageWrapper = styled.div<{ $isActive: boolean }>`
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
overflow: auto;
|
||||
display: ${(props) => (props.$isActive ? "block" : "none")};
|
||||
`;
|
||||
|
||||
/**
|
||||
* 全屏页面容器(无 padding)
|
||||
* 用于终端等需要全屏显示的插件
|
||||
*/
|
||||
const FullscreenWrapper = styled.div`
|
||||
const FullscreenWrapper = styled.div<{ $isActive: boolean }>`
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
display: ${(props) => (props.$isActive ? "flex" : "none")};
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
`;
|
||||
@@ -124,106 +125,109 @@ function AppContent() {
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 渲染当前页面
|
||||
* 渲染所有页面(保持挂载状态)
|
||||
*
|
||||
* 根据 currentPage 状态渲染对应的页面组件
|
||||
* - 静态页面: 直接渲染对应组件
|
||||
* - 动态插件页面: 使用 PluginUIRenderer 渲染
|
||||
* 所有页面组件都会被渲染,但只有当前页面可见
|
||||
* 这样可以保持页面状态,避免切换时重置
|
||||
*
|
||||
* _需求: 2.2, 3.2_
|
||||
*/
|
||||
const renderPage = () => {
|
||||
// 检查是否为动态插件页面 (plugin:xxx 格式)
|
||||
if (currentPage.startsWith("plugin:")) {
|
||||
const pluginId = currentPage.slice(7); // 移除 "plugin:" 前缀
|
||||
|
||||
// 需要全屏显示的插件列表(终端已内置,不再需要插件)
|
||||
const fullscreenPlugins: string[] = [];
|
||||
const isFullscreen = fullscreenPlugins.includes(pluginId);
|
||||
|
||||
if (isFullscreen) {
|
||||
return (
|
||||
<FullscreenWrapper>
|
||||
<PluginUIRenderer pluginId={pluginId} onNavigate={setCurrentPage} />
|
||||
</FullscreenWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageWrapper>
|
||||
<PluginUIRenderer pluginId={pluginId} onNavigate={setCurrentPage} />
|
||||
const renderAllPages = () => {
|
||||
return (
|
||||
<>
|
||||
{/* Provider Pool 页面 */}
|
||||
<PageWrapper $isActive={currentPage === "provider-pool"}>
|
||||
<ProviderPoolPage />
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
// 静态页面路由
|
||||
switch (currentPage) {
|
||||
case "provider-pool":
|
||||
return (
|
||||
<PageWrapper>
|
||||
<ProviderPoolPage />
|
||||
</PageWrapper>
|
||||
);
|
||||
case "api-server":
|
||||
return (
|
||||
<PageWrapper>
|
||||
<ApiServerPage />
|
||||
</PageWrapper>
|
||||
);
|
||||
case "agent":
|
||||
// Agent 页面有自己的布局,不需要 PageWrapper
|
||||
return (
|
||||
{/* API Server 页面 */}
|
||||
<PageWrapper $isActive={currentPage === "api-server"}>
|
||||
<ApiServerPage />
|
||||
</PageWrapper>
|
||||
|
||||
{/* Agent 页面 - 使用 div 包装以支持显示/隐藏 */}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
display: currentPage === "agent" ? "flex" : "none",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<AgentChatPage onNavigate={(page) => setCurrentPage(page as Page)} />
|
||||
);
|
||||
case "terminal":
|
||||
// 终端工作区 - 分块布局(对齐 Waveterm TileLayout)
|
||||
return <TerminalWorkspace onNavigate={setCurrentPage} />;
|
||||
case "sysinfo":
|
||||
// 系统监控页面
|
||||
return (
|
||||
<FullscreenWrapper>
|
||||
<SysinfoView />
|
||||
</FullscreenWrapper>
|
||||
);
|
||||
case "files":
|
||||
// 文件浏览器页面
|
||||
return (
|
||||
<FullscreenWrapper>
|
||||
<FileBrowserView />
|
||||
</FullscreenWrapper>
|
||||
);
|
||||
case "web":
|
||||
// 内嵌浏览器页面
|
||||
return (
|
||||
<FullscreenWrapper>
|
||||
<WebView />
|
||||
</FullscreenWrapper>
|
||||
);
|
||||
case "tools":
|
||||
return (
|
||||
<PageWrapper>
|
||||
<ToolsPage onNavigate={setCurrentPage} />
|
||||
</PageWrapper>
|
||||
);
|
||||
case "plugins":
|
||||
return (
|
||||
<PageWrapper>
|
||||
<PluginsPage />
|
||||
</PageWrapper>
|
||||
);
|
||||
case "settings":
|
||||
return (
|
||||
<PageWrapper>
|
||||
<SettingsPage />
|
||||
</PageWrapper>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<PageWrapper>
|
||||
<ApiServerPage />
|
||||
</PageWrapper>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* 终端工作区 - 使用 div 包装以支持显示/隐藏 */}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
display: currentPage === "terminal" ? "flex" : "none",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<TerminalWorkspace onNavigate={setCurrentPage} />
|
||||
</div>
|
||||
|
||||
{/* 系统监控页面 */}
|
||||
<FullscreenWrapper $isActive={currentPage === "sysinfo"}>
|
||||
<SysinfoView />
|
||||
</FullscreenWrapper>
|
||||
|
||||
{/* 文件浏览器页面 */}
|
||||
<FullscreenWrapper $isActive={currentPage === "files"}>
|
||||
<FileBrowserView />
|
||||
</FullscreenWrapper>
|
||||
|
||||
{/* 内嵌浏览器页面 */}
|
||||
<FullscreenWrapper $isActive={currentPage === "web"}>
|
||||
<WebView />
|
||||
</FullscreenWrapper>
|
||||
|
||||
{/* Tools 页面 */}
|
||||
<PageWrapper $isActive={currentPage === "tools"}>
|
||||
<ToolsPage onNavigate={setCurrentPage} />
|
||||
</PageWrapper>
|
||||
|
||||
{/* Plugins 页面 */}
|
||||
<PageWrapper $isActive={currentPage === "plugins"}>
|
||||
<PluginsPage />
|
||||
</PageWrapper>
|
||||
|
||||
{/* Settings 页面 */}
|
||||
<PageWrapper $isActive={currentPage === "settings"}>
|
||||
<SettingsPage />
|
||||
</PageWrapper>
|
||||
|
||||
{/* 动态插件页面 */}
|
||||
{currentPage.startsWith("plugin:") &&
|
||||
(() => {
|
||||
const pluginId = currentPage.slice(7);
|
||||
const fullscreenPlugins: string[] = [];
|
||||
const isFullscreen = fullscreenPlugins.includes(pluginId);
|
||||
|
||||
if (isFullscreen) {
|
||||
return (
|
||||
<FullscreenWrapper $isActive={true}>
|
||||
<PluginUIRenderer
|
||||
pluginId={pluginId}
|
||||
onNavigate={setCurrentPage}
|
||||
/>
|
||||
</FullscreenWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageWrapper $isActive={true}>
|
||||
<PluginUIRenderer
|
||||
pluginId={pluginId}
|
||||
onNavigate={setCurrentPage}
|
||||
/>
|
||||
</PageWrapper>
|
||||
);
|
||||
})()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// 引导完成回调
|
||||
@@ -251,7 +255,7 @@ function AppContent() {
|
||||
<ComponentDebugProvider>
|
||||
<AppContainer>
|
||||
<AppSidebar currentPage={currentPage} onNavigate={setCurrentPage} />
|
||||
<MainContent>{renderPage()}</MainContent>
|
||||
<MainContent>{renderAllPages()}</MainContent>
|
||||
{/* ProxyCast Connect 确认弹窗 */}
|
||||
{/* _Requirements: 5.2_ */}
|
||||
<ConnectConfirmDialog
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* @file TerminalPage.tsx
|
||||
* @description 内置终端页面组件 - 对齐 waveterm 单容器架构
|
||||
* @description 内置终端页面组件 - 多容器架构保持状态
|
||||
* @module components/terminal/TerminalPage
|
||||
*
|
||||
* ## 架构说明(对齐 waveterm)
|
||||
* - 只有一个 term-connectelem(终端容器)
|
||||
* - 切换标签页时,销毁旧的 TermWrap,创建新的 TermWrap
|
||||
* - 这样可以避免多个终端容器导致的布局问题
|
||||
* ## 架构说明
|
||||
* - 每个标签页有独立的终端容器和 TermWrap 实例
|
||||
* - 切换标签页时,只是显示/隐藏对应的容器
|
||||
* - 保持所有终端的状态,不销毁 TermWrap
|
||||
*
|
||||
* _Requirements: 8.7, 8.8, 12.1, 12.2, 12.3, 12.4, 12.5, 12.6_
|
||||
*/
|
||||
@@ -43,6 +43,7 @@ interface Tab {
|
||||
title: string;
|
||||
status: SessionStatus;
|
||||
isSSH?: boolean;
|
||||
termWrap?: TermWrap; // 保持 TermWrap 实例
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -375,7 +376,7 @@ const EmptyTabsPlaceholder: React.FC<{
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// 主组件 - 对齐 waveterm 单容器架构
|
||||
// 主组件 - 多容器架构保持状态
|
||||
// ============================================================================
|
||||
|
||||
export function TerminalPage() {
|
||||
@@ -392,18 +393,16 @@ export function TerminalPage() {
|
||||
const [fontSize, setFontSize] = useState<number>(loadFontSizePreference());
|
||||
const tabIdCounter = useRef(0);
|
||||
|
||||
// 单一终端容器引用(对齐 waveterm 的 connectElemRef)
|
||||
const connectElemRef = useRef<HTMLDivElement>(null);
|
||||
// 当前 TermWrap 实例引用(对齐 waveterm 的 model.termRef)
|
||||
const termWrapRef = useRef<TermWrap | null>(null);
|
||||
// 终端容器的父容器引用
|
||||
const terminalContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 获取当前活动的标签页
|
||||
const activeTab = tabs.find((t) => t.id === activeTabId);
|
||||
|
||||
// 获取当前活动的 TermWrap
|
||||
const getActiveTermWrap = useCallback(() => {
|
||||
return termWrapRef.current;
|
||||
}, []);
|
||||
return activeTab?.termWrap || null;
|
||||
}, [activeTab]);
|
||||
|
||||
// 创建新终端
|
||||
const handleNewTerminal = useCallback(async () => {
|
||||
@@ -453,6 +452,11 @@ export function TerminalPage() {
|
||||
const tab = tabs.find((t) => t.id === tabId);
|
||||
if (tab) {
|
||||
try {
|
||||
// 销毁 TermWrap
|
||||
if (tab.termWrap) {
|
||||
tab.termWrap.dispose();
|
||||
}
|
||||
// 关闭后端会话
|
||||
await closeTerminal(tab.sessionId);
|
||||
} catch (err) {
|
||||
console.error("[TerminalPage] 关闭终端会话失败:", err);
|
||||
@@ -477,10 +481,15 @@ export function TerminalPage() {
|
||||
const handleThemeChange = useCallback((theme: ThemeName) => {
|
||||
setCurrentTheme(theme);
|
||||
saveThemePreference(theme);
|
||||
// 更新当前终端的主题
|
||||
if (termWrapRef.current) {
|
||||
termWrapRef.current.setTheme(theme);
|
||||
}
|
||||
// 更新所有终端的主题
|
||||
setTabs((prev) =>
|
||||
prev.map((tab) => {
|
||||
if (tab.termWrap) {
|
||||
tab.termWrap.setTheme(theme);
|
||||
}
|
||||
return tab;
|
||||
}),
|
||||
);
|
||||
}, []);
|
||||
|
||||
// 字体大小变化
|
||||
@@ -488,10 +497,15 @@ export function TerminalPage() {
|
||||
const handleFontSizeChange = useCallback((size: number) => {
|
||||
setFontSize(size);
|
||||
saveFontSizePreference(size);
|
||||
// 更新当前终端的字体大小
|
||||
if (termWrapRef.current) {
|
||||
termWrapRef.current.setFontSize(size);
|
||||
}
|
||||
// 更新所有终端的字体大小
|
||||
setTabs((prev) =>
|
||||
prev.map((tab) => {
|
||||
if (tab.termWrap) {
|
||||
tab.termWrap.setFontSize(size);
|
||||
}
|
||||
return tab;
|
||||
}),
|
||||
);
|
||||
}, []);
|
||||
|
||||
// 搜索功能
|
||||
@@ -578,162 +592,181 @@ export function TerminalPage() {
|
||||
}, []);
|
||||
|
||||
// ============================================================================
|
||||
// 核心:当活动标签页变化时,重新创建 TermWrap(对齐 waveterm)
|
||||
// 核心:为每个标签页创建独立的终端容器和 TermWrap
|
||||
// ============================================================================
|
||||
|
||||
// 键盘事件处理器(对齐 waveterm 的 handleTerminalKeydown)
|
||||
// 键盘事件处理器
|
||||
// 返回 true = 允许事件传递到终端
|
||||
// 返回 false = 阻止事件传递到终端(已处理)
|
||||
const handleTerminalKeydown = useCallback((e: KeyboardEvent): boolean => {
|
||||
// 只处理 keydown 事件
|
||||
if (e.type !== "keydown") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const termWrap = termWrapRef.current;
|
||||
if (!termWrap) return true;
|
||||
|
||||
const isMac = /mac/i.test(navigator.userAgent);
|
||||
|
||||
// Shift+End - 滚动到底部
|
||||
if (
|
||||
e.shiftKey &&
|
||||
!e.ctrlKey &&
|
||||
!e.altKey &&
|
||||
!e.metaKey &&
|
||||
e.key === "End"
|
||||
) {
|
||||
termWrap.terminal.scrollToBottom();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Shift+Home - 滚动到顶部
|
||||
if (
|
||||
e.shiftKey &&
|
||||
!e.ctrlKey &&
|
||||
!e.altKey &&
|
||||
!e.metaKey &&
|
||||
e.key === "Home"
|
||||
) {
|
||||
termWrap.terminal.scrollToLine(0);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cmd+End (macOS) - 滚动到底部
|
||||
if (
|
||||
isMac &&
|
||||
e.metaKey &&
|
||||
!e.ctrlKey &&
|
||||
!e.altKey &&
|
||||
!e.shiftKey &&
|
||||
e.key === "End"
|
||||
) {
|
||||
termWrap.terminal.scrollToBottom();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cmd+Home (macOS) - 滚动到顶部
|
||||
if (
|
||||
isMac &&
|
||||
e.metaKey &&
|
||||
!e.ctrlKey &&
|
||||
!e.altKey &&
|
||||
!e.shiftKey &&
|
||||
e.key === "Home"
|
||||
) {
|
||||
termWrap.terminal.scrollToLine(0);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Shift+PageDown - 向下滚动一页
|
||||
if (
|
||||
e.shiftKey &&
|
||||
!e.ctrlKey &&
|
||||
!e.altKey &&
|
||||
!e.metaKey &&
|
||||
e.key === "PageDown"
|
||||
) {
|
||||
termWrap.terminal.scrollPages(1);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Shift+PageUp - 向上滚动一页
|
||||
if (
|
||||
e.shiftKey &&
|
||||
!e.ctrlKey &&
|
||||
!e.altKey &&
|
||||
!e.metaKey &&
|
||||
e.key === "PageUp"
|
||||
) {
|
||||
termWrap.terminal.scrollPages(-1);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 未处理的事件,允许传递到终端
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const container = connectElemRef.current;
|
||||
if (!container || !activeTab) {
|
||||
// 没有活动标签页,清理旧的 TermWrap
|
||||
if (termWrapRef.current) {
|
||||
termWrapRef.current.dispose();
|
||||
termWrapRef.current = null;
|
||||
const handleTerminalKeydown = useCallback(
|
||||
(e: KeyboardEvent): boolean => {
|
||||
// 只处理 keydown 事件
|
||||
if (e.type !== "keydown") {
|
||||
return true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 销毁旧的 TermWrap
|
||||
if (termWrapRef.current) {
|
||||
termWrapRef.current.dispose();
|
||||
termWrapRef.current = null;
|
||||
}
|
||||
const termWrap = getActiveTermWrap();
|
||||
if (!termWrap) return true;
|
||||
|
||||
// 清空容器
|
||||
container.innerHTML = "";
|
||||
const isMac = /mac/i.test(navigator.userAgent);
|
||||
|
||||
// 创建新的 TermWrap(对齐 waveterm)
|
||||
const termWrap = new TermWrap(activeTab.sessionId, container, {
|
||||
onStatusChange: (status) => handleStatusChange(activeTab.id, status),
|
||||
themeName: currentTheme,
|
||||
fontSize: fontSize,
|
||||
keydownHandler: handleTerminalKeydown,
|
||||
// Shift+End - 滚动到底部
|
||||
if (
|
||||
e.shiftKey &&
|
||||
!e.ctrlKey &&
|
||||
!e.altKey &&
|
||||
!e.metaKey &&
|
||||
e.key === "End"
|
||||
) {
|
||||
termWrap.terminal.scrollToBottom();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Shift+Home - 滚动到顶部
|
||||
if (
|
||||
e.shiftKey &&
|
||||
!e.ctrlKey &&
|
||||
!e.altKey &&
|
||||
!e.metaKey &&
|
||||
e.key === "Home"
|
||||
) {
|
||||
termWrap.terminal.scrollToLine(0);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cmd+End (macOS) - 滚动到底部
|
||||
if (
|
||||
isMac &&
|
||||
e.metaKey &&
|
||||
!e.ctrlKey &&
|
||||
!e.altKey &&
|
||||
!e.shiftKey &&
|
||||
e.key === "End"
|
||||
) {
|
||||
termWrap.terminal.scrollToBottom();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cmd+Home (macOS) - 滚动到顶部
|
||||
if (
|
||||
isMac &&
|
||||
e.metaKey &&
|
||||
!e.ctrlKey &&
|
||||
!e.altKey &&
|
||||
!e.shiftKey &&
|
||||
e.key === "Home"
|
||||
) {
|
||||
termWrap.terminal.scrollToLine(0);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Shift+PageDown - 向下滚动一页
|
||||
if (
|
||||
e.shiftKey &&
|
||||
!e.ctrlKey &&
|
||||
!e.altKey &&
|
||||
!e.metaKey &&
|
||||
e.key === "PageDown"
|
||||
) {
|
||||
termWrap.terminal.scrollPages(1);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Shift+PageUp - 向上滚动一页
|
||||
if (
|
||||
e.shiftKey &&
|
||||
!e.ctrlKey &&
|
||||
!e.altKey &&
|
||||
!e.metaKey &&
|
||||
e.key === "PageUp"
|
||||
) {
|
||||
termWrap.terminal.scrollPages(-1);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 未处理的事件,允许传递到终端
|
||||
return true;
|
||||
},
|
||||
[getActiveTermWrap],
|
||||
);
|
||||
|
||||
// 为新标签页创建终端
|
||||
useEffect(() => {
|
||||
const container = terminalContainerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
// 为每个没有 termWrap 的标签页创建终端
|
||||
tabs.forEach((tab) => {
|
||||
if (tab.termWrap) return; // 已有 TermWrap,跳过
|
||||
|
||||
// 创建独立的容器
|
||||
const termContainer = document.createElement("div");
|
||||
termContainer.className = "term-connectelem";
|
||||
termContainer.dataset.tabId = tab.id;
|
||||
termContainer.style.display = tab.id === activeTabId ? "block" : "none";
|
||||
container.appendChild(termContainer);
|
||||
|
||||
// 创建 TermWrap
|
||||
const termWrap = new TermWrap(tab.sessionId, termContainer, {
|
||||
onStatusChange: (status) => handleStatusChange(tab.id, status),
|
||||
themeName: currentTheme,
|
||||
fontSize: fontSize,
|
||||
keydownHandler: handleTerminalKeydown,
|
||||
});
|
||||
|
||||
// 保存到标签页
|
||||
tab.termWrap = termWrap;
|
||||
|
||||
// 设置 ResizeObserver
|
||||
const rszObs = new ResizeObserver(() => {
|
||||
termWrap.handleResize_debounced();
|
||||
});
|
||||
rszObs.observe(termContainer);
|
||||
|
||||
// 异步初始化终端
|
||||
termWrap.initTerminal().catch(console.error);
|
||||
|
||||
// 如果是当前活动标签,自动聚焦
|
||||
if (tab.id === activeTabId) {
|
||||
setTimeout(() => termWrap.focus(), 10);
|
||||
}
|
||||
|
||||
// 清理函数会在标签页关闭时由 handleCloseTab 调用
|
||||
});
|
||||
|
||||
termWrapRef.current = termWrap;
|
||||
|
||||
// 设置 ResizeObserver(对齐 waveterm)
|
||||
const rszObs = new ResizeObserver(() => {
|
||||
termWrap.handleResize_debounced();
|
||||
// 更新容器显示状态
|
||||
const allContainers = container.querySelectorAll(".term-connectelem");
|
||||
allContainers.forEach((elem) => {
|
||||
const htmlElem = elem as HTMLElement;
|
||||
const tabId = htmlElem.dataset.tabId;
|
||||
htmlElem.style.display = tabId === activeTabId ? "block" : "none";
|
||||
});
|
||||
rszObs.observe(container);
|
||||
|
||||
// 异步初始化终端(对齐 waveterm 的 fireAndForget)
|
||||
termWrap.initTerminal().catch(console.error);
|
||||
|
||||
// 自动聚焦
|
||||
setTimeout(() => termWrap.focus(), 10);
|
||||
|
||||
return () => {
|
||||
termWrap.dispose();
|
||||
rszObs.disconnect();
|
||||
};
|
||||
// 注意:handleTerminalKeydown 使用 useCallback 且无依赖,不会导致重新创建
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [activeTab?.sessionId, currentTheme, fontSize, handleStatusChange]);
|
||||
// 聚焦当前活动终端
|
||||
if (activeTab?.termWrap) {
|
||||
setTimeout(() => activeTab.termWrap?.focus(), 10);
|
||||
}
|
||||
}, [
|
||||
tabs,
|
||||
activeTabId,
|
||||
currentTheme,
|
||||
fontSize,
|
||||
handleStatusChange,
|
||||
handleTerminalKeydown,
|
||||
activeTab,
|
||||
]);
|
||||
|
||||
// 没有标签页时显示空状态
|
||||
if (tabs.length === 0) {
|
||||
@@ -794,11 +827,11 @@ export function TerminalPage() {
|
||||
onClearSearch={handleClearSearch}
|
||||
/>
|
||||
|
||||
{/* 单一终端容器(对齐 waveterm 的 term-connectelem) */}
|
||||
{/* 多终端容器(每个标签页一个容器) */}
|
||||
<div
|
||||
ref={connectElemRef}
|
||||
className="term-connectelem"
|
||||
onClick={() => termWrapRef.current?.focus()}
|
||||
ref={terminalContainerRef}
|
||||
className="term-container-wrapper"
|
||||
onClick={() => activeTab?.termWrap?.focus()}
|
||||
/>
|
||||
|
||||
{/* 错误提示 */}
|
||||
|
||||
@@ -49,6 +49,25 @@
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
/* 多终端容器包装器 */
|
||||
.term-container-wrapper {
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 每个终端容器独立定位 */
|
||||
.term-container-wrapper .term-connectelem {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: 5px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
/* 关键:xterm 容器必须填满父元素 */
|
||||
.term-connectelem .xterm {
|
||||
height: 100%;
|
||||
|
||||
Reference in New Issue
Block a user