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/call-record/CallRecordModal.jsx |  243 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 243 insertions(+), 0 deletions(-)

diff --git a/web-app/src/components/call-record/CallRecordModal.jsx b/web-app/src/components/call-record/CallRecordModal.jsx
new file mode 100644
index 0000000..aaad755
--- /dev/null
+++ b/web-app/src/components/call-record/CallRecordModal.jsx
@@ -0,0 +1,243 @@
+import React, { useState, useEffect } from 'react';
+import { Modal, Spin, Button, message } from 'antd';
+import { ReloadOutlined } from '@ant-design/icons';
+import OutboundBotAPIService from '../../services/OutboundBotAPIService';
+import AudioPlayer from './AudioPlayer';
+import ConversationList from './ConversationList';
+import './CallRecordModal.css';
+
+// 已接通状态值(数字格式)
+const CONNECTED_STATUSES_NUM = [1, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31];
+// 已接通状态值(字符串格式,如API返回"Succeeded")
+const CONNECTED_STATUSES_STR = ['Succeeded', 'succeeded', 'Success', 'success'];
+
+/**
+ * 判断是否为已接通状态
+ * 兼容数字和字符串两种状态格式
+ */
+const isConnectedStatus = (callStatus) => {
+  if (callStatus === null || callStatus === undefined) return false;
+  // 如果是数字
+  if (typeof callStatus === 'number') {
+    return CONNECTED_STATUSES_NUM.includes(callStatus);
+  }
+  // 如果是字符串(可能是"Succeeded"等)
+  if (typeof callStatus === 'string') {
+    return CONNECTED_STATUSES_STR.includes(callStatus);
+  }
+  return false;
+};
+
+/**
+ * 获取caseId
+ */
+const getCaseId = () => {
+  // 优先从URL参数获取
+  const urlParams = new URLSearchParams(window.location.search);
+  const caseIdFromUrl = urlParams.get('caseId');
+  if (caseIdFromUrl) return caseIdFromUrl;
+  
+  // 从localStorage获取
+  try {
+    const timelineData = localStorage.getItem('case_data_timeline');
+    if (timelineData) {
+      const parsed = JSON.parse(timelineData);
+      return parsed.case_id || null;
+    }
+  } catch (e) {
+    console.error('解析localStorage数据失败:', e);
+  }
+  return null;
+};
+
+/**
+ * 解析conversations JSON字符串
+ */
+const parseConversations = (conversationsStr) => {
+  if (!conversationsStr) return [];
+  try {
+    return JSON.parse(conversationsStr);
+  } catch (e) {
+    console.error('解析对话记录失败:', e);
+    return [];
+  }
+};
+
+/**
+ * 通话记录弹窗组件
+ */
+const CallRecordModal = ({ visible, onClose, record }) => {
+  const [loading, setLoading] = useState(false);
+  const [error, setError] = useState(null);
+  const [callData, setCallData] = useState(null);
+  const [audioBlob, setAudioBlob] = useState(null);
+  const [audioLoading, setAudioLoading] = useState(false);
+
+  // 获取通话记录数据
+  const fetchCallRecord = async () => {
+    if (!record) return;
+    
+    setLoading(true);
+    setError(null);
+    setAudioBlob(null);
+    
+    try {
+      const caseId = getCaseId();
+      if (!caseId) {
+        throw new Error('未找到案件ID');
+      }
+      
+      const params = {
+        caseId,
+        personId: record.person_id,
+        jobId: record.job_id
+      };
+      
+      const response = await OutboundBotAPIService.getConversationLog(params);
+      
+      if (response && response.data && response.data.length > 0) {
+        // 取最后一条记录
+        const lastRecord = response.data[response.data.length - 1];
+        console.log('📞 通话记录数据:', lastRecord);
+        console.log('📞 callStatus值:', lastRecord.callStatus, '类型:', typeof lastRecord.callStatus);
+        setCallData(lastRecord);
+        
+        // 如果有recordUrl,加载音频文件
+        const recordUrl = lastRecord.record_url || lastRecord.recordUrl;
+        if (recordUrl) {
+          loadAudioFile(recordUrl);
+        }
+      } else {
+        throw new Error('未找到通话记录');
+      }
+    } catch (err) {
+      console.error('获取通话记录失败:', err);
+      setError(err.message || '获取通话记录失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 加载音频文件
+  const loadAudioFile = async (recordUrl) => {
+    setAudioLoading(true);
+    try {
+      const blob = await OutboundBotAPIService.getAudioFile(recordUrl);
+      setAudioBlob(blob);
+    } catch (err) {
+      console.error('加载音频文件失败:', err);
+      message.error('加载录音文件失败');
+    } finally {
+      setAudioLoading(false);
+    }
+  };
+
+  // 弹窗打开时加载数据
+  useEffect(() => {
+    if (visible && record) {
+      fetchCallRecord();
+    }
+  }, [visible, record]);
+
+  // 关闭弹窗时重置状态
+  useEffect(() => {
+    if (!visible) {
+      setCallData(null);
+      setError(null);
+      setAudioBlob(null);
+    }
+  }, [visible]);
+
+  // 生成弹窗标题
+  const getModalTitle = () => {
+    if (!record) return '通话记录';
+    const creatorName = record.creator || '当事人';
+    return `AI调解员与${creatorName}的通话`;
+  };
+
+  // 渲染加载状态
+  const renderLoading = () => (
+    <div className="modal-loading">
+      <Spin tip="加载中..." />
+    </div>
+  );
+
+  // 渲染错误状态
+  const renderError = () => (
+    <div className="modal-error">
+      <p className="error-text">{error}</p>
+      <Button 
+        type="primary" 
+        icon={<ReloadOutlined />}
+        onClick={fetchCallRecord}
+      >
+        重试
+      </Button>
+    </div>
+  );
+
+  // 渲染未接通状态
+  const renderNotConnected = () => (
+    <div className="modal-not-connected">
+      <p>未接通,无通话记录</p>
+    </div>
+  );
+
+  // 渲染通话记录内容
+  const renderContent = () => {
+    if (!callData) {
+      console.log('📞 renderContent: callData为空');
+      return renderNotConnected();
+    }
+    
+    const callStatus = callData.call_status ?? callData.callStatus;
+    console.log('📞 renderContent: callStatus=', callStatus, 'isConnected=', isConnectedStatus(callStatus));
+    
+    // 未接通状态
+    if (!isConnectedStatus(callStatus)) {
+      return renderNotConnected();
+    }
+    
+    // 已接通状态
+    const conversations = parseConversations(callData.conversations);
+    const recordUrl = callData.record_url || callData.recordUrl;
+    const creatorName = record?.creator || '当事人';
+    
+    return (
+      <div className="modal-content">
+        {/* 录音播放器 - 根据recordUrl和audioLoading状态显示 */}
+        <AudioPlayer 
+          recordUrl={recordUrl}
+          audioBlob={audioBlob}
+          loading={audioLoading}
+          loadingText="正在加载录音..."
+        />
+        
+        {/* 对话记录列表 */}
+        <ConversationList 
+          conversations={conversations}
+          contactName={creatorName}
+        />
+      </div>
+    );
+  };
+
+  return (
+    <Modal
+      className="call-record-modal"
+      title={getModalTitle()}
+      open={visible}
+      onCancel={onClose}
+      footer={null}
+      width={600}
+      centered
+      destroyOnClose
+    >
+      {loading && renderLoading()}
+      {!loading && error && renderError()}
+      {!loading && !error && renderContent()}
+    </Modal>
+  );
+};
+
+export default CallRecordModal;

--
Gitblit v1.8.0