/**
|
* 案件数据全局Context
|
* 提供案件数据的全局状态管理和localStorage持久化
|
*/
|
|
import React, { createContext, useContext, useState, useEffect, useRef } from 'react';
|
import { message } from 'antd';
|
import ProcessAPIService from '../services/ProcessAPIService';
|
import OutboundBotAPIService from '../services/OutboundBotAPIService';
|
import EvidenceAPIService from '../services/EvidenceAPIService';
|
import MediationAgreementAPIService from '../services/MediationAgreementAPIService';
|
import { getMergedParams } from '../utils/urlParams';
|
import { mockTimelineData } from '../mocks/timeline';
|
import { getFallbackStartTime, parseTimeString } from '../utils/timeFormatter';
|
|
// 创建Context
|
const CaseDataContext = createContext(null);
|
|
// localStorage键名
|
const STORAGE_KEY = 'case_data_timeline';
|
const OUTBOUND_JOBS_KEY = 'outbound_call_jobs';
|
|
/**
|
* CaseDataProvider组件
|
* 包裹需要访问案件数据的组件树
|
*/
|
export const CaseDataProvider = ({ children }) => {
|
const [caseData, setCaseData] = useState(null);
|
const [processNodes, setProcessNodes] = useState([]); // 流程节点数据
|
const [loading, setLoading] = useState(false);
|
const [error, setError] = useState(null);
|
const [taskStartTime, setTaskStartTime] = useState(null); // 任务开始时间
|
const [isTaskTimeFallback, setIsTaskTimeFallback] = useState(false); // 是否降级模式
|
const [hasLoaded, setHasLoaded] = useState(false); // 防止重复加载
|
|
// 证据材料数据
|
const [evidenceData, setEvidenceData] = useState({
|
applicantMaterials: [],
|
respondentMaterials: [],
|
loading: false,
|
error: null
|
});
|
|
// 调解协议数据
|
const [agreementData, setAgreementData] = useState({
|
content: '',
|
loading: false,
|
error: null
|
});
|
|
/**
|
* 保存数据到localStorage
|
*/
|
const saveToStorage = (data) => {
|
try {
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
} catch (err) {
|
console.error('Failed to save data to localStorage:', err);
|
}
|
};
|
|
/**
|
* 检查是否有活跃的外呼任务
|
* @returns {boolean} 是否有活跃任务
|
*/
|
const hasActiveOutboundJobs = () => {
|
try {
|
const stored = localStorage.getItem(OUTBOUND_JOBS_KEY);
|
if (!stored) return false;
|
|
const jobs = JSON.parse(stored);
|
const activeStatuses = [
|
'Scheduling',
|
'Executing',
|
'Paused',
|
'Drafted',
|
'InProgress',
|
'Calling',
|
'Ringing',
|
'Answered'
|
];
|
|
// 过滤出活跃状态的任务
|
const activeJobs = jobs.filter(job => activeStatuses.includes(job.callStatus));
|
return activeJobs.length > 0;
|
} catch (err) {
|
console.error('检查活跃外呼任务失败:', err);
|
return false;
|
}
|
};
|
|
/**
|
* 触发智能外呼
|
* @param {Object} timeline - 案件时间线数据
|
*/
|
const triggerOutboundCall = async (timeline) => {
|
try {
|
const { mediation } = timeline;
|
if (!mediation) {
|
console.warn('缺少必要参数:timeline.mediation,跳过外呼触发:', timeline);
|
return;
|
}
|
const { state } = mediation;
|
if (state >= 2) { // 2:调解成功,3:调解失败,4:人工接管
|
console.warn('调解状态已结束,mediation.state:', state);
|
return;
|
}
|
// 检查是否已有活跃任务,如有则跳过
|
if (hasActiveOutboundJobs()) {
|
console.log('检测到活跃外呼任务,跳过发起新外呼');
|
return;
|
}
|
|
const mediationId = timeline.id;
|
const caseId = timeline.case_id;
|
|
if (!mediationId || !caseId) {
|
console.warn('缺少必要参数,跳过外呼触发:', { mediationId, caseId });
|
return;
|
}
|
|
console.log('发起智能外呼,mediationId:', mediationId, ', caseId:', caseId);
|
|
// 调用外呼API
|
const response = await OutboundBotAPIService.makeCallV2({
|
mediationId: String(mediationId),
|
caseId: String(caseId),
|
callAuto: 0,
|
callPersonId: ''
|
});
|
|
// 处理响应
|
if (response?.data && Array.isArray(response.data)) {
|
const successJobs = [];
|
const failedJobs = [];
|
|
// 分类成功和失败的记录
|
response.data.forEach(item => {
|
if (item.errorCode === 0 && item.jobId) {
|
successJobs.push({
|
jobId: item.jobId,
|
callStatus: item.callStatus || 'Scheduling',
|
personId: item.personId,
|
mediationId: item.mediationId,
|
caseId: String(caseId), // 添加 caseId 字段用于轮询
|
perTypeName: item.perTypeName || '', // 当事人类型名称(申请方当事人/被申请方当事人)
|
perClassName: item.perClassName || '', // 添加人员类型名称
|
trueName: item.trueName || '', // 添加真实姓名
|
startTime: item.createTime || item.createdTime || item.start_time, // 兼容 createTime 和 createdTime
|
pollStartTime: Date.now(),
|
retryCount: 0
|
});
|
} else if (item.errorCode > 0) {
|
failedJobs.push({
|
personId: item.personId,
|
message: item.message || '未知错误',
|
perTypeName: item.perTypeName || '', // 当事人类型名称(申请方当事人/被申请方当事人)
|
perClassName: item.perClassName || '', // 添加人员类型名称
|
trueName: item.trueName || '', // 添加真实姓名
|
errorCode: item.errorCode, // 添加错误码
|
startTime: new Date(item.startTime || item.start_time).getTime() // 转换为时间戳
|
});
|
}
|
});
|
|
// 存储成功的任务到 localStorage
|
if (successJobs.length > 0) {
|
// 新任务发起时,先清除所有旧的成功任务(替换而不是追加)
|
localStorage.setItem(OUTBOUND_JOBS_KEY, JSON.stringify(successJobs));
|
console.log('存储外呼任务成功,数量:', successJobs.length);
|
|
// 外呼成功后,清除所有失败记录(新任务开始时应清空旧记录)
|
localStorage.removeItem(`${OUTBOUND_JOBS_KEY}_failed`);
|
console.log('外呼成功后清除所有失败记录');
|
|
// 触发自定义事件,通知 OutboundCallWidget 组件立即刷新
|
setTimeout(() => {
|
window.dispatchEvent(new CustomEvent('outbound-jobs-updated'));
|
}, 300);
|
}
|
|
// 存储失败的任务到 localStorage(用于气泡显示)
|
if (failedJobs.length > 0) {
|
console.log('准备存储失败任务:', failedJobs);
|
// 读取现有的失败任务
|
const storedFailedJobs = JSON.parse(localStorage.getItem(`${OUTBOUND_JOBS_KEY}_failed`) || '[]');
|
|
// 去重:按 personId 去重,保留最新的错误信息
|
const uniqueFailedJobs = [...storedFailedJobs];
|
failedJobs.forEach(newJob => {
|
const existingIndex = uniqueFailedJobs.findIndex(job => job.personId === newJob.personId);
|
if (existingIndex >= 0) {
|
// 更新已存在的失败任务
|
uniqueFailedJobs[existingIndex] = newJob;
|
} else {
|
// 添加新的失败任务
|
uniqueFailedJobs.push(newJob);
|
}
|
});
|
|
// 清理超过24小时的失败任务
|
const now = Date.now();
|
const cleanedFailedJobs = uniqueFailedJobs.filter(job => {
|
return (now - job.startTime) < 24 * 60 * 60 * 1000; // 24小时
|
});
|
|
localStorage.setItem(`${OUTBOUND_JOBS_KEY}_failed`, JSON.stringify(cleanedFailedJobs));
|
console.log('存储外呼失败任务,数量:', cleanedFailedJobs.length);
|
}
|
|
// 提示失败的任务
|
if (failedJobs.length > 0) {
|
const failedMsg = failedJobs
|
.map(job => `${job.personId}(${job.message})`)
|
.join('、');
|
message.warning(`部分联系人外呼失败:${failedMsg}`);
|
console.warn('部分外呼失败:', failedJobs);
|
}
|
}
|
} catch (err) {
|
console.error('智能外呼发起失败:', err);
|
message.error('智能外呼发起失败,请检查网络后重试');
|
}
|
};
|
|
/**
|
* 加载任务时间数据
|
*/
|
const loadTaskTime = async (timeline) => {
|
try {
|
const mediationId = timeline.id;
|
const nodeId = timeline.current_node?.id;
|
|
if (!mediationId || !nodeId) {
|
throw new Error('缺少必要的参数: mediation_id 或 node_id');
|
}
|
|
console.log('Loading task time with:', { mediationId, nodeId });
|
|
const response = await ProcessAPIService.getTaskTime(mediationId, nodeId);
|
|
// 解析API返回的开始时间
|
const startTimeStr = response.data?.startTime;
|
if (startTimeStr) {
|
const startTime = parseTimeString(startTimeStr);
|
if (startTime) {
|
setTaskStartTime(startTime);
|
setIsTaskTimeFallback(false);
|
console.log('Task start time loaded:', new Date(startTime).toLocaleString());
|
return;
|
}
|
}
|
|
throw new Error('无法解析开始时间');
|
} catch (err) {
|
console.error('Failed to load task time:', err);
|
|
// 降级到本地计时
|
const fallbackTime = getFallbackStartTime();
|
setTaskStartTime(fallbackTime);
|
setIsTaskTimeFallback(true);
|
message.warning('任务时间API不可用,使用本地计时');
|
console.log('Using fallback start time:', new Date(fallbackTime).toLocaleString());
|
}
|
};
|
|
/**
|
* 加载证据材料数据
|
* @param {Object} params - 参数对象
|
* @param {string} params.caseId - 案件ID
|
* @param {string} params.caseType - 案件类型
|
* @param {string} params.platformCode - 平台编码
|
*/
|
const loadEvidenceData = async (params) => {
|
setEvidenceData(prev => ({ ...prev, loading: true, error: null }));
|
|
try {
|
const response = await EvidenceAPIService.getEvidenceList({
|
case_id: params.caseId,
|
case_type: params.caseType || params.caseTypeFirst,
|
platform_code: params.platformCode
|
});
|
|
console.log('Evidence API response:', response);
|
|
const responseData = response.data || [];
|
|
// 分离申请人和被申请人材料
|
const applicantData = responseData.find(item => item.per_type === '15_020008-1');
|
const respondentData = responseData.find(item => item.per_type === '15_020008-2');
|
|
const applicantList = applicantData?.file_list?.slice(0, applicantData.file_count) || [];
|
const respondentList = respondentData?.file_list?.slice(0, respondentData.file_count) || [];
|
|
setEvidenceData({
|
applicantMaterials: applicantList,
|
respondentMaterials: respondentList,
|
loading: false,
|
error: null
|
});
|
|
console.log('Evidence data loaded:', { applicantList, respondentList });
|
} catch (err) {
|
console.error('加载证据材料失败:', err);
|
setEvidenceData(prev => ({
|
...prev,
|
loading: false,
|
error: err.message || '加载证据材料失败'
|
}));
|
}
|
};
|
|
/**
|
* 加载调解协议数据
|
* @param {string} caseId - 案件ID
|
*/
|
const loadAgreementData = async (caseId) => {
|
setAgreementData(prev => ({ ...prev, loading: true, error: null }));
|
|
try {
|
const response = await MediationAgreementAPIService.generateAgreement(caseId);
|
|
if (response?.data?.agreeContent) {
|
setAgreementData({
|
content: response.data.agreeContent,
|
loading: false,
|
error: null
|
});
|
console.log('Agreement data loaded');
|
} else {
|
setAgreementData({
|
content: '',
|
loading: false,
|
error: '协议内容为空'
|
});
|
}
|
} catch (err) {
|
console.error('加载调解协议失败:', err);
|
setAgreementData({
|
content: '',
|
loading: false,
|
error: err.message || '加载调解协议失败'
|
});
|
}
|
};
|
|
/**
|
* 加载案件数据
|
*/
|
const loadCaseData = async (forceRefresh = false) => {
|
// 防止重复加载(除非强制刷新)
|
if (hasLoaded && !forceRefresh) {
|
console.log('Data already loaded, skipping...');
|
return;
|
}
|
|
setLoading(true);
|
setError(null);
|
|
try {
|
// 获取URL参数(合并默认值)
|
const params = getMergedParams();
|
|
console.log('Loading case data with params:', params);
|
|
if (!params.caseId) {
|
setError('caseId缺失');
|
setLoading(false);
|
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
|
const response = await ProcessAPIService.getCaseProcessInfo(
|
params.caseId,
|
{
|
caseTypeFirst: params.caseTypeFirst,
|
platform_code: params.platform_code,
|
authorization: params.auth_token || params.authorization || ''
|
}
|
);
|
|
// 提取timeline数据
|
const timelineData = response.timeline || response.data?.timeline || response;
|
|
// 提取nodes数据(确保为数组),兼容 nodeList 和 nodes 两种字段名
|
const nodesData = response.data?.nodeList || response.data?.nodes || response.nodes || [];
|
|
console.log('API Response:', response);
|
console.log('Extracted nodesData:', nodesData);
|
|
// 更新状态
|
setCaseData(timelineData);
|
setProcessNodes(Array.isArray(nodesData) ? nodesData : []); // 确保为数组
|
setHasLoaded(true); // 标记已加载
|
|
// 并行加载证据材料和调解协议数据(在终态检查之前,确保数据完整性)
|
await Promise.all([
|
loadEvidenceData({
|
caseId: params.caseId,
|
caseType: params.caseType || params.caseTypeFirst,
|
caseTypeFirst: params.caseTypeFirst,
|
platformCode: params.platform_code
|
}),
|
loadAgreementData(params.caseId)
|
]);
|
|
// 检查终态状态(调解成功/失败/人工接管),终态不执行外呼和存储
|
const mediationState = timelineData.mediation?.state;
|
const isTerminalState = [2, 3, 4].includes(mediationState);
|
|
if (isTerminalState) {
|
console.log('案件已处于终态状态:', mediationState, ',跳过外呼和存储');
|
return;
|
}
|
|
// 保存到localStorage
|
saveToStorage(timelineData);
|
|
// 触发智能外呼
|
await triggerOutboundCall(timelineData);
|
|
|
// 加载任务时间数据
|
await loadTaskTime(timelineData);
|
|
console.log('Case data loaded successfully:', timelineData);
|
} catch (err) {
|
console.error('Failed to load case data:', err);
|
setError(err.message || '加载案件数据失败');
|
|
// 显示错误提示
|
message.error('加载案件数据失败,请稍后重试');
|
|
// 使用Mock数据(缓存数据不包含nodes,所以统一使用Mock)
|
setHasLoaded(true);
|
|
} finally {
|
setLoading(false);
|
}
|
};
|
|
/**
|
* 刷新数据(强制从API获取)
|
*/
|
const refreshData = () => {
|
loadCaseData(true);
|
};
|
|
// 防止重复加载的引用标识
|
const loadInitRef = useRef(false);
|
|
/**
|
* 组件挂载时加载数据
|
*/
|
useEffect(() => {
|
console.log('===== CaseDataContext useEffect triggered =====');
|
console.log('loadInitRef.current:', loadInitRef.current);
|
console.log('hasLoaded:', hasLoaded);
|
console.log('processNodes:', processNodes);
|
|
// StrictMode下防止双重调用
|
if (!loadInitRef.current) {
|
loadInitRef.current = true;
|
console.log('Calling loadCaseData()...');
|
loadCaseData();
|
} else {
|
console.log('loadCaseData() skipped due to loadInitRef.current === true');
|
}
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}, []); // 只在挂载时执行一次
|
|
// 提供的值
|
const value = {
|
caseData,
|
processNodes, // 流程节点数据
|
loading,
|
error,
|
refreshData,
|
loadCaseData,
|
taskStartTime, // 任务开始时间
|
isTaskTimeFallback, // 是否降级模式
|
// 证据材料数据
|
evidenceData,
|
loadEvidenceData,
|
// 调解协议数据
|
agreementData,
|
loadAgreementData
|
};
|
|
return (
|
<CaseDataContext.Provider value={value}>
|
{children}
|
</CaseDataContext.Provider>
|
);
|
};
|
|
/**
|
* 自定义Hook:使用案件数据
|
* @returns {Object} { caseData, loading, error, refreshData, loadCaseData }
|
*/
|
export const useCaseData = () => {
|
const context = useContext(CaseDataContext);
|
|
if (!context) {
|
throw new Error('useCaseData must be used within CaseDataProvider');
|
}
|
|
return context;
|
};
|
|
export default CaseDataContext;
|