# 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` | **降级跳转** | `` 跳企微 → `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"`,三点按钮用原生 `