tony.cheng
2026-03-17 0c8432f4ff5c95faca4ca62d6a4ec4618feba627
feat: 完善AI调解状态控制功能及相关文档更新

- 实现state=5状态的特殊显示规则(文本'AI调解暂停中',红色圆点)
- 完成终止按钮红色渐变样式和恢复按钮绿色渐变样式
- 实现终止操作成功后自动关闭外呼气泡的联动功能
- 将状态控制按钮逻辑从TabContainer迁移至FloatingControlPanel
- 更新完整的OpenSpec文档,包括proposal、design、tasks和spec规格说明
- 使用CustomEvent实现组件间通信,确保UI状态同步
- 完善CSS类名职责分离,保证样式隔离不影响人工接管按钮
9 files modified
231 ■■■■ changed files
openspec/changes/implement-mediation-state-control/design.md 60 ●●●● patch | view | raw | blame | history
openspec/changes/implement-mediation-state-control/proposal.md 7 ●●●● patch | view | raw | blame | history
openspec/changes/implement-mediation-state-control/specs/mediation-state-control/spec.md 60 ●●●● patch | view | raw | blame | history
openspec/changes/implement-mediation-state-control/tasks.md 13 ●●●● patch | view | raw | blame | history
web-app/src/App.css 10 ●●●● patch | view | raw | blame | history
web-app/src/components/common/OutboundCallWidget.jsx 12 ●●●●● patch | view | raw | blame | history
web-app/src/components/dashboard/FloatingControlPanel.jsx 35 ●●●● patch | view | raw | blame | history
web-app/src/contexts/CaseDataContext.jsx 29 ●●●● patch | view | raw | blame | history
web-app/src/utils/stateTranslator.js 5 ●●●●● patch | view | raw | blame | history
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`)完全隔离
### 交互设计
**加载状态**:
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
- **风险**:频繁的状态变更可能影响调解流程的一致性
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
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] 测试外呼气泡联动关闭功能
- [ ] 用户验收测试
- [ ] 性能测试(确保不会影响页面加载速度)
- [ ] 跨浏览器兼容性测试
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);
}
/* 恢复按钮 - 绿色主题 */
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]);
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' }}>
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);
web-app/src/utils/stateTranslator.js
@@ -14,9 +14,10 @@
    1: '调解中',
    2: '调解成功',
    3: '调解失败',
    4: '人工接管'
    4: '人工接管',
    5: 'AI调解已暂停'
  };
  return stateMap[state] || '未知状态';
};