From 15332eb2a66375b0b2aff130737202682980a23d Mon Sep 17 00:00:00 2001
From: chengmw <chengmingwei_1984122@126.com>
Date: Tue, 31 Mar 2026 15:47:47 +0800
Subject: [PATCH] feat: 优化外呼组件和 Tab 容器功能
---
web-app/src/components/dashboard/TabContainer.jsx | 285 ++++++++++++++++++++++++++++----------------------------
1 files changed, 143 insertions(+), 142 deletions(-)
diff --git a/web-app/src/components/dashboard/TabContainer.jsx b/web-app/src/components/dashboard/TabContainer.jsx
index eab6b9b..90b1e5e 100644
--- a/web-app/src/components/dashboard/TabContainer.jsx
+++ b/web-app/src/components/dashboard/TabContainer.jsx
@@ -1,4 +1,4 @@
-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';
@@ -6,6 +6,8 @@
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;
@@ -133,7 +135,7 @@
<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>
@@ -153,17 +155,36 @@
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';
@@ -181,23 +202,25 @@
};
// 获取角色显示名称
+ // 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,
@@ -393,7 +416,40 @@
{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>
@@ -439,6 +495,13 @@
</div>
))}
</div>
+
+ {/* 通话记录弹窗 */}
+ <CallRecordModal
+ visible={callRecordVisible}
+ onClose={() => setCallRecordVisible(false)}
+ record={currentRecord}
+ />
</>
);
};
@@ -447,16 +510,16 @@
* 证据材料汇总
*/
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);
@@ -480,59 +543,24 @@
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) => {
@@ -564,7 +592,7 @@
<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>
@@ -698,7 +726,7 @@
message.success('审核通过!');
handleCloseAuditModal();
// 重新加载数据
- loadData();
+ handleReload();
} catch (err) {
console.error('审核失败:', err);
message.error('审核失败,请重试');
@@ -736,7 +764,7 @@
message.success('材料已退回,等待补充提交');
setReturnModalVisible(false);
handleCloseAuditModal();
- loadData();
+ handleReload();
} catch (err) {
console.error('退回失败:', err);
message.error('退回失败,请重试');
@@ -1307,10 +1335,10 @@
* 调解协议
*/
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);
@@ -1319,10 +1347,9 @@
download: false,
regenerate: false,
});
- const loadedRef = useRef(false);
- // 获取 caseId
- const caseId = caseData?.caseId || getMergedParams().caseId;
+ // 获取 caseId(兼容驼峰和蛇形命名)
+ const caseId = caseData?.caseId || caseData?.case_id || getMergedParams().caseId;
// 处理协议内容展示(纯文本,处理换行)
const renderAgreementContent = (content) => {
@@ -1343,36 +1370,12 @@
});
};
- // 首次加载协议内容
- 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 () => {
@@ -1394,35 +1397,29 @@
if (!caseId) return;
setActionLoading(prev => ({ ...prev, download: true }));
try {
- // 调用API获取协议内容
+ // 调用API获取PDF文件流
const response = await MediationAgreementAPIService.downloadAgreement(caseId);
- if (response?.data?.agreeContent) {
- const agreementContent = response.data.agreeContent;
-
- // 创建Blob对象
- const blob = new Blob([agreementContent], {
- type: 'application/pdf'
- });
-
- // 创建下载链接
- const url = window.URL.createObjectURL(blob);
- const link = document.createElement('a');
- link.href = url;
- link.download = `调解协议_${caseId}.pdf`;
-
- // 触发下载
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
-
- // 清理URL对象
- window.URL.revokeObjectURL(url);
-
- message.success('协议下载成功!');
- } else {
- message.error('未获取到协议内容');
- }
+ // 创建Blob对象(PDF格式)
+ const blob = new Blob([response.data], {
+ type: 'application/pdf'
+ });
+
+ // 创建下载链接
+ const url = window.URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = `调解协议_${caseId}.pdf`;
+
+ // 触发下载
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+
+ // 清理URL对象
+ window.URL.revokeObjectURL(url);
+
+ message.success('协议下载成功!');
} catch (err) {
console.error('下载协议失败:', err);
message.error('下载协议失败,请稍后重试');
@@ -1436,9 +1433,9 @@
if (!caseId) return;
setActionLoading(prev => ({ ...prev, regenerate: true }));
try {
- const response = await MediationAgreementAPIService.generateAgreement(caseId);
+ const response = await MediationAgreementAPIService.regenerateAgreement(caseId);
if (response?.data?.agreeContent) {
- setAgreementContent(response.data.agreeContent);
+ loadAgreementData(caseId);
message.success('协议重新生成成功!');
}
} catch (err) {
@@ -1484,11 +1481,8 @@
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('保存协议失败,请稍后重试');
@@ -1513,7 +1507,7 @@
<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>
@@ -1727,3 +1721,10 @@
};
export default TabContainer;
+
+
+
+
+
+
+
--
Gitblit v1.8.0