| | |
| | | import React, { useState, useEffect, useRef, forwardRef, useImperativeHandle } from 'react'; |
| | | import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react'; |
| | | import { useCaseData } from '../../contexts/CaseDataContext'; |
| | | import { formatDuration, formatSuccessRate, formatRoundCount } from '../../utils/stateTranslator'; |
| | | import ProcessAPIService from '../../services/ProcessAPIService'; |
| | |
| | | import MediationAgreementAPIService from '../../services/MediationAgreementAPIService'; |
| | | import { getMergedParams } from '../../utils/urlParams'; |
| | | import { message, Spin, Tag, Modal, Button, Input, Image } from 'antd'; |
| | | import { PhoneOutlined } from '@ant-design/icons'; |
| | | import { CallRecordModal } from '../call-record'; |
| | | |
| | | const { TextArea } = Input; |
| | | |
| | |
| | | <div className="success-label">预计调解成功概率</div> |
| | | <div className="success-change"> |
| | | |
| | | {/* <i className="fas fa-arrow-up"></i><span>较{updateTime} +8%</span> */} |
| | | <i className="fas fa-arrow-up"></i><span>较{updateTime} +8%</span> |
| | | </div> |
| | | <div style={{ marginTop: 15, fontSize: '0.9rem', color: 'var(--gray-color)' }}> |
| | | 协商沟通:<span style={{ color: 'var(--dark-color)', fontWeight: 600 }}>{roundCount}</span> |
| | |
| | | const [records, setRecords] = useState([]); |
| | | const [loading, setLoading] = useState(false); |
| | | const [error, setError] = useState(null); |
| | | // 通话记录弹窗状态 |
| | | const [callRecordVisible, setCallRecordVisible] = useState(false); |
| | | const [currentRecord, setCurrentRecord] = useState(null); |
| | | |
| | | // 获取案件数据 |
| | | const { caseData } = useCaseData(); |
| | | const timeline = caseData || {}; |
| | | |
| | | const caseState = timeline.mediation?.state; |
| | | |
| | | |
| | | // 格式化时间戳为 YYYY-MM-DD HH:MM:SS |
| | | const formatTimestamp = (timestamp) => { |
| | | if (!timestamp) return ''; |
| | | const date = new Date(timestamp); |
| | | const year = date.getFullYear(); |
| | | const month = String(date.getMonth() + 1).padStart(2, '0'); |
| | | const day = String(date.getDate()).padStart(2, '0'); |
| | | const hours = String(date.getHours()).padStart(2, '0'); |
| | | const minutes = String(date.getMinutes()).padStart(2, '0'); |
| | | const seconds = String(date.getSeconds()).padStart(2, '0'); |
| | | return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; |
| | | }; |
| | | |
| | | // person_type到avatar类型的映射 |
| | | // 1: 申请人, 2: 被申请人, 3: AI调解员, 4: 调解员 |
| | | const getAvatarType = (personType) => { |
| | | const typeMap = { |
| | | '1': 'ai', |
| | | '2': 'applicant', |
| | | '3': 'respondent', |
| | | '1': 'applicant', |
| | | '2': 'respondent', |
| | | '3': 'ai', |
| | | '4': 'mediator' |
| | | }; |
| | | return typeMap[personType] || 'ai'; |
| | |
| | | }; |
| | | |
| | | // 获取角色显示名称 |
| | | // 1: 申请人, 2: 被申请人, 3: AI调解员, 4: 调解员 |
| | | const getRoleDisplayName = (personType, creatorName) => { |
| | | const roleMap = { |
| | | '1': 'AI调解员', |
| | | '2': `申请人(${creatorName})`, |
| | | '3': `被申请人(${creatorName})`, |
| | | '1': `申请人(${creatorName})`, |
| | | '2': `被申请人(${creatorName})`, |
| | | '3': 'AI调解员', |
| | | '4': `调解员(${creatorName})` |
| | | }; |
| | | return roleMap[personType] || creatorName; |
| | | }; |
| | | |
| | | // 数据格式化函数 |
| | | // 数据格式化函数(保留原始数据字段用于通话记录功能) |
| | | const formatRecordData = (apiRecords) => { |
| | | return apiRecords.map(record => ({ |
| | | ...record, // 保留原始数据字段(person_id, job_id, creator等) |
| | | avatar: getAvatarType(record.person_type), |
| | | name: getRoleDisplayName(record.person_type, record.creator), |
| | | avatarText: record.creator?.charAt(0) || '', // 头像显示名字第一个字 |
| | | time: record.create_time, |
| | | avatarText: record.creator?.charAt(0) || '', |
| | | time: formatTimestamp(record.create_time), |
| | | content: record.result, |
| | | tags: record.tagList?.map(tag => ({ |
| | | text: tag.tag_name, |
| | |
| | | {getAvatarContent(item.avatar, item.avatarText)} |
| | | </div> |
| | | <div className="item-source"> |
| | | <div style={{ fontWeight: 600, fontSize: '0.95rem', color: 'var(--dark-color)', marginBottom: 2 }}>{item.name}</div> |
| | | <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 2 }}> |
| | | <span style={{ fontWeight: 600, fontSize: '0.95rem', color: 'var(--dark-color)' }}>{item.name}</span> |
| | | {item.avatar !== 'ai' && item.avatar !== 'mediator' && ( |
| | | <span |
| | | className="call-record-btn" |
| | | style={{ |
| | | display: 'inline-flex', |
| | | alignItems: 'center', |
| | | gap: 4, |
| | | padding: '2px 8px', |
| | | fontSize: '0.75rem', |
| | | background: '#e3f2fd', |
| | | color: '#1890ff', |
| | | borderRadius: 12, |
| | | cursor: 'pointer', |
| | | transition: 'all 0.2s' |
| | | }} |
| | | onClick={(e) => { |
| | | e.stopPropagation(); |
| | | setCurrentRecord(item); |
| | | setCallRecordVisible(true); |
| | | }} |
| | | onMouseEnter={(e) => { |
| | | e.target.style.background = '#bbdefb'; |
| | | }} |
| | | onMouseLeave={(e) => { |
| | | e.target.style.background = '#e3f2fd'; |
| | | }} |
| | | > |
| | | <PhoneOutlined style={{ fontSize: 12 }} /> |
| | | 通话记录 |
| | | </span> |
| | | )} |
| | | </div> |
| | | <div style={{ fontSize: '0.8rem', color: 'var(--gray-color)', display: 'flex', alignItems: 'center', gap: 6 }}> |
| | | <i className="far fa-clock"></i> |
| | | <span>{item.time}</span> |
| | |
| | | </div> |
| | | ))} |
| | | </div> |
| | | |
| | | {/* 通话记录弹窗 */} |
| | | <CallRecordModal |
| | | visible={callRecordVisible} |
| | | onClose={() => setCallRecordVisible(false)} |
| | | record={currentRecord} |
| | | /> |
| | | </> |
| | | ); |
| | | }; |
| | |
| | | * 证据材料汇总 |
| | | */ |
| | | const EvidenceBoard = ({ onStatusChange }) => { |
| | | const [loading, setLoading] = useState(false); |
| | | const [error, setError] = useState(null); |
| | | // 从 Context 获取证据材料数据 |
| | | const { evidenceData, loadEvidenceData } = useCaseData(); |
| | | const { applicantMaterials, respondentMaterials, loading, error } = evidenceData; |
| | | |
| | | // 审核弹窗状态 |
| | | const [auditModalVisible, setAuditModalVisible] = useState(false); |
| | | const [currentAuditItem, setCurrentAuditItem] = useState(null); |
| | | const [auditRemark, setAuditRemark] = useState(''); |
| | | const [auditLoading, setAuditLoading] = useState(false); |
| | | const [returnModalVisible, setReturnModalVisible] = useState(false); |
| | | const [applicantMaterials, setApplicantMaterials] = useState([]); |
| | | const [respondentMaterials, setRespondentMaterials] = useState([]); |
| | | // 弹窗数据状态 |
| | | const [modalDataLoading, setModalDataLoading] = useState(false); |
| | | const [personInfo, setPersonInfo] = useState(null); |
| | |
| | | return '已审核'; |
| | | }; |
| | | |
| | | // 加载数据 |
| | | const loadData = async () => { |
| | | // 使用getMergedParams获取参数(URL参数优先,默认值兜底) |
| | | const params = getMergedParams(); |
| | | const caseId = params.caseId; |
| | | const caseType = params.caseType ||params.caseTypeFirst; |
| | | const platformCode = params.platform_code; |
| | | |
| | | console.log('EvidenceBoard loadData params:', { caseId, caseType, platformCode }); |
| | | |
| | | setLoading(true); |
| | | setError(null); |
| | | |
| | | try { |
| | | // 调用API获取数据 |
| | | const response = await EvidenceAPIService.getEvidenceList({ |
| | | case_id: caseId, |
| | | case_type: caseType, |
| | | platform_code: platformCode |
| | | }); |
| | | |
| | | console.log('EvidenceBoard 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) || []; |
| | | |
| | | setApplicantMaterials(applicantList); |
| | | setRespondentMaterials(respondentList); |
| | | |
| | | // 计算并通知Tab标题的整体审核状态 |
| | | const overallStatus = calculateOverallTabStatus(applicantList, respondentList); |
| | | if (onStatusChange) { |
| | | onStatusChange(overallStatus); |
| | | } |
| | | |
| | | } catch (err) { |
| | | console.error('加载证据材料失败:', err); |
| | | setError('数据加载失败,请稍后重试'); |
| | | } finally { |
| | | setLoading(false); |
| | | } |
| | | }; |
| | | |
| | | // 组件挂载时加载数据 |
| | | // 监听数据变化,通知Tab标题状态 |
| | | useEffect(() => { |
| | | loadData(); |
| | | }, []); // eslint-disable-line react-hooks/exhaustive-deps |
| | | const overallStatus = calculateOverallTabStatus(applicantMaterials, respondentMaterials); |
| | | if (onStatusChange) { |
| | | onStatusChange(overallStatus); |
| | | } |
| | | }, [applicantMaterials, respondentMaterials]); // eslint-disable-line react-hooks/exhaustive-deps |
| | | |
| | | // 重新加载函数 |
| | | const handleReload = () => { |
| | | const params = getMergedParams(); |
| | | loadEvidenceData({ |
| | | caseId: params.caseId, |
| | | caseType: params.caseType || params.caseTypeFirst, |
| | | caseTypeFirst: params.caseTypeFirst, |
| | | platformCode: params.platform_code |
| | | }); |
| | | }; |
| | | |
| | | // 计算区域内整体审核状态(用于卡片标题旁的Tag显示) |
| | | const calculateOverallStatus = (materials) => { |
| | |
| | | <i className="fas fa-exclamation-circle"></i> 数据加载失败 |
| | | </div> |
| | | <div>{error}</div> |
| | | <button onClick={loadData} style={{ |
| | | <button onClick={handleReload} style={{ |
| | | marginTop: 15, padding: '8px 16px', backgroundColor: '#1890ff', |
| | | color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' |
| | | }}>重新加载</button> |
| | |
| | | message.success('审核通过!'); |
| | | handleCloseAuditModal(); |
| | | // 重新加载数据 |
| | | loadData(); |
| | | handleReload(); |
| | | } catch (err) { |
| | | console.error('审核失败:', err); |
| | | message.error('审核失败,请重试'); |
| | |
| | | message.success('材料已退回,等待补充提交'); |
| | | setReturnModalVisible(false); |
| | | handleCloseAuditModal(); |
| | | loadData(); |
| | | handleReload(); |
| | | } catch (err) { |
| | | console.error('退回失败:', err); |
| | | message.error('退回失败,请重试'); |
| | |
| | | * 调解协议 |
| | | */ |
| | | const AgreementSection = () => { |
| | | const { caseData } = useCaseData(); |
| | | const [agreementContent, setAgreementContent] = useState(''); |
| | | const [loading, setLoading] = useState(false); |
| | | const [error, setError] = useState(null); |
| | | // 从 Context 获取调解协议数据 |
| | | const { caseData, agreementData, loadAgreementData } = useCaseData(); |
| | | const { content: agreementContent, loading, error } = agreementData; |
| | | |
| | | const [editModalVisible, setEditModalVisible] = useState(false); |
| | | const [editContent, setEditContent] = useState(''); |
| | | const [editLoading, setEditLoading] = useState(false); |
| | |
| | | download: false, |
| | | regenerate: false, |
| | | }); |
| | | const loadedRef = useRef(false); |
| | | |
| | | // 获取 caseId(兼容驼峰和蛇形命名) |
| | | const caseId = caseData?.caseId || caseData?.case_id || getMergedParams().caseId; |
| | |
| | | }); |
| | | }; |
| | | |
| | | // 首次加载协议内容 |
| | | const loadAgreement = async () => { |
| | | if (!caseId) { |
| | | setError('缺少案件ID,无法加载协议'); |
| | | return; |
| | | } |
| | | setLoading(true); |
| | | setError(null); |
| | | try { |
| | | const response = await MediationAgreementAPIService.generateAgreement(caseId); |
| | | if (response?.data?.agreeContent) { |
| | | setAgreementContent(response.data.agreeContent); |
| | | } else { |
| | | setError('协议内容为空'); |
| | | } |
| | | } catch (err) { |
| | | console.error('加载协议失败:', err); |
| | | setError('加载协议失败,请稍后重试'); |
| | | } finally { |
| | | setLoading(false); |
| | | // 重新加载协议 |
| | | const handleReload = () => { |
| | | if (caseId) { |
| | | loadAgreementData(caseId); |
| | | } |
| | | }; |
| | | |
| | | // 组件挂载时加载协议 |
| | | useEffect(() => { |
| | | if (caseId && !loadedRef.current) { |
| | | loadedRef.current = true; |
| | | loadAgreement(); |
| | | } |
| | | }, [caseId]); // eslint-disable-line react-hooks/exhaustive-deps |
| | | |
| | | // 确认协议 |
| | | const handleConfirmAgreement = async () => { |
| | |
| | | try { |
| | | const response = await MediationAgreementAPIService.regenerateAgreement(caseId); |
| | | if (response?.data?.agreeContent) { |
| | | setAgreementContent(response.data.agreeContent); |
| | | loadAgreementData(caseId); |
| | | message.success('协议重新生成成功!'); |
| | | } |
| | | } catch (err) { |
| | |
| | | await MediationAgreementAPIService.updateAgreement(caseId, editContent); |
| | | message.success('协议修改保存成功!'); |
| | | handleCloseEditModal(); |
| | | // 刷新父页面协议内容 |
| | | const response = await MediationAgreementAPIService.generateAgreement(caseId); |
| | | if (response?.data?.agreeContent) { |
| | | setAgreementContent(response.data.agreeContent); |
| | | } |
| | | // 刷新协议内容 |
| | | loadAgreementData(caseId); |
| | | } catch (err) { |
| | | console.error('保存协议失败:', err); |
| | | message.error('保存协议失败,请稍后重试'); |
| | |
| | | <div style={{ fontSize: '1.2rem', marginBottom: 10 }}> |
| | | <i className="fas fa-exclamation-circle"></i> {error} |
| | | </div> |
| | | <button onClick={loadAgreement} style={{ |
| | | <button onClick={handleReload} style={{ |
| | | marginTop: 15, padding: '8px 16px', backgroundColor: '#1890ff', |
| | | color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' |
| | | }}>重新加载</button> |
| | |
| | | }; |
| | | |
| | | export default TabContainer; |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |