- Introduced `puzzleResidences.ts` to define puzzle residence configurations including properties like display name, position, and puzzle availability. - Added `reflectionProgress.ts` to manage badge unlocking logic based on place completion, including functions to determine unlocked badges and video sources for scenes.
168 lines
4.4 KiB
TypeScript
168 lines
4.4 KiB
TypeScript
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 (
|
|
<div className="fixed inset-0 flex items-center justify-center bg-black/80 z-50">
|
|
<div
|
|
className={cn(
|
|
'w-[1600px] bg-black/90 rounded-[32px] px-20 py-16',
|
|
'shadow-[0_20px_60px_rgba(240,190,117,0.8)] border-4 border-[#d4c5a9] indent-14',
|
|
'relative',
|
|
className
|
|
)}
|
|
>
|
|
{/* 跳过按钮 - 只在动画进行中显示 */}
|
|
{isAnimating && enableAnimation && (
|
|
<button
|
|
onClick={handleSkip}
|
|
className="absolute top-8 right-8 px-6 py-2 rounded-full bg-black/60 text-white text-xl backdrop-blur-sm border border-white/30 hover:bg-black/75 transition-colors"
|
|
>
|
|
跳过
|
|
</button>
|
|
)}
|
|
|
|
<div className="text-white text-3xl leading-relaxed space-y-8">
|
|
<p className="whitespace-pre-wrap">{displayedText}</p>
|
|
</div>
|
|
|
|
{/* 关闭按钮 - 只在动画完成后显示 */}
|
|
{!isAnimating && (
|
|
<div className="flex justify-center mt-14">
|
|
<button
|
|
onClick={handleClose}
|
|
className="px-12 py-2 bg-gradient-to-br from-[#d4c5a9] to-[#b8a88a] text-[#4e402e] text-3xl font-bold rounded-full border-3 border-[#8B7355] shadow-lg hover:scale-105 transition-transform"
|
|
>
|
|
继续
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default Narration
|