550 lines
46 KiB
Markdown
550 lines
46 KiB
Markdown
|
|
# 技术核查 — 坐席端会话条目操作菜单(左栏三点菜单 / 右键菜单)
|
|||
|
|
|
|||
|
|
> **文档编号**: 技术核查-坐席会话条目操作菜单-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`<br>`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`<br>`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`<br>`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`<br>`conversations.py:493-499` | ✅ 真实 |
|
|||
|
|
|
|||
|
|
**四个端点均带权限装饰器**:
|
|||
|
|
- `assign` / `transfer` / `grab` → `@require_permission("conversation", "update", "all")`
|
|||
|
|
- `resolve` / `pin` / `todo` → `@require_permission("conversation", "update", "own")`
|
|||
|
|
|
|||
|
|
> ⚠️ **权限差异对菜单设计的直接影响**:`pin` / `todo` 是 `own` 权限,意味着后端**只允许对自己名下的会话**置顶/代办。若三点菜单在「同事会话」「历史会话」条目上无差别地展示这两项,用户点击后将收到 403,属于必须在前端拦截的可见性约束(详见 §1.2 与 §4.4)。
|
|||
|
|
|
|||
|
|
**对比参照 — PRD-011 §3.1 提到的 Mock 问题现状**:
|
|||
|
|
`TaskDetailView.vue` 的 `handleAction()` **已被修复**,不再是 PRD-011 所记录的无条件 `ElMessage.success`:
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
// 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 个模式 chip**,PM 设计菜单项状态时须逐条对齐:
|
|||
|
|
|
|||
|
|
| # | 按钮 | 代码位置 | 显示条件(`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`<br>= `status === 'serving' && (is_mine \|\| is_collaborator)`<br>(`ChatArea.vue:325-329`) | 无 | 固定「🤝 摇人」 | ❌ **保留** |
|
|||
|
|
| 6 | **结单 / 等待确认** | `:177-193` | `status === 'serving'` → 结单(danger)<br>`status === 'pending_close'` → 「⏳ 等待员工确认」(warning, disabled) | `pending_close` 态恒禁用 | 二态互斥 | ❌ **保留** |
|
|||
|
|
| — | 历史会话开关 chip | `:100-110` | 常驻(在 chips 区,非 actions 区) | `historyLoading` 时 `is-loading` | `historyMode`→「🕐 返回当前」;否则→「🕐 历史会话」 | ❌ 保留 |
|
|||
|
|
|
|||
|
|
**其他相关状态源**:
|
|||
|
|
- `conversation.status` 五态:`ai_handling` / `queued` / `serving` / `pending_close` / `resolved`(`api/conversation.ts:46`)
|
|||
|
|
- `is_mine`、`is_collaborator`、`can_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: 260px`(`global.css:98`,`min-width: 200px`,可拖拽调整 `global.css:270-271`)。
|
|||
|
|
条目内部可用宽度 ≈ 260 − 6×2(margin)− 10×2(padding)= **228px**。
|
|||
|
|
|
|||
|
|
| 区域 | 宽度占用 | 可否让位 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| 头像 `.conv-avatar-wrap` | 48px + gap 8px = **56px** | ❌ 不可 |
|
|||
|
|
| `.conv-target-avatar`(右侧缩略头像) | 约 28-32px + gap 8px ≈ **36-40px**,`section !== '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-125`,`v-if="showGrab && can_grab"`)与「退出」`leave-btn`(`:127-136`,`v-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:52` → `conversationStore.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 会裁剪菜单
|
|||
|
|
|
|||
|
|
```css
|
|||
|
|
/* 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-item`(`position: relative`)内部的菜单,
|
|||
|
|
1. **横向必被裁掉** —— 260px 窄栏内放不下一个 120-160px 的菜单还要向右展开;
|
|||
|
|
2. **纵向在列表首尾会被滚动容器裁掉**。
|
|||
|
|
|
|||
|
|
→ **菜单 DOM 必须脱离左栏容器**,只有两条合规路径:`position: fixed` + 视口坐标(`MessageItem.vue` 现有做法),或 Teleport 到 body(Element 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`):
|
|||
|
|
|
|||
|
|
```html
|
|||
|
|
<!-- 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.0–v1.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.0`(`package.json`) | 2.x,`el-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` 现有右键实现(复用成本评估)
|
|||
|
|
|
|||
|
|
```js
|
|||
|
|
// 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 }
|
|||
|
|
```
|
|||
|
|
```html
|
|||
|
|
<!-- :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>
|
|||
|
|
```
|
|||
|
|
```css
|
|||
|
|
/* :631-669 */
|
|||
|
|
.context-menu { position: fixed; z-index: 1000; ... }
|
|||
|
|
.context-menu__overlay { position: fixed; inset: 0; z-index: 999; }
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**逐项评估**:
|
|||
|
|
|
|||
|
|
| 技术点 | 现有实现 | 评估 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| 定位算法 | `left/top = clientX/clientY`,`position: 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 项缺陷一并复制到左栏 —— **不建议**;
|
|||
|
|
- **提取 + 加固为通用 composable**(`useContextMenu`:fixed 定位 + 视口翻转 + 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 支持组虽是技术人群,但坐席工作台是**多人轮岗**场景,不能假设全员知晓<br>🔴 需覆写浏览器原生右键菜单,部分用户会感到被剥夺<br>⚠️ 触摸屏/远程桌面(VDI)环境下右键可能不可靠 —— 需确认坐席实际办公环境 |
|
|||
|
|
| **适用性** | ❌ **不适合作为唯一入口**。作为快捷方式是加分项 |
|
|||
|
|
|
|||
|
|
#### 选项 2:三点按钮 + Popover(用户倾向方案)
|
|||
|
|
|
|||
|
|
| 维度 | 评估 |
|
|||
|
|
|---|---|
|
|||
|
|
| **可行性** | ✅ 可行,且有现成模板 `AiReplyModeSwitch.vue` |
|
|||
|
|
| **改造量** | 中。`el-popover` + `MoreFilled` 图标(`@element-plus/icons-vue@^2.3.0` 已装)+ 菜单项可见性矩阵,**1.5-2.0 人日**(大头在菜单项状态矩阵与归属权限判定,不在控件本身) |
|
|||
|
|
| **优势** | ✅ 发现性明确,零学习成本,与微信/钉钉/Slack 一致<br>✅ `el-popover` **默认 Teleport 到 body**,自动绕开 §1.3.4 的 overflow 裁剪<br>✅ Popper.js 自动处理 `placement` 翻转与贴边,260px 窄栏用 `placement="bottom-end"` 即可稳定<br>✅ z-index 由 EP 统一管理(2000+ 自增),不会与 `MessageItem` 的 1000 打架 |
|
|||
|
|
| **风险** | ⚠️ **占位冲突**(§1.3.2)—— 必须与 `.conv-target-avatar` 二选一或叠放,PM 须明确<br>⚠️ **冒泡冲突** —— 三点按钮必须 `@click.stop`,否则点开菜单会连带切换会话。项目内已有正确先例(`ConversationItem.vue:122/133` 的 `@click.stop`),风险可控<br>⚠️ hover 显隐会引入「鼠标移出条目→按钮消失→菜单是否跟着关」的边界判定,需明确规则(建议:菜单打开期间按钮**强制常显**并给条目加 `.is-menu-open` 态)<br>⚠️ 每个条目挂一个 `el-popover` 实例,50+ 条目时有轻微渲染开销(EP popover 内容 lazy 渲染,实测可接受;若上虚拟滚动则必须改为单例菜单) |
|
|||
|
|
| **适用性** | ✅ **适合作为主入口** |
|
|||
|
|
|
|||
|
|
#### 选项 3:三点按钮为主 + 右键为快捷方式(并存)
|
|||
|
|
|
|||
|
|
| 维度 | 评估 |
|
|||
|
|
|---|---|
|
|||
|
|
| **可行性** | ✅ 可行,**且是成本最低的并存实现** —— 前提是菜单内容抽成一个独立组件,两个触发源共用同一份菜单项定义与 handler |
|
|||
|
|
| **改造量** | 选项 2 基础上 **+0.3-0.5 人日**(仅新增 `@contextmenu.prevent` 触发 + `el-popover` 的 `virtual-triggering` 坐标注入) |
|
|||
|
|
| **优势** | ✅ 覆盖新手(三点可见)与熟手(右键高效)两类用户<br>✅ 与 IM 类产品心智完全一致(微信 PC 会话列表:右键有菜单;企微:右键有菜单)<br>✅ **对宋献本人的原始诉求(需求 1 问右键、需求 2 要三点)是唯一的「全都要」解**,且不是妥协——二者本就是同一菜单的两个触发器 |
|
|||
|
|
| **风险** | ⚠️ 需保证两个入口的菜单项与状态**永远一致**(用同一 `computed` 生成菜单项数组即可根治)<br>⚠️ 互斥:右键打开 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-popover` 的 `virtual-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`/`keydown`(`ConversationItem.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.vue`,Phase 2 落地时必然重写。**这是三个月后二次返工的确切来源。**
|
|||
|
|
|
|||
|
|
### 3.2 kind 分派设计
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
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 | 键盘导航与 a11y(role/↑↓/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-avatar`(hover 时缩略头像 `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-btn`(`ConversationItem.vue:116-136`)应同步移除,避免同一动作两个入口;不收则菜单只有 4 项、更聚焦。**架构倾向:本次不动,保持第三行按钮**,减少变更面。
|
|||
|
|
3. **结单**是否也进左栏菜单?用户未提,但它是与置顶/代办同等级的会话动作。**架构建议不进**——结单需走摘要确认 Dialog(`ChatArea.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.4:v1.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`/`todo` 是 `update:own` 权限(`conversations.py:374, 467`) | 🟡 中 | §4.4 矩阵在前端严格拦截;后端 403 时前端给明确文案而非静默(现状 `stores/conversation.ts:539-541` 仅 `console.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.0~v1.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) | 高见远(架构师) |
|
|||
|
|
</content>
|
|||
|
|
</invoke>
|