feat: 更新音频和引导对话,优化用户体验
- 在多个场景中添加音频资源,增强学习互动性。 - 更新引导对话文本,确保信息准确性和一致性。 - 调整组件结构,清理不必要的代码,提升可维护性。 - 引入结束窗口,鼓励用户深入了解历史背景。
This commit is contained in:
1 parent
d7ff7d3a45
commit
6e106032fd
141 files changed
+1375
-479
No files matched your search
@@ -0,0 +1,119 @@
|
||||
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, 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]);
|
||||
|
||||
// 全部文字出现后 1 秒自动关闭
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
if (isAnimating) return;
|
||||
if (!text || displayedText !== text) return;
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
onContinue();
|
||||
}, 800);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [visible, isAnimating, displayedText, text, 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-8xl 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>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
export { default as Overlay } from "./Overlay";
|
||||
export type { OverlayProps } from "./Overlay";
|
||||
|
||||
export { EndingOverlay } from "./EndingOverlay";
|
||||
export type { EndingOverlayProps } from "./EndingOverlay";
|
||||
Reference in new issue
Block a user