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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-08-08 16:30:08 +08:00
co-authored by Claude Opus 4.7
parent 1a57a4f2e9
commit eae1e31c23
@@ -0,0 +1,542 @@
# 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:** 把 ChatHeader 顶部右侧改为"清空 icon + 模型下拉 + 文件面板折叠 icon";标题前加 agentName 首字圆形 icon;文件面板默认展开;侧栏搜索框聚焦高亮;分组默认全部展开。
**Architecture:** 三个文件局部改动,不新增组件。ChatHeader 改为 props-driven`modelOptions``modelValue``filePanelVisible`),通过 `update:modelValue``toggle-file-panel` 两个事件与父组件通讯。ConversationSidebar 调整 `syncExpandedWithCurrent` 默认行为并补充搜索框 `:focus` 样式。index.vue 默认 `filePanelVisible = true`、新增 `handleModelSelectChange` / `handleToggleFilePanel`、删除 `handleTabChange``setActiveTab` 调用。LlmModelModal 文件保留不引用。
**Tech Stack:** Vue 3.5 + Composition API + Element Plus 2.13`el-select` / `el-option` / `Fold` / `Expand` / `Delete` 图标)+ Sasssass-embedded 1.97)。无前端测试框架,沿用手动 dev 验证(Task 5)。
---
## File Structure
| 文件 | 操作 | 职责 |
|---|---|---|
| `src/views/frontend/components/ChatHeader.vue` | 改 | 模板重排(avatar / clear icon / model select / sidebar fold icon);脚本替换(LlmModelModal 移除、新增 modelOptions 等 props、单一 update:modelValue 通道);样式新增(.agent-avatar / .icon-btn / .model-select |
| `src/views/frontend/components/ConversationSidebar.vue` | 改 | `.sidebar-search` 追加 `:focus` 样式;`syncExpandedWithCurrent` 改为全部展开 |
| `src/views/frontend/index.vue` | 改 | `filePanelVisible` 默认 `true`;删除 `handleTabChange``handleFilePanelClose` 中移除 `setActiveTab` 调用;新增 `modelSelectOptions` computed、`handleModelSelectChange``handleToggleFilePanel`;模板 ChatHeader props 与事件调整 |
| `src/views/frontend/components/LlmModelModal.vue` | 保留 | 文件保留,未来如需"更多模型"入口再启用 |
无新增文件;不新增组件抽象。
---
## Task 1: ChatHeader —— props 与事件契约改造
**Files:**
- Modify: `src/views/frontend/components/ChatHeader.vue:33-67``<script setup>` 段)
- [ ] **Step 1: 替换 icon 导入**
将:
```js
import { Cpu } from '@element-plus/icons-vue'
import LlmModelModal from './LlmModelModal.vue'
```
改为:
```js
import { Delete, Fold, Expand } from '@element-plus/icons-vue'
```
- [ ] **Step 2: 删除 LlmModelModal 引用与 tab 状态**
删除以下声明(出现在 `import` 之后、`defineProps` 之前):
```js
const activeTab = ref('agent')
const showModelModal = ref(false)
```
- [ ] **Step 3: 扩展 props**
将:
```js
const props = defineProps({
agentName: {
type: String,
default: '请选择会话'
},
currentModel: {
type: Object,
default: null
}
})
```
改为:
```js
const props = defineProps({
agentName: {
type: String,
default: '请选择会话'
},
currentModel: {
type: Object,
default: null
},
modelOptions: {
type: Array,
default: () => []
},
modelValue: {
type: String,
default: ''
},
filePanelVisible: {
type: Boolean,
default: true
}
})
```
- [ ] **Step 4: 替换 emits**
将:
```js
const emit = defineEmits(['tab-change', 'model-change', 'clear'])
```
改为:
```js
const emit = defineEmits(['clear', 'update:modelValue', 'toggle-file-panel'])
```
- [ ] **Step 5: 删除 tab/modal 相关函数、删除 expose,新增 `agentNameInitial` / `handleSelectChange` / `handleToggleClick`**
将整个 `<script setup>` 中除 `defineProps` / `defineEmits` / icon 导入之外的所有函数(含 `handleTabClick` / `handleModelSelect` / `defineExpose` 块)替换为:
```js
const agentNameInitial = computed(() => {
const name = (props.agentName || '').trim()
if (!name) return '?'
// 中文取首字,英文取首字母
return [...name][0]
})
function handleSelectChange(value) {
emit('update:modelValue', value)
}
function handleToggleClick() {
emit('toggle-file-panel')
}
```
并在 `<script setup>` 顶部补充 `import { computed } from 'vue'`(如果尚未导入)。
---
## Task 2: ChatHeader —— 模板替换
**Files:**
- Modify: `src/views/frontend/components/ChatHeader.vue:0-31``<template>` 段)
- [ ] **Step 1: 整段替换 `<template>` 内容**
`<template>` 块从:
```vue
<template>
<div class="chat-header">
<div class="header-left">
<span class="conversation-name">{{ agentName }}</span>
</div>
<div class="header-right">
<!-- 清空会话 -->
<el-button @click="emit('clear')">
清空会话
</el-button>
<!-- 模型选择 -->
<el-button @click="showModelModal = true">
{{ currentModel?.name || currentModel?.modelName || '选择模型' }}
</el-button>
<el-button
:type="activeTab === 'agent' ? 'primary' : 'default'"
@click="handleTabClick('agent')"
>
智能体
</el-button>
<el-button
:type="activeTab === 'file' ? 'primary' : 'default'"
@click="handleTabClick('file')"
>
文件
</el-button>
</div>
<!-- 模型选择弹窗 -->
<LlmModelModal v-model="showModelModal" @select="handleModelSelect" />
</div>
</template>
```
替换为:
```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"
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>
```
---
## Task 3: ChatHeader —— 样式新增
**Files:**
- Modify: `src/views/frontend/components/ChatHeader.vue``<style scoped lang="scss">` 段)
- [ ] **Step 1: 重写 `<style scoped lang="scss">` 内容**
将现有 `<style scoped lang="scss">` 整段替换为:
```scss
.chat-header {
padding: 11px;
border-bottom: 1px solid var(--fe-border);
display: flex;
justify-content: space-between;
align-items: center;
background: var(--fe-bg-elevated);
color: var(--fe-text-primary);
.header-left {
display: flex;
align-items: center;
gap: 10px;
max-width: 480px;
.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;
}
.conversation-name {
font-size: 16px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
}
}
.header-right {
display: flex;
gap: 8px;
align-items: center;
.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);
transition: all 0.2s;
&: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); }
}
}
}
```
---
## Task 4: ConversationSidebar —— 搜索框聚焦高亮
**Files:**
- Modify: `src/views/frontend/components/ConversationSidebar.vue``<style scoped lang="scss">``.sidebar-search` 块)
- [ ] **Step 1: 在 `.sidebar-search` 内追加 `:focus` 规则**
找到:
```scss
.sidebar-search {
padding: 8px 12px 0;
:deep(.el-input__wrapper) {
background: var(--fe-bg-elevated);
box-shadow: 0 0 0 1px var(--fe-border) inset;
border-radius: var(--fe-radius-md);
}
:deep(.el-input__inner) {
color: var(--fe-text-primary);
&::placeholder { color: var(--fe-text-muted); }
}
:deep(.el-input__prefix .el-icon) { color: var(--fe-text-muted); }
}
```
替换为:
```scss
.sidebar-search {
padding: 8px 12px 0;
:deep(.el-input__wrapper) {
background: var(--fe-bg-elevated);
box-shadow: 0 0 0 1px var(--fe-border) inset;
border-radius: var(--fe-radius-md);
transition: box-shadow 0.15s, background 0.15s;
}
:deep(.el-input__wrapper.is-focus) {
box-shadow: 0 0 0 1px var(--fe-accent) inset;
background: var(--fe-bg-elevated);
}
:deep(.el-input__inner) {
color: var(--fe-text-primary);
&::placeholder { color: var(--fe-text-muted); }
}
:deep(.el-input__wrapper.is-focus .el-input__inner::placeholder) {
color: var(--fe-text-secondary);
}
:deep(.el-input__prefix .el-icon) { color: var(--fe-text-muted); }
}
```
---
## Task 5: ConversationSidebar —— 分组默认全部展开
**Files:**
- Modify: `src/views/frontend/components/ConversationSidebar.vue``<script setup>``syncExpandedWithCurrent`
- [ ] **Step 1: 重写 `syncExpandedWithCurrent` 函数体**
将:
```js
function syncExpandedWithCurrent() {
if (currentSession.value) {
expandedGroups.value = new Set([groupOf(currentSession.value)])
} else {
expandedGroups.value = new Set(['today'])
}
}
```
替换为:
```js
function syncExpandedWithCurrent() {
// 用户要求默认全部展开;用户可点击分组标题手动收/展
expandedGroups.value = new Set(['today', 'yesterday', 'earlier'])
}
```
---
## Task 6: index.vue —— 文件面板默认展开 + 新增模型切换与折叠处理
**Files:**
- Modify: `src/views/frontend/index.vue`
- [ ] **Step 1: 文件面板默认 `true`**
`const filePanelVisible = ref(false);` 改为 `const filePanelVisible = ref(true);`
- [ ] **Step 2: 新增 `modelSelectOptions` computed 与 `handleModelSelectChange`**
`currentModel = ref(null)` 附近、`initModels` 之前,插入:
```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}`)
}
}
```
- [ ] **Step 3: 新增 `handleToggleFilePanel`**
`handleModelChange` 之后插入:
```js
function handleToggleFilePanel() {
filePanelVisible.value = !filePanelVisible.value
}
```
- [ ] **Step 4: 删除 `handleTabChange` 函数体**
`handleTabChange` 整个函数(含之前的 `function handleTabChange(tab) { ... }`)连同其上方空行一起删除。
- [ ] **Step 5: 删除 `handleFilePanelClose` 中的 `setActiveTab` 调用**
将:
```js
function handleFilePanelClose() {
filePanelVisible.value = false;
previewVisible.value = false;
chatHeaderRef.value?.setActiveTab('agent');
}
```
改为:
```js
function handleFilePanelClose() {
filePanelVisible.value = false;
previewVisible.value = false;
}
```
- [ ] **Step 6: 替换模板上的 `<ChatHeader>` 标签**
将:
```html
<ChatHeader
ref="chatHeaderRef"
:agent-name="currentConversation?.agentName || '请选择会话'"
:current-model="currentModel"
@tab-change="handleTabChange"
@model-change="handleModelChange"
@clear="handleClear"
/>
```
替换为:
```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"
/>
```
---
## Task 7: 端到端冒烟清单
**Files:** 无新增;仅执行 dev 自检。
- [ ] **Step 1: ChatHeader 视觉**
启动 `npm run dev`,进入 `/frontend`,目测:
- 左侧 `agent-avatar` 圆形 iconaccent 背景、白字、显示 agentName 首字),后跟 agentName
- 右侧依次:垃圾桶 icon(清空)、模型 el-select 下拉、文件面板折叠 icon(默认 Fold,因为 filePanelVisible 默认 true);
- 不再有 "智能体" / "文件" 两个 tab 按钮;不再有 `LlmModelModal` 弹窗入口。
- [ ] **Step 2: 文件面板默认展开 + icon 切换**
- 页面加载后文件面板可见;
- 点击 ChatHeader 右侧 Fold icon → 文件面板收起,icon 变为 Expand
- 再点 Expand icon → 文件面板展开,icon 变回 Fold。
- [ ] **Step 3: 模型下拉**
- el-select 显示当前模型名称;
- 切换下拉 → 立即生效(chatWindow 后续发送消息会带上新模型);
- `ElMessage` 提示"已选择模型: …"。
- [ ] **Step 4: 清空 icon**
- 点击垃圾桶 icon → WebSocket 发送 `clear` 类型消息,聊天区消息清空;
- 控制台无 error。
- [ ] **Step 5: 搜索框聚焦高亮**
- 点击侧栏搜索框 → 输入框边框变为 accent 色;
- placeholder 颜色由 muted 变 secondary
- 失焦恢复。
- [ ] **Step 6: 分组默认全部展开**
- 页面加载后 今天 / 昨天 / 更早 三段同时展开;
- 切换会话后仍全部展开(不回到"仅当前分组");
- 点击任一分组标题可手动折叠;切换会话或刷新会重置为全部展开。
- [ ] **Step 7: 不做 git 推送**
本次仅修改本地文件,不运行 `git push` / `git commit`(用户自行决定是否提交)。