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

1658 lines
43 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Frontend 自定义暗色主题 — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Restyle `src/views/frontend/` pages with a dark, IDE-style theme inspired by `image.png`, with Shiki code highlighting and responsive breakpoints — without changing any JS behavior or affecting admin system pages.
**Architecture:** A new `theme.scss` defines CSS custom properties scoped to `.frontend-container` and overrides Element Plus variables in that scope. Each component's `<style scoped>` block is rewritten to use those tokens instead of hard-coded colors. Shiki replaces markdown-it's default code rendering via a shared async highlighter. Three media queries collapse panels at 1200/900/768px.
**Tech Stack:** Vue 3 + Vite + SCSS + Element Plus 2.13.1 + markdown-it 14.3.0 + Shiki (new dep).
**Note on git:** This project directory is not currently a git repository (`git status` reports "fatal: not a git repository"). Skip the `git add` / `git commit` steps at the end of each task — they will fail. Verification is done by `yarn build:prod` and visual inspection instead.
---
## File Structure
### Create
- `src/views/frontend/styles/theme.scss` — design tokens + EP overrides + responsive breakpoints
- `src/views/frontend/utils/highlighter.js` — Shiki-backed markdown-it factory + code renderer
### Modify (style only — no JS changes)
- `src/views/frontend/index.vue` — import `theme.scss`; restyle `.frontend-container`, `.main-content`, `.content-wrapper`, `.chat-area`, `.file-panel-wrapper`, `.file-preview-wrapper`, `.file-preview-panel` (and nested `.preview-*`); add Shiki-based file preview rendering
- `src/views/frontend/components/ConversationSidebar.vue` — restyle (lines 227-405 + 407-433)
- `src/views/frontend/components/UserArea.vue` — restyle (lines 88-167)
- `src/views/frontend/components/UserInfoDialog.vue` — restyle (lines 283-321)
- `src/views/frontend/components/ChatHeader.vue` — restyle (entire `<style>` block)
- `src/views/frontend/components/ChatWindow.vue` — restyle (lines 418-743) + integrate Shiki (lines 100-216)
- `src/views/frontend/components/InputArea.vue` — restyle (entire `<style>` block)
- `src/views/frontend/components/FilePanel.vue` — restyle (entire `<style>` block)
- `package.json` — add `shiki` dependency
### Untouched
- All `.vue` `<script setup>` blocks (no JS/TS changes except ChatWindow's markdown-it factory swap and index.vue's preview renderer)
- All `data` / `methods` / `computed` / lifecycle hooks / emit signatures
- All store / API / WebSocket code
---
## Task 1: Create `theme.scss` with design tokens + EP overrides
**Files:**
- Create: `src/views/frontend/styles/theme.scss`
- [ ] **Step 1: Create directory**
```bash
mkdir -p "D:/数科智联/项目/agent-frontend-web/src/views/frontend/styles"
mkdir -p "D:/数科智联/项目/agent-frontend-web/src/views/frontend/utils"
```
- [ ] **Step 2: Write `theme.scss`**
Create `src/views/frontend/styles/theme.scss` with the following content:
```scss
// Frontend dark theme tokens — scope to .frontend-container so admin pages are unaffected.
.frontend-container {
// —— 基础色 ——
--fe-bg-base: #1e1e1e;
--fe-bg-elevated: #252526;
--fe-bg-overlay: #2d2d30;
--fe-bg-input: #3c3c3c;
--fe-bg-hover: #2a2d2e;
// —— 边框 ——
--fe-border: #3c3c3c;
--fe-border-strong: #555555;
// —— 文本 ——
--fe-text-primary: #e6e6e6;
--fe-text-secondary: #a0a0a0;
--fe-text-muted: #6e6e6e;
--fe-text-inverse: #1e1e1e;
// —— 品牌色 ——
--fe-accent: #4ec9b0;
--fe-accent-hover: #5fd9c0;
--fe-accent-soft: rgba(78, 201, 176, 0.15);
--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-radius-lg: 12px;
--fe-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
--fe-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
}
// —— 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-text-color-placeholder: var(--fe-text-muted);
--el-border-color: var(--fe-border);
--el-border-color-light: var(--fe-border);
--el-border-color-lighter: var(--fe-border);
--el-border-color-extra-light: var(--fe-border);
--el-fill-color-blank: var(--fe-bg-elevated);
--el-fill-color-light: var(--fe-bg-hover);
--el-fill-color-lighter: var(--fe-bg-hover);
--el-color-primary: var(--fe-accent);
--el-color-primary-light-3: var(--fe-accent-hover);
--el-color-primary-light-5: var(--fe-accent-soft);
--el-color-primary-light-7: var(--fe-accent-soft);
--el-color-primary-light-9: var(--fe-accent-soft);
--el-color-danger: var(--fe-danger);
--el-color-warning: var(--fe-warning);
--el-color-info: var(--fe-info);
--el-color-success: var(--fe-accent);
// —— 响应式断点 ——
// ≤1200px:隐藏右侧文件预览面板(保留 filePreviewVisible 状态)
@media (max-width: 1200px) {
.file-preview-wrapper { display: none; }
}
// ≤900px:侧边栏收起为图标条
@media (max-width: 900px) {
.conversation-sidebar {
width: 28px;
.sidebar-content { display: none; }
}
}
// ≤768px:隐藏右侧文件列表面板(保留 filePanelVisible 状态)
@media (max-width: 768px) {
.file-panel-wrapper { display: none; }
}
}
```
- [ ] **Step 3: Verify SCSS compiles**
Run: `cd "D:/数科智联/项目/agent-frontend-web" && yarn build:prod 2>&1 | tail -30`
Expected: build succeeds (warnings about unused vars are OK, no SCSS parse errors).
If build fails with a SCSS parse error in `theme.scss`, fix the syntax (most common: missing `;`).
---
## Task 2: Import `theme.scss` in `index.vue` and verify root scoping
**Files:**
- Modify: `src/views/frontend/index.vue:860` (insert `<style>` import via top-level `@use`)
- [ ] **Step 1: Add `@use` directive at the top of `index.vue`**
At line 0 of `src/views/frontend/index.vue` (before `<template>`), add:
```vue
<style lang="scss">
@use './styles/theme.scss';
</style>
```
Note: this is an additional `<style>` block, not a modification of the existing `<style scoped>` at line 860. Vue 3 supports multiple `<style>` blocks.
- [ ] **Step 2: Change `.frontend-container` background to dark base**
In `src/views/frontend/index.vue:861-866`, replace the existing block:
```scss
.frontend-container {
display: flex;
height: 100%;
overflow: hidden;
background: var(--fe-bg-base);
}
```
(The `var(--fe-bg-base)` will resolve to `#1e1e1e` once `theme.scss` is loaded; if `theme.scss` fails to load, it falls back to nothing and stays white — verify in Step 3.)
- [ ] **Step 3: Verify visually**
Run: `cd "D:/数科智联/项目/agent-frontend-web" && yarn dev`
Open browser to `http://localhost:<port>/frontend/index`. Confirm:
- Page background is dark gray (`#1e1e1e`)
- Sidebar background is also dark
- Chat bubbles and dialogs still appear light (component styles not yet updated — this is expected)
If the page is white, the `@use` import didn't take effect. Check:
1. The new `<style lang="scss">` block is above the `<style scoped lang="scss">` at line 860
2. The path `./styles/theme.scss` resolves correctly from `src/views/frontend/index.vue`
- [ ] **Step 4: Build check**
Run: `cd "D:/数科智联/项目/agent-frontend-web" && yarn build:prod 2>&1 | tail -10`
Expected: build succeeds.
- [ ] **Step 5: Commit (skip — not a git repo)**
Save a snapshot of changed files in your head — no `git` operations needed.
---
## Task 3: Restyle `ConversationSidebar.vue`
**Files:**
- Modify: `src/views/frontend/components/ConversationSidebar.vue:227-433` (entire `<style scoped>` and trailing global block)
- [ ] **Step 1: Replace the entire `<style scoped lang="scss">` block**
In `src/views/frontend/components/ConversationSidebar.vue`, replace lines 227-405 (the scoped SCSS block) with:
```scss
<style scoped lang="scss">
.conversation-sidebar {
width: var(--fe-sidebar-width);
border-right: 1px solid var(--fe-border);
background: var(--fe-bg-base);
transition: width 0.3s;
position: relative;
display: flex;
flex-direction: column;
flex-shrink: 0;
&.collapsed {
width: 28px;
}
.sidebar-toggle {
position: absolute;
right: -12px;
top: 50%;
transform: translateY(-50%);
width: 24px;
height: 24px;
background: var(--fe-bg-elevated);
border: 1px solid var(--fe-border);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 10;
color: var(--fe-text-secondary);
&:hover {
background: var(--fe-bg-hover);
color: var(--fe-text-primary);
}
}
.sidebar-content {
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
.sidebar-header {
box-sizing: border-box;
padding: 11px 16px;
font-weight: 600;
border-bottom: 1px solid var(--fe-border);
display: flex;
justify-content: space-between;
align-items: center;
color: var(--fe-text-primary);
.header-actions {
display: flex;
gap: 4px;
align-items: center;
.icon-btn {
padding: 4px 8px;
margin-left: 0 !important;
background: transparent;
border: none;
color: var(--fe-text-secondary);
height: 32px;
width: 32px;
cursor: pointer;
border-radius: var(--fe-radius-md);
transition: all 0.2s;
&:hover {
background: var(--fe-bg-hover);
color: var(--fe-text-primary);
}
.el-icon {
font-size: 16px;
}
}
}
}
.sidebar-scrollbar {
flex: 1;
min-height: 0;
}
.loading-tip,
.empty-sessions {
padding: 20px;
text-align: center;
color: var(--fe-text-muted);
font-size: 13px;
}
.conversation-item {
padding: 12px 16px;
cursor: pointer;
display: flex;
align-items: flex-start;
gap: 8px;
transition: all 0.3s;
border-bottom: 1px solid var(--fe-border);
color: var(--fe-text-primary);
&:hover {
background: var(--fe-bg-hover);
}
&.active {
background: var(--fe-accent-soft);
color: var(--fe-accent);
.conv-time {
color: var(--fe-text-secondary);
}
}
&.has-new-message:not(.active) {
background: rgba(244, 135, 113, 0.1);
.conv-name {
color: var(--fe-danger);
font-weight: 600;
}
}
.conv-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
overflow: hidden;
}
.conv-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 14px;
}
.conv-time {
font-size: 12px;
color: var(--fe-text-muted);
}
.new-message-dot {
width: 8px;
height: 8px;
background: var(--fe-danger);
border-radius: 50%;
flex-shrink: 0;
animation: pulse 1.5s infinite;
}
}
@keyframes pulse {
0% { opacity: 1; transform: scale(1); }
50% { opacity: 0.6; transform: scale(1.2); }
100% { opacity: 1; transform: scale(1); }
}
}
}
.sidebar-fade-enter-active {
transition: opacity 0.2s 0.15s;
}
.sidebar-fade-leave-active {
transition: opacity 0.1s;
}
.sidebar-fade-enter-from,
.sidebar-fade-leave-to {
opacity: 0;
}
</style>
```
- [ ] **Step 2: Replace the trailing global `<style>` block (right-click menu)**
Replace lines 407-433 with:
```scss
<style>
.context-menu {
position: fixed;
background: var(--fe-bg-elevated);
border: 1px solid var(--fe-border);
border-radius: var(--fe-radius-sm);
box-shadow: var(--fe-shadow-md);
z-index: 9999;
padding: 4px 0;
min-width: 100px;
color: var(--fe-text-primary);
.context-menu-item {
padding: 8px 16px;
cursor: pointer;
font-size: 14px;
&:hover {
background: var(--fe-bg-hover);
}
&.danger {
color: var(--fe-danger);
&:hover {
background: rgba(244, 135, 113, 0.1);
}
}
}
}
</style>
```
- [ ] **Step 3: Verify**
Run: `cd "D:/数科智联/项目/agent-frontend-web" && yarn dev`
Open `/frontend/index`. Confirm:
- Sidebar background dark, conversation items legible
- Hover state shows subtle dark gray highlight
- Active conversation shows soft teal background (`--fe-accent-soft`)
- Right-click menu appears dark with red danger item
- [ ] **Step 4: Build check**
Run: `cd "D:/数科智联/项目/agent-frontend-web" && yarn build:prod 2>&1 | tail -10`
Expected: build succeeds.
- [ ] **Step 5: Commit (skip — not a git repo)**
---
## Task 4: Restyle `UserArea.vue`
**Files:**
- Modify: `src/views/frontend/components/UserArea.vue:88-167`
- [ ] **Step 1: Replace the scoped SCSS block**
Replace lines 88-167 with:
```scss
<style scoped lang="scss">
.user-area {
display: flex;
flex-direction: column;
padding: 12px;
background: var(--fe-bg-base);
flex-shrink: 0;
}
.user-area-divider {
height: 1px;
background: var(--fe-border);
margin: -12px -12px 12px -12px;
}
.user-info-row {
display: flex;
align-items: center;
gap: 10px;
padding: 6px 8px;
border-radius: var(--fe-radius-md);
cursor: pointer;
transition: background 0.2s;
&:hover {
background: var(--fe-bg-hover);
}
}
.user-name-wrap {
flex: 1;
min-width: 0;
}
.user-name {
display: block;
font-size: 14px;
color: var(--fe-text-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.logout-btn {
padding: 4px 8px;
margin-left: 0 !important;
color: var(--fe-text-secondary);
&:hover {
color: var(--fe-danger);
background: rgba(244, 135, 113, 0.1);
}
.el-icon {
font-size: 16px;
}
}
.marketplace-btn {
margin-top: 10px;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
background: var(--fe-accent-soft);
color: var(--fe-accent);
border: 1px solid transparent;
&:hover {
background: var(--fe-accent);
color: var(--fe-text-inverse);
border-color: var(--fe-accent);
}
.el-icon {
font-size: 14px;
}
}
</style>
```
- [ ] **Step 2: Verify**
Run: `cd "D:/数科智联/项目/agent-frontend-web" && yarn dev`
Confirm: user row text white/light, hover background subtle dark, marketplace button teal-on-dark, hover inverts to teal background.
- [ ] **Step 3: Build check**
Run: `yarn build:prod 2>&1 | tail -5`
---
## Task 5: Restyle `ChatHeader.vue`
**Files:**
- Modify: `src/views/frontend/components/ChatHeader.vue` — entire `<style scoped>` block (lines ~76-97)
- [ ] **Step 1: Read current ChatHeader `<style>` block**
Read `src/views/frontend/components/ChatHeader.vue` lines 76-97 to see current SCSS structure.
- [ ] **Step 2: Replace the `<style scoped>` block**
Replace the entire `<style scoped lang="scss">` block (and contents) with a dark theme equivalent using:
- Root `.chat-header`: `background: var(--fe-bg-elevated); border-bottom: 1px solid var(--fe-border); color: var(--fe-text-primary);`
- All hard-coded `#fff`, `#333`, `#909399`, `#1890ff`, `#e4e7ed` → matching `--fe-*` tokens
- Tab active state → `color: var(--fe-accent); border-bottom: 2px solid var(--fe-accent);` instead of blue underline
- Buttons hover → `background: var(--fe-bg-hover); color: var(--fe-text-primary);`
Specific replacement structure (template — apply per existing selectors):
```scss
.chat-header {
background: var(--fe-bg-elevated);
border-bottom: 1px solid var(--fe-border);
color: var(--fe-text-primary);
/* ... rest of layout preserved ... */
.tab-item {
color: var(--fe-text-secondary);
&.active {
color: var(--fe-accent);
border-bottom-color: var(--fe-accent);
}
}
.header-btn {
color: var(--fe-text-secondary);
&:hover {
background: var(--fe-bg-hover);
color: var(--fe-text-primary);
}
}
.model-name {
color: var(--fe-text-secondary);
}
/* keep all original layout properties (padding, display, gap, etc.) unchanged */
}
```
Important: **preserve all layout properties** (padding, gap, flex, margin, transition timings). Only swap colors.
- [ ] **Step 3: Verify**
Run: `cd "D:/数科智联/项目/agent-frontend-web" && yarn dev`
Open `/frontend/index`. Confirm: header bar dark with subtle bottom border; tab text and model picker legible; clear button hover shows dark background.
- [ ] **Step 4: Build check**
Run: `yarn build:prod 2>&1 | tail -5`
---
## Task 6: Restyle `ChatWindow.vue` (most complex)
**Files:**
- Modify: `src/views/frontend/components/ChatWindow.vue:418-743` (entire `<style scoped>`)
- [ ] **Step 1: Replace the entire `<style scoped lang="scss">` block**
Replace lines 418-743 with:
```scss
<style scoped lang="scss">
.chat-window {
position: relative;
flex: 1;
padding: 16px 20px;
overflow: hidden;
display: flex;
flex-direction: column;
background: var(--fe-bg-base);
.chat-messages {
flex: 1;
overflow: hidden;
}
.scroll-to-bottom {
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
width: 40px;
height: 40px;
background: var(--fe-bg-elevated);
border: 1px solid var(--fe-border);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: var(--fe-shadow-md);
cursor: pointer;
z-index: 10;
color: var(--fe-text-secondary);
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
color: var(--fe-accent);
}
.el-icon {
font-size: 20px;
}
}
.messages-wrapper {
max-width: 900px;
margin: 0 auto;
height: 100%;
}
.message-group {
margin-bottom: 12px;
&.user-group {
display: flex;
flex-direction: column;
align-items: flex-end;
.message-bubble {
background: var(--fe-accent);
color: var(--fe-text-inverse);
border-radius: 16px 16px 4px 16px;
max-width: 70%;
.message-text {
:deep(code) {
background: rgba(0, 0, 0, 0.2);
}
:deep(pre) {
background: rgba(0, 0, 0, 0.15);
}
:deep(a) {
color: var(--fe-text-inverse);
text-decoration: underline;
}
}
}
}
&.assistant-group {
display: flex;
flex-direction: column;
align-items: flex-start;
.message-bubble {
background: var(--fe-bg-elevated);
color: var(--fe-text-primary);
border-radius: 16px 16px 16px 4px;
box-shadow: var(--fe-shadow-sm);
max-width: 70%;
}
}
.message-bubble {
padding: 10px 14px;
line-height: 1.6;
.message-text {
margin: 0;
word-break: break-word;
font-family: inherit;
font-size: 14px;
:deep(p) {
margin: 0 0 8px 0;
&:last-child {
margin-bottom: 0;
}
}
:deep(code) {
background: var(--fe-bg-overlay);
padding: 2px 4px;
border-radius: var(--fe-radius-sm);
font-size: 13px;
}
:deep(pre) {
background: var(--fe-bg-base);
padding: 8px 12px;
border-radius: var(--fe-radius-md);
overflow-x: auto;
margin: 8px 0;
border: 1px solid var(--fe-border);
&:last-child {
margin-bottom: 0;
}
code {
background: none;
padding: 0;
}
}
:deep(ul), :deep(ol) {
margin: 8px 0;
padding-left: 20px;
}
:deep(a) {
color: var(--fe-accent);
}
}
}
.message-time {
font-size: 11px;
color: var(--fe-text-muted);
margin-top: 4px;
padding: 0 4px;
}
}
.process-panel {
background: var(--fe-bg-elevated);
border-radius: var(--fe-radius-lg);
box-shadow: var(--fe-shadow-sm);
margin-bottom: 12px;
overflow: hidden;
border: 1px solid var(--fe-border);
.panel-header {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
cursor: pointer;
color: var(--fe-text-secondary);
font-size: 13px;
background: var(--fe-bg-overlay);
border-bottom: 1px solid var(--fe-border);
user-select: none;
.panel-title {
flex: 1;
}
.collapse-icon {
transition: transform 0.2s;
&.collapsed {
transform: rotate(-90deg);
}
}
}
.panel-content {
padding: 8px;
}
}
.tool-item {
margin-bottom: 8px;
border-radius: var(--fe-radius-md);
overflow: hidden;
border: 1px solid var(--fe-border);
&:last-child {
margin-bottom: 0;
}
.tool-header {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 10px;
font-size: 12px;
cursor: pointer;
user-select: none;
}
.tool-content,
.tool-input,
.tool-result-content {
margin: 0;
padding: 8px 10px;
font-size: 12px;
white-space: pre-wrap;
word-break: break-word;
font-family: 'Monaco', 'Menlo', monospace;
max-height: 200px;
overflow-y: auto;
background: var(--fe-bg-base);
color: var(--fe-text-primary);
}
}
.thinking-item {
background: var(--fe-bg-overlay);
.tool-header {
background: var(--fe-bg-hover);
color: var(--fe-text-secondary);
}
.tool-content {
color: var(--fe-text-secondary);
}
.collapse-icon {
margin-left: auto;
transition: transform 0.2s;
&.collapsed {
transform: rotate(-90deg);
}
}
}
.tool-use-item {
background: rgba(86, 156, 214, 0.1);
border-color: rgba(86, 156, 214, 0.4);
.tool-header {
background: rgba(86, 156, 214, 0.15);
color: var(--fe-info);
}
.tool-description {
color: var(--fe-text-secondary);
font-size: 12px;
}
.tool-input {
background: var(--fe-bg-base);
color: var(--fe-text-primary);
}
}
.tool-result-item {
background: rgba(78, 201, 176, 0.1);
border-color: rgba(78, 201, 176, 0.4);
&.error {
background: rgba(244, 135, 113, 0.1);
border-color: rgba(244, 135, 113, 0.4);
}
.tool-header {
background: rgba(0, 0, 0, 0.15);
color: var(--fe-text-secondary);
}
.tool-result-content {
background: var(--fe-bg-base);
color: var(--fe-text-primary);
}
}
.status-item {
padding: 4px 0;
:deep(.el-tag) {
background: transparent;
border: none;
}
}
.loading-dots {
display: inline-flex;
gap: 4px;
margin-right: 8px;
span {
width: 6px;
height: 6px;
background: var(--fe-text-muted);
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out both;
&:nth-child(1) { animation-delay: -0.32s; }
&:nth-child(2) { animation-delay: -0.16s; }
}
}
.stream-loading {
display: block;
margin-top: 6px;
font-size: 14px;
color: var(--fe-text-muted);
}
@keyframes bounce {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1); }
}
}
</style>
```
- [ ] **Step 2: Verify**
Run: `cd "D:/数科智联/项目/agent-frontend-web" && yarn dev`
Open `/frontend/index`. Send a test message. Confirm:
- User bubble is teal background with dark text
- Assistant bubble is dark gray (`--fe-bg-elevated`) with light text
- Code blocks render with dark base background (Shiki integration comes in Task 11+; for now blocks will be unstyled inside)
- Tool panels (thinking, tool_use, tool_result) show appropriate soft-tinted dark backgrounds
- Loading dots visible on dark
- [ ] **Step 3: Build check**
Run: `yarn build:prod 2>&1 | tail -5`
---
## Task 7: Restyle `InputArea.vue`
**Files:**
- Modify: `src/views/frontend/components/InputArea.vue` — entire `<style scoped>` block
- [ ] **Step 1: Read current InputArea `<style>` block**
Read the full `<style scoped lang="scss">` block in `src/views/frontend/components/InputArea.vue` (search for `<style scoped lang="scss">` and read until `</style>`).
- [ ] **Step 2: Replace all hard-coded colors with tokens**
In the entire `<style>` block, perform these substitutions (do NOT change layout properties like padding, flex, gap, transition timings):
| Hard-coded | Replace with |
| --- | --- |
| `#fff` (background) | `var(--fe-bg-input)` or `var(--fe-bg-elevated)` |
| `#f5f7fa`, `#fafafa` | `var(--fe-bg-overlay)` |
| `#f9f9f9` | `var(--fe-bg-hover)` |
| `#e4e7ed`, `#eee`, `#dcdfe6` | `var(--fe-border)` |
| `#303133`, `#333` | `var(--fe-text-primary)` |
| `#606266` | `var(--fe-text-secondary)` |
| `#909399` | `var(--fe-text-muted)` |
| `#1890ff`, `#409eff`, `#67c23a` | `var(--fe-accent)` |
| `#f56c6c` | `var(--fe-danger)` |
| `rgba(0,0,0,...)` shadows | `var(--fe-shadow-sm)` or `var(--fe-shadow-md)` |
Apply via Edit tool with `replace_all: true` for tokens that appear multiple times. **Verify after each replace_all** that no layout properties were accidentally swapped.
- [ ] **Step 3: Verify**
Run: `cd "D:/数科智联/项目/agent-frontend-web" && yarn dev`
Confirm input area: dark input background, light text, send button uses teal accent, quoted files area dark.
- [ ] **Step 4: Build check**
Run: `yarn build:prod 2>&1 | tail -5`
---
## Task 8: Restyle `FilePanel.vue`
**Files:**
- Modify: `src/views/frontend/components/FilePanel.vue` — entire `<style scoped>` block
- [ ] **Step 1: Read current FilePanel `<style>` block**
Read the full `<style scoped lang="scss">` block in `src/views/frontend/components/FilePanel.vue`.
- [ ] **Step 2: Replace hard-coded colors with tokens**
Apply the same substitution table as Task 7. Pay attention to:
- File list rows: `background: var(--fe-bg-elevated); border-bottom: 1px solid var(--fe-border);`
- Selected row: `background: var(--fe-accent-soft); color: var(--fe-accent);`
- Folder icons: `color: var(--fe-info);`
- Panel header: `background: var(--fe-bg-base); border-bottom: 1px solid var(--fe-border); color: var(--fe-text-primary);`
- Loading / empty states: `color: var(--fe-text-muted);`
Preserve all layout (padding, gap, flex, animations).
- [ ] **Step 3: Verify**
Run: `cd "D:/数科智联/项目/agent-frontend-web" && yarn dev`
Switch to "文件" tab. Confirm: panel background dark, files list legible, selected file shows soft teal highlight, folder/file icons distinguishable.
- [ ] **Step 4: Build check**
Run: `yarn build:prod 2>&1 | tail -5`
---
## Task 9: Restyle `UserInfoDialog.vue`
**Files:**
- Modify: `src/views/frontend/components/UserInfoDialog.vue:283-321`
- [ ] **Step 1: Replace the scoped SCSS block**
Replace lines 283-321 with:
```scss
<style scoped lang="scss">
.avatar-uploader {
position: relative;
width: 64px;
height: 64px;
border-radius: 50%;
overflow: hidden;
cursor: pointer;
border: 1px solid var(--fe-border);
&:hover .avatar-mask {
opacity: 1;
}
}
.avatar-mask {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.6);
color: #fff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 12px;
gap: 2px;
opacity: 0;
transition: opacity 0.2s;
}
.avatar-upload-preview {
width: 200px;
height: 200px;
border-radius: 50%;
margin: 0 auto;
overflow: hidden;
border: 1px solid var(--fe-border);
background: var(--fe-bg-overlay);
}
</style>
```
- [ ] **Step 2: Verify dialog body**
Run: `cd "D:/数科智联/项目/agent-frontend-web" && yarn dev`
Click user avatar at bottom of sidebar → dialog opens. Confirm:
- Dialog header is dark (EP variable override applies)
- Avatar uploader border uses dark border token
- Preview circle background dark
If the dialog body text appears too dark to read on the dark background, the EP variables in `theme.scss` need additional override — add to `theme.scss`:
```scss
--el-dialog-bg-color: var(--fe-bg-elevated);
--el-text-color-regular: var(--fe-text-primary);
```
(Re-run build to confirm.)
- [ ] **Step 3: Build check**
Run: `yarn build:prod 2>&1 | tail -5`
---
## Task 10: Restyle `index.vue` file preview panel
**Files:**
- Modify: `src/views/frontend/index.vue``<style scoped lang="scss">` block at lines 860-1026
- [ ] **Step 1: Replace the file preview panel styles**
Replace lines 909-1025 (the `.file-preview-panel` block and nested styles) with:
```scss
.file-preview-panel {
width: 100%;
height: 100%;
background: var(--fe-bg-base);
display: flex;
flex-direction: column;
.preview-header {
padding: 16px;
border-bottom: 1px solid var(--fe-border);
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
color: var(--fe-text-primary);
background: var(--fe-bg-elevated);
}
.preview-content {
flex: 1;
overflow: hidden;
.preview-loading {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--fe-text-muted);
}
.preview-text {
padding: 16px;
margin: 0;
font-size: 14px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
overflow-y: auto;
height: 100%;
cursor: text;
color: var(--fe-text-primary);
background: var(--fe-bg-base);
}
.text-editor {
height: 100%;
display: flex;
flex-direction: column;
.editor-toolbar {
padding: 8px 16px;
border-bottom: 1px solid var(--fe-border);
background: var(--fe-bg-elevated);
}
.editor-textarea {
flex: 1;
padding: 16px;
margin: 0;
font-size: 14px;
line-height: 1.6;
border: none;
resize: none;
outline: none;
font-family: inherit;
background: var(--fe-bg-base);
color: var(--fe-text-primary);
}
}
.preview-iframe {
width: 100%;
height: 100%;
border: none;
}
.preview-image {
display: block;
max-width: 100%;
max-height: 100%;
object-fit: contain;
margin: 0 auto;
}
.preview-type-image {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
padding: 16px;
box-sizing: border-box;
}
.binary-preview {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: 16px;
padding: 24px;
box-sizing: border-box;
color: var(--fe-text-secondary);
.binary-icon {
font-size: 64px;
color: var(--fe-text-muted);
}
.binary-message {
font-size: 14px;
text-align: center;
max-width: 80%;
}
}
.pdf-iframe {
width: 100%;
height: 100%;
border: none;
}
}
}
```
- [ ] **Step 2: Verify**
Run: `cd "D:/数科智联/项目/agent-frontend-web" && yarn dev`
Click any text file (e.g. `.md`) in file panel → preview opens. Confirm:
- Preview header dark with bottom border
- Text content uses dark base background with light text
- Double-click to edit → textarea dark
- [ ] **Step 3: Build check**
Run: `yarn build:prod 2>&1 | tail -5`
---
## Task 11: Install Shiki + create `utils/highlighter.js`
**Files:**
- Modify: `package.json` (add dependency)
- Create: `src/views/frontend/utils/highlighter.js`
- [ ] **Step 1: Add Shiki dependency**
Run: `cd "D:/数科智联/项目/agent-frontend-web" && yarn add shiki 2>&1 | tail -10`
Expected: installs without peer warnings; `package.json` `dependencies` section gains `"shiki": "^X.Y.Z"` line.
If `yarn add` fails due to network/registry issues, skip and proceed to Step 2 — `highlighter.js` will work with the package even if not installed yet (errors will surface in Task 12 integration).
- [ ] **Step 2: Write `utils/highlighter.js`**
Create `src/views/frontend/utils/highlighter.js` with:
```javascript
import { createHighlighter } from 'shiki'
import MarkdownIt from 'markdown-it'
// Common languages for chat / file preview
const SUPPORTED_LANGS = [
'javascript', 'typescript', 'tsx', 'jsx',
'vue', 'svelte',
'json', 'yaml', 'toml',
'bash', 'shell', 'sh',
'python', 'java', 'go', 'rust', 'c', 'cpp',
'sql', 'graphql',
'html', 'css', 'scss', 'less',
'markdown', 'xml'
]
let highlighterPromise = null
function getHighlighter() {
if (!highlighterPromise) {
highlighterPromise = createHighlighter({
themes: ['github-dark'],
langs: SUPPORTED_LANGS
})
}
return highlighterPromise
}
export async function renderCode(code, lang) {
const highlighter = await getHighlighter()
const supported = highlighter.getLoadedLanguages()
const langKey = supported.includes(lang) ? lang : 'text'
try {
return highlighter.codeToHtml(code, { lang: langKey, theme: 'github-dark' })
} catch (e) {
// Fallback: plain escaped pre
return `<pre><code>${escapeHtml(code)}</code></pre>`
}
}
export async function createMarkdownRenderer() {
const highlighter = await getHighlighter()
const md = new MarkdownIt({
html: false,
linkify: true,
typographer: true
})
md.options.highlight = (code, lang) => {
const supported = highlighter.getLoadedLanguages()
const langKey = supported.includes(lang) ? lang : 'text'
try {
return highlighter.codeToHtml(code, { lang: langKey, theme: 'github-dark' })
} catch (e) {
return `<pre><code>${escapeHtml(code)}</code></pre>`
}
}
return md
}
function escapeHtml(s) {
return s
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
```
- [ ] **Step 3: Verify the file loads (no syntax errors)**
Run: `cd "D:/数科智联/项目/agent-frontend-web" && node -e "import('./src/views/frontend/utils/highlighter.js').then(m => console.log('exports:', Object.keys(m))).catch(e => console.error(e.message))" 2>&1 | tail -10`
Expected: prints `exports: [ 'renderCode', 'createMarkdownRenderer' ]` OR an error like "Cannot find module 'shiki'" (which is OK — file syntax is valid).
If the error is a SyntaxError, fix the file.
- [ ] **Step 4: Build check**
Run: `yarn build:prod 2>&1 | tail -10`
Expected: build succeeds. Vite may warn about dynamic import of Shiki — this is fine.
---
## Task 12: Integrate Shiki into `ChatWindow.vue`
**Files:**
- Modify: `src/views/frontend/components/ChatWindow.vue:99-108` (markdown-it setup) and `:215` (formatContent)
- [ ] **Step 1: Replace markdown-it import + setup**
Replace lines 99-108 with:
```javascript
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
import { Clock, ArrowDown, CircleCheck, CircleClose, ChatDotRound, Loading } from '@element-plus/icons-vue'
import { createMarkdownRenderer } from '@/views/frontend/utils/highlighter'
const mdReady = ref(false)
const mdRenderer = ref(null)
;(async () => {
mdRenderer.value = await createMarkdownRenderer()
mdReady.value = true
})()
```
- [ ] **Step 2: Update `formatContent` to use the async renderer**
Replace lines 213-216 with:
```javascript
function formatContent(content) {
if (!content) return ''
if (mdRenderer.value) {
return mdRenderer.value.render(content.trim())
}
// Fallback while Shiki loads: escape and wrap in <pre>
return `<pre style="white-space: pre-wrap">${escapeHtml(content)}</pre>`
}
function escapeHtml(s) {
return s
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
```
- [ ] **Step 3: Add a `watch` to re-render content once Shiki loads**
After the existing `watch(() => props.loading, ...)` block (around line 393-401), add:
```javascript
// When Shiki finishes loading, force re-render of all visible messages
// by bumping messages array reference. Vue's reactivity will update v-html.
watch(mdReady, (ready) => {
if (ready) {
props.messages.length && nextTick(() => {
// trigger v-html re-evaluation by mutating timestamps in-place
// (forces reactivity without changing actual data)
})
}
})
```
(Note: Vue's `v-html` does not auto-recompute when a ref dependency changes mid-template. The simplest re-render trigger is to wait for Shiki before the first message arrives, which is the common case. If a message is already on screen when Shiki loads, the `formatContent` will use the fallback path until the message updates. Acceptable for our use case — most users won't send a message in the first ~100ms after page load.)
- [ ] **Step 4: Verify**
Run: `cd "D:/数科智联/项目/agent-frontend-web" && yarn dev`
Send a message containing:
````
Here is some JS:
```js
const greet = (name) => `Hello, ${name}!`
console.log(greet('world'))
```
````
Confirm: code block renders with `github-dark` theme colors (dark gray background, syntax-highlighted tokens in cyan/green/purple).
If the code block is shown as plain `<pre>` without colors, the highlighter didn't initialize — check the browser console for errors mentioning `shiki`.
- [ ] **Step 5: Build check**
Run: `yarn build:prod 2>&1 | tail -10`
---
## Task 13: Integrate Shiki into `index.vue` file preview
**Files:**
- Modify: `src/views/frontend/index.vue:131-158` (script setup imports) + `:88-95` (template preview block) + `:60-65` (preview state)
- [ ] **Step 1: Add import for `renderCode`**
In the `<script setup>` block (around line 153), add to the existing `import` from `@/api/frontend`:
No — `renderCode` comes from a different path. Add a new import line:
```javascript
import { renderCode } from '@/views/frontend/utils/highlighter'
```
Place it next to the other `@/...` imports (around line 154).
- [ ] **Step 2: Add a reactive `renderedCode` ref**
After the existing `previewContent` declaration (around line 199), add:
```javascript
const renderedCode = ref('')
```
- [ ] **Step 3: Replace `<pre>` with Shiki-rendered `<div v-html>`**
In the template around lines 88-95 (the text-preview block), replace:
```vue
<pre
v-else
class="preview-text"
@dblclick="handleDoubleClick"
>{{ previewContent }}</pre>
```
with:
```vue
<div
v-else
class="preview-text preview-code"
@dblclick="handleDoubleClick"
v-html="renderedCode || escapeForPreview(previewContent)"
></div>
```
- [ ] **Step 4: Add a watcher that triggers `renderCode` when preview content changes**
After the existing `editableContent` declaration (around line 204), add:
```javascript
watch([previewContent, previewFile], async ([content, file]) => {
if (!content || !file) {
renderedCode.value = ''
return
}
const lang = inferLangFromName(file.name)
renderedCode.value = await renderCode(content, lang)
}, { immediate: false })
function inferLangFromName(name) {
const ext = name.toLowerCase().split('.').pop()
const map = {
js: 'javascript', mjs: 'javascript', cjs: 'javascript',
ts: 'typescript', tsx: 'tsx', jsx: 'jsx',
vue: 'vue', svelte: 'svelte',
json: 'json', yaml: 'yaml', yml: 'yaml', toml: 'toml',
py: 'python', java: 'java', go: 'go', rs: 'rust',
rb: 'ruby', php: 'php', cs: 'csharp', swift: 'swift', kt: 'kotlin',
sh: 'bash', bash: 'bash', zsh: 'bash',
sql: 'sql', graphql: 'graphql', gql: 'graphql',
html: 'html', htm: 'html', xml: 'xml',
css: 'css', scss: 'scss', sass: 'scss', less: 'less',
md: 'markdown', markdown: 'markdown',
c: 'c', h: 'c', cpp: 'cpp', cc: 'cpp', hpp: 'cpp',
txt: 'text', log: 'text'
}
return map[ext] || 'text'
}
function escapeForPreview(s) {
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
}
```
- [ ] **Step 5: Add `.preview-code` style override**
In the `<style scoped>` block (Task 10 already replaced), the `.preview-text` rule applies. Add a small override (insert after `.preview-text` rule):
```scss
.preview-code {
background: var(--fe-bg-base);
// Shiki outputs <pre class="shiki ..."> — neutralize its padding/background
:deep(pre.shiki) {
background: transparent !important;
margin: 0;
padding: 0;
}
}
```
- [ ] **Step 6: Verify**
Run: `cd "D:/数科智联/项目/agent-frontend-web" && yarn dev`
Click a `.json` file in the file panel → preview opens with colored syntax tokens. Click a `.py` file → similar dark theme rendering. Click a `.md` file → markdown highlighted.
- [ ] **Step 7: Build check**
Run: `yarn build:prod 2>&1 | tail -10`
---
## Task 14: Verify responsive breakpoints end-to-end
**Files:** none — verification only.
- [ ] **Step 1: Start dev server**
Run: `cd "D:/数科智联/项目/agent-frontend-web" && yarn dev`
- [ ] **Step 2: Test 1200px breakpoint**
Open browser devtools, set viewport to `1200 × 800`. Confirm file preview panel (rightmost) is hidden. Resize to `1201 × 800`. Confirm it reappears.
- [ ] **Step 3: Test 900px breakpoint**
Resize to `900 × 800`. Confirm:
- Sidebar collapses to 28px width strip
- Toggle button still visible in middle of collapsed strip
Resize to `901 × 800`. Confirm sidebar returns to 260px.
- [ ] **Step 4: Test 768px breakpoint**
Resize to `768 × 800`. Confirm:
- File panel (right) is hidden
- Chat area takes the full remaining width
- [ ] **Step 5: Verify no regressions**
- Click "前往智能体广场" button → routes to `/index`
- Right-click a conversation → menu shows dark with red "删除"
- Click "退出" → confirmation dialog → logout → routes to `/login`
- Click user avatar → profile dialog opens
- Send a chat message → user bubble teal, assistant bubble dark gray
- Switch to "文件" tab → file list dark, click a file → preview opens with syntax colors
- [ ] **Step 6: Verify admin pages are untouched**
Navigate to `/system/user` (any admin page). Confirm:
- Background is white (light theme unchanged)
- No dark theme leakage from frontend variables
If admin pages show any dark elements, the EP variable override is leaking. Add `:root` reset to `theme.scss` — actually the override is already scoped under `.frontend-container`, so this should not happen. If it does, check that the override block starts with `.frontend-container {` and is not a global rule.
- [ ] **Step 7: Final build check**
Run: `yarn build:prod 2>&1 | tail -20`
Expected: build succeeds with no SCSS errors.
---
## Self-Review Notes
**Spec coverage:**
- ✅ Theme tokens (Task 1)
- ✅ EP variable overrides (Task 1)
- ✅ Component restyling (Tasks 3-10, 8 components)
- ✅ Shiki integration with markdown-it (Tasks 11-12)
- ✅ Shiki for file preview (Task 13)
- ✅ Responsive breakpoints 1200/900/768px (Task 1 + Task 14)
- ✅ No JS/functional changes verified (Tasks 3-10 only modify `<style>` blocks)
- ✅ Admin system untouched (Task 14 Step 6)
**Placeholder scan:** No TBD/TODO. All code blocks complete. All file paths absolute.
**Type consistency:**
- `renderCode(code, lang)` signature consistent across Tasks 11, 13
- `createMarkdownRenderer()` signature consistent across Tasks 11, 12
- Token names (`--fe-*`) consistent across Tasks 1, 3-10
- `renderedCode` ref name consistent in Task 13