10 KiB
10 KiB
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
模板变更
<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 块内追加:
.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:
function syncExpandedWithCurrent() {
// 用户要求默认全部展开;当前会话所在分组由图标高亮区分,无需特殊收/展
expandedGroups.value = new Set(['today', 'yesterday', 'earlier'])
}
toggleGroup 行为保持不变——用户可手动收/展某个分组,刷新或切换会话后会重置为全部展开。
3. src/views/frontend/index.vue
状态修改
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():
function handleToggleFilePanel() {
filePanelVisible.value = !filePanelVisible.value
}
模板调整
把 <ChatHeader> 改为:
<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"
/>
脚本新增
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 引用)。
数据流
index.vue持有currentModel/modelList/filePanelVisible;- 把
modelSelectOptions与model-value传给 ChatHeader; - ChatHeader
el-selectchange → emitupdate:modelValue(value)→ 父按 value 在modelList中查找匹配项 → 写currentModel→ emitmodel-change; - ChatHeader 点击折叠 icon → emit
toggle-file-panel→ 父翻转filePanelVisible; - 父根据
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="..."残留; - 删除
setActiveTabexpose 方法时确认 index.vue 内无chatHeaderRef.value?.setActiveTab(...)调用(handleFilePanelClose中有,需要一并删除); - 移除
LlmModelModal引用可能使该文件成为孤儿;保留即可,无外部影响; el-select与el-button共存于.header-right的gap: 8px样式需保持视觉留白与上一轮8px一致。