This commit is contained in:
2026-08-07 13:56:29 +08:00
commit 152d5273a5
306 changed files with 38651 additions and 0 deletions
@@ -0,0 +1,264 @@
# Frontend 自定义暗色主题 — Design Spec
**日期:** 2026-08-06
**状态:** Approved
**参考:** 项目根目录 `image.png`(深色三栏 AI IDE 风格)
## 背景
`src/views/frontend/index.vue` 是核心 AI 对话页面,当前使用 Element Plus 默认浅色主题 + 局部样式,整体观感偏传统后台。用户要求基于 `image.png`(类 Cursor 的暗色三栏 IDE 风)对 frontend 自定义样式,以匹配产品"AI 智能体 IDE"的定位。
## 目标
将 frontend 页面(不含管理后台)改造为:
1. 暗色三栏布局:左(会话列表)/ 中(聊天 + 输入)/ 右(文件列表面板 + 预览面板)
2. 统一的深色主题 token,作用域隔离,不污染管理后台
3. 响应式:屏幕变窄时自动收起右侧文件面板,必要时隐藏侧边栏
4. 代码块使用 **Shiki** 渲染,支持与聊天消息中 Markdown 代码块 + 文件预览中的源码文件同款高亮
## 非目标
- **不**改动管理后台(`src/views/system/*``src/views/tool/*` 等)
- **不**新增功能(Git 工具、Progress 面板、终端、Workspace 切换等),仅样式
- **不**调整后端 API、不改 WebSocket 协议
- **不**引入新的 UI 组件库(继续使用 Element Plus + 少量自定义)
- **不**重构 ChatWindow 的消息结构 / 渲染逻辑(保留 markdown-it,仅替换代码块 highlight 插件)
## 现有约束
| 项目 | 现状 |
| --- | --- |
| 根容器类名 | `.frontend-container`(已存在于 `index.vue:861` |
| Markdown 渲染 | 已用 `markdown-it@14.3.0`ChatWindow |
| 代码高亮 | 暂未配置(默认无高亮或浏览器 monospace) |
| 布局 | flex 三栏:sidebar / main-content / file-panel-wrapper + file-preview-wrapper |
| 文件面板显隐 | 由 `filePanelVisible` ref 控制(小屏需保持这个行为) |
| Element Plus 版本 | 2.13.1(支持 `dark` 命名空间 + CSS 变量覆盖) |
## 方案概览
### Phase 1:主题基础设施(半天)
新增 `src/views/frontend/styles/theme.scss`,定义 SCSS 变量 + CSS 自定义属性,**作用域挂载在 `.frontend-container` 下**
```scss
.frontend-container {
// 基础色
--fe-bg-base: #1e1e1e;
--fe-bg-elevated: #252526;
--fe-bg-overlay: #2d2d30;
--fe-bg-input: #3c3c3c;
// 边框
--fe-border: #3c3c3c;
--fe-border-strong: #555;
// 文本
--fe-text-primary: #e6e6e6;
--fe-text-secondary: #a0a0a0;
--fe-text-muted: #6e6e6e;
--fe-text-inverse: #1e1e1e;
// 品牌色
--fe-accent: #4ec9b0;
--fe-accent-hover: #5fd9c0;
--fe-danger: #f48771;
--fe-warning: #dcdcaa;
--fe-info: #569cd6;
// 布局尺寸
--fe-sidebar-width: 260px;
--fe-file-panel-width: 280px;
--fe-file-preview-width: 600px;
// 阴影 / 圆角
--fe-radius-sm: 4px;
--fe-radius-md: 6px;
--fe-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
}
```
并 override Element Plus 暗色变量(仅在 `.frontend-container` 作用域内有效,避免影响其他页面):
```scss
.frontend-container {
--el-bg-color: var(--fe-bg-elevated);
--el-bg-color-page: var(--fe-bg-base);
--el-bg-color-overlay: var(--fe-bg-overlay);
--el-text-color-primary: var(--fe-text-primary);
--el-text-color-regular: var(--fe-text-secondary);
--el-text-color-secondary: var(--fe-text-muted);
--el-border-color: var(--fe-border);
--el-border-color-light: var(--fe-border);
--el-fill-color-blank: var(--fe-bg-elevated);
--el-color-primary: var(--fe-accent);
--el-color-primary-light-3: var(--fe-accent-hover);
--el-color-danger: var(--fe-danger);
--el-color-warning: var(--fe-warning);
--el-color-info: var(--fe-info);
}
```
`index.vue` 顶部 `@import './styles/theme.scss';` 引入一次。
### Phase 2:组件样式改造(1.5 天,逐组件,**不改 props/emit/函数体**
| 组件 | 主要变更 |
| --- | --- |
| `ConversationSidebar.vue` | 整体改为暗色 token;激活态用 `--fe-accent` 替代 `#409eff`;新消息小红点保留;右侧收缩按钮用暗色 hover |
| `UserArea.vue` | 头像区背景色改暗;昵称文字 `--fe-text-primary`;按钮 hover 用 `--fe-bg-overlay` |
| `ChatHeader.vue` | 顶部栏背景 `--fe-bg-elevated`,下边框 `--fe-border`;模型选择器 / 清空按钮使用暗色变量 |
| `ChatWindow.vue` | 消息气泡:用户右侧 `--fe-accent` 浅色背景;助手左侧 `--fe-bg-elevated`;代码块由 Shiki 输出;Markdown 文本颜色用 `--fe-text-primary` |
| `InputArea.vue` | 输入框背景 `--fe-bg-input`;按钮图标色改 `--fe-text-secondary`loading 状态颜色保留 |
| `FilePanel.vue` | 列表背景 `--fe-bg-elevated`,选中态 `--fe-bg-overlay` + `--fe-accent` 边框 |
| `UserInfoDialog.vue` | `el-dialog` 暗色(依赖 EP 变量覆盖);头像遮罩、按钮沿用暗色 token |
| `index.vue` 文件预览面板 | 顶部 header 用 `--fe-bg-elevated`;文本预览 `<pre>` 用 Shiki 输出背景 `--fe-bg-base` + `--fe-text-primary` |
**不动的部分:**
- 所有 `data` / `methods` / `computed` / 生命周期函数
- 模板的事件绑定、v-if/v-show 条件、ref 名称
- WebSocket 协议、API 调用、Pinia store
### Phase 3Shiki 集成(半天)
1. 安装:`yarn add shiki`(不锁版本,跟随 markdown-it 主版本策略)
2. 新增 `src/views/frontend/utils/highlighter.js`
```js
import { createHighlighter } from 'shiki'
import markdownIt from 'markdown-it'
const highlighterPromise = createHighlighter({
themes: ['github-dark'],
langs: ['javascript', 'typescript', 'vue', 'json', 'bash',
'python', 'java', 'go', 'rust', 'sql', 'yaml',
'html', 'css', 'scss', 'markdown', 'tsx', 'jsx']
})
export async function createMarkdownRenderer() {
const highlighter = await highlighterPromise
const md = markdownIt({ html: false, linkify: true, breaks: true })
md.options.highlight = (code, lang) => {
const langKey = highlighter.getLoadedLanguages().includes(lang) ? lang : 'text'
return highlighter.codeToHtml(code, { lang: langKey, theme: 'github-dark' })
}
return md
}
```
3. `ChatWindow.vue`:把当前 `new markdownIt(...)` 替换为 `const md = await createMarkdownRenderer()`;保留 `mermaid` / 链接点击等现有扩展(如果有),并保留对 `code` 标签外层包裹(语言标签)
4. `index.vue` 文件预览面板:`<pre>{{ previewContent }}</pre>` 改为 `<div v-html="renderedCode"></div>``renderedCode` 在 `previewContent` 变化时由同样的 highlighter 渲染;不识别扩展名时降级为纯文本
5. **异步初始化**:在 `ChatWindow` `setup` 顶层用 `await` 不可行(不能阻塞 setup),改用 `onMounted` 中预热 highlighter 缓存到一个共享 `ref`;首屏代码块用 fallback`<pre><code>{{ code }}</code></pre>`)展示 200ms 后由 watcher 替换
### Phase 4:响应式断点(半天)
在 `theme.scss` 中追加:
```scss
.frontend-container {
// 1200px 以下:隐藏右侧文件预览面板
@media (max-width: 1200px) {
.file-preview-wrapper { display: none; }
}
// 900px 以下:默认收起侧边栏
@media (max-width: 900px) {
.conversation-sidebar { width: 28px; }
.conversation-sidebar .sidebar-content { display: none; }
}
// 768px 以下:隐藏右侧文件列表面板(小屏仅保留聊天)
@media (max-width: 768px) {
.file-panel-wrapper { display: none; }
}
}
```
**保留现有 `filePanelVisible` 行为不变** —— 用户主动开关的状态在小屏下依然生效(只是视觉上多了一层媒体查询的隐藏,两者独立工作)。
## 组件契约
### `src/views/frontend/styles/theme.scss`
- 无 export,纯 SCSS
- `:root` 之外的所有变量挂在 `.frontend-container` 选择器下
### `src/views/frontend/utils/highlighter.js`
| 名称 | 类型 | 说明 |
| --- | --- | --- |
| `createMarkdownRenderer()` | `() => Promise<MarkdownIt>` | 异步创建带 Shiki 高亮的 markdown-it 实例 |
| `renderCode(code, lang?)` | `(code: string, lang?: string) => Promise<string>` | 文件预览专用,直接返回 HTML |
### ChatWindow.vue 改动
- 新增 `import { createMarkdownRenderer } from '@/views/frontend/utils/highlighter'`
- `md` 从同步创建改为异步预热 + `ref` 持有
- 模板中 `v-html` 渲染前判断 `mdReady.value`,未就绪时降级 `<pre>`
## 数据流
无新数据流。Shiki 的 highlighter 实例是模块级单例,ChatWindow 与文件预览共用。
## 错误处理
- Shiki 加载失败:`markdown-it` 的 `highlight` 函数降级为 `(c) => \`<pre><code>${escapeHtml(c)}</code></pre>\`,避免阻塞消息渲染
- 不支持的语言:fallback 到 `'text'` 语言(无高亮但保留配色)
- 文件预览读取失败:已有 `ElMessage.error`,不动
## 视觉规范
| 用途 | 值 |
| --- | --- |
| 主背景 | `#1e1e1e` |
| 卡片 / 面板 | `#252526` |
| 输入框 / 浮层 | `#3c3c3c` |
| 主色(accent | `#4ec9b0`(青绿,类似 VS Code Dark+ |
| 危险 | `#f48771` |
| 文本主 | `#e6e6e6` |
| 文本次 | `#a0a0a0` |
| 代码块主题 | `github-dark`(与整体色调一致) |
## 测试
### 手动测试清单
1. **主题覆盖**:访问 `/frontend/index`,确认背景变暗;切到管理后台 `/system/user`,确认**未受污染**
2. **三栏布局**:左 260px / 中 flex / 右 280px(文件列表面板)+ 600px(文件预览)
3. **响应式**:浏览器窗口拉到 1100px → 文件预览消失;拉到 850px → 侧边栏自动收起;拉到 750px → 文件列表面板消失
4. **Shiki 渲染**:发一条带 ```js const x = 1 ``` 的消息,确认代码块背景为 `github-dark` 主题色,语法高亮生效
5. **文件预览**:点击 markdown / json / py 文件,源码用同款暗色主题渲染
6. **原有功能**:新建会话、刷新会话、退出登录、上传头像、修改昵称、二次确认弹窗、WebSocket 收发消息、上下文压缩条、模型切换、token 计数 —— 均**无变化**
### 回归
- `yarn dev` 启动后控制台无 SCSS / Vue warn
- `yarn build:prod` 通过
- 任意管理后台页面(系统管理、监控、工具)打开,目视无暗色污染
## 风险
| 风险 | 缓解 |
| --- | --- |
| Element Plus 组件深色覆盖不完整(部分组件用 hardcoded 色) | Phase 2 组件级逐一覆盖;如 `el-dialog` 头背景、`el-message` 背景用 `!important` + 选择器权重提升 |
| Shiki bundle 体积大 | 仅打包 `github-dark` 主题 + 16 个常用语言;按需扩展 |
| 文件预览大量代码时高亮阻塞 | highlighter 异步;用 `requestIdleCallback``setTimeout(0)` 让出主线程 |
| 响应式断点与既有状态(如 `filePanelVisible`)冲突 | 断点仅控制 `display`,状态变量保留;大屏恢复时面板重新可见 |
## 后续可扩展(不在本次范围)
- 主题切换(亮 / 暗 / 系统跟随)
- Shiki 多主题切换
- 侧边栏拖拽调宽
- 文件预览面板内嵌 Monaco Editor
## 实施顺序
| 阶段 | 任务 | 依赖 |
| --- | --- | --- |
| 1.1 | 创建 `theme.scss` 基础 token + EP 变量覆盖 | 无 |
| 1.2 | `index.vue` 引入 `@import './styles/theme.scss'` | 1.1 |
| 1.3 | 验证 EP 按钮 / dialog / message 颜色变化 | 1.2 |
| 2.x | 逐组件改造样式(不改 JS | 1.3 |
| 3.1 | 安装 shiki + 新增 `utils/highlighter.js` | 无 |
| 3.2 | ChatWindow 集成 markdown-it + Shiki | 3.1 |
| 3.3 | 文件预览面板使用同一 highlighter | 3.2 |
| 4.1 | 三个媒体查询断点 | 1.3 |
| 4.2 | 响应式回归测试 | 4.1 + 全部组件完成 |