# 员工结束会话功能 - 技术方案
> **版本**: v1.3
> **日期**: 2026-07-31
> **关联PRD**: ../01-产品文档/02-会话管理/PRD-REQ-会话-001-员工结束会话-v1.3.md(§十一 v1.3 整合区增量)
> **关联原型**: ../01-产品文档/02-会话管理/原型-REQ-会话-001-结束会话流程-v1.3.html §⑨
> **状态**: 已拍板(方案 A 5 项细节 2026-07-31 落地)
---
## 一、概述
本文档描述 v1.2 调整的技术实现方案。**核心变更**:
1. 操作按钮从 5 态(hidden/disabled/active/urgent/waiting)扩展为 **6 态**(移除 hidden / 恢复 end / 新增 reopen)
2. 新增 **4 种引导语**渲染逻辑
3. store 新增 `showHeaderExitBtn` 字段,**控制顶部退出按钮仅 AI 场景显示**
4. 双入口(顶部退出 + 操作按钮 end)**场景互斥**,从根源避免双入口混乱
5. 24h 重新打开按钮实现
> 沿用 v1.1:排队进度底部消息胶囊、右栏移除 queue section、清理 api/queue 和 api/quiz。
---
## 二、技术架构
### 2.1 现有代码结构(v1.2 基准)
```
frontend-h5/src/components/chat/
├── InputBar.vue # 输入栏(含操作按钮 6 态 + 引导语)— v1.2 改造
├── ChatPanel.vue # 对话区(含顶部退出按钮 v-show)— v1.2 改造
├── MessageBubble.vue # 消息气泡(含"会话已关闭"系统消息样式)
├── EvaluationDialog.vue # 满意度评价弹窗
├── ResolveConfirmCard.vue # 坐席结单确认卡片
└── ... 其他
frontend-h5/src/stores/
└── conversation.ts # 新增 showHeaderExitBtn 字段 + reopenSession action
frontend-h5/src/components/assistant/
├── RightPanel.vue # 右栏(v1.1 已移除 queue section)— v1.2 沿用
├── QueueWaiting.vue # 标记弃用,保留文件以备未来恢复
└── ...
```
### 2.2 关键现有代码(InputBar.vue:194-243 v1.1 现状)
```typescript
type CallAgentState = 'hidden' | 'disabled' | 'active' | 'urgent' | 'waiting'
```
**v1.2 改造**:扩展为 6 态,详细见 §3.1。
### 2.3 关键现有代码(ChatPanel.vue:59-65 顶部退出按钮)
```vue
```
**v1.2 改造**:增加 `v-show="store.showHeaderExitBtn"`,详见 §3.5。
---
## 三、方案设计
### 3.1 操作按钮 6 态状态机(v1.2 核心变更)
**位置**:InputBar.vue 控件区第一层(行 78-88)
**6 态定义**:
| 状态值 | 触发条件 | 按钮文案 | 按钮图标 | 样式 | 可点击 |
|--------|---------|---------|---------|------|--------|
| `disabled` | 无会话 / AI<3 轮 / 坐席离线 / 会话过期 | 人工坐席 | 🔒 | 灰色禁用 | ❌ |
| `active` | AI≥3 轮 / 无紧急词 | 人工坐席 | 🎧 | 绿色描边 | ✅ |
| `urgent` | 检测到紧急关键词 | 人工坐席 | 🚨 | 红色脉冲 | ✅ |
| `waiting` | 排队中 | 排队等待 | ⏳ | 橙色描边 | ✅(取消排队)|
| `end` 🆕 | 坐席已接入(serving) | 结束咨询 | 📴 | 红色填充 | ✅(弹评价)|
| `reopen` 🆕 | 会话已关闭 + 24h 内 | 重新打开 | 🔄 | 蓝色填充 | ✅(reopen API)|
**v1.2 移除**:
- ❌ `hidden` 态 — 完全移除,所有场景都有按钮呈现
**优先级顺序**(v1.2 修订):
```
end > reopen > waiting > active/urgent > disabled
```
### 3.2 状态流转图
```
┌── (打开应用 / 重新进入)
↓
┌──────────────────┐
│ disabled │ ← 无会话
│ "先说问题" │
└────────┬─────────┘
│ 用户输入 + AI ≥3 轮
↓
┌──────────────────┐
│ active │ ── 检测到紧急词 ──→ urgent
│ "人工坐席" │ │
└────────┬─────────┘ │
│ 点击 / 紧急关键词 │
↓ ↓
┌──────────────────┐ ┌──────────────────┐
│ waiting │ ←─────────────│ urgent │
│ "排队等待" │ │ "人工坐席" │
└────────┬─────────┘ └──────────────────┘
│ 坐席接听
↓
┌──────────────────┐
│ end 🆕 │
│ "结束咨询" │
└────────┬─────────┘
│ 坐席挂断 / 用户点结束
↓ (close + 评价)
┌──────────────────┐
│ reopen 🆕 │ ← 会话已关闭 + 24h 内
│ "重新打开" │
└────────┬─────────┘
│ 点击 reopen
↓
┌──────────────────┐
│ active │ (回到正常会话流程)
└──────────────────┘
```
### 3.3 store 新增字段(v1.2 关键)
**文件**:`stores/conversation.ts`
```typescript
// 新增:顶部退出按钮是否可见(仅 AI 会话显示)
const showHeaderExitBtn = computed(() => {
const conv = currentConversation.value
if (!conv) return false // 无会话:隐藏
if (conv.status === 'resolved') return false // 会话已关闭:隐藏
// 人工咨询启动后(waiting / serving):隐藏
const isHumanActive =
conv.status === 'waiting' ||
conv.status === 'serving' ||
!!queuePositionData.value // 排队中
if (isHumanActive) return false
// AI 对话中:显示
return true
})
// 新增:会话是否在 24h 重新打开窗口内
const canReopen = computed(() => {
const conv = currentConversation.value
if (!conv || conv.status !== 'resolved') return false
if (!conv.resolved_at) return false
const elapsed = Date.now() - new Date(conv.resolved_at).getTime()
return elapsed < 24 * 60 * 60 * 1000 // 24h 内
})
// 新增:重新打开会话 action(调用 closing.ts 已有的 reopenConversation)
async function reopenCurrentConversation(): Promise {
const conv = currentConversation.value
if (!conv || !canReopen.value) return
const result = await reopenConversation(conv.conversation_id)
// 更新 store,触发 UI 切回 active 态
if (result) {
// ... 复用现有 openConversation 流程
}
}
```
### 3.4 组件改动
#### 3.4.1 InputBar.vue
**改动 1:状态类型扩展**
```typescript
// v1.1
type CallAgentState = 'hidden' | 'disabled' | 'active' | 'urgent' | 'waiting'
// v1.2
type CallAgentState = 'disabled' | 'active' | 'urgent' | 'waiting' | 'end' | 'reopen'
```
**改动 2:状态计算逻辑**
```typescript
const callAgentState = computed(() => {
const conv = store.currentConversation
if (!conv) return 'disabled' // v1.2: 无会话 = disabled(不再 hidden)
// 🆕 会话已关闭 + 24h 内 → reopen
if (conv.status === 'resolved' && store.canReopen) return 'reopen'
// 🆕 坐席已接入 → end
if (conv.status === 'serving') return 'end'
// 🆕 顶部按钮可见性也通过 store.showHeaderExitBtn 暴露给父组件
if (conv.status === 'waiting') return 'waiting'
// 紧急关键词检测
if (checkUrgentKeywords()) return 'urgent'
// AI ≥3 轮 / canCallAgent
if (store.canCallAgent) return 'active'
// 兜底:disabled(含 AI<3 轮、坐席离线、过期等场景)
return 'disabled'
})
```
**改动 3:按钮文案 + 图标扩展**
```typescript
const callAgentBtnText = computed(() => {
if (callAgentState.value === 'waiting') return '排队等待'
if (callAgentState.value === 'end') return '结束咨询' // 🆕
if (callAgentState.value === 'reopen') return '重新打开' // 🆕
return '人工坐席'
})
const callAgentBtnIcon = computed(() => {
if (callAgentState.value === 'waiting') return '⏳'
if (callAgentState.value === 'urgent') return '🚨'
if (callAgentState.value === 'active') return '🎧'
if (callAgentState.value === 'end') return '📴' // 🆕
if (callAgentState.value === 'reopen') return '🔄' // 🆕
return '🔒'
})
```
**改动 4:按钮样式扩展**
```typescript
const callAgentBtnClass = computed(() => ({
'call-agent-btn--disabled': callAgentState.value === 'disabled',
'call-agent-btn--active': callAgentState.value === 'active',
'call-agent-btn--urgent': callAgentState.value === 'urgent',
'call-agent-btn--waiting': callAgentState.value === 'waiting',
'call-agent-btn--end': callAgentState.value === 'end', // 🆕
'call-agent-btn--reopen': callAgentState.value === 'reopen', // 🆕
}))
```
**改动 5:点击行为扩展(end / reopen 分支)**
```typescript
function handleCallAgent(): void {
if (callAgentState.value === 'disabled') return
// 🆕 end 态:触发结束会话流程(与顶部按钮共用 emit)
if (callAgentState.value === 'end') {
emit('end-conversation')
return
}
// 🆕 reopen 态:调用 reopen API
if (callAgentState.value === 'reopen') {
store.reopenCurrentConversation()
return
}
if (callAgentState.value === 'waiting') {
emit('cancel-queue')
return
}
// active / urgent → 呼叫坐席
emit('call-agent')
}
```
**改动 6:emit 扩展**
```typescript
const emit = defineEmits<{
(e: 'call-agent'): void
(e: 'cancel-queue'): void
(e: 'end-conversation'): void // 🆕(操作按钮 end 态专用,复用 handleExitWithEvaluation)
}>()
```
**改动 7:新增引导语渲染**
```vue
```
**改动 8:样式新增(end / reopen)**
```scss
// 🆕 end 态:红色填充(最显眼的"危险/结束"语义)
.call-agent-btn--end {
border-color: var(--color-danger);
background: var(--color-danger);
color: #fff;
font-weight: 700;
&:hover {
background: #dc2626;
}
&:active {
transform: scale(0.96);
}
}
// 🆕 reopen 态:蓝色填充(中性"恢复"语义)
.call-agent-btn--reopen {
border-color: var(--info, #1989fa);
background: var(--info-light, #ecf5ff);
color: var(--info, #1989fa);
&:hover {
background: var(--info, #1989fa);
color: #fff;
}
&:active {
transform: scale(0.96);
}
}
```
#### 3.4.2 ChatPanel.vue(顶部按钮可见性)
**改动 1:顶部按钮 v-show 绑定**
```vue