"use client"; import React, { useState, useRef, useEffect } from "react"; import { Mic, MicOff, Upload, Play, Pause, Trash2, X } from "lucide-react"; import { Tooltip, Upload as AntdUpload } from "antd"; import { InboxOutlined } from "@ant-design/icons"; import WaveSurfer from "wavesurfer.js"; import { useUploadFile } from "../../app/service/domain/service"; // 自定义样式 const audioRecorderStyles = ` .slider { -webkit-appearance: none; appearance: none; background: transparent; cursor: pointer; } .slider::-webkit-slider-track { background: rgba(255, 255, 255, 0.2); height: 4px; border-radius: 2px; } .slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; background: #3b82f6; height: 12px; width: 12px; border-radius: 50%; cursor: pointer; transition: all 0.2s ease; } .slider::-webkit-slider-thumb:hover { background: #2563eb; transform: scale(1.1); } .slider::-moz-range-track { background: rgba(255, 255, 255, 0.2); height: 4px; border-radius: 2px; border: none; } .slider::-moz-range-thumb { background: #3b82f6; height: 12px; width: 12px; border-radius: 50%; cursor: pointer; border: none; transition: all 0.2s ease; } .slider::-moz-range-thumb:hover { background: #2563eb; transform: scale(1.1); } `; interface AudioRecorderProps { /** 当前音频URL */ audioUrl?: string; /** 录制完成回调 */ onAudioRecorded: (audioBlob: Blob, audioUrl: string) => void; /** 删除音频回调 */ onAudioDeleted: () => void; /** 组件标题 */ title?: string; /** 是否显示关闭按钮 */ showCloseButton?: boolean; /** 关闭回调 */ onClose?: () => void; } /** * 音频录制组件,支持录制、上传和播放功能 */ export function AudioRecorder({ audioUrl, onAudioRecorded, onAudioDeleted, title = "请上传参考音频", showCloseButton = false, onClose, }: AudioRecorderProps) { const [mode, setMode] = useState<"upload" | "record">("upload"); // 当前模式:上传或录制 const [isRecording, setIsRecording] = useState(false); const [isPlaying, setIsPlaying] = useState(false); const [volume, setVolume] = useState(1); const [isMuted, setIsMuted] = useState(false); const { uploadFile, isUploading } = useUploadFile(); const mediaRecorderRef = useRef(null); const chunksRef = useRef([]); // 开始录制 const startRecording = async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const mediaRecorder = new MediaRecorder(stream); mediaRecorderRef.current = mediaRecorder; chunksRef.current = []; mediaRecorder.ondataavailable = (event) => { if (event.data.size > 0) { chunksRef.current.push(event.data); } }; mediaRecorder.onstop = () => { const audioBlob = new Blob(chunksRef.current, { type: "audio/wav" }); const audioUrl = URL.createObjectURL(audioBlob); onAudioRecorded(audioBlob, audioUrl); stream.getTracks().forEach((track) => track.stop()); }; mediaRecorder.start(); setIsRecording(true); } catch (error) { console.error("录制失败:", error); } }; // 停止录制 const stopRecording = () => { if (mediaRecorderRef.current && isRecording) { mediaRecorderRef.current.stop(); setIsRecording(false); } }; // 播放/暂停控制 const togglePlay = () => { setIsPlaying(!isPlaying); }; // 音量控制 const toggleMute = () => { setIsMuted(!isMuted); }; // 音量调节 const handleVolumeChange = (e: React.ChangeEvent) => { const newVolume = parseFloat(e.target.value); setVolume(newVolume); if (isMuted && newVolume > 0) { setIsMuted(false); } }; // 删除音频 const handleDelete = () => { setIsPlaying(false); onAudioDeleted(); }; // 渲染上传/录制状态 if (!audioUrl) { return ( <>
{/* 头部 - 只显示关闭按钮 */} {showCloseButton && (
)} {/* 主要内容区域 */}
{mode === "upload" ? ( // 上传模式
false} customRequest={async ({ file, onSuccess, onError }) => { try { const fileObj = file as File; console.log("开始上传文件:", fileObj.name, fileObj.type, fileObj.size); if (fileObj && fileObj.type.startsWith("audio/")) { // 使用 hook 上传文件到七牛云 console.log("调用 uploadFile hook..."); const uploadedUrl = await uploadFile(fileObj); console.log("上传成功,URL:", uploadedUrl); // 上传成功后,调用回调函数 onAudioRecorded(fileObj, uploadedUrl); onSuccess?.(uploadedUrl); } else { console.log("文件类型不是音频:", fileObj?.type); const error = new Error("文件类型不是音频文件"); onError?.(error); } } catch (error) { console.error("上传失败:", error); // 上传失败时直接报告错误,不使用本地文件作为备选 onError?.(error as Error); } }} showUploadList={false} className="bg-transparent border-dashed border-white/20 hover:border-white/40" disabled={isUploading} >
{isUploading ? "Uploading..." : "Drag audio file here or click to upload"}
) : ( // 录制模式
{isRecording ? ( // 录制中状态
Recording...
{/* 录制状态指示器 */}
Recording...
) : ( // 录制准备状态
🎙️
Click to start recording
)}
)}
{/* 底部模式切换图标 */}
); } // 渲染播放状态 return ( <>
{/* 头部 - 只显示操作按钮 */}
{showCloseButton && ( )}
{/* WaveSurfer 波形图区域 */}
{/* 播放控制 */}
{/* 音频设置 */}
{Math.round(volume * 100)}%
1x
); } interface WaveformPlayerProps { audioUrl: string; isPlaying: boolean; onPlayStateChange: (isPlaying: boolean) => void; volume?: number; isMuted?: boolean; } function WaveformPlayer({ audioUrl, isPlaying, onPlayStateChange, volume = 1, isMuted = false, }: WaveformPlayerProps) { const containerRef = useRef(null); const wavesurferRef = useRef(null); useEffect(() => { if (!containerRef.current || !audioUrl) return; const ws = WaveSurfer.create({ container: containerRef.current, waveColor: "#3b82f6", progressColor: "#1d4ed8", cursorColor: "#1e40af", height: 64, barWidth: 2, barGap: 1, normalize: true, url: audioUrl, }); // 监听播放状态变化 ws.on("play", () => onPlayStateChange(true)); ws.on("pause", () => onPlayStateChange(false)); ws.on("finish", () => onPlayStateChange(false)); wavesurferRef.current = ws; return () => { ws.destroy(); }; }, [audioUrl, onPlayStateChange]); // 同步外部播放状态和音量 useEffect(() => { if (!wavesurferRef.current) return; if (isPlaying && !wavesurferRef.current.isPlaying()) { wavesurferRef.current.play(); } else if (!isPlaying && wavesurferRef.current.isPlaying()) { wavesurferRef.current.pause(); } // 设置音量 const currentVolume = isMuted ? 0 : volume; wavesurferRef.current.setVolume(currentVolume); }, [isPlaying, volume, isMuted]); return (
); }