- 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)
16 KiB
技术方案 — 员工端 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 核查结论:坐席端早已实现
代码核查确认,坐席端群聊能力完整可用,本次无需任何改动。其入口是「邀请」按钮,而非名为「群聊」的按钮:
| 能力 | 文件 · 行号 | 状态 |
|---|---|---|
| 参与者横条 | 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.vueL69 的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):
/** 无会话时点击群聊的提示文案。 */
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起始标签在 L133,L132 为注释行),<ParticipantList />位于 L141。以实测为准。
3.6 为什么不在 InputBar 里挂 InviteParticipantSheet
InviteParticipantSheet.vue 由面板内部自行挂载,handleGroupChat 不直接调用它:
- 挂载点一:
ParticipantStrip.vueL91 - 挂载点二:
ParticipantList.vueL115
若 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 无新增告警(
showToastimport 在分支 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 关联回写 |
高见远 |