| | |
| | | import React, { useState, useEffect, useCallback, useRef } from 'react'; |
| | | import { useCaseData } from '../../contexts/CaseDataContext'; |
| | | import OutboundBotAPIService from '../../services/OutboundBotAPIService'; |
| | | import ProcessAPIService from '../../services/ProcessAPIService'; |
| | | import { message } from 'antd'; |
| | | |
| | | const OUTBOUND_JOBS_KEY = 'outbound_call_jobs'; |
| | | |
| | | // 活跃状态列表 |
| | | const ACTIVE_STATUSES = ['Scheduling', 'InProgress', 'Calling', 'Ringing', 'Answered']; |
| | | const ACTIVE_STATUSES = ['Scheduling', 'Executing', 'Paused', 'Drafted', 'InProgress', 'Calling', 'Ringing', 'Answered']; |
| | | |
| | | // Scheduling 状态 - 此状态变化不需要调用更新API |
| | | const SCHEDULING_STATUS = 'Scheduling'; |
| | | const BACKEND_STATUSES = ['Scheduling', 'Executing', 'Succeeded', 'Paused', 'Failed', 'Cancelled', 'Drafted']; |
| | | const STATUS_TO_BACKEND = { |
| | | InProgress: 'Executing', |
| | | Calling: 'Executing', |
| | | Ringing: 'Executing', |
| | | Answered: 'Executing' |
| | | }; |
| | | const isActiveStatus = (status) => !status || ACTIVE_STATUSES.includes(status); |
| | | |
| | | /** |
| | | * 智能外呼通话显示组件 |
| | |
| | | const OutboundCallWidget = ({ onSwitchTab, onRefreshData }) => { |
| | | const { caseData } = useCaseData(); |
| | | const [isVisible, setIsVisible] = useState(false); // 默认隐藏 |
| | | const [isMinimized, setIsMinimized] = useState(true); |
| | | const [isMinimized, setIsMinimized] = useState(false); // 默认展开(非最小化) |
| | | const [calls, setCalls] = useState([]); |
| | | const [mediationRecords, setMediationRecords] = useState([]); // AI调解记录 |
| | | const isMountedRef = useRef(true); |
| | | |
| | | // 轮询间隔(毫秒) |
| | |
| | | |
| | | // 获取 caseId |
| | | const caseId = caseData?.caseId || caseData?.case_id; |
| | | |
| | | // 获取 mediationId |
| | | const mediationId = caseData?.mediation?.id; |
| | | |
| | | // 加载AI调解记录(无Loading效果) |
| | | const loadMediationRecords = useCallback(async () => { |
| | | if (!mediationId) return; |
| | | |
| | | try { |
| | | const response = await ProcessAPIService.getProcessRecords({ |
| | | mediation_id: mediationId |
| | | }); |
| | | |
| | | if (isMountedRef.current) { |
| | | setMediationRecords(response.data || []); |
| | | console.log('AI调解记录加载成功:', response.data?.length || 0, '条'); |
| | | } |
| | | } catch (err) { |
| | | console.error('加载AI调解记录失败:', err); |
| | | // 不显示错误提示,不设置loading状态 |
| | | } |
| | | }, [mediationId]); |
| | | |
| | | // 格式化通话时长 |
| | | const formatDuration = (seconds) => { |
| | |
| | | } |
| | | }); |
| | | |
| | | // 获取成功任务的 personId 集合 |
| | | const successPersonIds = new Set(successJobs.map(job => job.personId)); |
| | | |
| | | // 过滤掉已有成功任务的 personId 对应的失败任务(成功任务优先) |
| | | const filteredFailedJobs = uniqueFailedJobs.filter(job => !successPersonIds.has(job.personId)); |
| | | |
| | | // 合并所有任务 |
| | | return [...successJobs, ...uniqueFailedJobs]; |
| | | return [...successJobs, ...filteredFailedJobs]; |
| | | } catch (err) { |
| | | console.error('读取外呼任务失败:', err); |
| | | return []; |
| | |
| | | const results = await Promise.all( |
| | | jobsToUpdate.map(async (job) => { |
| | | try { |
| | | const statusToUpdate = job.backendStatus || job.newStatus; |
| | | if (!statusToUpdate) { |
| | | return { success: false, job }; |
| | | } |
| | | await OutboundBotAPIService.updateCallStatus({ |
| | | jobId: job.jobId, |
| | | callStatus: job.newStatus |
| | | callStatus: statusToUpdate |
| | | }); |
| | | console.log(`状态更新成功: ${job.jobId} -> ${job.newStatus}`); |
| | | console.log(`状态更新成功: ${job.jobId} -> ${statusToUpdate}`); |
| | | return { success: true, job }; |
| | | } catch (err) { |
| | | console.error(`状态更新失败: ${job.jobId}`, err); |
| | |
| | | const now = Date.now(); |
| | | return jobs.filter(job => { |
| | | // 检查是否为活跃状态 |
| | | if (ACTIVE_STATUSES.includes(job.callStatus)) { |
| | | if (isActiveStatus(job.callStatus)) { |
| | | // 检查是否超时(2小时) |
| | | const elapsed = now - (job.pollStartTime || job.startTime || now); |
| | | if (elapsed > 2 * 60 * 60 * 1000) { |
| | |
| | | const storedJobs = loadJobsFromStorage(); |
| | | |
| | | // 分离成功任务和失败任务 |
| | | const successJobs = storedJobs.filter(job => !job.errorCode && ACTIVE_STATUSES.includes(job.callStatus)); |
| | | const successJobs = storedJobs.filter(job => !job.errorCode && isActiveStatus(job.callStatus)); |
| | | const failedJobs = storedJobs.filter(job => job.errorCode > 0); |
| | | |
| | | if (successJobs.length === 0) { |
| | |
| | | |
| | | if (response?.data) { |
| | | const newStatus = response.data.callStatus; |
| | | if (!newStatus) { |
| | | return job; |
| | | } |
| | | const backendStatus = STATUS_TO_BACKEND[newStatus] || newStatus; |
| | | |
| | | // 如果状态发生变化,更新任务 |
| | | if (newStatus !== job.callStatus) { |
| | | console.log(`任务 ${job.jobId} 状态更新: ${job.callStatus} -> ${newStatus}`); |
| | | |
| | | // 检查是否需要调用后端更新API(排除Scheduling状态) |
| | | if (job.callStatus !== SCHEDULING_STATUS) { |
| | | if (backendStatus !== SCHEDULING_STATUS && BACKEND_STATUSES.includes(backendStatus)) { |
| | | jobsNeedBackendUpdate.push({ |
| | | ...job, |
| | | newStatus |
| | | newStatus, |
| | | backendStatus |
| | | }); |
| | | } |
| | | |
| | | // 如果是终态,可以从轮询中移除 |
| | | if (!ACTIVE_STATUSES.includes(newStatus)) { |
| | | if (!isActiveStatus(newStatus)) { |
| | | console.log(`任务 ${job.jobId} 达到终态: ${newStatus}`); |
| | | return null; // 标记为删除 |
| | | } |
| | |
| | | |
| | | // 定时轮询通话状态 |
| | | useEffect(() => { |
| | | // 组件挂载时设置为 true |
| | | isMountedRef.current = true; |
| | | |
| | | // 初始加载 |
| | | fetchCallStatus(); |
| | | loadMediationRecords(); // 初始加载调解记录 |
| | | |
| | | // 设置轮询定时器(10秒间隔) |
| | | const interval = setInterval(fetchCallStatus, POLL_INTERVAL); |
| | | const interval = setInterval(() => { |
| | | fetchCallStatus(); |
| | | loadMediationRecords(); // 每10秒加载一次AI调解记录 |
| | | }, POLL_INTERVAL); |
| | | |
| | | // 监听外呼任务更新事件(立即刷新) |
| | | const handleOutboundJobsUpdated = () => { |
| | | console.log('收到外呼任务更新事件,立即刷新'); |
| | | fetchCallStatus(); |
| | | }; |
| | | 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]); |
| | | }, [fetchCallStatus, loadMediationRecords]); |
| | | |
| | | // 关闭气泡 |
| | | const handleClose = (e) => { |
| | |
| | | // 其他状态的任务正常显示 |
| | | return true; |
| | | }); |
| | | |
| | | // 如果没有活跃任务且不可见,不渲染任何内容 |
| | | if (activeCalls.length === 0 && !isVisible) { |
| | | return null; |
| | | } |
| | | |
| | | // 如果最小化,显示AI客服图标 |
| | | if (isMinimized) { |
| | |
| | | {call.errorCode > 0 ? ( |
| | | // 失败任务显示 |
| | | <span> |
| | | {call.perClassName || '联系人'} |
| | | {call.perTypeName || '联系人'} |
| | | {call.trueName && `(${call.trueName})`}: |
| | | {call.message} |
| | | </span> |
| | | ) : ( |
| | | // 成功任务显示 |
| | | // 成功任务显示 - 使用 perTypeName 字段(申请方当事人/被申请方当事人) |
| | | <span> |
| | | 正在与{call.perClassName || '申请方'}({call.trueName || call.personId})电话沟通中... |
| | | 正在与{call.perTypeName || '申请方当事人'}({call.trueName || call.personId})电话沟通中... |
| | | </span> |
| | | )} |
| | | </div> |
| | |
| | | ); |
| | | }; |
| | | |
| | | export default OutboundCallWidget; |
| | | export default OutboundCallWidget; |