1658 lines
43 KiB
Markdown
1658 lines
43 KiB
Markdown
# 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, '&')
|
||
.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:
|
||
|
||
```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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''')
|
||
}
|
||
```
|
||
|
||
- [ ] **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, '&').replace(/</g, '<').replace(/>/g, '>')
|
||
}
|
||
```
|
||
|
||
- [ ] **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
|