diff --git a/docs/前端合并审阅问题与修复复盘.md b/docs/前端合并审阅问题与修复复盘.md new file mode 100644 index 0000000..2249db4 --- /dev/null +++ b/docs/前端合并审阅问题与修复复盘.md @@ -0,0 +1,276 @@ +# 前端合并审阅问题与修复复盘 + +> 审阅与修复日期:2026-08-29 +> 涉及提交:`f9efc4f`,合并提交 `c6c28e4`。 +> 文档用途:记录前端分支合并后暴露的问题域、形成原因、实际后果、修复思路和落地方案,供后续技术文档、比赛材料与博客写作使用。 + +## 1. 结论 + +原前端提交一次增加了 42 个文件和约 8000 行内容,但没有在提交前执行成功的生产构建。合并后同时存在工程配置、组件完整性、接口契约、流式协议和文件树状态五个问题域。 + +本轮处理结果: + +| 编号 | 问题域 | 原级别 | 处理结果 | +| --- | --- | --- | --- | +| F-01 | TypeScript 与生产构建不可用 | P0 | 已修复,`pnpm build` 通过 | +| F-02 | 路由引用未提交页面 | P0 | 已改为统一占位页,并补充基础编辑器组件 | +| F-03 | 前后端 Contract 系统性漂移 | P1 | 已增加 Wire DTO 和显式 Service 映射 | +| F-04 | SSE 跨网络分片丢失事件 | P1 | 已重写增量解析状态机 | +| F-05 | 文件树右键操作目标错误 | P1 | 已改为保存实际右键节点 | +| F-06 | 根目录新增文件不可见且路径异常 | P2 | 已处理顶层插入与路径拼接 | +| F-07 | Chat 仍使用模拟流式输出 | P2 | 已接入真实 `/api/chat` SSE | + +## 2. F-01:TypeScript 与生产构建不可用 + +### 原因 + +Vite 配置了 `@` 指向 `src`,但 `tsconfig.app.json` 没有配置 `baseUrl` 和 `paths`。Vite 和 TypeScript 使用不同的模块解析配置,只配置其中一侧后,开发服务器可能暂时工作,`vue-tsc` 仍无法解析全部别名。 + +提交中还存在多个独立错误: + +- `ComputedRef` 与字符串直接比较,缺少 `.value`; +- `FileTreePanel.vue` 在两个 Script 中重复导入 `FileNode`; +- 浏览器 ESM 代码调用 CommonJS `require()`; +- Chat Store 使用不存在的 `conversation_id` 变量; +- Service 聚合文件导出不存在的 `ApiError`; +- StatusBar 使用没有表达式的 `@click`。 + +### 后果 + +- `pnpm build` 无法生成生产包; +- CI 无法验证前端; +- 别名错误产生的大量隐式 `any` 干扰真正错误定位; +- `main` 不再满足“可构建”要求。 + +### 解决思路 + +先恢复唯一可信的构建基线,再处理运行时问题。路径别名同时配置给 Vite 和 TypeScript,其余错误按 Vue 3 Composition API 和浏览器 ESM 规则逐项修复。 + +### 解决方案 + +- 在 `tsconfig.app.json` 增加 `baseUrl` 和 `@/*` 映射; +- 在 Script 中通过 `.value` 读取 ComputedRef; +- 拆分递归文件树组件,删除第二个 Script 和 `require()`; +- 修正 Chat 变量名和类型导出; +- 删除无意义的空事件绑定; +- 将 `pnpm build` 作为提交前强制检查。 + +## 3. F-02:路由和公共组件引用未提交文件 + +### 原因 + +路由表和 Secondary Sidebar 按最终页面结构一次性写完,但对应页面没有随提交进入仓库。Workspace 也引用了不存在的 `EditorHeader.vue` 和 `EditorPane.vue`。缺失项覆盖 Search、Chat、Agent、Task、Skill、Plugin、Theme、Settings 和多个 Sidebar Panel,共 21 个 Vue 文件。 + +### 后果 + +- 修复路径别名后,TypeScript 和 Vite 仍因模块不存在而失败; +- 开发者无法判断页面是遗漏提交,还是尚未实现; +- 后续成员可能分别创建同名但职责不同的组件。 + +### 解决思路 + +路由只能引用当前提交真实存在的组件。为保留产品信息架构,使用一个明确标注“功能开发中”的公共占位页,避免建立一批内容为空的伪页面。 + +### 解决方案 + +- 新增统一 `PlaceholderView.vue`; +- 未实现功能路由暂时指向占位页; +- Secondary Sidebar 对未实现 Panel 显示说明文本; +- 增加可运行的基础 Editor Header 和 Textarea Pane; +- 文档明确占位路由不代表业务页面完成。 + +## 4. F-03:前后端 Contract 系统性漂移 + +### 原因 + +前端先按页面需要定义了扁平 View Model,并直接把它们作为 HTTP 请求和响应类型。后端已经形成明确的 Pydantic Contract,包括分页包装、嵌套 Manifest、枚举和值对象,两边没有通过 OpenAPI 或人工核对完成同步。 + +典型差异: + +| 模块 | 原前端假设 | FastAPI 实际 Contract | +| --- | --- | --- | +| Notes | `folder_path/content` | `folder/markdown` | +| Search | 单值筛选、`results/total` | 数组筛选、`items/page` | +| Agent | `task`、可选 Provider | `input`、Provider 与 Model 必填 | +| Permission | `allow + scope` | `allow_once/allow_session/deny` | +| Skill / Plugin | 扁平对象 | `manifest + runtime status` | +| Provider | Capability 对象 | Capability 数组 | +| Task | `due_date`、priority、source | `due_at`,后两项尚未进入后端 | +| Index | `full/fts/vector` | `all/notes/vectors` | + +### 后果 + +- Agent 创建、Permission 响应等请求稳定返回 422; +- 列表接口拿到对象后被当成数组使用; +- Skill、Plugin 和 Provider 页面读取不到标识和能力; +- TypeScript 声称调用安全,但运行时结构完全不同; +- 捕获异常后回退 Mock 会掩盖真实联调失败。 + +### 解决思路 + +区分 Wire DTO 和 View Model。HTTP 边界严格使用与 FastAPI 一致的 `Api*` 类型,Service 显式完成转换,页面展示字段不反向污染后端请求。 + +### 解决方案 + +- 增加 `ApiNote`、`ApiAgentRun`、`ApiSkill`、`ApiPlugin`、`ApiProviderConfig`、`ApiTask`、`ApiIndexStatus` 等 Wire DTO; +- Notes Service 改用 `folder`、`markdown` 和真实分页结构; +- Search Service 将单值 UI Filter 转换为后端数组,并映射 `items/page`; +- Agent Service 使用 `input`、`tool_timeout_seconds` 和 `run_timeout_seconds`; +- Permission Store 将 `allow + once/session` 转换为后端枚举; +- Skill 和 Plugin Service 展开嵌套 Manifest; +- 增加 Plugin Permission PUT; +- Provider Capability 数组转换为界面布尔 Map; +- Task Service 只发送后端支持字段,并转换 `due_date/due_at`; +- Index Service 显式转换 Scope; +- 默认离线 Provider ID 统一为后端的 `mock`。 + +## 5. F-04:SSE 跨网络分片丢失事件 + +### 原因 + +旧解析器把 `eventName` 和 `dataStr` 声明在每次 `reader.read()` 的循环内部。网络 Chunk 与 SSE Event 没有一一对应关系,一个事件的 `event:`、`data:` 和结尾空行可以分别落在多个 Chunk 中。 + +```text +Chunk 1: event: TextDelta\n +Chunk 2: data: {"event":"TextDelta", ...}\n\n +``` + +读取 Chunk 2 时事件名已被重置成 `message`。如果 data 与空行分开,data 内容也会丢失。 + +### 后果 + +- Chat 增量文本偶发不显示; +- Agent 终态事件无法触发完成回调; +- 问题受网络分片影响,开发机难以稳定复现; +- 长回答和远程 Provider 更容易出现错误。 + +### 解决思路 + +SSE 解析状态必须跨 Chunk 保存,以完整行和空行结束事件为边界,不能以单次网络读取为边界。 + +### 解决方案 + +- 将 Event Name 和 Data Lines 移到读取循环外; +- Buffer 只移除已经形成完整行的内容; +- 支持 LF、CRLF、多行 data 和注释行; +- 流结束时 flush TextDecoder 和剩余 Event; +- 终态回调增加去重; +- SSE URL 复用普通 HTTP 的 Base URL 解析。 + +## 6. F-05:文件树右键操作目标错误 + +### 原因 + +右键菜单打开时保存了 `contextMenuPath`,但执行删除和重命名时读取的是 `workspaceStore.activeFile`。右键节点与当前编辑节点是两个独立状态。 + +### 后果 + +用户右键未激活文件并点击删除时,可能关闭或修改正在编辑的另一个文件。这属于潜在数据破坏问题。 + +### 解决思路 + +菜单操作必须绑定菜单打开时的目标对象,不能在点击命令时从无关的 Active State 推断。 + +### 解决方案 + +- 使用 `contextTarget: Ref` 保存右键节点; +- Rename 和 Delete 只消费 `contextTarget`; +- 菜单关闭后清空目标; +- 将递归 Node 独立为 `FileTreeNode.vue`,通过类型化 Emit 向上传递节点。 + +## 7. F-06:根目录新增文件不可见且路径异常 + +### 原因 + +Store 把 `/` 当作普通父节点查找,但文件树没有代表根目录的虚拟节点。Service 直接使用 `folderPath + '/' + name` 拼接路径,根目录会得到 `//name.md`。 + +### 后果 + +- Service 返回成功,但新建项目没有加入界面文件树; +- 打开的文件路径带双斜杠; +- 接入真实文件系统后可能产生平台间路径差异。 + +### 解决思路与方案 + +顶层数组本身就是根节点的 children。当 `parentPath` 为 `/` 或空字符串时直接写入 `fileTree.value`,Mock Service 拼接根目录路径时只保留一个 `/`。 + +## 8. F-07:Chat 使用模拟流式输出 + +### 原因 + +Chat Store 已经存在 SSE Service,但发送消息后仍通过 `setInterval` 拼接固定文本,没有调用后端。 + +### 后果 + +- 后端 Provider、RAG、错误事件和取消无法通过前端验证; +- 页面看似工作,实际没有形成前后端链路; +- SSE 解析缺陷长期被 Mock 掩盖。 + +### 解决思路与方案 + +保留初始展示数据,但用户主动发送消息时调用真实 `/api/chat`。请求使用当前 Provider、Model、RAG 开关和消息历史;TextDelta 追加到 Assistant Message;Error、网络失败、Done 和主动取消同步更新 Streaming State。默认使用离线 `mock / mock-1`,无需外部 API Key。 + +## 9. 验证 + +```powershell +cd frontend +pnpm install --frozen-lockfile +pnpm build + +cd ../backend +uv run pytest + +cd .. +git diff --check +``` + +结果: + +```text +frontend production build passed +73 frontend modules transformed +62 backend tests passed +preview returned HTTP 200 +git diff --check passed +``` + +## 10. 预防措施 + +- PR 创建前必须执行与 CI 相同的 `pnpm build`; +- 路由只引用当前提交存在的文件; +- FastAPI `/openapi.json` 是 Wire Contract 的唯一事实来源; +- View Model 与 API DTO 分层,Service 必须显式转换; +- 不用 Mock Fallback 掩盖 4xx、5xx 和契约错误; +- SSE 测试按任意 Chunk 边界构造数据,不能假定一次 read 等于一次 Event; +- 删除、移动和覆盖等高影响操作必须携带明确目标 ID 或对象; +- 合并后如果发现 P0,先恢复主分支构建,再继续业务页面开发。 + +## 11. 当前边界与后续事项 + +首次修复解决了前端壳子的工程正确性和接口边界。之后已继续补齐全部业务路由页面;Tauri Host、Stronghold 和真实文件系统仍属于桌面集成阶段。后续仍需要: + +- 为 Service DTO 映射增加自动化契约测试; +- 为 SSE Parser 增加跨 Chunk 单元测试; +- 用 Tauri Command 替换 Mock Workspace Service; +- 在 CI 中加入前端构建和后端测试两个必需检查。 + +## 12. 全页面完成后的第二轮审阅与修复 + +全部页面接通后再次审阅,发现编译通过并不等于交互状态正确。本轮问题与处理如下: + +| 编号 | 问题 | 原因与后果 | 解决方案 | +| --- | --- | --- | --- | +| F-08 | Markdown 没有真实渲染 | 写作与源码模式共用同一个 `textarea`,Chat 也把 Markdown 当纯文本显示 | 使用 `marked` 解析 GFM,使用 DOMPurify 清洗 HTML;编辑页提供源码输入和实时预览,Chat 回答复用安全渲染器 | +| F-09 | 重命名后路径仍是旧值 | 只修改节点名称,没有同步节点、子节点、打开文件和编辑器路径,后续保存或删除会作用于旧路径 | 新增递归路径迁移,同时更新 `openFiles`、活动路径和 Editor 当前路径;Mock 内容缓存也随路径迁移 | +| F-10 | 删除活动文件后正文错位 | Workspace 切换了活动文件,但 Editor 仍保留已删除文件正文 | 删除文件或文件夹时统一清理其所有打开路径;若存在下一个文件则加载,否则关闭编辑器 | +| F-11 | 异步读取和保存存在竞态 | 快速切换文件可能让较早请求覆盖较新文件;保存过程中继续编辑会被错误标记为已保存 | 使用读取版本号丢弃过期结果;保存使用路径和正文快照,只有快照仍是最新内容时才标记 `saved` | +| F-12 | Agent 权限弹窗跨 Run 残留 | 切换 Run、ToolResult 和终态事件没有释放 PermissionRequest | 加载 Run 前清空请求,并在 ToolResult、Completed、Failed、Cancelled 时同步清理;同时更新本地 Run 状态 | +| F-13 | Chat 丢弃非文本 SSE 事件 | Store 只处理 TextDelta 和 Error,Tool Call 请求会留下空消息 | 增加 Thinking、ToolCall Start/Delta/End、Usage 和 Citation 状态处理及页面卡片展示 | +| F-14 | Task 字段表现为保存但实际丢失 | 前端展示后端不支持的 Priority/Source,更新请求又漏掉后端已支持的 `note_id` | 暂时移除不可持久化字段的编辑与筛选;补齐 `note_id` 更新与解除关联的 `null` 语义 | +| F-15 | 编辑器设置不生效 | Settings Store 与 Editor/Theme 没有联动,自动保存固定为 1500ms | 自动保存、默认模式、拼写检查、字号、行高和行宽改为实际驱动编辑器,并保存到 Local Storage | +| F-16 | Vector 降级提示永远不可达 | `vectorUnavailable` 只声明不赋值,向量错误直接清空结果 | 对明确的向量、Embedding、模型和 Provider 不可用错误自动重试 FTS,并显示降级状态 | +| F-17 | 护眼主题与 Plugin 导航状态异常 | Sepia 只有预览卡没有 Token;Plugin 路由被错误映射为 Skill 激活状态 | 增加 Sepia Design Token,并按真实路由名计算主导航选中项 | +| F-18 | 文件切换仍可能丢失未保存内容 | `loadFile` 直接替换路径和正文,且旧的自动保存定时器会在切换后保存错误文件 | 切换前取消定时器,等待正在执行的保存并保存最新快照;保存失败或存在冲突时阻止切换;各入口仅在加载成功后更新 Workspace 活动路径 | +| F-19 | 编辑器外观启动时被默认值覆盖 | Theme Store 的立即监听早于 `initTheme` 执行,先把默认值写进 Local Storage | 增加 Hydration 状态,初始化前监听只更新 CSS,不持久化;读取本地配置完成后再允许写入 | + +安全边界:Markdown 解析结果不得直接使用未经清洗的 `v-html`。DOMPurify 是渲染链路的必需依赖,后续升级 `marked` 或允许扩展 Markdown 时也必须保留清洗步骤。 diff --git a/docs/前端壳子与接口层开发说明.md b/docs/前端壳子与接口层开发说明.md new file mode 100644 index 0000000..735c321 --- /dev/null +++ b/docs/前端壳子与接口层开发说明.md @@ -0,0 +1,201 @@ +# 前端壳子与接口层开发说明 + +> 更新日期:2026-08-29 +> 适用范围:Vue 3 + TypeScript 页面、Workspace、公共 Service、FastAPI 接口适配和 SSE。 +> 文档用途:帮助团队理解当前前端可用能力、模块边界、启动方式和后续页面开发入口。 + +## 1. 当前实现状态 + +当前前端已经形成一条可安装、可类型检查、可生产构建和可联调的基础链路: + +```text +Vue Router +→ App Shell +→ Pinia Store +→ Service / FastAPI DTO Adapter +→ HTTP 或 SSE +→ FastAPI +``` + +当前已经落地的页面和公共界面包括: + +- Vault 入口页; +- 应用标题栏、主侧边栏、辅助侧边栏和状态栏; +- Workspace 文件树; +- Markdown 写作/源码模式、手动保存和自动保存状态; +- 文件打开、新建、删除和重命名交互壳子; +- Search 查询、筛选、结果列表和 Citation 定位; +- Chat 会话、Provider/Model/Skill 选择和 SSE 输出; +- Agent Run 创建、Trace、取消和权限确认; +- Task 筛选、创建、编辑、状态切换和删除; +- Skill、Plugin 生命周期管理; +- Theme 预览、切换和编辑器 Token 覆盖; +- Settings 的通用、编辑器、Provider、索引、权限和 AI Core 诊断分区; +- 可收起主导航、功能型二级侧栏、状态栏和 `Ctrl+P` 命令面板。 + +原统一占位页已经删除,所有已注册业务路由均指向真实页面。当前 Workspace 文件能力仍使用 Web Mock Adapter;Tauri 文件系统、Stronghold 和桌面窗口能力应在桌面容器阶段接入,不影响页面与 Store 的调用边界。 + +## 2. 目录与职责 + +```text +frontend/src/ +├── components/common/ App Shell、导航、命令面板与扩展公共组件 +├── contracts/index.ts UI View Model 与 FastAPI Wire DTO +├── features/ 按页面领域拆分的业务组件 +├── features/editor/ 编辑器头部与写作/源码编辑区 +├── features/vault/ Vault 入口 +├── features/workspace/ Workspace 与递归文件树 +├── router/index.ts 页面路由和 Vault Guard +├── services/ HTTP、SSE、DTO 映射和模块 API +├── stores/ Pinia 状态 +└── styles/tokens.css Design Token +``` + +职责约定: + +- Component 不直接拼接后端 URL; +- Store 负责页面状态和业务操作编排; +- Service 负责 HTTP/SSE 调用以及 Wire DTO 到 View Model 的转换; +- `contracts/index.ts` 同时保留界面模型和以 `Api` 开头的 FastAPI DTO,两者不能混用; +- OpenAPI `/openapi.json` 是后端 Wire Contract 的最终依据。 + +## 3. 路由与页面壳子 + +已注册路由: + +```text +/ +/workspace +/search +/chat +/agent/runs/:runId? +/tasks +/extensions/skills +/extensions/plugins +/themes +/settings +``` + +除 Vault 入口外,其余路由需要先打开 Vault。全部路由均使用懒加载真实页面组件,既保持首屏包体可控,也避免占位页面掩盖缺失实现。 + +## 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 当前由以下组件构成: + +```text +WorkspaceView +├── EditorHeader +└── EditorPane + +SecondarySidebar +└── FileTreePanel + └── FileTreeNode(递归) +``` + +文件树把右键目标保存在 `contextTarget`,重命名和删除始终作用于实际被右键的节点,不再依赖当前编辑文件。根目录使用 `/` 表示,新增根级文件时直接写入 Store 顶层数组。 + +当前 `workspaceService` 仍是 Web 开发模式下的 Mock Adapter。保存、重命名和删除只保留调用边界,尚未接入 Tauri 文件系统命令。进入桌面端阶段后,应替换 Service 内部实现,不改变 Component 和 Store 的调用方式。 + +## 6. HTTP 接口层 + +公共请求由 `apiClient.ts` 处理: + +- 支持 GET、POST、PUT、PATCH 和 DELETE; +- 使用 `VITE_API_BASE_URL`,并兼容旧的 `VITE_API_BASE`; +- 自动附加 `X-Request-Id`; +- 将后端统一错误体转换为 `ApiErrorClass`; +- 204 响应返回 `undefined`。 + +Service 已适配当前 FastAPI Contract: + +| 模块 | 主要适配内容 | +| --- | --- | +| Notes | `folder`、`markdown`、直接 Note 响应和 `{items, page}` | +| Search | 数组筛选字段、`items/page` 响应和 Search View Model 映射 | +| Chat | `provider_id`、`model`、`messages` 和 ModelEvent SSE | +| Agent | `input`、秒级 Timeout 字段、Run DTO 和 Permission Decision | +| Skill / Plugin | 嵌套 `manifest`、安装 `package_path` 和 Plugin Permission PUT | +| Provider | Provider Type、Capability 数组、模型列表包装和 Test 响应 | +| Task | `due_at`、分页响应和当前后端支持字段 | +| Index | `all/notes/vectors` Scope、Job 与状态 DTO | +| System | `/health` 和 `/api/status` 的真实响应字段 | + +界面模型中存在的展示字段不能直接发送给后端。例如 Task View Model 的 `priority` 和 `source` 当前只是界面层字段,Service 创建与更新请求不会把它们发送给不支持这些字段的 FastAPI Contract。 + +## 7. SSE + +`SseClient` 同时服务于 Chat 和 Agent Event: + +- 使用与普通 HTTP 相同的 API Base URL; +- 支持 POST Chat Stream 和 GET Agent Event Stream; +- 使用 `TextDecoder` 处理 UTF-8 增量字节; +- 在网络分片之间保留 `event` 和多行 `data` 状态; +- 以空行作为单个 SSE Event 的结束标志; +- 识别 `Done`、`RunCompleted`、`RunFailed` 和 `RunCancelled`; +- 支持 AbortController 主动取消。 + +Chat Store 已从定时器模拟输出切换为真实 `/api/chat` SSE。默认离线联调配置为: + +```text +provider_id = mock +model = mock-1 +``` + +## 8. 环境和启动 + +```powershell +cd frontend +pnpm install --frozen-lockfile +pnpm dev +``` + +联调前在另一个终端启动后端: + +```powershell +cd backend +uv run uvicorn app.main:app --reload --host 127.0.0.1 --port 8000 +``` + +生产构建: + +```powershell +cd frontend +pnpm build +``` + +## 9. 当前验证基线 + +```text +pnpm build passed +uv run pytest 62 passed +preview smoke HTTP 200 +git diff --check passed +``` + +当前前端没有单独的单元测试脚本,`pnpm build` 同时执行 `vue-tsc -b` 与 Vite 生产构建。后端测试出现过一次 `.pytest_cache` 无法写入的 Windows 权限警告,不影响 62 项测试结果,也不涉及产品代码。 + +浏览器可视化冒烟在本次执行环境中因浏览器运行资源缺失未能启动;HTTP 冒烟已确认前端入口、后端健康检查与 OpenAPI 均能访问。进入合并验收前,仍建议团队在本机打开各路由完成一次人工视觉检查。 + +## 10. 后续开发要求 + +- 新页面文件与路由修改必须在同一提交中出现; +- 新增或修改接口时同步更新 FastAPI DTO、Service 映射和接口文档; +- 不允许用 `as any` 或错误返回类型掩盖 Contract 差异; +- SSE 相关变更需要覆盖跨 Chunk、CRLF、多行 data、终态事件和取消; +- Workspace 接入 Tauri 后,需要增加路径规范化、写入失败恢复和外部修改冲突测试; +- 页面新增交互必须经过键盘、空状态、加载状态、错误状态和窄窗口检查; +- Workspace 的写作/源码模式目前共享同一 Markdown 数据源,后续接入 Milkdown 与 CodeMirror 6 时不得改变 Store/Service 边界或造成切换丢稿。 diff --git a/frontend/package.json b/frontend/package.json index e02a8eb..8fa7393 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,6 +23,7 @@ "@milkdown/vue": "^7.22.0", "@vueuse/core": "^14.0.0", "codemirror": "^6.0.0", + "dompurify": "^3.4.14", "marked": "^15.0.0", "pinia": "^4.0.0", "vue": "^3.5.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index fa6908b..87bc7a4 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: codemirror: specifier: ^6.0.0 version: 6.0.2 + dompurify: + specifier: ^3.4.14 + version: 3.4.14 marked: specifier: ^15.0.0 version: 15.0.12 @@ -829,11 +832,6 @@ packages: '@volar/typescript@2.4.15': resolution: {integrity: sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true '@vue-macros/common@3.1.4': resolution: {integrity: sha512-/5Fv+6DgIcM9ajY05ZmKBv+LMX1M9A0X+IUwDRVdt67ciw8OV9bvG2r34p3RiEadlsQybjhKPRKNXDC8Bp23cw==} @@ -2608,13 +2606,11 @@ snapshots: '@volar/source-map@2.4.15': {} - '@volar/typescript@2.4.15(typescript@5.9.3)': + '@volar/typescript@2.4.15': dependencies: '@volar/language-core': 2.4.15 path-browserify: 1.0.1 vscode-uri: 3.2.0 - optionalDependencies: - typescript: 5.9.3 '@vue-macros/common@3.1.4(vue@3.5.41(typescript@5.9.3))': dependencies: @@ -3652,7 +3648,7 @@ snapshots: vue-tsc@2.2.12(typescript@5.9.3): dependencies: - '@volar/typescript': 2.4.15(typescript@5.9.3) + '@volar/typescript': 2.4.15 '@vue/language-core': 2.2.12(typescript@5.9.3) typescript: 5.9.3 diff --git a/frontend/src/components/common/AppShell.vue b/frontend/src/components/common/AppShell.vue index d6ef9a7..fe7c007 100644 --- a/frontend/src/components/common/AppShell.vue +++ b/frontend/src/components/common/AppShell.vue @@ -1,15 +1,15 @@ + + + + diff --git a/frontend/src/components/common/ExtensionListPanel.vue b/frontend/src/components/common/ExtensionListPanel.vue new file mode 100644 index 0000000..bb18ca0 --- /dev/null +++ b/frontend/src/components/common/ExtensionListPanel.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/frontend/src/components/common/PrimarySidebar.vue b/frontend/src/components/common/PrimarySidebar.vue index e5d0047..8a24394 100644 --- a/frontend/src/components/common/PrimarySidebar.vue +++ b/frontend/src/components/common/PrimarySidebar.vue @@ -1,9 +1,10 @@ @@ -62,6 +67,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 +131,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 34c8a1b..1c51e1f 100644 --- a/frontend/src/components/common/SecondarySidebar.vue +++ b/frontend/src/components/common/SecondarySidebar.vue @@ -27,7 +27,7 @@ const sidebarTitle = computed(() => { return titles[props.component || ''] || '' }) -const showSkillToggle = computed(() => routeName === 'skills' || routeName === 'plugins') +const showSkillToggle = computed(() => routeName.value === 'skills' || routeName.value === 'plugins')