Files
agent-frontend-web/docs/superpowers/specs/2026-08-06-frontend-dark-theme-design.md
2026-08-07 13:56:29 +08:00

11 KiB
Raw Permalink Blame History

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.0ChatWindow
代码高亮 暂未配置(默认无高亮或浏览器 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-secondaryloading 状态颜色保留
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
    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>renderedCodepreviewContent 变化时由同样的 highlighter 渲染;不识别扩展名时降级为纯文本
  5. 异步初始化:在 ChatWindow setup 顶层用 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-ithighlight 函数降级为 (c) => \
    ${escapeHtml(c)}
    `,避免阻塞消息渲染
  • 不支持的语言: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 异步;用 requestIdleCallbacksetTimeout(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 + 全部组件完成