508 lines
36 KiB
Markdown
508 lines
36 KiB
Markdown
# 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.0–v1.3 是 2400–2700 行的全量高保真原型;v1.4 是为论证「B 方案三栏分栏比例」与「排查步骤全屏流程图」而**新画的专题示意图**(961 行),并非 v1.3 的增量演进。v1.5/v1.6 沿此专题线继续演进。
|
||
|
||
**后果(这是本次必须先做字段回填的原因)**:v1.6 的左栏 `.conv-item` 只有「头像/姓名/时间/摘要/未读数」5 个字段,而真实代码 `ConversationItem.vue` 的条目含:
|
||
|
||
| 元素 | 真实代码 | v1.6 原型 |
|
||
|---|---|---|
|
||
| 置顶 📌 / 代办 📋 图标 | ✅ `:50` `:52` | ❌ |
|
||
| tag-badge(VIP/招手/需介入/情绪/坐席名/待确认) | ✅ 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` 已装)14–16px | 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 个优先级图标严重挤压。左栏条目内部可用宽度仅 ≈ 228px(260 − 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-index(2000+ 自增)或新引入的 `--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=[]` | 置顶 / 代办 / ~~转接(灰)~~ | 转接项 disabled,hover 提示「暂无可用坐席」 |
|
||
|
||
> **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 | a11y(P1-2)+ 键盘导航(P2-1) | 0.3 人日(可 P2 单排) |
|
||
| — | **合计** | **3.4–3.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>
|