From 85d9447504b83d4b4b0550d4b5199e284e14a06f Mon Sep 17 00:00:00 2001
From: tony.cheng <chengmingwei_1984122@126.com>
Date: Thu, 12 Mar 2026 17:04:05 +0800
Subject: [PATCH] feat: 数据加载稳定性重构 - 将证据材料和调解协议API集中到CaseDataContext统一管理

---
 web-app/src/components/dashboard/TabContainer.jsx |  237 ++++++++++++++++++++++++++++++++---------------------------
 1 files changed, 128 insertions(+), 109 deletions(-)

diff --git a/web-app/src/components/dashboard/TabContainer.jsx b/web-app/src/components/dashboard/TabContainer.jsx
index b16768d..3e0a166 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 } 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';
@@ -11,11 +11,19 @@
 
 /**
  * 选项卡容器组件 - 4个选项卡
+ * 通过 forwardRef 暴露 switchTab 方法供父组件调用
  */
-const TabContainer = () => {
+const TabContainer = forwardRef((props, ref) => {
   const [activeTab, setActiveTab] = useState('mediation-data-board');
   // 证据材料汇总Tab的审核状态badge
   const [evidenceBadge, setEvidenceBadge] = useState(null);
+
+  // 暴露 switchTab 方法给父组件
+  useImperativeHandle(ref, () => ({
+    switchTab: (tabKey) => {
+      setActiveTab(tabKey);
+    }
+  }));
 
   const tabs = [
     { key: 'mediation-data-board', label: '调解分析', icon: 'fa-chart-line' },
@@ -76,7 +84,7 @@
       </div>
     </div>
   );
-};
+});
 
 /**
  * 调解数据看板
@@ -439,16 +447,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);
@@ -472,59 +480,24 @@
     return '已审核';
   };
 
-  // 加载数据
-  const loadData = async () => {
-    // 使用getMergedParams获取参数(URL参数优先,默认值兜底)
-    const params = getMergedParams();
-    const caseId = params.caseId;
-    const 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) => {
@@ -556,7 +529,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>
@@ -690,7 +663,7 @@
       message.success('审核通过!');
       handleCloseAuditModal();
       // 重新加载数据
-      loadData();
+      handleReload();
     } catch (err) {
       console.error('审核失败:', err);
       message.error('审核失败,请重试');
@@ -728,7 +701,7 @@
       message.success('材料已退回,等待补充提交');
       setReturnModalVisible(false);
       handleCloseAuditModal();
-      loadData();
+      handleReload();
     } catch (err) {
       console.error('退回失败:', err);
       message.error('退回失败,请重试');
@@ -1082,9 +1055,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}
@@ -1100,11 +1119,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>
@@ -1246,10 +1272,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);
@@ -1258,10 +1284,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) => {
@@ -1282,36 +1307,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 () => {
@@ -1333,7 +1334,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);
@@ -1348,9 +1370,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) {
@@ -1396,11 +1418,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('保存协议失败,请稍后重试');
@@ -1425,7 +1444,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