Files
Nexus/docs/design/plans/2026-06-02-terminal-refactor.md
T

219 lines
8.4 KiB
Markdown
Raw Normal View History

# Terminal 页重构技术计划
**日期**: 2026-06-02
**对应设计文档**: `docs/design/specs/2026-06-02-terminal-refactor-design.md`
**阶段编号**: T1 ~ T6
---
## 涉及文件清单
### 新建文件
| 文件 | 说明 |
|------|------|
| `frontend/src/composables/terminal/useTerminalSession.ts` | 单会话 xterm + WS 生命周期 |
| `frontend/src/composables/terminal/useTerminalSessions.ts` | 多会话 Tab 管理 |
| `frontend/src/composables/terminal/useTerminalSettings.ts` | fontSize/scrollback localStorage 持久化 |
| `frontend/src/composables/terminal/useTerminalCmdBar.ts` | 命令输入 + 历史记录 |
| `frontend/src/components/terminal/TerminalTabBar.vue` | Tab 条 |
| `frontend/src/components/terminal/TerminalToolbar.vue` | 工具栏(状态/字号/断开) |
| `frontend/src/components/terminal/TerminalArea.vue` | xterm 挂载区 + per-session overlay |
| `frontend/src/components/terminal/TerminalCmdBar.vue` | 命令栏 + 快捷键面板 |
| `frontend/src/components/terminal/TerminalQuickBar.vue` | 快捷命令栏 |
| `frontend/src/components/terminal/TerminalServerPicker.vue` | 服务器选择(inline + dialog 两种模式) |
### 修改文件
| 文件 | 改动 |
|------|------|
| `frontend/src/pages/TerminalPage.vue` | 瘦身至 ~130 行,仅组合上述组件 |
### 不改动文件
| 文件 | 原因 |
|------|------|
| `server/api/webssh.py` | 后端逻辑已完善,协议不变 |
| `frontend/src/composables/useTerminalQuickCommands.ts` | 已结构良好,T6 仅小调整 |
| `frontend/src/components/ShellHighlightField.vue` | 无需变更 |
| `frontend/src/utils/wsUrl.ts` | 无需变更 |
---
## 实施步骤
### T1markRaw 修复 + Uptime 计时器内嵌(Critical 修复)
**目标**:解决 C1Vue 代理 xterm/WebSocket)和 C3Uptime timer 外部 Map
**步骤**
1.`TerminalPage.vue` 中,将 `TermSession` interface 拆分:
- 新增 `TermSessionNative` type,字段:`ws / term / fitAddon / resizeObserver / reconnectTimer / pingTimer / uptimeTimer`
- 新增 module-level `const nativeMap = new Map<string, TermSessionNative>()`
-`TermSession` interface 中删除上述字段
2. 所有原生对象创建时包裹 `markRaw()`
- `term = markRaw(new Terminal(...))`
- `fitAddon = markRaw(new FitAddon())`
- `resizeObserver = markRaw(new ResizeObserver(...))`
- WebSocket 无需 markRaw(已在 connectSession 内部使用,不进 reactive
3.`_uptimeTimers` Map 删除,uptimeTimer 改存入 `nativeMap[sessionId].uptimeTimer`
4. 更新 `closeTab``onBeforeUnmount` 中的清理逻辑,从 `nativeMap` 获取 native 对象后清理
**验收**Vue DevTools → Components 面板,`sessions` ref 中不出现 Terminal/WebSocket/FitAddon 对象
---
### T2Per-Session OverlayCritical 修复)
**目标**:解决 C2(全局 overlay 状态)
**步骤**
1.`TermSession` interface 中新增字段:
```typescript
showOverlay: boolean // 替代全局 showDisconnectOverlay
overlayMsg: string // 替代全局 disconnectMsg
reconnectCountdown: string // 移入 session
```
2. 删除全局 `showDisconnectOverlay`、`disconnectMsg`、`reconnectCountdown` refs
3. 在 `connectSession`、`startReconnect`、`disconnect`、`closeTab` 中,将原来操作全局 ref 的代码改为操作 `s.showOverlay / s.overlayMsg / s.reconnectCountdown`
4. 模板中的 `<v-overlay>` 绑定改为 `activeSession?.showOverlay`
5. 关闭最后一个 Tab 时,不显示 overlay,让空状态区域自然显示(删除 `showDisconnectOverlay = true` 的 closeTab 逻辑)
**验收**
- 打开 2 个会话,让会话 2 断开,切换到会话 2,overlay 显示
- 关闭最后一个会话,出现空状态服务器选择器,不出现 overlay
---
### T3:提取 Composables
**目标**:解决 M1(单文件过大),提取 4 个 Composable
#### T3-A`useTerminalSettings`
```typescript
// 仅管理 fontSize / scrollback 的 localStorage 持久化
export function useTerminalSettings() {
const fontSize = ref(...)
const scrollback = ref(...)
function changeFontSize(delta: number): void
function changeScrollback(val: number): void
return { fontSize, scrollback, scrollbackOptions, changeFontSize, changeScrollback }
}
```
#### T3-B`useTerminalCmdBar`
```typescript
// 管理命令输入、历史记录
export function useTerminalCmdBar(getActiveSession: () => TermSession | null) {
const cmdInput = ref('')
function sendCmd(): void
function cmdHistoryUp(): void
function cmdHistoryDown(): void
function sendCtrl(key: string): void
function sendKey(key: string): void
return { cmdInput, sendCmd, cmdHistoryUp, cmdHistoryDown, sendCtrl, sendKey }
}
```
#### T3-C`useTerminalSessions`
核心:多 Tab 管理,包含 `sessions / activeSessionId / activeSession / newSession / closeTab / switchTab / applyRouteQuery`
#### T3-D`useTerminalSession`
核心:单会话连接 + 重连 + ping + uptime,参数为 `sessionData` ref 和 `nativeMap`
**注意**:T3-C/D 是最大改动,需完整测试后再提交
---
### T4:拆分 Components
**目标**:消除模板耦合,每个组件职责单一
| 组件 | Props | Emits |
|------|-------|-------|
| `TerminalTabBar` | `sessions, activeSessionId` | `switch, close, new` |
| `TerminalToolbar` | `session, fontSize, scrollback` | `font-change, scrollback-change, disconnect, fullscreen` |
| `TerminalArea` | `sessions, activeSessionId` | `context-menu` |
| `TerminalCmdBar` | `disabled` | `send, ctrl, key` |
| `TerminalQuickBar` | `commands` | `exec, manage` |
| `TerminalServerPicker` | `servers, mode` | `select` |
`TerminalPage.vue` 仅负责组合,约 80 行模板 + 30 行 setup。
---
### T5:修复 M2(DOM 查询) + M3(服务器选择器去重)
**M2 修复**
- `TerminalCmdBar.vue` 组件内部声明 `const inputRef = ref<HTMLTextAreaElement | null>(null)`
- 将 `<ShellHighlightField>` 替换为绑定 ref 的原生 textarea(或 ShellHighlightField 补充 expose
- 删除 `getCmdInputEl()` 函数
**M3 修复**
- 新建 `TerminalServerPicker.vue`props: `mode: 'inline' | 'dialog'`
- 空状态区域使用 `<TerminalServerPicker mode="inline">`
- 新建会话对话框内容使用 `<TerminalServerPicker mode="dialog">`
- 删除模板中的重复列表代码
---
### T6Minor 体验修复
**E1 修复**(向同一服务器新建会话):
- `newSession` 中检测到同 serverId 时,弹出 snackbar:「会话已存在,已切换过去。如需新建请关闭原会话。」
- 提供一个可选 `opts.forceNew: boolean` 参数供未来扩展
**E2 修复**(初始 cd 指令时序):
- 将 `sendCdToSession` 从 `setTimeout(200)` 改为在 `TERMINAL_INIT` 消息处理中直接发送,不依赖超时
**E3 修复**(重复快捷键):
- 删除 `onKeydown` 中重复的 `e.key === '='` 分支,只保留 `e.key === '+'`(标准 Chromium 行为)
**E4 修复**(快捷命令加载失败提示):
- `loadQuickCmds` catch 块改为:`snackbar('快捷命令加载失败,使用内置命令', 'warning')` 后再赋值内置
---
## 依赖与配置变更
- 无新 npm 包依赖
- 无 Vue Router / Pinia 结构变更
- xterm 版本不变(@xterm/xterm
---
## 测试要点
| 场景 | 验证方式 |
|------|----------|
| markRaw 生效 | Vue DevTools sessions ref 无 Terminal 实例 |
| 多 Tab 断开 overlay | 手动断网,切换 Taboverlay 正确显示 |
| 最后 Tab 关闭 | 显示空状态选择器,不显示 overlay |
| xterm resize | 拖拽侧栏/改字号,xterm 正确填充容器 |
| reconnect 指数退避 | 断连后等待,验证 1s/2s/4s 重连间隔 |
| 命令历史 | 发送命令,↑/↓ 正确回溯 |
| 快捷命令 | 点击执行,命令发送到活动终端 |
| pendingPath 跳转 | `/terminal?server_id=1&path=/var/log` 打开后自动 cd |
| T1-T6 全部功能回归 | 手动端到端测试所有功能 |
---
## 回滚方式
全程在功能分支 `refactor/terminal` 开发,main 分支不受影响。
任何阶段出现严重问题,直接 `git checkout main -- frontend/src/pages/TerminalPage.vue` 回滚到重构前。
---
## 执行顺序建议
```
T1markRaw + timer, 1h → T2per-session overlay, 1h
→ T6minor fixes, 0.5h
→ T3composables, 2h)→ T4components, 2h)→ T5dom + picker, 0.5h
→ 全量测试 → changelog → 部署
```
建议 T1+T2+T6 作为第一批次(安全稳定性),T3+T4+T5 作为第二批次(架构重构)分两次 PR 合并。