11 KiB
11 KiB
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 页面(不含管理后台)改造为:
- 暗色三栏布局:左(会话列表)/ 中(聊天 + 输入)/ 右(文件列表面板 + 预览面板)
- 统一的深色主题 token,作用域隔离,不污染管理后台
- 响应式:屏幕变窄时自动收起右侧文件面板,必要时隐藏侧边栏
- 代码块使用 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 下:
.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 作用域内有效,避免影响其他页面):
.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 3:Shiki 集成(半天)
- 安装:
yarn add shiki(不锁版本,跟随 markdown-it 主版本策略) - 新增
src/views/frontend/utils/highlighter.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 } ChatWindow.vue:把当前new markdownIt(...)替换为const md = await createMarkdownRenderer();保留mermaid/ 链接点击等现有扩展(如果有),并保留对code标签外层包裹(语言标签)index.vue文件预览面板:<pre>{{ previewContent }}</pre>改为<div v-html="renderedCode"></div>,renderedCode在previewContent变化时由同样的 highlighter 渲染;不识别扩展名时降级为纯文本- 异步初始化:在
ChatWindowsetup顶层用await不可行(不能阻塞 setup),改用onMounted中预热 highlighter 缓存到一个共享ref;首屏代码块用 fallback(<pre><code>{{ code }}</code></pre>)展示 200ms 后由 watcher 替换
Phase 4:响应式断点(半天)
在 theme.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) => \
`,避免阻塞消息渲染${escapeHtml(c)} - 不支持的语言:fallback 到
'text'语言(无高亮但保留配色) - 文件预览读取失败:已有
ElMessage.error,不动
视觉规范
| 用途 | 值 |
|---|---|
| 主背景 | #1e1e1e |
| 卡片 / 面板 | #252526 |
| 输入框 / 浮层 | #3c3c3c |
| 主色(accent) | #4ec9b0(青绿,类似 VS Code Dark+) |
| 危险 | #f48771 |
| 文本主 | #e6e6e6 |
| 文本次 | #a0a0a0 |
| 代码块主题 | github-dark(与整体色调一致) |
测试
手动测试清单
- 主题覆盖:访问
/frontend/index,确认背景变暗;切到管理后台/system/user,确认未受污染 - 三栏布局:左 260px / 中 flex / 右 280px(文件列表面板)+ 600px(文件预览)
- 响应式:浏览器窗口拉到 1100px → 文件预览消失;拉到 850px → 侧边栏自动收起;拉到 750px → 文件列表面板消失
- Shiki 渲染:发一条带
js const x = 1的消息,确认代码块背景为github-dark主题色,语法高亮生效 - 文件预览:点击 markdown / json / py 文件,源码用同款暗色主题渲染
- 原有功能:新建会话、刷新会话、退出登录、上传头像、修改昵称、二次确认弹窗、WebSocket 收发消息、上下文压缩条、模型切换、token 计数 —— 均无变化
回归
yarn dev启动后控制台无 SCSS / Vue warnyarn 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 + 全部组件完成 |