import { useState, useCallback, useMemo, useEffect, useRef } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { createPortal } from "react-dom"; import { AnimatePresence, motion } from "framer-motion"; import { Title, Image, Cursor, DragFill } from "@/components"; import type { DragFillResult } from "@/components"; import { cn, getBackgroundImage, getStaticImage } from "@/lib/utils"; import { useScale } from "@/contexts/ScaleContext"; const starName = "luxun"; // 文学作品拖拽选项 const literatureOptions = [ { value: "朝花夕拾", label: "朝花夕拾", image: "luxun/reflection/scene2/拖拽-朝花夕拾" }, { value: "阿Q正传", label: "阿Q正传", image: "luxun/reflection/scene2/拖拽-阿Q正传" }, { value: "故事新编", label: "故事新编", image: "luxun/reflection/scene2/拖拽-故事新编" }, { value: "百草园到三味书屋", label: "百草园到三味书屋", image: "luxun/reflection/scene2/拖拽-百草园到三味书屋" }, { value: "精神胜利法", label: "精神胜利法", image: "luxun/reflection/scene2/拖拽-精神胜利法" }, { value: "润土", label: "润土", image: "luxun/reflection/scene2/拖拽-润土" }, ]; // 目标位置 const targetBlanks = [ { id: "blank1", label: "位置1" }, { id: "blank2", label: "位置2" }, { id: "blank3", label: "位置3" }, ]; const LuxunReflectionScene2 = () => { const navigate = useNavigate(); const { moduleName } = useParams<{ moduleName: string; }>(); const [showDialog, setShowDialog] = useState(false); const [visited, setVisited] = useState>(new Set()); const [dragValue, setDragValue] = useState([]); const completedRef = useRef(false); const { scale, offsetX, offsetY, DESIGN_WIDTH, DESIGN_HEIGHT } = useScale(); const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []); // 重置游戏状态 useEffect(() => { if (showDialog) { setDragValue([]); completedRef.current = false; } }, [showDialog]); // 检查是否完成 useEffect(() => { if (dragValue.length === 3 && !completedRef.current) { completedRef.current = true; setTimeout(() => { setVisited((prev) => { const next = new Set(prev); next.add(1); return next; }); setShowDialog(false); }, 500); } }, [dragValue]); const progressCount = visited.size; const total = 1; const completed = progressCount >= total; const subtitle = completed ? "探索文学作品·精神传承(已完成)" : `探索文学作品·精神传承(${progressCount}/${total})`; const subtitleClassName = completed ? "text-[#fff998] [text-shadow:_0_8px_8px_rgba(0,0,0,0.35),_0_0_16px_rgba(0,0,0,0.25)]" : undefined; const handleValueChange = useCallback((newValue: DragFillResult[]) => { if (newValue.length <= 3) { setDragValue(newValue); } }, []); const maskSrc = getStaticImage("components/蒙版遮罩"); const backSrc = getStaticImage("components/关闭"); const overlayStyle = useMemo( () => ({ position: "fixed", inset: 0, zIndex: 9999, pointerEvents: "none", }), [] ); const scaledContainerStyle = useMemo( () => ({ position: "absolute", width: `${DESIGN_WIDTH}px`, height: `${DESIGN_HEIGHT}px`, left: `${offsetX}px`, top: `${offsetY}px`, transform: `scale(${scale})`, transformOrigin: "top left", overflow: "hidden", }), [scale, offsetX, offsetY, DESIGN_WIDTH, DESIGN_HEIGHT] ); return (
<Image className="absolute right-5 top-5" src={getStaticImage(`${starName}/reflection/scene2/离开`)} button onClick={() => { if (moduleName && starName) { navigate(`/${moduleName}/${starName}/home`); } }} /> {/* 点击区域 - 打开拖拽弹窗 */} <Cursor className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" onClick={() => setShowDialog(true)} read={visited.has(1)} /> {completed && ( <Image className="absolute bottom-0 right-0" src={getStaticImage("components/place/继续前进")} button onClick={() => { if (moduleName && starName) { navigate(`/${moduleName}/${starName}/reflection3`); } }} /> )} {/* 拖拽弹窗 */} {mounted && createPortal( <AnimatePresence> {showDialog && ( <motion.div key="literature-drag-dialog" style={overlayStyle} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.3, ease: "easeInOut" }} > <div style={scaledContainerStyle}> <div className={cn("absolute inset-0 flex flex-col", "bg-black/70")} style={{ pointerEvents: "auto" }} > {/* 背景蒙版 */} {maskSrc && ( <Image src={maskSrc} className="absolute inset-0 w-full h-full object-cover select-none pointer-events-none" aria-hidden /> )} {/* 返回按钮 */} <Image src={backSrc} onClick={() => setShowDialog(false)} alt="返回" className="absolute top-5 right-5 z-20" button /> {/* 内容区域 */} <div className="relative z-10 w-full h-full flex items-center justify-center px-30 py-16 pt-30"> <div className="w-full max-w-6xl"> <div className="text-5xl font-bold text-white mb-8 text-center"> 将文学作品拖拽到对应位置 </div> <DragFill options={literatureOptions.map((opt) => ({ value: opt.value, label: opt.label, }))} blanks={targetBlanks} value={dragValue} onChange={handleValueChange} allowDuplicate={false} renderOption={(option) => { const opt = literatureOptions.find( (o) => o.value === option.value ); return ( <div className="flex flex-col items-center gap-2"> <Image src={getStaticImage(opt?.image || "")} className="w-32 h-32 object-contain" /> <span className="text-2xl text-white">{option.label}</span> </div> ); }} renderBlank={(blank, filledValue, isHovered) => { const filledOption = filledValue ? literatureOptions.find((opt) => opt.value === filledValue) : null; return ( <div className={cn( "w-48 h-48 border-2 rounded-lg flex flex-col items-center justify-center gap-2 transition-all", filledValue ? "border-white bg-white/20" : "border-dashed border-white/50 bg-white/10", isHovered && "border-white bg-white/30" )} > {filledOption ? ( <> <Image src={getStaticImage(filledOption.image)} className="w-24 h-24 object-contain" /> <span className="text-white text-lg">{filledOption.label}</span> </> ) : ( <span className="text-white text-xl">{blank.label}</span> )} </div> ); }} /> </div> </div> </div> </div> </motion.div> )} </AnimatePresence>, document.body )} </div> ); }; export default LuxunReflectionScene2;