From 6bb08c2297be1b6415c8bc02e6917eba6ee355e5 Mon Sep 17 00:00:00 2001
From: shimai <shimai@example.com>
Date: Fri, 03 Apr 2026 10:42:08 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/test/tony.cheng/260312' into test/shimai.huang/260309

---
 web-app/src/components/dashboard/TabContainer.jsx |  233 ++++++++++++++++++++++++++++++----------------------------
 1 files changed, 120 insertions(+), 113 deletions(-)

diff --git a/web-app/src/components/dashboard/TabContainer.jsx b/web-app/src/components/dashboard/TabContainer.jsx
index 3f5bd66..ca215e3 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;
 
@@ -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 () => {
@@ -1432,7 +1435,7 @@
     try {
       const response = await MediationAgreementAPIService.regenerateAgreement(caseId);
       if (response?.data?.agreeContent) {
-        setAgreementContent(response.data.agreeContent);
+        loadAgreementData(caseId);
         message.success('协议重新生成成功!');
       }
     } catch (err) {
@@ -1478,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('保存协议失败,请稍后重试');
@@ -1507,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>
@@ -1721,3 +1721,10 @@
 };
 
 export default TabContainer;
+
+
+
+
+
+
+

--
Gitblit v1.8.0