From 66f460633a7e868c0a0d27923b40eecb145f4002 Mon Sep 17 00:00:00 2001 From: join_zhong Date: Tue, 30 Dec 2025 00:29:11 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E6=B7=BB=E5=8A=A0=E9=BB=84=E7=82=8E?= =?UTF-8?q?=E5=9F=B9=E6=88=90=E9=95=BF=E4=B9=8B=E8=B7=AF=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E5=86=85=E5=AE=B9=EF=BC=9B=E5=AD=A6=E4=B8=9A=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E6=A1=86=E5=92=8C=E6=80=9D=E6=83=B3=E9=80=89=E6=8B=A9=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/huangyanpei/reflection/scene2.tsx | 88 +++++--- .../reflection/scene2/ChoiceDialog1.tsx | 201 ++++++++++++++++++ .../reflection/scene2/ChoiceDialog2.tsx | 191 +++++++++++++++++ src/pages/huangyanpei/reflection/scene3.tsx | 2 +- src/pages/huangyanpei/reflection/scene4.tsx | 2 +- 5 files changed, 455 insertions(+), 29 deletions(-) create mode 100644 src/pages/huangyanpei/reflection/scene2/ChoiceDialog1.tsx create mode 100644 src/pages/huangyanpei/reflection/scene2/ChoiceDialog2.tsx diff --git a/src/pages/huangyanpei/reflection/scene2.tsx b/src/pages/huangyanpei/reflection/scene2.tsx index b39372b..e034c39 100644 --- a/src/pages/huangyanpei/reflection/scene2.tsx +++ b/src/pages/huangyanpei/reflection/scene2.tsx @@ -1,30 +1,32 @@ import { useState } from "react"; -import { Title, Image, Cursor, ExhibitOverlay } from "@/components"; +import { Title, Image, Cursor } from "@/components"; import { getBackgroundImage, getStaticImage } from "@/lib/utils"; import { useNavigate, useParams } from "react-router-dom"; +import ChoiceDialog1 from "./scene2/ChoiceDialog1"; +import ChoiceDialog2 from "./scene2/ChoiceDialog2"; const starName = "huangyanpei"; -const exhibitSrcMap: Record<1, string> = { - // 预留展品图片路径:请在 images 中放置对应资源 - 1: "huangyanpei/reflection/scene2/10_002", -}; - const HuangyanpeiReflectionScene2 = () => { const navigate = useNavigate(); const { moduleName } = useParams<{ moduleName: string; }>(); - const [activeExhibitId, setActiveExhibitId] = useState<1 | null>(null); + const [showDialog1, setShowDialog1] = useState(false); + const [showDialog2, setShowDialog2] = useState(false); const [visited, setVisited] = useState>(new Set()); + const [choice1, setChoice1] = useState< + "读书" | "没出息" | "女子不能上学" | null + >(null); + const [choice2, setChoice2] = useState<"铁工科" | "商科" | null>(null); if (!moduleName) { return null; } const progressCount = visited.size; - const total = 1; + const total = 2; const completed = progressCount >= total; const subtitle = completed ? "探索浦东中学·成长之阶(已完成)" @@ -58,36 +60,68 @@ const HuangyanpeiReflectionScene2 = () => { }} /> - {/* 互动展品光标:后续可替换为拖拽/选择类互动 */} + {choice2 && ( + 已解锁指示牌盖板 + )} + + {/* 互动展品光标1:显示第一个选择弹窗 */} setActiveExhibitId(1)} - read={visited.has(1)} + className="absolute left-118 bottom-105" + onClick={() => setShowDialog1(true)} + read={choice1 !== null} /> - {/* 展品遮罩:当前使用单张图片,占位为后续互动内容 */} - { - if (activeExhibitId !== null) { - setVisited((prev) => { - const next = new Set(prev); - next.add(activeExhibitId); - return next; - }); - } - setActiveExhibitId(null); + {/* 互动展品光标2:显示第二个选择弹窗 */} + setShowDialog2(true)} + read={choice2 !== null} + /> + + {/* 第一个选择弹窗 */} + setShowDialog1(false)} + onSelect={(choice) => { + setChoice1(choice); + setVisited((prev) => { + const next = new Set(prev); + next.add(1); + return next; + }); }} /> - {completed && ( + {/* 第二个选择弹窗 */} + setShowDialog2(false)} + onSelect={(choice) => { + setChoice2(choice); + setVisited((prev) => { + const next = new Set(prev); + next.add(2); + return next; + }); + }} + /> + + {completed && choice2 && ( { - navigate(`/${moduleName}/${starName}/reflection3`); + // 根据选择决定导航目标 + if (choice2 === "铁工科") { + navigate(`/${moduleName}/${starName}/reflection3`); + } else if (choice2 === "商科") { + navigate(`/${moduleName}/${starName}/reflection4`); + } }} /> )} diff --git a/src/pages/huangyanpei/reflection/scene2/ChoiceDialog1.tsx b/src/pages/huangyanpei/reflection/scene2/ChoiceDialog1.tsx new file mode 100644 index 0000000..585fc2b --- /dev/null +++ b/src/pages/huangyanpei/reflection/scene2/ChoiceDialog1.tsx @@ -0,0 +1,201 @@ +import { useState, useEffect, useMemo, useCallback } from "react"; +import { createPortal } from "react-dom"; +import { AnimatePresence, motion } from "framer-motion"; +import { cn, getStaticImage } from "@/lib/utils"; +import { Image } from "@/components"; +import { useScale } from "@/contexts/ScaleContext"; + +export interface ChoiceDialog1Props { + /** 是否可见 */ + visible?: boolean; + /** 关闭回调 */ + onClose?: () => void; + /** 选择回调 */ + onSelect?: (choice: "读书" | "没出息" | "女子不能上学") => void; +} + +/** + * 第一个选择弹窗:人物和三个选择 + * - 使用蒙版遮罩作为背景 + * - 显示人物10_004和三个选择选项 + */ +const ChoiceDialog1 = ({ + visible = true, + onClose, + onSelect, +}: ChoiceDialog1Props) => { + const maskSrc = getStaticImage("components/蒙版遮罩"); + const backSrc = getStaticImage("components/关闭"); + const characterSrc = getStaticImage("huangyanpei/reflection/scene2/10_004"); + const choice1Src = getStaticImage("huangyanpei/reflection/scene2/10_005"); + const choice2Src = getStaticImage("huangyanpei/reflection/scene2/10_006"); + const choice3Src = getStaticImage("huangyanpei/reflection/scene2/10_007"); + + const { scale, offsetX, offsetY, DESIGN_WIDTH, DESIGN_HEIGHT } = useScale(); + const [mounted, setMounted] = useState(false); + + // SSR 兼容 + useEffect(() => { + setMounted(true); + }, []); + + const handleClose = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + onClose?.(); + }, + [onClose] + ); + + const handleChoiceClick = useCallback( + (choice: "读书" | "没出息" | "女子不能上学") => { + onSelect?.(choice); + onClose?.(); + }, + [onSelect, onClose] + ); + + // 样式 + 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] + ); + + if (!mounted) return null; + + const content = ( + + {visible && ( + +
+
+ {/* 背景蒙版 */} + {maskSrc && ( + + )} + + {/* 返回按钮 */} + {onClose && ( + 返回 + )} + + {/* 内容区域 */} +
+ {/* 人物 - 左侧 */} +
+ 人物 +
+ + {/* 三个选择选项 - 右侧水平排列 */} +
+ {/* 选择A:读书 */} +
handleChoiceClick("读书")} + > + {/* 标签A */} +
+ A +
+ {/* 图标 */} + 读书 + {/* 文字说明 */} +
+ 读书只为 +
+ 升官发财 +
+
+ + {/* 选择B:没出息 */} +
handleChoiceClick("没出息")} + > + {/* 标签B */} +
+ B +
+ {/* 图标 */} + 没出息 + {/* 文字说明 */} +
+ 手工劳作 +
+ 没出息 +
+
+ + {/* 选择C:女子不能上学 */} +
handleChoiceClick("女子不能上学")} + > + {/* 标签C */} +
+ C +
+ {/* 图标 */} + 女子不能上学 + {/* 文字说明 */} +
+ 女子 +
+ 不能上学 +
+
+
+
+
+
+
+ )} +
+ ); + + return createPortal(content, document.body); +}; + +export default ChoiceDialog1; diff --git a/src/pages/huangyanpei/reflection/scene2/ChoiceDialog2.tsx b/src/pages/huangyanpei/reflection/scene2/ChoiceDialog2.tsx new file mode 100644 index 0000000..b8cbb11 --- /dev/null +++ b/src/pages/huangyanpei/reflection/scene2/ChoiceDialog2.tsx @@ -0,0 +1,191 @@ +import { useState, useEffect, useMemo, useCallback } from "react"; +import { createPortal } from "react-dom"; +import { AnimatePresence, motion } from "framer-motion"; +import { cn, getStaticImage } from "@/lib/utils"; +import { Image } from "@/components"; +import { useScale } from "@/contexts/ScaleContext"; + +export interface ChoiceDialog2Props { + /** 是否可见 */ + visible?: boolean; + /** 关闭回调 */ + onClose?: () => void; + /** 选择回调 */ + onSelect?: (choice: "铁工科" | "商科") => void; +} + +/** + * 第二个选择弹窗:背景图选择 + * - 使用蒙版遮罩作为背景 + * - 显示两个选择:10_008(箭头指向右边)和旋转后的背景图 + */ +const ChoiceDialog2 = ({ + visible = true, + onClose, + onSelect, +}: ChoiceDialog2Props) => { + const maskSrc = getStaticImage("components/蒙版遮罩"); + const backSrc = getStaticImage("components/关闭"); + const choiceSrc = getStaticImage("huangyanpei/reflection/scene2/10_008"); + + const { scale, offsetX, offsetY, DESIGN_WIDTH, DESIGN_HEIGHT } = useScale(); + const [mounted, setMounted] = useState(false); + + // SSR 兼容 + useEffect(() => { + setMounted(true); + }, []); + + const handleClose = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + onClose?.(); + }, + [onClose] + ); + + const handleChoiceClick = useCallback( + (choice: "铁工科" | "商科") => { + onSelect?.(choice); + onClose?.(); + }, + [onSelect, onClose] + ); + + // 样式 + 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] + ); + + if (!mounted) return null; + + const content = ( + + {visible && ( + +
+
+ {/* 背景蒙版 */} + {maskSrc && ( + + )} + + {/* 返回按钮 */} + {onClose && ( + 返回 + )} + + {/* 内容区域 */} +
+
+ {/* 选择A:铁工科(箭头指向左边) */} +
handleChoiceClick("铁工科")} + > + {/* 标签A */} +
+ A +
+ {/* 箭头按钮 - 需要旋转180度使其指向左边 */} +
+ 铁工科 + {/* 文字覆盖层 */} +
+
+ 铁工科 +
+
+
+
+ + {/* 选择B:商科(箭头指向右边) */} +
handleChoiceClick("商科")} + > + {/* 标签B */} +
+ B +
+ {/* 箭头按钮 */} +
+ 商科 + {/* 文字覆盖层 */} +
+
+ 商科 +
+
+
+
+
+
+
+
+
+ )} +
+ ); + + return createPortal(content, document.body); +}; + +export default ChoiceDialog2; diff --git a/src/pages/huangyanpei/reflection/scene3.tsx b/src/pages/huangyanpei/reflection/scene3.tsx index 8582825..fe731f2 100644 --- a/src/pages/huangyanpei/reflection/scene3.tsx +++ b/src/pages/huangyanpei/reflection/scene3.tsx @@ -38,7 +38,7 @@ const HuangyanpeiReflectionScene3 = () => { className="w-full h-full bg-cover bg-center bg-no-repeat" style={{ backgroundImage: getBackgroundImage( - `huangyanpei/reflection/scene3_001` + `huangyanpei/reflection/scene3/11_001` ), }} > diff --git a/src/pages/huangyanpei/reflection/scene4.tsx b/src/pages/huangyanpei/reflection/scene4.tsx index 1551416..ddeccee 100644 --- a/src/pages/huangyanpei/reflection/scene4.tsx +++ b/src/pages/huangyanpei/reflection/scene4.tsx @@ -38,7 +38,7 @@ const HuangyanpeiReflectionScene4 = () => { className="w-full h-full bg-cover bg-center bg-no-repeat" style={{ backgroundImage: getBackgroundImage( - `huangyanpei/reflection/scene4_001` + `huangyanpei/reflection/scene4/12_001` ), }} >