| | |
| | | import React, { useState } from 'react'; |
| | | import React, { useState, useEffect, useCallback, useRef, forwardRef, useImperativeHandle } from 'react'; |
| | | import { useCaseData } from '../../contexts/CaseDataContext'; |
| | | 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; |
| | | |
| | | /** |
| | | * 选项卡容器组件 - 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' }, |
| | | { key: 'mediation-board', label: 'AI调解实时看板', icon: 'fa-tv' }, |
| | | { key: 'evidence-board', label: '证据材料汇总', icon: 'fa-file-alt', badge: '待审核' }, |
| | | { key: 'evidence-board', label: '证据材料汇总', icon: 'fa-file-alt', badge: evidenceBadge }, |
| | | { key: 'agreement-section', label: '调解协议', icon: 'fa-file-contract', badge: '待确认' }, |
| | | ]; |
| | | |
| | | // 更新证据材料汇总Tab的badge状态 |
| | | const handleEvidenceStatusChange = (status) => { |
| | | setEvidenceBadge(status); |
| | | }; |
| | | |
| | | return ( |
| | | <div className="tab-container"> |
| | |
| | | {/* AI调解实时看板 */} |
| | | <div className={`tab-pane ${activeTab === 'mediation-board' ? 'active' : ''}`}> |
| | | <div className="tab-content-area"> |
| | | <MediationBoard /> |
| | | <MediationBoard activeTab={activeTab} /> |
| | | </div> |
| | | </div> |
| | | |
| | | {/* 证据材料汇总 */} |
| | | <div className={`tab-pane ${activeTab === 'evidence-board' ? 'active' : ''}`}> |
| | | <div className="tab-content-area"> |
| | | <EvidenceBoard /> |
| | | <EvidenceBoard onStatusChange={handleEvidenceStatusChange} /> |
| | | </div> |
| | | </div> |
| | | |
| | |
| | | </div> |
| | | </div> |
| | | ); |
| | | }); |
| | | |
| | | /** |
| | | * 获取成功率同比数据 |
| | | */ |
| | | 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(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> |
| | |
| | | <i className="fas fa-exclamation-circle"></i> |
| | | 主要分歧点 |
| | | </div> |
| | | <div className="gap-update-time">3小时前更新</div> |
| | | <div className="gap-update-time">{updateTime}</div> |
| | | <div className="gap-content"> |
| | | 双方在以下方面存在明显分歧: |
| | | <ul> |
| | | <li> |
| | | <strong>拖欠工资金额</strong>:李晓明主张拖欠3个月工资¥42,000,公司承认拖欠但只认可¥35,000 |
| | | </li> |
| | | <li> |
| | | <strong>克扣绩效奖金</strong>:李晓明主张应发绩效奖金¥10,800,公司以未完成KPI为由拒绝支付 |
| | | </li> |
| | | <li> |
| | | <strong>经济补偿金</strong>:李晓明要求支付解除劳动合同经济补偿¥12,000,公司认为员工主动辞职不应支付 |
| | | </li> |
| | | </ul> |
| | | {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">68%</div> |
| | | <div className="success-label">预计调解成功概率</div> |
| | | <div className="success-change"> |
| | | <i className="fas fa-arrow-up"></i> |
| | | <span>较3小时前 +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 }}>第6轮</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> |
| | | ); |
| | |
| | | /** |
| | | * AI调解实时看板 |
| | | */ |
| | | const MediationBoard = () => { |
| | | const boardItems = [ |
| | | const MediationBoard = ({ activeTab }) => { |
| | | // 状态管理 |
| | | 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': 'applicant', |
| | | '2': 'respondent', |
| | | '3': 'ai', |
| | | '4': 'mediator' |
| | | }; |
| | | return typeMap[personType] || 'ai'; |
| | | }; |
| | | |
| | | // tag_style到UI标签样式的映射 |
| | | const getTagStyleType = (tagStyle) => { |
| | | const styleMap = { |
| | | 'red': 'tag-type-5', |
| | | 'blue': 'tag-type-1', |
| | | 'green': 'tag-type-2', |
| | | 'orange': 'tag-type-3' |
| | | }; |
| | | return styleMap[tagStyle] || 'tag-type-1'; |
| | | }; |
| | | |
| | | // 获取角色显示名称 |
| | | // 1: 申请人, 2: 被申请人, 3: AI调解员, 4: 调解员 |
| | | const getRoleDisplayName = (personType, creatorName) => { |
| | | const roleMap = { |
| | | '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: formatTimestamp(record.create_time), |
| | | content: record.result, |
| | | tags: record.tagList?.map(tag => ({ |
| | | text: tag.tag_name, |
| | | type: getTagStyleType(tag.tag_style) |
| | | })) || [] |
| | | })); |
| | | }; |
| | | |
| | | // 看板轮询间隔(毫秒) |
| | | 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) { |
| | | if (!silent) throw new Error('未找到调解ID'); |
| | | return; |
| | | } |
| | | |
| | | // 调用API获取记录列表 |
| | | const response = await ProcessAPIService.getProcessRecords({ |
| | | mediation_id: mediationId |
| | | }); |
| | | |
| | | // 格式化数据 |
| | | const formattedRecords = formatRecordData(response.data || []); |
| | | if (isBoardMountedRef.current) { |
| | | setRecords(formattedRecords); |
| | | } |
| | | |
| | | } catch (err) { |
| | | if (!silent) { |
| | | setError(err.message); |
| | | console.error('获取调解记录失败:', err); |
| | | message.error(`获取调解记录失败: ${err.message}`); |
| | | } else { |
| | | console.warn('[MediationBoard] 静默刷新失败:', err.message); |
| | | } |
| | | } finally { |
| | | if (!silent) { |
| | | setLoading(false); |
| | | } |
| | | } |
| | | }, [timeline.mediation?.id]); |
| | | |
| | | // Tab激活时:首次加载 + 启动周期性轮询 |
| | | useEffect(() => { |
| | | isBoardMountedRef.current = true; |
| | | |
| | | if (activeTab === 'mediation-board') { |
| | | // 首次加载(带 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; |
| | | }; |
| | | } |
| | | |
| | | return () => { isBoardMountedRef.current = false; }; |
| | | }, [activeTab, loadMediationRecords]); |
| | | |
| | | // 如果还在加载中,显示Loading状态 |
| | | if (loading) { |
| | | return ( |
| | | <div style={{ |
| | | display: 'flex', |
| | | justifyContent: 'center', |
| | | alignItems: 'center', |
| | | height: '400px' |
| | | }}> |
| | | <Spin size="large" tip="正在加载调解记录..." /> |
| | | </div> |
| | | ); |
| | | } |
| | | |
| | | // 如果有错误,显示错误信息 |
| | | if (error) { |
| | | return ( |
| | | <div style={{ |
| | | textAlign: 'center', |
| | | padding: '40px', |
| | | color: '#ff4d4f' |
| | | }}> |
| | | <div style={{ fontSize: '1.2rem', marginBottom: '10px' }}> |
| | | <i className="fas fa-exclamation-circle"></i> |
| | | 数据加载失败 |
| | | </div> |
| | | <div>{error}</div> |
| | | <button |
| | | onClick={loadMediationRecords} |
| | | style={{ |
| | | marginTop: '15px', |
| | | padding: '8px 16px', |
| | | backgroundColor: '#1890ff', |
| | | color: 'white', |
| | | border: 'none', |
| | | borderRadius: '4px', |
| | | cursor: 'pointer' |
| | | }} |
| | | > |
| | | 重新加载 |
| | | </button> |
| | | </div> |
| | | ); |
| | | } |
| | | |
| | | // 使用动态数据或默认mock数据 |
| | | const displayRecords = records.length > 0 ? records : [ |
| | | { |
| | | avatar: 'ai', |
| | | name: 'AI调解员', |
| | | avatarText: '', |
| | | time: '09:30:05', |
| | | content: '已分别联系李晓明(申请人)和广东好又多贸易有限公司(被申请人)。双方均表示愿意接受调解,希望通过协商解决劳动争议/拖欠、克扣工资纠纷。', |
| | | tags: [{ text: '意愿调查', type: 'tag-type-1' }, { text: '初步接触', type: 'tag-type-2' }], |
| | |
| | | { |
| | | avatar: 'applicant', |
| | | name: '申请人(李晓明)', |
| | | avatarText: '李', |
| | | time: '09:35:22', |
| | | content: '我在好又多公司担任销售经理3年,公司拖欠我3个月工资共¥42,000,还克扣了季度绩效奖金¥10,800。另外,公司单方面解除劳动合同,应支付经济补偿金¥12,000。我要求公司支付总共¥52,800。', |
| | | tags: [{ text: '诉求表达', type: 'tag-type-3' }, { text: '情绪激动', type: 'tag-type-5' }], |
| | |
| | | { |
| | | avatar: 'respondent', |
| | | name: '被申请人(好又多公司)', |
| | | avatarText: '好', |
| | | time: '09:40:15', |
| | | content: '公司确实遇到了资金周转困难,拖欠工资问题承认。但李晓明主张的金额有误:1) 3个月工资应为¥35,000(含请假扣款);2) 绩效奖金因未完成KPI指标不应发放;3) 是李晓明主动提出辞职,不应支付经济补偿金。公司最多支付¥38,500。', |
| | | tags: [{ text: '诉求表达', type: 'tag-type-3' }], |
| | | }, |
| | | ]; |
| | | |
| | | // 从timeline获取沟通情况总结 |
| | | const communicationSummary = timeline.mediation?.summary || '暂无沟通情况总结'; |
| | | |
| | | const getAvatarClass = (avatar) => { |
| | | const map = { |
| | | ai: 'avatar-ai', |
| | |
| | | return map[avatar] || 'content-ai'; |
| | | }; |
| | | |
| | | const getAvatarContent = (avatar) => { |
| | | const getAvatarContent = (avatar, avatarText) => { |
| | | if (avatar === 'ai') return <i className="fas fa-robot"></i>; |
| | | if (avatar === 'applicant') return '李'; |
| | | if (avatar === 'respondent') return '好'; |
| | | if (avatar === 'mediator') return '调'; |
| | | return <i className="fas fa-robot"></i>; |
| | | // 显示名字的第一个字 |
| | | return avatarText || (avatar === 'applicant' ? '申' : avatar === 'respondent' ? '被' : '调'); |
| | | }; |
| | | |
| | | return ( |
| | |
| | | 沟通情况总结 |
| | | </div> |
| | | <div style={{ lineHeight: 1.5 }}> |
| | | 截至目前,AI调解员已与申请双方进行协商沟通6轮,期间申请人补充材料2次,被申请人补充材料2次,双方对调解方案表现出积极协商态度。 |
| | | {communicationSummary} |
| | | </div> |
| | | </div> |
| | | |
| | | <div className="board-container" style={{ maxHeight: 450, overflowY: 'auto' }}> |
| | | {boardItems.map((item, index) => ( |
| | | {displayRecords.map((item, index) => ( |
| | | <div key={index} className="board-item" style={{ |
| | | background: '#f8f9fa', |
| | | borderRadius: 'var(--border-radius)', |
| | |
| | | item.avatar === 'respondent' ? 'linear-gradient(135deg, #e9c46a, #e76f51)' : |
| | | 'linear-gradient(135deg, #7209b7, #3a0ca3)', |
| | | }}> |
| | | {getAvatarContent(item.avatar)} |
| | | {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> |
| | |
| | | </div> |
| | | ))} |
| | | </div> |
| | | |
| | | {/* 通话记录弹窗 */} |
| | | <CallRecordModal |
| | | visible={callRecordVisible} |
| | | onClose={() => setCallRecordVisible(false)} |
| | | record={currentRecord} |
| | | /> |
| | | </> |
| | | ); |
| | | }; |
| | |
| | | /** |
| | | * 证据材料汇总 |
| | | */ |
| | | const EvidenceBoard = () => { |
| | | const applicantEvidence = [ |
| | | { name: '劳动合同', desc: '2023年8月1日-2026年1月31日,约定月工资¥14,000', time: '2026-01-15 09:35', status: 'verified' }, |
| | | { name: '2026年1-3月工资条', desc: '显示工资发放记录,1月后无发放记录', time: '2026-01-15 09:36', status: 'verified' }, |
| | | { name: '2026年第一季度考勤记录', desc: '显示全勤,无请假缺勤记录', time: '2026-01-15 09:37', status: 'verified' }, |
| | | { name: '绩效考评表', desc: '第一季度绩效评分85分,达到奖金发放标准', time: '2026-01-15 09:38', status: 'verified' }, |
| | | { name: '解除劳动合同通知书', desc: '公司2026年1月15日单方面解除合同', time: '2026-01-15 09:45', status: 'pending' }, |
| | | ]; |
| | | const EvidenceBoard = ({ onStatusChange }) => { |
| | | // 从 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 [modalDataLoading, setModalDataLoading] = useState(false); |
| | | const [personInfo, setPersonInfo] = useState(null); |
| | | const [evidenceImages, setEvidenceImages] = useState([]); |
| | | const [platformUrl, setPlatformUrl] = useState(''); |
| | | |
| | | const respondentEvidence = [ |
| | | { name: '2026年1-3月考勤异常记录', desc: '显示李晓明1月有2天事假,应扣款¥1,333', time: '2026-01-15 09:50', status: 'pending' }, |
| | | { name: '第一季度销售业绩报表', desc: '显示李晓明未完成销售KPI指标(完成率87%)', time: '2026-01-15 09:52', status: 'pending' }, |
| | | { name: '员工离职申请表', desc: '李晓明2026年1月10日提交的辞职申请', time: '2026-01-15 09:55', status: 'pending' }, |
| | | ]; |
| | | // 计算整体审核状态(综合申请人和被申请人所有材料) |
| | | const calculateOverallTabStatus = (applicantList, respondentList) => { |
| | | const allMaterials = [...applicantList, ...respondentList]; |
| | | if (!allMaterials || allMaterials.length === 0) return null; |
| | | |
| | | // 存在驳回状态 -> 显示"驳回" |
| | | const hasRejected = allMaterials.some(m => m.audit_state === -2); |
| | | if (hasRejected) return '驳回'; |
| | | |
| | | // 存在待审核状态(0或-2) -> 显示"待审核" |
| | | const hasPending = allMaterials.some(m => m.audit_state === 0 || m.audit_state === -2); |
| | | if (hasPending) return '待审核'; |
| | | |
| | | // 所有材料都已审核 -> 显示"已审核" |
| | | return '已审核'; |
| | | }; |
| | | |
| | | // 监听数据变化,通知Tab标题状态 |
| | | useEffect(() => { |
| | | 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) => { |
| | | if (!materials || materials.length === 0) return { text: '无数据', color: 'default' }; |
| | | const hasRejected = materials.some(m => m.audit_state === -2); |
| | | if (hasRejected) return { text: '驳回', color: 'red' }; |
| | | const hasPending = materials.some(m => m.audit_state === 0 || m.audit_state === -2); |
| | | if (hasPending) return { text: '待审核', color: 'orange' }; |
| | | return { text: '已审核', color: 'green' }; |
| | | }; |
| | | |
| | | const applicantStatus = calculateOverallStatus(applicantMaterials); |
| | | const respondentStatus = calculateOverallStatus(respondentMaterials); |
| | | |
| | | // Loading状态 |
| | | if (loading) { |
| | | return ( |
| | | <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: 300 }}> |
| | | <Spin size="large" tip="加载证据材料中..." /> |
| | | </div> |
| | | ); |
| | | } |
| | | |
| | | // 错误状态 |
| | | if (error) { |
| | | return ( |
| | | <div style={{ textAlign: 'center', padding: 40, color: '#ff4d4f' }}> |
| | | <div style={{ fontSize: '1.2rem', marginBottom: 10 }}> |
| | | <i className="fas fa-exclamation-circle"></i> 数据加载失败 |
| | | </div> |
| | | <div>{error}</div> |
| | | <button onClick={handleReload} style={{ |
| | | marginTop: 15, padding: '8px 16px', backgroundColor: '#1890ff', |
| | | color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' |
| | | }}>重新加载</button> |
| | | </div> |
| | | ); |
| | | } |
| | | |
| | | // 获取材料审核状态样式 |
| | | const getAuditStatusStyle = (auditState) => { |
| | | if (auditState === 1) { |
| | | // 已审核 - 绿色边框 |
| | | return { |
| | | color: '#52c41a', |
| | | background: '#f6ffed', |
| | | border: '1px solid #b7eb8f', |
| | | borderRadius: 4, |
| | | padding: '2px 10px', |
| | | fontSize: '0.8rem', |
| | | fontWeight: 500, |
| | | }; |
| | | } |
| | | // 待审核 - 橙色边框 |
| | | return { |
| | | color: '#fa8c16', |
| | | background: '#fff7e6', |
| | | border: '1px solid #ffd591', |
| | | borderRadius: 4, |
| | | padding: '2px 10px', |
| | | fontSize: '0.8rem', |
| | | fontWeight: 500, |
| | | }; |
| | | }; |
| | | |
| | | // 打开审核弹窗 |
| | | const handleOpenAuditModal = async (item, type) => { |
| | | // 获取参数 |
| | | const params = getMergedParams(); |
| | | const caseId = params.caseId; |
| | | |
| | | // 设置当前审核项 |
| | | const auditItem = { |
| | | ...item, |
| | | personType: type, |
| | | per_type: type === 'applicant' ? '15_020008-1' : '15_020008-2' |
| | | }; |
| | | setCurrentAuditItem(auditItem); |
| | | setAuditRemark(''); |
| | | setAuditModalVisible(true); |
| | | |
| | | // 获取platform_url |
| | | try { |
| | | const caseDataTimeline = JSON.parse(localStorage.getItem('case_data_timeline') || '{}'); |
| | | const pUrl = caseDataTimeline?.process_config?.platform_url || ''; |
| | | setPlatformUrl(pUrl); |
| | | } catch (e) { |
| | | console.warn('获取platform_url失败:', e); |
| | | } |
| | | |
| | | // 调用API获取弹窗数据 |
| | | setModalDataLoading(true); |
| | | setPersonInfo(null); |
| | | setEvidenceImages([]); |
| | | |
| | | try { |
| | | // 并行调用两个API |
| | | const [personInfoRes, evidenceListRes] = await Promise.all([ |
| | | EvidenceAPIService.getPersonInfo({ |
| | | case_id: caseId, |
| | | evidence_type: item.evidence_type, |
| | | per_type: auditItem.per_type |
| | | }), |
| | | EvidenceAPIService.getEvidenceListByPerson({ |
| | | case_id: caseId, |
| | | evidence_type: item.evidence_type, |
| | | per_type: auditItem.per_type, |
| | | person_id: item.person_id |
| | | }) |
| | | ]); |
| | | |
| | | console.log('getPersonInfo response:', personInfoRes); |
| | | console.log('getEvidenceListByPerson response:', evidenceListRes); |
| | | |
| | | // 设置当事人信息 |
| | | if (personInfoRes?.data) { |
| | | setPersonInfo(personInfoRes.data); |
| | | } |
| | | |
| | | // 设置图片列表 |
| | | if (evidenceListRes?.data && Array.isArray(evidenceListRes.data)) { |
| | | setEvidenceImages(evidenceListRes.data); |
| | | } |
| | | } catch (err) { |
| | | console.error('加载弹窗数据失败:', err); |
| | | message.error('加载材料详情失败'); |
| | | } finally { |
| | | setModalDataLoading(false); |
| | | } |
| | | }; |
| | | |
| | | // 关闭审核弹窗 |
| | | const handleCloseAuditModal = () => { |
| | | setAuditModalVisible(false); |
| | | setCurrentAuditItem(null); |
| | | setAuditRemark(''); |
| | | setPersonInfo(null); |
| | | setEvidenceImages([]); |
| | | }; |
| | | |
| | | // 审核通过 |
| | | const handleAuditPass = async () => { |
| | | if (!currentAuditItem) return; |
| | | |
| | | const params = getMergedParams(); |
| | | const caseId = params.caseId; |
| | | |
| | | setAuditLoading(true); |
| | | try { |
| | | // 获取文件ID列表 |
| | | const fileIdList = evidenceImages.map(img => img.file_id || img.id).filter(Boolean); |
| | | |
| | | await EvidenceAPIService.auditEvidence({ |
| | | case_id: caseId, |
| | | evidence_type: currentAuditItem.evidence_type, |
| | | person_id: currentAuditItem.person_id, |
| | | file_id_list: fileIdList, |
| | | audit_user: '当前用户', // TODO: 从登录信息获取 |
| | | audit_state: 1, |
| | | audit_remark: '' |
| | | }); |
| | | |
| | | message.success('审核通过!'); |
| | | handleCloseAuditModal(); |
| | | // 重新加载数据 |
| | | handleReload(); |
| | | } catch (err) { |
| | | console.error('审核失败:', err); |
| | | message.error('审核失败,请重试'); |
| | | } finally { |
| | | setAuditLoading(false); |
| | | } |
| | | }; |
| | | |
| | | // 退回补充 |
| | | const handleAuditReturn = async () => { |
| | | if (!currentAuditItem) return; |
| | | if (!auditRemark.trim()) { |
| | | message.warning('请填写退回意见'); |
| | | return; |
| | | } |
| | | |
| | | const params = getMergedParams(); |
| | | const caseId = params.caseId; |
| | | |
| | | setAuditLoading(true); |
| | | try { |
| | | // 获取文件ID列表 |
| | | const fileIdList = evidenceImages.map(img => img.file_id || img.id).filter(Boolean); |
| | | |
| | | await EvidenceAPIService.auditEvidence({ |
| | | case_id: caseId, |
| | | evidence_type: currentAuditItem.evidence_type, |
| | | person_id: currentAuditItem.person_id, |
| | | file_id_list: fileIdList, |
| | | audit_user: '当前用户', // TODO: 从登录信息获取 |
| | | audit_state: 2, |
| | | audit_remark: auditRemark.trim() |
| | | }); |
| | | |
| | | message.success('材料已退回,等待补充提交'); |
| | | setReturnModalVisible(false); |
| | | handleCloseAuditModal(); |
| | | handleReload(); |
| | | } catch (err) { |
| | | console.error('退回失败:', err); |
| | | message.error('退回失败,请重试'); |
| | | } finally { |
| | | setAuditLoading(false); |
| | | } |
| | | }; |
| | | |
| | | // 打开退回弹窗 |
| | | const handleOpenReturnModal = () => { |
| | | setAuditRemark(''); |
| | | setReturnModalVisible(true); |
| | | }; |
| | | |
| | | // 关闭退回弹窗 |
| | | const handleCloseReturnModal = () => { |
| | | setReturnModalVisible(false); |
| | | setAuditRemark(''); |
| | | }; |
| | | |
| | | const renderEvidenceItem = (item, type) => ( |
| | | <div key={item.name} className="evidence-item" style={{ |
| | |
| | | justifyContent: 'space-between', |
| | | alignItems: 'flex-start', |
| | | marginBottom: 10, |
| | | position: 'relative', |
| | | }}> |
| | | <div style={{ flex: 1 }}> |
| | | <div style={{ flex: 1, paddingRight: 70 }}> |
| | | <div style={{ fontWeight: 600, fontSize: '0.9rem', marginBottom: 4, color: 'var(--dark-color)' }}>{item.name}</div> |
| | | <div style={{ fontSize: '0.8rem', color: 'var(--gray-color)', lineHeight: 1.4 }}>{item.desc}</div> |
| | | <div style={{ fontSize: '0.8rem', color: 'var(--gray-color)', lineHeight: 1.4 }}>{item.result}</div> |
| | | <div style={{ fontSize: '0.75rem', color: 'var(--gray-color)', marginTop: 4, display: 'flex', alignItems: 'center', gap: 5 }}> |
| | | <i className="far fa-clock"></i> |
| | | <span>上传时间:{item.time}</span> |
| | | <span>上传时间:{item.update_time || item.create_time}</span> |
| | | </div> |
| | | {/* 待审核时显示审核按钮 */} |
| | | {item.audit_state !== 1 && ( |
| | | <button |
| | | onClick={() => handleOpenAuditModal(item, type)} |
| | | style={{ |
| | | marginTop: 8, |
| | | padding: '4px 12px', |
| | | fontSize: '0.75rem', |
| | | background: '#1A6FB8', |
| | | color: 'white', |
| | | border: 'none', |
| | | borderRadius: 4, |
| | | cursor: 'pointer', |
| | | fontWeight: 600, |
| | | transition: 'all 0.2s ease', |
| | | }} |
| | | onMouseOver={(e) => { |
| | | e.target.style.background = '#0d4a8a'; |
| | | e.target.style.transform = 'translateY(-1px)'; |
| | | }} |
| | | onMouseOut={(e) => { |
| | | e.target.style.background = '#1A6FB8'; |
| | | e.target.style.transform = 'translateY(0)'; |
| | | }} |
| | | > |
| | | 审核 |
| | | </button> |
| | | )} |
| | | </div> |
| | | <div style={{ marginLeft: 10 }}> |
| | | <span style={{ |
| | | fontSize: '0.75rem', |
| | | padding: '3px 8px', |
| | | borderRadius: 12, |
| | | fontWeight: 600, |
| | | background: item.status === 'verified' ? '#d4edda' : '#fff3cd', |
| | | color: item.status === 'verified' ? '#155724' : '#856404', |
| | | border: `1px solid ${item.status === 'verified' ? '#c3e6cb' : '#ffeaa7'}`, |
| | | }}> |
| | | {item.status === 'verified' ? '已审核' : '待审核'} |
| | | </span> |
| | | {/* 右上角审核状态标签 */} |
| | | <div style={{ |
| | | position: 'absolute', |
| | | top: 12, |
| | | right: 12, |
| | | ...getAuditStatusStyle(item.audit_state) |
| | | }}> |
| | | {item.audit_state === 1 ? '已审核' : '待审核'} |
| | | </div> |
| | | </div> |
| | | ); |
| | |
| | | <i className="fas fa-user-tie" style={{ color: 'var(--primary-color)', marginRight: 8 }}></i> |
| | | 申请人材料 |
| | | </div> |
| | | <div style={{ fontSize: '0.85rem', color: 'var(--gray-color)', marginLeft: 'auto' }}>{applicantEvidence.length}份材料</div> |
| | | <Tag color={applicantStatus.color}>{applicantStatus.text}</Tag> |
| | | <div style={{ fontSize: '0.85rem', color: 'var(--gray-color)', marginLeft: 'auto' }}>{applicantMaterials.length}份材料</div> |
| | | </div> |
| | | <div> |
| | | {applicantEvidence.map((item) => renderEvidenceItem(item, 'applicant'))} |
| | | <div style={{ maxHeight: 400, overflowY: 'auto' }}> |
| | | {applicantMaterials.length > 0 ? ( |
| | | applicantMaterials.map((item) => renderEvidenceItem(item, 'applicant')) |
| | | ) : ( |
| | | <div style={{ textAlign: 'center', padding: 40, color: 'var(--gray-color)' }}>暂无材料</div> |
| | | )} |
| | | </div> |
| | | </div> |
| | | |
| | |
| | | <i className="fas fa-building" style={{ color: '#e9c46a', marginRight: 8 }}></i> |
| | | 被申请人材料 |
| | | </div> |
| | | <div style={{ fontSize: '0.85rem', color: 'var(--gray-color)', marginLeft: 'auto' }}>{respondentEvidence.length}份材料</div> |
| | | <Tag color={respondentStatus.color}>{respondentStatus.text}</Tag> |
| | | <div style={{ fontSize: '0.85rem', color: 'var(--gray-color)', marginLeft: 'auto' }}>{respondentMaterials.length}份材料</div> |
| | | </div> |
| | | <div> |
| | | {respondentEvidence.map((item) => renderEvidenceItem(item, 'respondent'))} |
| | | <div style={{ maxHeight: 400, overflowY: 'auto' }}> |
| | | {respondentMaterials.length > 0 ? ( |
| | | respondentMaterials.map((item) => renderEvidenceItem(item, 'respondent')) |
| | | ) : ( |
| | | <div style={{ textAlign: 'center', padding: 40, color: 'var(--gray-color)' }}>暂无材料</div> |
| | | )} |
| | | </div> |
| | | </div> |
| | | |
| | | {/* 证据材料审查弹窗 */} |
| | | <Modal |
| | | title={null} |
| | | visible={auditModalVisible} |
| | | onCancel={handleCloseAuditModal} |
| | | footer={null} |
| | | width={900} |
| | | centered |
| | | destroyOnClose |
| | | bodyStyle={{ padding: 0 }} |
| | | className="doc-audit-modal" |
| | | > |
| | | {currentAuditItem && ( |
| | | <div style={{ background: '#f5f7fa' }}> |
| | | {/* 头部 - 蓝色渐变 */} |
| | | <div style={{ |
| | | background: 'linear-gradient(135deg, #1A6FB8 0%, #0d4a8a 100%)', |
| | | color: 'white', |
| | | padding: '20px 30px', |
| | | }}> |
| | | <div style={{ fontSize: 20, fontWeight: 600, marginBottom: 8 }}>证据材料审查</div> |
| | | <div style={{ fontSize: 14, opacity: 0.9 }}> |
| | | {currentAuditItem.personType === 'applicant' ? '申请人' : '被申请人'}提交的证据材料 |
| | | </div> |
| | | </div> |
| | | |
| | | {/* 主体内容 */} |
| | | <div style={{ padding: '20px 30px', background: 'white', margin: 0 }}> |
| | | {/* 审核说明 tip-box */} |
| | | <div style={{ |
| | | background: '#f0f8ff', |
| | | borderRadius: 8, |
| | | padding: 16, |
| | | marginBottom: 24, |
| | | border: '1px solid #d9eafb', |
| | | }}> |
| | | <div style={{ color: '#1A6FB8', marginBottom: 8, fontSize: 15, fontWeight: 600 }}>审核说明</div> |
| | | <div style={{ color: '#555', fontSize: 14 }}> |
| | | 请仔细核对申请人提交的材料,确保材料真实、完整、有效。如有疑问或需要补充材料,请使用"退回补充"功能。 |
| | | </div> |
| | | </div> |
| | | |
| | | {/* 材料基本信息 */} |
| | | <div style={{ marginBottom: 24 }}> |
| | | <div style={{ |
| | | color: '#1A6FB8', |
| | | fontSize: 16, |
| | | fontWeight: 600, |
| | | marginBottom: 16, |
| | | display: 'flex', |
| | | alignItems: 'center', |
| | | }}> |
| | | <span style={{ |
| | | display: 'inline-block', |
| | | width: 4, |
| | | height: 16, |
| | | background: '#1A6FB8', |
| | | marginRight: 10, |
| | | borderRadius: 2, |
| | | }}></span> |
| | | 材料基本信息 |
| | | </div> |
| | | {modalDataLoading ? ( |
| | | <div style={{ textAlign: 'center', padding: 20 }}> |
| | | <Spin tip="加载中..." /> |
| | | </div> |
| | | ) : ( |
| | | <div style={{ |
| | | display: 'grid', |
| | | gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', |
| | | gap: 16, |
| | | }}> |
| | | <div style={{ display: 'flex', marginBottom: 12 }}> |
| | | <span style={{ fontWeight: 500, color: '#666', minWidth: 80 }}>提交人:</span> |
| | | <span style={{ color: '#222', fontWeight: 500 }}> |
| | | {personInfo |
| | | ? `${personInfo.per_class_name || (currentAuditItem.personType === 'applicant' ? '申请人' : '被申请人')}-${personInfo.true_name || ''}` |
| | | : (currentAuditItem.personType === 'applicant' ? '申请方' : '被申请方')} |
| | | </span> |
| | | </div> |
| | | <div style={{ display: 'flex', marginBottom: 12 }}> |
| | | <span style={{ fontWeight: 500, color: '#666', minWidth: 80 }}>材料类型:</span> |
| | | <span style={{ color: '#222', fontWeight: 500 }}> |
| | | {currentAuditItem.evidence_type_name || currentAuditItem.name || '证据材料'} |
| | | </span> |
| | | </div> |
| | | <div style={{ display: 'flex', marginBottom: 12 }}> |
| | | <span style={{ fontWeight: 500, color: '#666', minWidth: 80 }}>材料数量:</span> |
| | | <span style={{ color: '#222', fontWeight: 500 }}> |
| | | {personInfo?.total_count ? `${personInfo.total_count}份` : `${evidenceImages.length || 1}份`} |
| | | </span> |
| | | </div> |
| | | <div style={{ display: 'flex', marginBottom: 12 }}> |
| | | <span style={{ fontWeight: 500, color: '#666', minWidth: 80 }}>提交时间:</span> |
| | | <span style={{ color: '#222', fontWeight: 500 }}> |
| | | {personInfo?.submit_time || currentAuditItem.update_time || currentAuditItem.create_time} |
| | | </span> |
| | | </div> |
| | | </div> |
| | | )} |
| | | </div> |
| | | |
| | | {/* 材料说明 */} |
| | | <div style={{ marginBottom: 24 }}> |
| | | <div style={{ |
| | | color: '#1A6FB8', |
| | | fontSize: 16, |
| | | fontWeight: 600, |
| | | marginBottom: 16, |
| | | display: 'flex', |
| | | alignItems: 'center', |
| | | }}> |
| | | <span style={{ |
| | | display: 'inline-block', |
| | | width: 4, |
| | | height: 16, |
| | | background: '#1A6FB8', |
| | | marginRight: 10, |
| | | borderRadius: 2, |
| | | }}></span> |
| | | 材料说明 |
| | | </div> |
| | | <div style={{ |
| | | border: '1px solid #e8e8e8', |
| | | borderRadius: 6, |
| | | padding: 16, |
| | | background: '#fafafa', |
| | | minHeight: 100, |
| | | fontSize: 14, |
| | | lineHeight: 1.7, |
| | | }}> |
| | | <p style={{ marginBottom: 8 }}> |
| | | <strong>{currentAuditItem.name}</strong> |
| | | </p> |
| | | <p style={{ margin: 0, color: '#555' }}> |
| | | {currentAuditItem.result || '暂无详细说明'} |
| | | </p> |
| | | </div> |
| | | </div> |
| | | |
| | | {/* 材料清单 */} |
| | | <div style={{ marginBottom: 24 }}> |
| | | <div style={{ |
| | | color: '#1A6FB8', |
| | | fontSize: 16, |
| | | fontWeight: 600, |
| | | marginBottom: 16, |
| | | display: 'flex', |
| | | alignItems: 'center', |
| | | }}> |
| | | <span style={{ |
| | | display: 'inline-block', |
| | | width: 4, |
| | | height: 16, |
| | | background: '#1A6FB8', |
| | | marginRight: 10, |
| | | borderRadius: 2, |
| | | }}></span> |
| | | 材料清单 |
| | | </div> |
| | | <table style={{ |
| | | width: '100%', |
| | | borderCollapse: 'collapse', |
| | | borderRadius: 6, |
| | | overflow: 'hidden', |
| | | boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.03)', |
| | | border: '1px solid #f0f0f0', |
| | | }}> |
| | | <thead> |
| | | <tr> |
| | | <th style={{ |
| | | background: '#fafafa', |
| | | color: '#333', |
| | | fontWeight: 600, |
| | | textAlign: 'left', |
| | | padding: 16, |
| | | borderBottom: '1px solid #f0f0f0', |
| | | fontSize: 14, |
| | | width: '40%', |
| | | }}>材料信息</th> |
| | | <th style={{ |
| | | background: '#fafafa', |
| | | color: '#333', |
| | | fontWeight: 600, |
| | | textAlign: 'left', |
| | | padding: 16, |
| | | borderBottom: '1px solid #f0f0f0', |
| | | fontSize: 14, |
| | | width: '60%', |
| | | }}>材料预览</th> |
| | | </tr> |
| | | </thead> |
| | | <tbody> |
| | | <tr> |
| | | <td style={{ padding: 16, borderBottom: '1px solid #f5f5f5', verticalAlign: 'top', fontSize: 14 }}> |
| | | <div style={{ fontWeight: 600, color: '#333', marginBottom: 6 }}>{currentAuditItem.name}</div> |
| | | <div style={{ fontSize: 13, color: '#666', marginBottom: 4 }}> |
| | | 材料类型:{currentAuditItem.evidence_type_name || '证据材料'} |
| | | </div> |
| | | <div style={{ fontSize: 13, color: '#666', marginBottom: 4 }}> |
| | | 文件格式:{personInfo?.suffix || 'PDF/图片'} |
| | | </div> |
| | | <div style={{ fontSize: 13, color: '#666', marginBottom: 4 }}> |
| | | 上传时间:{personInfo?.submit_time || currentAuditItem.update_time || currentAuditItem.create_time} |
| | | </div> |
| | | {personInfo?.total_file_size && ( |
| | | <div style={{ fontSize: 13, color: '#666', marginBottom: 4 }}> |
| | | 文件大小:{personInfo.total_file_size}MB |
| | | </div> |
| | | )} |
| | | </td> |
| | | <td style={{ padding: 16, borderBottom: '1px solid #f5f5f5', verticalAlign: 'top', fontSize: 14 }}> |
| | | {modalDataLoading ? ( |
| | | <div style={{ textAlign: 'center', padding: 20 }}> |
| | | <Spin size="small" /> |
| | | </div> |
| | | ) : evidenceImages.length > 0 ? ( |
| | | <> |
| | | <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 |
| | | ? (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} |
| | | style={{ |
| | | width: 100, |
| | | height: 80, |
| | | borderRadius: 4, |
| | | overflow: 'hidden', |
| | | border: '1px solid #e8e8e8', |
| | | cursor: 'pointer', |
| | | transition: 'all 0.2s', |
| | | }} |
| | | > |
| | | <Image |
| | | src={imgUrl} |
| | | alt={fileName} |
| | | style={{ width: '100%', height: '100%', objectFit: 'cover' }} |
| | | fallback="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjgwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxyZWN0IHdpZHRoPSIxMDAiIGhlaWdodD0iODAiIGZpbGw9IiNmNWY1ZjUiLz48dGV4dCB4PSI1MCIgeT0iNDAiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzk5OSI+5Zu+54mH6aKE6KeIPC90ZXh0Pjwvc3ZnPg==" |
| | | preview={{ |
| | | 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> |
| | | ); |
| | | })} |
| | | </div> |
| | | </Image.PreviewGroup> |
| | | <div style={{ fontSize: 13, color: '#666', marginTop: 8 }}>共{evidenceImages.length}个文件</div> |
| | | </> |
| | | ) : ( |
| | | <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}> |
| | | <div style={{ |
| | | width: 100, |
| | | height: 80, |
| | | borderRadius: 4, |
| | | overflow: 'hidden', |
| | | border: '1px solid #e8e8e8', |
| | | background: 'linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%)', |
| | | display: 'flex', |
| | | alignItems: 'center', |
| | | justifyContent: 'center', |
| | | color: '#999', |
| | | fontSize: 13, |
| | | fontWeight: 500, |
| | | }}> |
| | | 暂无图片 |
| | | </div> |
| | | </div> |
| | | )} |
| | | </td> |
| | | </tr> |
| | | </tbody> |
| | | </table> |
| | | </div> |
| | | |
| | | {/* 操作按钮 */} |
| | | <div style={{ |
| | | display: 'flex', |
| | | justifyContent: 'center', |
| | | gap: 16, |
| | | marginTop: 32, |
| | | paddingTop: 24, |
| | | borderTop: '1px solid #f0f0f0', |
| | | }}> |
| | | <Button |
| | | type="primary" |
| | | size="large" |
| | | loading={auditLoading} |
| | | onClick={handleAuditPass} |
| | | style={{ |
| | | background: '#1A6FB8', |
| | | borderColor: '#1A6FB8', |
| | | minWidth: 120, |
| | | height: 40, |
| | | fontWeight: 500, |
| | | boxShadow: '0 2px 0 rgba(0, 0, 0, 0.045)', |
| | | }} |
| | | > |
| | | 审核通过 |
| | | </Button> |
| | | <Button |
| | | size="large" |
| | | loading={auditLoading} |
| | | onClick={handleOpenReturnModal} |
| | | style={{ |
| | | background: 'white', |
| | | color: '#1A6FB8', |
| | | borderColor: '#1A6FB8', |
| | | minWidth: 120, |
| | | height: 40, |
| | | fontWeight: 500, |
| | | boxShadow: '0 2px 0 rgba(0, 0, 0, 0.015)', |
| | | }} |
| | | > |
| | | 退回补充 |
| | | </Button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | )} |
| | | </Modal> |
| | | |
| | | {/* 退回补充子弹窗 */} |
| | | <Modal |
| | | title={ |
| | | <div style={{ background: '#1A6FB8', margin: '-20px -24px', padding: '16px 24px', color: 'white' }}> |
| | | <span style={{ fontSize: 16, fontWeight: 600 }}>退回补充材料</span> |
| | | </div> |
| | | } |
| | | visible={returnModalVisible} |
| | | onCancel={handleCloseReturnModal} |
| | | footer={null} |
| | | width={480} |
| | | centered |
| | | destroyOnClose |
| | | bodyStyle={{ paddingTop: 24 }} |
| | | > |
| | | <div style={{ marginBottom: 20 }}> |
| | | <label style={{ display: 'block', fontWeight: 500, color: '#333', marginBottom: 8, fontSize: 14 }}> |
| | | 退回意见: |
| | | </label> |
| | | <TextArea |
| | | value={auditRemark} |
| | | onChange={(e) => setAuditRemark(e.target.value)} |
| | | placeholder="请详细说明需要补充的材料内容及原因..." |
| | | rows={5} |
| | | style={{ |
| | | borderRadius: 4, |
| | | resize: 'vertical', |
| | | }} |
| | | /> |
| | | </div> |
| | | <div style={{ |
| | | background: '#fafafa', |
| | | margin: '0 -24px -24px', |
| | | padding: '16px 24px', |
| | | display: 'flex', |
| | | justifyContent: 'flex-end', |
| | | gap: 12, |
| | | }}> |
| | | <Button onClick={handleCloseReturnModal} style={{ padding: '6px 16px' }}> |
| | | 取消 |
| | | </Button> |
| | | <Button |
| | | type="primary" |
| | | loading={auditLoading} |
| | | onClick={handleAuditReturn} |
| | | style={{ background: '#1A6FB8', borderColor: '#1A6FB8', padding: '6px 16px' }} |
| | | > |
| | | 确认退回 |
| | | </Button> |
| | | </div> |
| | | </Modal> |
| | | </div> |
| | | ); |
| | | }; |
| | |
| | | * 调解协议 |
| | | */ |
| | | const AgreementSection = () => { |
| | | // 从 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); |
| | | const [actionLoading, setActionLoading] = useState({ |
| | | confirm: false, |
| | | download: false, |
| | | regenerate: false, |
| | | }); |
| | | |
| | | // 获取 caseId(兼容驼峰和蛇形命名) |
| | | const caseId = caseData?.caseId || caseData?.case_id || getMergedParams().caseId; |
| | | |
| | | // 处理协议内容展示(纯文本,处理换行) |
| | | const renderAgreementContent = (content) => { |
| | | if (!content) return null; |
| | | // 处理 \n 换行,简单过滤 Markdown 符号 |
| | | const lines = content.split('\n'); |
| | | return lines.map((line, index) => { |
| | | // 简单过滤 ** 符号,保留文本 |
| | | const cleanLine = line.replace(/\*\*/g, ''); |
| | | if (!cleanLine.trim()) { |
| | | return <br key={index} />; |
| | | } |
| | | return ( |
| | | <p key={index} style={{ margin: '8px 0', lineHeight: 1.6 }}> |
| | | {cleanLine} |
| | | </p> |
| | | ); |
| | | }); |
| | | }; |
| | | |
| | | // 重新加载协议 |
| | | const handleReload = () => { |
| | | if (caseId) { |
| | | loadAgreementData(caseId); |
| | | } |
| | | }; |
| | | |
| | | // 确认协议 |
| | | const handleConfirmAgreement = async () => { |
| | | if (!caseId) return; |
| | | setActionLoading(prev => ({ ...prev, confirm: true })); |
| | | try { |
| | | await MediationAgreementAPIService.confirmAgreement(caseId, 'mediator'); |
| | | message.success('协议确认成功!'); |
| | | } catch (err) { |
| | | console.error('确认协议失败:', err); |
| | | message.error('确认协议失败,请稍后重试'); |
| | | } finally { |
| | | setActionLoading(prev => ({ ...prev, confirm: false })); |
| | | } |
| | | }; |
| | | |
| | | // 下载协议 |
| | | const handleDownloadAgreement = async () => { |
| | | if (!caseId) return; |
| | | setActionLoading(prev => ({ ...prev, download: true })); |
| | | try { |
| | | // 调用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); |
| | | message.error('下载协议失败,请稍后重试'); |
| | | } finally { |
| | | setActionLoading(prev => ({ ...prev, download: false })); |
| | | } |
| | | }; |
| | | |
| | | // 重新生成协议 |
| | | const handleRegenerateAgreement = async () => { |
| | | if (!caseId) return; |
| | | setActionLoading(prev => ({ ...prev, regenerate: true })); |
| | | try { |
| | | const response = await MediationAgreementAPIService.regenerateAgreement(caseId); |
| | | if (response?.data?.agreeContent) { |
| | | loadAgreementData(caseId); |
| | | message.success('协议重新生成成功!'); |
| | | } |
| | | } catch (err) { |
| | | console.error('重新生成协议失败:', err); |
| | | message.error('重新生成协议失败,请稍后重试'); |
| | | } finally { |
| | | setActionLoading(prev => ({ ...prev, regenerate: false })); |
| | | } |
| | | }; |
| | | |
| | | // 打开修改弹窗 |
| | | const handleOpenEditModal = async () => { |
| | | if (!caseId) return; |
| | | setEditModalVisible(true); |
| | | setEditLoading(true); |
| | | try { |
| | | const response = await MediationAgreementAPIService.getAgreementDetail(caseId); |
| | | if (response?.data?.agreeContent) { |
| | | setEditContent(response.data.agreeContent); |
| | | } |
| | | } catch (err) { |
| | | console.error('获取协议内容失败:', err); |
| | | message.error('获取协议内容失败'); |
| | | } finally { |
| | | setEditLoading(false); |
| | | } |
| | | }; |
| | | |
| | | // 关闭修改弹窗 |
| | | const handleCloseEditModal = () => { |
| | | setEditModalVisible(false); |
| | | setEditContent(''); |
| | | }; |
| | | |
| | | // 保存修改 |
| | | const handleSaveEdit = async () => { |
| | | if (!caseId || !editContent.trim()) { |
| | | message.warning('协议内容不能为空'); |
| | | return; |
| | | } |
| | | setEditLoading(true); |
| | | try { |
| | | await MediationAgreementAPIService.updateAgreement(caseId, editContent); |
| | | message.success('协议修改保存成功!'); |
| | | handleCloseEditModal(); |
| | | // 刷新协议内容 |
| | | loadAgreementData(caseId); |
| | | } catch (err) { |
| | | console.error('保存协议失败:', err); |
| | | message.error('保存协议失败,请稍后重试'); |
| | | } finally { |
| | | setEditLoading(false); |
| | | } |
| | | }; |
| | | |
| | | // Loading 状态 |
| | | if (loading) { |
| | | return ( |
| | | <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: 300 }}> |
| | | <Spin size="large" tip="正在生成调解协议..." /> |
| | | </div> |
| | | ); |
| | | } |
| | | |
| | | // 错误状态 |
| | | if (error) { |
| | | return ( |
| | | <div style={{ textAlign: 'center', padding: 40, color: '#ff4d4f' }}> |
| | | <div style={{ fontSize: '1.2rem', marginBottom: 10 }}> |
| | | <i className="fas fa-exclamation-circle"></i> {error} |
| | | </div> |
| | | <button onClick={handleReload} style={{ |
| | | marginTop: 15, padding: '8px 16px', backgroundColor: '#1890ff', |
| | | color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' |
| | | }}>重新加载</button> |
| | | </div> |
| | | ); |
| | | } |
| | | |
| | | return ( |
| | | <div style={{ |
| | | background: '#f8f9fa', |
| | |
| | | maxHeight: 450, |
| | | overflowY: 'auto', |
| | | }}> |
| | | {/* 协议内容展示区域 */} |
| | | <div style={{ lineHeight: 1.5, fontSize: '0.9rem' }}> |
| | | <div style={{ textAlign: 'center', color: 'var(--secondary-color)', fontSize: '1.2rem', marginBottom: 20, fontWeight: 700 }}> |
| | | 李晓明与广东好又多贸易有限公司劳动争议调解协议书 |
| | | </div> |
| | | |
| | | <p><strong>甲方(申请人):</strong>李晓明</p> |
| | | <p><strong>乙方(被申请人):</strong>广东好又多贸易有限公司</p> |
| | | |
| | | <p style={{ textIndent: '2em', marginTop: 15 }}> |
| | | 甲方与乙方因劳动报酬支付问题发生争议,双方于2026年1月15日向"云小调"劳动争议AI调解智能体申请调解。经AI调解员调解,双方本着平等自愿、互谅互让的原则,达成如下调解协议: |
| | | </p> |
| | | |
| | | <h3 style={{ color: 'var(--secondary-color)', margin: '15px 0 8px', fontSize: '1rem' }}>一、争议事项</h3> |
| | | <p style={{ textIndent: '2em' }}>甲方主张乙方拖欠2026年1月至3月共三个月工资、2026年第一季度绩效奖金以及解除劳动合同经济补偿金,合计人民币52,800元。</p> |
| | | |
| | | <h3 style={{ color: 'var(--secondary-color)', margin: '15px 0 8px', fontSize: '1rem' }}>二、调解结果</h3> |
| | | <p style={{ textIndent: '2em' }}>经调解,双方达成一致意见如下:</p> |
| | | <ul style={{ paddingLeft: 30, marginBottom: 12 }}> |
| | | <li style={{ marginBottom: 6 }}>乙方同意向甲方支付劳动报酬共计人民币肆万肆仟元整(¥44,000)</li> |
| | | <li style={{ marginBottom: 6 }}>支付方式:分两期支付 |
| | | <ul style={{ paddingLeft: 20 }}> |
| | | <li>第一期:人民币贰万贰仟元整(¥22,000),于2026年1月30日前支付</li> |
| | | <li>第二期:人民币贰万贰仟元整(¥22,000),于2026年2月28日前支付</li> |
| | | </ul> |
| | | </li> |
| | | <li style={{ marginBottom: 6 }}>支付账户:甲方指定的银行账户(账户信息另行提供)</li> |
| | | </ul> |
| | | |
| | | <h3 style={{ color: 'var(--secondary-color)', margin: '15px 0 8px', fontSize: '1rem' }}>三、双方权利义务</h3> |
| | | <ul style={{ paddingLeft: 30, marginBottom: 12 }}> |
| | | <li style={{ marginBottom: 6 }}>乙方应按约定时间足额支付上述款项</li> |
| | | <li style={{ marginBottom: 6 }}>甲方收到全部款项后,双方劳动关系正式解除</li> |
| | | <li style={{ marginBottom: 6 }}>双方互不追究其他法律责任,本协议履行完毕后,争议事项一次性了结</li> |
| | | </ul> |
| | | |
| | | {/* 签名区域 */} |
| | | <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 20, paddingTop: 15, borderTop: '1px solid var(--border-color)' }}> |
| | | <div style={{ textAlign: 'center', width: '45%' }}> |
| | | <div style={{ borderBottom: '1px solid var(--dark-color)', padding: '15px 0 5px', marginBottom: 5 }}></div> |
| | | <div>甲方(申请人):李晓明</div> |
| | | <div style={{ fontSize: '0.8rem', color: 'var(--gray-color)' }}>签字/盖章</div> |
| | | <div style={{ fontSize: '0.8rem', color: 'var(--gray-color)', marginTop: 5 }}>日期:2026年1月15日</div> |
| | | {agreementContent ? ( |
| | | renderAgreementContent(agreementContent) |
| | | ) : ( |
| | | <div style={{ textAlign: 'center', color: 'var(--gray-color)', padding: 40 }}> |
| | | 暂无协议内容 |
| | | </div> |
| | | <div style={{ textAlign: 'center', width: '45%' }}> |
| | | <div style={{ borderBottom: '1px solid var(--dark-color)', padding: '15px 0 5px', marginBottom: 5 }}></div> |
| | | <div>乙方(被申请人):广东好又多贸易有限公司</div> |
| | | <div style={{ fontSize: '0.8rem', color: 'var(--gray-color)' }}>签字/盖章</div> |
| | | <div style={{ fontSize: '0.8rem', color: 'var(--gray-color)', marginTop: 5 }}>日期:2026年1月15日</div> |
| | | </div> |
| | | </div> |
| | | )} |
| | | </div> |
| | | |
| | | {/* 操作按钮 */} |
| | |
| | | justifyContent: 'center', |
| | | gap: 12, |
| | | }}> |
| | | <button style={{ |
| | | padding: '10px 20px', |
| | | border: '2px solid #c3e6cb', |
| | | borderRadius: 'var(--border-radius)', |
| | | fontWeight: 600, |
| | | fontSize: '0.9rem', |
| | | cursor: 'pointer', |
| | | display: 'flex', |
| | | alignItems: 'center', |
| | | gap: 6, |
| | | background: '#d4edda', |
| | | color: '#155724', |
| | | }}> |
| | | <i className="fas fa-check-circle"></i> |
| | | <button |
| | | onClick={handleConfirmAgreement} |
| | | disabled={actionLoading.confirm} |
| | | style={{ |
| | | padding: '10px 20px', |
| | | border: '2px solid #c3e6cb', |
| | | borderRadius: 'var(--border-radius)', |
| | | fontWeight: 600, |
| | | fontSize: '0.9rem', |
| | | cursor: actionLoading.confirm ? 'not-allowed' : 'pointer', |
| | | display: 'flex', |
| | | alignItems: 'center', |
| | | gap: 6, |
| | | background: '#d4edda', |
| | | color: '#155724', |
| | | opacity: actionLoading.confirm ? 0.6 : 1, |
| | | }}> |
| | | <i className={actionLoading.confirm ? "fas fa-spinner fa-spin" : "fas fa-check-circle"}></i> |
| | | 确认协议 |
| | | </button> |
| | | <button style={{ |
| | | padding: '10px 20px', |
| | | border: '2px solid #ffeaa7', |
| | | borderRadius: 'var(--border-radius)', |
| | | fontWeight: 600, |
| | | fontSize: '0.9rem', |
| | | cursor: 'pointer', |
| | | display: 'flex', |
| | | alignItems: 'center', |
| | | gap: 6, |
| | | background: '#fff3cd', |
| | | color: '#856404', |
| | | }}> |
| | | <button |
| | | onClick={handleOpenEditModal} |
| | | style={{ |
| | | padding: '10px 20px', |
| | | border: '2px solid #ffeaa7', |
| | | borderRadius: 'var(--border-radius)', |
| | | fontWeight: 600, |
| | | fontSize: '0.9rem', |
| | | cursor: 'pointer', |
| | | display: 'flex', |
| | | alignItems: 'center', |
| | | gap: 6, |
| | | background: '#fff3cd', |
| | | color: '#856404', |
| | | }}> |
| | | <i className="fas fa-edit"></i> |
| | | 修改协议 |
| | | </button> |
| | | <button style={{ |
| | | padding: '10px 20px', |
| | | border: '2px solid #bbdefb', |
| | | borderRadius: 'var(--border-radius)', |
| | | fontWeight: 600, |
| | | fontSize: '0.9rem', |
| | | cursor: 'pointer', |
| | | display: 'flex', |
| | | alignItems: 'center', |
| | | gap: 6, |
| | | background: '#e3f2fd', |
| | | color: '#1565c0', |
| | | }}> |
| | | <i className="fas fa-download"></i> |
| | | <button |
| | | onClick={handleDownloadAgreement} |
| | | disabled={actionLoading.download} |
| | | style={{ |
| | | padding: '10px 20px', |
| | | border: '2px solid #bbdefb', |
| | | borderRadius: 'var(--border-radius)', |
| | | fontWeight: 600, |
| | | fontSize: '0.9rem', |
| | | cursor: actionLoading.download ? 'not-allowed' : 'pointer', |
| | | display: 'flex', |
| | | alignItems: 'center', |
| | | gap: 6, |
| | | background: '#e3f2fd', |
| | | color: '#1565c0', |
| | | opacity: actionLoading.download ? 0.6 : 1, |
| | | }}> |
| | | <i className={actionLoading.download ? "fas fa-spinner fa-spin" : "fas fa-download"}></i> |
| | | 下载协议 |
| | | </button> |
| | | <button style={{ |
| | | padding: '10px 20px', |
| | | border: '2px solid #bee5eb', |
| | | borderRadius: 'var(--border-radius)', |
| | | fontWeight: 600, |
| | | fontSize: '0.9rem', |
| | | cursor: 'pointer', |
| | | display: 'flex', |
| | | alignItems: 'center', |
| | | gap: 6, |
| | | background: '#d1ecf1', |
| | | color: '#0c5460', |
| | | }}> |
| | | <i className="fas fa-redo"></i> |
| | | <button |
| | | onClick={handleRegenerateAgreement} |
| | | disabled={actionLoading.regenerate} |
| | | style={{ |
| | | padding: '10px 20px', |
| | | border: '2px solid #bee5eb', |
| | | borderRadius: 'var(--border-radius)', |
| | | fontWeight: 600, |
| | | fontSize: '0.9rem', |
| | | cursor: actionLoading.regenerate ? 'not-allowed' : 'pointer', |
| | | display: 'flex', |
| | | alignItems: 'center', |
| | | gap: 6, |
| | | background: '#d1ecf1', |
| | | color: '#0c5460', |
| | | opacity: actionLoading.regenerate ? 0.6 : 1, |
| | | }}> |
| | | <i className={actionLoading.regenerate ? "fas fa-spinner fa-spin" : "fas fa-redo"}></i> |
| | | 重新生成 |
| | | </button> |
| | | </div> |
| | | |
| | | {/* 修改调解协议书弹窗 */} |
| | | <Modal |
| | | title={null} |
| | | visible={editModalVisible} |
| | | onCancel={handleCloseEditModal} |
| | | footer={null} |
| | | width={1000} |
| | | centered |
| | | destroyOnClose |
| | | bodyStyle={{ padding: 0 }} |
| | | > |
| | | <div style={{ background: '#f5f7fa' }}> |
| | | {/* 头部 */} |
| | | <div style={{ |
| | | background: '#1A6FB8', |
| | | color: 'white', |
| | | padding: '25px 30px', |
| | | borderBottom: '5px solid #0d4a8a', |
| | | }}> |
| | | <div style={{ fontSize: 24, fontWeight: 600, marginBottom: 10 }}>在线修改调解协议书</div> |
| | | <div style={{ fontSize: 16, opacity: 0.9 }}>编辑协议内容</div> |
| | | </div> |
| | | |
| | | {/* 主体内容 */} |
| | | <div style={{ padding: '25px 30px', background: 'white' }}> |
| | | {/* 编辑提示 */} |
| | | <div style={{ |
| | | fontSize: 14, |
| | | color: '#666', |
| | | marginBottom: 15, |
| | | padding: 10, |
| | | background: '#f0f8ff', |
| | | borderRadius: 5, |
| | | borderLeft: '3px solid #1A6FB8', |
| | | }}> |
| | | <strong>编辑提示:</strong>下方文本框中的协议内容可直接编辑修改。编辑完成后,请点击底部的"保存修改"按钮。 |
| | | </div> |
| | | |
| | | {/* 协议编辑区域 */} |
| | | <div style={{ marginBottom: 25 }}> |
| | | {editLoading ? ( |
| | | <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: 400 }}> |
| | | <Spin size="large" tip="加载协议内容..." /> |
| | | </div> |
| | | ) : ( |
| | | <TextArea |
| | | value={editContent} |
| | | onChange={(e) => setEditContent(e.target.value)} |
| | | style={{ |
| | | border: '1px solid #d0e3ff', |
| | | borderRadius: 8, |
| | | minHeight: 500, |
| | | padding: 20, |
| | | fontSize: 16, |
| | | lineHeight: 1.8, |
| | | background: '#f9fafc', |
| | | }} |
| | | placeholder="请输入协议内容..." |
| | | /> |
| | | )} |
| | | </div> |
| | | |
| | | {/* 操作按钮 */} |
| | | <div style={{ |
| | | display: 'flex', |
| | | justifyContent: 'flex-end', |
| | | marginTop: 30, |
| | | paddingTop: 20, |
| | | borderTop: '1px solid #eaeaea', |
| | | }}> |
| | | <Button |
| | | type="primary" |
| | | size="large" |
| | | loading={editLoading} |
| | | onClick={handleSaveEdit} |
| | | style={{ |
| | | padding: '12px 36px', |
| | | height: 'auto', |
| | | fontWeight: 600, |
| | | fontSize: 16, |
| | | background: '#1A6FB8', |
| | | borderColor: '#1A6FB8', |
| | | }} |
| | | > |
| | | 保存修改 |
| | | </Button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </Modal> |
| | | </div> |
| | | ); |
| | | }; |
| | | |
| | | export default TabContainer; |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |