From 6f45735adfdcd973a19f638f9ced9629f79cd6de Mon Sep 17 00:00:00 2001
From: shimai <shimai@example.com>
Date: Wed, 15 Apr 2026 16:12:01 +0800
Subject: [PATCH] v2.0: 提交当前所有变更,准备创建v2.0标签

---
 web-app/src/components/dashboard/TabContainer.jsx |  377 +++++++++++++++++++++++++++--------------------------
 1 files changed, 193 insertions(+), 184 deletions(-)

diff --git a/web-app/src/components/dashboard/TabContainer.jsx b/web-app/src/components/dashboard/TabContainer.jsx
index a102314..9cf5070 100644
--- a/web-app/src/components/dashboard/TabContainer.jsx
+++ b/web-app/src/components/dashboard/TabContainer.jsx
@@ -1,11 +1,18 @@
-import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
+import React, { useState, useEffect, useCallback, useRef, forwardRef, useImperativeHandle } from 'react';
 import { useCaseData } from '../../contexts/CaseDataContext';
-import { formatDuration, formatSuccessRate, formatRoundCount } from '../../utils/stateTranslator';
+import { formatDuration, formatSuccessRate } from '../../utils/stateTranslator';
 import ProcessAPIService from '../../services/ProcessAPIService';
 import EvidenceAPIService from '../../services/EvidenceAPIService';
 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';
+
+// 新增组件导入
+import PartyInfoCard from './PartyInfoCard';
+import NegotiationProgress from './NegotiationProgress';
+import AISuggestionCard from './AISuggestionCard';
 
 const { TextArea } = Input;
 
@@ -87,22 +94,53 @@
 });
 
 /**
+ * 获取成功率同比数据
+ */
+const getSuccessRateYoY = (mediation) => {
+  // 优先使用API返回的同比值
+  if (mediation?.yoy_success_rate !== undefined && mediation?.yoy_success_rate !== null) {
+    return {
+      rate: mediation.yoy_success_rate,
+      hours: mediation.yoy_before_hours || 0
+    };
+  }
+  
+  // 计算同比值
+  const currentRate = mediation?.success_rate || 0;
+  const lastRate = mediation?.last_success_rate || 0;
+  const diff = (currentRate - lastRate) * 100;
+  
+  return {
+    rate: diff,
+    hours: mediation?.yoy_before_hours || 0
+  };
+};
+
+/**
  * 调解数据看板
  */
 const MediationDataBoard = () => {
   const { caseData } = useCaseData();
   const timeline = caseData || {};
+  const mediation = timeline.mediation || {};
   
   // 从 timeline 获取数据
   const gapContent = timeline.result || '暂无分歧分析';
   const updateTime = formatDuration(timeline.before_duration);
-  const successRate = formatSuccessRate(timeline.mediation?.success_rate);
-  const roundCount = formatRoundCount(timeline.mediation?.mediation_count);
+  const successRate = formatSuccessRate(mediation.success_rate);
+  
+  // 获取成功率数值(用于进度条)
+  const successRateValue = (mediation.success_rate || 0) * 100;
+  
+  // 获取同比数据
+  const yoyData = getSuccessRateYoY(mediation);
+  const yoyRate = yoyData.rate >= 0 ? `+${yoyData.rate.toFixed(0)}%` : `${yoyData.rate.toFixed(0)}%`;
+  const yoyHours = yoyData.hours;
   
   return (
     <div className="mediation-metrics">
-      {/* 左侧:诉求差距分析 */}
-      <div className="metric-card">
+      {/* 左侧:诉求差距分析 + AI建议 */}
+      <div className="metric-card left-column">
         <div className="metric-title">
           <i className="fas fa-exclamation-circle"></i>
           <span>诉求差距分析</span>
@@ -118,28 +156,36 @@
               {gapContent}
             </div>
           </div>
+          {/* AI调解建议 */}
+          <AISuggestionCard />
         </div>
       </div>
 
-      {/* 右侧:调解数据 */}
-      <div className="metric-card">
-        <div className="metric-title">
-          <i className="fas fa-exchange-alt"></i>
-          <span>调解数据</span>
-        </div>
-        <div className="metric-content">
-          <div className="success-metric">
-            <div className="success-value">{successRate}</div>
-            <div className="success-label">预计调解成功概率</div>
-            <div className="success-change">
-              
-              {/* <i className="fas fa-arrow-up"></i><span>较{updateTime} +8%</span> */}
+      {/* 右侧:申请双方 + 成功率 + 协商沟通 */}
+      <div className="metric-card right-column">
+        {/* 申请双方信息 */}
+        <PartyInfoCard />
+        
+        {/* 预计调解成功率 */}
+        <div className="success-rate-section">
+          <div className="success-rate-label">预计调解成功率</div>
+          <div className="success-rate-row">
+            <span className="success-rate-value">{successRate}</span>
+            <div className="success-rate-yoy">
+              <img src="/mom.png" alt="" className="yoy-icon-img" />
+              <span className="yoy-rate">{yoyRate}</span>
+              <span className="yoy-time">较{yoyHours}小时前</span>
             </div>
-            <div style={{ marginTop: 15, fontSize: '0.9rem', color: 'var(--gray-color)' }}>
-              协商沟通:<span style={{ color: 'var(--dark-color)', fontWeight: 600 }}>{roundCount}</span>
+          </div>
+          <div className="success-rate-progress">
+            <div className="progress-bar-bg">
+              <div className="progress-bar-fill" style={{ width: `${successRateValue}%` }}></div>
             </div>
           </div>
         </div>
+        
+        {/* 协商沟通进度 */}
+        <NegotiationProgress />
       </div>
     </div>
   );
@@ -153,24 +199,36 @@
   const [records, setRecords] = useState([]);
   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 [callRecordVisible, setCallRecordVisible] = useState(false);
+  const [currentRecord, setCurrentRecord] = useState(null);
   
   // 获取案件数据
-  const { caseData, refreshData } = useCaseData();
+  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';
@@ -188,23 +246,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,
@@ -213,16 +273,23 @@
     }));
   };
   
-  // 获取调解记录数据
-  const loadMediationRecords = async () => {
-    setLoading(true);
+  // 看板轮询间隔(毫秒)
+  const BOARD_POLL_INTERVAL = 5000; // 5秒
+  const isBoardMountedRef = useRef(true);
+
+  // 获取调解记录数据(首次加载带 loading,后续静默刷新)
+  const loadMediationRecords = useCallback(async (silent = false) => {
+    if (!silent) {
+      setLoading(true);
+    }
     setError(null);
     
     try {
       // 从timeline中获取mediation_id
       const mediationId = timeline.mediation?.id;
       if (!mediationId) {
-        throw new Error('未找到调解ID');
+        if (!silent) throw new Error('未找到调解ID');
+        return;
       }
       
       // 调用API获取记录列表
@@ -232,23 +299,50 @@
       
       // 格式化数据
       const formattedRecords = formatRecordData(response.data || []);
-      setRecords(formattedRecords);
+      if (isBoardMountedRef.current) {
+        setRecords(formattedRecords);
+      }
       
     } catch (err) {
-      setError(err.message);
-      console.error('获取调解记录失败:', err);
-      message.error(`获取调解记录失败: ${err.message}`);
+      if (!silent) {
+        setError(err.message);
+        console.error('获取调解记录失败:', err);
+        message.error(`获取调解记录失败: ${err.message}`);
+      } else {
+        console.warn('[MediationBoard] 静默刷新失败:', err.message);
+      }
     } finally {
-      setLoading(false);
+      if (!silent) {
+        setLoading(false);
+      }
     }
-  };
+  }, [timeline.mediation?.id]);
   
-  // 监听Tab切换
+  // Tab激活时:首次加载 + 启动周期性轮询
   useEffect(() => {
+    isBoardMountedRef.current = true;
+
     if (activeTab === 'mediation-board') {
-      loadMediationRecords();
+      // 首次加载(带 loading 效果)
+      loadMediationRecords(false);
+
+      // 周期性静默刷新(不显示 loading)
+      const pollTimer = setInterval(() => {
+        if (isBoardMountedRef.current) {
+          loadMediationRecords(true);
+        }
+      }, BOARD_POLL_INTERVAL);
+
+      console.log('[MediationBoard] 启动周期轮询,间隔:', BOARD_POLL_INTERVAL, 'ms');
+
+      return () => {
+        clearInterval(pollTimer);
+        isBoardMountedRef.current = false;
+      };
     }
-  }, [activeTab]);
+
+    return () => { isBoardMountedRef.current = false; };
+  }, [activeTab, loadMediationRecords]);
   
   // 如果还在加载中,显示Loading状态
   if (loading) {
@@ -352,68 +446,6 @@
     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={{
@@ -462,7 +494,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>
@@ -508,76 +573,13 @@
           </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>
+      
+      {/* 通话记录弹窗 */}
+      <CallRecordModal
+        visible={callRecordVisible}
+        onClose={() => setCallRecordVisible(false)}
+        record={currentRecord}
+      />
     </>
   );
 };
@@ -1797,3 +1799,10 @@
 };
 
 export default TabContainer;
+
+
+
+
+
+
+

--
Gitblit v1.8.0