import { useState, useEffect } from "react"; import { cn } from "@/lib/utils"; export interface NarrationProps { /** 旁白文本 */ text: string; /** 是否可见 */ visible: boolean; /** 关闭回调 */ onClose: () => void; /** 语音播报的音频文件路径(可选) */ audioSrc?: string; /** 自定义样式类名 */ className?: string; /** 是否启用逐字动画,默认 true */ enableAnimation?: boolean; /** 文字动画速度(毫秒/字),默认 100ms */ animationSpeed?: number; } /** * Narration - 旁白组件 * * 功能: * - 文字逐字显示动画 * - 可选语音播报 * - 关闭按钮 */ const Narration = ({ text, visible, onClose, audioSrc, className, enableAnimation = true, animationSpeed = 100, }: NarrationProps) => { const [displayedText, setDisplayedText] = useState(""); const [isAnimating, setIsAnimating] = useState(true); const [audio, setAudio] = useState(null); const [isSkipped, setIsSkipped] = useState(false); // 文字动画效果 useEffect(() => { if (!visible) { setDisplayedText(""); setIsAnimating(true); setIsSkipped(false); return; } // 如果已经跳过,不再执行动画 if (isSkipped) { return; } // 如果不启用动画,直接显示全部文本 if (!enableAnimation) { setDisplayedText(text); setIsAnimating(false); return; } let currentIndex = 0; setDisplayedText(""); setIsAnimating(true); const timer = setInterval(() => { if (currentIndex < text.length) { setDisplayedText(text.slice(0, currentIndex + 1)); currentIndex++; } else { setIsAnimating(false); clearInterval(timer); } }, animationSpeed); return () => clearInterval(timer); }, [text, visible, enableAnimation, animationSpeed, isSkipped]); // 跳过动画 const handleSkip = () => { setDisplayedText(text); setIsAnimating(false); setIsSkipped(true); }; // 语音播报 useEffect(() => { if (!visible || !audioSrc) return; const audioElement = new Audio(audioSrc); audioElement.play().catch((error) => { console.warn("Audio playback failed:", error); }); setAudio(audioElement); return () => { audioElement.pause(); audioElement.currentTime = 0; }; }, [visible, audioSrc]); // 停止音频 const handleClose = () => { if (audio) { audio.pause(); audio.currentTime = 0; } onClose(); }; if (!visible) return null; return (
{/* 跳过按钮 - 只在动画进行中显示 */} {isAnimating && enableAnimation && ( )}

{displayedText}

{/* 关闭按钮 - 只在动画完成后显示 */} {!isAnimating && (
)}
); }; export default Narration;