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