From 0c8432f4ff5c95faca4ca62d6a4ec4618feba627 Mon Sep 17 00:00:00 2001
From: tony.cheng <chengmingwei_1984122@126.com>
Date: Tue, 17 Mar 2026 15:31:29 +0800
Subject: [PATCH] feat: 完善AI调解状态控制功能及相关文档更新
---
web-app/src/components/dashboard/FloatingControlPanel.jsx | 430 +++++++++++++++++++++++++++++++++++++++++++++++++----
1 files changed, 392 insertions(+), 38 deletions(-)
diff --git a/web-app/src/components/dashboard/FloatingControlPanel.jsx b/web-app/src/components/dashboard/FloatingControlPanel.jsx
index e0bbf8b..c3f5001 100644
--- a/web-app/src/components/dashboard/FloatingControlPanel.jsx
+++ b/web-app/src/components/dashboard/FloatingControlPanel.jsx
@@ -1,59 +1,413 @@
-import React from 'react';
+import React, { useState } from 'react';
+import { Modal, message, Input } from 'antd';
import { useCaseData } from '../../contexts/CaseDataContext';
import { translateMediationState } from '../../utils/stateTranslator';
+import { useTaskTimer } from '../../hooks/useTaskTimer';
+import ProcessAPIService from '../../services/ProcessAPIService';
+import { getMergedParams } from '../../utils/urlParams';
+
+const { TextArea } = Input;
+
+// 终态状态(不显示人工接管按钮)
+const TERMINAL_STATES = [2, 3]; // 调解成功、调解失败
+const TAKEOVER_STATE = 4; // 人工接管
+const PAUSED_STATE = 5; // 已终止/暂停状态
+
+/**
+ * 获取案件ID
+ * 优先级:Context > URL参数 > localStorage
+ */
+const resolveCaseId = (caseData) => {
+ // 1. 从Context获取
+ if (caseData?.case_id) return String(caseData.case_id);
+
+ // 2. 从 URL 参数获取
+ const params = getMergedParams();
+ if (params.caseId) return String(params.caseId);
+
+ // 3. 从localStorage获取
+ try {
+ const stored = JSON.parse(localStorage.getItem('case_data_timeline') || '{}');
+ if (stored.case_id) return String(stored.case_id);
+ } catch { /* ignore */ }
+
+ return null;
+};
+
+/**
+ * 获取当前用户名
+ */
+const resolveUserName = () => {
+ try {
+ const user = JSON.parse(localStorage.getItem('currentUser') || '{}');
+ return user.user_name || 'No User';
+ } catch {
+ return 'No User';
+ }
+};
+
+/**
+ * 印章组件 - 终态状态展示(调解成功/失败/人工接管)
+ */
+const TakeoverStamp = ({ state }) => {
+ const stateTextMap = {
+ 2: '调解成功',
+ 3: '调解失败',
+ 4: '已人工接管'
+ };
+ const text = stateTextMap[state] || '已人工接管';
+
+ return (
+ <div className="takeover-stamp">
+ <span className="takeover-stamp-text">
+ <i className="fas fa-stamp"></i>
+ {text}
+ </span>
+ </div>
+ );
+};
+
+/**
+ * 接管按钮组件
+ */
+const TakeoverButton = ({ loading, onClick }) => (
+ <button
+ className="floating-control-btn"
+ onClick={onClick}
+ disabled={loading}
+ style={loading ? { opacity: 0.6, cursor: 'not-allowed' } : {}}
+ >
+ {loading ? (
+ <><i className="fas fa-spinner fa-spin"></i>接管中...</>
+ ) : (
+ <><i className="fas fa-user-tie"></i>人工接管</>
+ )}
+ </button>
+);
/**
* 底部悬浮控制面板
*/
-const FloatingControlPanel = ({ onManualTakeover }) => {
- const { caseData } = useCaseData();
- const timeline = caseData || {};
+const FloatingControlPanel = () => {
+ const { caseData, taskStartTime, isTaskTimeFallback, refreshData } = useCaseData();
+ const [takeoverLoading, setTakeoverLoading] = useState(false);
+ const [confirmVisible, setConfirmVisible] = useState(false);
+ // 状态控制相关状态
+ const [controlLoading, setControlLoading] = useState(false);
+ const [controlConfirmVisible, setControlConfirmVisible] = useState(false);
+ const [controlAction, setControlAction] = useState(null); // 'terminate' or 'resume'
+ const [remark, setRemark] = useState('');
+
+ const timeline = caseData || {};
const state = timeline.mediation?.state;
const nodeName = timeline.current_node?.node_name || '';
const orderNo = timeline.current_node?.order_no || 1;
+
+ const { formattedTime } = useTaskTimer(taskStartTime, isTaskTimeFallback);
+
+ // 生成状态文本
+ const getStatusText = () => {
+ const stateNum = Number(state);
+ if (stateNum === 1) {
+ return `调解进行中-阶段${orderNo}:${nodeName}`;
+ }
+ return translateMediationState(state) || '调解进行中';
+ };
+
+ const statusText = getStatusText();
+
+ // ==================== 状态控制按钮逻辑 ====================
- // 根据状态生成状态文本
- let statusText = '';
- if (state === 1) {
- // 调解中状态
- statusText = `调解进行中-阶段${orderNo}:${nodeName}`;
- } else {
- // 其他状态
- statusText = translateMediationState(state) || '调解进行中';
- }
-
- // 获取已进行时间(使用before_duration)
- const elapsedTime = timeline.before_duration || '25分钟';
-
- const handleTakeover = () => {
- if (onManualTakeover) {
- onManualTakeover();
- } else {
- if (window.confirm('确认要人工接管调解吗?接管后将结束AI调解,由工作人员继续处理。')) {
- alert('AI调解已结束,已转为人工接管模式。工作人员将继续处理本次调解。');
+ /**
+ * 判断是否显示状态控制按钮
+ * 状态为0(未开始)、1(进行中)、5(已终止)时显示
+ */
+ const shouldShowControlButton = () => {
+ const stateNum = Number(state);
+ // 进行中(1)显示终止按钮,已终止(5)显示恢复按钮
+ return stateNum === 1 || stateNum === 5;
+ };
+
+ /**
+ * 获取状态控制按钮属性
+ */
+ const getControlButtonProps = () => {
+ const stateNum = Number(state);
+
+ if (stateNum === 1) {
+ return {
+ text: '终止',
+ style: 'terminate',
+ action: 'terminate'
+ };
+ } else if (stateNum === 5) {
+ return {
+ text: '恢复',
+ style: 'resume',
+ action: 'resume'
+ };
+ }
+
+ return null;
+ };
+
+ /**
+ * 处理状态控制按钮点击
+ */
+ const handleControlButtonClick = () => {
+ const buttonProps = getControlButtonProps();
+ if (!buttonProps) return;
+
+ setControlAction(buttonProps.action);
+ setControlConfirmVisible(true);
+ };
+
+ /**
+ * 处理状态控制确认
+ */
+ const handleControlConfirmOk = async () => {
+ if (!controlAction) return;
+
+ setControlLoading(true);
+ try {
+ const params = getMergedParams();
+ const actionCode = controlAction === 'terminate' ? 0 : 1;
+
+ if (!params.caseId) {
+ throw new Error('案件ID不能为空');
}
+
+ await ProcessAPIService.updateMediationState(params.caseId, {
+ action: actionCode,
+ userName: localStorage.getItem('userName') || '调解员',
+ remark: remark || ''
+ });
+
+ message.success(controlAction === 'terminate' ? '调解已终止' : '调解已恢复');
+ setControlConfirmVisible(false);
+ setRemark('');
+ setControlAction(null);
+
+ // 如果是终止操作,触发事件关闭外呼气泡
+ if (controlAction === 'terminate') {
+ window.dispatchEvent(new CustomEvent('mediation-terminated'));
+ console.log('调解终止,触发外呼气泡关闭事件');
+ }
+
+ refreshData();
+ } catch (error) {
+ console.error('状态更新失败:', error);
+ message.error(error.message || '状态更新失败,请稍后重试');
+ } finally {
+ setControlLoading(false);
}
};
+ /**
+ * 处理状态控制取消
+ */
+ const handleControlConfirmCancel = () => {
+ setControlConfirmVisible(false);
+ setRemark('');
+ setControlAction(null);
+ };
+
+ // ==================== 人工接管逻辑 ====================
+
+ /**
+ * 处理接管API调用
+ */
+ const executeTakeover = async () => {
+ console.log('executeTakeover 开始执行');
+ const caseId = resolveCaseId(caseData);
+ console.log('获取到的 caseId:', caseId);
+
+ if (!caseId) {
+ message.error('无法获取案件ID,请刷新页面后重试');
+ return;
+ }
+
+ const userName = resolveUserName();
+ console.log('获取到的 userName:', userName);
+
+ setTakeoverLoading(true);
+ try {
+ console.log('调用 ProcessAPIService.takeover, caseId:', caseId, ', userName:', userName);
+ const response = await ProcessAPIService.takeover(caseId, { userName });
+ console.log('接管API返回:', response);
+ message.success('人工接管成功');
+ refreshData();
+ } catch (err) {
+ console.error('接管API异常:', err);
+ handleTakeoverError(err);
+ } finally {
+ setTakeoverLoading(false);
+ }
+ };
+
+ /**
+ * 处理接管错误响应
+ */
+ const handleTakeoverError = (err) => {
+ const status = err?.response?.status || err?.status;
+ const msg = err?.response?.data?.message || err?.message || '';
+
+ // 400错误:已被接管或已结束 → 重新加载页面
+ if (status === 400) {
+ console.warn('接管失败(400):', msg);
+ refreshData();
+ return;
+ }
+
+ // 其他错误 → 提示用户
+ console.error('人工接管失败:', err);
+ message.error(msg || '人工接管失败,请稍后重试');
+ };
+
+ /**
+ * 点击接管按钮 - 显示确认对话框
+ */
+ const handleTakeover = () => {
+ console.log('点击人工接管按钮');
+ setConfirmVisible(true);
+ };
+
+ /**
+ * 确认接管
+ */
+ const handleConfirmOk = async () => {
+ console.log('用户点击确定,开始执行接管');
+ setConfirmVisible(false);
+ await executeTakeover();
+ };
+
+ /**
+ * 取消接管
+ */
+ const handleConfirmCancel = () => {
+ console.log('用户点击取消');
+ setConfirmVisible(false);
+ };
+
+ /**
+ * 渲染状态控制按钮(终止/恢复)
+ */
+ const renderStateControlButton = () => {
+ if (!shouldShowControlButton()) return null;
+
+ const buttonProps = getControlButtonProps();
+ if (!buttonProps) return null;
+
+ const isTerminate = buttonProps.style === 'terminate';
+
+ return (
+ <button
+ className={`state-control-btn ${isTerminate ? 'state-control-btn--terminate' : 'state-control-btn--resume'}`}
+ onClick={handleControlButtonClick}
+ disabled={controlLoading}
+ >
+ {controlLoading ? (
+ <><i className="fas fa-spinner fa-spin"></i>处理中...</>
+ ) : (
+ <><i className={isTerminate ? "fas fa-pause-circle" : "fas fa-play-circle"}></i>{buttonProps.text}</>
+ )}
+ </button>
+ );
+ };
+
+ /**
+ * 渲染控制区域(按钮或印章)
+ */
+ const renderControlAction = () => {
+ const stateNum = Number(state);
+
+ // 终态状态(调解成功/失败/人工接管):显示印章
+ if (TERMINAL_STATES.includes(stateNum) || stateNum === TAKEOVER_STATE) {
+ return <TakeoverStamp state={stateNum} />;
+ }
+
+ // 已终止/暂停状态(5):显示恢复按钮和人工接管按钮
+ if (stateNum === PAUSED_STATE) {
+ return (
+ <>
+ {renderStateControlButton()}
+ <TakeoverButton loading={takeoverLoading} onClick={handleTakeover} />
+ </>
+ );
+ }
+
+ // 调解中(1):显示终止按钮和人工接管按钮
+ return (
+ <>
+ {renderStateControlButton()}
+ <TakeoverButton loading={takeoverLoading} onClick={handleTakeover} />
+ </>
+ );
+ };
+
return (
- <div className="floating-control-panel">
- <div className="control-status">
- <div className="status-indicator">
- <span className="status-dot"></span>
- <span className="status-text">
- {statusText}{' '}
- <span style={{ color: 'gray' }}>(已进行:{elapsedTime})</span>
- </span>
+ <>
+ <div className="floating-control-panel">
+ <div className="control-status">
+ <div className="status-indicator">
+ <span className="status-dot" style={Number(state) === 5 ? { background: '#e63946' } : {}}></span>
+ <span className="status-text">
+ {statusText}{' '}
+ <span style={{ color: 'gray' }}>
+ (已进行:{formattedTime}
+ {isTaskTimeFallback && <span style={{ color: '#faad14' }}> *</span>}
+ )
+ </span>
+ </span>
+ </div>
+ </div>
+ <div className="control-action">
+ {renderControlAction()}
</div>
</div>
- <div className="control-action">
- <button className="floating-control-btn" onClick={handleTakeover}>
- <i className="fas fa-user-tie"></i>
- 人工接管
- </button>
- </div>
- </div>
+
+ {/* 状态控制确认对话框(终止/恢复) */}
+ <Modal
+ title={controlAction === 'terminate' ? '确认终止调解' : '确认恢复调解'}
+ visible={controlConfirmVisible}
+ onOk={handleControlConfirmOk}
+ onCancel={handleControlConfirmCancel}
+ okText="确定"
+ cancelText="取消"
+ confirmLoading={controlLoading}
+ >
+ <p>
+ {controlAction === 'terminate'
+ ? '确定要终止当前AI调解流程吗?终止后调解将暂停,可在适当时机恢复。'
+ : '确定要恢复AI调解流程吗?恢复后将从当前位置继续调解。'}
+ </p>
+ <div style={{ marginTop: 15 }}>
+ <label style={{ display: 'block', marginBottom: 5, fontWeight: 500 }}>
+ 备注(可选):
+ </label>
+ <TextArea
+ value={remark}
+ onChange={(e) => setRemark(e.target.value)}
+ placeholder="请输入操作备注..."
+ rows={3}
+ />
+ </div>
+ </Modal>
+
+ {/* 人工接管确认对话框 */}
+ <Modal
+ title="人工接管确认"
+ visible={confirmVisible}
+ onOk={handleConfirmOk}
+ onCancel={handleConfirmCancel}
+ okText="确定"
+ cancelText="取消"
+ confirmLoading={takeoverLoading}
+ >
+ <p>确定人工接管本调解案件吗?</p>
+ </Modal>
+ </>
);
};
--
Gitblit v1.8.0