feat:init
This commit is contained in:
1 parent
53d0eeac6b
commit
ae0c050010
494 files changed
+17701
-2318
No files matched your search
@@ -0,0 +1,215 @@
|
||||
import { ReactNode, useState, Children, isValidElement } from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { cn, getBackgroundImage } from "@/lib/utils";
|
||||
import { Overlay } from "@/components/overlay";
|
||||
import { useVideoContextSafe } from "@/contexts/VideoContext";
|
||||
|
||||
interface GuideStepsProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
visible?: boolean;
|
||||
onVisibleChange?: (visible: boolean) => void;
|
||||
currentStep?: number;
|
||||
onStepChange?: (step: number) => void;
|
||||
/** 每个步骤对应的高亮选择器数组,索引对应步骤序号 */
|
||||
highlightSelectors?: (string | string[])[];
|
||||
/** 控制是否显示遮罩层(包括背景蒙版和 Overlay 遮罩,默认:true,保持向后兼容) */
|
||||
showMask?: boolean;
|
||||
/** 自定义 z-index,用于控制层级(默认:50) */
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
interface GuideStepsItemProps {
|
||||
children: ReactNode;
|
||||
speaker?: "guide" | "student";
|
||||
}
|
||||
|
||||
const GuideSteps = ({
|
||||
children,
|
||||
className,
|
||||
visible,
|
||||
onVisibleChange,
|
||||
currentStep: controlledStep,
|
||||
onStepChange,
|
||||
highlightSelectors,
|
||||
showMask = true,
|
||||
zIndex = 50,
|
||||
}: GuideStepsProps) => {
|
||||
const [internalStep, setInternalStep] = useState(0);
|
||||
const [internalVisible, setInternalVisible] = useState(true);
|
||||
|
||||
// 安全地获取视频上下文(如果不在 VideoProvider 中,返回 null)
|
||||
const videoContext = useVideoContextSafe();
|
||||
|
||||
// 收集每个步骤的内容和 speaker 信息
|
||||
const stepsData = Children.toArray(children).map((child) => {
|
||||
if (
|
||||
isValidElement<GuideStepsItemProps>(child) &&
|
||||
child.type === GuideStepsItem
|
||||
) {
|
||||
return {
|
||||
content: child.props.children,
|
||||
speaker: child.props.speaker || "guide", // 默认为 "guide" 保持向后兼容
|
||||
};
|
||||
}
|
||||
// 如果不是 GuideStepsItem,保持原有行为
|
||||
return {
|
||||
content: child,
|
||||
speaker: "guide" as const,
|
||||
};
|
||||
});
|
||||
|
||||
const steps = stepsData.map((step) => step.content);
|
||||
const totalSteps = steps.length;
|
||||
|
||||
// 使用受控或非受控模式
|
||||
const isControlled = controlledStep !== undefined;
|
||||
const currentStep = isControlled ? controlledStep : internalStep;
|
||||
const baseVisible = visible !== undefined ? visible : internalVisible;
|
||||
|
||||
// 如果视频正在播放或内容不可见,则不显示 GuideSteps
|
||||
// 如果不在 VideoProvider 中(videoContext 为 null),则正常显示
|
||||
const isVisible =
|
||||
baseVisible &&
|
||||
(!videoContext ||
|
||||
(!videoContext.isVideoPlaying && videoContext.isContentVisible));
|
||||
|
||||
// 获取当前步骤的 speaker
|
||||
const currentSpeaker = stepsData[currentStep]?.speaker || "guide";
|
||||
|
||||
const updateStep = (newStep: number) => {
|
||||
if (!isControlled) {
|
||||
setInternalStep(newStep);
|
||||
}
|
||||
onStepChange?.(newStep);
|
||||
};
|
||||
|
||||
const updateVisible = (newVisible: boolean) => {
|
||||
if (visible === undefined) {
|
||||
setInternalVisible(newVisible);
|
||||
}
|
||||
onVisibleChange?.(newVisible);
|
||||
};
|
||||
|
||||
const handleNext = (e: React.MouseEvent) => {
|
||||
e.stopPropagation(); // 阻止事件冒泡
|
||||
if (currentStep < totalSteps - 1) {
|
||||
updateStep(currentStep + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTextClick = () => {
|
||||
if (currentStep < totalSteps - 1) {
|
||||
updateStep(currentStep + 1);
|
||||
} else {
|
||||
// 最后一步,隐藏组件
|
||||
updateVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取当前步骤的高亮选择器
|
||||
const currentHighlightSelectors =
|
||||
highlightSelectors && highlightSelectors[currentStep]
|
||||
? highlightSelectors[currentStep]
|
||||
: undefined;
|
||||
|
||||
// 根据 speaker 选择背景图片
|
||||
const currentBackgroundPath =
|
||||
currentSpeaker === "student" ? "components/引导板2" : "components/引导板";
|
||||
|
||||
// GuideSteps 内容
|
||||
const guideStepsContent = (
|
||||
<AnimatePresence>
|
||||
{isVisible && (
|
||||
<motion.div
|
||||
key="guide-steps-container"
|
||||
className={cn(
|
||||
"fixed inset-0 flex items-center justify-center",
|
||||
className
|
||||
)}
|
||||
style={{ zIndex: zIndex + 1 }}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.3, ease: "easeInOut" }}
|
||||
>
|
||||
{/* 引导板容器和背景 - 作为整体切换,确保 padding 和背景同步 */}
|
||||
{/* 只在 isVisible 为 true 时才渲染,避免阻塞 exit 动画 */}
|
||||
{isVisible && (
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={`container-${currentSpeaker}`}
|
||||
className={cn(
|
||||
"absolute bottom-0 w-[1764px] h-[656px] pt-75 pb-10",
|
||||
currentSpeaker === "student" ? "pl-50 pr-40" : "pr-80"
|
||||
)}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2, ease: "easeInOut" }}
|
||||
>
|
||||
{/* 背景层 */}
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(currentBackgroundPath),
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 引导文本区域 - 只在步骤变化时动画 */}
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={`text-${currentStep}`}
|
||||
className="relative h-full py-20 pl-25 cursor-pointer select-none z-10"
|
||||
onClick={handleTextClick}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.3, ease: "easeInOut" }}
|
||||
>
|
||||
<div className="text-[#4e402e] text-4xl font-bold leading-15">
|
||||
{steps[currentStep]}
|
||||
</div>
|
||||
|
||||
{/* 下一步提示 - 最后一页不显示 */}
|
||||
{currentStep < totalSteps - 1 && (
|
||||
<div className="absolute bottom-15 right-4 flex items-center gap-2">
|
||||
<span
|
||||
onClick={handleNext}
|
||||
className="text-[#8B7355] text-2xl font-medium cursor-pointer hover:text-[#6d5a43] transition-colors select-none"
|
||||
>
|
||||
继续
|
||||
</span>
|
||||
<span className="text-[#8B7355] text-2xl animate-pulse">
|
||||
→
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
|
||||
// 始终使用 Overlay 包裹,showMask 由 Overlay 处理
|
||||
return (
|
||||
<Overlay
|
||||
selectors={currentHighlightSelectors || []}
|
||||
visible={isVisible}
|
||||
zIndex={zIndex}
|
||||
showMask={showMask}
|
||||
>
|
||||
{guideStepsContent}
|
||||
</Overlay>
|
||||
);
|
||||
};
|
||||
|
||||
const GuideStepsItem = ({ children }: GuideStepsItemProps) => {
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export { GuideSteps, GuideStepsItem };
|
||||
@@ -0,0 +1 @@
|
||||
export { GuideSteps, GuideStepsItem } from "./GuideSteps";
|
||||
Reference in new issue
Block a user