Files
wecom_it_smart_desk/docs/02-技术文档/01-架构设计/架构-REQ-会话-001-工具栏统一设计-v1.3.md
T
Simon facc04aa65 chore: docs 结构整改 + compose 双目录对齐(合并重建提交)
本提交为 .git 对象库损坏后的重建提交,内容等价于原先三个本地提交
(5e2fd4c2 / 57a53c98 / 5d7e1873)的累积结果,未做任何额外改动。

一、docs 结构整改(整改 #14)
根因:重构时新结构为 untracked 文件,执行 git stash(未带 -u)未纳入,
随后 git reset 拉回 HEAD 旧 tracked 树,导致旧树复活、新旧两棵目录
树并存于 docs/,共 791 文件、双分类体系冲突。

修复动作:
- b2 同名异主题文件改名迁移保全 9 个
- C 类 39 个孤立文件按主题正确归类
- A/B1 类 222 个重复文件删除(新结构已有内容副本)
- 9 个旧独有空目录删除
- 270 处内部引用按 verified 映射改写
- 整改记录 #14 登记于 04-运维文档/部署运维

结果:docs 791 → 569 文件,顶层仅规范 8 类 + 治理文件,单树恢复。
残留:约 20 处指向从未存在文件的陈旧死链,归入独立文档卫生任务。

二、compose 双目录对齐(消除踩坑 A)
- docker-compose.yml:nginx 前端挂载全部由根目录 frontend-*/dist
  改为 src/frontend-*/dist(h5 / agent / admin / terminal)
- docker-compose.dev.yml:dev 服务 build context 与卷同步改 src/
- 效果:本地 docker compose up 不再把根目录 stale dist 挂回,
  与线上一致,分叉隐患消除(已 docker compose config 校验通过)

防复发铁律:
- 重构须提交;仓库修复须 git stash -u 或先 commit
- 新结构须 git add 并提交,避免再次 untracked 复活
- H5 改动只动 src/frontend-h5/,禁改根目录遗留 frontend-*/
2026-08-07 22:31:32 +08:00

747 lines
40 KiB
Markdown
Raw 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.
# 工具栏统一设计 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