shimai
2026-02-27 cd65fdeabc462afd4fe99e4c789a243bf9b64ee6
web-app/src/components/dashboard/TabContainer.jsx
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from 'react';
import React, { useState, useEffect, useRef, forwardRef, useImperativeHandle } from 'react';
import { useCaseData } from '../../contexts/CaseDataContext';
import { formatDuration, formatSuccessRate, formatRoundCount } from '../../utils/stateTranslator';
import ProcessAPIService from '../../services/ProcessAPIService';
@@ -6,16 +6,25 @@
import MediationAgreementAPIService from '../../services/MediationAgreementAPIService';
import { getMergedParams } from '../../utils/urlParams';
import { message, Spin, Tag, Modal, Button, Input, Image } from 'antd';
import config from '../../config/env';
const { TextArea } = Input;
/**
 * 选项卡容器组件 - 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 +85,7 @@
      </div>
    </div>
  );
};
});
/**
 * 调解数据看板
@@ -1282,6 +1291,32 @@
    });
  };
  const resolveDownloadFileName = (contentDisposition) => {
    if (!contentDisposition) return '';
    const utf8Match = contentDisposition.match(/filename\*\s*=\s*UTF-8''([^;]+)/i);
    if (utf8Match?.[1]) {
      try {
        return decodeURIComponent(utf8Match[1]);
      } catch (err) {
        return utf8Match[1];
      }
    }
    const fileNameMatch = contentDisposition.match(/filename\s*=\s*"?([^"]+)"?/i);
    if (fileNameMatch?.[1]) return fileNameMatch[1];
    return '';
  };
  const triggerFileDownload = (blob, fileName) => {
    const url = window.URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.href = url;
    link.download = fileName;
    document.body.appendChild(link);
    link.click();
    link.remove();
    window.URL.revokeObjectURL(url);
  };
  // 首次加载协议内容
  const loadAgreement = async () => {
    if (!caseId) {
@@ -1333,11 +1368,45 @@
    if (!caseId) return;
    setActionLoading(prev => ({ ...prev, download: true }));
    try {
      await MediationAgreementAPIService.downloadAgreement(caseId);
      message.success('协议下载成功!');
      const token = localStorage.getItem('access_token');
      const response = await fetch(`${config.baseURL}/api/v1/medi-agreement/download`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          ...(token ? { Authorization: `Bearer ${token}` } : {})
        },
        body: JSON.stringify({ caseId }),
        credentials: config.withCredentials ? 'include' : 'omit'
      });
      const contentType = response.headers.get('Content-Type') || '';
      if (contentType.includes('application/pdf')) {
        if (!response.ok) {
          throw new Error('下载协议失败,请稍后重试');
        }
        const blob = await response.blob();
        const contentDisposition = response.headers.get('Content-Disposition') || '';
        const fileName = resolveDownloadFileName(contentDisposition) || '调解协议书.pdf';
        triggerFileDownload(blob, fileName);
        message.success('协议下载成功!');
        return;
      }
      const data = await response.json();
      if (data?.code !== 200 && data?.code !== 201) {
        throw new Error(data?.message || '下载协议失败,请稍后重试');
      }
      if (data?.data?.needConfirm || data?.message === '待确认') {
        message.warning('协议待确认,请先确认协议');
        return;
      }
      if (data?.data?.agreeContent) {
        setAgreementContent(data.data.agreeContent);
      }
      message.success(data?.message || '已返回协议内容');
    } catch (err) {
      console.error('下载协议失败:', err);
      message.error('下载协议失败,请稍后重试');
      message.error(err?.message || '下载协议失败,请稍后重试');
    } finally {
      setActionLoading(prev => ({ ...prev, download: false }));
    }