feat: 完善AI调解状态控制功能及相关文档更新
- 实现state=5状态的特殊显示规则(文本'AI调解暂停中',红色圆点)
- 完成终止按钮红色渐变样式和恢复按钮绿色渐变样式
- 实现终止操作成功后自动关闭外呼气泡的联动功能
- 将状态控制按钮逻辑从TabContainer迁移至FloatingControlPanel
- 更新完整的OpenSpec文档,包括proposal、design、tasks和spec规格说明
- 使用CustomEvent实现组件间通信,确保UI状态同步
- 完善CSS类名职责分离,保证样式隔离不影响人工接管按钮
| | |
| | | ## 组件设计 |
| | | |
| | | ### 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. 确认对话框设计 |
| | | **触发条件**:用户点击状态控制按钮 |
| | |
| | | - 操作按钮:确认/取消 |
| | | |
| | | ### 3. 状态管理集成 |
| | | **数据来源**:从localStorage中的`case_data_timeline`获取案件状态 |
| | | **数据来源**:从CaseDataContext获取案件状态 |
| | | **更新机制**:API调用成功后重新加载案件数据 |
| | | |
| | | ### 4. 外呼气泡联动关闭 |
| | | **触发条件**:终止操作API调用成功后 |
| | | **实现机制**: |
| | | 1. FloatingControlPanel触发自定义事件`mediation-terminated` |
| | | 2. OutboundCallWidget监听该事件 |
| | | 3. 事件处理: |
| | | - 设置isVisible=false关闭气泡 |
| | | - 设置isMinimized=true最小化 |
| | | - 清空localStorage外呼任务数据 |
| | | - 清空组件状态中的通话列表 |
| | | |
| | | ## API集成设计 |
| | | |
| | |
| | | ## 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`)完全隔离 |
| | | |
| | | ### 交互设计 |
| | | **加载状态**: |
| | |
| | | |
| | | ## Success Criteria |
| | | - 按钮正确显示在人工接管按钮右侧 |
| | | - 状态为0或1时显示"终止"按钮(蓝色样式) |
| | | - 状态为5时显示"恢复"按钮(绿色样式) |
| | | - 状态为1时显示"终止"按钮(红色渐变样式) |
| | | - 状态为5时显示"恢复"按钮(绿色渐变样式) |
| | | - 其他状态下不显示该按钮 |
| | | - 点击按钮后正确显示确认对话框 |
| | | - API调用成功后页面数据正确刷新 |
| | | - API调用失败时显示相应错误提示 |
| | | - 终止操作成功后自动关闭外呼气泡组件 |
| | | - state=5时状态文本显示为"AI调解暂停中" |
| | | - state=5时状态圆点显示为红色 |
| | | |
| | | ## Risks & Mitigations |
| | | - **风险**:频繁的状态变更可能影响调解流程的一致性 |
| | |
| | | ### 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 不应显示状态控制按钮 |
| | | |
| | |
| | | Given 用户确认终止操作 |
| | | When 系统调用ProcessAPIService.updateMediationState({action: 0}) |
| | | And API返回成功响应 |
| | | Then 应显示成功消息"案件状态更新成功" |
| | | Then 应显示成功消息"调解已终止" |
| | | And 应触发mediation-terminated事件关闭外呼气泡 |
| | | And 应重新加载当前页面数据 |
| | | And 按钮状态应相应更新 |
| | | |
| | |
| | | Given 用户确认恢复操作 |
| | | When 系统调用ProcessAPIService.updateMediationState({action: 1}) |
| | | And API返回成功响应 |
| | | Then 应显示成功消息"案件状态更新成功" |
| | | Then 应显示成功消息"调解已恢复" |
| | | And 应重新加载当前页面数据 |
| | | And 按钮状态应相应更新 |
| | | |
| | |
| | | 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 |
| | | 无 |
| | |
| | | |
| | | ### 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] 测试外呼气泡联动关闭功能 |
| | | - [ ] 用户验收测试 |
| | | - [ ] 性能测试(确保不会影响页面加载速度) |
| | | - [ ] 跨浏览器兼容性测试 |
| | |
| | | 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); |
| | | } |
| | | |
| | | /* 恢复按钮 - 绿色主题 */ |
| | |
| | | }; |
| | | 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]); |
| | |
| | | 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(); |
| | | |
| | | // ==================== 状态控制按钮逻辑 ==================== |
| | | |
| | |
| | | setControlConfirmVisible(false); |
| | | setRemark(''); |
| | | setControlAction(null); |
| | | |
| | | // 如果是终止操作,触发事件关闭外呼气泡 |
| | | if (controlAction === 'terminate') { |
| | | window.dispatchEvent(new CustomEvent('mediation-terminated')); |
| | | console.log('调解终止,触发外呼气泡关闭事件'); |
| | | } |
| | | |
| | | refreshData(); |
| | | } catch (error) { |
| | |
| | | * 渲染控制区域(按钮或印章) |
| | | */ |
| | | 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 ( |
| | |
| | | <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' }}> |
| | |
| | | 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 |
| | |
| | | // 加载任务时间数据 |
| | | 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); |
| | |
| | | 1: '调解中', |
| | | 2: '调解成功', |
| | | 3: '调解失败', |
| | | 4: '人工接管' |
| | | 4: '人工接管', |
| | | 5: 'AI调解已暂停' |
| | | }; |
| | | |
| | | |
| | | return stateMap[state] || '未知状态'; |
| | | }; |
| | | |