Files

747 lines
40 KiB
Markdown
Raw Permalink Normal View History

# 工具栏统一设计 v1.3 — 删除 IntegrationZone 坐席按钮 — 架构设计
> **版本**: v1.3
> **日期**: 2026-08-04
> **作者**: 高见远(架构师)
> **需求编号**: REQ-会话-001
> **PRD 基线**: `docs/01-产品文档/02-会话管理/PRD-REQ-会话-001-工具栏统一设计-v1.3.md`
> **原型基线**: `docs/01-产品文档/02-会话管理/原型-REQ-会话-001-工具栏统一设计v1.3-删除IntegrationZone坐席.html`
> **状态**: [待评审]
---
## 目录
1. [实现方案](#1-实现方案)
2. [框架选型](#2-框架选型)
3. [文件列表及相对路径](#3-文件列表及相对路径)
4. [数据结构和接口(类图)](#4-数据结构和接口类图)
5. [程序调用流程(时序图)](#5-程序调用流程时序图)
6. [任务列表(有序、含依赖关系)](#6-任务列表有序含依赖关系)
7. [依赖包列表](#7-依赖包列表)
8. [共享知识(跨文件约定)](#8-共享知识跨文件约定)
9. [待明确事项](#9-待明确事项)
---
## 1. 实现方案
### 1.1 总体思路(两步走)
PRD 目标单一:删除 `IntegrationZone.vue` 中的"人工坐席"按钮(带锁图标 🔒 那个),让 H5 用户端坐席入口**唯一化**(仅 InputBar 工具栏头像按钮)。
但实际仓库现状偏离基线,需要**两个连续的还原+删除动作**:
1. **Step A — 还原**:本地 `InputBar.vue` 当前是 commit `f9c25147` 留下的"v1.3 错误实施"状态(3 按钮 + 工具栏在 InputBar 底部),与线上 v1.2 不一致。需要先**还原到 v1.2 形态**(4 按钮 + 工具栏在 InputBar 内上方居中 + 坐席头像带 5 色徽标)。
2. **Step B — 删除**:在 InputBar 处于 v1.2 形态后,再按 PRD 删 `IntegrationZone.vue` 中的坐席按钮。
> **历史教训锚点**v1.3 之前的"删除 InputBar 工具栏头像 + 移动到 InputBar 底部"曾被紧急回滚到 v1.2。本次 v1.3 真实意图 = **恢复 v1.2 形态 + 删 IntegrationZone 冗余入口**,绝不可与中间错误实施混为一谈。
### 1.2 关键设计决策
| 决策 | 理由 |
|------|------|
| **D1**:先恢复 InputBar 再删 IntegrationZone(顺序强制) | 若 InputBar 还未恢复就先删 IntegrationZone,会出现"线上无坐席入口"的事故窗口 |
| **D2**InputBar 坐席按钮直接调 `store` 副作用(不 emit 给 ChatPanel | 沿用 f9c25147 commit 的"v1.3.4 单入口"设计;emit 链路需要 ChatPanel 中转,与"工具栏按钮为唯一入口"的轻量化目标冲突 |
| **D3**ChatPanel 父组件仅清理 `@call-agent` 监听器 + `handleDirectCall` 函数 | `@cancel-queue` / `@end-conversation` / `@reopen-conversation` 仍由 QueueCapsule 透传触发(Q3 限定范围) |
| **D4**IntegrationZone 模板仅删除 1 个 `<button class="call-agent-btn">` 节点 | QueueCapsule + 引导语 + 容器样式全部保留(PRD REQ-07/08 |
| **D5**IntegrationZone 4 个 emits 仅删除 `callAgent`,保留其他 3 个 | `cancelQueue` / `endConversation` / `reopenConversation` 仍由 QueueCapsule 透传 |
| **D6**helper 文件(inputBarCallAgentState.ts / integrationZoneLogic.ts / inputBarGuideText.ts)不动 | Q4 暂搁,6 个月后评估 |
| **D7**InputBar.test.ts **重写**而非追加 | 当前测试是 v1.3 错误实施"3 按钮 + slideDown"的契约,需要回退到 v1.2"4 按钮 + slideUp"的契约 |
### 1.3 边界与不变量
- **B1**IntegrationZone.vue 的 QueueCapsule 子组件继续接收 `:agent-online` prop(v1.2 透传契约不变)。
- **B2**InputBar.vue 的 emoji / 文件 / 语音 3 个按钮行为完全不变(仅位置从底部回退到 InputBar 内上方居中)。
- **B3**`store.shakeAgent()` / `cancelQueue()` / `reopenCurrentConversation()` / `closeCurrentConversation()` 4 个 action 全部保留。
- **B4**`inputBarCallAgentState.computeCallAgentState` 6 态优先级与 P1 Bug 修复(waiting + !agentOnline → disabled)不变。
- **B5**:容器组件层级 `ChatPanel > IntegrationZone > QueueCapsule` 不变。
---
## 2. 框架选型
**无任何框架变更**,沿用 v1.2 基线:
| 维度 | 选型 | 版本 | 不变原因 |
|------|------|------|----------|
| 前端框架 | Vue 3 (Composition API + `<script setup>`) | ^3.4 | H5 用户端基线 |
| 状态管理 | Pinia | ^2.1 | store 单例(`useConversationStore` |
| UI 组件库 | Vant 4 | ^4.8 | H5 移动端基线 |
| 构建工具 | Vite | ^5.0 | H5 项目基线 |
| 样式预处理 | 原生 CSS + scoped(无 Sass/Less | — | 与现有代码风格一致 |
| 测试框架 | Vitest(纯函数契约测试) | ^1.0 | 沿用 helper 文件的测试范式 |
| 类型系统 | TypeScript | ^5.0 | `<script setup lang="ts">` |
**架构模式**:本任务不涉及架构模式变更(仍是 Component + Store + Helper 三层)。仅在前端 H5 端做局部 DOM/逻辑调整。
---
## 3. 文件列表及相对路径
| 文件 | 改动 | 涉及阶段 | 备注 |
|------|------|----------|------|
| `src/frontend-h5/src/components/chat/InputBar.vue` | **还原**到 v1.24 按钮工具栏 + 工具栏 InputBar 内上方居中 + 坐席头像按钮 + 5 色徽标) | T01 | 从 f9c25147 commit 的反向 patch 或手动重做 |
| `src/frontend-h5/src/components/chat/IntegrationZone.vue` | 删除 1 个 `<button class="call-agent-btn">` 节点 + 1 个 `callAgent` emit + 4 个相关 computed + 1 个 handleClick 函数 + 1 处 `.integration-zone .call-agent-btn*` 样式 | T02 | QueueCapsule + 引导语 + 容器样式全部保留 |
| `src/frontend-h5/src/components/chat/ChatPanel.vue` | 清理 `@call-agent="handleDirectCall"` 监听器 + `handleDirectCall` 函数(保留 3 个 QueueCapsule 透传监听器) | T03 | Q3 限定范围:仅 call-agent |
| `src/frontend-h5/src/components/chat/InputBar.test.ts` | **重写**为 v1.3 = v1.2 的契约测试(4 按钮 + 5 色徽标映射 + slideUp 动画) | T04 | 现有测试是错误实施的"3 按钮"契约 |
| `src/frontend-h5/src/components/chat/integrationZoneLogic.test.ts` | **不动** | — | 纯函数不变(Q4 |
| `src/frontend-h5/src/components/chat/integrationZoneLogic.ts` | **不动** | — | helper 保留 |
| `src/frontend-h5/src/components/chat/inputBarCallAgentState.ts` | **不动** | — | helper 保留 |
| `src/frontend-h5/src/components/chat/inputBarGuideText.ts` | **不动** | — | helper 保留 |
| `src/frontend-h5/src/components/chat/QueueCapsule.vue` | **不动** | — | 子组件不变 |
| `src/frontend-h5/src/components/chat/QueueCapsule.test.ts` | **不动** | — | 子组件测试不变(36 用例已 PASS) |
| `src/frontend-h5/src/stores/conversation.ts` | **不动** | — | 4 个 action 全部保留 |
| `src/frontend-h5/src/stores/integrationZone.ts` | **不动** | — | `shiftHours` 班次能力预留 |
**新增文件**:无。
**删除文件**:无。
---
## 4. 数据结构和接口(类图)
```mermaid
classDiagram
direction LR
class ChatPanel {
+useConversationStore() ConversationStore
+useEmployeeStore() EmployeeStore
+useIntegrationZoneStore() IntegrationZoneStore
-callAgentState: ComputedRef~CallAgentState~
-guideText: ComputedRef~string|null~
-showExitConfirm: Ref~boolean~
+scrollToBottom() Promise~void~
+handleExitWithEvaluation() Promise~void~
+handleCancelQueue() Promise~void~ // 保留
+handleEndConversation() Promise~void~ // 保留
+handleReopenConversation() Promise~void~ // 保留
-handleDirectCall() Promise~void~ // ⚠️ v1.3 删除(无效监听器)
}
class IntegrationZone {
+props: IntegrationZoneProps
-emits: callAgent // ⚠️ v1.3 删除
-emits: cancelQueue // 保留
-emits: endConversation // 保留
-emits: reopenConversation // 保留
-btnText: ComputedRef~string~ // ⚠️ v1.3 删除
-btnIcon: ComputedRef~string~ // ⚠️ v1.3 删除
-btnTitle: ComputedRef~string~ // ⚠️ v1.3 删除
-btnClass: ComputedRef~Record~ // ⚠️ v1.3 删除
-guideClass: ComputedRef~Record~ // 保留
-handleClick() void // ⚠️ v1.3 删除
-render QueueCapsule // 保留
-render guide div // 保留
}
class InputBar {
+URGENT_KEYWORDS: RegExp[]
-hasUrgentKeywords: ComputedRef~boolean~
-showEmojiPanel: Ref~boolean~
-inputText: Ref~string~
-callAgentState: ComputedRef~CallAgentState~ // ⚠️ v1.3 重新绑定到模板
-callAgentBtnClass: ComputedRef~Record~ // 🆕 v1.3 恢复
-callAgentBtnIcon: ComputedRef~string~ // 🆕 v1.3 恢复
-callAgentBtnText: ComputedRef~string~ // 🆕 v1.3 恢复
-callAgentBtnTitle: ComputedRef~string~ // 🆕 v1.3 恢复
-voiceBtnState: ComputedRef
-voiceBtnClass: ComputedRef~Record~
-showVoiceButton: ComputedRef~boolean~
+handleEmoji() void
+handleFile() void
+handleVoiceToggle() Promise~void~
+handleCallAgentClick() Promise~void~ // 🆕 v1.3 恢复
+handleSend() Promise~void~
}
class QueueCapsule {
+props: QueueCapsuleProps
-emits: cancel-queue
-emits: reopen-conversation
+handleClick() void
}
class ConversationStore {
+agentOnline: boolean
+currentConversation: Conversation
+canReopen: boolean
+canCallAgent: boolean
+messages: Message[]
+shakeAgent() Promise~string~
+cancelQueue() Promise~void~
+closeCurrentConversation() Promise~void~
+reopenCurrentConversation() Promise~void~
}
class inputBarCallAgentState {
<<module>>
+computeCallAgentState(ctx) CallAgentState
+computeCallAgentBtnText(state) string
+computeCallAgentBtnTitle(state, agentOnline) string
}
class integrationZoneLogic {
<<module>>
+computeIntegrationBtnText(state) string
+computeIntegrationBtnIcon(state) string
+computeIntegrationBtnTitle(state, agentOnline) string
+computeIntegrationBtnClass(state) Record
+computeIntegrationGuideClass(text) Record
+computeCallAction(state) EmitEvent|'none'
}
class inputBarGuideText {
<<module>>
+computeGuideText(ctx) string|null
+isGuideTextWarn(text) boolean
}
ChatPanel "1" --> "1" IntegrationZone : 父→子
ChatPanel "1" --> "1" InputBar : 父→子
IntegrationZone "1" --> "1" QueueCapsule : 父→子
ChatPanel ..> ConversationStore : uses
ChatPanel ..> inputBarCallAgentState : computeCallAgentState
ChatPanel ..> inputBarGuideText : computeGuideText
IntegrationZone ..> integrationZoneLogic : btn/guide/click
InputBar ..> inputBarCallAgentState : 6 态 + 文案 + title
InputBar ..> integrationZoneLogic : btnClass + callAction
InputBar ..> ConversationStore : shakeAgent
QueueCapsule ..> ConversationStore : reopenCurrentConversation
```
**图例**:🆕 = v1.3 恢复;⚠️ = v1.3 删除;保留/不动 = 默认无标记。
### 4.1 v1.3 删除的接口(IntegrationZone
| 接口 | 类型 | 位置 | 删除原因 |
|------|------|------|----------|
| `callAgent` emit | `(e: 'callAgent'): void` | `IntegrationZone.vue:101-102` | 按钮已删除,emit 不再有触发源 |
| `btnText` computed | `ComputedRef<string>` | `IntegrationZone.vue:126` | 依赖 `computeIntegrationBtnText`(无消费者) |
| `btnIcon` computed | `ComputedRef<string>` | `IntegrationZone.vue:131` | 同上 |
| `btnTitle` computed | `ComputedRef<string>` | `IntegrationZone.vue:136` | 同上 |
| `btnClass` computed | `ComputedRef<Record<string, boolean>>` | `IntegrationZone.vue:141` | 同上 |
| `handleClick()` function | `(): void` | `IntegrationZone.vue:167-186` | 无按钮可点击 |
| `call-agent-btn` template node | `<button class="call-agent-btn">` | `IntegrationZone.vue:34-43` | PRD REQ-01 |
| `.integration-zone .call-agent-btn*` styles | CSS (~110 行) | `IntegrationZone.vue:213-321` | PRD REQ-04 |
### 4.2 v1.3 恢复的接口(InputBar
| 接口 | 类型 | 说明 |
|------|------|------|
| `callAgentBtnClass` | `ComputedRef<Record<string, boolean>>` | 6 态 modifier 映射(沿用 `.call-agent-btn--*` 命名) |
| `callAgentBtnIcon` | `ComputedRef<string>` | 6 态 emoji(🔒 / 🎧 / 🚨 / ⏳ / 📴 / 🔄) |
| `callAgentBtnText` | `ComputedRef<string>` | 6 态文案("人工坐席" / "排队取消" / "结束咨询" / "重新打开" |
| `callAgentBtnTitle` | `ComputedRef<string>` | 6 态 title(沿用 `computeCallAgentBtnTitle` |
| `agent-badge` DOM 节点 | `<div class="agent-badge is-{state}">` | 5 色徽标(online/urgent/waiting/offline/end |
| `handleCallAgentClick()` | `(): Promise<void>` | 直接调 `store.shakeAgent()` / `cancelQueue()` / `reopenCurrentConversation()`(按 state 路由) |
---
## 5. 程序调用流程(时序图)
### 5.1 v1.3 后唯一的坐席入口点击流程
```mermaid
sequenceDiagram
autonumber
actor User as H5 用户
participant Toolbar as InputBar 工具栏<br/>(emoji/file/voice/👤坐席)
participant Logic as inputBarCallAgentState.ts
participant Store as useConversationStore
participant Toast as Vant Toast
participant API as 后端 API
User->>Toolbar: 点击坐席头像按钮
activate Toolbar
Toolbar->>Logic: computeCallAgentState(ctx)
Logic-->>Toolbar: CallAgentState (active/urgent/waiting/end/reopen/disabled)
alt state === 'disabled'
Toolbar-->>User: 无响应(按钮 :disabled
else state === 'active' or 'urgent'
Toolbar->>Store: shakeAgent()
Store->>API: POST /api/v1/conversations/{id}/shake-agent
alt 成功
API-->>Store: 200 OK + funny_phrase
Store->>Toast: showToast(funny_phrase)
else 失败
API-->>Store: 4xx/5xx
Store->>Toast: showToast('呼叫失败,请稍后重试')
end
else state === 'waiting'
Toolbar->>Store: cancelQueue()
Store->>API: POST /api/v1/conversations/{id}/cancel-queue
API-->>Store: 200 OK
Store->>Toast: showToast('已取消排队')
else state === 'reopen'
Toolbar->>Store: reopenCurrentConversation()
Store->>API: POST /api/v1/conversations/{id}/reopen
API-->>Store: 200 OK
else state === 'end'
Toolbar->>Store: closeCurrentConversation()
Note over Toolbar,Store: ⚠️ v1.3 实际: 走 ChatPanel 顶部"结束会话"按钮<br/>(头像右侧红色 ⏏ 按钮),<br/>因为 serving 态工具栏坐席按钮=disabled (v1.2 P1 修复)
end
deactivate Toolbar
```
### 5.2 v1.3 后事件流全景(IntegrationZone 旁路化)
```mermaid
sequenceDiagram
autonumber
participant User
participant InputBar
participant ChatPanel
participant IntegrationZone as IntegrationZone<br/>(无坐席按钮)
participant QueueCapsule
participant Store
participant Backend
rect rgb(240, 248, 255)
Note over User,Backend: 🆕 v1.3 真实事件流(InputBar 工具栏头像为唯一坐席入口)
User->>InputBar: 点击坐席头像
InputBar->>InputBar: handleCallAgentClick()
InputBar->>Store: shakeAgent() / cancelQueue() / reopenCurrentConversation()
Store->>Backend: API call
Backend-->>Store: 200 OK
Store-->>User: toast + UI 更新
end
rect rgb(255, 248, 248)
Note over User,Backend: 🔻 v1.3 已断开的路径(IntegrationZone 坐席按钮已删除)
User-xIntegrationZone: 点击 IntegrationZone 坐席按钮
Note right of IntegrationZone: ⚠️ DOM 节点已删除,<br/>无 click handler 可触发
end
rect rgb(248, 255, 248)
Note over User,Backend: ✅ 仍生效的路径(QueueCapsule 内部触发)
User->>QueueCapsule: 点击"取消排队"按钮
QueueCapsule->>Store: cancelQueue()v1.2y 行为:内部直接调)
QueueCapsule-->>ChatPanel: emit('cancel-queue')(被 ChatPanel 接收)
Note right of ChatPanel: ChatPanel.handleCancelQueue()<br/>仅作为兜底(QueueCapsule 已<br/>直接调 store,重复调是幂等的)
end
```
### 5.3 InputBar 6 态按钮颜色映射(v1.3 恢复)
| state | modifier class | icon | text | title | 触发 action |
|-------|---------------|------|------|-------|------------|
| `disabled` | `call-agent-btn--disabled` | 🔒 | 人工坐席 | "再多描述几句话即可激活" | 无(按钮 :disabled |
| `active` | `call-agent-btn--active` | 🎧 | 人工坐席 | "点击呼叫人工坐席" | `store.shakeAgent()` |
| `urgent` | `call-agent-btn--urgent` | 🚨 | 人工坐席 | "检测到紧急问题,直接呼叫人工坐席" | `store.shakeAgent()` |
| `waiting` | `call-agent-btn--waiting` | ⏳ | 排队取消 | "点击取消排队" | `store.cancelQueue()` |
| `end` | `call-agent-btn--end` | 📴 | 结束咨询 | "点击结束本次人工咨询" | `store.closeCurrentConversation()`v1.3 实际由顶部按钮走) |
| `reopen` | `call-agent-btn--reopen` | 🔄 | 重新打开 | "24小时内可重新打开此会话" | `store.reopenCurrentConversation()` |
---
## 6. 任务列表(有序、含依赖关系)
### T01 — 恢复 InputBar.vue 到 v1.2 状态
| 维度 | 内容 |
|------|------|
| **任务 ID** | T01 |
| **任务名** | 恢复 InputBar.vue 到 v1.2 形态(4 按钮工具栏 + InputBar 内上方居中 + 坐席头像 + 5 色徽标) |
| **依赖** | 无 |
| **优先级** | P0 |
| **涉及文件** | `src/frontend-h5/src/components/chat/InputBar.vue``src/frontend-h5/src/components/chat/inputBarCallAgentState.ts`(仅 import)、`src/frontend-h5/src/components/chat/integrationZoneLogic.ts`(仅 import |
| **负责方** | 工程师(前端) |
**详细步骤**
1. **Step 1.1 — 模板调整**
- 删除当前 v1.3 错误实施的 `<div class="input-bar__toolbar-wrapper">`(位于 InputBar 底部,InputBar.vue:115-198)。
-`<div class="input-bar__row">` **之后**插入 v1.2 形态的工具栏(4 按钮:emoji / 文件 / 语音 / 坐席头像)。
- 工具栏容器使用 `class="input-bar__toolbar"`,位置在输入行**下方、表情面板下方**(v1.2 DOM 顺序:`resize-handle``input-bar__row``emoji-panel``input-bar__toolbar`)。
- 删除 SVG defs 中的"v1.3 高端 SVG 图标"`g-emoji` / `g-doc-*` / `g-mic-*` 等,InputBar.vue:27-65),恢复 v1.2 的简单 emoji 字符(😊 / 📎 / 🎤)+ agent 按钮的图片头像(沿用原型 `agent-avatar-v0.8.jpg`)。
- 表情按钮的激活态 class 从 `is-active-emoji` 保持不变;移除 emoji 面板的 `slideDown` 动画,恢复 `slideUp` 动画(`from translateY(20px) → 0`)。
2. **Step 1.2 — 坐席头像按钮恢复**
- 在工具栏内、语音按钮**之后**插入 1 个 `<button class="input-bar__call-agent-btn">` 节点:
```html
<button
class="input-bar__call-agent-btn"
:class="callAgentBtnClass"
:title="callAgentBtnTitle"
:aria-label="callAgentBtnTitle"
:disabled="callAgentState === 'disabled'"
@click="handleCallAgentClick"
>
<span class="input-bar__call-agent-btn__icon">{{ callAgentBtnIcon }}</span>
<span class="input-bar__call-agent-btn__text">{{ callAgentBtnText }}</span>
<span class="agent-badge" :class="agentBadgeClass"></span>
</button>
```
- 移除错误注释 `<!-- 🆕 v1.3:坐席按钮已彻底移除(由 IntegrationZone 接管 6 态 agent 入口) -->`InputBar.vue:196)。
3. **Step 1.3 — script 调整**
- 恢复以下 5 个 computed(从 f9c25147 commit 的反向 patch 中取):
- `callAgentState`(已存在,需**重新绑定**到模板 + 移除 `void callAgentState` 标注)
- `callAgentBtnClass: ComputedRef<Record<string, boolean>>`(基于 `computeIntegrationBtnClass`
- `callAgentBtnIcon: ComputedRef<string>`(内联 6 行 switch,沿用 IntegrationZone 逻辑)
- `callAgentBtnText: ComputedRef<string>`(基于 `computeCallAgentBtnText`
- `callAgentBtnTitle: ComputedRef<string>`(基于 `computeCallAgentBtnTitle`
- `agentBadgeClass: ComputedRef<Record<string, boolean>>`5 色徽标:is-online/urgent/waiting/offline/end
- 恢复 `handleCallAgentClick()` 函数(按 `computeCallAction` 路由到 `store.shakeAgent/cancelQueue/reopenCurrentConversation`)。
- 恢复 5 处 import`computeCallAgentBtnText` / `computeCallAgentBtnTitle` / `computeIntegrationBtnClass` / `computeCallAction` / `CallAgentState`。
- 移除 `void callAgentState` 标注(InputBar.vue:319)。
4. **Step 1.4 — 样式调整**
- 删除 `.input-bar__toolbar-wrapper` 样式(InputBar.vue:828-834)。
- 删除 `.input-bar__toolbar` 的"v1.3 玻璃拟态 + 多层 box-shadow + radial 渐变"样式(InputBar.vue:836-876),恢复 v1.2 的简单 flex 居中。
- 删除 `.input-bar__btn` 的 v1.3 高端 SVG 适配样式(`width: 40px` 圆形玻璃 + backdrop-filterInputBar.vue:881-925),恢复 v1.2 的 32px 简单圆形按钮。
- 删除 `.input-bar__btn--voice.is-voice-recording/.is-voice-recognized` 的 3 态渐变样式(InputBar.vue:942-971),恢复 v1.2 的简单 🎤 字符按钮。
- 恢复 `.input-bar__call-agent-btn` 样式(沿用 IntegrationZone 的 `.call-agent-btn` 6 态 modifier 命名)。
- 恢复 `.agent-badge` 5 色徽标样式(online=绿/urgent=红/waiting=橙/offline=灰/end=蓝)。
- 恢复 `slideUp` keyframes(删除 `slideDown`InputBar.vue:991-1002)。
5. **Step 1.5 — 验证**
- `pnpm vue-tsc --noEmit` 0 错误。
- `grep -n "callAgentBtnClass\|callAgentBtnIcon\|callAgentBtnText\|callAgentBtnTitle\|agentBadgeClass\|input-bar__call-agent-btn" src/frontend-h5/src/components/chat/InputBar.vue` 输出非空。
- `grep -n "void callAgentState\|v1.3.*坐席按钮已彻底移除" src/frontend-h5/src/components/chat/InputBar.vue` 输出为空。
**验收标准**
- [ ] REQ-A1InputBar.vue 模板包含 4 个按钮(emoji / 文件 / 语音 / 坐席头像)。
- [ ] REQ-A2:工具栏位于 `input-bar__row` **之后**v1.2 位置:InputBar 内上方居中)。
- [ ] REQ-A3:坐席头像按钮可见 `agent-badge` 子节点,class 随 5 色切换。
- [ ] REQ-A4vue-tsc 0 错误。
- [ ] REQ-A5`pnpm vitest run InputBar.test.ts` 当前仍 PASS(错误实施的契约测试,不影响 v1.2 实现)。
---
### T02 — 删除 IntegrationZone.vue 坐席按钮
| 维度 | 内容 |
|------|------|
| **任务 ID** | T02 |
| **任务名** | 删除 IntegrationZone.vue 中的"人工坐席"按钮(template / script / style / emits 4 处) |
| **依赖** | T01(确认 InputBar.vue 已恢复 v1.2 形态) |
| **优先级** | P0 |
| **涉及文件** | `src/frontend-h5/src/components/chat/IntegrationZone.vue` |
| **负责方** | 工程师(前端) |
**详细步骤**
1. **Step 2.1 — Template 删除**
- 删除 `IntegrationZone.vue:30-65` 中的 `<button class="call-agent-btn">` 节点(4 行结构 + 2 个 `<span>` 子节点)。
- 保留 `QueueCapsule` + `guide div` 节点(不变)。
- 更新文件头注释 `IntegrationZone.vue:5-15`,标记"v1.3 已删除坐席按钮,仅保留 QueueCapsule + 引导语"。
2. **Step 2.2 — Script 删除**
- 删除 `defineEmits<{ (e: 'callAgent'): void ... }>` 中的 `callAgent` 声明(IntegrationZone.vue:102)。
- 保留 `cancelQueue` / `endConversation` / `reopenConversation` 3 个 emit(仍由 QueueCapsule 透传)。
- 删除 `btnText` / `btnIcon` / `btnTitle` / `btnClass` 4 个 computedIntegrationZone.vue:126, 131, 136, 141)。
- 删除 `handleClick()` 函数(IntegrationZone.vue:167-186)。
- 清理 importIntegrationZone.vue:83-91):
- 删除 `computeIntegrationBtnText` / `computeIntegrationBtnIcon` / `computeIntegrationBtnTitle` / `computeIntegrationBtnClass`(无消费者)
- 保留 `computeIntegrationGuideClass` / `computeCallAction`(虽然 `computeCallAction` 也无消费者,但保留以备未来重新引入按钮时复用 — 标注 `void` 抑制 noUnusedLocals)。
- 保留 `IntegrationZoneProps` type import。
3. **Step 2.3 — Style 删除**
- 删除 `.integration-zone .call-agent-btn` 容器样式(IntegrationZone.vue:213-232)。
- 删除 `.integration-zone .call-agent-btn--disabled/--active/--urgent/--waiting/--end/--reopen` 6 个 modifier 样式(IntegrationZone.vue:235-312)。
- 删除 `.integration-zone .call-agent-btn__icon` / `.__text` 样式(IntegrationZone.vue:315-321)。
- 保留 `.integration-zone` 容器 + `.integration-zone :deep(.queue-capsule)` + `.integration-zone__guide*` 样式。
4. **Step 2.4 — 验证**
- `pnpm vue-tsc --noEmit` 0 错误。
- `grep -n "call-agent-btn\|btnText\|btnIcon\|btnTitle\|btnClass\|handleClick\|callAgent" src/frontend-h5/src/components/chat/IntegrationZone.vue` 输出为空。
- `grep -n "QueueCapsule\|guide" src/frontend-h5/src/components/chat/IntegrationZone.vue` 输出非空。
- `pnpm vitest run integrationZoneLogic.test.ts` 仍 PASS(纯函数不变)。
**验收标准**
- [ ] REQ-B1IntegrationZone.vue 模板仅渲染 2 个元素(QueueCapsule + 引导语)。
- [ ] REQ-B2IntegrationZone.vue 浏览器开发者工具中无 `<button class="call-agent-btn">` DOM 节点。
- [ ] REQ-B3IntegrationZone.vue 的 `defineEmits` 仅声明 3 个事件(cancelQueue / endConversation / reopenConversation)。
- [ ] REQ-B4vue-tsc 0 错误。
- [ ] REQ-B5integrationZoneLogic.test.ts 36 用例仍 PASS(不删除任何用例)。
---
### T03 — 清理 ChatPanel.vue 中对 IntegrationZone 已失效的事件监听器
| 维度 | 内容 |
|------|------|
| **任务 ID** | T03 |
| **任务名** | 清理 ChatPanel.vue 中 `@call-agent="handleDirectCall"` 监听器 + `handleDirectCall` 函数(保守范围) |
| **依赖** | T02IntegrationZone 坐席按钮已删除) |
| **优先级** | P1 |
| **涉及文件** | `src/frontend-h5/src/components/chat/ChatPanel.vue` |
| **负责方** | 工程师(前端) |
**详细步骤**
1. **Step 3.1 — Template 清理**
- 删除 `ChatPanel.vue:116` 的 `@call-agent="handleDirectCall"` 监听器。
- 保留 `ChatPanel.vue:117-119` 的 3 个监听器(`@cancel-queue` / `@end-conversation` / `@reopen-conversation`),因为它们仍由 QueueCapsule 透传触发。
2. **Step 3.2 — Script 清理**
- 删除 `handleDirectCall()` 函数(ChatPanel.vue:339-360)。
- 保留 `handleCancelQueue()` / `handleEndConversation()` / `handleReopenConversation()` 3 个函数(仍被 QueueCapsule 透传触发)。
- 更新文件头注释 `ChatPanel.vue:9-11`,标记"v1.3 已删除 handleDirectCall 监听器"。
3. **Step 3.3 — 不删除的清单**
- 保留 `IntegrationZone` 的 `callAgent` emit 已经被删除(D2),但 QueueCapsule 的 3 个 emit 仍存在。
- 保留 `callAgentState` 和 `guideText` 2 个 computed(仍透传给 IntegrationZone 的 QueueCapsule + 引导语)。
- 保留 `computeCallAgentState` import(仍用于 callAgentState computed)。
- 保留 `computeGuideText` import(仍用于 guideText computed)。
4. **Step 3.4 — 验证**
- `pnpm vue-tsc --noEmit` 0 错误。
- `grep -n "handleDirectCall\|@call-agent" src/frontend-h5/src/components/chat/ChatPanel.vue` 输出为空。
- `grep -n "handleCancelQueue\|handleEndConversation\|handleReopenConversation\|@cancel-queue\|@end-conversation\|@reopen-conversation" src/frontend-h5/src/components/chat/ChatPanel.vue` 输出非空。
**验收标准**
- [ ] REQ-C1ChatPanel.vue 模板中无 `@call-agent` 监听器。
- [ ] REQ-C2ChatPanel.vue script 中无 `handleDirectCall` 函数定义。
- [ ] REQ-C3ChatPanel.vue 模板仍保留 3 个 QueueCapsule 透传监听器。
- [ ] REQ-C4vue-tsc 0 错误。
> **设计说明**(为何不删除全部 4 个监听器):QueueCapsule 内部仍触发 `cancel-queue` / `reopen-conversation` 2 个 emitIntegrationZone 仍转发 `end-conversation` 1 个 emit 给 ChatPanel。3 个监听器在 v1.3 后**仍然有效**,不应作为"代码冗余"清理。
---
### T04 — 补充 InputBar.test.ts 中 v1.3 专项测试
| 维度 | 内容 |
|------|------|
| **任务 ID** | T04 |
| **任务名** | 重写 InputBar.test.ts 为 v1.3 = v1.2 的契约测试(4 按钮 + 5 色徽标映射 + slideUp 动画 + 6 态按钮) |
| **依赖** | T01InputBar.vue 已恢复 v1.2 形态) |
| **优先级** | P0 |
| **涉及文件** | `src/frontend-h5/src/components/chat/InputBar.test.ts` |
| **负责方** | QA + 工程师 |
**详细步骤**
1. **Step 4.1 — 删除错误契约**
- 删除 `V13_TOOLBAR_BUTTONS = ['emoji', 'file', 'voice']`InputBar.test.ts:118-119)。
- 删除 `TOOLBAR_POSITION_INDEX = 5` 的"最末位置"断言(InputBar.test.ts:108)。
- 删除 `V13_SLIDE_DIRECTION = 'down'`InputBar.test.ts:128)。
2. **Step 4.2 — 恢复 v1.2 契约**
- 恢复 `V13_TOOLBAR_BUTTONS = ['emoji', 'file', 'voice', 'agent']`4 按钮)。
- 恢复 `V12_TOOLBAR_BUTTONS = ['emoji', 'file', 'voice', 'agent']`(v1.2 与 v1.3 相同,便于对比)。
- 恢复 `V13_SLIDE_DIRECTION = 'up'`slideUp 动画)。
- 恢复 `TOOLBAR_POSITION_INDEX = 2`(工具栏在 `input-bar__row` 之后、`emoji-panel` 之前)。
3. **Step 4.3 — 新增 5 色徽标契约测试**:
- `computeAgentBadgeClass(state)` 纯函数:
- `active` → `{ 'is-online': true }`
- `urgent` → `{ 'is-urgent': true }`
- `waiting` → `{ 'is-waiting': true }`
- `disabled` → `{ 'is-offline': true }`
- `end` → `{ 'is-end': true }`
- `reopen` → `{ 'is-end': true }`reopen 复用 end 蓝色,PRD 沿用 v1.2 行为)
4. **Step 4.4 — 新增 6 态按钮 contract 测试**
- 验证 `callAgentBtnClass` 包含正确的 modifier key(如 `call-agent-btn--active` / `--urgent` / `--waiting` / `--end` / `--reopen` / `--disabled`)。
- 验证 `callAgentBtnIcon` 在 6 态下分别返回 🔒 / 🎧 / 🚨 / ⏳ / 📴 / 🔄。
- 验证 `callAgentBtnText` 在 6 态下分别返回 "人工坐席" / "人工坐席" / "人工坐席" / "排队取消" / "结束咨询" / "重新打开"。
5. **Step 4.5 — 新增 handleCallAgentClick 路由测试**
- state=active → 调 `store.shakeAgent()`
- state=urgent → 调 `store.shakeAgent()`
- state=waiting → 调 `store.cancelQueue()`
- state=reopen → 调 `store.reopenCurrentConversation()`
- state=end → 不调(实际由顶部按钮走)
- state=disabled → 不响应
6. **Step 4.6 — 验证**
- `pnpm vitest run InputBar.test.ts` 全 PASS(预计 50-55 个用例,包含 v1.2 沿用 + v1.3 新增 5 色徽标 + 6 态按钮 + handleCallAgentClick 路由)。
**验收标准**
- [ ] REQ-D1InputBar.test.ts 中 `V13_TOOLBAR_BUTTONS` 长度为 4(包含 'agent')。
- [ ] REQ-D2InputBar.test.ts 中新增 `computeAgentBadgeClass` 5 色映射测试 5-6 个用例。
- [ ] REQ-D3InputBar.test.ts 中新增 `callAgentBtnClass` / `callAgentBtnIcon` / `callAgentBtnText` 6 态测试 18 个用例。
- [ ] REQ-D4InputBar.test.ts 中新增 `handleCallAgentClick` 路由测试 5-6 个用例。
- [ ] REQ-D5:所有用例 PASS(无失败、无 skip)。
---
### T05 — 全局一致性审查
| 维度 | 内容 |
|------|------|
| **任务 ID** | T05 |
| **任务名** | 全局一致性审查(IS_PASS: YES/NO |
| **依赖** | T01、T02、T03、T04 |
| **优先级** | P0 |
| **涉及文件** | 全 chat 模块(grep 验证) |
| **负责方** | 架构师(高见远)+ 工程师 |
**详细步骤**
1. **Step 5.1 — 跨文件 import 审查**
- 验证 `ChatPanel.vue` 不再 import `handleDirectCall` 相关依赖。
- 验证 `InputBar.vue` import 5 个 helper`computeCallAgentState` / `computeCallAgentBtnText` / `computeCallAgentBtnTitle` / `computeIntegrationBtnClass` / `computeCallAction`)。
- 验证 `IntegrationZone.vue` import 仍包含 `QueueCapsule` + `computeIntegrationGuideClass`。
2. **Step 5.2 — 跨文件函数引用审查**:
- `grep -rn "handleDirectCall" src/frontend-h5/src/` 应输出为空。
- `grep -rn "@call-agent" src/frontend-h5/src/components/chat/` 应输出为空。
- `grep -rn "call-agent-btn" src/frontend-h5/src/components/chat/IntegrationZone.vue` 应输出为空;`InputBar.vue` 应输出非空。
3. **Step 5.3 — 数据流审查**
- 验证坐席状态从 `store.agentOnline` / `store.canCallAgent` / `store.canReopen` → `computeCallAgentState` → `InputBar.callAgentState` → `callAgentBtnClass/Icon/Text/Title` 的数据流不断裂。
- 验证坐席动作从 `InputBar.handleCallAgentClick` → `store.shakeAgent/cancelQueue/reopenCurrentConversation` → 后端 API → store state → 响应式更新 InputBar 工具栏徽标。
4. **Step 5.4 — 测试覆盖审查**
- `InputBar.test.ts` PASS4 按钮 + 5 色徽标 + 6 态按钮 + handleCallAgentClick 路由)
- `inputBarCallAgentState.test.ts` PASS6 态计算 + v1.2y Bug 修复)
- `integrationZoneLogic.test.ts` PASS(按钮 class / 引导语 class / callAction 映射)
- `inputBarGuideText.test.ts` PASS4 种场景 → v1.3.5 后仅 2 场景生效)
- `QueueCapsule.test.ts` PASS36 用例)
- `conversation-v1.2.test.ts` PASSstore 4 个 action
5. **Step 5.5 — 浏览器实测**
- 登录 H5 → 打开新会话 → 确认 InputBar 工具栏 4 按钮(emoji / 文件 / 语音 / 坐席头像)正确显示
- 确认 IntegrationZone 容器**仅渲染** QueueCapsule + 引导语(无坐席按钮)
- 浏览器开发者工具:搜索 "call-agent-btn" DOM 节点应输出为空
- 浏览器控制台无 Vue 警告、无 prop 类型错误
**验收标准**
- [ ] REQ-E1:所有 grep 验证通过(详见 Step 5.1-5.3)。
- [ ] REQ-E2:所有 vitest 测试通过。
- [ ] REQ-E3:浏览器实测无视觉异常、控制台无报错。
- [ ] REQ-E4:架构师签发 IS_PASS: YES(写在本任务清单末尾)。
---
### 6.x 任务依赖图
```mermaid
graph TD
T01["T01: 恢复 InputBar.vue<br/>到 v1.2 状态<br/>(4 按钮 + 坐席头像)"]
T02["T02: 删除 IntegrationZone.vue<br/>坐席按钮"]
T03["T03: 清理 ChatPanel.vue<br/>@call-agent 监听器"]
T04["T04: 重写 InputBar.test.ts<br/>v1.3 = v1.2 契约测试"]
T05["T05: 全局一致性审查<br/>(IS_PASS: YES/NO)"]
T01 --> T02
T02 --> T03
T01 --> T04
T01 --> T05
T02 --> T05
T03 --> T05
T04 --> T05
classDef p0 fill:#fee,stroke:#c00,color:#000
classDef p1 fill:#efe,stroke:#0a0,color:#000
class T01,T02,T04,T05 p0
class T03 p1
```
**任务统计**:5 个任务(满足 ≤5 硬性上限)。P0 = 4 个,P1 = 1 个。
---
## 7. 依赖包列表
**无新增依赖**。所有依赖已在 v1.2 基线锁定。
```yaml
# package.json 锁定版本(无需变更)
dependencies:
vue: ^3.4.0 # Composition API + <script setup>
pinia: ^2.1.0 # store 单例
vant: ^4.8.0 # UI 组件库
@vueuse/core: ^10.0.0 # 工具 composables
socket.io-client: ^4.7.0 # WebSocket
axios: ^1.6.0 # HTTP
devDependencies:
vite: ^5.0.0
typescript: ^5.0.0
vitest: ^1.0.0
@vue/test-utils: ^2.4.0
vue-tsc: ^1.8.0
```
---
## 8. 共享知识(跨文件约定)
### 8.1 关键约定
1. **坐席入口唯一化**:v1.3 后 H5 端坐席入口**仅**为 InputBar 工具栏坐席头像按钮(图片头像 + 5 色徽标)。IntegrationZone 中的旧"人工坐席"按钮已**完全删除**(无 DOM 节点、无 emit、无样式)。
2. **6 态按钮位置**v1.3 = v1.2 形态,6 态按钮在 InputBar 工具栏**最右侧**emoji / 文件 / 语音 / **坐席**),**不是**在 IntegrationZone 容器中。
3. **数据流统一**:坐席按钮 6 态计算由 `InputBar.vue` 内部完成(基于 `inputBarCallAgentState.computeCallAgentState` + `storeToRefs(store)`),**不依赖** IntegrationZone 的 prop 透传。IntegrationZone 的 `callAgentState` prop 仅用于 QueueCapsule 内部降级判定。
4. **Action 调用路径**:坐席按钮直接调 `store.shakeAgent() / cancelQueue() / reopenCurrentConversation()`**不经 ChatPanel 中转**D2 决策)。`end` 态例外:实际由 ChatPanel 顶部"⏏"按钮走,因为 v1.2 P1 修复已让 InputBar 工具栏坐席按钮在 `serving` 态保持 disabled。
5. **helper 文件保留**`inputBarCallAgentState.ts` / `integrationZoneLogic.ts` / `inputBarGuideText.ts` 三个 helper 文件**完整保留**(Q4 暂搁),未来若整合区恢复坐席按钮可直接复用。
6. **事件冒泡规则**
- InputBar 坐席按钮 → **不 emit**,直接调 store
- QueueCapsule 内部按钮 → **直接调 store**`reopenCurrentConversation`+ 同步 emit`cancel-queue` / `reopen-conversation`)给 IntegrationZone 透传
- IntegrationZone → 仅**透传** QueueCapsule 的 emit`cancelQueue` / `endConversation` / `reopenConversation`)给 ChatPanel
### 8.2 跨文件函数契约
| 函数 | 文件 | 签名 | 消费者 |
|------|------|------|--------|
| `computeCallAgentState(ctx)` | `inputBarCallAgentState.ts:63` | `(ctx: CallAgentStateContext) => CallAgentState` | InputBar.vue、ChatPanel.vue |
| `computeCallAgentBtnText(state)` | `inputBarCallAgentState.ts:88` | `(state: CallAgentState) => string` | InputBar.vuev1.3 恢复) |
| `computeCallAgentBtnTitle(state, agentOnline)` | `inputBarCallAgentState.ts:99` | `(state, agentOnline) => string` | InputBar.vuev1.3 恢复) |
| `computeIntegrationBtnClass(state)` | `integrationZoneLogic.ts:129` | `(state) => Record<string, boolean>` | InputBar.vuev1.3 恢复,沿用 .call-agent-btn--* 命名) |
| `computeIntegrationGuideClass(text)` | `integrationZoneLogic.ts:144` | `(text) => Record<string, boolean>` | IntegrationZone.vue(保留) |
| `computeCallAction(state)` | `integrationZoneLogic.ts:160` | `(state) => EmitEvent \| 'none'` | InputBar.vuev1.3 恢复,按结果路由 store action |
| `computeGuideText(ctx)` | `inputBarGuideText.ts:67` | `(ctx) => string \| null` | ChatPanel.vue(透传给 IntegrationZone |
### 8.3 编码风格约束
- **不使用 Sass/Less**:所有样式为原生 CSS + `scoped` 属性。
- **不使用 TypeScript class**:所有组件用 `<script setup lang="ts">` 函数式写法。
- **不使用 Vuex**:所有 store 用 Pinia`defineStore` + `storeToRefs`)。
- **不使用 prop drilling**:跨组件状态通过 Pinia store 共享。
- **helper 文件范式**:纯函数 + 显式 ctx 入参,无 Vue/DOM 依赖,单测用 `pnpm vitest run` 跑通。
### 8.4 测试范式(沿用 inputBarCallAgentState.ts 范式)
- **不直接 mount SFC**:避免 `@vue/test-utils` 依赖
- **纯函数复刻 + 断言**:在测试文件中以纯函数复刻组件 computed 的语义,断言纯函数输出
- **mock store / composables**:使用 `vi.fn()` + `vi.useFakeTimers()` 隔离副作用
---
## 9. 待明确事项
### Q3 细化(仅 call-agent 清理 vs 全 4 个清理)
**当前架构师建议(已写入 T03)**:**保守范围**,仅清理 `@call-agent` + `handleDirectCall`,保留其他 3 个监听器(`@cancel-queue` / `@end-conversation` / `@reopen-conversation`)。
**理由**
1. QueueCapsule 内部仍直接调 `store.reopenCurrentConversation()`,但 QueueCapsule 的 `reopen-conversation` emit 仍被 IntegrationZone 透传给 ChatPanel(虽然 ChatPanel 的 `handleReopenConversation` 调用是幂等的)。
2. QueueCapsule 的 `cancel-queue` emit 仍被 IntegrationZone 透传给 ChatPanel。
3. IntegrationZone 容器未来可能新增"end 态按钮"(产品 P2 提案),提前保留 `end-conversation` 监听器可避免未来重新添加。
**请主理人确认**:是否接受"保守范围"清理,还是一次性清掉全部 4 个无效监听器(同时移除 IntegrationZone 的 3 个 emit 透传)?
**风险评估**
- 保守范围:低风险(保留的 3 个监听器仍由 QueueCapsule 触发,是有效代码)。
- 全清理:中风险(需要确认 QueueCapsule 内部是否完全独立于 ChatPanel 的 3 个 handler,否则可能漏改)。
### 其他无
其余 4 个 PRD 待确认问题(Q1/Q2/Q4)主理人已答:
- **Q1**:恢复 InputBar.vue 到 v1.2T01 已覆盖)
- **Q2**:不新建 IntegrationZone.test.ts(沿用现有 integrationZoneLogic.test.ts
- **Q4**:helper 文件保留(按主理人指示,6 个月后评估)
---
## 评审签字
- [ ] 架构师:高见远
- [ ] 产品经理:许清楚
- [ ] 测试负责人:(待评审)
- [ ] 主理人:Duckula