shimai
2026-04-03 e4dfe9f17d64d016376872b786d2987805ee41ef
web-app/src/components/dashboard/TabContainer.jsx
@@ -1,11 +1,18 @@
import React, { useState, useEffect, 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 { 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,
@@ -352,108 +412,6 @@
    return avatarText || (avatar === 'applicant' ? '申' : avatar === 'respondent' ? '被' : '调');
  };
  // 状态控制按钮显示逻辑
  const shouldShowControlButton = () => {
    const show = caseState === 0 || caseState === 1 || caseState === 5;
    console.log('状态控制按钮显示检查:', {
      caseState,
      show,
      conditions: {
        'caseState === 0': caseState === 0,
        'caseState === 1': caseState === 1,
        'caseState === 5': caseState === 5
      }
    });
    return show;
  };
  const getControlButtonProps = () => {
    console.log('获取按钮属性:', { caseState });
    if (caseState === 0 || caseState === 1) {
      return {
        text: '终止',
        style: 'terminate',
        action: 'terminate'
      };
    } else if (caseState === 5) {
      return {
        text: '恢复',
        style: 'resume',
        action: 'resume'
      };
    }
    console.log('未匹配到按钮属性,返回null');
    return null;
  };
  // 处理状态控制按钮点击
  const handleControlButtonClick = (action) => {
    console.log('状态控制按钮点击:', { action });
    setControlAction(action);
    setConfirmModalVisible(true);
  };
  // 处理确认对话框确认
  const handleConfirmOk = async () => {
    console.log('确认对话框确认:', { controlAction, remark });
    if (!controlAction) {
      console.warn('控制动作为空');
      return;
    }
    setControlLoading(true);
    try {
      const params = getMergedParams();
      const actionCode = controlAction === 'terminate' ? 0 : 1;
      console.log('准备调用API:', {
        caseId: params.caseId,
        actionCode,
        userName: localStorage.getItem('userName') || '调解员',
        remark: remark || ''
      });
      // 验证必要参数
      if (!params.caseId) {
        throw new Error('案件ID不能为空');
      }
      await ProcessAPIService.updateMediationState(params.caseId, {
        action: actionCode,
        userName: localStorage.getItem('userName') || '调解员',
        remark: remark || ''
      });
      message.success('案件状态更新成功');
      setConfirmModalVisible(false);
      setRemark('');
      setControlAction(null);
      // 刷新数据
      refreshData();
    } catch (error) {
      console.error('状态更新失败:', error);
      const errorMessage = error.message || '状态更新失败,请稍后重试';
      message.error(errorMessage);
      // 如果是网络错误,提供更多帮助信息
      if (errorMessage.includes('网络') || errorMessage.includes('Network')) {
        message.info('请检查网络连接或联系管理员');
      }
    } finally {
      setControlLoading(false);
    }
  };
  // 处理确认对话框取消
  const handleConfirmCancel = () => {
    setConfirmModalVisible(false);
    setControlAction(null);
    setRemark('');
  };
  return (
    <>
      <div className="mediation-summary" style={{
@@ -502,7 +460,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>
@@ -548,81 +539,13 @@
          </div>
        ))}
      </div>
      {/* 状态控制按钮区域 */}
      {shouldShowControlButton() && (() => {
        const buttonProps = getControlButtonProps();
        if (!buttonProps) return null;
        return (
          <div style={{
            marginTop: 20,
            paddingTop: 15,
            borderTop: '1px solid #e9ecef',
            display: 'flex',
            justifyContent: 'center',
            gap: 12
          }}>
            <button
              onClick={() => handleControlButtonClick(buttonProps.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',
                ...(buttonProps.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>{buttonProps.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}
      />
    </>
  );
};
@@ -1842,3 +1765,10 @@
};
export default TabContainer;