From d5d5f337e4930d1c85228c81485b4d3fded3f384 Mon Sep 17 00:00:00 2001
From: tony.cheng <chengmingwei_1984122@126.com>
Date: Tue, 17 Mar 2026 11:08:41 +0800
Subject: [PATCH] feat: 实现AI调解状态控制功能(终止/恢复)
---
web-app/src/components/dashboard/TabContainer.jsx | 366 ++++++++++++++++++++++++++++++++++++---------------
1 files changed, 258 insertions(+), 108 deletions(-)
diff --git a/web-app/src/components/dashboard/TabContainer.jsx b/web-app/src/components/dashboard/TabContainer.jsx
index c9e448b..a102314 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';
@@ -154,9 +154,16 @@
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
+ // 状态控制相关状态
+ const [controlLoading, setControlLoading] = useState(false);
+ const [confirmModalVisible, setConfirmModalVisible] = useState(false);
+ const [controlAction, setControlAction] = useState(null); // 'terminate' or 'resume'
+ const [remark, setRemark] = useState('');
+
// 获取案件数据
- const { caseData } = useCaseData();
+ const { caseData, refreshData } = useCaseData();
const timeline = caseData || {};
+ const caseState = timeline.mediation?.state;
// person_type到avatar类型的映射
const getAvatarType = (personType) => {
@@ -345,6 +352,68 @@
return avatarText || (avatar === 'applicant' ? '申' : avatar === 'respondent' ? '被' : '调');
};
+ // 状态控制按钮显示逻辑
+ const shouldShowControlButton = () => {
+ return caseState === 0 || caseState === 1 || caseState === 5;
+ };
+
+ const getControlButtonProps = () => {
+ if (caseState === 0 || caseState === 1) {
+ return {
+ text: '终止',
+ style: 'terminate',
+ action: 'terminate'
+ };
+ } else if (caseState === 5) {
+ return {
+ text: '恢复',
+ style: 'resume',
+ action: 'resume'
+ };
+ }
+ return null;
+ };
+
+ // 处理状态控制按钮点击
+ const handleControlButtonClick = (action) => {
+ setControlAction(action);
+ setConfirmModalVisible(true);
+ };
+
+ // 处理确认对话框确认
+ const handleConfirmOk = async () => {
+ if (!controlAction) return;
+
+ setControlLoading(true);
+ try {
+ const params = getMergedParams();
+ const actionCode = controlAction === 'terminate' ? 0 : 1;
+
+ await ProcessAPIService.updateMediationState(params.caseId, {
+ action: actionCode,
+ userName: localStorage.getItem('userName') || '调解员'
+ });
+
+ message.success('案件状态更新成功');
+ setConfirmModalVisible(false);
+ setRemark('');
+
+ // 刷新数据
+ refreshData();
+ } catch (error) {
+ console.error('状态更新失败:', error);
+ message.error(error.message || '状态更新失败,请稍后重试');
+ } finally {
+ setControlLoading(false);
+ }
+ };
+
+ // 处理确认对话框取消
+ const handleConfirmCancel = () => {
+ setConfirmModalVisible(false);
+ setControlAction(null);
+ setRemark('');
+ };
return (
<>
<div className="mediation-summary" style={{
@@ -439,6 +508,76 @@
</div>
))}
</div>
+
+ {/* 状态控制按钮区域 */}
+ {shouldShowControlButton() && (
+ <div style={{
+ marginTop: 20,
+ paddingTop: 15,
+ borderTop: '1px solid #e9ecef',
+ display: 'flex',
+ justifyContent: 'center',
+ gap: 12
+ }}>
+ <button
+ onClick={() => handleControlButtonClick(getControlButtonProps().action)}
+ disabled={controlLoading}
+ style={{
+ padding: '10px 20px',
+ borderRadius: 'var(--border-radius)',
+ fontWeight: 600,
+ fontSize: '0.9rem',
+ cursor: controlLoading ? 'not-allowed' : 'pointer',
+ display: 'flex',
+ alignItems: 'center',
+ gap: 6,
+ border: 'none',
+ ...(getControlButtonProps().style === 'terminate' ? {
+ background: '#1A6FB8',
+ color: 'white',
+ } : {
+ background: '#52c41a',
+ color: 'white',
+ }),
+ opacity: controlLoading ? 0.6 : 1,
+ }}
+ >
+ {controlLoading ? (
+ <><i className="fas fa-spinner fa-spin"></i>处理中...</>
+ ) : (
+ <><i className="fas fa-pause-circle"></i>{getControlButtonProps().text}</>
+ )}
+ </button>
+ </div>
+ )}
+
+ {/* 状态控制确认对话框 */}
+ <Modal
+ title={controlAction === 'terminate' ? '确认终止调解' : '确认恢复调解'}
+ visible={confirmModalVisible}
+ onOk={handleConfirmOk}
+ onCancel={handleConfirmCancel}
+ okText="确定"
+ cancelText="取消"
+ confirmLoading={controlLoading}
+ >
+ <p>
+ {controlAction === 'terminate'
+ ? '确定要终止当前AI调解流程吗?终止后调解将暂停,可在适当时机恢复。'
+ : '确定要恢复AI调解流程吗?恢复后将从当前位置继续调解。'}
+ </p>
+ <div style={{ marginTop: 15 }}>
+ <label style={{ display: 'block', marginBottom: 5, fontWeight: 500 }}>
+ 备注(可选):
+ </label>
+ <Input.TextArea
+ value={remark}
+ onChange={(e) => setRemark(e.target.value)}
+ placeholder="请输入操作备注..."
+ rows={3}
+ />
+ </div>
+ </Modal>
</>
);
};
@@ -447,16 +586,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 +619,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 +668,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 +802,7 @@
message.success('审核通过!');
handleCloseAuditModal();
// 重新加载数据
- loadData();
+ handleReload();
} catch (err) {
console.error('审核失败:', err);
message.error('审核失败,请重试');
@@ -736,7 +840,7 @@
message.success('材料已退回,等待补充提交');
setReturnModalVisible(false);
handleCloseAuditModal();
- loadData();
+ handleReload();
} catch (err) {
console.error('退回失败:', err);
message.error('退回失败,请重试');
@@ -1090,9 +1194,55 @@
<Image.PreviewGroup>
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
{evidenceImages.map((img, index) => {
+ // 判断 show_url 是否以 http 开头
+ const isFullUrl = img.show_url && img.show_url.startsWith('http');
+ // 如果是完整URL直接使用,否则拼接 platformUrl
const imgUrl = img.show_url
- ? (platformUrl ? `${platformUrl}/${img.show_url}` : img.show_url)
+ ? (isFullUrl ? img.show_url : (platformUrl ? `${platformUrl}/${img.show_url}` : img.show_url))
: '';
+ // 获取文件名:true_name 为空时取 file_name
+ const rawFileName = img.true_name || img.file_name || `材料${index + 1}`;
+ // 获取文件类型后缀
+ const suffix = img.suffix || '';
+ // 检查文件名是否已包含后缀,避免重复
+ const fileName = suffix && rawFileName.toLowerCase().endsWith(`.${suffix.toLowerCase()}`)
+ ? rawFileName
+ : (suffix ? `${rawFileName}.${suffix}` : rawFileName);
+ const isPdf = suffix.toLowerCase() === 'pdf';
+
+ if (isPdf) {
+ // PDF文件:直接打开PDF链接
+ return (
+ <div
+ key={img.file_id || img.id || index}
+ style={{
+ width: 100,
+ height: 80,
+ borderRadius: 4,
+ overflow: 'hidden',
+ border: '1px solid #e8e8e8',
+ cursor: 'pointer',
+ transition: 'all 0.2s',
+ background: '#f5f5f5',
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'center',
+ }}
+ onClick={() => {
+ // 直接打开PDF
+ window.open(imgUrl, '_blank');
+ }}
+ >
+ <i className="fas fa-file-pdf" style={{ fontSize: 32, color: '#ff4d4f' }} />
+ <span style={{ fontSize: 10, color: '#666', marginTop: 4, maxWidth: 90, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
+ {fileName}
+ </span>
+ </div>
+ );
+ }
+
+ // 图片文件:使用图片预览
return (
<div
key={img.file_id || img.id || index}
@@ -1108,11 +1258,18 @@
>
<Image
src={imgUrl}
- alt={img.file_name || `材料${index + 1}`}
+ alt={fileName}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
fallback="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjgwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxyZWN0IHdpZHRoPSIxMDAiIGhlaWdodD0iODAiIGZpbGw9IiNmNWY1ZjUiLz48dGV4dCB4PSI1MCIgeT0iNDAiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzk5OSI+5Zu+54mH6aKE6KeIPC90ZXh0Pjwvc3ZnPg=="
preview={{
- mask: <span style={{ fontSize: 12 }}>预览</span>
+ mask: (
+ <span style={{ fontSize: 12, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
+ <span>预览</span>
+ <span style={{ fontSize: 10, marginTop: 2, maxWidth: 80, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
+ {fileName}
+ </span>
+ </span>
+ )
}}
/>
</div>
@@ -1254,10 +1411,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);
@@ -1266,10 +1423,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) => {
@@ -1290,36 +1446,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 () => {
@@ -1341,7 +1473,28 @@
if (!caseId) return;
setActionLoading(prev => ({ ...prev, download: true }));
try {
- await MediationAgreementAPIService.downloadAgreement(caseId);
+ // 调用API获取PDF文件流
+ const response = await MediationAgreementAPIService.downloadAgreement(caseId);
+
+ // 创建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);
@@ -1356,9 +1509,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) {
@@ -1404,11 +1557,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('保存协议失败,请稍后重试');
@@ -1433,7 +1583,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>
--
Gitblit v1.8.0