| | |
| | | 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); |
| | | |
| | | /** |
| | | * 智能外呼通话显示组件 |
| | | * 显示在页面右下角的气泡组件,支持多人通话 |
| | | * @param {Object} props |
| | | * @param {Function} props.onSwitchTab - Tab切换回调 |
| | | * @param {Function} props.onRefreshData - 数据刷新回调 |
| | | */ |
| | | const OutboundCallWidget = () => { |
| | | 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 isMountedRef = useRef(true); |
| | | |
| | |
| | | }; |
| | | |
| | | /** |
| | | * 批量更新通话状态到后端 |
| | | * @param {Array} jobsToUpdate - 需要更新的任务列表 |
| | | * @returns {Promise<boolean>} 是否有成功的更新 |
| | | */ |
| | | const updateCallStatusToBackend = async (jobsToUpdate) => { |
| | | if (!jobsToUpdate || jobsToUpdate.length === 0) return false; |
| | | |
| | | try { |
| | | // 并行调用所有任务的更新API |
| | | 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: statusToUpdate |
| | | }); |
| | | console.log(`状态更新成功: ${job.jobId} -> ${statusToUpdate}`); |
| | | return { success: true, job }; |
| | | } catch (err) { |
| | | console.error(`状态更新失败: ${job.jobId}`, err); |
| | | return { success: false, job }; |
| | | } |
| | | }) |
| | | ); |
| | | |
| | | // 检查是否有成功的更新 |
| | | const hasSuccess = results.some(r => r.success); |
| | | return hasSuccess; |
| | | } catch (err) { |
| | | console.error('批量更新状态失败:', err); |
| | | return false; |
| | | } |
| | | }; |
| | | |
| | | /** |
| | | * 触发页面更新(刷新数据 + 切换Tab) |
| | | */ |
| | | const triggerPageUpdate = useCallback(() => { |
| | | // 刷新案件数据 |
| | | if (onRefreshData) { |
| | | onRefreshData(); |
| | | } |
| | | // 切换到AI调解实时看板 |
| | | if (onSwitchTab) { |
| | | onSwitchTab('mediation-board'); |
| | | } |
| | | }, [onRefreshData, onSwitchTab]); |
| | | |
| | | /** |
| | | * 移除终态或超时的任务 |
| | | * @param {Array} jobs - 当前任务数组 |
| | | * @returns {Array} 清理后的任务数组 |
| | |
| | | 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) { |
| | |
| | | return; |
| | | } |
| | | |
| | | // 收集需要更新到后端的任务(状态变化且原状态不是Scheduling) |
| | | const jobsNeedBackendUpdate = []; |
| | | |
| | | // 并行查询所有任务的状态 |
| | | const updatedJobs = await Promise.all( |
| | | successJobs.map(async (job) => { |
| | |
| | | |
| | | 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 (backendStatus !== SCHEDULING_STATUS && BACKEND_STATUSES.includes(backendStatus)) { |
| | | jobsNeedBackendUpdate.push({ |
| | | ...job, |
| | | newStatus, |
| | | backendStatus |
| | | }); |
| | | } |
| | | |
| | | // 如果是终态,可以从轮询中移除 |
| | | if (!ACTIVE_STATUSES.includes(newStatus)) { |
| | | if (!isActiveStatus(newStatus)) { |
| | | console.log(`任务 ${job.jobId} 达到终态: ${newStatus}`); |
| | | return null; // 标记为删除 |
| | | } |
| | |
| | | // 清理超时任务 |
| | | const cleanedJobs = cleanupJobs(filteredJobs); |
| | | |
| | | // 如果有需要更新到后端的任务,批量调用更新API |
| | | if (jobsNeedBackendUpdate.length > 0) { |
| | | const hasUpdateSuccess = await updateCallStatusToBackend(jobsNeedBackendUpdate); |
| | | |
| | | // 如果有成功的更新,触发页面更新 |
| | | if (hasUpdateSuccess) { |
| | | triggerPageUpdate(); |
| | | } |
| | | } |
| | | |
| | | // 保存到 localStorage |
| | | saveJobsToStorage(cleanedJobs); |
| | | |
| | |
| | | setIsVisible(true); |
| | | } |
| | | } |
| | | }, [isVisible]); |
| | | }, [isVisible, triggerPageUpdate]); |
| | | |
| | | // 定时轮询通话状态 |
| | | useEffect(() => { |
| | |
| | | // 设置轮询定时器(10秒间隔) |
| | | const interval = setInterval(fetchCallStatus, POLL_INTERVAL); |
| | | |
| | | // 监听外呼任务更新事件(立即刷新) |
| | | const handleOutboundJobsUpdated = () => { |
| | | console.log('收到外呼任务更新事件,立即刷新'); |
| | | fetchCallStatus(); |
| | | }; |
| | | window.addEventListener('outbound-jobs-updated', handleOutboundJobsUpdated); |
| | | |
| | | // 清理函数 |
| | | return () => { |
| | | clearInterval(interval); |
| | | window.removeEventListener('outbound-jobs-updated', handleOutboundJobsUpdated); |
| | | isMountedRef.current = false; |
| | | }; |
| | | }, [fetchCallStatus]); |
| | |
| | | {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; |