diff --git a/Cunkebao/dist/.vite/manifest.json b/Cunkebao/dist/.vite/manifest.json index 86ef2f1e..5e8f12f1 100644 --- a/Cunkebao/dist/.vite/manifest.json +++ b/Cunkebao/dist/.vite/manifest.json @@ -1,18 +1,14 @@ { - "_charts-CM0JFsjx.js": { - "file": "assets/charts-CM0JFsjx.js", + "_charts-CtV6DO5_.js": { + "file": "assets/charts-CtV6DO5_.js", "name": "charts", "imports": [ - "_ui-Dkyp_L4f.js", + "_ui-BRTknrR5.js", "_vendor-BPPoWDlG.js" ] }, - "_ui-D0C0OGrH.css": { - "file": "assets/ui-D0C0OGrH.css", - "src": "_ui-D0C0OGrH.css" - }, - "_ui-Dkyp_L4f.js": { - "file": "assets/ui-Dkyp_L4f.js", + "_ui-BRTknrR5.js": { + "file": "assets/ui-BRTknrR5.js", "name": "ui", "imports": [ "_vendor-BPPoWDlG.js" @@ -21,6 +17,10 @@ "assets/ui-D0C0OGrH.css" ] }, + "_ui-D0C0OGrH.css": { + "file": "assets/ui-D0C0OGrH.css", + "src": "_ui-D0C0OGrH.css" + }, "_utils-DiZV3oaL.js": { "file": "assets/utils-DiZV3oaL.js", "name": "utils", @@ -33,18 +33,18 @@ "name": "vendor" }, "index.html": { - "file": "assets/index-DYycL-yo.js", + "file": "assets/index-DGdErvda.js", "name": "index", "src": "index.html", "isEntry": true, "imports": [ "_vendor-BPPoWDlG.js", "_utils-DiZV3oaL.js", - "_ui-Dkyp_L4f.js", - "_charts-CM0JFsjx.js" + "_ui-BRTknrR5.js", + "_charts-CtV6DO5_.js" ], "css": [ - "assets/index-CHPV8625.css" + "assets/index-DoT8YtM8.css" ] } } \ No newline at end of file diff --git a/Cunkebao/dist/index.html b/Cunkebao/dist/index.html index bedc8ebb..7cac6838 100644 --- a/Cunkebao/dist/index.html +++ b/Cunkebao/dist/index.html @@ -11,13 +11,13 @@ - + - - + + - +
diff --git a/Cunkebao/src/components/Upload/AudioRecorder/index.tsx b/Cunkebao/src/components/Upload/AudioRecorder/index.tsx new file mode 100644 index 00000000..06025b17 --- /dev/null +++ b/Cunkebao/src/components/Upload/AudioRecorder/index.tsx @@ -0,0 +1,391 @@ +import React, { useState, useRef, useCallback } from "react"; +import { Button, message, Modal } from "antd"; +import { + AudioOutlined, + PlayCircleOutlined, + PauseCircleOutlined, + SendOutlined, + DeleteOutlined, +} from "@ant-design/icons"; +import { uploadFile } from "@/api/common"; + +interface AudioRecorderProps { + onAudioUploaded: (filePath: string) => void; + className?: string; + disabled?: boolean; + maxDuration?: number; // 最大录音时长(秒) +} + +type RecordingState = + | "idle" + | "recording" + | "recorded" + | "playing" + | "uploading"; + +const AudioRecorder: React.FC = ({ + onAudioUploaded, + className, + disabled = false, + maxDuration = 60, +}) => { + const [visible, setVisible] = useState(false); + const [state, setState] = useState("idle"); + const [recordingTime, setRecordingTime] = useState(0); + const [audioBlob, setAudioBlob] = useState(null); + const [audioUrl, setAudioUrl] = useState(""); + + const mediaRecorderRef = useRef(null); + const audioRef = useRef(null); + const timerRef = useRef(null); + const chunksRef = useRef([]); + + // 打开弹窗 + const openRecorder = () => { + setVisible(true); + }; + + // 关闭弹窗并重置状态 + const closeRecorder = () => { + if (state === "recording") { + stopRecording(); + } + if (state === "playing") { + pauseAudio(); + } + deleteRecording(); + setVisible(false); + }; + + // 开始录音 + const startRecording = useCallback(async () => { + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + + // 尝试使用MP3格式,如果不支持则回退到WebM + let mimeType = "audio/mp3"; + if (!MediaRecorder.isTypeSupported(mimeType)) { + mimeType = "audio/mpeg"; + if (!MediaRecorder.isTypeSupported(mimeType)) { + mimeType = "audio/webm"; + } + } + + const mediaRecorder = new MediaRecorder(stream, { mimeType }); + mediaRecorderRef.current = mediaRecorder; + chunksRef.current = []; + + mediaRecorder.ondataavailable = event => { + if (event.data.size > 0) { + chunksRef.current.push(event.data); + } + }; + + mediaRecorder.onstop = () => { + const blob = new Blob(chunksRef.current, { type: mimeType }); + setAudioBlob(blob); + const url = URL.createObjectURL(blob); + setAudioUrl(url); + setState("recorded"); + + // 停止所有音频轨道 + stream.getTracks().forEach(track => track.stop()); + }; + + mediaRecorder.start(); + setState("recording"); + setRecordingTime(0); + + // 开始计时 + timerRef.current = setInterval(() => { + setRecordingTime(prev => { + const newTime = prev + 1; + if (newTime >= maxDuration) { + stopRecording(); + } + return newTime; + }); + }, 1000); + } catch (error) { + console.error("录音失败:", error); + message.error("无法访问麦克风,请检查权限设置"); + } + }, [maxDuration]); + + // 停止录音 + const stopRecording = useCallback(() => { + if ( + mediaRecorderRef.current && + mediaRecorderRef.current.state === "recording" + ) { + mediaRecorderRef.current.stop(); + } + if (timerRef.current) { + clearInterval(timerRef.current); + timerRef.current = null; + } + }, []); + + // 播放录音 + const playAudio = useCallback(() => { + if (audioRef.current && audioUrl) { + audioRef.current.play(); + setState("playing"); + } + }, [audioUrl]); + + // 暂停播放 + const pauseAudio = useCallback(() => { + if (audioRef.current) { + audioRef.current.pause(); + setState("recorded"); + } + }, []); + + // 删除录音 + const deleteRecording = useCallback(() => { + if (audioUrl) { + URL.revokeObjectURL(audioUrl); + } + setAudioBlob(null); + setAudioUrl(""); + setRecordingTime(0); + setState("idle"); + }, [audioUrl]); + + // 发送录音 + const sendAudio = useCallback(async () => { + if (!audioBlob) return; + + try { + setState("uploading"); + + // 创建文件对象 + const timestamp = Date.now(); + const fileExtension = + audioBlob.type.includes("mp3") || audioBlob.type.includes("mpeg") + ? "mp3" + : "webm"; + const audioFile = new File( + [audioBlob], + `audio_${timestamp}.${fileExtension}`, + { + type: audioBlob.type, + }, + ); + + // 打印文件格式信息 + console.log("音频文件信息:", { + fileName: audioFile.name, + fileType: audioFile.type, + fileSize: audioFile.size, + fileExtension: fileExtension, + blobType: audioBlob.type, + }); + + // 上传文件 + const filePath = await uploadFile(audioFile); + + // 调用回调函数 + onAudioUploaded(filePath); + + // 重置状态并关闭弹窗 + deleteRecording(); + setVisible(false); + message.success("语音发送成功"); + } catch (error) { + console.error("语音上传失败:", error); + message.error("语音发送失败,请重试"); + setState("recorded"); + } + }, [audioBlob, onAudioUploaded, deleteRecording]); + + // 格式化时间显示 + const formatTime = (seconds: number) => { + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`; + }; + + // 渲染弹窗内容 + const renderModalContent = () => { + switch (state) { + case "idle": + return ( +
+
+ 点击下方按钮开始录音 +
+
+ ); + + case "recording": + return ( +
+
+
+ {formatTime(recordingTime)} +
+
+ 正在录音中... +
+
+ +
+ ); + + case "recorded": + case "playing": + return ( +
+
+
+ 录音时长: {formatTime(recordingTime)} +
+
+ {state === "playing" + ? "正在播放..." + : "录音完成,可以试听或发送"} +
+
+ +
+
+ +
+ +
+
+ ); + + case "uploading": + return ( +
+ +
+ ); + + default: + return null; + } + }; + + return ( + <> +