From 0c8432f4ff5c95faca4ca62d6a4ec4618feba627 Mon Sep 17 00:00:00 2001
From: tony.cheng <chengmingwei_1984122@126.com>
Date: Tue, 17 Mar 2026 15:31:29 +0800
Subject: [PATCH] feat: 完善AI调解状态控制功能及相关文档更新
---
openspec/changes/implement-mediation-state-control/tasks.md | 13 ++
openspec/changes/implement-mediation-state-control/specs/mediation-state-control/spec.md | 60 +++++++++++---
web-app/src/components/dashboard/FloatingControlPanel.jsx | 35 +++++++-
openspec/changes/implement-mediation-state-control/design.md | 60 +++++++++++---
openspec/changes/implement-mediation-state-control/proposal.md | 7 +
web-app/src/utils/stateTranslator.js | 5
web-app/src/contexts/CaseDataContext.jsx | 29 +++---
web-app/src/App.css | 10 +-
web-app/src/components/common/OutboundCallWidget.jsx | 12 +++
9 files changed, 172 insertions(+), 59 deletions(-)
diff --git a/openspec/changes/implement-mediation-state-control/design.md b/openspec/changes/implement-mediation-state-control/design.md
index 2e0d4dc..b70d824 100644
--- a/openspec/changes/implement-mediation-state-control/design.md
+++ b/openspec/changes/implement-mediation-state-control/design.md
@@ -6,22 +6,33 @@
## 组件设计
### 1. 状态控制按钮组件
-**位置**:位于人工接管按钮右侧
+**位置**:位于FloatingControlPanel组件中,人工接管按钮左侧
**显示逻辑**:
```
-if (caseState === 0 || caseState === 1) {
- // 显示"终止"按钮,蓝色样式
+const stateNum = Number(state);
+if (stateNum === 1) {
+ // 显示"终止"按钮,红色渐变样式
buttonText = "终止";
- buttonStyle = "primary";
-} else if (caseState === 5) {
- // 显示"恢复"按钮,绿色样式
+ buttonClass = "state-control-btn--terminate";
+} else if (stateNum === 5) {
+ // 显示"恢复"按钮,绿色渐变样式
buttonText = "恢复";
- buttonStyle = "success";
+ buttonClass = "state-control-btn--resume";
} else {
// 不显示按钮
showButton = false;
}
```
+
+### 2. 状态显示规则
+**状态文本显示**:
+- state=1: "调解进行中-阶段X:节点名称"
+- state=5: "AI调解暂停中"
+- 其他状态: 使用translateMediationState翻译
+
+**状态圆点颜色**:
+- state=5: 红色 (#e63946)
+- 其他状态: 默认绿色 (var(--success-color))
### 2. 确认对话框设计
**触发条件**:用户点击状态控制按钮
@@ -32,8 +43,19 @@
- 操作按钮:确认/取消
### 3. 状态管理集成
-**数据来源**:从localStorage中的`case_data_timeline`获取案件状态
+**数据来源**:从CaseDataContext获取案件状态
**更新机制**:API调用成功后重新加载案件数据
+
+### 4. 外呼气泡联动关闭
+**触发条件**:终止操作API调用成功后
+**实现机制**:
+1. FloatingControlPanel触发自定义事件`mediation-terminated`
+2. OutboundCallWidget监听该事件
+3. 事件处理:
+ - 设置isVisible=false关闭气泡
+ - 设置isMinimized=true最小化
+ - 清空localStorage外呼任务数据
+ - 清空组件状态中的通话列表
## API集成设计
@@ -56,15 +78,25 @@
## UI/UX设计
### 视觉设计
-**终止按钮**:
-- 背景色:#1A6FB8(项目主题蓝色,与人工接管按钮相同)
+**终止按钮**(红色渐变主题):
+- 背景:linear-gradient(135deg, #e63946 0%, #c1121f 100%)
+- 阴影:0 2px 8px rgba(230, 57, 70, 0.3)
- 文字颜色:白色
-- 悬停效果:背景色加深至#0d4a8a
+- 悬停效果:背景变为linear-gradient(135deg, #f04a57 0%, #d41926 100%)
+- 悬停阴影:0 4px 16px rgba(230, 57, 70, 0.4)
-**恢复按钮**:
-- 背景色:#52c41a(Ant Design success green,区别于人工接管的蓝色)
+**恢复按钮**(绿色渐变主题):
+- 背景:linear-gradient(135deg, #52c41a 0%, #389e0d 100%)
+- 阴影:0 2px 8px rgba(82, 196, 26, 0.3)
- 文字颜色:白色
-- 悬停效果:背景色加深
+- 悬停效果:背景变为linear-gradient(135deg, #5fd42b 0%, #42b417 100%)
+- 悬停阴影:0 4px 16px rgba(82, 196, 26, 0.4)
+
+**样式类名规范**:
+- 基础类:`.state-control-btn`
+- 终止变体:`.state-control-btn--terminate`
+- 恢复变体:`.state-control-btn--resume`
+- 与人工接管按钮样式(`.floating-control-btn`)完全隔离
### 交互设计
**加载状态**:
diff --git a/openspec/changes/implement-mediation-state-control/proposal.md b/openspec/changes/implement-mediation-state-control/proposal.md
index f0fbbe4..93499b3 100644
--- a/openspec/changes/implement-mediation-state-control/proposal.md
+++ b/openspec/changes/implement-mediation-state-control/proposal.md
@@ -23,12 +23,15 @@
## Success Criteria
- 按钮正确显示在人工接管按钮右侧
-- 状态为0或1时显示"终止"按钮(蓝色样式)
-- 状态为5时显示"恢复"按钮(绿色样式)
+- 状态为1时显示"终止"按钮(红色渐变样式)
+- 状态为5时显示"恢复"按钮(绿色渐变样式)
- 其他状态下不显示该按钮
- 点击按钮后正确显示确认对话框
- API调用成功后页面数据正确刷新
- API调用失败时显示相应错误提示
+- 终止操作成功后自动关闭外呼气泡组件
+- state=5时状态文本显示为"AI调解暂停中"
+- state=5时状态圆点显示为红色
## Risks & Mitigations
- **风险**:频繁的状态变更可能影响调解流程的一致性
diff --git a/openspec/changes/implement-mediation-state-control/specs/mediation-state-control/spec.md b/openspec/changes/implement-mediation-state-control/specs/mediation-state-control/spec.md
index 601ac74..003d038 100644
--- a/openspec/changes/implement-mediation-state-control/specs/mediation-state-control/spec.md
+++ b/openspec/changes/implement-mediation-state-control/specs/mediation-state-control/spec.md
@@ -5,18 +5,18 @@
### Requirement: 状态控制按钮显示逻辑
系统 SHALL 根据案件当前状态动态显示状态控制按钮。
-#### Scenario: 案件处于初始或进行中状态
-Given 案件状态为0(初始)或1(进行中)
+#### Scenario: 案件处于进行中状态
+Given 案件状态为1(进行中)
When 页面加载时
-Then 应显示"终止"按钮,样式为主题蓝色
+Then 应显示"终止"按钮,样式为红色渐变
#### Scenario: 案件处于暂停状态
Given 案件状态为5(暂停)
When 页面加载时
-Then 应显示"恢复"按钮,样式为主题绿色
+Then 应显示"恢复"按钮,样式为绿色渐变
#### Scenario: 案件处于其他状态
-Given 案件状态为2(成功)、3(失败)、4(终止)或其他状态
+Given 案件状态为0(初始)、2(成功)、3(失败)、4(人工接管)或其他状态
When 页面加载时
Then 不应显示状态控制按钮
@@ -44,7 +44,8 @@
Given 用户确认终止操作
When 系统调用ProcessAPIService.updateMediationState({action: 0})
And API返回成功响应
-Then 应显示成功消息"案件状态更新成功"
+Then 应显示成功消息"调解已终止"
+And 应触发mediation-terminated事件关闭外呼气泡
And 应重新加载当前页面数据
And 按钮状态应相应更新
@@ -52,7 +53,7 @@
Given 用户确认恢复操作
When 系统调用ProcessAPIService.updateMediationState({action: 1})
And API返回成功响应
-Then 应显示成功消息"案件状态更新成功"
+Then 应显示成功消息"调解已恢复"
And 应重新加载当前页面数据
And 按钮状态应相应更新
@@ -79,17 +80,50 @@
Then 应显示数据加载指示器
And 应暂时禁用用户交互
+### Requirement: 状态显示规则
+系统 SHALL 根据案件状态显示相应的状态文本和指示器颜色。
+
+#### Scenario: 进行中状态显示
+Given 案件状态为1(进行中)
+When 页面显示状态信息时
+Then 状态文本应显示为"调解进行中-阶段X:节点名称"
+And 状态圆点应显示为绿色
+
+#### Scenario: 暂停状态显示
+Given 案件状态为5(暂停)
+When 页面显示状态信息时
+Then 状态文本应显示为"AI调解暂停中"
+And 状态圆点应显示为红色(#e63946)
+
+### Requirement: 外呼气泡联动关闭
+终止操作成功后 SHALL 自动关闭外呼气泡组件。
+
+#### Scenario: 终止成功后关闭外呼气泡
+Given 用户成功执行终止操作
+When API返回成功响应
+Then 系统应触发自定义事件"mediation-terminated"
+And OutboundCallWidget应监听该事件
+And 外呼气泡应自动关闭(isVisible=false)
+And 外呼任务数据应从localStorage中清除
+
## MODIFIED Requirements
-### Requirement: 现有按钮布局调整
-人工接管按钮的布局 SHALL 为新按钮预留空间。
+### Requirement: 按钮组件位置调整
+状态控制按钮 SHALL 位于FloatingControlPanel组件中。
#### Scenario: 按钮容器布局
-Given 页面包含人工接管按钮
+Given 页面包含FloatingControlPanel组件
When 添加状态控制按钮后
-Then 两个按钮应水平排列
-And 状态控制按钮应位于人工接管按钮右侧
-And 按钮间应有适当的间距
+Then 状态控制按钮和人工接管按钮应水平排列
+And 状态控制按钮应位于人工接管按钮左侧
+And 按钮间应有15px的间距
+
+#### Scenario: 样式隔离
+Given 页面同时包含状态控制按钮和人工接管按钮
+When 渲染页面时
+Then 状态控制按钮应使用独立的CSS类名(state-control-btn)
+And 人工接管按钮应使用独立的CSS类名(floating-control-btn)
+And 两种按钮样式应完全隔离互不影响
## REMOVED Requirements
无
diff --git a/openspec/changes/implement-mediation-state-control/tasks.md b/openspec/changes/implement-mediation-state-control/tasks.md
index 0e240e2..d02cf97 100644
--- a/openspec/changes/implement-mediation-state-control/tasks.md
+++ b/openspec/changes/implement-mediation-state-control/tasks.md
@@ -9,21 +9,28 @@
### Phase 2: 前端实现
- [x] 在TabContainer组件中添加状态控制按钮
+- [x] 将按钮逻辑迁移到FloatingControlPanel组件
- [x] 实现按钮显示逻辑(根据案件状态动态显示)
- [x] 添加确认对话框组件
- [x] 实现API调用逻辑
- [x] 添加页面刷新机制
- [x] 实现错误处理和提示
+- [x] 实现终止后外呼气泡联动关闭功能
### Phase 3: 样式和交互优化
-- [x] 调整按钮样式(终止按钮蓝色,恢复按钮绿色)
+- [x] 调整按钮样式(终止按钮红色渐变,恢复按钮绿色渐变)
+- [x] 实现状态文本显示规则(state=5显示"AI调解暂停中")
+- [x] 实现状态圆点颜色规则(state=5显示红色)
- [x] 优化确认对话框的用户体验
- [x] 添加加载状态指示
- [x] 确保响应式设计兼容性
+- [x] 使用独立CSS类名实现样式隔离
### Phase 4: 测试和验证
-- [ ] 单元测试按钮显示逻辑
-- [ ] 集成测试API调用流程
+- [x] 单元测试按钮显示逻辑
+- [x] 集成测试API调用流程
+- [x] 测试state=5状态显示(文本和圆点颜色)
+- [x] 测试外呼气泡联动关闭功能
- [ ] 用户验收测试
- [ ] 性能测试(确保不会影响页面加载速度)
- [ ] 跨浏览器兼容性测试
diff --git a/web-app/src/App.css b/web-app/src/App.css
index c2f4790..84b67ec 100644
--- a/web-app/src/App.css
+++ b/web-app/src/App.css
@@ -549,15 +549,15 @@
transform: none;
}
-/* 终止按钮 - 蓝色主题 */
+/* 终止按钮 - 红色渐变主题 */
.state-control-btn--terminate {
- background: linear-gradient(135deg, #1A6FB8 0%, #0d4a8a 100%);
- box-shadow: 0 2px 8px rgba(26, 111, 184, 0.3);
+ background: linear-gradient(135deg, #e63946 0%, #c1121f 100%);
+ box-shadow: 0 2px 8px rgba(230, 57, 70, 0.3);
}
.state-control-btn--terminate:hover {
- background: linear-gradient(135deg, #1d7fcc 0%, #0f55a0 100%);
- box-shadow: 0 4px 16px rgba(26, 111, 184, 0.4);
+ background: linear-gradient(135deg, #f04a57 0%, #d41926 100%);
+ box-shadow: 0 4px 16px rgba(230, 57, 70, 0.4);
}
/* 恢复按钮 - 绿色主题 */
diff --git a/web-app/src/components/common/OutboundCallWidget.jsx b/web-app/src/components/common/OutboundCallWidget.jsx
index 2fe026b..9bead7c 100644
--- a/web-app/src/components/common/OutboundCallWidget.jsx
+++ b/web-app/src/components/common/OutboundCallWidget.jsx
@@ -357,10 +357,22 @@
};
window.addEventListener('outbound-jobs-updated', handleOutboundJobsUpdated);
+ // 监听调解终止事件(关闭外呼气泡)
+ const handleMediationTerminated = () => {
+ console.log('收到调解终止事件,关闭外呼气泡');
+ setIsVisible(false);
+ setIsMinimized(true);
+ // 清空localStorage中的外呼任务
+ localStorage.removeItem(OUTBOUND_JOBS_KEY);
+ setCalls([]);
+ };
+ window.addEventListener('mediation-terminated', handleMediationTerminated);
+
// 清理函数
return () => {
clearInterval(interval);
window.removeEventListener('outbound-jobs-updated', handleOutboundJobsUpdated);
+ window.removeEventListener('mediation-terminated', handleMediationTerminated);
isMountedRef.current = false;
};
}, [fetchCallStatus]);
diff --git a/web-app/src/components/dashboard/FloatingControlPanel.jsx b/web-app/src/components/dashboard/FloatingControlPanel.jsx
index 1f94fd6..c3f5001 100644
--- a/web-app/src/components/dashboard/FloatingControlPanel.jsx
+++ b/web-app/src/components/dashboard/FloatingControlPanel.jsx
@@ -107,9 +107,15 @@
const { formattedTime } = useTaskTimer(taskStartTime, isTaskTimeFallback);
// 生成状态文本
- const statusText = state === 1
- ? `调解进行中-阶段${orderNo}:${nodeName}`
- : (translateMediationState(state) || '调解进行中');
+ const getStatusText = () => {
+ const stateNum = Number(state);
+ if (stateNum === 1) {
+ return `调解进行中-阶段${orderNo}:${nodeName}`;
+ }
+ return translateMediationState(state) || '调解进行中';
+ };
+
+ const statusText = getStatusText();
// ==================== 状态控制按钮逻辑 ====================
@@ -182,6 +188,12 @@
setControlConfirmVisible(false);
setRemark('');
setControlAction(null);
+
+ // 如果是终止操作,触发事件关闭外呼气泡
+ if (controlAction === 'terminate') {
+ window.dispatchEvent(new CustomEvent('mediation-terminated'));
+ console.log('调解终止,触发外呼气泡关闭事件');
+ }
refreshData();
} catch (error) {
@@ -308,11 +320,22 @@
* 渲染控制区域(按钮或印章)
*/
const renderControlAction = () => {
+ const stateNum = Number(state);
+
// 终态状态(调解成功/失败/人工接管):显示印章
- if (TERMINAL_STATES.includes(state) || state === TAKEOVER_STATE) {
- return <TakeoverStamp state={state} />;
+ if (TERMINAL_STATES.includes(stateNum) || stateNum === TAKEOVER_STATE) {
+ return <TakeoverStamp state={stateNum} />;
}
+ // 已终止/暂停状态(5):显示恢复按钮和人工接管按钮
+ if (stateNum === PAUSED_STATE) {
+ return (
+ <>
+ {renderStateControlButton()}
+ <TakeoverButton loading={takeoverLoading} onClick={handleTakeover} />
+ </>
+ );
+ }
// 调解中(1):显示终止按钮和人工接管按钮
return (
@@ -328,7 +351,7 @@
<div className="floating-control-panel">
<div className="control-status">
<div className="status-indicator">
- <span className="status-dot"></span>
+ <span className="status-dot" style={Number(state) === 5 ? { background: '#e63946' } : {}}></span>
<span className="status-text">
{statusText}{' '}
<span style={{ color: 'gray' }}>
diff --git a/web-app/src/contexts/CaseDataContext.jsx b/web-app/src/contexts/CaseDataContext.jsx
index bfce87a..48e6f11 100644
--- a/web-app/src/contexts/CaseDataContext.jsx
+++ b/web-app/src/contexts/CaseDataContext.jsx
@@ -369,20 +369,6 @@
return;
}
- EvidenceAPIService.processCaseFilesOcr(params.caseId).catch((ocrError) => {
- console.error('触发案件文件OCR失败:', ocrError);
- });
-
- try {
- await OutboundBotAPIService.syncStatusByCase({ caseId: params.caseId });
- } catch (syncError) {
- console.error('同步外呼状态失败:', syncError);
- }
- try {
- await OutboundBotAPIService.backfillConversationByCase({ caseId: params.caseId });
- } catch (backfillError) {
- console.error('回补通话记录失败:', backfillError);
- }
// 调用API获取数据
// 将URL中的auth_token转换为authorization传入API
@@ -447,6 +433,21 @@
// 加载任务时间数据
await loadTaskTime(timelineData);
+ EvidenceAPIService.processCaseFilesOcr(params.caseId).catch((ocrError) => {
+ console.error('触发案件文件OCR失败:', ocrError);
+ });
+
+ try {
+ await OutboundBotAPIService.syncStatusByCase({ caseId: params.caseId });
+ } catch (syncError) {
+ console.error('同步外呼状态失败:', syncError);
+ }
+ try {
+ await OutboundBotAPIService.backfillConversationByCase({ caseId: params.caseId });
+ } catch (backfillError) {
+ console.error('回补通话记录失败:', backfillError);
+ }
+
console.log('Case data loaded successfully:', timelineData);
} catch (err) {
console.error('Failed to load case data:', err);
diff --git a/web-app/src/utils/stateTranslator.js b/web-app/src/utils/stateTranslator.js
index b3550b5..ebda6b2 100644
--- a/web-app/src/utils/stateTranslator.js
+++ b/web-app/src/utils/stateTranslator.js
@@ -14,9 +14,10 @@
1: '调解中',
2: '调解成功',
3: '调解失败',
- 4: '人工接管'
+ 4: '人工接管',
+ 5: 'AI调解已暂停'
};
-
+
return stateMap[state] || '未知状态';
};
--
Gitblit v1.8.0