Files
agent-frontend-web/docs/superpowers/specs/2026-08-08-frontend-header-restructure-design.md
T

276 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Frontend 顶部重排 + 搜索高亮 + 分组默认展开 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` 一致。