From e5f803c36430cffa462578977bfd3bed3436048a Mon Sep 17 00:00:00 2001 From: KiriAky 107 Date: Sat, 29 Aug 2026 22:49:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=E6=8E=A5=E9=80=9A=E5=AE=8C?= =?UTF-8?q?=E6=95=B4=E9=A1=B5=E9=9D=A2=E8=B7=AF=E7=94=B1=E4=B8=8E=E6=A1=8C?= =?UTF-8?q?=E9=9D=A2=E5=A3=B3=E5=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/前端壳子与接口层开发说明.md | 60 +++++++---- frontend/src/components/common/AppShell.vue | 6 +- .../src/components/common/CommandPalette.vue | 101 ++++++++++++++++++ .../src/components/common/PrimarySidebar.vue | 24 ++++- .../components/common/SecondarySidebar.vue | 16 +-- frontend/src/components/common/StatusBar.vue | 4 +- .../src/features/common/PlaceholderView.vue | 34 ------ frontend/src/features/editor/EditorHeader.vue | 29 ++++- frontend/src/features/editor/EditorPane.vue | 9 +- frontend/src/router/index.ts | 17 ++- 10 files changed, 218 insertions(+), 82 deletions(-) create mode 100644 frontend/src/components/common/CommandPalette.vue delete mode 100644 frontend/src/features/common/PlaceholderView.vue diff --git a/docs/前端壳子与接口层开发说明.md b/docs/前端壳子与接口层开发说明.md index 72cded5..735c321 100644 --- a/docs/前端壳子与接口层开发说明.md +++ b/docs/前端壳子与接口层开发说明.md @@ -1,12 +1,12 @@ # 前端壳子与接口层开发说明 > 更新日期:2026-08-29 -> 适用范围:Vue 3 + TypeScript 前端壳子、Workspace、公共 Service、FastAPI 接口适配和 SSE。 +> 适用范围:Vue 3 + TypeScript 页面、Workspace、公共 Service、FastAPI 接口适配和 SSE。 > 文档用途:帮助团队理解当前前端可用能力、模块边界、启动方式和后续页面开发入口。 ## 1. 当前实现状态 -本轮修复后,前端已经形成一条可安装、可类型检查、可生产构建的基础链路: +当前前端已经形成一条可安装、可类型检查、可生产构建和可联调的基础链路: ```text Vue Router @@ -17,25 +17,32 @@ Vue Router → FastAPI ``` -当前可以使用的界面包括: +当前已经落地的页面和公共界面包括: - Vault 入口页; - 应用标题栏、主侧边栏、辅助侧边栏和状态栏; - Workspace 文件树; -- 基础 Markdown 文本编辑区; +- Markdown 写作/源码模式、手动保存和自动保存状态; - 文件打开、新建、删除和重命名交互壳子; -- 未完成模块的统一占位页。 +- Search 查询、筛选、结果列表和 Citation 定位; +- Chat 会话、Provider/Model/Skill 选择和 SSE 输出; +- Agent Run 创建、Trace、取消和权限确认; +- Task 筛选、创建、编辑、状态切换和删除; +- Skill、Plugin 生命周期管理; +- Theme 预览、切换和编辑器 Token 覆盖; +- Settings 的通用、编辑器、Provider、索引、权限和 AI Core 诊断分区; +- 可收起主导航、功能型二级侧栏、状态栏和 `Ctrl+P` 命令面板。 -Search、Chat、Agent、Task、Skill、Plugin、Theme 和 Settings 已保留稳定路由,但除公共 Store 与 Service 外,具体业务页面仍待后续实现。占位页用于保证主分支可构建、导航目标可识别,不代表对应功能页面已经验收。 +原统一占位页已经删除,所有已注册业务路由均指向真实页面。当前 Workspace 文件能力仍使用 Web Mock Adapter;Tauri 文件系统、Stronghold 和桌面窗口能力应在桌面容器阶段接入,不影响页面与 Store 的调用边界。 ## 2. 目录与职责 ```text frontend/src/ -├── components/common/ App Shell 与公共导航组件 +├── components/common/ App Shell、导航、命令面板与扩展公共组件 ├── contracts/index.ts UI View Model 与 FastAPI Wire DTO -├── features/common/ 未实现功能的统一占位页 -├── features/editor/ 基础编辑器头部与文本编辑区 +├── features/ 按页面领域拆分的业务组件 +├── features/editor/ 编辑器头部与写作/源码编辑区 ├── features/vault/ Vault 入口 ├── features/workspace/ Workspace 与递归文件树 ├── router/index.ts 页面路由和 Vault Guard @@ -69,9 +76,23 @@ frontend/src/ /settings ``` -除 Vault 入口外,其余路由需要先打开 Vault。尚未完成的页面统一加载 `PlaceholderView.vue`,后续开发时应逐个替换为真实页面组件,不要在路由中提前引用尚未提交的文件。 +除 Vault 入口外,其余路由需要先打开 Vault。全部路由均使用懒加载真实页面组件,既保持首屏包体可控,也避免占位页面掩盖缺失实现。 -## 4. Workspace 与编辑器 +## 4. 页面实现边界 + +| 页面 | 当前可用能力 | 主要 Store / Service | +| --- | --- | --- | +| Workspace | 文件树、新建、重命名、删除、打开、编辑、保存、模式切换 | `workspaceStore`、`editorStore`、`workspaceService` | +| Search | FTS/Vector/Hybrid、文件夹与标签筛选、结果定位 | `searchStore`、`searchService` | +| Chat | 会话选择、模型配置、RAG、Skill、SSE、Citation | `chatStore`、`providerStore`、`chatService` | +| Agent | Run 配置、Tool 选择、Trace SSE、权限确认、取消 | `agentStore`、`agentService` | +| Tasks | 状态筛选、CRUD、完成与恢复 | `taskStore`、`taskService` | +| Skills | 列表、详情、安装、启停、卸载 | `skillStore`、`skillService` | +| Plugins | 列表、权限确认、安装、启停、卸载 | `pluginStore`、`pluginService` | +| Themes | 主题预览、应用、字体与行高覆盖、恢复默认 | `themeStore` | +| Settings | 通用、编辑器、Provider、索引、权限、诊断 | `settingsStore`、`providerStore`、相关 Service | + +## 5. Workspace 与编辑器 Workspace 当前由以下组件构成: @@ -89,7 +110,7 @@ SecondarySidebar 当前 `workspaceService` 仍是 Web 开发模式下的 Mock Adapter。保存、重命名和删除只保留调用边界,尚未接入 Tauri 文件系统命令。进入桌面端阶段后,应替换 Service 内部实现,不改变 Component 和 Store 的调用方式。 -## 5. HTTP 接口层 +## 6. HTTP 接口层 公共请求由 `apiClient.ts` 处理: @@ -115,7 +136,7 @@ Service 已适配当前 FastAPI Contract: 界面模型中存在的展示字段不能直接发送给后端。例如 Task View Model 的 `priority` 和 `source` 当前只是界面层字段,Service 创建与更新请求不会把它们发送给不支持这些字段的 FastAPI Contract。 -## 6. SSE +## 7. SSE `SseClient` 同时服务于 Chat 和 Agent Event: @@ -134,7 +155,7 @@ provider_id = mock model = mock-1 ``` -## 7. 环境和启动 +## 8. 环境和启动 ```powershell cd frontend @@ -156,7 +177,7 @@ cd frontend pnpm build ``` -## 8. 当前验证基线 +## 9. 当前验证基线 ```text pnpm build passed @@ -165,13 +186,16 @@ preview smoke HTTP 200 git diff --check passed ``` -后端测试出现过一次 `.pytest_cache` 无法写入的 Windows 权限警告,不影响 62 项测试结果,也不涉及产品代码。 +当前前端没有单独的单元测试脚本,`pnpm build` 同时执行 `vue-tsc -b` 与 Vite 生产构建。后端测试出现过一次 `.pytest_cache` 无法写入的 Windows 权限警告,不影响 62 项测试结果,也不涉及产品代码。 -## 9. 后续开发要求 +浏览器可视化冒烟在本次执行环境中因浏览器运行资源缺失未能启动;HTTP 冒烟已确认前端入口、后端健康检查与 OpenAPI 均能访问。进入合并验收前,仍建议团队在本机打开各路由完成一次人工视觉检查。 + +## 10. 后续开发要求 - 新页面文件与路由修改必须在同一提交中出现; - 新增或修改接口时同步更新 FastAPI DTO、Service 映射和接口文档; - 不允许用 `as any` 或错误返回类型掩盖 Contract 差异; - SSE 相关变更需要覆盖跨 Chunk、CRLF、多行 data、终态事件和取消; - Workspace 接入 Tauri 后,需要增加路径规范化、写入失败恢复和外部修改冲突测试; -- Search、Agent、Skill、Plugin 等占位页应按功能逐个替换,不一次提交大量空页面。 +- 页面新增交互必须经过键盘、空状态、加载状态、错误状态和窄窗口检查; +- Workspace 的写作/源码模式目前共享同一 Markdown 数据源,后续接入 Milkdown 与 CodeMirror 6 时不得改变 Store/Service 边界或造成切换丢稿。 diff --git a/frontend/src/components/common/AppShell.vue b/frontend/src/components/common/AppShell.vue index 289cfac..b53ab2f 100644 --- a/frontend/src/components/common/AppShell.vue +++ b/frontend/src/components/common/AppShell.vue @@ -4,12 +4,11 @@ import { useRoute, useRouter } from 'vue-router' import { useWorkspaceStore } from '@/stores/workspace' import { useThemeStore } from '@/stores/theme' import { useEditorStore } from '@/stores/editor' -import { useSettingsStore } from '@/stores/settings' -import { useAgentStore } from '@/stores/agent' import PrimarySidebar from './PrimarySidebar.vue' import SecondarySidebar from './SecondarySidebar.vue' import StatusBar from './StatusBar.vue' import TitleBar from './TitleBar.vue' +import CommandPalette from './CommandPalette.vue' defineProps<{ showSecondarySidebar?: boolean @@ -18,8 +17,6 @@ defineProps<{ const workspaceStore = useWorkspaceStore() const themeStore = useThemeStore() const editorStore = useEditorStore() -const settingsStore = useSettingsStore() -const agentStore = useAgentStore() const route = useRoute() const router = useRouter() @@ -58,6 +55,7 @@ defineExpose({ openCitation }) + diff --git a/frontend/src/components/common/CommandPalette.vue b/frontend/src/components/common/CommandPalette.vue new file mode 100644 index 0000000..b501f1a --- /dev/null +++ b/frontend/src/components/common/CommandPalette.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/frontend/src/components/common/PrimarySidebar.vue b/frontend/src/components/common/PrimarySidebar.vue index e5d0047..5aa7886 100644 --- a/frontend/src/components/common/PrimarySidebar.vue +++ b/frontend/src/components/common/PrimarySidebar.vue @@ -1,9 +1,10 @@ @@ -62,6 +69,11 @@ function navigate(name: string) { z-index: var(--z-sidebar); } +.primary-sidebar.expanded { width: var(--sidebar-primary-width-expanded); } +.primary-sidebar.expanded .nav-item { flex-direction: row; justify-content: flex-start; gap: var(--space-md); padding: 0 var(--space-lg); } +.primary-sidebar.expanded .nav-icon { margin-bottom: 0; } +.primary-sidebar.expanded .nav-label { font-size: var(--font-size-sm); } + .nav-list { flex: 1; padding: var(--space-sm) 0; @@ -121,4 +133,6 @@ function navigate(name: string) { padding: var(--space-sm) 0; border-top: 1px solid var(--color-border-subtle); } + +.collapse-button { width: calc(100% - 8px); } diff --git a/frontend/src/components/common/SecondarySidebar.vue b/frontend/src/components/common/SecondarySidebar.vue index 99c5a68..1c51e1f 100644 --- a/frontend/src/components/common/SecondarySidebar.vue +++ b/frontend/src/components/common/SecondarySidebar.vue @@ -1,6 +1,11 @@ - - - - diff --git a/frontend/src/features/editor/EditorHeader.vue b/frontend/src/features/editor/EditorHeader.vue index 42a2801..82d0497 100644 --- a/frontend/src/features/editor/EditorHeader.vue +++ b/frontend/src/features/editor/EditorHeader.vue @@ -4,12 +4,24 @@ import { useWorkspaceStore } from '@/stores/workspace' const editorStore = useEditorStore() const workspaceStore = useWorkspaceStore() + +const statusText: Record = { + idle: '空闲', dirty: '未保存', saving: '保存中…', saved: '已保存', save_failed: '保存失败', + external_changed: '外部文件已变化', conflict: '存在编辑冲突', +} @@ -23,4 +35,17 @@ const workspaceStore = useWorkspaceStore() border-bottom: 1px solid var(--color-border-subtle); color: var(--color-text-secondary); } +.file-identity { display: grid; min-width: 0; } +.file-identity strong, .file-identity small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.file-identity small { color: var(--color-text-tertiary); font-size: var(--font-size-xs); } +.editor-actions, .mode-switch { display: flex; align-items: center; gap: var(--space-sm); } +.save-status { color: var(--color-text-tertiary); font-size: var(--font-size-xs); } +.save-status.dirty, .save-status.external_changed { color: var(--color-warning); } +.save-status.save_failed, .save-status.conflict { color: var(--color-error); } +.save-status.saved { color: var(--color-success); } +.mode-switch { gap: 2px; padding: 2px; border-radius: var(--radius-md); background: var(--color-background-secondary); } +.mode-switch button, .save-button { padding: 5px 9px; border-radius: var(--radius-sm); } +.mode-switch button.active { background: var(--color-surface-primary); color: var(--color-accent-primary); box-shadow: var(--shadow-sm); } +.save-button { background: var(--color-accent-primary); color: var(--color-text-inverse); } +.save-button:disabled { opacity: .55; } diff --git a/frontend/src/features/editor/EditorPane.vue b/frontend/src/features/editor/EditorPane.vue index d4a60ba..f575ee5 100644 --- a/frontend/src/features/editor/EditorPane.vue +++ b/frontend/src/features/editor/EditorPane.vue @@ -5,10 +5,12 @@ const editorStore = useEditorStore()