import { useState, useEffect } from 'react' import { cn } from '@/lib/utils' import { useAudioManager } from '@/hooks/useAudioManager' export interface NarrationProps { /** 旁白文本 */ text: string /** 是否可见 */ visible: boolean /** 关闭回调 */ onClose: () => void /** 语音播报的音频文件路径(可选) */ audioSrc?: string /** 自定义样式类名 */ className?: string /** 是否启用逐字动画,默认 true */ enableAnimation?: boolean /** 文字动画速度(毫秒/字),默认 100ms */ animationSpeed?: number /** 覆盖层出现时是否自动播放音频(默认 true) */ autoPlayAudio?: boolean /** 音频分组 id,相同 channel 内互斥,不同 channel 可同时播放;不传则全局互斥 */ audioChannelId?: string } /** * Narration - 旁白组件 * * 功能: * - 文字逐字显示动画 * - 可选语音播报 * - 关闭按钮 */ const Narration = ({ text, visible, onClose, audioSrc, className, enableAnimation = true, animationSpeed = 100, autoPlayAudio = true, audioChannelId, }: NarrationProps) => { const [displayedText, setDisplayedText] = useState('') const [isAnimating, setIsAnimating] = useState(true) const [isSkipped, setIsSkipped] = useState(false) // 旁白音频:默认自动播放,支持按 channel 分组互斥 const { currentSrc, play, stop } = useAudioManager( audioChannelId ? { mode: 'channel', channelId: audioChannelId, } : { mode: 'global' } ) // 文字动画效果 useEffect(() => { if (!visible) { setDisplayedText('') setIsAnimating(true) setIsSkipped(false) // 旁白关闭时,如果当前正在播放该旁白音频,则停止 if (audioSrc && currentSrc === audioSrc) { stop() } 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, audioSrc, currentSrc, stop]) // 跳过动画 const handleSkip = () => { setDisplayedText(text) setIsAnimating(false) setIsSkipped(true) } // 旁白出现时自动播放音频 useEffect(() => { if (!visible) return if (!autoPlayAudio) return if (!audioSrc) return play(audioSrc) }, [visible, autoPlayAudio, audioSrc, play]) // 停止音频 const handleClose = () => { if (audioSrc && currentSrc === audioSrc) { stop() } onClose() } if (!visible) return null return (
{/* 跳过按钮 - 只在动画进行中显示 */} {isAnimating && enableAnimation && ( )}

{displayedText}

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