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', '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 = ({ onSwitchTab, onRefreshData }) => {
|
const { caseData, refreshNodeData } = useCaseData();
|
const [isVisible, setIsVisible] = useState(false); // 默认隐藏
|
const [isMinimized, setIsMinimized] = useState(false); // 默认展开(非最小化)
|
const [calls, setCalls] = useState([]);
|
const [mediationRecords, setMediationRecords] = useState([]); // AI调解记录
|
const [aiProcessingTasks, setAiProcessingTasks] = useState([]); // AI处理中任务
|
const isMountedRef = useRef(true);
|
|
// 轮询间隔(毫秒)
|
const POLL_INTERVAL = 2000; // 2秒
|
|
// 节点数据轻量刷新间隔(毫秒)—— 与主轮询对齐,避免节点进度延迟
|
const NODE_REFRESH_INTERVAL = 2000; // 2秒
|
|
// 最大重试次数
|
const MAX_RETRY_COUNT = 10;
|
|
// 获取 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) => {
|
const mins = Math.floor(seconds / 60);
|
const secs = seconds % 60;
|
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
};
|
|
/**
|
* 从 localStorage 读取外呼任务(包括成功和失败的任务)
|
* @returns {Array} 任务数组
|
*/
|
const loadJobsFromStorage = () => {
|
try {
|
// 读取成功的任务
|
const storedSuccess = localStorage.getItem(OUTBOUND_JOBS_KEY);
|
const successJobs = storedSuccess ? JSON.parse(storedSuccess) : [];
|
|
// 读取失败的任务
|
const storedFailed = localStorage.getItem(`${OUTBOUND_JOBS_KEY}_failed`);
|
const failedJobs = storedFailed ? JSON.parse(storedFailed) : [];
|
console.log('读取失败任务:', failedJobs);
|
|
// 清理失败任务 - 按 errorCode 不同策略
|
const now = Date.now();
|
const cleanedFailedJobs = failedJobs.filter(job => {
|
// errorCode: 1001 - 超过 startTime 就清理
|
if (job.errorCode === 1001) {
|
const jobStartTime = typeof job.startTime === 'string' ? new Date(job.startTime).getTime() : job.startTime;
|
return now < jobStartTime; // 当前时间小于 startTime 才保留
|
}
|
|
// errorCode: 1002 - 跨天清理 (比较日期是否不同)
|
if (job.errorCode === 1002) {
|
const startDate = new Date(job.startTime);
|
const nowDate = new Date(now);
|
|
// 格式化为 YYYY-MM-DD 进行比较
|
const startDateString = startDate.getFullYear() + '-' +
|
String(startDate.getMonth() + 1).padStart(2, '0') + '-' +
|
String(startDate.getDate()).padStart(2, '0');
|
|
const nowDateString = nowDate.getFullYear() + '-' +
|
String(nowDate.getMonth() + 1).padStart(2, '0') + '-' +
|
String(nowDate.getDate()).padStart(2, '0');
|
|
// 如果日期相同,保留;如果日期不同,清理
|
return startDateString === nowDateString;
|
}
|
|
// 其他 errorCode 使用默认 24 小时策略
|
return (now - job.startTime) < 24 * 60 * 60 * 1000;
|
});
|
|
// 如果清理后数量变化,更新 localStorage
|
if (cleanedFailedJobs.length !== failedJobs.length) {
|
localStorage.setItem(`${OUTBOUND_JOBS_KEY}_failed`, JSON.stringify(cleanedFailedJobs));
|
}
|
|
// 按 personId 去重失败任务
|
const uniqueFailedJobs = [];
|
const seenPersonIds = new Set();
|
cleanedFailedJobs.forEach(job => {
|
if (!seenPersonIds.has(job.personId)) {
|
seenPersonIds.add(job.personId);
|
uniqueFailedJobs.push(job);
|
}
|
});
|
|
// 获取成功任务的 personId 集合
|
const successPersonIds = new Set(successJobs.map(job => job.personId));
|
|
// 过滤掉已有成功任务的 personId 对应的失败任务(成功任务优先)
|
const filteredFailedJobs = uniqueFailedJobs.filter(job => !successPersonIds.has(job.personId));
|
|
// 合并所有任务
|
return [...successJobs, ...filteredFailedJobs];
|
} catch (err) {
|
console.error('读取外呼任务失败:', err);
|
return [];
|
}
|
};
|
|
/**
|
* 保存任务到 localStorage
|
* @param {Array} jobs - 任务数组
|
*/
|
const saveJobsToStorage = (jobs) => {
|
try {
|
if (jobs.length === 0) {
|
localStorage.removeItem(OUTBOUND_JOBS_KEY);
|
} else {
|
localStorage.setItem(OUTBOUND_JOBS_KEY, JSON.stringify(jobs));
|
}
|
} catch (err) {
|
console.error('保存外呼任务失败:', err);
|
}
|
};
|
|
/**
|
* [已废弃] 批量更新通话状态到后端
|
* 改造后状态更新由后端回调驱动,前端不再调用 update-status API
|
*/
|
// const updateCallStatusToBackend = async (jobsToUpdate) => { ... 已移除 };
|
|
/**
|
* 触发页面更新(轻量刷新节点数据 + 切换Tab)
|
* 不调用 onRefreshData(全量刷新会重复触发外呼/OCR),改用轻量级 refreshNodeData
|
*/
|
const triggerPageUpdate = useCallback(() => {
|
// 轻量刷新节点数据(仅 timeline + processNodes,不触发外呼等副作用)
|
if (refreshNodeData) {
|
console.log('[Widget] 终态检测 → 立即轻量刷新节点数据');
|
refreshNodeData();
|
}
|
// 切换到AI调解实时看板
|
if (onSwitchTab) {
|
onSwitchTab('mediation-board');
|
}
|
}, [refreshNodeData, onSwitchTab]);
|
|
/**
|
* 移除终态或超时的任务
|
* @param {Array} jobs - 当前任务数组
|
* @returns {Array} 清理后的任务数组
|
*/
|
const cleanupJobs = (jobs) => {
|
const now = Date.now();
|
return jobs.filter(job => {
|
// 检查是否为活跃状态
|
if (isActiveStatus(job.callStatus)) {
|
// 检查是否超时(2小时)
|
const elapsed = now - (job.pollStartTime || job.startTime || now);
|
if (elapsed > 2 * 60 * 60 * 1000) {
|
console.warn('外呼轮询超时(2小时),自动停止,jobId:', job.jobId);
|
return false;
|
}
|
return true;
|
}
|
return false;
|
});
|
};
|
|
/**
|
* 查询通话状态
|
*/
|
const fetchCallStatus = useCallback(async () => {
|
// ── Step 1: 从后端补充活跃记录(解决后端创建任务前端不知情的问题)──
|
if (mediationId) {
|
try {
|
const backendResp = await OutboundBotAPIService.getActiveRecords({ mediation_id: mediationId });
|
const backendJobs = backendResp?.data || [];
|
if (backendJobs.length > 0) {
|
const storedRaw = localStorage.getItem(OUTBOUND_JOBS_KEY);
|
const storedJobs = storedRaw ? JSON.parse(storedRaw) : [];
|
const storedJobIds = new Set(storedJobs.map(j => j.jobId));
|
let changed = false;
|
backendJobs.forEach(bj => {
|
if (!storedJobIds.has(bj.jobId)) {
|
const createTs = bj.createTime ? new Date(bj.createTime).getTime() : Date.now();
|
storedJobs.push({
|
...bj,
|
startTime: createTs,
|
pollStartTime: Date.now(),
|
retryCount: 0,
|
});
|
changed = true;
|
console.log('[Widget] 从后端补充活跃外呼任务:', bj.jobId, bj.perTypeName, bj.callStatus);
|
}
|
});
|
if (changed) {
|
localStorage.setItem(OUTBOUND_JOBS_KEY, JSON.stringify(storedJobs));
|
}
|
}
|
} catch (e) {
|
console.warn('[Widget] 从后端获取活跃记录失败:', e);
|
}
|
}
|
|
// ── Step 2: 从 localStorage 读取任务(与原逻辑一致)──
|
const storedJobs = loadJobsFromStorage();
|
|
// 分离成功任务和失败任务
|
const successJobs = storedJobs.filter(job => !job.errorCode && isActiveStatus(job.callStatus));
|
const failedJobs = storedJobs.filter(job => job.errorCode > 0);
|
|
if (successJobs.length === 0) {
|
// 没有活跃任务,更新状态并返回
|
if (isMountedRef.current) {
|
setCalls([...failedJobs]);
|
if (failedJobs.length > 0 && !isVisible) {
|
setIsVisible(true);
|
}
|
}
|
return;
|
}
|
|
// 收集需要更新到后端的任务(状态变化且原状态不是Scheduling)
|
const jobsNeedBackendUpdate = [];
|
|
// 并行查询所有任务的状态
|
const updatedJobs = await Promise.all(
|
successJobs.map(async (job) => {
|
try {
|
const response = await OutboundBotAPIService.getCallStatus({
|
caseRef: job.caseId,
|
phoneNumber: job.phoneNumber,
|
jobId: job.jobId
|
});
|
|
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 (!isActiveStatus(newStatus)) {
|
console.log(`任务 ${job.jobId} 达到终态: ${newStatus}`);
|
return null; // 标记为删除
|
}
|
|
return {
|
...job,
|
callStatus: newStatus,
|
pollStartTime: Date.now(), // 重置超时计时
|
retryCount: 0 // 重置重试计数
|
};
|
}
|
|
// 状态未变化,保留原任务
|
return job;
|
}
|
|
// API 返回空数据,保留原任务
|
return job;
|
} catch (err) {
|
console.error('获取通话状态失败:', err);
|
|
// 累加重试计数
|
const retryCount = job.retryCount + 1;
|
|
if (retryCount >= MAX_RETRY_COUNT) {
|
console.error('重试次数超限,jobId:', job.jobId);
|
message.error('外呼状态查询失败次数过多,已停止监控');
|
return null; // 标记为删除
|
}
|
|
return { ...job, retryCount };
|
}
|
})
|
);
|
|
// 过滤掉标记为删除的任务(null)
|
const filteredJobs = updatedJobs.filter(job => job !== null);
|
|
// 清理超时任务
|
const cleanedJobs = cleanupJobs(filteredJobs);
|
|
// 轮询仅用于 UI 展示,不再调用后端 update-status API
|
// 状态更新已由后端回调驱动,前端不再触发推进
|
|
// 如果有终态任务,触发页面更新
|
const hasTerminalJobs = jobsNeedBackendUpdate.length > 0;
|
if (hasTerminalJobs) {
|
triggerPageUpdate();
|
}
|
|
// 保存到 localStorage
|
saveJobsToStorage(cleanedJobs);
|
|
// 合并成功任务和失败任务
|
const allJobs = [...cleanedJobs, ...failedJobs];
|
|
// 更新组件状态
|
if (isMountedRef.current) {
|
setCalls(allJobs);
|
// 如果有任务,显示气泡
|
if (allJobs.length > 0 && !isVisible) {
|
setIsVisible(true);
|
}
|
}
|
}, [isVisible, triggerPageUpdate, mediationId]);
|
|
// 记录上一次 AI 处理任务数,用于检测 "处理中→完成" 变化
|
const prevAiTaskCountRef = useRef(0);
|
|
// 轮询 AI 处理状态
|
const fetchAiProcessingStatus = useCallback(async () => {
|
if (!mediationId) return;
|
try {
|
const resp = await OutboundBotAPIService.getAiProcessingStatus({ mediation_id: mediationId });
|
const tasks = resp?.data || [];
|
if (isMountedRef.current) {
|
const prevCount = prevAiTaskCountRef.current;
|
prevAiTaskCountRef.current = tasks.length;
|
setAiProcessingTasks(tasks);
|
|
// AI 任务从 "有" → "无"(处理完成),立即刷新节点数据以感知新节点推进
|
if (prevCount > 0 && tasks.length === 0) {
|
console.log('[Widget] AI处理全部完成 → 立即刷新节点数据');
|
if (refreshNodeData) refreshNodeData();
|
}
|
|
// 如果有 AI 处理中任务,确保悬浮窗可见
|
if (tasks.length > 0 && !isVisible) {
|
setIsVisible(true);
|
}
|
}
|
} catch (e) {
|
// 不影响主流程,静默失败
|
console.warn('[Widget] 查询AI处理状态失败:', e);
|
}
|
}, [mediationId, isVisible, refreshNodeData]);
|
|
// 定时轮询通话状态
|
useEffect(() => {
|
// 组件挂载时设置为 true
|
isMountedRef.current = true;
|
|
// 初始加载
|
fetchCallStatus();
|
loadMediationRecords(); // 初始加载调解记录
|
|
// AI 处理状态初始加载
|
fetchAiProcessingStatus();
|
|
// 设置轮询定时器(10秒间隔)
|
const interval = setInterval(() => {
|
fetchCallStatus();
|
loadMediationRecords(); // 每10秒加载一次AI调解记录
|
fetchAiProcessingStatus(); // 每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, loadMediationRecords, fetchAiProcessingStatus]);
|
|
// 独立定时器:周期性轻量刷新 timeline + processNodes(不依赖闭包变量,避免过期状态问题)
|
useEffect(() => {
|
const mediationState = caseData?.mediation?.state;
|
// 仅在调解进行中时启动周期性刷新
|
if (mediationState !== 1) return;
|
|
console.log('[Widget] 启动节点数据周期性轻量刷新,间隔:', NODE_REFRESH_INTERVAL, 'ms');
|
const nodeRefreshTimer = setInterval(() => {
|
if (isMountedRef.current && refreshNodeData) {
|
console.log('[Widget] 执行节点数据轻量刷新');
|
refreshNodeData();
|
}
|
}, NODE_REFRESH_INTERVAL);
|
|
return () => clearInterval(nodeRefreshTimer);
|
}, [caseData?.mediation?.state, refreshNodeData]);
|
|
// 关闭气泡
|
const handleClose = (e) => {
|
e.stopPropagation();
|
setIsVisible(false);
|
setIsMinimized(true);
|
};
|
|
// 展开气泡
|
const handleExpand = () => {
|
setIsMinimized(false);
|
setIsVisible(true);
|
};
|
|
// 过滤掉已过期的任务(用于气泡显示)
|
const now = Date.now();
|
const activeCalls = calls.filter(call => {
|
// errorCode: 1001 - 超过 startTime 视为过期
|
if (call.errorCode === 1001) {
|
const callStartTime = typeof call.startTime === 'string' ? new Date(call.startTime).getTime() : call.startTime;
|
return now < callStartTime; // 当前时间小于 startTime 才显示
|
}
|
// errorCode: 1002 - 跨天视为过期
|
if (call.errorCode === 1002) {
|
const startDate = new Date(call.startTime);
|
const nowDate = new Date(now);
|
const startDateString = startDate.getFullYear() + '-' +
|
String(startDate.getMonth() + 1).padStart(2, '0') + '-' +
|
String(startDate.getDate()).padStart(2, '0');
|
const nowDateString = nowDate.getFullYear() + '-' +
|
String(nowDate.getMonth() + 1).padStart(2, '0') + '-' +
|
String(nowDate.getDate()).padStart(2, '0');
|
return startDateString === nowDateString; // 同一天才显示
|
}
|
// 其他状态的任务正常显示
|
return true;
|
});
|
|
// 如果没有活跃任务且没有AI处理中任务且不可见,不渲染任何内容
|
if (activeCalls.length === 0 && aiProcessingTasks.length === 0 && !isVisible) {
|
return null;
|
}
|
|
// 如果最小化,显示AI客服图标
|
if (isMinimized) {
|
return (
|
<div
|
onClick={handleExpand}
|
style={{
|
position: 'fixed',
|
right: 20,
|
bottom: 80,
|
width: 56,
|
height: 56,
|
borderRadius: '50%',
|
background: 'linear-gradient(135deg, #1A6FB8 0%, #0d4a8a 100%)',
|
display: 'flex',
|
alignItems: 'center',
|
justifyContent: 'center',
|
cursor: 'pointer',
|
boxShadow: '0 4px 12px rgba(26, 111, 184, 0.4)',
|
zIndex: 1000,
|
transition: 'all 0.3s ease',
|
}}
|
onMouseEnter={(e) => {
|
e.currentTarget.style.transform = 'scale(1.1)';
|
}}
|
onMouseLeave={(e) => {
|
e.currentTarget.style.transform = 'scale(1)';
|
}}
|
>
|
<i
|
className="fas fa-headset"
|
style={{
|
fontSize: 24,
|
color: 'white',
|
}}
|
/>
|
{/* 红点提示有通话或AI处理中 */}
|
{(activeCalls.length + aiProcessingTasks.length) > 0 && (
|
<div
|
style={{
|
position: 'absolute',
|
top: -2,
|
right: -2,
|
width: 16,
|
height: 16,
|
borderRadius: '50%',
|
background: aiProcessingTasks.length > 0 ? '#722ED1' : '#ff4d4f',
|
display: 'flex',
|
alignItems: 'center',
|
justifyContent: 'center',
|
fontSize: 10,
|
color: 'white',
|
fontWeight: 'bold',
|
}}
|
>
|
{activeCalls.length + aiProcessingTasks.length}
|
</div>
|
)}
|
</div>
|
);
|
}
|
|
// 展开状态 - 显示通话气泡
|
return (
|
<div
|
style={{
|
position: 'fixed',
|
right: 20,
|
bottom: 80,
|
zIndex: 1000,
|
display: 'flex',
|
flexDirection: 'column',
|
gap: 10,
|
maxWidth: 320,
|
}}
|
>
|
{activeCalls.map((call, index) => (
|
<div
|
key={call.jobId || call.phoneNumber || index}
|
style={{
|
background: 'linear-gradient(135deg, #1A6FB8 0%, #0d4a8a 100%)',
|
borderRadius: 12,
|
padding: '16px 20px',
|
color: 'white',
|
boxShadow: '0 4px 16px rgba(26, 111, 184, 0.3)',
|
position: 'relative',
|
minWidth: 280,
|
}}
|
>
|
{/* 关闭按钮 */}
|
<button
|
onClick={handleClose}
|
style={{
|
position: 'absolute',
|
top: 8,
|
right: 8,
|
width: 24,
|
height: 24,
|
borderRadius: '50%',
|
border: 'none',
|
background: 'rgba(255,255,255,0.2)',
|
color: 'white',
|
cursor: 'pointer',
|
display: 'flex',
|
alignItems: 'center',
|
justifyContent: 'center',
|
fontSize: 14,
|
transition: 'all 0.2s ease',
|
}}
|
onMouseEnter={(e) => {
|
e.currentTarget.style.background = 'rgba(255,255,255,0.3)';
|
}}
|
onMouseLeave={(e) => {
|
e.currentTarget.style.background = 'rgba(255,255,255,0.2)';
|
}}
|
>
|
<i className="fas fa-times" />
|
</button>
|
|
{/* 头部 - 智能体工作中 */}
|
<div
|
style={{
|
display: 'flex',
|
alignItems: 'center',
|
gap: 10,
|
marginBottom: 12,
|
}}
|
>
|
<div
|
style={{
|
width: 36,
|
height: 36,
|
borderRadius: '50%',
|
background: 'rgba(255,255,255,0.2)',
|
display: 'flex',
|
alignItems: 'center',
|
justifyContent: 'center',
|
}}
|
>
|
<i className="fas fa-robot" style={{ fontSize: 18 }} />
|
</div>
|
<div style={{ flex: 1 }}>
|
<div
|
style={{
|
fontSize: 16,
|
fontWeight: 600,
|
display: 'flex',
|
alignItems: 'center',
|
gap: 8,
|
}}
|
>
|
智能体工作中
|
<span
|
style={{
|
width: 8,
|
height: 8,
|
borderRadius: '50%',
|
background: call.errorCode > 0 ? '#ff4d4f' : '#52c41a',
|
animation: 'pulse 2s infinite',
|
}}
|
/>
|
</div>
|
</div>
|
</div>
|
|
{/* 通话信息 */}
|
<div style={{ marginBottom: 10 }}>
|
<div style={{ fontSize: 14, opacity: 0.9, lineHeight: 1.5 }}>
|
{call.errorCode > 0 ? (
|
// 失败任务显示
|
<span>
|
{call.perTypeName || '联系人'}
|
{call.trueName && `(${call.trueName})`}:
|
{call.message}
|
</span>
|
) : (
|
// 成功任务显示 - 使用 perTypeName 字段(申请方当事人/被申请方当事人)
|
<span>
|
正在与{call.perTypeName || '申请方当事人'}({call.trueName || call.personId})电话沟通中...
|
</span>
|
)}
|
</div>
|
</div>
|
|
{/* 通话时长(仅对成功任务显示)*/}
|
{!call.errorCode && call.pollStartTime && (
|
<div
|
style={{
|
display: 'flex',
|
alignItems: 'center',
|
gap: 6,
|
fontSize: 13,
|
opacity: 0.85,
|
}}
|
>
|
<i className="far fa-clock" />
|
<span>已持续: {formatDuration(Math.floor((Date.now() - call.pollStartTime) / 1000))}</span>
|
</div>
|
)}
|
</div>
|
))}
|
|
{/* AI 处理中加载态卡片 */}
|
{aiProcessingTasks.map((task, index) => (
|
<div
|
key={`ai-${task.jobId || index}`}
|
style={{
|
background: 'linear-gradient(135deg, #722ED1 0%, #531DAB 100%)',
|
borderRadius: 12,
|
padding: '16px 20px',
|
color: 'white',
|
boxShadow: '0 4px 16px rgba(114, 46, 209, 0.3)',
|
position: 'relative',
|
minWidth: 280,
|
}}
|
>
|
{/* 头部 - AI 分析中 */}
|
<div
|
style={{
|
display: 'flex',
|
alignItems: 'center',
|
gap: 10,
|
marginBottom: 12,
|
}}
|
>
|
<div
|
style={{
|
width: 36,
|
height: 36,
|
borderRadius: '50%',
|
background: 'rgba(255,255,255,0.2)',
|
display: 'flex',
|
alignItems: 'center',
|
justifyContent: 'center',
|
}}
|
>
|
<i className="fas fa-brain" style={{ fontSize: 18, animation: 'pulse 1.5s infinite' }} />
|
</div>
|
<div style={{ flex: 1 }}>
|
<div
|
style={{
|
fontSize: 16,
|
fontWeight: 600,
|
display: 'flex',
|
alignItems: 'center',
|
gap: 8,
|
}}
|
>
|
AI 分析中
|
<span
|
style={{
|
width: 8,
|
height: 8,
|
borderRadius: '50%',
|
background: '#B37FEB',
|
animation: 'pulse 1.5s infinite',
|
}}
|
/>
|
</div>
|
</div>
|
</div>
|
|
{/* 分析信息 */}
|
<div style={{ marginBottom: 10 }}>
|
<div style={{ fontSize: 14, opacity: 0.95, lineHeight: 1.6 }}>
|
<span>{task.personName || '当事人'} 通话分析</span>
|
</div>
|
<div style={{ fontSize: 12, opacity: 0.75, marginTop: 6, display: 'flex', flexDirection: 'column', gap: 4 }}>
|
{(task.steps || []).includes('summary') && (
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<i className="fas fa-file-alt" style={{ fontSize: 11 }} />
|
<span>对话总结生成中...</span>
|
</div>
|
)}
|
{(task.steps || []).includes('emotion') && (
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<i className="fas fa-heart" style={{ fontSize: 11 }} />
|
<span>情绪识别中...</span>
|
</div>
|
)}
|
{(task.steps || []).includes('success_rate') && (
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<i className="fas fa-chart-line" style={{ fontSize: 11 }} />
|
<span>成功率评估中...</span>
|
</div>
|
)}
|
</div>
|
</div>
|
</div>
|
))}
|
|
{/* 无通话时的占位提示 */}
|
{activeCalls.length === 0 && aiProcessingTasks.length === 0 && isVisible && (
|
<div
|
style={{
|
background: 'linear-gradient(135deg, #1A6FB8 0%, #0d4a8a 100%)',
|
borderRadius: 12,
|
padding: '16px 20px',
|
color: 'white',
|
boxShadow: '0 4px 16px rgba(26, 111, 184, 0.3)',
|
position: 'relative',
|
minWidth: 280,
|
}}
|
>
|
{/* 关闭按钮 */}
|
<button
|
onClick={handleClose}
|
style={{
|
position: 'absolute',
|
top: 8,
|
right: 8,
|
width: 24,
|
height: 24,
|
borderRadius: '50%',
|
border: 'none',
|
background: 'rgba(255,255,255,0.2)',
|
color: 'white',
|
cursor: 'pointer',
|
display: 'flex',
|
alignItems: 'center',
|
justifyContent: 'center',
|
fontSize: 14,
|
}}
|
>
|
<i className="fas fa-times" />
|
</button>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
<div
|
style={{
|
width: 36,
|
height: 36,
|
borderRadius: '50%',
|
background: 'rgba(255,255,255,0.2)',
|
display: 'flex',
|
alignItems: 'center',
|
justifyContent: 'center',
|
}}
|
>
|
<i className="fas fa-robot" style={{ fontSize: 18 }} />
|
</div>
|
<div>
|
<div style={{ fontSize: 16, fontWeight: 600 }}>智能外呼系统</div>
|
<div style={{ fontSize: 13, opacity: 0.85 }}>暂无进行中的通话</div>
|
</div>
|
</div>
|
</div>
|
)}
|
|
{/* CSS 动画 */}
|
<style>{`
|
@keyframes pulse {
|
0%, 100% { opacity: 1; }
|
50% { opacity: 0.5; }
|
}
|
`}</style>
|
</div>
|
);
|
};
|
|
export default OutboundCallWidget;
|