- 删除 .claude/settings.local.json 配置文件,清理不必要的代码。 - 更新多个组件以引入音频资源,增强用户互动体验。 - 调整引导对话文本,确保信息准确性和一致性。 - 更新图像资源,提升视觉内容的准确性和一致性。
144 lines
4.0 KiB
TypeScript
144 lines
4.0 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { getStaticImage } from "@/lib/utils";
|
|
import { useAudioManager } from "@/hooks/useAudioManager";
|
|
|
|
export interface EndingOverlayProps {
|
|
/** 是否可见 */
|
|
visible: boolean;
|
|
/** 中间显示的文本内容 */
|
|
text: string;
|
|
/** 点击继续回调 */
|
|
onContinue: () => void;
|
|
/** 文字动画速度(毫秒/字),默认 60ms */
|
|
animationSpeed?: number;
|
|
/** 结语对应的音频地址(可选) */
|
|
audioSrc?: string;
|
|
/** 覆盖层出现时是否自动播放音频(默认 true) */
|
|
autoPlayAudio?: boolean;
|
|
/** 音频分组 id,相同 channel 内互斥,不同 channel 可同时播放;不传则全局互斥 */
|
|
audioChannelId?: string;
|
|
}
|
|
|
|
/**
|
|
* 反思模块统一的结语覆盖层
|
|
* 使用 components/overlay/bg 作为背景
|
|
*/
|
|
export const EndingOverlay = ({
|
|
visible,
|
|
text,
|
|
onContinue,
|
|
animationSpeed = 60,
|
|
audioSrc,
|
|
autoPlayAudio = true,
|
|
audioChannelId,
|
|
}: EndingOverlayProps) => {
|
|
const [displayedText, setDisplayedText] = useState("");
|
|
const [isAnimating, setIsAnimating] = useState(false);
|
|
|
|
// 结语音频:默认自动播放,支持按 channel 分组互斥
|
|
const { currentSrc, isPlaying, play, stop } = useAudioManager(
|
|
audioChannelId
|
|
? {
|
|
mode: "channel",
|
|
channelId: audioChannelId,
|
|
}
|
|
: { mode: "global" }
|
|
);
|
|
|
|
// 文字逐字动画
|
|
useEffect(() => {
|
|
if (!visible) {
|
|
setDisplayedText("");
|
|
setIsAnimating(false);
|
|
// 结语关闭时,如果当前正在播放该结语音频,则停止
|
|
if (audioSrc && currentSrc === audioSrc) {
|
|
stop();
|
|
}
|
|
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);
|
|
}, [visible, text, animationSpeed, audioSrc, currentSrc, stop]);
|
|
|
|
// 覆盖层显示时自动播放音频
|
|
useEffect(() => {
|
|
if (!visible) return;
|
|
if (!autoPlayAudio) return;
|
|
if (!audioSrc) return;
|
|
|
|
play(audioSrc);
|
|
}, [visible, autoPlayAudio, audioSrc, play]);
|
|
|
|
// 全部文字出现后自动关闭(如果有音频,需等待音频播放完成)
|
|
useEffect(() => {
|
|
if (!visible) return;
|
|
if (isAnimating) return;
|
|
if (!text || displayedText !== text) return;
|
|
|
|
// 如果有音频,需要等待音频播放完成
|
|
if (audioSrc) {
|
|
// 检查当前播放的音频是否是我们期望的音频,且已播放完成
|
|
if (currentSrc === audioSrc && !isPlaying) {
|
|
// 音频播放完成后,等待 800ms 再关闭
|
|
const timer = setTimeout(() => {
|
|
onContinue();
|
|
}, 800);
|
|
return () => clearTimeout(timer);
|
|
}
|
|
// 音频还在播放,等待播放完成
|
|
return;
|
|
}
|
|
|
|
// 没有音频,文字动画完成后等待 800ms 关闭
|
|
const timer = setTimeout(() => {
|
|
onContinue();
|
|
}, 800);
|
|
|
|
return () => clearTimeout(timer);
|
|
}, [
|
|
visible,
|
|
isAnimating,
|
|
displayedText,
|
|
text,
|
|
audioSrc,
|
|
currentSrc,
|
|
isPlaying,
|
|
onContinue,
|
|
]);
|
|
|
|
if (!visible) return null;
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-[10000] flex items-center justify-center bg-transparent">
|
|
{/* 文本内容层 */}
|
|
<div
|
|
className="relative z-10 flex items-center max-w-full transition-all py-50 px-24 h-[825px] bg-cover bg-center bg-no-repeat"
|
|
style={{
|
|
backgroundImage: `url(${getStaticImage("components/overlay/bg")})`,
|
|
}}
|
|
>
|
|
<div className="text-4xl leading-relaxed text-[#7b3f00] font-semibold tracking-wide">
|
|
{displayedText}
|
|
</div>
|
|
|
|
{/* 底部渐隐提示条,仅做视觉平衡 */}
|
|
{/* <div className="mt-16 h-4 w-full bg-gradient-to-r from-transparent via-[rgba(0,0,0,0.35)] to-transparent rounded-full" /> */}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|