Files
wecom_it_smart_desk/docs/01-产品文档/04-坐席工作台/PRD-REQ-坐席-012-会话条目操作菜单-v1.0.md
T

508 lines
36 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.
# PRD — 坐席端会话条目操作菜单(左栏三点菜单 + 右键菜单)
> **REQ编号**: REQ-坐席-012
> **版本**: v1.0
> **状态**: 🟡 待评审 — 交互形态与中栏取舍已由需求方拍板,菜单矩阵与组件边界待技术评审
> **优先级**: P1
> **日期**: 2026-08-09
> **作者**: 许清楚(产品经理)
> **需求来源**: 宋献(IT 支持组组长)口述需求 4 条
> **技术依据**: `docs/02-技术文档/技术架构/技术核查-坐席会话条目操作菜单-v1.0.md`(高见远,2026-08-09
> **配套原型**: `原型-REQ-坐席-000-坐席工作台-v1.7.html`
> **关联**: PRD-REQ-坐席-011(统一工作队列重构)、REQ-坐席-004、REQ-坐席-009、REQ-坐席-010
> **性质**: 增量 PRD。仅描述本次新增/变更,不复述既有功能。
---
## 1. 背景
### 1.1 需求原文与映射
| # | 宋献原始需求 | 本 PRD 对应 |
|---|---|---|
| 1 | 确认现有坐席架构,控件能否在「选中对象-右键调出菜单」模式实现 | §3.1 D-3、§4.3 |
| 2 | 左栏每个会话表单右边增加菜单按钮(三个点),点击弹出接单/置顶/代办/转接 | §4.1、§4.2、§5 |
| 3 | 上述完成后,去除中栏上方「接单/置顶/代办/转接」4 个按钮 | §3.1 D-2、§6 |
| 4 | 先基于最新原型更新细致原型图,与用户确认后再改代码 | §3.1 D-1、§10 |
### 1.2 🔴 前置事实修正(必须在评审开场说明)
需求提出时的前置认知是「V1.0–V1.6 原型中这 4 个按钮丢失了」。经架构师代码核查,**该认知需要修正两处**:
| 认知 | 事实 |
|---|---|
| 「按钮丢失了」 | ❌ **4 个按钮在真实运行代码中从未丢失**。接单 `UserInfoBar.vue:116-124`、置顶 `:127-133`、代办 `:136-142`、转接 `:145-164`,且四者均为**服务端真闭环**(store → API → 后端端点,非 Mock) |
| 「从 v1.5 开始丢失」 | ❌ 丢失只发生在**原型 HTML 侧**,且起始版本是 **v1.4**。v1.0–v1.3 均含 4 按钮(关键词计数实证:接单 11-12 次 / 置顶 3 次 / 代办 1 次 / 转接 2 次),v1.4 起归零 |
**根因**v1.4 起原型**换了血统**。v1.0v1.3 是 24002700 行的全量高保真原型;v1.4 是为论证「B 方案三栏分栏比例」与「排查步骤全屏流程图」而**新画的专题示意图**(961 行),并非 v1.3 的增量演进。v1.5/v1.6 沿此专题线继续演进。
**后果(这是本次必须先做字段回填的原因)**v1.6 的左栏 `.conv-item` 只有「头像/姓名/时间/摘要/未读数」5 个字段,而真实代码 `ConversationItem.vue` 的条目含:
| 元素 | 真实代码 | v1.6 原型 |
|---|---|---|
| 置顶 📌 / 代办 📋 图标 | ✅ `:50` `:52` | ❌ |
| tag-badgeVIP/招手/需介入/情绪/坐席名/待确认) | ✅ 6 类 `:56-82` | ❌ |
| 优先级图标(⛔👥⭐🔁) | ✅ 最多 4 个 `:84-95` | ❌ |
| 紧急度星级 | ✅ 5 星 `:107-114` | ❌ |
| 接手 / 退出 link 按钮 | ✅ `:116-136` | ❌ |
| 处理对象缩略头像 | ✅ `:141-148` | ❌ |
**若直接在 v1.6 上加三点按钮,会得出「条目右侧有大片空槽」的错误结论**——真实条目右侧已被 `.conv-target-avatar` 占满(`section !== 'history'` 时恒显示)。这是 §3.1 D-1 选择 v1.3 而非 v1.6 作为 v1.7 基线的直接原因。
### 1.3 本次真正的新增工作量在哪里
架构核查指出:现有 4 个按钮**几乎没有基于会话归属的可见性控制**——置顶/代办在中栏任何状态下都常驻且不禁用。这在中栏可接受(中栏只显示当前打开的会话,多为自己的);**但一旦下沉到左栏,左栏同时呈现「我的会话 / 同事会话 / 历史会话」三个分区**(`ConversationList.vue:43-76`),菜单项必须新增**归属维度**的可见性规则。
> **本次改造的产品设计核心不是"控件搬家",而是补齐一张此前不存在的「状态 × 归属 × 分区」菜单项可见性矩阵**(§5)。控件本身是低成本工作。
---
## 2. 产品目标
| # | 目标 | 衡量方式 |
|---|---|---|
| G-1 | 把会话的**列表管理动作**(接单/置顶/代办)下沉到动作对象所在处,消除「操作在中栏、效果在左栏」的焦点跳跃 | 置顶/代办操作后无需视线迁移即可看到条目重排 |
| G-2 | 在不牺牲对话过程效率的前提下精简中栏顶栏 | 中栏操作控件由 6 个降至 3 个(转接/摇人/结单),且**转接路径不退化**(保持 3 步) |
| G-3 | 菜单壳一次抽对,避免 PRD-011 Phase 2 与虚拟滚动落地时二次重写 | 新增 `kind`(审批/工单)时只加一个菜单项工厂,不改菜单壳 |
## 2.1 用户故事
| # | 故事 |
|---|---|
| US-1 | 作为坐席,我想在左栏直接对某条会话置顶/代办,这样我不必先打开它、再去中栏操作、再回来看列表变化 |
| US-2 | 作为坐席,我想在扫视排队会话时就地接单,这样我不必"先点开、再接单"两步走 |
| US-3 | 作为熟练坐席,我想右键会话条目直接出菜单,这样我不必先 hover 找到三点按钮 |
| US-4 | 作为坐席,我不想在同事的会话上看到「置顶」——点了会 403 报错,还不给我任何提示 |
| US-5 | 作为坐席,我在中栏读完员工描述判断"这不归我管"时,希望能就地转接,而不是回左栏重新定位这条会话 |
---
## 3. 已锁定决策
### 3.1 需求方已拍板项(D-1 ~ D-6
| # | 决策项 | 结论 | 依据 |
|---|---|---|---|
| **D-1** | 原型基线 | **以 v1.3 为基线复制为 v1.7**(保留高保真左/中/右三栏),再增量叠加 v1.4~v1.6 演进特征(B 方案分栏比例、排查步骤全屏流程图、智能推荐竖排、工具栏重组、Emoji 图标统一)。**不以 v1.6 为基线** | §1.2;架构报告 §1.4 / R-2 |
| **D-2** | 中栏按钮处理 | **删除「接单/置顶/代办」3 个,保留「转接」**。转接与既有的「摇人」「结单」构成语义一致的**对话过程动作组**(转接=交出去 / 摇人=拉进来 / 结单=做完了) | §6;架构报告 §5.1 |
| **D-3** | 交互形态 | **三点按钮为主入口 + 右键为快捷方式,两者并存**。二者共用同一份菜单项定义与 handler | §4.3;架构报告 §2.3 选项 3 |
| **D-4** | 菜单实例架构 | **菜单为单例**,挂在 `ConversationList` 层级,**不挂在条目内部** | §7;架构报告 §2.4-5 |
| **D-5** | 三点按钮落位 | **替换** `.conv-target-avatar`(同栅格位淡入淡出),非覆盖、非并列 | §4.1;架构报告 §4.3 |
| **D-6** | 菜单弹层定位 | `placement="bottom-end"`,宽 140px,视口下缘不足时翻转 `top-end`**DOM 必须 `position:fixed` 或 Teleport 到 body** | §4.2;架构报告 §1.3.4 |
### 3.2 D-2 的完整论证(为什么不是 4 个全删)
需求原文是「去除中栏上方 4 个按钮」。经操作路径推演,**4 个一刀切全删会造成分档不同的退化**:
| 操作 | 当前路径 | 全删后路径 | 退化判定 |
|---|---|---|---|
| 接单 | 中栏 1 步 | 左栏 定位→hover→三点→接单 | ⚪ **不退化,删除是净收益**。接单发生在打开会话**之前/之时**,左栏本就是接单的自然场所(Zendesk / ServiceNow 同构)。且中栏该按钮在 `status !== 'queued'` 时恒为 disabled 的「已接单」,长期占位不可点 |
| 置顶 | 中栏 1 步 | 左栏 4 步 | 🟡 **轻微退化,可接受**。置顶是**列表组织行为**,其效果(条目上浮)只在左栏可见。操作与反馈同处一栏,语义反而更内聚 |
| 代办 | 中栏 1 步 | 左栏 4 步 | 🟡 同上 |
| **转接** | `点转接 → 选坐席 → 确认` = **3 步** | `视线移左栏 → 定位当前条目 → hover → 点三点 → 点转接 → 选坐席 → 确认` = **6+ 步** | 🔴 **显著退化,不接受** |
**转接不可下放的三条理由**
1. **触发时机根本不同**。置顶/代办是坐席**扫视队列时**的列表管理动作;转接是坐席**读完员工描述、判断"这不归我管"那一刻**的对话过程动作——此时注意力与鼠标都在中栏。要求回左栏是**注意力焦点的强制迁移**。
2. **"定位当前条目"本身有成本**。左栏三分区、可滚动,且条目会因置顶/紧急度**动态重排**,当前会话未必在视口内。这一步不是零成本的。
3. **删了转接反而制造不一致**。中栏保留「摇人」「结单」是既定事实。若转接被赶到左栏,结果是**同为对话过程中的协作类动作,入口被劈成两处**——这比"全部在中栏"或"全部在左栏"都差。
**同时,左栏三点菜单仍提供「转接」**。两个入口共用同一 handler 与同一坐席选择 Dialog,符合"高频动作允许多入口"原则,不构成语义分裂。
> **给需求方的备选(如坚持"顶栏太挤"是主要动机)**:可将 3 个被删按钮收进中栏自己的「⋯」总菜单,视觉上顶栏只剩「摇人 / 结单 / ⋯」,功能零退化。此形态**未在 v1.7 中作为主方案呈现**,如需要请在评审时提出。
---
## 4. 交互设计
### 4.1 三点按钮(主入口)
| 项 | 规格 | 依据 |
|---|---|---|
| **位置** | `.conversation-item` 最右侧,**替换** `.conv-target-avatar` 所在栅格位 | D-5 |
| **切换方式** | 同一栅格位内:缩略头像 `opacity:0` 淡出、三点 `opacity:1` 淡入(`transition ≈ 140ms`)。**不改变布局盒模型,无抖动** | 架构报告 R-7 |
| **命中区** | 24 × 24 px;图标 `MoreFilled``@element-plus/icons-vue@^2.3.0` 已装)1416px | D-5 |
| **默认态** | 隐藏(`opacity:0; pointer-events:none` | — |
| **显示触发** | `.conversation-item:hover` `.is-menu-open` 按钮 `:focus-visible` | 保证键盘可达 |
| **菜单打开期间** | 按钮**强制常显**,条目加 `.is-menu-open` 类(背景保持 hover 态),避免"鼠标移出→按钮消失→菜单悬空"的观感断裂 | 架构报告 §2.3 选项 2 风险项 |
| **历史分区** | `section === 'history'` 本无缩略头像,三点直接占该位(可常显淡色 `opacity:.35`hover 加深) | D-5 |
| **事件** | 必须 `@click.stop` —— 根节点 `@click="$emit('click')"``ConversationItem.vue:20`,不阻断会连带切换会话。项目内已有正确先例(`:122` `:133``@click.stop` | 架构报告 §2.4-4 |
**为什么不选"覆盖"或"并列"**
- 覆盖(absolute 盖在缩略头像上):hover 时缩略头像被遮挡,信息丢失且视觉脏;
- 并列(插到缩略头像右侧):再吃掉 ~24px,`.conversation-info` 从 ~132px 压到 ~108px,第一行 6 类 tag + 4 个优先级图标严重挤压。左栏条目内部可用宽度仅 ≈ 228px260 margin 12 padding 20),没有余量。
### 4.2 菜单弹层
| 项 | 规格 |
|---|---|
| 宽度 | 140px(4 项中文两字 + 图标;不得超过侧栏 260px) |
| 定位 | `placement="bottom-end"`(右对齐向下) |
| 翻转 | 视口下缘空间不足时自动翻转 `top-end`;右缘不足时自动贴边 |
| **DOM 归属** | 🔴 **必须 `position:fixed` + 视口坐标,或 Teleport 到 body**。**禁止**以 `position:absolute` 挂在 `.conversation-item` 内部 |
| 层级 | 使用 EP popper 默认 z-index2000+ 自增)或新引入的 `--z-dropdown: 2000` token。**不得沿用 `MessageItem.vue` 的 1000**(低于 EP popper,会被 dropdown/messagebox 盖住) |
| 关闭时机 | 点击菜单外 ∪ Esc ∪ 列表滚动 ∪ 窗口 resize ∪ 切换会话 ∪ 在另一条目再次右键 |
| 互斥 | 全局仅一个菜单可见(单例天然满足,见 §7) |
**为什么 DOM 必须脱离左栏**(这是硬约束,不是优化建议):
```css
.workspace-sidebar { overflow: hidden; } /* global.css:277 */
.conversation-list-scroll { overflow-x: hidden; } /* global.css:359 */
```
双重裁剪导致:① 横向——260px 窄栏内放不下 140px 菜单向右展开,必被裁;② 纵向——列表首尾条目的菜单会被滚动容器裁掉。
### 4.3 右键(快捷方式)
| 项 | 规格 |
|---|---|
| 绑定 | `.conversation-item` 根节点 `@contextmenu.prevent.stop` |
| 定位 | 以鼠标 `clientX/clientY` 为锚点,`position:fixed` |
| 菜单内容 | **与三点按钮完全一致**——由同一个 `computed` 生成的菜单项数组驱动,不允许两套定义 |
| 覆写原生菜单 | 是(`.prevent`)。**仅在会话条目上覆写**,列表空白区、消息区、输入框保留浏览器原生右键 |
| 右键是否需先选中条目 | **否**。右键直接对该条目生效,不改变当前打开的会话(避免误切换丢失输入草稿) |
**为什么两者并存而不是二选一**
- 纯右键**发现性为零**。左栏是坐席每天使用频率最高的区域,且服务台存在**多人轮岗与新人**,不能假设全员知晓。零发现性交互在此位置是产品事故。
- 需求 1(问右键)与需求 2(要三点)**本就是同一菜单的两个触发器**,不是二选一。并存的边际成本仅 0.3–0.5 人日。
### 4.4 反馈与错误处理
| 场景 | 反馈 |
|---|---|
| 操作成功 | 条目即时反映状态变化(📌/📋 图标出现或消失、条目重排)。**不额外弹 toast**——视觉变化本身即反馈 |
| 接单成功 | 条目从「排队区」移入「我的会话」,并自动打开该会话 |
| 转接成功 | 条目移出「我的会话」,`ElMessage.success('已转接给 XXX')` |
| **后端 403 / 失败** | 🔴 必须给明确 `ElMessage.error` 文案。**现状是既有缺陷**`stores/conversation.ts:539-541``console.error`,用户零感知。本次一并修复(P0-7) |
| 无可用坐席 | 「转接」项 disabled + 灰字提示「暂无可用坐席」 |
---
## 5. 菜单项可见性矩阵(核心交付物)
> 按 会话 `status` × 归属(`is_mine` / `is_collaborator` / `can_grab`)× 分区(`section`)分派。
> 与架构报告 §4.4 完全一致,本节为其产品侧收口版本。
| 菜单项 | 显示条件 | 禁用条件 | 动态文案 | 后端权限 | 备注 |
|---|---|---|---|---|---|
| **接单** | `status === 'queued'` | — | 「接单」 | `update:all` | 排队会话专属 |
| **置顶 / 取消置顶** | `is_mine \|\| is_collaborator` | — | `is_pinned` → 「取消置顶」;否则「置顶」 | `update:own` | ⚠️ **非本人会话点击必 403**,前端必须靠显示条件拦住 |
| **代办 / 取消代办** | `is_mine \|\| is_collaborator` | — | `is_todo` → 「取消代办」;否则「代办」 | `update:own` | ⚠️ 同上 |
| **转接** | `status === 'serving' && is_mine` | `availableAgents.length === 0` → 禁用,提示「暂无可用坐席」 | 「转接…」(省略号表示会开 Dialog) | `update:all` | 点击开独立 Dialog,见 Q-3 |
| **接手** | `can_grab && !is_mine && status === 'serving'` | — | 「接手」 | `update:all` | 与第三行 `grab-btn` 同义,双入口,见 Q-4 |
| **退出协作** | `is_collaborator && status === 'serving'` | — | 「退出协作」(danger 样式) | — | 与第三行 `leave-btn` 同义,见 Q-4 |
> ⚠️ **实现细节校准(影响 A-11 双入口一致性验收)**:第三行 `grab-btn` 的显示条件在两个分区**并不相同**——
> `ConversationList.vue:49`(我的会话区)传入 `show-grab = status==='serving' && !is_mine && !is_collaborator`
> `ConversationList.vue:62`(同事会话区)传入 `show-grab = status==='serving' && !is_mine`**少了 `!is_collaborator`**)。
> 条目内再与 `conversation.can_grab` 取交集(`ConversationItem.vue:117`)。
> 菜单工厂 `useConversationMenuItems.ts` **必须复用同一套条件**(建议把该判断上提为一个共享 `computed`,两处引用),
> 否则会出现「第三行有接手 link、菜单里没有」或反之的不一致,直接违反 A-11。
### 5.1 分区实例推演(原型必须逐一演示)
| 实例 | 状态 | 菜单实际渲染项 | 说明 |
|---|---|---|---|
| **A. 排队会话** | `status=queued``is_mine=false` | **接单** | 仅 1 项。置顶/代办不显示(非本人) |
| **B. 我的会话(serving** | `is_mine=true`, `is_pinned=false`, `is_todo=false` | 置顶 / 代办 / 转接… | 3 项。无接单(非 queued)、无接手(非 can_grab |
| **C. 我的会话(已置顶已代办)** | `is_mine=true`, `is_pinned=true`, `is_todo=true` | **取消置顶** / **取消代办** / 转接… | 文案动态翻转 |
| **D. 同事会话** | `is_mine=false`, `can_grab=true`, `status=serving` | **接手** | 仅 1 项。**置顶/代办被显示条件拦住**(点了会 403) |
| **E. 我协作中的会话** | `is_collaborator=true`, `status=serving` | 置顶 / 代办 / **退出协作**(danger) | 无转接(`is_mine=false` |
| **F. 历史会话** | `section=history`, `status=resolved` | **(空)** | 全部条件不满足 → 菜单显示「无可用操作」占位,或三点按钮直接不可点 |
| **G. 转接无可用坐席** | `is_mine=true`, `availableAgents=[]` | 置顶 / 代办 / ~~转接(灰)~~ | 转接项 disabledhover 提示「暂无可用坐席」 |
> **F 的产品决策**:历史会话菜单为空时,**三点按钮仍渲染但呈禁用态**`opacity:.35``cursor:default`,点击无响应),**不弹空菜单**。理由:弹出一个写着"无可用操作"的空菜单是无效交互;但完全不渲染按钮会让历史分区条目右侧出现空洞,与其它分区视觉不齐。
### 5.2 与「结单」的边界
「结单」**不进入**左栏菜单,理由见 Q-5。中栏保留其原有入口。
---
## 6. 中栏改造
### 6.1 变更清单(`UserInfoBar.vue:114-194`
| # | 控件 | 代码位置 | 本次处理 |
|---|---|---|---|
| 1 | 接单 | `:116-124` | ❌ **删除** |
| 2 | 置顶 | `:127-133` | ❌ **删除** |
| 3 | 代办 | `:136-142` | ❌ **删除** |
| 4 | **转接** | `:145-164` | ✅ **保留**D-2 |
| 5 | 摇人 | `:167-174` | ✅ 保留(不在本次范围) |
| 6 | 结单 / 等待确认 | `:177-193` | ✅ 保留(不在本次范围) |
| — | 历史会话开关 chip | `:100-110` | ✅ 保留(在 chips 区,非 actions 区) |
**改造后中栏 actions 区 = 转接 / 摇人 / 结单**,语义收敛为「对话过程动作组」。
### 6.2 转接入口一致性要求
中栏「转接」与左栏菜单「转接」必须:
- 调用同一 store action`transferConv`);
- 打开**同一个坐席选择 Dialog 组件**(Q-3 结论);
- 保留现有的 `ElMessageBox.confirm` 二次确认(`ChatArea.vue:531-547`)。
不得出现"中栏是下拉、左栏是 Dialog"的两套 UI。
---
## 7. 组件抽象边界与 PRD-011 兼容
### 7.1 抽象原则
> **抽「菜单壳」,不抽「业务动作」。**
```
components/common/WorkItemActionMenu.vue 【本次新建】纯 UI 壳,零业务逻辑
职责:单例渲染 / fixed 或 Teleport 定位 / 视口翻转
/ Esc·scroll·resize·outside 关闭 / role=menu + ↑↓ 键盘导航
/ z-index token / 分组分隔线 / danger 项样式
Props : items: MenuItem[] visible: boolean anchor: {x,y} | HTMLElement
Emits : select(action: string) close()
⛔ 不认识 conversation / approval / ticket,不 import 任何 store 与 api
composables/useConversationMenuItems.ts 【本次新建】会话菜单项工厂
入参:conversation + currentAgentId + section('my'|'colleague'|'history')
出参:MenuItem[](已按 §5 矩阵算好 hidden / disabled / label
✅ 承载全部可见性规则,可单测,PRD-011 Phase 2 原样复用
composables/useTodoMenuItems.ts 【PRD-011 Phase 2 新建】任务菜单项工厂
components/conversation/ConversationList.vue 【本次改造】持有单例菜单 + 路由 action 到 store
components/conversation/ConversationItem.vue 【本次改造】仅加三点按钮与 @contextmenu
只 emit('open-menu', {id, x, y}),不含菜单 DOM
```
### 7.2 `MenuItem` 类型契约
```ts
interface MenuItem {
key: string
label: string
icon?: string
danger?: boolean
disabled?: boolean
hidden?: boolean
tooltip?: string
/** 🔴 关键字段:决定容器采用哪种反馈策略 */
execMode: 'direct' | 'redirect' | 'disabled'
}
```
**`execMode` 为什么是必需字段**(对宋献"未来待办条目进入左栏后菜单怎么分派"的直接回答):
| kind | 操作性质 | 执行路径 | 反馈模型 |
|---|---|---|---|
| `conversation` | **服务端真闭环** | store → API → 后端端点 → 刷新列表 | 同步成功/失败 |
| `approval` | **降级跳转** | `<a target="_blank">` 跳企微 → `sys_approval_change` 回调回写 → WS 推送 | 异步最终一致 |
| `ticket` | **当前无对象** | `ITSMService.get_todo_list()` 无条件 `return []` | 无(PRD-011 U-1.2 |
> **必须分派,且不只是文案不同——是执行语义与反馈模型的根本不同。** 会话操作点完即生效;审批操作点完只是跳走,真正的状态变更在企微侧异步回写。**若三类共用一套「点击 → toast 成功」的反馈模型,就会重演 PRD-011 §3.1 定性为 P0 的那个错误**(绿色成功提示 + 外部系统实际未变更)。
### 7.3 与 PRD-011 Phase 2 的衔接检查表
| PRD-011 Phase 2 任务 | 本设计是否兼容 | 说明 |
|---|---|---|
| 定义统一 `ListItem`(含 `kind` 判别式) | ✅ | `MenuItem[]` 由 kind 专属工厂生成,菜单壳不感知 kind |
| 条目组件支持会话态 / 任务态两种渲染 | ✅ | 条目只 `emit('open-menu')`,两态共用同一菜单壳 |
| 左栏 Tab 分层(会话/待办/全部) | ✅ | 「全部」Tab 混排时,容器按 `item.kind` 选工厂 |
| **U-5 虚拟滚动** | ✅ | **单例架构天然兼容**(见下) |
| U-6 待办未读/变更标识 | ➖ | 与菜单无关 |
**D-4 单例架构的强制理由**PRD-011 U-5 已把虚拟滚动挂起。虚拟滚动会在滚动时**回收并复用条目 DOM**——若菜单实例挂在条目内部,reference 元素被销毁会导致菜单错位或残留。单例架构的实现成本与逐条挂载**相同**,却可免除未来上虚拟滚动时的整体返工。这正是宋献「避免三个月后第二次重做」诉求的具体着力点。
---
## 8. 产品经理自决项结论(Q-3 / Q-4 / Q-5
架构报告 §4.4 将三项留给产品决策。以下为结论与论证。
### Q-3 转接的坐席选择:二级菜单 还是 独立 Dialog?
> ### ✅ **结论:独立 Dialog。菜单项文案为「转接…」(带省略号,遵循"点击会开弹窗"的通用约定)。**
| 论据 | 说明 |
|---|---|
| **① 窄栏二级翻转过于脆弱** | 主菜单已 140px、`bottom-end` 右对齐。二级菜单需再向左或向右展开 ~160px。在 260px 侧栏 + 视口右缘的组合下,Popper 需同时做**水平翻转 + 垂直翻转**,边缘 case 组合爆炸,稳定性无法保证 |
| **② 坐席列表承载不下** | 二级菜单需展示 `姓名 (当前负载/最大负载)`,且在线坐席可能十余人,需要**滚动 + 搜索**。二级 popover 里塞搜索框是反模式 |
| **③ 悬停路径问题** | 二级菜单靠 hover 展开时,鼠标从主菜单项斜向移动到二级区域会经过其它菜单项,导致二级意外收起(经典 "diagonal problem"),需额外做安全三角形算法——成本高于直接开 Dialog |
| **④ 与中栏入口天然统一** | 中栏保留的「转接」也需要选坐席。共用同一个 Dialog 组件后,**两个入口 UI 完全一致**,符合 §6.2 的一致性要求。若左栏用二级菜单、中栏用下拉,就是两套 UI |
| **⑤ 转接是有后果的动作** | 转接后会话脱手。现有实现已带 `ElMessageBox.confirm` 二次确认。Dialog 形态与"需要慎重决策"的语义匹配,二级菜单的轻量感反而不合适 |
**Dialog 规格**:宽 380px,标题「转接会话」,含搜索框 + 坐席列表(头像 / 姓名 / `负载/上限`)+ 满载坐席置灰不可选 + 取消按钮。选中坐席后走原有二次确认。
**反方意见记录**:二级菜单少一次弹窗、路径更短。但转接**日频次不高**(非置顶/代办那类高频动作),为低频动作牺牲稳定性不划算。
---
### Q-4 「接手」「退出协作」是否收进三点菜单(收则移除第三行按钮)?
> ### ✅ **结论:收进菜单,但第三行 `grab-btn` / `leave-btn` 保留不动 —— 双入口并存。**
**这与架构建议("本次不动,保持第三行按钮,菜单只放 4 项")有分歧,理由如下:**
| 论据 | 说明 |
|---|---|
| **① 右键路径的完整性** | D-3 已定右键并存。坐席右键条目时,鼠标位置是任意的——若菜单里没有「接手」,坐席仍需**移动鼠标到第三行那个小 link 按钮**才能接手。这让右键快捷方式在最需要它的同事会话场景下失效,自相矛盾 |
| **② 移除第三行按钮才是真损失** | 第三行 grab/leave 是 `v-if` **数据驱动的常显 link**,零 hover 成本、一次点击即达。三点菜单需 hover→点击→点击共 3 步。**移除它是明确的效率退化**,不能为了"避免重复"而做 |
| **③ "同一动作两个入口"不是缺陷** | 这与 §3.2 中栏保留转接是同一条原则:**高频/关键动作允许多入口**,只要共用同一 handler。真正的缺陷是两个入口**行为不一致**,而非入口数量 |
| **④ 变更面反而更小** | 保留第三行按钮 = 不动既有 DOM,只在菜单工厂里多产出两个 MenuItem。移除按钮才需要改 `ConversationItem.vue:116-136` 并回归测试 |
**约束**:两个入口必须 `emit` 同一事件(`grab` / `leave`),走同一 store action。**禁止**在菜单里另写一份逻辑。
**验收要求**:同事会话条目上,第三行「接手」link 与菜单「接手」项必须同时可见、同时消失(受同一 `can_grab` 条件驱动)。
---
### Q-5 「结单」是否进左栏菜单?
> ### ✅ **结论:不进。维持中栏唯一入口。**
| 论据 | 说明 |
|---|---|
| **① 路径错位** | 结单需走摘要确认 Dialog(`ChatArea.vue:448-502`),坐席要在弹窗里确认/编辑会话摘要。**在左栏一个 24px 的三点按钮里触发一个需要阅读与编辑的模态流程**,是明确的路径错位 |
| **② 语义归属** | 结单属「对话过程动作组」(转接/摇人/结单),与置顶/代办的"列表管理"性质不同。§3.2 已确立该分组,结单进左栏会破坏刚刚建立的分组一致性 |
| **③ 结单前需要看对话内容** | 结单意味着"问题已解决",这个判断必须基于中栏的对话内容。坐席不可能在左栏扫视时判断某条会话可以结单——**动作发生地必然是中栏** |
| **④ 误操作成本高** | 结单会触发员工端确认流程(`pending_close` 态)。在密集列表中一个 hover 出现的小菜单里放置这种动作,误触成本过高 |
| **⑤ 用户未提** | 需求原文只提 4 个操作,不做需求扩张 |
---
## 9. 需求池(Requirements Pool
### P0 — Must have
| # | 需求 |
|---|---|
| P0-1 | 新建 `WorkItemActionMenu.vue` 单例菜单壳:fixed/Teleport 定位、视口翻转、Esc / 点击外部 / 滚动 / resize / 切换会话 关闭、z-index ≥ 2000、danger 项样式 |
| P0-2 | 新建 `useConversationMenuItems.ts`:完整实现 §5 矩阵,含动态文案翻转与 disabled 计算,**附单元测试覆盖 §5.1 的 A–G 七个实例** |
| P0-3 | `ConversationItem.vue` 新增三点按钮:§4.1 全部规格(同栅格位淡入淡出、24×24、hover/`.is-menu-open`/`:focus-visible` 显示、`@click.stop` |
| P0-4 | `ConversationItem.vue` 新增 `@contextmenu.prevent.stop`,与三点按钮共用同一菜单项定义 |
| P0-5 | `ConversationList.vue` 持有单例菜单实例,按 action 路由到对应 store action |
| P0-6 | `UserInfoBar.vue` 删除接单/置顶/代办 3 个按钮,**保留转接** |
| P0-7 | 修复既有缺陷:`stores/conversation.ts` 中 4 个操作的失败分支由 `console.error` 改为 `ElMessage.error` 明确文案(403 场景必须有用户可见反馈) |
| P0-8 | 转接坐席选择 Dialog 组件化,中栏与左栏共用 |
### P1 — Should have
| # | 需求 |
|---|---|
| P1-1 | 引入 z-index token`--z-dropdown: 2000` / `--z-modal: 3000` / `--z-toast: 9000`),替换菜单相关硬编码 |
| P1-2 | 菜单 `role="menu"` / 项 `role="menuitem"`,三点按钮用原生 `<button>``el-button`(可 Tab 聚焦、Enter/Space 激活) |
| P1-3 | 历史分区三点按钮禁用态(§5.1 F 的处理) |
| P1-4 | 转接 Dialog 支持坐席姓名搜索 |
### P2 — Nice to have
| # | 需求 |
|---|---|
| P2-1 | 菜单 ↑↓ 方向键导航 + Enter 确认(可挂接既有 `useKeyboardShortcuts.ts` |
| P2-2 | 将 `MessageItem.vue` 的自研右键菜单迁移到 `WorkItemActionMenu`,顺带修复其 5 项缺陷(无边界翻转 / z-index 1000 偏低 / 关闭时机不全 / 无 a11y / 无单例互斥) |
| P2-3 | 中栏按钮删除做成配置开关,支持 2 周内灰度回滚 |
| P2-4 | 为转接注册全局快捷键(如 `Ctrl+Shift+T` |
---
## 10. 验收标准
### 10.1 功能验收
| # | 验收项 | 通过标准 |
|---|---|---|
| A-1 | 三点按钮显隐 | 默认不可见;hover 条目出现且缩略头像淡出;**条目高度与各元素位置零位移** |
| A-2 | 点击不误触 | 点击三点按钮**不切换当前会话**(当前打开的会话保持不变) |
| A-3 | 右键一致性 | 同一条目上,右键菜单项与三点菜单项**逐项完全一致**(项数、顺序、文案、禁用态) |
| A-4 | 菜单不被裁剪 | 左栏**第一条**与**最后一条**会话的菜单完整可见,不被侧栏或滚动容器裁掉;视口下缘不足时向上翻转 |
| A-5 | 矩阵正确性 | §5.1 的 A–G 七个实例逐一验证,菜单项与预期完全一致 |
| A-6 | 403 拦截 | 同事会话菜单中**不出现**置顶/代办项;构造非法请求时前端有明确错误提示 |
| A-7 | 单例互斥 | 打开条目 A 的菜单后,右键条目 B,A 的菜单自动关闭,同屏只有一个菜单 |
| A-8 | 关闭时机 | Esc / 点击外部 / 滚动列表 / 窗口 resize / 切换会话 —— 五种情况菜单均关闭 |
| A-9 | 中栏改造 | 中栏 actions 区仅剩 转接 / 摇人 / 结单;**转接路径仍为 3 步** |
| A-10 | 转接双入口 | 中栏转接与左栏菜单转接打开**同一个 Dialog**,行为一致 |
| A-11 | 接手双入口 | 同事会话条目第三行「接手」link 与菜单「接手」项同时可见/同时消失,点击效果相同 |
| A-12 | 状态即时反映 | 置顶/代办后条目 📌/📋 图标即时出现或消失,列表按新权重重排 |
### 10.2 架构验收(防返工)
| # | 验收项 | 通过标准 |
|---|---|---|
| B-1 | 单例 | 全局 DOM 中 `.work-item-action-menu` 实例数恒 ≤ 1,**不随会话条目数增长** |
| B-2 | 壳零业务 | `WorkItemActionMenu.vue``import` 不含任何 store / api / `Conversation` 类型 |
| B-3 | 工厂可单测 | `useConversationMenuItems.ts` 为纯函数,可脱离组件树单测 |
| B-4 | DOM 归属 | 菜单 DOM 的 `parentElement``body` 或使用 `position: fixed` |
### 10.3 明确不作为验收依据
- ❌ toast 成功提示(沿用 PRD-011 §6 Phase 0 的验收原则)
- ❌ 原型截图(原型是设计依据,不是实现验收标准)
---
## 11. 风险登记
| 编号 | 风险 | 等级 | 缓解 |
|---|---|---|---|
| **R-1** | 转接若被误删将造成 3→6 步路径退化 | 🔴 高 → 🟢 已缓解 | D-2 决定中栏保留转接;若评审推翻,必须同时落地 P2-4 快捷键 |
| **R-2** | v1.7 若沿用 v1.6 低保真基线,菜单落位设计与实现对不上 | 🔴 高 → 🟢 已缓解 | D-1 改以 v1.3 为基线并回填真实字段 |
| **R-3** | z-index 冲突(项目现有 `100/999/1000/1200/3000/9999/999999` 混用,EP popper 默认 2000+ | 🟡 中 | P1-1 引入 token;优先用 EP popover 交由其管理 |
| **R-4** | 菜单可见性与后端权限不一致 → 403。`pin`/`todo``update:own` | 🟡 中 | §5 矩阵前端严格拦截 + P0-7 失败提示 |
| **R-5** | 复制 `MessageItem.vue` 右键实现,带入其 5 项缺陷 | 🟡 中 | 强制走 `WorkItemActionMenu` 新壳,禁止复制粘贴 |
| **R-6** | 未来虚拟滚动导致条目内菜单错位 | 🟡 中 → 🟢 已缓解 | D-4 本次即采用单例架构 |
| **R-7** | 三点按钮与缩略头像争位导致布局抖动 | 🟢 低 | D-5 同栅格位淡入淡出,不改盒模型;A-1 专项验收 |
| **R-8** | 右键在 VDI / 远程桌面 / 触摸屏环境下不可靠 | 🟢 低 | 三点按钮为主入口,右键仅为快捷方式,降级不影响可用性。**需宋献确认坐席实际办公环境(架构 Q-7)** |
| **R-9** | 坐席习惯迁移成本:老坐席习惯在中栏点置顶 | 🟢 低 | 变更公告 + 首次使用时的引导气泡(可选);P2-3 保留灰度回滚 |
---
## 12. 明确不在本次范围
- 中栏「摇人」「结单」的任何改动
- 待办条目(审批/工单)进入左栏 —— 属 PRD-011 Phase 2
- 左栏虚拟滚动 —— 属 PRD-011 U-5(本次仅在架构上预留兼容)
- 会话列表排序权重调整
- 后端任何改动(4 个端点及权限装饰器均已就绪,无需变更)
- 键盘方向键导航(列为 P2-1,不阻塞本次)
---
## 13. 待需求方(宋献)评审确认清单
| # | 待确认项 | 本 PRD 的默认取值 | 若推翻的影响 |
|---|---|---|---|
| **C-1** | 中栏**保留转接**是否接受?(原需求是 4 个全删) | 保留(D-2) | 推翻则转接路径 3→6 步,必须补 P2-4 快捷键,工期 +0.3 人日 |
| **C-2** | 是否接受**右键并存**? | 接受(D-3) | 推翻则去掉 P0-4,省 0.3–0.5 人日,但熟练坐席效率损失 |
| **C-3** | 是否接受 v1.7 **回填左栏真实字段**的额外工作量? | 接受(D-1) | 推翻则原型不可作为开发依据 |
| **C-4** | 坐席实际办公环境是否存在 **VDI / 远程桌面 / 触摸屏**? | 假定为普通 PC 浏览器 | 若有 VDI,右键可靠性下降,需重新评估 C-2 |
| **C-5** | 历史会话条目的三点按钮:**禁用态**还是**完全不渲染**? | 禁用态(§5.1 F) | 视觉偏好项,成本相同 |
| **C-6** | 备选形态:是否需要看「中栏 3 按钮收进中栏自己的『⋯』菜单」的 A/B 对比? | 未在 v1.7 中呈现 | 若需要,v1.7 需追加一版对比图 |
| **C-7** | Q-4 结论(接手/退出**双入口**,不移除第三行按钮)是否认可? | 双入口 | 若要求单入口,需明确保留哪一个 |
---
## 14. 工期估算
> 粗估,用于排序参考,不作为承诺。基于架构报告 §4.1 的分解并按本 PRD 结论调整。
| 阶段 | 内容 | 估时 |
|---|---|---|
| S1 | `WorkItemActionMenu.vue` 单例壳 | 1.0 人日 |
| S2 | `useConversationMenuItems.ts` + 单测(§5 矩阵) | 0.5 人日 |
| S3 | `ConversationItem` 三点按钮 + 右键触发 | 0.5 人日 |
| S4 | `ConversationList` 单例接线 + action 路由 | 0.5 人日 |
| S5 | 中栏删 3 留转接 | 0.2 人日 |
| S6 | 转接 Dialog 组件化(Q-3+ 中栏复用 | 0.5 人日 |
| S7 | P0-7 失败反馈修复 | 0.2 人日 |
| S8 | a11yP1-2+ 键盘导航(P2-1 | 0.3 人日(可 P2 单排) |
| — | **合计** | **3.43.7 人日** |
---
## 15. 变更记录
| 日期 | 版本 | 变更内容 | 变更人 |
|---|---|---|---|
| 2026-08-09 | v1.0 | 创建。基于架构核查报告 v1.0 与宋献口述需求,固化 D-1~D-6 六项决策;产出菜单项可见性矩阵(§5)与七实例推演;给出 Q-3/Q-4/Q-5 三项自决结论(Q-4 与架构建议存在分歧并说明理由);定义 `WorkItemActionMenu` + `useConversationMenuItems` 组件边界与 PRD-011 Phase 2 兼容检查表;登记 R-1~R-9 风险与 C-1~C-7 待确认项 | 许清楚(产品经理) |
</content>
</invoke>