Files
agent-frontend-web/docs/superpowers/plans/2026-08-08-frontend-header-restructure.md

542 lines
15 KiB
Markdown
Raw Permalink 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 顶部重排 + 搜索高亮 + 分组默认展开 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`(用户自行决定是否提交)。