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