docs(spec): frontend 顶部重排 + 搜索高亮 + 分组默认展开

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-08-08 16:29:05 +08:00
co-authored by Claude Opus 4.7
parent ba19b553f3
commit 1a57a4f2e9
@@ -0,0 +1,276 @@
# 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` 一致。