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/AudioPlayer.jsx |  228 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 228 insertions(+), 0 deletions(-)

diff --git a/web-app/src/components/call-record/AudioPlayer.jsx b/web-app/src/components/call-record/AudioPlayer.jsx
new file mode 100644
index 0000000..f70271d
--- /dev/null
+++ b/web-app/src/components/call-record/AudioPlayer.jsx
@@ -0,0 +1,228 @@
+import React, { useState, useRef, useEffect } from 'react';
+import { Button } from 'antd';
+import { PlayCircleOutlined, PauseCircleOutlined, ReloadOutlined, DownloadOutlined } from '@ant-design/icons';
+import './AudioPlayer.css';
+
+/**
+ * 录音播放器组件
+ * @param {string} recordUrl - 录音文件相对路径(可选)
+ * @param {Blob|null} audioBlob - 音频Blob对象(可选)
+ * @param {Function} onLoadAudio - 加载音频的函数,返回Promise<Blob>
+ * @param {boolean} loading - 是否正在加载音频
+ * @param {string} loadingText - 加载中的提示文字
+ */
+const AudioPlayer = ({ 
+  recordUrl, 
+  audioBlob, 
+  onLoadAudio, 
+  loading = false, 
+  loadingText = '加载中...'
+}) => {
+  const [isPlaying, setIsPlaying] = useState(false);
+  const [currentTime, setCurrentTime] = useState(0);
+  const [duration, setDuration] = useState(0);
+  const [loadError, setLoadError] = useState(false);
+  const [audioSrc, setAudioSrc] = useState(null);
+  const audioRef = useRef(null);
+
+  // 格式化时间显示
+  const formatTime = (seconds) => {
+    if (!seconds || isNaN(seconds)) return '00:00';
+    const mins = Math.floor(seconds / 60);
+    const secs = Math.floor(seconds % 60);
+    return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
+  };
+
+  // 处理播放/暂停
+  const handlePlayPause = () => {
+    if (!audioRef.current) return;
+    
+    if (isPlaying) {
+      audioRef.current.pause();
+    } else {
+      audioRef.current.play().catch(() => {
+        setLoadError(true);
+      });
+    }
+    setIsPlaying(!isPlaying);
+  };
+
+  // 处理时间更新
+  const handleTimeUpdate = () => {
+    if (audioRef.current) {
+      setCurrentTime(audioRef.current.currentTime);
+    }
+  };
+
+  // 处理元数据加载
+  const handleLoadedMetadata = () => {
+    if (audioRef.current) {
+      setDuration(audioRef.current.duration);
+      setLoadError(false);
+    }
+  };
+
+  // 处理加载错误
+  const handleError = () => {
+    setLoadError(true);
+    setIsPlaying(false);
+  };
+
+  // 处理播放结束
+  const handleEnded = () => {
+    setIsPlaying(false);
+    setCurrentTime(0);
+  };
+
+  // 重试加载
+  const handleRetry = () => {
+    setLoadError(false);
+    if (onLoadAudio) {
+      onLoadAudio();
+    } else if (audioRef.current && audioSrc) {
+      audioRef.current.load();
+    }
+  };
+
+  // 处理进度条点击
+  const handleProgressClick = (e) => {
+    if (!audioRef.current || !duration) return;
+    
+    const progressBar = e.currentTarget;
+    const rect = progressBar.getBoundingClientRect();
+    const clickX = e.clientX - rect.left;
+    const newTime = (clickX / rect.width) * duration;
+    
+    audioRef.current.currentTime = newTime;
+    setCurrentTime(newTime);
+  };
+
+  // 处理下载音频
+  const handleDownload = () => {
+    if (!audioBlob) return;
+    
+    // 从recordUrl中提取文件名
+    let fileName = '录音文件.wav';
+    if (recordUrl) {
+      const parts = recordUrl.split(/[/\\]/);
+      if (parts.length > 0) {
+        fileName = parts[parts.length - 1];
+        if (!fileName.endsWith('.wav')) {
+          fileName += '.wav';
+        }
+      }
+    }
+    
+    // 创建下载链接
+    const blobUrl = URL.createObjectURL(audioBlob);
+    const link = document.createElement('a');
+    link.href = blobUrl;
+    link.download = fileName;
+    link.style.display = 'none';
+    document.body.appendChild(link);
+    link.click();
+    document.body.removeChild(link);
+    URL.revokeObjectURL(blobUrl);
+  };
+
+  // 计算进度百分比
+  const progressPercent = duration > 0 ? (currentTime / duration) * 100 : 0;
+
+  // 当audioBlob变化时更新音频源
+  useEffect(() => {
+    if (audioBlob) {
+      const url = URL.createObjectURL(audioBlob);
+      setAudioSrc(url);
+      setLoadError(false);
+      return () => URL.revokeObjectURL(url);
+    }
+  }, [audioBlob]);
+
+  // 加载中状态
+  if (loading) {
+    return (
+      <div className="audio-player audio-player-loading">
+        <div className="loading-content">
+          <div className="loading-spinner"></div>
+          <span className="loading-text">{loadingText}</span>
+        </div>
+      </div>
+    );
+  }
+
+  // 无录音文件状态
+  if (!recordUrl && !audioSrc) {
+    return (
+      <div className="audio-player audio-player-empty">
+        <div className="empty-content">
+          <span className="empty-icon">🎙️</span>
+          <span className="empty-text">没有通话录音文件,无法播放</span>
+        </div>
+      </div>
+    );
+  }
+
+  // 加载失败状态
+  if (loadError) {
+    return (
+      <div className="audio-player audio-player-error">
+        <span className="error-text">录音文件加载失败</span>
+        <Button 
+          type="link" 
+          icon={<ReloadOutlined />} 
+          onClick={handleRetry}
+        >
+          重试
+        </Button>
+      </div>
+    );
+  }
+
+  return (
+    <div className="audio-player">
+      {audioSrc && (
+        <audio
+          ref={audioRef}
+          src={audioSrc}
+          onTimeUpdate={handleTimeUpdate}
+          onLoadedMetadata={handleLoadedMetadata}
+          onError={handleError}
+          onEnded={handleEnded}
+          preload="metadata"
+        />
+      )}
+      
+      <Button
+        type="text"
+        className="play-btn"
+        icon={isPlaying ? <PauseCircleOutlined /> : <PlayCircleOutlined />}
+        onClick={handlePlayPause}
+      />
+      
+      <div 
+        className="progress-bar"
+        onClick={handleProgressClick}
+      >
+        <div 
+          className="progress-fill"
+          style={{ width: `${progressPercent}%` }}
+        />
+      </div>
+      
+      <span className="time-display">
+        {formatTime(currentTime)} / {formatTime(duration)}
+      </span>
+      
+      <Button
+        type="text"
+        className="download-btn"
+        icon={<DownloadOutlined />}
+        onClick={handleDownload}
+        disabled={!audioBlob}
+        title="下载录音"
+      />
+    </div>
+  );
+};
+
+export default AudioPlayer;

--
Gitblit v1.8.0