15 KiB
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 图标)+ Sass(sass-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 导入
将:
import { Cpu } from '@element-plus/icons-vue'
import LlmModelModal from './LlmModelModal.vue'
改为:
import { Delete, Fold, Expand } from '@element-plus/icons-vue'
- Step 2: 删除 LlmModelModal 引用与 tab 状态
删除以下声明(出现在 import 之后、defineProps 之前):
const activeTab = ref('agent')
const showModelModal = ref(false)
- Step 3: 扩展 props
将:
const props = defineProps({
agentName: {
type: String,
default: '请选择会话'
},
currentModel: {
type: Object,
default: null
}
})
改为:
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
将:
const emit = defineEmits(['tab-change', 'model-change', 'clear'])
改为:
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 块)替换为:
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> 块从:
<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>
替换为:
<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"> 整段替换为:
.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规则
找到:
.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); }
}
替换为:
.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函数体
将:
function syncExpandedWithCurrent() {
if (currentSession.value) {
expandedGroups.value = new Set([groupOf(currentSession.value)])
} else {
expandedGroups.value = new Set(['today'])
}
}
替换为:
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: 新增
modelSelectOptionscomputed 与handleModelSelectChange
在 currentModel = ref(null) 附近、initModels 之前,插入:
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 之后插入:
function handleToggleFilePanel() {
filePanelVisible.value = !filePanelVisible.value
}
- Step 4: 删除
handleTabChange函数体
将 handleTabChange 整个函数(含之前的 function handleTabChange(tab) { ... })连同其上方空行一起删除。
- Step 5: 删除
handleFilePanelClose中的setActiveTab调用
将:
function handleFilePanelClose() {
filePanelVisible.value = false;
previewVisible.value = false;
chatHeaderRef.value?.setActiveTab('agent');
}
改为:
function handleFilePanelClose() {
filePanelVisible.value = false;
previewVisible.value = false;
}
- Step 6: 替换模板上的
<ChatHeader>标签
将:
<ChatHeader
ref="chatHeaderRef"
:agent-name="currentConversation?.agentName || '请选择会话'"
:current-model="currentModel"
@tab-change="handleTabChange"
@model-change="handleModelChange"
@clear="handleClear"
/>
替换为:
<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圆形 icon(accent 背景、白字、显示 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(用户自行决定是否提交)。