From 989b17902bda004d9bf4eafbe957241efbf273e7 Mon Sep 17 00:00:00 2001 From: Nexus Agent Date: Fri, 12 Jun 2026 01:47:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(terminal):=20=E5=8F=B3=E4=BE=A7=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E5=99=A8=E5=88=97=E8=A1=A8=E4=B8=8E=E6=90=9C=E7=B4=A2?= =?UTF-8?q?=E5=8E=86=E5=8F=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 终端与会话并行:右侧常驻选服列表,combobox 共用服务器页最近 10 条搜索历史。 Co-authored-by: Cursor --- ...-12-terminal-server-rail-search-history.md | 38 ++++++ ...-12-terminal-server-rail-search-history.md | 34 +++++ .../src/components/terminal/TerminalArea.vue | 5 +- .../terminal/TerminalServerPicker.vue | 112 +++++++++------- .../terminal/TerminalServerPickerList.vue | 29 +++++ .../terminal/TerminalServerPickerSearch.vue | 27 ++++ .../terminal/useTerminalSessions.ts | 27 ++++ frontend/src/pages/TerminalPage.vue | 120 +++++++++--------- 8 files changed, 289 insertions(+), 103 deletions(-) create mode 100644 docs/audit/2026-06-12-terminal-server-rail-search-history.md create mode 100644 docs/changelog/2026-06-12-terminal-server-rail-search-history.md create mode 100644 frontend/src/components/terminal/TerminalServerPickerList.vue create mode 100644 frontend/src/components/terminal/TerminalServerPickerSearch.vue diff --git a/docs/audit/2026-06-12-terminal-server-rail-search-history.md b/docs/audit/2026-06-12-terminal-server-rail-search-history.md new file mode 100644 index 00000000..4dfdff5e --- /dev/null +++ b/docs/audit/2026-06-12-terminal-server-rail-search-history.md @@ -0,0 +1,38 @@ +# 审计 — 终端页右侧服务器列表与搜索历史 + +**Changelog**: `docs/changelog/2026-06-12-terminal-server-rail-search-history.md` + +## 范围(文件清单) + +| 文件 | 变更 | +|------|------| +| `frontend/src/pages/TerminalPage.vue` | 左右分栏,右侧常驻列表 | +| `frontend/src/components/terminal/TerminalArea.vue` | 空态文案 | +| `frontend/src/components/terminal/TerminalServerPicker.vue` | sidebar 模式 + combobox | +| `frontend/src/components/terminal/TerminalServerPickerSearch.vue` | 新建 | +| `frontend/src/components/terminal/TerminalServerPickerList.vue` | 新建 | +| `frontend/src/composables/terminal/useTerminalSessions.ts` | search-history API | + +## Step 3 规则扫描 + +| H | 规则 | 结论 | +|---|------|------| +| H1 | 安全 | PASS — 复用已有 search-history API | +| H2 | 无静默吞错 | PASS | +| H3 | 无新密钥 | PASS | +| H4 | CUD | N/A | + +## Closure + +| H | 判定 | 依据 | +|---|------|------| +| H1–H4 | PASS | 纯前端布局 + 已有 API | + +## DoD + +- [x] type-check +- [ ] 生产 `#/terminal` 验证 + +## 验证 + +右侧列表 + 搜索历史 10 条;与会话并行。 diff --git a/docs/changelog/2026-06-12-terminal-server-rail-search-history.md b/docs/changelog/2026-06-12-terminal-server-rail-search-history.md new file mode 100644 index 00000000..31169b55 --- /dev/null +++ b/docs/changelog/2026-06-12-terminal-server-rail-search-history.md @@ -0,0 +1,34 @@ +# 终端页右侧服务器列表与搜索历史 + +**日期**:2026-06-12 + +## 动机 + +终端与会话选服需同时进行:左侧操作 SSH,右侧随时选下一台;搜索词与服务器页共用最近记录。 + +## 变更 + +- 终端页 **右侧常驻** 服务器列表(300px),有会话时也可切换目标 +- 搜索框改为 **v-combobox**,与服务器页共用 `/api/servers/search-history`(展示最近 **10** 条) +- 回车或选历史项写入历史;空终端区提示「从右侧列表选择」 + +## 涉及文件 + +- `frontend/src/pages/TerminalPage.vue` +- `frontend/src/components/terminal/TerminalArea.vue` +- `frontend/src/components/terminal/TerminalServerPicker.vue` +- `frontend/src/components/terminal/TerminalServerPickerSearch.vue`(新建) +- `frontend/src/components/terminal/TerminalServerPickerList.vue`(新建) +- `frontend/src/composables/terminal/useTerminalSessions.ts` + +## 验证 + +```bash +cd frontend && npm run type-check +``` + +打开 `#/terminal`:右侧列表;搜索下拉见历史;左侧终端可并行使用。 + +## 迁移 + +无。仅前端构建部署。 diff --git a/frontend/src/components/terminal/TerminalArea.vue b/frontend/src/components/terminal/TerminalArea.vue index df106a46..f2345195 100644 --- a/frontend/src/components/terminal/TerminalArea.vue +++ b/frontend/src/components/terminal/TerminalArea.vue @@ -18,10 +18,9 @@ > mdi-console-network
WebSSH 终端
-
- 选择一台服务器开始会话,或从服务器页点击「终端」 +
+ 从右侧列表选择服务器,或点击上方 + 新建标签
-
- - - +
+ 服务器 + ({{ servers.length }}) +
+
+ - - - - - - - 暂无匹配的服务器 - - - - - - @@ -86,4 +101,15 @@ defineEmits<{ max-height: min(42vh, 320px); overflow-y: auto; } +.terminal-server-rail { + width: 300px; + flex-shrink: 0; + min-height: 0; + background: rgb(var(--v-theme-surface)); + border-left: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); +} +.terminal-server-rail__list { + overflow-y: auto; + min-height: 0; +} diff --git a/frontend/src/components/terminal/TerminalServerPickerList.vue b/frontend/src/components/terminal/TerminalServerPickerList.vue new file mode 100644 index 00000000..11dfca46 --- /dev/null +++ b/frontend/src/components/terminal/TerminalServerPickerList.vue @@ -0,0 +1,29 @@ + + + diff --git a/frontend/src/components/terminal/TerminalServerPickerSearch.vue b/frontend/src/components/terminal/TerminalServerPickerSearch.vue new file mode 100644 index 00000000..d5b6dc8e --- /dev/null +++ b/frontend/src/components/terminal/TerminalServerPickerSearch.vue @@ -0,0 +1,27 @@ + + + diff --git a/frontend/src/composables/terminal/useTerminalSessions.ts b/frontend/src/composables/terminal/useTerminalSessions.ts index 391c9b34..ff39ca01 100644 --- a/frontend/src/composables/terminal/useTerminalSessions.ts +++ b/frontend/src/composables/terminal/useTerminalSessions.ts @@ -29,8 +29,10 @@ import { XTERM_CLASSIC_THEME } from './xtermTheme' import { decodeTerminalPayload } from '@/utils/terminalPayload' import { fetchPagePerPage } from '@/utils/paginatedFetch' import type { ServerApiItem } from '@/types/api' +import { useServerSearchHistory } from '@/composables/useServerSearchHistory' const MAX_TABS = 10 +const TERMINAL_SEARCH_HISTORY = 10 export function useTerminalSessions(nexusDrawer: Ref) { const route = useRoute() @@ -47,6 +49,10 @@ export function useTerminalSessions(nexusDrawer: Ref) { const serversLoading = ref(false) const serverSearch = ref('') const quickCmds = ref([]) + const { items: serverSearchHistory, loadHistoryOnly, record: recordServerSearch } = + useServerSearchHistory() + + const terminalSearchHistory = computed(() => serverSearchHistory.value.slice(0, TERMINAL_SEARCH_HISTORY)) const activeSession = computed( () => sessions.value.find((s) => s.id === activeSessionId.value) || null, @@ -708,6 +714,7 @@ export function useTerminalSessions(nexusDrawer: Ref) { async function loadServers() { serversLoading.value = true try { + await loadHistoryOnly() const { items } = await fetchPagePerPage('/servers/', {}, 1, -1) servers.value = items.map(toTerminalServerItem) } catch { @@ -718,6 +725,23 @@ export function useTerminalSessions(nexusDrawer: Ref) { } } + function commitServerSearch() { + void recordServerSearch(serverSearch.value) + } + + function onServerSearchUpdate(val: string | null) { + if (val == null || val === '') { + serverSearch.value = '' + void recordServerSearch('') + return + } + const picked = String(val).trim() + serverSearch.value = picked + if (serverSearchHistory.value.includes(picked)) { + void recordServerSearch(picked) + } + } + function onGlobalKeydown(e: KeyboardEvent) { if (e.ctrlKey && e.key === 'l') { e.preventDefault() @@ -771,6 +795,7 @@ export function useTerminalSessions(nexusDrawer: Ref) { servers, serversLoading, serverSearch, + terminalSearchHistory, filteredServers, showEmptyPicker, quickCmds, @@ -792,6 +817,8 @@ export function useTerminalSessions(nexusDrawer: Ref) { loadQuickCmds, execQuickCmd, loadServers, + commitServerSearch, + onServerSearchUpdate, applyRouteQuery, onGlobalKeydown, disposeAllSessions, diff --git a/frontend/src/pages/TerminalPage.vue b/frontend/src/pages/TerminalPage.vue index 9fc3225c..6d281ecc 100644 --- a/frontend/src/pages/TerminalPage.vue +++ b/frontend/src/pages/TerminalPage.vue @@ -36,62 +36,64 @@ -
- +
+
+ - + - - - + - + - +
+ +
@@ -102,9 +104,11 @@ @@ -140,6 +144,7 @@ const { termContainer, showServerDialog, serverSearch, + terminalSearchHistory, filteredServers, showEmptyPicker, quickCmds, @@ -160,6 +165,8 @@ const { execQuickCmd, loadServers, serversLoading, + commitServerSearch, + onServerSearchUpdate, applyRouteQuery, onGlobalKeydown, disposeAllSessions, @@ -185,10 +192,6 @@ const termMenuLocation = computed(() => menuY.value > window.innerHeight * 0.55 ? 'top end' : 'bottom end', ) -function onServerSearch(v: string) { - serverSearch.value = v -} - function openTermMenu(e: MouseEvent, context: 'terminal' | 'command') { e.preventDefault() termMenuContext.value = context @@ -293,6 +296,9 @@ onBeforeUnmount(() => { .terminal-root--classic { background: #000000; } +.terminal-main-row { + min-height: 0; +} .font-mono { font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', Menlo, monospace; }