From 6b14010e1765842acc437ab130e5815bc623788d Mon Sep 17 00:00:00 2001
From: chengmw <chengmingwei_1984122@126.com>
Date: Thu, 02 Apr 2026 14:59:59 +0800
Subject: [PATCH] feat: 优化警告提示和调解流程 UI,新增 AI 背景和图片资源
---
web-app/src/components/dashboard/TabContainer.jsx | 362 ++++++++++++++++++++------------------------------
1 files changed, 146 insertions(+), 216 deletions(-)
diff --git a/web-app/src/components/dashboard/TabContainer.jsx b/web-app/src/components/dashboard/TabContainer.jsx
index 40b8521..e390898 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, 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;
+
+
+
+
+
+
+
--
Gitblit v1.8.0