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

15 KiB
Raw Permalink Blame History

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-drivenmodelOptionsmodelValuefilePanelVisible),通过 update:modelValuetoggle-file-panel 两个事件与父组件通讯。ConversationSidebar 调整 syncExpandedWithCurrent 默认行为并补充搜索框 :focus 样式。index.vue 默认 filePanelVisible = true、新增 handleModelSelectChange / handleToggleFilePanel、删除 handleTabChangesetActiveTab 调用。LlmModelModal 文件保留不引用。

Tech Stack: Vue 3.5 + Composition API + Element Plus 2.13el-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;删除 handleTabChangehandleFilePanelClose 中移除 setActiveTab 调用;新增 modelSelectOptions computed、handleModelSelectChangehandleToggleFilePanel;模板 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: 新增 modelSelectOptions computed 与 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 圆形 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(用户自行决定是否提交)。