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;
|