219 lines
8.4 KiB
Markdown
219 lines
8.4 KiB
Markdown
# 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` | 无需变更 |
|
||
|
||
---
|
||
|
||
## 实施步骤
|
||
|
||
### T1:markRaw 修复 + Uptime 计时器内嵌(Critical 修复)
|
||
|
||
**目标**:解决 C1(Vue 代理 xterm/WebSocket)和 C3(Uptime 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 对象
|
||
|
||
---
|
||
|
||
### T2:Per-Session Overlay(Critical 修复)
|
||
|
||
**目标**:解决 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">`
|
||
- 删除模板中的重复列表代码
|
||
|
||
---
|
||
|
||
### T6:Minor 体验修复
|
||
|
||
**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 | 手动断网,切换 Tab,overlay 正确显示 |
|
||
| 最后 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` 回滚到重构前。
|
||
|
||
---
|
||
|
||
## 执行顺序建议
|
||
|
||
```
|
||
T1(markRaw + timer, 1h) → T2(per-session overlay, 1h)
|
||
→ T6(minor fixes, 0.5h)
|
||
→ T3(composables, 2h)→ T4(components, 2h)→ T5(dom + picker, 0.5h)
|
||
→ 全量测试 → changelog → 部署
|
||
```
|
||
|
||
建议 T1+T2+T6 作为第一批次(安全稳定性),T3+T4+T5 作为第二批次(架构重构)分两次 PR 合并。
|