Files
wecom_it_smart_desk/docs/02-技术文档/技术架构/技术核查-坐席会话条目操作菜单-v1.0.md
T

46 KiB
Raw Blame History

技术核查 — 坐席端会话条目操作菜单(左栏三点菜单 / 右键菜单)

文档编号: 技术核查-坐席会话条目操作菜单-v1.0 版本: v1.0 状态: 核查完成 — 供产品设计原型 v1.7 与后续开发决策使用 日期: 2026-08-09 作者: 高见远(架构师) 需求来源: 宋献(IT 支持组组长)口述需求 4 条(见 §0) 关联: PRD-REQ-坐席-011-统一工作队列重构-v0.1、REQ-坐席-004、REQ-坐席-009、REQ-坐席-010 核查范围: 仅 src/frontend-agent/(活跃代码)与 src/backend/。根目录 frontend-agent/ 为 2026-07-13 monorepo 重组前遗留副本,线上无挂载、不在核查范围


0. 需求原文与核查任务映射

# 需求原文 本文对应章节
1 确认现有坐席架构,控件能否在「选中对象-右键调出菜单」模式实现 §2 全章 + §4
2 左栏每个会话表单右边增加菜单按钮(三个点),点击弹出接单/置顶/代办/转接 §2.2、§2.3、§4
3 上述完成后,去除中栏上方 4 个按钮 §5.1本文提出重大异议
4 先基于最新原型(V1.6)更新细致原型图 §1.4本文提出前置警告
前置认知:「V1.0–V1.6 原型中 4 个按钮丢失了」 §1.4 结论:认知需修正

1. A. 现状核查(代码实证)

1.1 四个操作的完整实现链路

结论:四个操作全部为真实闭环,非 Mock。

操作 UI 组件 事件处理 Store Action API 函数 后端端点 闭环
接单 UserInfoBar.vue:116-124 <el-button @click="$emit('assign')"> ChatArea.vue:431-442 handleAssign() stores/conversation.ts:498-505 assignConv() api/conversation.ts:176-181 assignConversation() POST /conversations/{id}/assign
backend/app/api/conversations.py:265-271
真实
置顶 UserInfoBar.vue:127-133 ChatArea.vue:507-514 handleTogglePin() stores/conversation.ts:535-542 togglePinConv() api/conversation.ts:215-218 togglePin() POST /conversations/{id}/pin
conversations.py:373-378
真实
代办 UserInfoBar.vue:136-142 ChatArea.vue:519-526 handleToggleTodo() stores/conversation.ts:549-556 toggleTodoConv() api/conversation.ts:227-230 toggleTodo() POST /conversations/{id}/todo
conversations.py:466-471
真实
转接 UserInfoBar.vue:145-164 <el-dropdown> + 坐席列表 ChatArea.vue:531-547 handleTransfer(targetAgentId)(含 ElMessageBox.confirm 二次确认) stores/conversation.ts:564-571 transferConv() api/conversation.ts:239-244 transferConversation() POST /conversations/{id}/transfer
conversations.py:493-499
真实

四个端点均带权限装饰器

  • assign / transfer / grab@require_permission("conversation", "update", "all")
  • resolve / pin / todo@require_permission("conversation", "update", "own")

⚠️ 权限差异对菜单设计的直接影响pin / todoown 权限,意味着后端只允许对自己名下的会话置顶/代办。若三点菜单在「同事会话」「历史会话」条目上无差别地展示这两项,用户点击后将收到 403,属于必须在前端拦截的可见性约束(详见 §1.2 与 §4.4)。

对比参照 — PRD-011 §3.1 提到的 Mock 问题现状 TaskDetailView.vuehandleAction() 已被修复,不再是 PRD-011 所记录的无条件 ElMessage.success

// src/frontend-agent/src/components/chat/TaskDetailView.vue:127-133(当前实际代码)
function handleAction(action: string): void {
  if (props.todoItem.type === 'approval') {
    console.info('[TaskDetailView] 审批动作已跳转企微审批原系统:', action)
    return
  }
  ElMessage.info('该操作需在原系统中完成')
}

结论:会话侧 4 个操作是真闭环,任务侧(审批/工单)是降级跳转。二者性质根本不同——这一差异是 §5 组件抽象设计的核心依据。建议同步更新 PRD-011 §3.1 的行号与描述,该条 P0 阻塞项的表述已过期。


1.2 中栏 UserInfoBar 按钮完整清单与可见性条件

UserInfoBar.vue:114-194.user-info-bar__actions 区共 6 个操作控件 + 1 个模式 chipPM 设计菜单项状态时须逐条对齐:

# 按钮 代码位置 显示条件(v-if 禁用条件(:disabled 文案动态切换 本次是否移除
1 接单 :116-124 常驻显示 status !== 'queued' queued→「接单」;其他→「已接单」 用户要求移除
2 置顶 :127-133 常驻显示 is_pinned→「取消置顶」;否则→「📌 置顶」 用户要求移除
3 代办 :136-142 常驻显示 is_todo→「取消代办」;否则→「📋 代办」 用户要求移除
4 转接 :145-164 常驻显示 无(但列表可为空,空时显示 disabled 项「暂无可用坐席」:159-161 固定「转接」,二级为在线坐席列表 姓名 (当前负载/最大负载) 用户要求移除
5 摇人 :167-174 canInviteCollaborator
= status === 'serving' && (is_mine || is_collaborator)
ChatArea.vue:325-329
固定「🤝 摇人」 保留
6 结单 / 等待确认 :177-193 status === 'serving' → 结单(danger
status === 'pending_close' → 「 等待员工确认」(warning, disabled
pending_close 态恒禁用 二态互斥 保留
历史会话开关 chip :100-110 常驻(在 chips 区,非 actions 区) historyLoadingis-loading historyMode→「🕐 返回当前」;否则→「🕐 历史会话」 保留

其他相关状态源

  • conversation.status 五态:ai_handling / queued / serving / pending_close / resolvedapi/conversation.ts:46
  • is_mineis_collaboratorcan_grab 三个归属判别字段(api/conversation.ts:69, 72, 79
  • 转接目标坐席列表来源:agentStore.availableAgents,由 stores/agent.ts:226-243 loadAvailableAgents() 拉取 getAgents('online')排除自己;DEV 环境失败时降级到 mock 数据

⚠️ 关键设计缺口:现有 4 个按钮几乎没有基于会话归属的可见性控制——置顶/代办按钮在任何状态下都常驻且不禁用。这在中栏是可接受的(中栏只显示当前打开的会话,多为自己的),但一旦下沉到左栏,左栏同时呈现「我的会话 / 同事会话 / 历史会话」三个分区(ConversationList.vue:44-77,菜单项必须新增归属维度的可见性规则。这是本次改造真正新增的产品设计工作量,而非单纯的控件搬家。


1.3 左栏 ConversationItem 现状:DOM 结构、右侧空间占用、事件绑定

1.3.1 DOM 结构(ConversationItem.vue:11-149

.conversation-item  (display:flex; align-items:center; gap:8px; padding:8px 10px; margin:1px 6px; position:relative)
│                    ← global.css:473-484@click="$emit('click')" 绑在根节点(:21
├── .conv-avatar-wrap        (48×48 头像 + .new-msg-dot 新消息圆点)          :23-43
├── .conversation-info       (flex:1; min-width:0; overflow:hidden)          :46-138
│   ├── .conversation-name   第一行                                          :48-96
│   │   ├── 📌 置顶图标 (v-if is_pinned)                                     :50
│   │   ├── 📋 代办图标 (v-if is_todo)                                       :52
│   │   ├── 姓名 .text-ellipsis                                              :54
│   │   ├── VIP / 招手 / 需介入 / 情绪 / 坐席名 / 待确认  共 6 类 tag-badge    :56-82
│   │   └── .priority-icons  (margin-left:auto)  最多 4 个 16×16 图标         :84-95
│   ├── .conversation-summary-row  第二行:摘要(flex:1) + 时间(flex-shrink:0)  :99-102
│   └── .conversation-meta         第三行:5 星紧急度 + 「接手」/「退出」按钮  :105-137
└── .conv-target-avatar      右侧 处理对象缩略头像(v-if showTargetAvatar    :141-148

1.3.2 右侧空间占用盘点(这是三点按钮方案最大的物理约束

左栏总宽 --sidebar-width: 260pxglobal.css:98min-width: 200px,可拖拽调整 global.css:270-271)。 条目内部可用宽度 ≈ 260 − 6×2margin)− 10×2padding= 228px

区域 宽度占用 可否让位
头像 .conv-avatar-wrap 48px + gap 8px = 56px 不可
.conv-target-avatar(右侧缩略头像) 约 28-32px + gap 8px ≈ 36-40pxsection !== 'history'恒显示:316-321 ⚠️ 可争议
.conversation-info 剩余 132-136px

结论:.conversation-item 的最右侧已被 .conv-target-avatar 占用(除历史会话外恒显示)。三点按钮没有现成的空槽位,PM 必须在以下三条中做出明确取舍,不能回避:

  • 方案 a:三点按钮 hover 时覆盖absolute 定位)在 .conv-target-avatar 之上 —— 保留信息密度,但 hover 时缩略头像被遮挡;
  • 方案 b:三点按钮替换 .conv-target-avatar(hover 时缩略头像淡出、三点淡入)—— 视觉最干净,推荐;
  • 方案 c:三点按钮插到 .conv-target-avatar 右侧 —— 再吃掉 ~24px.conversation-info 压到 ~108px,第一行的 6 类 tag-badge + 4 个优先级图标将严重挤压,不推荐

补充约束:第三行 .conversation-meta 已存在「接手」grab-btn:116-125v-if="showGrab && can_grab")与「退出」leave-btn:127-136v-if="showLeave")两个 margin-left:auto 的 link 按钮。三点菜单里若也放「接手」,会与之重复,需在 §4.4 的菜单项矩阵中统一收口。

1.3.3 hover 态与事件绑定现状

现状 代码位置
hover 背景 .conversation-item:hover { background: var(--bg-hover) } global.css:486-488
active 态 .conversation-item.active { background: accent-soft; border-color: accent } global.css:490-493
hover 显隐控件 当前完全没有。所有子元素都是常显或 v-if 数据驱动
根节点点击 @click="$emit('click')"ConversationList.vue:52conversationStore.selectConversation(conv.id) ConversationItem.vue:21
内部按钮防冒泡 已有先例:@click.stop="$emit('grab')" / @click.stop="$emit('leave')" :122, :133
右键事件 左栏当前无任何 @contextmenu 绑定 全项目仅 MessageItem.vue:23 一处
键盘可达性 。根节点是裸 <div>,无 tabindex、无 role、无 @keydown。列表不可 Tab 遍历、不可方向键导航 ConversationItem.vue:12-21
虚拟滚动 未启用。全项目 grep virtual / RecycleScroller 零命中ConversationList.vue:44-77 为三段 v-for 全量渲染

1.3.4 🔴 关键约束:父容器 overflow 会裁剪菜单

/* global.css:269-279 */
.workspace-sidebar { width: var(--sidebar-width); overflow: hidden; position: relative; }
/* global.css:356-360 */
.conversation-list-scroll { flex: 1; overflow-y: auto; overflow-x: hidden; }

双重裁剪:滚动容器 overflow-x: hidden + 侧栏 overflow: hidden

推论(必须写入技术约束):任何以 position: absolute 挂在 .conversation-itemposition: relative)内部的菜单,

  1. 横向必被裁掉 —— 260px 窄栏内放不下一个 120-160px 的菜单还要向右展开;
  2. 纵向在列表首尾会被滚动容器裁掉

菜单 DOM 必须脱离左栏容器,只有两条合规路径:position: fixed + 视口坐标(MessageItem.vue 现有做法),或 Teleport 到 bodyElement Plus Popper 默认行为)。这一条直接决定了 §4 的技术选型。


1.4 「原型丢失 vs 代码丢失」——确定结论

结论:4 个按钮在真实运行代码中从未丢失,只在原型 HTML 的 v1.4 及之后版本中不存在。用户的前置认知需要修正两处:一是丢失发生在原型侧不是代码侧;二是起始版本是 v1.4 不是 v1.5。

逐版本关键词计数实证

原型版本 行数 接单 置顶 代办 转接 结单 判定
v1.0 2447 11 3 1 2 1 有(:1559-1567
v1.1 2448 11 3 1 2 1
v1.2 2544 12 3 1 2 1
v1.3 2707 12 3 1 2 1
v1.4.archive 961 0 0 0 0 0 首次消失
v1.5 1058 0 0 0 0 0
v1.6 1088 0 0 0 0 0

v1.0 原文(原型-...-v1.0.html:1559-1567):

<!-- 2026-07-24 优化:接单按钮(用户信息栏右侧操作区) -->
<div style="display:flex;align-items:center;gap:6px;margin-left:auto;">
  <button class="btn-action primary" ...>📥 接单</button>
  <button class="btn-action warning" ...>📌 置顶</button>
  <button class="btn-action default" ...>📋 代办</button>
  <button class="btn-action default" ...>🔄 转接</button>
</div>

根因:v1.4 起原型换了血统,不是同一条演进线

版本 <title> 性质
v1.0v1.3 (完整工作台原型) 全量高保真原型2400-2700 行
v1.4 坐席工作台 v1.4(B方案分栏 + 排查步骤全屏流程图) 专题示意原型961 行
v1.5 v1.5(B方案分栏 + 全屏流程图 + 智能推荐竖排 + 工具栏重组) 专题示意原型
v1.6 v1.6(B方案分栏 + 全屏流程图 + 智能推荐竖排 + 工具栏重组 + 工具栏图标统一) 专题示意原型

v1.4 是为论证「B 方案三栏分栏比例」和「排查步骤全屏流程图」而新画的布局示意图,不是 v1.3 的增量演进。证据:v1.6 通篇带 col-badge 标注(260px 固定 中栏 ≈ 60% 右栏 ≈ 40%),中栏 .user-info-bar:539-546)只有头像+姓名+工号+在线状态三个元素,无任何操作按钮区;左栏 .conv-item:498-533)只有 头像/姓名/时间/摘要/未读数 五个字段。

对照真实代码的保真度落差(这是给 PM 的核心警告):

元素 真实代码(ConversationItem.vue v1.6 原型(.conv-item
置顶/代办图标 📌📋
tag-badge VIP/招手/需介入/情绪/坐席名/待确认 共 6 类
优先级图标 👥🔁 最多 4 个
紧急度星级 5 星
接手/退出按钮
处理对象缩略头像
中栏操作按钮 6 个 0 个

🔴 给 PM 的前置警告:需求 4「基于 V1.6 更新细致原型图」若直接在 v1.6 上加三点菜单,产出的原型将继续偏离真实实现两代——按 v1.6 那个五字段的简版条目去设计菜单按钮位置,会得出「右侧有大片空槽」的错误结论,而真实条目右侧已被缩略头像占满(§1.3.2)。

建议v1.7 的左栏必须回填真实字段(可参照 v1.3 的高保真度 + ConversationItem.vue 的当前实现),否则本次评审通过的原型无法作为开发依据。这是一个需要 PM 明确接受的额外工作量,不宜静默跳过。


2. B. 交互形态可行性评估

2.1 技术底座盘点

能力 现状 结论
Element Plus 版本 element-plus@^2.7.0package.json 2.xel-dropdown / el-popover 均可用
EP 有无原生右键菜单组件 没有。Element Plus 2.x 不提供 ContextMenu 组件(对比 Ant Design 的 Dropdown trigger=['contextMenu']、Naive UI 的 n-dropdown :x :y ⚠️ 右键方案需自行拼装
el-dropdown 现有用法 UserInfoBar.vue:145(转接)、TopBar.vue:37(在线状态) 团队已熟练
el-popover 现有用法 AiReplyModeSwitch.vue:24-82🤖 三态开关,含 trigger="click" placement="bottom-start" :hide-after="0" popper-class 最佳复用模板
自研右键菜单 MessageItem.vue:23, 135-155, 326-336 一处 ⚠️ 实现较粗糙,见 §2.2
@vueuse/core ^14.0.0 onClickOutside / useElementBounding,可用于自研定位
z-index 约定 无统一约定。散落 100/999/1000/1200/3000/9999/999999 🔴 风险,见 §6 R-3

2.2 精读 MessageItem.vue 现有右键实现(复用成本评估)

// MessageItem.vue:326-336
function showContextMenu(event: MouseEvent): void {
  contextMenuStyle.value = { left: `${event.clientX}px`, top: `${event.clientY}px` }
  contextMenuVisible.value = true
}
function closeContextMenu(): void { contextMenuVisible.value = false }
<!-- :23 -->  <div class="message-row" @contextmenu.prevent="showContextMenu">
<!-- :135 --> <div v-if="contextMenuVisible" class="context-menu" :style="contextMenuStyle">
<!-- :155 --> <div v-if="contextMenuVisible" class="context-menu__overlay" @click="closeContextMenu"></div>
/* :631-669 */
.context-menu          { position: fixed; z-index: 1000; ... }
.context-menu__overlay { position: fixed; inset: 0; z-index: 999; }

逐项评估

技术点 现有实现 评估
定位算法 left/top = clientX/clientYposition: fixed fixed 是正确选择,天然规避 §1.3.4 的 overflow 裁剪。这是本实现最有价值的部分
边界处理 完全没有。菜单在视口右缘/下缘会溢出屏幕外 🔴 缺陷。中栏消息区较宽、菜单项少(最多 3 项 ≈ 120px 高),问题不明显;左栏条目密集,列表底部条目右键必然溢出视口下缘,不修复不可用
关闭时机 全屏 overlay @click 关闭 ⚠️ 仅覆盖鼠标左键点击。未处理:Esc 键、滚轮滚动、窗口 resize、在别处再次右键(会叠出第二个菜单)、路由/会话切换
z-index 菜单 1000 / overlay 999 🔴 低于 Element Plus popper 默认起始值 2000。EP 的 el-dropdown/el-popover/el-message-box 若同时在场会盖住自研菜单
无障碍 role="menu" / role="menuitem",无焦点管理,Esc 不关闭,方向键不导航 🔴 不满足基本 a11y
多实例互斥 无。每个 MessageItem 各持一份 contextMenuVisible,理论上可同时弹出多个 ⚠️ 靠 overlay 侥幸规避

复用成本量化

  • 直接复制:0.2 人日,但把上述 5 项缺陷一并复制到左栏 —— 不建议
  • 提取 + 加固为通用 composableuseContextMenufixed 定位 + 视口翻转 + Esc/scroll/resize 关闭 + 全局单例互斥 + z-index token):1.0-1.5 人日,同时可反向修复 MessageItem.vue 的现有缺陷;
  • 改用 el-popover 虚拟触发virtual-triggering + virtual-ref):EP 自带 Popper.js 翻转/贴边/Teleport/z-index 自动管理,0.5 人日,但需接受 EP popper 的默认动画与样式覆写成本。

2.3 三选项对比

选项 1:纯右键上下文菜单(选中对象 → 右键调出)

维度 评估
可行性 技术上完全可行。@contextmenu.prevent 绑到 .conversation-item 根节点即可
改造量 中。1.0-1.5 人日(含 composable 加固)
优势 零视觉侵入 —— 不占用 §1.3.2 中已经紧张的右侧 24px;对熟练坐席效率最高;天然适配未来 PRD-011 的长列表
风险 🔴 发现性为零。无任何视觉提示,新坐席不可能自己发现。IT 支持组虽是技术人群,但坐席工作台是多人轮岗场景,不能假设全员知晓
🔴 需覆写浏览器原生右键菜单,部分用户会感到被剥夺
⚠️ 触摸屏/远程桌面(VDI)环境下右键可能不可靠 —— 需确认坐席实际办公环境
适用性 不适合作为唯一入口。作为快捷方式是加分项

选项 2:三点按钮 + Popover(用户倾向方案)

维度 评估
可行性 可行,且有现成模板 AiReplyModeSwitch.vue
改造量 中。el-popover + MoreFilled 图标(@element-plus/icons-vue@^2.3.0 已装)+ 菜单项可见性矩阵,1.5-2.0 人日(大头在菜单项状态矩阵与归属权限判定,不在控件本身)
优势 发现性明确,零学习成本,与微信/钉钉/Slack 一致
el-popover 默认 Teleport 到 body,自动绕开 §1.3.4 的 overflow 裁剪
Popper.js 自动处理 placement 翻转与贴边,260px 窄栏用 placement="bottom-end" 即可稳定
z-index 由 EP 统一管理(2000+ 自增),不会与 MessageItem 的 1000 打架
风险 ⚠️ 占位冲突(§1.3.2)—— 必须与 .conv-target-avatar 二选一或叠放,PM 须明确
⚠️ 冒泡冲突 —— 三点按钮必须 @click.stop,否则点开菜单会连带切换会话。项目内已有正确先例(ConversationItem.vue:122/133@click.stop),风险可控
⚠️ hover 显隐会引入「鼠标移出条目→按钮消失→菜单是否跟着关」的边界判定,需明确规则(建议:菜单打开期间按钮强制常显并给条目加 .is-menu-open 态)
⚠️ 每个条目挂一个 el-popover 实例,50+ 条目时有轻微渲染开销(EP popover 内容 lazy 渲染,实测可接受;若上虚拟滚动则必须改为单例菜单)
适用性 适合作为主入口

选项 3:三点按钮为主 + 右键为快捷方式(并存)

维度 评估
可行性 可行,且是成本最低的并存实现 —— 前提是菜单内容抽成一个独立组件,两个触发源共用同一份菜单项定义与 handler
改造量 选项 2 基础上 +0.3-0.5 人日(仅新增 @contextmenu.prevent 触发 + el-popovervirtual-triggering 坐标注入)
优势 覆盖新手(三点可见)与熟手(右键高效)两类用户
与 IM 类产品心智完全一致(微信 PC 会话列表:右键有菜单;企微:右键有菜单)
对宋献本人的原始诉求(需求 1 问右键、需求 2 要三点)是唯一的「全都要」解,且不是妥协——二者本就是同一菜单的两个触发器
风险 ⚠️ 需保证两个入口的菜单项与状态永远一致(用同一 computed 生成菜单项数组即可根治)
⚠️ 互斥:右键打开 A 条目菜单时若又点 B 条目三点,须先关 A(EP popover 需手动做单例控制,或用全局 activeMenuId
适用性 推荐

2.4 必答技术点逐条结论

# 技术点 结论
1 Element Plus 是否有可复用组件 部分有。el-popover 推荐,模板见 AiReplyModeSwitch.vue:24-82)、el-dropdown(可用但 trigger 不支持 contextmenu,且默认渲染一个触发容器,用于条目内略重)。EP 2.x 无原生 ContextMenu 组件,右键需用 el-popovervirtual-triggering + virtual-ref 拼装,或自研
2 复用 MessageItem 右键实现的成本 直接复制 0.2 人日但继承 5 项缺陷(无边界翻转 / z-index 1000 偏低 / 关闭时机不全 / 无 a11y / 无单例互斥);提取加固为通用 composable 1.0-1.5 人日;改用 el-popover 虚拟触发 0.5 人日且缺陷全免 —— 推荐后者
3 260px 窄栏下的弹出方向与溢出裁剪 🔴 必须 Teleport 或 fixed.workspace-sidebar{overflow:hidden}global.css:277+ .conversation-list-scroll{overflow-x:hidden}global.css:359)双重裁剪,absolute 定位的菜单必被裁。用 el-popover 默认 teleport 到 body 即可根除。弹出方向建议 placement="bottom-end"(右对齐向下),Popper 在视口下缘不足时自动翻转为 top-end。菜单宽度建议 132-148px(4 项中文两字 + 图标),不要超过侧栏宽度
4 三点按钮与条目点击的冒泡冲突 ⚠️ 真实存在但已有解@click="$emit('click')" 绑在根节点(ConversationItem.vue:21),三点按钮须 @click.stop;项目内 grab-btn/leave-btn:122/:133)已是此写法。额外注意el-popover 的 popper 内容 teleport 到 body 后不在条目 DOM 子树内,不会冒泡到条目——菜单项点击天然安全;但 #reference 插槽内的按钮仍在子树内,必须 .stop。右键同理需 @contextmenu.prevent.stop
5 虚拟滚动(PRD-011 U-5)下菜单跟随 当前未启用虚拟滚动(全项目零命中)。但 PRD-011 U-5 已挂起该项。🔴 前瞻性约束:虚拟滚动会在滚动时回收并复用条目 DOM,若菜单实例挂在条目内部,滚动时 reference 元素被销毁 → 菜单错位或残留。唯一稳妥的架构是「单例菜单」:整个左栏只渲染一个菜单组件(挂在 ConversationList 层级或全局),条目只负责 emit('open-menu', { item, x, y })建议本次就按单例架构实现,成本相同,可免除未来上虚拟滚动时的返工
6 键盘可达性 🔴 现状为零:.conversation-item 是裸 <div>,无 tabindex/role/keydownConversationItem.vue:12-21)。本次改造不会让它变差,但也不会自动变好。最低要求:三点按钮用原生 <button>el-button(可 Tab 聚焦、Enter/Space 激活);菜单容器加 role="menu",项加 role="menuitem",支持 ↑↓ 导航与 Esc 关闭 —— el-popover 不自带这套,需手写约 0.3 人日。建议列为 P2 单独排期,不阻塞本次。项目已有 useKeyboardShortcuts.ts(含 Escape/Tab/方向键处理框架,:110/:124-125/:151),可挂接

3. C. 与 PRD-011 的兼容性设计

3.1 耦合点识别

PRD-011 Phase 2 计划:「定义统一 ListItem 类型(含 kind 判别式),条目组件支持会话态与任务态两种渲染」(PRD-011 §6 Phase 2)。

本次要在左栏会话条目上加操作菜单 —— 这正是那个「条目组件」。若本次把菜单硬编码进 ConversationItem.vuePhase 2 落地时必然重写。这是三个月后二次返工的确切来源。

3.2 kind 分派设计

graph TD
  A["ListItem (PRD-011 统一类型)"] --> B{"kind 判别式"}
  B -->|conversation| C["会话菜单<br/>接单 / 置顶 / 代办 / 转接<br/>+ 接手 / 退出协作)"]
  B -->|approval| D["审批菜单<br/>在企微审批中打开<br/>U-1 已定:降级跳转)"]
  B -->|ticket| E["工单菜单<br/>在 ITSM 中打开<br/>U-1.2 阻塞:读链路即断)"]
  C --> F["WorkItemActionMenu<br/>(统一渲染 + 定位 + 关闭 + a11y"]
  D --> F
  E --> F
  F --> G["emit('action', { kind, id, action, payload })"]
  G --> H["ConversationList / WorkQueueList<br/>按 kind 路由到对应 store action"]

三类菜单的本质差异(必须在架构上体现,不能靠 if-else 混写)

kind 操作性质 执行路径 反馈模型 依据
conversation 服务端真闭环 store → API → 后端端点 → fetchConversations() 刷新 同步成功/失败 §1.1 实证
approval 降级跳转 <a target="_blank"> 跳企微 → sys_approval_change 回调回写 → WS 推送 异步最终一致 PRD-011 §6 Phase 0、U-1
ticket 当前无对象 ITSMService.get_todo_list() 无条件 return []itsm_service.py:113-130 PRD-011 U-1.2

🔴 对宋献的直接回答:「未来待办条目进入左栏后,同一个三点菜单是否要按 kind 分派出不同菜单项?」 必须分派,而且不只是菜单项文案不同 —— 是执行语义与反馈模型的根本不同。 会话操作点完即生效;审批操作点完只是跳走,真正的状态变更在企微侧异步回写。若共用一套「点击 → toast 成功」的反馈模型,就会重演 PRD-011 §3.1 定性为 P0 的那个错误(绿色成功提示 + 外部系统实际未变更)。菜单项定义必须携带 execMode: 'direct' | 'redirect' | 'disabled' 字段,由容器按 mode 分派反馈策略。

3.3 建议的组件抽象边界

结论:应该抽 WorkItemActionMenu,但要抽在正确的层次上——抽「菜单壳」,不抽「业务动作」。

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()
  MenuItem: { key, label, icon?, danger?, disabled?, hidden?,
              execMode: 'direct'|'redirect'|'disabled', tooltip? }
  ⛔ 不认识 conversation / approval / ticket,不 import 任何 store 与 api

composables/useConversationMenuItems.ts         ← 【本次新建】会话菜单项工厂
  入参:conversation + currentAgentId + section('my'|'colleague'|'history')
  出参:MenuItem[](已按 §4.4 矩阵算好 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

为什么这样切

  1. 单例菜单挂在 List 层 → 直接解决 §2.4-5 的虚拟滚动难题,且 Phase 2 换成 WorkQueueList 时菜单壳零改动;
  2. 菜单项工厂按 kind 分文件 → 完全对齐 PRD-011 的 kind 判别式,新增 kind 只加一个 composable,不动壳;
  3. 壳不认识业务 → 中栏 MessageItem.vue 的右键菜单也可以迁过来复用,顺带修掉 §2.2 列出的 5 项缺陷(可作为 P2 收益)。

与 PRD-011 Phase 2 的衔接检查表

PRD-011 Phase 2 任务 本次设计是否兼容
定义统一 ListItem(含 kind MenuItem[] 由 kind 专属工厂生成,WorkItemActionMenu 不感知 kind
条目组件支持会话态/任务态两种渲染 条目只 emit('open-menu'),两态共用同一菜单壳
左栏 Tab 分层(会话/待办/全部) 「全部」Tab 混排时,容器按 item.kind 选工厂即可
U-5 虚拟滚动 单例架构天然兼容
U-6 待办未读/变更标识 与菜单无关

4. D. 明确推荐

4.1 推荐方案

推荐选项 3:三点按钮为主入口 + 右键为快捷方式,菜单壳单例化并抽象为 WorkItemActionMenu

我同意宋献的方案 2 方向(三点按钮),但认为不应止步于此,理由如下。

  1. 需求 1 与需求 2 不是二选一,而是同一件事的两个触发器。 宋献先问「右键能不能做」,再提「加三个点」——从提问顺序看,右键是他真正想要的效率形态,三点是他为了保险给出的可实现形态。二者共用同一份菜单定义时,并存的边际成本只有 0.3-0.5 人日(见 §2.3 选项 3)。为省这半天而只做一个,是错误的取舍。

  2. 纯右键(选项 1)不可单独采用。 左栏会话列表是坐席每天使用频率最高的区域,且服务台存在轮岗与新人。零发现性的交互在这个位置是产品事故,不是效率优化。

  3. 单例菜单架构不是过度设计,是防返工的最小投入。 PRD-011 U-5 已把虚拟滚动挂起、Phase 2 已明确要做统一容器。若本次把 el-popover 逐条挂在 ConversationItem 内,虚拟滚动落地时条目 DOM 回收会直接导致菜单错位——那时的重写成本 ≥ 现在多花的 0.5 人日。这正是宋献要求「避免三个月后第二次重做」的具体着力点。

推荐实施顺序

阶段 内容 估时
S1 WorkItemActionMenu.vue(单例壳,fixed + 翻转 + 关闭时机 + z-index token 1.0 人日
S2 useConversationMenuItems.ts(§4.4 可见性矩阵)+ 单测 0.5 人日
S3 ConversationItem 三点按钮(hover 显隐 + @click.stop+ @contextmenu.prevent.stop 0.5 人日
S4 ConversationList 单例接线 + action 路由到 store 0.5 人日
S5 中栏按钮调整(见 §5.1,不建议全删 0.3 人日
S6 键盘导航与 a11yrole/↑↓/Esc 0.3 人日(可 P2
合计 2.8-3.1 人日

4.2 与用户倾向的差异点(明确说明,不附和)

宋献倾向 我的建议 论据
交互形态 三点按钮(方案 2 三点按钮 + 右键并存 并存边际成本仅 0.3-0.5 人日;需求 1 本就在问右键
组件位置 (未明确) 菜单单例挂 List 层,不挂条目内 PRD-011 U-5 虚拟滚动 + Phase 2 统一容器
中栏按钮 全删 4 个 仅删 3 个,转接保留(详见 §5.1 操作路径退化,见 §5.1 论证
原型基线 基于 v1.6 v1.6 需先回填左栏真实字段再改 v1.6 是专题示意图,左栏保真度落后真实代码两代(§1.4)

4.3 三点按钮的落位建议(供 PM 直接采用)

  • 位置.conversation-item 最右侧,替换 .conv-target-avatarhover 时缩略头像 opacity:0 淡出、三点 opacity:1 淡入,同一栅格位,不产生布局抖动)——即 §1.3.2 的方案 b;
  • 尺寸24×24px 命中区,图标 MoreFilled@element-plus/icons-vue@^2.3.0 已装)14-16px
  • 显隐:默认隐藏,.conversation-item:hover.is-menu-open:focus-visible 时显示(保证键盘可达);
  • 菜单placement="bottom-end",宽 140px,视口下缘不足时自动翻转 top-end
  • 历史会话分区section === 'history' 时本无缩略头像(ConversationItem.vue:318),三点按钮直接占该位。

4.4 菜单项可见性矩阵(PM 设计原型的直接输入)

按会话 status × 归属(is_mine / is_collaborator / can_grab)× 所属分区(section)分派。这是 §1.2 指出的「真正新增的产品设计工作量」的收口。

菜单项 显示条件 禁用条件 动态文案 后端权限
接单 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 → 禁用并提示「暂无可用坐席」 「转接」,二级为在线坐席 姓名 (负载/上限) update:all
接手 can_grab && !is_mine && status === 'serving' 「接手」 update:all
退出协作 is_collaborator && status === 'serving' 「退出协作」(danger

待 PM 决策的三个问题

  1. 转接的二级菜单:三点菜单里做二级展开(el-popover 嵌套,260px 窄栏下体验待验证),还是点「转接」后弹独立 Dialog 选坐席?架构建议后者——窄栏内嵌套二级菜单在视口边缘的翻转组合过于脆弱,且坐席列表可能较长需要滚动与搜索。
  2. 接手/退出协作是否收进三点菜单?收进则第三行的 grab-btn/leave-btnConversationItem.vue:116-136)应同步移除,避免同一动作两个入口;不收则菜单只有 4 项、更聚焦。架构倾向:本次不动,保持第三行按钮,减少变更面。
  3. 结单是否也进左栏菜单?用户未提,但它是与置顶/代办同等级的会话动作。架构建议不进——结单需走摘要确认 DialogChatArea.vue:448-502),在左栏触发一个模态流程属于路径错位。

5. E. 风险与待明确事项

5.1 🔴 核心风险:移除中栏按钮导致的操作路径退化(正面回答)

判断:会构成退化,且退化程度按操作分为两档。必须区别对待,不建议 4 个一刀切全删。

场景推演

坐席正在中栏与员工对话,需要执行某操作:

操作 当前路径 全删后路径 退化判定
接单 中栏点「接单」 左栏找条目 → hover → 点三点 → 点接单 不退化。接单发生在打开会话之前/之时,左栏本就是接单的自然场所(Zendesk/ServiceNow 皆如此)。且中栏按钮在 status !== 'queued' 时恒为 disabled 的「已接单」,长期占位却不可点,删除反而是净收益
置顶 中栏点「置顶」 左栏找条目 → 三点 → 置顶 🟡 轻微退化,可接受。置顶是列表组织行为,其效果(条目上浮)也只在左栏可见。操作与反馈同处一栏,语义上反而更内聚
代办 中栏点「代办」 左栏找条目 → 三点 → 代办 🟡 同置顶
转接 中栏点「转接」→ 下拉选坐席 → 确认 左栏找条目 → hover → 三点 → 转接 → 选坐席 → 确认 🔴 显著退化

为什么转接是显著退化

  1. 触发时机不同。置顶/代办是列表管理动作,坐席在扫视队列时执行;转接是对话过程动作——坐席读完员工描述、判断"这不归我管"的那一刻,注意力和鼠标都在中栏。此时被要求"回左栏,找到当前这条,hover 出三点,点开菜单,选转接",是注意力焦点的强制迁移

  2. 交互步数从 3 步涨到 6 步

    • 现状:点转接 → 选坐席 → 确认
    • 全删后:视线移左栏 → 定位当前条目 → hover → 点三点 → 点转接 → 选坐席 → 确认
    • 且第 2 步「定位当前条目」本身有成本——左栏三分区、可滚动、条目会因置顶/紧急度动态重排ConversationList.vue:44-77),当前会话未必在视口内。
  3. 中栏本就要留按钮,删了转接反而制造不一致。用户明确只删 4 个,「摇人」(UserInfoBar.vue:167-174)与「结单」(:177-193)保留在中栏。结果是:中栏留着"摇人/结单",转接却要去左栏——同为对话过程中的协作类动作,入口被劈成两处。这比"全部在中栏"或"全部在左栏"都差。

缓解建议(按优先级)

优先级 建议 说明
P0 中栏保留「转接」,删除「接单/置顶/代办」3 个 与「摇人/结单」构成语义一致的"对话过程动作组"(转接/摇人/结单 = 把这件事交出去/拉人进来/做完了)。左栏三点菜单同时也提供转接,两个入口共用同一 handler,符合"高频动作允许多入口"原则
P1 若宋献坚持 4 个全删 → 必须补快捷键 项目已有 useKeyboardShortcuts.ts 框架(含 ctrl+shift+a 等组合的注册范式,:141-151)。为转接注册全局快捷键(如 Ctrl+Shift+T)作用于当前会话,可把 6 步压回 2 步
P1 若 4 个全删 → 中栏 UserInfoBar 收一个「⋯」总菜单 把接单/置顶/代办/转接收进中栏自己的三点菜单。视觉上达成用户"去掉 4 个按钮"的诉求(栏内只剩摇人/结单/⋯),功能上零退化。这可能是最贴近宋献真实意图的解——他要的多半是"顶栏太挤、按钮太多",而不是"这些功能不该在中栏"
P2 灰度与回滚 中栏按钮删除做成配置开关,上线后收集坐席反馈,2 周内可回滚

给宋献的一句话建议:如果去掉中栏 4 个按钮的动机是「顶栏视觉太拥挤」,那么把它们收进中栏自己的「⋯」菜单赶到左栏更优——既减少了视觉元素,又不迁移操作焦点。建议在原型 v1.7 里把这两种形态都画出来做 A/B 对比,再定稿。

5.2 风险登记

编号 风险 等级 缓解
R-1 转接操作路径退化,坐席转接效率下降 🔴 §5.1:中栏保留转接,或补快捷键,或改中栏「⋯」总菜单
R-2 原型 v1.7 基于低保真的 v1.6,左栏条目字段缺失导致菜单落位设计与实现对不上 🔴 §1.4v1.7 左栏必须回填 ConversationItem.vue 的真实字段后再设计;否则本次评审产出不可作为开发依据
R-3 z-index 层级冲突。项目无统一约定(100/999/1000/1200/3000/9999/999999 混用),EP popper 默认 2000+ 🟡 引入 z-index token(建议:--z-dropdown: 2000 对齐 EP--z-modal: 3000--z-toast: 9000);优先用 EP popover 交由其管理
R-4 菜单项可见性与后端权限不一致 → 点击后 403。pin/todoupdate:own 权限(conversations.py:374, 467 🟡 §4.4 矩阵在前端严格拦截;后端 403 时前端给明确文案而非静默(现状 stores/conversation.ts:539-541console.error用户无任何感知——这是既有缺陷,建议一并修)
R-5 复制 MessageItem.vue 右键实现,把无边界翻转 / 无 Esc 关闭 / 无单例互斥等缺陷带入左栏 🟡 §2.2:改用 el-popover 虚拟触发,或提取加固为通用 composable
R-6 未来虚拟滚动(PRD-011 U-5)导致条目内菜单实例随 DOM 回收而错位 🟡 §3.3:本次即采用单例菜单架构
R-7 三点按钮与 .conv-target-avatar 争位,导致 hover 时信息遮挡或布局抖动 🟢 §4.3:同栅格位淡入淡出替换,不改变布局盒模型
R-8 PRD-011 §3.1 的 P0 描述已过期(TaskDetailView.vue 的无条件 ElMessage.success 已修复) 🟢 建议 PM 更新 PRD-011 §3.1 行号与结论,避免后续评审基于过期事实

5.3 待明确事项

编号 事项 需谁决策 阻塞级别
Q-1 中栏 4 个按钮是全删、删 3 留转接、还是改收进中栏「⋯」菜单? 宋献 🔴 阻塞原型 v1.7 定稿
Q-2 三点按钮与 .conv-target-avatar 的占位取舍(覆盖 / 替换 / 并列) 宋献 + PM 🔴 阻塞原型
Q-3 转接的坐席选择用二级菜单还是独立 Dialog? PM(架构建议 Dialog 🟡 阻塞开发
Q-4 「接手」「退出协作」是否收进三点菜单(收则移除第三行按钮)? PM(架构建议不动) 🟡 阻塞开发
Q-5 「结单」是否进左栏菜单? PM(架构建议不进) 🟢
Q-6 是否同时提供右键快捷方式? 宋献(架构强烈建议:是) 🟡
Q-7 坐席实际办公环境是否有 VDI/远程桌面/触摸屏?影响右键可靠性 宋献 🟡 影响 Q-6
Q-8 原型 v1.7 是否接受"先回填左栏真实字段"的额外工作量? 宋献 🔴 阻塞原型
Q-9 键盘导航(↑↓/Esc/Tab)是本次做还是 P2 单排? 宋献 🟢

6. 附录:核查方法与证据清单

类别 文件 用途
中栏按钮 src/frontend-agent/src/components/chat/UserInfoBar.vue:114-194 6 个按钮清单与可见性条件
事件处理 src/frontend-agent/src/components/chat/ChatArea.vue:325-336, 431-547 4 个 handler + 摇人可见性 computed
Store src/frontend-agent/src/stores/conversation.ts:498-587 assignConv/togglePinConv/toggleTodoConv/transferConv/grabConv
API src/frontend-agent/src/api/conversation.ts:176-256 5 个 API 函数
后端 src/backend/app/api/conversations.py:265, 327, 373, 466, 493, 525 6 个端点与权限装饰器
左栏条目 src/frontend-agent/src/components/conversation/ConversationItem.vue(全文 550 行) DOM 结构 / 事件 / 计算属性
左栏列表 src/frontend-agent/src/components/conversation/ConversationList.vue:44-77 三分区渲染与 section 传参
全局样式 src/frontend-agent/src/styles/global.css:98, 269-279, 356-360, 473-501 侧栏宽度 / overflow 裁剪 / 条目样式
右键先例 src/frontend-agent/src/components/chat/MessageItem.vue:23, 135-155, 326-336, 631-669 定位 / 关闭 / z-index
EP popover 先例 src/frontend-agent/src/components/chat/ai-assist/AiReplyModeSwitch.vue:24-82 最佳复用模板
任务侧 Mock src/frontend-agent/src/components/chat/TaskDetailView.vue:127-133 证明 PRD-011 §3.1 已过期
依赖版本 src/frontend-agent/package.json element-plus ^2.7.0 / @element-plus/icons-vue ^2.3.0 / @vueuse/core ^14.0.0
快捷键框架 src/frontend-agent/src/composables/useKeyboardShortcuts.ts:102-234 可挂接的快捷键注册范式
原型 docs/01-产品文档/04-坐席工作台/原型-...-v1.0v1.6.html 逐版本关键词计数(§1.4
PRD docs/01-产品文档/04-坐席工作台/PRD-REQ-坐席-011-统一工作队列重构-v0.1.md kind 判别式 / U-5 / §3.1

核查声明:本文所有结论均基于 2026-08-09 时点 src/ 目录下的实际代码,未依赖任何历史文档描述或记忆。核查过程未修改任何现有文件,未编写任何业务代码。


7. 变更记录

日期 版本 变更内容 变更人
2026-08-09 v1.0 创建。完成现状核查(A)、三形态可行性评估(B)、PRD-011 兼容性设计(C)、方案推荐(D)、风险与待明确事项(E) 高见远(架构师)