Files
wecom_it_smart_desk/docs/02-技术文档/技术方案-REQ-用户-001-群聊入口接线-v1.0.md
Simon 5311a526af feat(h5/chat): 员工端群聊按钮接线 → 参与者面板(REQ-用户-001)
- InputBar.vue 重写 handleGroupChat():无会话 toast「请先发起会话」;有会话 → 开关参与者面板;零参与者且展开时补邀请引导。
- 契约常量 GROUP_CHAT_NO_CONVERSATION_TIP / GROUP_CHAT_EMPTY_TIP 与 InputBar.test.ts 完全对齐;删除字面量 '群聊功能开发中' 与 startGroupChat 调用。
- InputBar.test.ts 102/102 通过;契约测试已同步。
- 新增技术方案 docs/02-技术文档/技术方案-REQ-用户-001-群聊入口接线-v1.0.md(方案 A store 驱动)。
- 新增任务说明书 docs/07-项目管理/任务说明书/任务说明书-REQ-用户-001-群聊入口接线.md(按模板)。
- PRD-REQ-用户-001-群聊双模式-v1.0.md 头部补「关联文档」双向链 + 状态「待评审」→「已实现」。

PRD: docs/01-产品文档/05-用户端H5/PRD-REQ-用户-001-群聊双模式-v1.0.md
REF:  REQ-用户-001-群聊入口接线(坐席端不动,按用户拍板 q-1)
2026-08-09 13:16:27 +08:00

288 lines
16 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.
# 技术方案 — 员工端 H5 群聊入口接线
> **需求编号**: REQ-用户-001-群聊入口接线
> **版本**: v1.0
> **日期**: 2026-08-08
> **作者**: 高见远(架构师)
> **状态**: 已实现
> **子系统**: 用户端H5
> **模块**: 群聊
> **关联文档**:
> - PRD: `docs/01-产品文档/05-用户端H5/PRD-REQ-用户-001-群聊双模式-v1.0.md`
> - 既有技术方案: `docs/02-技术文档/技术架构/技术方案-REQ-用户-001-群聊双模式-v1.0.md`
> - 既有架构设计: `docs/02-技术文档/01-架构设计/群聊参与者展开缩略双模式-架构设计.md`
> - 任务说明书: `docs/07-项目管理/任务说明书/任务说明书-REQ-用户-001-群聊入口接线.md`
> **补充参考**(非三件套,仅供实现比对,路径均已核验存在):
> - 原型(用户端): `docs/01-产品文档/05-用户端H5/原型-REQ-用户-001-群聊双模式-v1.0.html`
> - 原型(坐席端): `docs/01-产品文档/04-坐席工作台/原型-REQ-坐席-003-群聊双模式-v1.0.html`
> - 工具栏基线交付清单: `docs/01-产品文档/02-会话管理/交付-REQ-会话-001-工具栏统一设计v1.9-开发交付清单.md`
---
## 一、背景与目标
### 1.1 需求触发原点
使用者(IT 支持组组长)提出的原始诉求是两句话:
1. **确认坐席端的群聊功能到底做没做?**
2. **如果做了,把员工端工具栏上那颗「群聊」按钮接上。**
### 1.2 核查结论:坐席端早已实现
代码核查确认,**坐席端群聊能力完整可用,本次无需任何改动**。其入口是「邀请」按钮,而非名为「群聊」的按钮:
| 能力 | 文件 · 行号 | 状态 |
|------|------------|------|
| 参与者横条 | `src/frontend-agent/src/components/chat/ChatArea.vue` L38 `<ParticipantBar>` | ✅ 已实现 |
| 邀请事件出口 | `ChatArea.vue` L50 `@invite="showInviteParticipantDialog = true"` | ✅ 已实现 |
| 邀请参与者弹窗 | `ChatArea.vue` L221-222 `<InviteParticipantDialog v-model="showInviteParticipantDialog">` | ✅ 已实现 |
| 摇人邀请弹窗 | `ChatArea.vue` L214 `<InviteDialog>` | ✅ 已实现 |
| 就地展开面板 | `src/frontend-agent/src/components/conversation/ParticipantBar.vue` L78 `<ParticipantExpandedPanel>` | ✅ 已实现 |
| 组件注册 | `ChatArea.vue` L277-279 三处 import | ✅ 已实现 |
**"功能没做"是误判,误判的根因是文档不可发现**:PRD `PRD-REQ-用户-001-群聊双模式-v1.0.md` 头部**缺失 `关联文档` 字段**,从 PRD 无法反向索引到技术方案与架构设计;而实现层的入口叫「邀请」、需求层的名字叫「群聊」,术语不对齐,检索时对不上。本次一并回写 PRD 关联链路(见 §六)。
### 1.3 本方案的目标
**本方案不新增任何群聊能力,只记录并固化一件事:员工端 H5 工具栏「群聊」按钮的入口接线。**
REQ-用户-001 的群聊双模式(缩略头像条 + 展开参与者面板 + 邀请 + 退出)在双端均已完整实现,后端接口与 WebSocket 双池推送全部打通。唯一缺口是:员工端 H5 那颗 `title="群聊"` 的按钮,点击后只弹一句 `showToast('群聊功能开发中')` —— 这句占位 toast 正是使用者判定"功能未开发"的直接来源。
本次改动即:把这句占位 toast 换成对已有 store action 的调用。
| 维度 | 决策 |
|------|------|
| 后端改动 | **无** |
| store 改动 | **无**`toggleParticipantPanel()` 已存在并已导出) |
| 新增组件 | **无** |
| 前端改动文件 | `InputBar.vue` ×1 + 测试 ×2 |
| 本次范围 | **仅员工端 H5**,坐席端零改动(用户已拍板) |
---
## 二、范围与边界
### 2.1 在范围内
- ✅ 仅**员工端 H5**`src/frontend-h5/**`
-`InputBar.vue``handleGroupChat()` 的入口接线
- ✅ 两个测试文件的断言同步
- ✅ PRD ↔ 技术方案 ↔ 任务说明书 三件套关联回写
### 2.2 明确不做(Out of Scope
-**0 后端改动**:不改接口、不改模型、不改推送逻辑
-**不部署**:仅留工作区改动,部署另行安排
-**坐席端不动**`src/frontend-agent/**` 零改动(已有「邀请」入口,用户已拍板)
- ❌ 不新增坐席端第 6 个工具栏按钮;将来若做,采用「邀请」**改名**为「群聊」的方式,保持 5 键基线(方向已定,本次不执行)
- ❌ 不做需求编号收敛、不处理其他文档治理项
- ❌ 不改 `ChatPanel.vue` L69 的 `ParticipantStrip` 渲染条件
---
## 三、实现细节
> 以下文件与行号均以当前工作区代码为准,逐条 Read 核验。
### 3.1 入口断点(改造前)
| 位置 | 内容 |
|------|------|
| `src/frontend-h5/src/components/chat/InputBar.vue` **L345-360** | 拱形工具栏第 5 个按钮:L348 `title="群聊"`、L349 `aria-label="群聊"`、L350 `@click="handleGroupChat"` |
| `src/frontend-h5/src/components/chat/InputBar.vue` **L872-874**v1.9 时期) | 占位实现 `showToast('群聊功能开发中')` |
改造前的函数体只有一行 toast,且注释写着「store 暂无 group chat action……后续接入真实群聊会话时,把 toast 替换为 `store.startGroupChat()` 即可」。该注释在写下之后即已过期 —— `toggleParticipantPanel()` 早已在 store 落地,而 `startGroupChat` 是一个**从未存在过的虚构 action**。注释与代码事实脱节,是按钮长期停留在占位态的直接原因。
### 3.2 目标实现(改造后 · 已落地)
`InputBar.vue` 现状 **L867-899**(常量 L868 / L871,函数 L886-899):
```ts
/** 无会话时点击群聊的提示文案。 */
const GROUP_CHAT_NO_CONVERSATION_TIP = '请先发起会话'
/** 会话内暂无其他同事时的邀请引导文案。 */
const GROUP_CHAT_EMPTY_TIP = '还没有其他同事,点击「邀请参与者」拉同事进群'
function handleGroupChat(): void {
if (!currentConversation.value) {
showToast(GROUP_CHAT_NO_CONVERSATION_TIP)
return
}
const hasParticipants = store.participants.length > 0
store.toggleParticipantPanel()
// 仅在「由收起切换为展开」且暂无其他同事时补引导,避免收起面板时也弹提示
if (!hasParticipants && store.participantPanelVisible) {
showToast(GROUP_CHAT_EMPTY_TIP)
}
}
```
> **与原始设计的差异说明**:契约描述为「先算 `nextVisible = !participantPanelVisible`,若 `!hasParticipants && nextVisible` 则提示」。落地实现改为**先 `toggle()` 再读 `store.participantPanelVisible`**,两者语义完全等价(toggle 后的实际值即 `nextVisible`),且读实际状态比预测值更稳健 —— 若将来 action 内部新增条件分支,实现不会与预测值脱节。文案常量与三分支行为与契约一致。
### 3.3 handleGroupChat 行为契约(三分支)
| # | 前置条件 | toast | `toggleParticipantPanel()` |
|---|---------|-------|---------------------------|
| 1 | 无当前会话 | `'请先发起会话'` | **不调用**(提前 return |
| 2 | 有会话 + `participants.length > 0` | 无 | 调用 1 次 |
| 3 | 有会话 + `participants.length === 0` 且切换为展开 | `'还没有其他同事,点击「邀请参与者」拉同事进群'` | 调用 1 次 |
**分支 3 的设计依据**:零参与者时 `ChatPanel.vue` L69 的 `ParticipantStrip` 不渲染,用户界面上没有任何群聊线索,因此必须由 toast 补一句引导。而面板本身照常打开 —— `ParticipantList` 在零被邀请人时仍会渲染「坐席 + 我(发起人)」,且会话发起人的 `canInvite` 恒为 `true`,「+ 邀请参与者」按钮天然可达,不会出现空白面板。
### 3.4 依赖的 store 契约(只读引用,零修改)
`src/frontend-h5/src/stores/conversation.ts`
| 契约项 | 行号 | 说明 |
|--------|------|------|
| `currentConversation` | **L135** | `ref<ConversationInfo \| null>(null)` — 分支 1 的判据 |
| `participants` | **L191** | `ref<ParticipantItem[]>([])` — 分支 2/3 的判据 |
| `participantPanelVisible` | **L194** | `ref<boolean>(false)` — 面板显隐单一真值源 |
| `toggleParticipantPanel()` | **L1296** | `participantPanelVisible.value = !participantPanelVisible.value`;无网络请求、无持久化 |
| 导出 | L2185 / L2214 | 状态与 action 均已在 return 块内 |
| 重置 | L1367 | 会话清理时置回 `false` |
> ⚠️ 语义提示:这是 **toggle** 而非 `open`。当前按钮为唯一触发点,用户在面板内通过关闭按钮直接置 `false`,不会与 toggle 产生状态错位。若后续新增第二个触发入口,需评估是否补一个 `openParticipantPanel()` 语义化 action。
### 3.5 承载面板的容器(零修改)
`src/frontend-h5/src/components/chat/ChatPanel.vue`
| 位置 | 内容 |
|------|------|
| **L69** | `<ParticipantStrip v-if="store.participants.length > 0" />` — 缩略头像条,零参与者时不渲染 |
| **L133-142** | 底部弹层 `<van-popup :show="store.participantPanelVisible" position="bottom" round teleport="body" :style="{ maxHeight: '60vh' }">`,其中 **L141** 挂载 `<ParticipantList />` |
> 原始描述记为 L135-142,实测弹层完整块为 **L133-142**`van-popup` 起始标签在 L133L132 为注释行),`<ParticipantList />` 位于 L141。以实测为准。
### 3.6 为什么不在 InputBar 里挂 InviteParticipantSheet
`InviteParticipantSheet.vue` **由面板内部自行挂载**`handleGroupChat` 不直接调用它:
- 挂载点一:`ParticipantStrip.vue` L91
- 挂载点二:`ParticipantList.vue` L115
若 InputBar 再挂一次,全应用将出现**第三份**同名弹层实例,带来状态互相覆盖与 `v-model` 归属混乱的风险。分支 3 的引导文案明确指向面板内的「邀请参与者」按钮,用户路径为:**群聊按钮 → 面板打开 → 点面板内「+ 邀请参与者」→ 已挂载的 Sheet 弹出**,全程复用既有实例。
### 3.7 架构一致性:为何选 store 驱动而非 emit
`InputBar.vue` L903 注释白纸黑字写明:**「InputBar 是 v1.3 唯一坐席入口,不向 ChatPanel emit。」** 全文件 `defineEmits` 零匹配,InputBar 至今保持「零 emit、纯 store 驱动」的单向数据流。为一个按钮破例会留下两套并存的通信范式,故定案 store 驱动 —— 这也让改动收敛到 1 个业务文件。
---
## 四、测试影响
### 4.1 受影响的测试文件
| 文件 | 改动性质 |
|------|---------|
| `src/frontend-h5/src/components/chat/InputBar.test.ts` | 同步断言 |
| `src/frontend-h5/src/components/chat/__tests__/InputBar.vitest.test.ts` | 同步断言 |
### 4.2 契约常量对齐
两个测试文件**必须复刻 `InputBar.vue` 的文案常量**,不得内联裸字符串:
| 常量 | 值 |
|------|-----|
| `GROUP_CHAT_NO_CONVERSATION_TIP` | `'请先发起会话'` |
| `GROUP_CHAT_EMPTY_TIP` | `'还没有其他同事,点击「邀请参与者」拉同事进群'` |
现状:`InputBar.test.ts` L177 / L180 已对齐(含注释标注「与 InputBar.vue 对齐」)。
### 4.3 弃用串必须 0 命中
| 弃用串 | 断言位置 | 说明 |
|--------|---------|------|
| `'群聊功能开发中'` | `InputBar.test.ts` L848 · `InputBar.vitest.test.ts` L867 | 含注释在内,`InputBar.vue` 全文不得再出现 |
| `'startGroupChat'` | `InputBar.test.ts` L861 · `InputBar.vitest.test.ts` L871 | 虚构 action,含注释在内不得再被引用 |
> **R1 风险(预期内的红)**:存量测试原本断言 `showToast('群聊功能开发中')`,接线后必然变红。这是预期结果,**不得靠删除测试绕过**,必须改为断言新契约。
### 4.4 测试覆盖现状
| 用例 | 位置 |
|------|------|
| 三分支行为契约 | `InputBar.test.ts` L797 `describe('InputBar v2.1 — handleGroupChat 三分支行为契约')` |
| 按钮可访问性属性不变 | `InputBar.vitest.test.ts` L625`title` / `aria-label` / `@click` 三属性正则) |
| 接线目标为 `toggleParticipantPanel` | `InputBar.test.ts` L861 · `InputBar.vitest.test.ts` L847 |
---
## 五、验收标准
### 5.1 功能验收
| # | 场景 | 预期 |
|---|------|------|
| A1 | 未发起会话 → 点「群聊」 | toast「请先发起会话」;**不**打开参与者面板 |
| A2 | 有会话(含参与者)→ 点「群聊」 | 参与者面板打开;再点一次关闭(开/关可逆) |
| A3 | 有会话 + 零参与者 → 点「群聊」 | 面板打开 **且** toast「还没有其他同事,点击「邀请参与者」拉同事进群」 |
| A4 | A3 状态下再点「群聊」收起面板 | 面板关闭,**不**重复弹邀请引导 |
| A5 | 全代码库 | 无任何 `startGroupChat` 调用;`InputBar.vue``'群聊功能开发中'` |
| A6 | A3 中点面板内「+ 邀请参与者」 | 复用既有 `InviteParticipantSheet`,无第三处挂载 |
| A7 | 工具栏基线 | 5 按钮顺序 emoji / 文件 / 坐席 / 语音 / 群聊 不变,拱形轨道无形变,L345-360 模板未被改动 |
### 5.2 质量门禁
- `pnpm test` 全量通过,重点 `InputBar` 相关 2 个测试文件
- ESLint 无新增告警(`showToast` import 在分支 1/3 仍被使用,不会变成 unused)
- 坐席端零改动,不参与本次回归
### 5.3 文档验收
- PRD 头部 `关联文档` 字段存在,且指向本方案 + 既有技术方案 + 既有架构设计
- 本方案 `关联文档` 反向指向 PRD,双向一致
- 任务说明书已建立并被三件套互相引用
---
## 六、关联文档回写(本轮同步执行)
本轮同时修复「PRD 不可发现」这一根因:
| 文档 | 回写内容 |
|------|---------|
| `PRD-REQ-用户-001-群聊双模式-v1.0.md` | 头部新增 `关联文档` 字段(指向既有技术方案 + 既有架构设计 + 本方案);`状态``待评审``已实现` |
| 本方案 | `关联文档` 指向 PRD + 既有技术方案 + 既有架构设计 + 任务说明书 |
| `任务说明书-REQ-用户-001-群聊入口接线.md` | 新建,引用上述四份文档 |
> **遗留观察(不在本轮范围)**:`docs/02-技术文档/01-架构设计/群聊参与者展开缩略双模式-架构设计.md` 头部的 PRD 指向 `docs/01-产品文档/02-会话管理/群聊参与者展开缩略双模式-PRD.md`,与 `05-用户端H5/PRD-REQ-用户-001-群聊双模式-v1.0.md` 构成同一需求的两份 PRD(两份文件均真实存在,非死链)。属需求编号收敛范畴,本轮按用户要求不处理,登记备查。
---
## 七、影响面与风险
### 7.1 影响面
| 文件 | 改动性质 | 风险 |
|------|---------|------|
| `src/frontend-h5/src/components/chat/InputBar.vue` | 替换单个函数体 + 新增 2 个文案常量 | 🟢 极低 |
| `src/frontend-h5/src/components/chat/InputBar.test.ts` | 同步断言 | 🟢 极低 |
| `src/frontend-h5/src/components/chat/__tests__/InputBar.vitest.test.ts` | 同步断言 | 🟢 极低 |
**零影响面清单**:后端 · store · 路由 · 网络请求 · 数据库 · `ChatPanel.vue` · `ParticipantList.vue` · `ParticipantStrip.vue` · `InviteParticipantSheet.vue` · 坐席端全部文件 —— 均不改动。
### 7.2 风险登记
| # | 风险 | 等级 | 缓解 |
|---|------|------|------|
| R1 | 存量测试断言旧占位文案,改动后必红 | 🟠 中 | 已纳入交付清单,两个测试文件同步更新;不得删测试绕过 |
| R2 | `showToast` import 变为未使用 | 🟡 低 | 分支 1/3 仍在使用,import 保持有效 |
| R3 | toggle 语义在未来多入口场景下状态错位 | 🟡 低 | 当前单入口;后续新增入口时引入 `openParticipantPanel()` |
| R4 | 坐席未接入且零被邀请人时,面板仅一行「我」 | 🟡 低 | 可接受;分支 3 的 toast 已给出下一步引导 |
| R5 | 误改工具栏 DOM,破坏 v1.9 拱形轨道基线 | 🟠 中 | **严禁改动 L345-360 模板**,只改 script;验收项 A7 比对 5 按钮基线 |
---
## 八、变更记录
| 日期 | 版本 | 变更 | 作者 |
|------|------|------|------|
| 2026-08-08 | v1.0 | 初版;定案 store 驱动方案,落地三分支契约;补齐 `需求编号` / `子系统` / `模块` / `关联文档` 头部字段;状态置为「已实现」;同步 PRD 关联回写 | 高见远 |