276 lines
10 KiB
Markdown
276 lines
10 KiB
Markdown
# Frontend 顶部重排 + 搜索高亮 + 分组默认展开 Design Spec
|
||
|
||
**日期:** 2026-08-08
|
||
**状态:** Approved
|
||
**范围:** UI 调整(仅本地代码,不做 git 推送)
|
||
**前置:** 已完成 docs/superpowers/specs/2026-08-08-frontend-dark-theme-sidebar-search-chat-design.md 的深色主题 + 会话搜索/分组 + 聊天气泡节奏改动。本 spec 在此基础上扩展。
|
||
|
||
## 背景
|
||
|
||
根目录 `chat.png` 中聊天区顶部为:`[agent avatar] [agent name] …… [icon: clear] [model select] [icon: collapse]`。
|
||
|
||
当前 `ChatHeader.vue` 顶部右侧有四个按钮:清空会话 / 选择模型 / 智能体 / 文件。"智能体" / "文件" 是互斥 tab,会同时影响 `filePanelVisible` 与 `previewVisible`。本次去掉两个 tab,换成一个独立的"折叠文件面板"icon,让文件面板默认展开,由 icon 控制显隐。
|
||
|
||
同时修正:
|
||
- 搜索框聚焦时仅边框变 accent、placeholder 略亮(已配置样式但未生效);
|
||
- 会话分组默认全部展开(当前默认仅当前会话所在分组展开)。
|
||
|
||
## 目标 / 非目标
|
||
|
||
**目标**
|
||
|
||
- ChatHeader 顶部右侧:`[icon: clear]` + `[el-select: model]` + `[icon: sidebar-fold/unfold]`;
|
||
- 标题前加 `agentName` 首字圆形 icon(accent 背景,白色字);
|
||
- 文件面板默认展开,点击 sidebar-fold icon 折叠,再点 sidebar-unfold icon 展开;
|
||
- 搜索框聚焦高亮边框与 placeholder;
|
||
- 会话分组默认全部展开(`today / yesterday / earlier`);
|
||
- 移除 `LlmModelModal` 在 ChatHeader 中的引用(文件保留);
|
||
- 移除 `handleTabChange` 中"agent"分支与 `setActiveTab` 调用点。
|
||
|
||
**非目标**
|
||
|
||
- 不改 WebSocket、流式消息、自动下拉、`InputArea`、`UserArea`、右键菜单、`ChatWindow`、浅色主题、`theme.scss`;
|
||
- 不持久化文件面板折叠状态、模型选择状态;
|
||
- 不动后端 `listLlmModels`、`getLlmAngent` 等接口;
|
||
- 不在本次引入 `useFilePanelStore` / `useModelStore`(保持父组件持有状态)。
|
||
|
||
## 改动清单
|
||
|
||
### 1. `src/views/frontend/components/ChatHeader.vue`
|
||
|
||
#### 模板变更
|
||
|
||
```vue
|
||
<template>
|
||
<div class="chat-header">
|
||
<div class="header-left">
|
||
<div class="agent-avatar">{{ agentNameInitial }}</div>
|
||
<span class="conversation-name">{{ agentName }}</span>
|
||
</div>
|
||
<div class="header-right">
|
||
<el-tooltip content="清空会话" placement="bottom" :show-after="300">
|
||
<el-button class="icon-btn" link @click="emit('clear')">
|
||
<el-icon><Delete /></el-icon>
|
||
</el-button>
|
||
</el-tooltip>
|
||
|
||
<el-select
|
||
class="model-select"
|
||
:model-value="modelValue"
|
||
@update:model-value="handleSelectChange"
|
||
size="default"
|
||
placeholder="选择模型"
|
||
>
|
||
<el-option
|
||
v-for="opt in modelOptions"
|
||
:key="opt.value"
|
||
:label="opt.label"
|
||
:value="opt.value"
|
||
/>
|
||
</el-select>
|
||
|
||
<el-tooltip :content="filePanelVisible ? '收起文件面板' : '展开文件面板'" placement="bottom" :show-after="300">
|
||
<el-button class="icon-btn" link @click="handleToggleClick">
|
||
<el-icon><component :is="filePanelVisible ? Fold : Expand" /></el-icon>
|
||
</el-button>
|
||
</el-tooltip>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
> 注:Element Plus 图标库中"面板折叠 / 展开"对应的图标名为 `Fold` / `Expand`(无 `SidebarFold` / `SidebarUnfold` 同名图标)。语义与"收起 / 展开文件面板"完全对应。
|
||
```
|
||
|
||
#### 脚本变更
|
||
|
||
- `import { Delete, Fold, Expand } from '@element-plus/icons-vue'`;
|
||
- 删除 `import LlmModelModal from './LlmModelModal.vue'`、`activeTab` ref、`showModelModal` ref、`handleTabClick` 函数、`setActiveTab` expose 方法;
|
||
- 删除 `emit('tab-change', tab)`;
|
||
- 新增 props:`modelOptions: Array<{label, value}>`、`modelValue: String`、`filePanelVisible: Boolean`;
|
||
- 新增 emits:`update:modelValue`、`toggle-file-panel`;
|
||
- 新增 computed `agentNameInitial`:取 `agentName` 的第一个字符(中文取首字、英文取首字母),为空时返回 `?`;
|
||
- 新增 `handleSelectChange(value)`:emit `update:modelValue`;
|
||
- 新增 `handleToggleClick()`:emit `toggle-file-panel`;
|
||
- 删除 `handleModelSelect(model)` 与 `model-change` emit——切换模型统一通过 `update:modelValue` 单事件流,避免重复。
|
||
|
||
#### 样式新增
|
||
|
||
```scss
|
||
.agent-avatar {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 50%;
|
||
background: var(--fe-accent);
|
||
color: var(--fe-text-inverse);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.header-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
max-width: 480px;
|
||
}
|
||
|
||
.icon-btn {
|
||
padding: 4px 8px;
|
||
background: transparent;
|
||
border: none;
|
||
color: var(--fe-text-secondary);
|
||
height: 32px;
|
||
width: 32px;
|
||
cursor: pointer;
|
||
border-radius: var(--fe-radius-md);
|
||
&:hover {
|
||
background: var(--fe-bg-hover);
|
||
color: var(--fe-text-primary);
|
||
}
|
||
.el-icon { font-size: 18px; }
|
||
}
|
||
|
||
.model-select {
|
||
width: 180px;
|
||
:deep(.el-select__wrapper) {
|
||
background: var(--fe-bg-elevated);
|
||
box-shadow: 0 0 0 1px var(--fe-border) inset;
|
||
border-radius: var(--fe-radius-md);
|
||
}
|
||
:deep(.el-select__placeholder) { color: var(--fe-text-muted); }
|
||
:deep(.el-select__selected-item) { color: var(--fe-text-primary); }
|
||
}
|
||
```
|
||
|
||
### 2. `src/views/frontend/components/ConversationSidebar.vue`
|
||
|
||
#### 搜索框聚焦高亮
|
||
|
||
在已有 `.sidebar-search` 块内追加:
|
||
|
||
```scss
|
||
.sidebar-search {
|
||
:deep(.el-input__wrapper.is-focus) {
|
||
box-shadow: 0 0 0 1px var(--fe-accent) inset;
|
||
background: var(--fe-bg-elevated);
|
||
}
|
||
:deep(.el-input__wrapper.is-focus .el-input__inner::placeholder) {
|
||
color: var(--fe-text-secondary);
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 分组默认全部展开
|
||
|
||
修改 `syncExpandedWithCurrent`:
|
||
|
||
```js
|
||
function syncExpandedWithCurrent() {
|
||
// 用户要求默认全部展开;当前会话所在分组由图标高亮区分,无需特殊收/展
|
||
expandedGroups.value = new Set(['today', 'yesterday', 'earlier'])
|
||
}
|
||
```
|
||
|
||
`toggleGroup` 行为保持不变——用户可手动收/展某个分组,刷新或切换会话后会重置为全部展开。
|
||
|
||
### 3. `src/views/frontend/index.vue`
|
||
|
||
#### 状态修改
|
||
|
||
```js
|
||
const filePanelVisible = ref(true) // 原为 false
|
||
```
|
||
|
||
#### 删除 / 新增函数
|
||
|
||
- 整体删除 `handleTabChange` 函数体(index.vue 模板上 `@tab-change="..."` 也删除);
|
||
- 删除 `handleFilePanelClose` 中对 `chatHeaderRef.value?.setActiveTab('agent')` 的调用(仅保留函数体中 `filePanelVisible.value = false; previewVisible.value = false`);
|
||
- 删除模板上 `@model-change="handleModelChange"`、`@clear` 保留;改用 `@update:model-value="handleModelSelectChange"` 单一通道;
|
||
- 新增 `handleToggleFilePanel()`:
|
||
|
||
```js
|
||
function handleToggleFilePanel() {
|
||
filePanelVisible.value = !filePanelVisible.value
|
||
}
|
||
```
|
||
|
||
#### 模板调整
|
||
|
||
把 `<ChatHeader>` 改为:
|
||
|
||
```html
|
||
<ChatHeader
|
||
ref="chatHeaderRef"
|
||
:agent-name="currentConversation?.agentName || '请选择会话'"
|
||
:current-model="currentModel"
|
||
:model-options="modelSelectOptions"
|
||
:model-value="currentModel?.modelName || currentModel?.name || ''"
|
||
:file-panel-visible="filePanelVisible"
|
||
@update:model-value="handleModelSelectChange"
|
||
@toggle-file-panel="handleToggleFilePanel"
|
||
@clear="handleClear"
|
||
/>
|
||
```
|
||
|
||
#### 脚本新增
|
||
|
||
```js
|
||
const modelSelectOptions = computed(() => modelList.value.map(m => ({
|
||
label: m.name || m.modelName || '',
|
||
value: m.modelName || m.name || ''
|
||
})))
|
||
|
||
function handleModelSelectChange(value) {
|
||
const found = modelList.value.find(m => (m.modelName || m.name) === value)
|
||
if (found) {
|
||
currentModel.value = found
|
||
ElMessage.success(`已选择模型: ${found.name || found.modelName}`)
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4. `src/views/frontend/components/LlmModelModal.vue`
|
||
|
||
不修改,保留文件以备后续扩展(不再从 ChatHeader 引用)。
|
||
|
||
## 数据流
|
||
|
||
1. `index.vue` 持有 `currentModel` / `modelList` / `filePanelVisible`;
|
||
2. 把 `modelSelectOptions` 与 `model-value` 传给 ChatHeader;
|
||
3. ChatHeader `el-select` change → emit `update:modelValue(value)` → 父按 value 在 `modelList` 中查找匹配项 → 写 `currentModel` → emit `model-change`;
|
||
4. ChatHeader 点击折叠 icon → emit `toggle-file-panel` → 父翻转 `filePanelVisible`;
|
||
5. 父根据 `filePanelVisible` 渲染 `<FilePanel>`。
|
||
|
||
## 异常处理
|
||
|
||
- `modelList` 为空:`el-select` 下拉空选项,不报错;
|
||
- `currentModel` 找不到匹配项:`handleModelSelectChange` 静默不更新,保留旧值;
|
||
- `agentName` 为空:`agent-avatar` 显示 `?`;
|
||
- 文件面板折叠 / 展开不持久化,刷新页面后默认展开;
|
||
- `LlmModelModal.vue` 文件保留但失去引用入口——下一轮如需"更多模型"再加回;本轮不删文件。
|
||
|
||
## 验证
|
||
|
||
| 步骤 | 期望 |
|
||
|---|---|
|
||
| 1 | 启动 dev,深色主题下顶部 ChatHeader:左侧 `agent-avatar` 圆形(accent 背景)+ agent 名称;右侧依次:垃圾桶 icon、模型下拉、文件面板折叠 icon |
|
||
| 2 | 切换会话,`agent-avatar` 文字随 `agentName` 首字变化 |
|
||
| 3 | 文件面板默认展开;点击 sidebar-fold icon → 文件面板收起,icon 变为 sidebar-unfold;再点 → 展开 |
|
||
| 4 | 切换模型下拉 → 立即生效;ChatHeader 模型 select 与后端 `listLlmModels` 一致 |
|
||
| 5 | 点击垃圾桶 icon → 触发 `handleClear`,WebSocket 发送 `clear` 类型消息,会话消息清空 |
|
||
| 6 | 搜索框聚焦:边框变为 accent 色,placeholder 颜色变浅;失焦恢复 |
|
||
| 7 | 会话侧栏分组今天 / 昨天 / 更早 默认全部展开;点击任一分组标题可手动折叠 |
|
||
| 8 | 浅色主题下所有上述功能保持一致(继承 `--fe-*`) |
|
||
| 9 | WebSocket 流式消息、自动下拉、Token 轮询等不回归 |
|
||
| 10 | 不做 git 推送;本地变更未提交 |
|
||
|
||
## 风险
|
||
|
||
- 删除 `handleTabChange` 时要确认 index.vue 模板上无 `@tab-change="..."` 残留;
|
||
- 删除 `setActiveTab` expose 方法时确认 index.vue 内无 `chatHeaderRef.value?.setActiveTab(...)` 调用(`handleFilePanelClose` 中有,需要一并删除);
|
||
- 移除 `LlmModelModal` 引用可能使该文件成为孤儿;保留即可,无外部影响;
|
||
- `el-select` 与 `el-button` 共存于 `.header-right` 的 `gap: 8px` 样式需保持视觉留白与上一轮 `8px` 一致。 |