feat: 更新音频和引导对话,优化用户体验
- 在多个场景中添加音频资源,增强学习互动性。 - 更新引导对话文本,确保信息准确性和一致性。 - 调整组件结构,清理不必要的代码,提升可维护性。 - 引入结束窗口,鼓励用户深入了解历史背景。
This commit is contained in:
1 parent
d7ff7d3a45
commit
6e106032fd
141 files changed
+1375
-479
No files matched your search
@@ -1,9 +1,24 @@
|
||||
import { ReactNode, useState, Children, isValidElement } from "react";
|
||||
import {
|
||||
ReactNode,
|
||||
useState,
|
||||
Children,
|
||||
isValidElement,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { cn, getBackgroundImage } from "@/lib/utils";
|
||||
import { Overlay } from "@/components/overlay";
|
||||
import { useVideoContextSafe } from "@/contexts/VideoContext";
|
||||
import { useAudioManager } from "@/hooks/useAudioManager";
|
||||
|
||||
/**
|
||||
* 使用示例:
|
||||
* <GuideSteps audioChannelId="guide-chenyun" visible>
|
||||
* <GuideStepsItem audioSrc="/audios/step1.mp3" autoPlayOnEnter>
|
||||
* 第一步内容...
|
||||
* </GuideStepsItem>
|
||||
* </GuideSteps>
|
||||
*/
|
||||
interface GuideStepsProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
@@ -17,11 +32,17 @@ interface GuideStepsProps {
|
||||
showMask?: boolean;
|
||||
/** 自定义 z-index,用于控制层级(默认:50) */
|
||||
zIndex?: number;
|
||||
/** 音频分组 id,相同 channel 内互斥,不同 channel 可同时播放;不传则全局互斥 */
|
||||
audioChannelId?: string;
|
||||
}
|
||||
|
||||
interface GuideStepsItemProps {
|
||||
children: ReactNode;
|
||||
speaker?: "guide" | "student";
|
||||
/** 当前步骤对应的音频地址 */
|
||||
audioSrc?: string;
|
||||
/** 进入该步骤时是否自动播放音频(有 audioSrc 且未显式设置时,默认自动播放) */
|
||||
autoPlayOnEnter?: boolean;
|
||||
}
|
||||
|
||||
const GuideSteps = ({
|
||||
@@ -34,6 +55,7 @@ const GuideSteps = ({
|
||||
highlightSelectors,
|
||||
showMask = true,
|
||||
zIndex = 50,
|
||||
audioChannelId,
|
||||
}: GuideStepsProps) => {
|
||||
const [internalStep, setInternalStep] = useState(0);
|
||||
const [internalVisible, setInternalVisible] = useState(true);
|
||||
@@ -41,21 +63,37 @@ const GuideSteps = ({
|
||||
// 安全地获取视频上下文(如果不在 VideoProvider 中,返回 null)
|
||||
const videoContext = useVideoContextSafe();
|
||||
|
||||
// 音频管理:默认全局互斥,如果传了 audioChannelId 则按 channel 互斥
|
||||
const { play, stop } = useAudioManager(
|
||||
audioChannelId
|
||||
? { mode: "channel", channelId: audioChannelId }
|
||||
: { mode: "global" }
|
||||
);
|
||||
|
||||
// 收集每个步骤的内容和 speaker 信息
|
||||
const stepsData = Children.toArray(children).map((child) => {
|
||||
if (
|
||||
isValidElement<GuideStepsItemProps>(child) &&
|
||||
child.type === GuideStepsItem
|
||||
) {
|
||||
const hasAudio = !!child.props.audioSrc;
|
||||
return {
|
||||
content: child.props.children,
|
||||
speaker: child.props.speaker || "guide", // 默认为 "guide" 保持向后兼容
|
||||
audioSrc: child.props.audioSrc,
|
||||
// 如果没有显式设置 autoPlayOnEnter,则在有 audioSrc 时默认自动播放
|
||||
autoPlayOnEnter:
|
||||
child.props.autoPlayOnEnter !== undefined
|
||||
? child.props.autoPlayOnEnter
|
||||
: hasAudio,
|
||||
};
|
||||
}
|
||||
// 如果不是 GuideStepsItem,保持原有行为
|
||||
return {
|
||||
content: child,
|
||||
speaker: "guide" as const,
|
||||
audioSrc: undefined,
|
||||
autoPlayOnEnter: false,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -76,6 +114,17 @@ const GuideSteps = ({
|
||||
|
||||
// 获取当前步骤的 speaker
|
||||
const currentSpeaker = stepsData[currentStep]?.speaker || "guide";
|
||||
const currentStepData = stepsData[currentStep];
|
||||
const currentAudioSrc = currentStepData?.audioSrc;
|
||||
const currentAutoPlayOnEnter = currentStepData?.autoPlayOnEnter ?? false;
|
||||
|
||||
// 步骤变化时,按需自动播放音频(依赖稳定值,避免循环触发)
|
||||
useEffect(() => {
|
||||
if (!isVisible) return;
|
||||
if (currentAutoPlayOnEnter && currentAudioSrc) {
|
||||
play(currentAudioSrc);
|
||||
}
|
||||
}, [isVisible, currentStep, currentAutoPlayOnEnter, currentAudioSrc, play]);
|
||||
|
||||
const updateStep = (newStep: number) => {
|
||||
if (!isControlled) {
|
||||
@@ -88,9 +137,20 @@ const GuideSteps = ({
|
||||
if (visible === undefined) {
|
||||
setInternalVisible(newVisible);
|
||||
}
|
||||
// 隐藏引导时,停止当前 channel 的音频
|
||||
if (!newVisible) {
|
||||
stop();
|
||||
}
|
||||
onVisibleChange?.(newVisible);
|
||||
};
|
||||
|
||||
// 受控 visible 场景:当外部把 visible 置为 false 时,也需要停止音频
|
||||
useEffect(() => {
|
||||
if (!isVisible) {
|
||||
stop();
|
||||
}
|
||||
}, [isVisible, stop]);
|
||||
|
||||
const handleNext = (e: React.MouseEvent) => {
|
||||
e.stopPropagation(); // 阻止事件冒泡
|
||||
if (currentStep < totalSteps - 1) {
|
||||
|
||||
Reference in new issue
Block a user