feat:添加孙中山场景

This commit is contained in:
zhongyidian committed 2026-01-02 23:32:26 +08:00
1 parent bf4a98bf9f
commit cad38e43df
55 files changed
+1704 -44

No files matched your search

@@ -0,0 +1,310 @@
import React, { useState, useRef, useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";
import {
Image,
ClueHintModal,
ClueBookModal,
flyToAnimation,
} from "@/components";
import { SuccessAlert } from "@/components/layout";
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
import { useClueStore } from "@/store/clueStore";
import type { ClueHint } from "@/components/clue";
const starName = "sunzhongshan";
const sceneId = `${starName}/reflection/scene1`;
// 线索配置 - 根据实际图片调整位置
const clueConfigs: ClueHint[] = [
{
id: `${sceneId}/clue1`,
name: "线索1",
position: {
top: "75%",
left: "95.5%",
width: "0%",
height: "0%",
},
},
{
id: `${sceneId}/clue2`,
name: "线索2",
position: {
top: "2%",
left: "34%",
width: "56%",
height: "50%",
},
},
{
id: `${sceneId}/clue3`,
name: "线索3",
position: {
top: "54%",
left: "20%",
width: "7%",
height: "34%",
},
},
{
id: `${sceneId}/clue4`,
name: "线索4",
position: {
top: "53%",
left: "48.5%",
width: "7%",
height: "34%",
},
},
];
const SunzhongshanReflectionScene1 = () => {
const navigate = useNavigate();
const { moduleName } = useParams<{
moduleName: string;
}>();
const [showClueHint, setShowClueHint] = useState(false);
const [showClueBook, setShowClueBook] = useState(false);
const [showSuccessAlert, setShowSuccessAlert] = useState(false);
const [successAlertContent, setSuccessAlertContent] = useState("");
const { collectClue, isClueCollected } = useClueStore();
const containerRef = useRef<HTMLDivElement>(null);
const clueBookButtonRef = useRef<HTMLDivElement>(null);
// 为每个线索创建 ref
const clueRefs = useRef<Map<string, React.RefObject<HTMLDivElement | null>>>(
new Map()
);
// 获取或创建线索的 ref
const getClueRef = (
clueId: string
): React.RefObject<HTMLDivElement | null> => {
if (!clueRefs.current.has(clueId)) {
clueRefs.current.set(clueId, React.createRef<HTMLDivElement>());
}
return clueRefs.current.get(clueId)!;
};
// 处理线索点击
const handleClueClick = (clueId: string, clueName: string) => {
// 如果已收集,不重复收集
if (isClueCollected(clueId)) {
setSuccessAlertContent("该线索已收集");
setShowSuccessAlert(true);
return;
}
// 收集线索
collectClue(clueId, clueName);
// 获取线索元素的 ref
const clueRef = getClueRef(clueId);
// 使用函数式 API 触发飞行动画
if (clueRef.current && clueBookButtonRef.current) {
flyToAnimation({
fromRef: clueRef as React.RefObject<HTMLElement>,
toRef: clueBookButtonRef as React.RefObject<HTMLElement>,
targetPosition: "center",
duration: 1200,
onComplete: () => {
// 动画完成后的回调
setSuccessAlertContent("恭喜您,获得该线索!");
setShowSuccessAlert(true);
},
});
}
};
// 处理背包点击 - 先收集线索1,然后打开线索提示框
const handleBackpackClick = () => {
const clue1Id = `${sceneId}/clue1`;
const clue1Name = "线索1";
// 如果线索1已收集,直接打开线索提示框
if (isClueCollected(clue1Id)) {
setShowClueHint(true);
return;
}
// 收集线索1
collectClue(clue1Id, clue1Name);
// 获取线索1的 ref
const clue1Ref = getClueRef(clue1Id);
// 执行飞行动画
if (clue1Ref.current && clueBookButtonRef.current) {
flyToAnimation({
fromRef: clue1Ref as React.RefObject<HTMLElement>,
toRef: clueBookButtonRef as React.RefObject<HTMLElement>,
targetPosition: "center",
duration: 1200,
onComplete: () => {
// 动画完成后,打开线索提示框
// setShowClueHint(true);
},
});
} else {
// 如果 ref 不可用,直接打开线索提示框
setShowClueHint(true);
}
};
// 获取线索图片
const getClueImage = (clueId: string) => {
const clueNumber = clueId.split("clue")[1];
return getStaticImage(`${sceneId}/线索${clueNumber}`);
};
// 获取线索详情图片
const getClueDetailImage = (clueId: string) => {
const clueNumber = clueId.split("clue")[1];
return getStaticImage(`${sceneId}/线索${clueNumber}-详情`);
};
// 初始化线索(如果还没有)
useEffect(() => {
clueConfigs.forEach((clue) => {
if (!isClueCollected(clue.id)) {
// 不自动收集,只是确保线索存在
}
});
}, [isClueCollected]);
return (
<div
ref={containerRef}
className="w-full h-full bg-cover bg-center bg-no-repeat relative"
style={{
backgroundImage: getBackgroundImage(`${starName}/reflection/scene1/bg`),
}}
>
{/* 关闭按钮 */}
<Image
className="absolute right-0 top-0 z-10"
src={getStaticImage(`${starName}/reflection/关闭`)}
button
onClick={() => {
if (moduleName && starName) {
navigate(`/${moduleName}/${starName}/reflection`);
}
}}
/>
{/* 线索点击区域 */}
{clueConfigs.map((clue) => {
const isCollected = isClueCollected(clue.id);
const clueRef = getClueRef(clue.id);
return (
<div key={clue.id}>
{/* 线索元素(用于克隆和飞行) */}
<div
ref={clueRef}
className="absolute pointer-events-none opacity-0"
style={{
top: clue.position.top,
left: clue.position.left,
width: clue.position.width,
height: clue.position.height,
}}
>
<div className="relative">
<Image
src={getClueImage(clue.id)}
className="w-20 h-20 border-2 border-yellow-400 rounded shadow-lg"
/>
<div className="absolute -bottom-6 left-1/2 -translate-x-1/2 bg-yellow-400 text-black px-2 py-1 rounded text-xs font-bold whitespace-nowrap">
{clue.name}
</div>
</div>
</div>
{/* 点击区域 */}
<div
className={`
absolute cursor-pointer transition-all duration-200
${
isCollected
? "opacity-30 hover:opacity-50"
: "opacity-0 hover:opacity-20"
}
hover:bg-yellow-400/20
`}
style={{
top: clue.position.top,
left: clue.position.left,
width: clue.position.width,
height: clue.position.height,
}}
onClick={() => handleClueClick(clue.id, clue.name)}
title={
isCollected ? `${clue.name}(已收集)` : `点击收集${clue.name}`
}
/>
</div>
);
})}
{/* 右下角-功能处 */}
<div className="absolute right-6 bottom-12 flex flex-col items-center gap-4 z-10">
<Image
src={getStaticImage(`${starName}/reflection/背包`)}
button
onClick={handleBackpackClick}
/>
<div
ref={clueBookButtonRef}
className="relative"
data-clue-book-button="true"
>
<Image
src={getStaticImage(`${starName}/reflection/线索簿`)}
button
onClick={() => setShowClueBook(true)}
/>
</div>
</div>
{/* 线索提示窗 */}
<ClueHintModal
visible={showClueHint}
backgroundImage={getStaticImage(`${starName}/reflection/scene1/bg`)}
clues={clueConfigs}
onClose={() => setShowClueHint(false)}
sourceContainerRef={containerRef}
/>
{/* 线索簿窗口 */}
<ClueBookModal
visible={showClueBook}
clues={clueConfigs.map((c) => ({
id: c.id,
name: c.name,
collected: isClueCollected(c.id),
}))}
onClose={() => setShowClueBook(false)}
getClueImage={getClueImage}
getClueDetailImage={getClueDetailImage}
/>
{/* 成功提示弹窗 */}
<SuccessAlert
visible={showSuccessAlert}
title="恭喜你!"
content={successAlertContent}
onContinue={() => {
setShowSuccessAlert(false);
setSuccessAlertContent("");
}}
onClose={() => {
setShowSuccessAlert(false);
setSuccessAlertContent("");
}}
/>
</div>
);
};
export default SunzhongshanReflectionScene1;