From 6bb08c2297be1b6415c8bc02e6917eba6ee355e5 Mon Sep 17 00:00:00 2001
From: shimai <shimai@example.com>
Date: Fri, 03 Apr 2026 10:42:08 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/test/tony.cheng/260312' into test/shimai.huang/260309

---
 web-app/src/components/dashboard/FloatingControlPanel.jsx |  422 +++++++++++++++++++++++++++++++++++++++++++++++++---
 1 files changed, 398 insertions(+), 24 deletions(-)

diff --git a/web-app/src/components/dashboard/FloatingControlPanel.jsx b/web-app/src/components/dashboard/FloatingControlPanel.jsx
index 8d016d9..c3f5001 100644
--- a/web-app/src/components/dashboard/FloatingControlPanel.jsx
+++ b/web-app/src/components/dashboard/FloatingControlPanel.jsx
@@ -1,39 +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 = ({ currentStep = 1, elapsedTime = '25分钟', onManualTakeover }) => {
-  const stepNames = ['阶段1:意愿调查', '阶段2:材料核实', '阶段3:事实认定', '阶段4:达成协议', '阶段5:履约回访'];
+const FloatingControlPanel = () => {
+  const { caseData, taskStartTime, isTaskTimeFallback, refreshData } = useCaseData();
+  const [takeoverLoading, setTakeoverLoading] = useState(false);
+  const [confirmVisible, setConfirmVisible] = useState(false);
   
-  const handleTakeover = () => {
-    if (onManualTakeover) {
-      onManualTakeover();
-    } else {
-      if (window.confirm('确认要人工接管调解吗?接管后将结束AI调解,由工作人员继续处理。')) {
-        alert('AI调解已结束,已转为人工接管模式。工作人员将继续处理本次调解。');
+  // 状态控制相关状态
+  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();
+
+  // ==================== 状态控制按钮逻辑 ====================
+  
+  /**
+   * 判断是否显示状态控制按钮
+   * 状态为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">
-            调解进行中-{stepNames[currentStep]}{' '}
-            <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