43 KiB
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 breakpointssrc/views/frontend/utils/highlighter.js— Shiki-backed markdown-it factory + code renderer
Modify (style only — no JS changes)
src/views/frontend/index.vue— importtheme.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 renderingsrc/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— addshikidependency
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
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:
// 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
@usedirective at the top ofindex.vue
At line 0 of src/views/frontend/index.vue (before <template>), add:
<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-containerbackground to dark base
In src/views/frontend/index.vue:861-866, replace the existing block:
.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:
- The new
<style lang="scss">block is above the<style scoped lang="scss">at line 860 - The path
./styles/theme.scssresolves correctly fromsrc/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:
<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:
<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:
<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):
.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:
<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:
<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:
--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:
.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:
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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
}
- 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:
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
formatContentto use the async renderer
Replace lines 213-216 with:
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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
}
- Step 3: Add a
watchto re-render content once Shiki loads
After the existing watch(() => props.loading, ...) block (around line 393-401), add:
// 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:
import { renderCode } from '@/views/frontend/utils/highlighter'
Place it next to the other @/... imports (around line 154).
- Step 2: Add a reactive
renderedCoderef
After the existing previewContent declaration (around line 199), add:
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:
<pre
v-else
class="preview-text"
@dblclick="handleDoubleClick"
>{{ previewContent }}</pre>
with:
<div
v-else
class="preview-text preview-code"
@dblclick="handleDoubleClick"
v-html="renderedCode || escapeForPreview(previewContent)"
></div>
- Step 4: Add a watcher that triggers
renderCodewhen preview content changes
After the existing editableContent declaration (around line 204), add:
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, '&').replace(/</g, '<').replace(/>/g, '>')
}
- Step 5: Add
.preview-codestyle override
In the <style scoped> block (Task 10 already replaced), the .preview-text rule applies. Add a small override (insert after .preview-text rule):
.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, 13createMarkdownRenderer()signature consistent across Tasks 11, 12- Token names (
--fe-*) consistent across Tasks 1, 3-10 renderedCoderef name consistent in Task 13