Files
Nexus/docs/design/plans/2026-06-02-terminal-refactor.md
2026-07-08 22:31:31 +08:00

219 lines
8.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 合并。