feat:init
This commit is contained in:
1 parent
53d0eeac6b
commit
ae0c050010
494 files changed
+17701
-2318
No files matched your search
@@ -1,409 +0,0 @@
|
||||
import { BackgroundImage, Image, WordViewerModal } from "@/components";
|
||||
import { IframeModal } from "@/components/Modal";
|
||||
import { getStaticImage, getPublicPath } from "@/utils";
|
||||
import {
|
||||
skillTrainingActivities,
|
||||
exchangeAndShowActivities,
|
||||
Activity,
|
||||
} from "@/data/activities";
|
||||
import clsx from "clsx";
|
||||
import { useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
/**
|
||||
* tab列表
|
||||
* 1. 01工作室介绍
|
||||
* 2. 02带教活动
|
||||
* 3. 03带教心得
|
||||
* 4. 04成果展示
|
||||
*/
|
||||
const tabList = [
|
||||
{
|
||||
id: 1,
|
||||
name: "01工作室介绍",
|
||||
image: getStaticImage("content/工作室介绍未选中.png"),
|
||||
selectedImage: getStaticImage("content/工作室介绍选中.png"),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "02带教活动",
|
||||
image: getStaticImage("content/带教活动未选中.png"),
|
||||
selectedImage: getStaticImage("content/带教活动选中.png"),
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "03带教心得",
|
||||
image: getStaticImage("content/带教心得未选中.png"),
|
||||
selectedImage: getStaticImage("content/带教心得选中.png"),
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "04成果展示",
|
||||
image: getStaticImage("content/成果展示未选中.png"),
|
||||
selectedImage: getStaticImage("content/成果展示选中.png"),
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 侧边栏
|
||||
* 论与技能转向培训
|
||||
* 教学实践与反思
|
||||
* 项目与课题研究
|
||||
* 经验传承与资源共享
|
||||
* 交流与展示活动
|
||||
*/
|
||||
const sideBarList = [
|
||||
{
|
||||
name: "理论与技能转向培训",
|
||||
key: 0,
|
||||
image: getStaticImage("content/理论与技能专项培训-未选中.png"),
|
||||
selectedImage: getStaticImage("content/理论与技能专项培训-选中.png"),
|
||||
},
|
||||
// {
|
||||
// name: "教学实践与反思",
|
||||
// image: getStaticImage("content/教学实践与反思-未选中.png"),
|
||||
// selectedImage: getStaticImage("content/教学实践与反思-选中.png"),
|
||||
// },
|
||||
// {
|
||||
// name: "项目与课题研究",
|
||||
// image: getStaticImage("content/项目与课题研究-未选中.png"),
|
||||
// selectedImage: getStaticImage("content/项目与课题研究-选中.png"),
|
||||
// },
|
||||
// {
|
||||
// name: "经验传承与资源共享",
|
||||
// image: getStaticImage("content/经验传承与资源共享-未选中.png"),
|
||||
// selectedImage: getStaticImage("content/经验传承与资源共享-选中.png"),
|
||||
// },
|
||||
{
|
||||
name: "交流与展示活动",
|
||||
key: 4,
|
||||
image: getStaticImage("content/交流与展示活动-未选中.png"),
|
||||
selectedImage: getStaticImage("content/交流与展示活动-选中.png"),
|
||||
},
|
||||
];
|
||||
|
||||
export const Tab = ({
|
||||
changeTab,
|
||||
className,
|
||||
defaultTabId = 1,
|
||||
}: {
|
||||
changeTab?: (id: number) => void;
|
||||
className?: string;
|
||||
defaultTabId?: number;
|
||||
}) => {
|
||||
const [selectedTabId, setSelectedTabId] = useState(defaultTabId);
|
||||
return (
|
||||
<>
|
||||
{/* tab */}
|
||||
<div className={clsx("w-full flex justify-end", className)}>
|
||||
{tabList.map((item) => (
|
||||
<Image
|
||||
key={item.id}
|
||||
src={selectedTabId === item.id ? item.selectedImage : item.image}
|
||||
alt={item.name}
|
||||
button
|
||||
onClick={() => {
|
||||
setSelectedTabId(item.id);
|
||||
changeTab?.(item.id);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<TabContent
|
||||
className="h-[calc(100%-180px)]"
|
||||
selectedTabId={selectedTabId}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const TabContent = ({
|
||||
className,
|
||||
selectedTabId,
|
||||
}: {
|
||||
className?: string;
|
||||
selectedTabId: number;
|
||||
}) => {
|
||||
const [selectedSideBarId, setSelectedSideBarId] = useState(0);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isActivitiesModalOpen, setIsActivitiesModalOpen] = useState(false);
|
||||
const [selectedActivity, setSelectedActivity] = useState<Activity | null>(
|
||||
null
|
||||
);
|
||||
const [selectedActivities, setSelectedActivities] = useState<Activity | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const handleActivityClick = (activity: Activity) => {
|
||||
setSelectedActivity(activity);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleActivitiesClick = (activities: Activity) => {
|
||||
setSelectedActivities(activities);
|
||||
setIsActivitiesModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setIsModalOpen(false);
|
||||
setSelectedActivity(null);
|
||||
};
|
||||
|
||||
const handleActivitiesCloseModal = () => {
|
||||
setIsActivitiesModalOpen(false);
|
||||
setSelectedActivities(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={clsx("overflow-y-auto custom-scrollbar", className)}>
|
||||
{/* 通过tab切换内容 */}
|
||||
{selectedTabId === 1 && (
|
||||
<div className="w-full">
|
||||
<Image
|
||||
width={"100%"}
|
||||
height={"auto"}
|
||||
src={getStaticImage("content/图文.png")}
|
||||
alt="工作室介绍"
|
||||
className="w-full h-auto"
|
||||
objectFit="contain"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{selectedTabId === 2 && (
|
||||
<div className="flex gap-10 pt-8">
|
||||
{/* 侧边栏 */}
|
||||
<div className="flex flex-col gap-10">
|
||||
{sideBarList.map((item) => (
|
||||
<Image
|
||||
key={item.key}
|
||||
src={
|
||||
selectedSideBarId === item.key
|
||||
? item.selectedImage
|
||||
: item.image
|
||||
}
|
||||
alt={item.name}
|
||||
button
|
||||
onClick={() => {
|
||||
setSelectedSideBarId(item.key);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<BackgroundImage
|
||||
src={getStaticImage("content/视频区域框.png")}
|
||||
width={1354}
|
||||
height={766}
|
||||
>
|
||||
<div className="h-full flex justify-center items-center">
|
||||
{selectedSideBarId === 0 ? (
|
||||
<>
|
||||
{/* 技能专项培训列表 */}
|
||||
<div className="w-full h-full p-8 overflow-y-auto custom-scrollbar">
|
||||
{/* 活动列表表格 */}
|
||||
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="bg-gradient-to-r from-[#82B2D6] to-[#6a9bc4] text-white">
|
||||
<th className="px-6 py-4 text-left text-3xl font-semibold w-[70%]">
|
||||
活动名称
|
||||
</th>
|
||||
<th className="px-6 py-4 text-left text-3xl font-semibold w-[30%]">
|
||||
时间
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{skillTrainingActivities.map((activity, index) => (
|
||||
<tr
|
||||
key={activity.id}
|
||||
onClick={() => handleActivityClick(activity)}
|
||||
className={clsx(
|
||||
"border-b border-gray-200 cursor-pointer transition-all duration-200 text-3xl",
|
||||
"hover:bg-[#82B2D6]/10 hover:shadow-md",
|
||||
index % 2 === 0 ? "bg-white" : "bg-gray-50"
|
||||
)}
|
||||
>
|
||||
<td className="px-6 py-4 text-gray-800 font-medium">
|
||||
{activity.name}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-gray-600">
|
||||
{activity.date}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : selectedSideBarId === 1 ? (
|
||||
<span className="text-[#82B2D6] text-4xl font-bold">
|
||||
图文、视频展示教学实践与反思内容
|
||||
</span>
|
||||
) : selectedSideBarId === 2 ? (
|
||||
<span className="text-[#82B2D6] text-4xl font-bold">
|
||||
图文、视频展示项目与课题研究内容
|
||||
</span>
|
||||
) : selectedSideBarId === 3 ? (
|
||||
<span className="text-[#82B2D6] text-4xl font-bold">
|
||||
图文、视频展示经验传承与资源共享内容
|
||||
</span>
|
||||
) : selectedSideBarId === 4 ? (
|
||||
<div className="w-full h-full pt-4 relative">
|
||||
{/* 历史活动列表 */}
|
||||
<div className="w-full h-full p-8 overflow-y-auto custom-scrollbar">
|
||||
{/* 活动列表表格 */}
|
||||
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="bg-gradient-to-r from-[#82B2D6] to-[#6a9bc4] text-white">
|
||||
<th className="px-6 py-4 text-left text-3xl font-semibold w-[70%]">
|
||||
活动名称
|
||||
</th>
|
||||
<th className="px-6 py-4 text-left text-3xl font-semibold w-[30%]">
|
||||
时间
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{exchangeAndShowActivities.map(
|
||||
(activity, index) => (
|
||||
<tr
|
||||
key={activity.id}
|
||||
onClick={() =>
|
||||
handleActivitiesClick(activity)
|
||||
}
|
||||
className={clsx(
|
||||
"border-b border-gray-200 cursor-pointer transition-all duration-200 text-3xl",
|
||||
"hover:bg-[#82B2D6]/10 hover:shadow-md",
|
||||
index % 2 === 0 ? "bg-white" : "bg-gray-50"
|
||||
)}
|
||||
>
|
||||
<td className="px-6 py-4 text-gray-800 font-medium">
|
||||
{activity.name}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-gray-600">
|
||||
{activity.date}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</BackgroundImage>
|
||||
</div>
|
||||
)}
|
||||
{selectedTabId === 3 && (
|
||||
<Image
|
||||
height={1415}
|
||||
src={getStaticImage("content/图文3.png")}
|
||||
alt="工作室介绍"
|
||||
/>
|
||||
)}
|
||||
{selectedTabId === 4 && (
|
||||
<div className="text-center text-[#82B2D6] text-4xl font-bold">
|
||||
暂无内容
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 活动文档弹窗 */}
|
||||
{selectedActivity && (
|
||||
<WordViewerModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={handleCloseModal}
|
||||
docPath={getPublicPath(selectedActivity.docPath)}
|
||||
title={selectedActivity.name}
|
||||
/>
|
||||
)}
|
||||
{selectedActivities && (
|
||||
<WordViewerModal
|
||||
isOpen={isActivitiesModalOpen}
|
||||
onClose={handleActivitiesCloseModal}
|
||||
docPath={getPublicPath(selectedActivities.docPath)}
|
||||
title={selectedActivities.name}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default function PublicityPage() {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
// 将路由参数 id 转换为数字,如果无效则默认为 1
|
||||
const defaultTabId = id ? parseInt(id, 10) : 1;
|
||||
|
||||
const changeTab = (id: number) => {
|
||||
console.log(id);
|
||||
};
|
||||
|
||||
const handleAvatarClick = () => {
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<BackgroundImage
|
||||
src={getStaticImage("content/内页底图02.png")}
|
||||
className="h-full w-full relative"
|
||||
backgroundSize="cover"
|
||||
backgroundPosition="center"
|
||||
>
|
||||
<div className=" w-full h-full pt-25">
|
||||
<Image
|
||||
src={getStaticImage("components/按钮-返回.png")}
|
||||
alt="返回"
|
||||
button
|
||||
className="absolute top-0 right-0"
|
||||
onClick={() => {
|
||||
navigate("/publicity");
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 数字人头像 */}
|
||||
{!isModalOpen && (
|
||||
<>
|
||||
<div
|
||||
className="flex flex-col items-center absolute right-4 bottom-2 cursor-pointer hover:scale-105 transition-all duration-300 z-10"
|
||||
onClick={handleAvatarClick}
|
||||
>
|
||||
<Image
|
||||
src={getStaticImage("publicity/数字人-头像框.png")}
|
||||
alt="数字人头像"
|
||||
/>
|
||||
<span className="text-white text-2xl font-bold tracking-wide">
|
||||
点击我随时为你解答
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Tab changeTab={changeTab} defaultTabId={defaultTabId} />
|
||||
</div>
|
||||
|
||||
{/* 嵌入式客服对话框 */}
|
||||
<IframeModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={handleCloseModal}
|
||||
url="https://www.edutech.host/digitalHuman/"
|
||||
title="数字人助手"
|
||||
mode="embedded"
|
||||
position="bottom-right"
|
||||
width="420px"
|
||||
height="650px"
|
||||
/>
|
||||
</BackgroundImage>
|
||||
);
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { BackgroundImage, Image } from "@/components";
|
||||
import { getStaticImage, getPublicPath } from "@/utils";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
|
||||
export default function Guide() {
|
||||
const navigate = useNavigate();
|
||||
const [showVideo, setShowVideo] = useState(true);
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
|
||||
// 监听视频播放完成
|
||||
const handleVideoEnd = () => {
|
||||
setShowVideo(false);
|
||||
};
|
||||
|
||||
// 跳过引导,直接进入宣传页
|
||||
const handleSkip = () => {
|
||||
navigate("/publicity");
|
||||
};
|
||||
|
||||
return (
|
||||
<BackgroundImage
|
||||
src={getStaticImage("guide/引导页背景.png")}
|
||||
className="h-full w-full relative"
|
||||
backgroundSize="cover"
|
||||
backgroundPosition="center"
|
||||
>
|
||||
{/* 跳过按钮 */}
|
||||
<Image
|
||||
src={getStaticImage("components/按钮-跳过.png")}
|
||||
alt="跳过"
|
||||
button
|
||||
className="absolute top-0 right-0"
|
||||
onClick={handleSkip}
|
||||
/>
|
||||
|
||||
{/* 标题 */}
|
||||
<Image
|
||||
src={getStaticImage("publicity/名师工作室-标题.png")}
|
||||
alt="标题"
|
||||
className="absolute left-0 top-30"
|
||||
/>
|
||||
|
||||
{/* 引导视频 - 仅在首次显示 */}
|
||||
{
|
||||
<div className="absolute w-[500px] left-20 -bottom-20 z-50">
|
||||
<video
|
||||
ref={videoRef}
|
||||
className="w-full h-full object-contain"
|
||||
src={getPublicPath("video/5_alpha channel.webm")}
|
||||
autoPlay
|
||||
playsInline
|
||||
onEnded={handleVideoEnd}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
{/* 引导内容区域 */}
|
||||
{
|
||||
<div className="absolute right-10 bottom-0 z-10">
|
||||
{/* 发言底板 */}
|
||||
<BackgroundImage
|
||||
className="p-20"
|
||||
width={1220}
|
||||
height={763}
|
||||
src={getStaticImage("guide/框.png")}
|
||||
>
|
||||
<p className="text-white text-3xl tracking-wide leading-15 text-left">
|
||||
各位朋友好!我是曹君,智慧商旅系主任、网络营销与直播电商专业带头人。我的名师工作室聚焦产教融合,紧扣新兴行业需求,对接虹口北外滩发展战略。
|
||||
带教以实战为核心,依托全国行业资源,让学员深入企业、乡村参与直播全流程;同时兼顾实践与教学能力双提升,致力培养复合型数字技能人才,期待与大家共成长!
|
||||
</p>
|
||||
</BackgroundImage>
|
||||
</div>
|
||||
}
|
||||
</BackgroundImage>
|
||||
);
|
||||
}
|
||||
+15
-20
@@ -1,29 +1,24 @@
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { Image } from "@/components";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { BackgroundImage, Image } from "@/components";
|
||||
import { getStaticImage } from "@/utils";
|
||||
|
||||
export default function Home() {
|
||||
const Home = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleStartExperiment = () => {
|
||||
console.log("点击开启实验");
|
||||
navigate("/menu");
|
||||
};
|
||||
|
||||
return (
|
||||
<BackgroundImage
|
||||
src={getStaticImage("home/进入页.png")}
|
||||
className="h-full w-full relative"
|
||||
backgroundSize="cover"
|
||||
backgroundPosition="center"
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{ backgroundImage: getBackgroundImage("home/首页底图") }}
|
||||
>
|
||||
<Image
|
||||
src={getStaticImage("components/按钮-进入.png")}
|
||||
alt="进入"
|
||||
onClick={() => {
|
||||
navigate("/menu");
|
||||
}}
|
||||
className="absolute bottom-38 left-1/2 -translate-1/2"
|
||||
button
|
||||
className="absolute bottom-10 left-1/2 transform -translate-x-1/2 -translate-y-1/2"
|
||||
onClick={handleStartExperiment}
|
||||
src={getStaticImage("home/首页-进入")}
|
||||
/>
|
||||
</BackgroundImage>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Home;
|
||||
+59
-187
@@ -1,207 +1,79 @@
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { Image } from "@/components";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { BackgroundImage, Image, Alert } from "@/components";
|
||||
import { getStaticImage } from "@/utils";
|
||||
import { useState, Fragment, useRef, useEffect } from "react";
|
||||
import { useTouch } from "@/hooks";
|
||||
|
||||
/**
|
||||
* 卢航-未选中.png
|
||||
* 龚丹-未选中.png
|
||||
* 曹君-未选中.png
|
||||
*/
|
||||
const teachers = [
|
||||
{
|
||||
id: 1,
|
||||
name: "卢航",
|
||||
avatar: getStaticImage("menu/卢航-未选中.png"),
|
||||
selectedAvatar: getStaticImage("menu/卢航-选中.png"),
|
||||
position: {
|
||||
top: "20.5%",
|
||||
left: "15%",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "曹君",
|
||||
avatar: getStaticImage("menu/曹君-未选中.png"),
|
||||
selectedAvatar: getStaticImage("menu/名师曹君选中.png"),
|
||||
position: {
|
||||
top: "10%",
|
||||
left: "39%",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "龚丹",
|
||||
avatar: getStaticImage("menu/龚丹-未选中.png"),
|
||||
selectedAvatar: getStaticImage("menu/龚丹-选中.png"),
|
||||
position: {
|
||||
top: "20.5%",
|
||||
left: "63%",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default function Home() {
|
||||
const Menu = () => {
|
||||
const navigate = useNavigate();
|
||||
const [selectedTeacherId, setSelectedTeacherId] = useState(0);
|
||||
const [showTipModal, setShowTipModal] = useState(false);
|
||||
|
||||
// 旋转相关状态
|
||||
const [rotation, setRotation] = useState(0);
|
||||
const [isAutoRotating, setIsAutoRotating] = useState(true);
|
||||
const autoRotateTimerRef = useRef<number | null>(null);
|
||||
const resumeTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const startRotationRef = useRef(0);
|
||||
/**
|
||||
*类型:1:革命先驱、文化民人、艺术名家、经济巨擘
|
||||
*/
|
||||
|
||||
// 自动旋转效果
|
||||
useEffect(() => {
|
||||
if (isAutoRotating) {
|
||||
const animate = () => {
|
||||
setRotation((prev) => prev + 0.2);
|
||||
autoRotateTimerRef.current = requestAnimationFrame(animate);
|
||||
};
|
||||
autoRotateTimerRef.current = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (autoRotateTimerRef.current !== null) {
|
||||
cancelAnimationFrame(autoRotateTimerRef.current);
|
||||
autoRotateTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [isAutoRotating]);
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (autoRotateTimerRef.current !== null) {
|
||||
cancelAnimationFrame(autoRotateTimerRef.current);
|
||||
}
|
||||
if (resumeTimerRef.current !== null) {
|
||||
clearTimeout(resumeTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 使用 useTouch 处理触摸手势
|
||||
const { handlers } = useTouch({
|
||||
onTouchStart: () => {
|
||||
// 停止自动旋转
|
||||
setIsAutoRotating(false);
|
||||
|
||||
// 清除恢复定时器
|
||||
if (resumeTimerRef.current) {
|
||||
clearTimeout(resumeTimerRef.current);
|
||||
resumeTimerRef.current = null;
|
||||
}
|
||||
|
||||
// 记录开始时的旋转角度
|
||||
startRotationRef.current = rotation;
|
||||
const Category = [
|
||||
{
|
||||
id: 1,
|
||||
name: "革命先驱",
|
||||
moduleName: "revolutionary",
|
||||
image: "menu/模块-革命先驱",
|
||||
},
|
||||
onTouchMove: (data) => {
|
||||
// 根据水平移动距离计算旋转角度(灵敏度 0.5)
|
||||
const deltaRotation = data.deltaX * 0.5;
|
||||
setRotation(startRotationRef.current + deltaRotation);
|
||||
{
|
||||
id: 2,
|
||||
name: "文化名人",
|
||||
moduleName: "cultural",
|
||||
image: "menu/模块-文化名人",
|
||||
},
|
||||
onTouchEnd: () => {
|
||||
// 1秒后恢复自动旋转
|
||||
resumeTimerRef.current = setTimeout(() => {
|
||||
setIsAutoRotating(true);
|
||||
}, 1000);
|
||||
{
|
||||
id: 3,
|
||||
name: "艺术名家",
|
||||
moduleName: "artistic",
|
||||
image: "menu/模块-艺术名家",
|
||||
},
|
||||
});
|
||||
|
||||
const handleSelectTeacher = (teacherId: number) => {
|
||||
if (teacherId !== 2) {
|
||||
// 点击卢航或龚丹时显示提示
|
||||
setShowTipModal(true);
|
||||
return;
|
||||
}
|
||||
setSelectedTeacherId(teacherId);
|
||||
navigate("/guide");
|
||||
};
|
||||
{
|
||||
id: 4,
|
||||
name: "经济巨擘",
|
||||
moduleName: "economic",
|
||||
image: "menu/模块-经济巨擘",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<BackgroundImage
|
||||
src={getStaticImage("menu/首页背景.png")}
|
||||
className="h-full w-full relative"
|
||||
backgroundSize="cover"
|
||||
backgroundPosition="center"
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{ backgroundImage: getBackgroundImage("menu/故居入口页底图") }}
|
||||
>
|
||||
<Image
|
||||
src={getStaticImage("components/按钮-返回.png")}
|
||||
alt="返回"
|
||||
className="absolute top-10 left-10"
|
||||
src={getStaticImage("menu/首页-返回")}
|
||||
button
|
||||
className="absolute top-0 right-0"
|
||||
onClick={() => navigate("/")}
|
||||
/>
|
||||
|
||||
<Image
|
||||
src={getStaticImage("menu/内页居中标题.png")}
|
||||
alt="文字"
|
||||
className="absolute bottom-0 left-1/2 transform -translate-x-1/2"
|
||||
/>
|
||||
<div className="absolute left-40 top-70 inline-grid grid-cols-2 gap-x-20 gap-y-10">
|
||||
{Category.map((item) => (
|
||||
<div
|
||||
className="relative w-[241px] h-[282px] bg-cover bg-center bg-no-repeat hover:brightness-110 hover:scale-105 transition-all cursor-pointer"
|
||||
style={{ backgroundImage: getBackgroundImage("menu/模块-通用") }}
|
||||
key={item.id}
|
||||
onClick={() =>
|
||||
navigate(`/entrance/${encodeURIComponent(item.moduleName)}`)
|
||||
}
|
||||
>
|
||||
<div className="absolute top-15 flex flex-col items-center">
|
||||
<Image key={item.id} src={getStaticImage(item.image)} />
|
||||
|
||||
{/* 提示弹窗 */}
|
||||
<Alert
|
||||
isOpen={showTipModal}
|
||||
onClose={() => setShowTipModal(false)}
|
||||
message="该名师工作室数字睿教正在筹划中"
|
||||
type="warning"
|
||||
/>
|
||||
|
||||
<BackgroundImage
|
||||
src={getStaticImage("menu/转盘合.png")}
|
||||
width={2058}
|
||||
height={2046}
|
||||
className="absolute -bottom-1/5 -left-14.5 cursor-grab active:cursor-grabbing select-none"
|
||||
backgroundSize="cover"
|
||||
backgroundPosition="center"
|
||||
{...handlers}
|
||||
style={{
|
||||
transform: `rotate(${rotation}deg)`,
|
||||
transition: "none",
|
||||
}}
|
||||
>
|
||||
{teachers.map((teacher, _index) => (
|
||||
<Fragment key={teacher.id}>
|
||||
<Image
|
||||
src={
|
||||
selectedTeacherId === teacher.id
|
||||
? teacher.selectedAvatar
|
||||
: teacher.avatar
|
||||
}
|
||||
alt={teacher.name}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleSelectTeacher(teacher.id);
|
||||
}}
|
||||
className="absolute"
|
||||
style={{
|
||||
top:
|
||||
selectedTeacherId === teacher.id
|
||||
? "4%"
|
||||
: teacher.position.top,
|
||||
left:
|
||||
selectedTeacherId === teacher.id
|
||||
? "36%"
|
||||
: teacher.position.left,
|
||||
transform: `rotate(${-rotation}deg)`,
|
||||
transition: "none",
|
||||
}}
|
||||
button={selectedTeacherId !== teacher.id}
|
||||
/>
|
||||
|
||||
{/* <div className="absolute top-[25%] left-[12%] text-gray-500 text-3xl font-bold cursor-not-allowed">
|
||||
该名师工作室数字睿教正在筹划中
|
||||
<div className="relative mt-23">
|
||||
<Image
|
||||
className="absolute bottom-5"
|
||||
src={getStaticImage("menu/未看完")}
|
||||
/>
|
||||
<Image src={getStaticImage("menu/未探索")} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute top-[25%] left-[65%] text-gray-500 text-3xl font-bold cursor-not-allowed">
|
||||
该名师工作室数字睿教正在筹划中
|
||||
</div> */}
|
||||
</Fragment>
|
||||
</div>
|
||||
))}
|
||||
</BackgroundImage>
|
||||
</BackgroundImage>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Menu;
|
||||
@@ -1,250 +0,0 @@
|
||||
import { BackgroundImage, Image } from "@/components";
|
||||
import { IframeModal } from "@/components/Modal";
|
||||
import { getPublicPath, getStaticImage } from "@/utils";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useFirstVisitStore } from "@/store";
|
||||
|
||||
// 解答弹窗组件
|
||||
interface DialogBoxProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 功能列表
|
||||
* 1. 01工作室介绍
|
||||
* 2. 02带教活动
|
||||
* 3. 03带教心得
|
||||
* 4. 04成果展示
|
||||
*/
|
||||
const functionList = [
|
||||
{
|
||||
id: 1,
|
||||
name: "01工作室介绍",
|
||||
image: getStaticImage("publicity/01未选中.png"),
|
||||
selectedImage: getStaticImage("publicity/01工作室介绍选中.png"),
|
||||
position: "absolute bottom-25 left-[35%]",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "02带教活动",
|
||||
image: getStaticImage("publicity/02未选中.png"),
|
||||
selectedImage: getStaticImage("publicity/02带教活动选中.png"),
|
||||
position: "absolute bottom-65 left-[50%]",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "03带教心得",
|
||||
image: getStaticImage("publicity/03未选中.png"),
|
||||
selectedImage: getStaticImage("publicity/03带教心得选中.png"),
|
||||
position: "absolute bottom-100 left-[65%]",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "04成果展示",
|
||||
image: getStaticImage("publicity/04未选中.png"),
|
||||
selectedImage: getStaticImage("publicity/04成果展示选中.png"),
|
||||
position: "absolute bottom-130 left-[80%]",
|
||||
},
|
||||
];
|
||||
|
||||
function DialogBox({ isOpen, onClose }: DialogBoxProps) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-start justify-center pt-10 pointer-events-none">
|
||||
{/* 遮罩层 */}
|
||||
{/* <div className="absolute inset-0 bg-black bg-opacity-50" /> */}
|
||||
|
||||
{/* 解答框 */}
|
||||
<BackgroundImage
|
||||
src={getStaticImage("publicity/功能-输入.png")}
|
||||
alt="解答框"
|
||||
width={877}
|
||||
height={218}
|
||||
overflowHidden={false}
|
||||
className="relative z-10 pointer-events-auto"
|
||||
>
|
||||
<div className="relative w-full h-full pt-20 pl-8">
|
||||
{/* 数字人头像 */}
|
||||
<Image
|
||||
src={getStaticImage("publicity/数字人-头像框.png")}
|
||||
alt="数字人头像"
|
||||
className="absolute -left-10 -top-10 z-10"
|
||||
/>
|
||||
{/* 关闭按钮 */}
|
||||
<Image
|
||||
src={getStaticImage("publicity/按钮-关闭.png")}
|
||||
alt="关闭按钮"
|
||||
className="absolute -right-5 -top-5"
|
||||
button
|
||||
onClick={onClose}
|
||||
/>
|
||||
{/* 输入框 */}
|
||||
<BackgroundImage
|
||||
src={getStaticImage("publicity/输入框.png")}
|
||||
alt="输入框"
|
||||
width={811}
|
||||
height={100}
|
||||
>
|
||||
<textarea
|
||||
placeholder="有什么我能帮你的吗直接输入即可.……"
|
||||
className="w-full h-full pl-15 pr-35 pt-4"
|
||||
></textarea>
|
||||
|
||||
{/* icon语音输入和发送按钮 */}
|
||||
<Image
|
||||
src={getStaticImage("publicity/功能-语音输入.png")}
|
||||
className="absolute right-20 bottom-6"
|
||||
button
|
||||
/>
|
||||
<Image
|
||||
src={getStaticImage("publicity/功能-发送.png")}
|
||||
className="absolute right-4 bottom-6"
|
||||
button
|
||||
/>
|
||||
</BackgroundImage>
|
||||
</div>
|
||||
</BackgroundImage>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PublicityPage() {
|
||||
const navigate = useNavigate();
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [selectedFunctionId, setSelectedFunctionId] = useState(0);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
// 首次访问状态管理
|
||||
const { hasSeenIntro, setHasSeenIntro, isExpired, reset } =
|
||||
useFirstVisitStore();
|
||||
const [showIntroVideo, setShowIntroVideo] = useState(false);
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
|
||||
// 检查是否显示引导视频
|
||||
useEffect(() => {
|
||||
// 如果存储已过期,重置状态
|
||||
if (isExpired()) {
|
||||
reset();
|
||||
setShowIntroVideo(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasSeenIntro) {
|
||||
setShowIntroVideo(true);
|
||||
}
|
||||
}, [hasSeenIntro, isExpired, reset]);
|
||||
|
||||
// 监听视频播放完成
|
||||
const handleVideoEnd = () => {
|
||||
setShowIntroVideo(false);
|
||||
setHasSeenIntro();
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const handleOpenDialog = () => {
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseDialog = () => {
|
||||
setIsDialogOpen(false);
|
||||
};
|
||||
const handleAvatarClick = () => {
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<BackgroundImage
|
||||
src={getStaticImage("publicity/内页底图01.png")}
|
||||
className="h-full w-full relative"
|
||||
backgroundSize="cover"
|
||||
backgroundPosition="center"
|
||||
>
|
||||
{/* 功能列表 */}
|
||||
{functionList.map((item) => (
|
||||
<Image
|
||||
key={item.id}
|
||||
src={selectedFunctionId === item.id ? item.selectedImage : item.image}
|
||||
alt={item.name}
|
||||
className={`${item.position} transition-all duration-300`}
|
||||
button
|
||||
onClick={() => {
|
||||
setSelectedFunctionId(item.id);
|
||||
navigate(`/content/${item.id}`);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Image
|
||||
src={getStaticImage("components/按钮-返回.png")}
|
||||
alt="返回"
|
||||
button
|
||||
className="absolute top-0 right-0"
|
||||
onClick={() => {
|
||||
navigate("/menu");
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 标题 */}
|
||||
<Image
|
||||
src={getStaticImage("publicity/名师工作室-标题.png")}
|
||||
alt="标题"
|
||||
className="absolute left-0 top-30"
|
||||
/>
|
||||
|
||||
{/* 数字人 - 未播放引导视频时显示 */}
|
||||
<Image
|
||||
src={getStaticImage("publicity/曹君名师数字人分身.png")}
|
||||
alt="数字人"
|
||||
className="absolute left-0 -bottom-20"
|
||||
/>
|
||||
|
||||
{/* 数字人头像 */}
|
||||
{/* {!isModalOpen && (
|
||||
<>
|
||||
<div
|
||||
className="flex flex-col items-center absolute right-4 bottom-2 cursor-pointer hover:scale-105 transition-all duration-300 z-10"
|
||||
onClick={handleAvatarClick}
|
||||
>
|
||||
<Image
|
||||
src={getStaticImage("publicity/数字人-头像框.png")}
|
||||
alt="数字人头像"
|
||||
/>
|
||||
<span className="text-white text-2xl font-bold tracking-wide">
|
||||
点击我随时为你解答
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)} */}
|
||||
{/* 提示 */}
|
||||
{/* <Image
|
||||
src={getStaticImage("publicity/点击提示.png")}
|
||||
alt="提示"
|
||||
className="absolute left-0 -bottom-10"
|
||||
button
|
||||
onClick={handleOpenDialog}
|
||||
/> */}
|
||||
|
||||
{/* 解答弹窗 */}
|
||||
{/* <DialogBox isOpen={isDialogOpen} onClose={handleCloseDialog} /> */}
|
||||
|
||||
{/* 嵌入式客服对话框 */}
|
||||
{/* <IframeModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={handleCloseModal}
|
||||
url="https://www.edutech.host/digitalHuman/"
|
||||
title="数字人助手"
|
||||
mode="embedded"
|
||||
position="bottom-right"
|
||||
width="420px"
|
||||
height="650px"
|
||||
/> */}
|
||||
</BackgroundImage>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { Image } from "@/components";
|
||||
|
||||
const Chenyun = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
// starName 固定为 chenyun
|
||||
const starName = "chenyun";
|
||||
|
||||
if (!moduleName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`${starName}/陈云故居底图`),
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
className="absolute right-37 top-22"
|
||||
src={getStaticImage("components/革命先驱故居-返回")}
|
||||
button
|
||||
onClick={() => navigate(`/entrance/${moduleName}`)}
|
||||
/>
|
||||
|
||||
<div className="absolute right-20 top-75 flex flex-col space-y-10">
|
||||
{/* 对话 */}
|
||||
<Image
|
||||
button
|
||||
src={getStaticImage("star/一-解锁")}
|
||||
onClick={() => navigate(`/${moduleName}/${starName}/place1`)}
|
||||
/>
|
||||
{/* 体悟 */}
|
||||
<Image
|
||||
button
|
||||
src={getStaticImage("star/二-解锁")}
|
||||
onClick={() => navigate(`/${moduleName}/${starName}/reflection`)}
|
||||
/>
|
||||
{/* 内化 */}
|
||||
<Image
|
||||
button
|
||||
src={getStaticImage("star/三-解锁")}
|
||||
onClick={() => navigate(`/${moduleName}/${starName}/internalization`)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Chenyun;
|
||||
@@ -0,0 +1,284 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import {
|
||||
Image,
|
||||
Video,
|
||||
type VideoAnchor,
|
||||
type DragFillResult,
|
||||
} from "@/components";
|
||||
import { getPublicPath, getStaticImage } from "@/lib/utils";
|
||||
import { SuccessAlert, DragQuizPanel } from "@/components/layout";
|
||||
import { MultipleChoiceQuizPanel } from "@/components/layout/quiz";
|
||||
|
||||
type QuizOption = {
|
||||
value: string;
|
||||
label: string;
|
||||
isCorrect?: boolean;
|
||||
explanation?: string; // 错误选项的解释
|
||||
};
|
||||
|
||||
type QuizAnchor = {
|
||||
id: string;
|
||||
time: number;
|
||||
title: string;
|
||||
prompt: string;
|
||||
type: "single" | "multiple" | "drag";
|
||||
options: QuizOption[];
|
||||
// 拖拽题专用
|
||||
blanks?: { id: string; label: string }[];
|
||||
};
|
||||
|
||||
// 星名固定为 chenyun
|
||||
const starName = "chenyun";
|
||||
|
||||
// 锚点数据:可根据实际视频时长调整 time
|
||||
const QUIZ_ANCHORS: QuizAnchor[] = [
|
||||
{
|
||||
id: "quiz-1",
|
||||
time: 12,
|
||||
title: "题目1",
|
||||
prompt: "陈云经济思想的核心内容指的是( )。",
|
||||
type: "multiple",
|
||||
options: [
|
||||
{ value: "A", label: "综合平衡", isCorrect: true },
|
||||
{
|
||||
value: "B",
|
||||
label: "鸟笼比喻",
|
||||
explanation: "鸟笼比喻是正确的处理好计划与市场的关系的一个发展阶段",
|
||||
},
|
||||
{ value: "C", label: "正确的处理好计划与市场的关系", isCorrect: true },
|
||||
{ value: "D", label: "经济建设的目的论", isCorrect: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "quiz-2",
|
||||
time: 30,
|
||||
title: "题目2",
|
||||
prompt: "陈云说过以下哪些话能着重体现出他的经济思想。",
|
||||
type: "multiple",
|
||||
options: [
|
||||
{
|
||||
value: "A",
|
||||
label: "不唯上、不唯书、只唯实,交换、比较、反复",
|
||||
isCorrect: true,
|
||||
},
|
||||
{ value: "B", label: "无农不稳,无粮则乱", isCorrect: true },
|
||||
{ value: "C", label: "个人名利淡如水,党的事业重如山。", isCorrect: true },
|
||||
{
|
||||
value: "D",
|
||||
label: "领导机关制定政策,要用百分之九十以上的时间作调查研究工作。",
|
||||
isCorrect: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "quiz-3",
|
||||
time: 48,
|
||||
title: "题目3",
|
||||
prompt: "我们新时代的青年可以从陈云同志身上学习到哪些精神",
|
||||
type: "drag",
|
||||
options: [
|
||||
{ value: "积极的人生态度", label: "积极的人生态度", isCorrect: true },
|
||||
{ value: "刻苦的学习精神", label: "刻苦的学习精神", isCorrect: true },
|
||||
{ value: "朴素的节俭精神", label: "朴素的节俭精神", isCorrect: true },
|
||||
{ value: "实事求是的精神", label: "实事求是的精神", isCorrect: true },
|
||||
],
|
||||
blanks: [
|
||||
{ id: "blank-1", label: "____" },
|
||||
{ id: "blank-2", label: "____" },
|
||||
{ id: "blank-3", label: "____" },
|
||||
{ id: "blank-4", label: "____" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const ChenyunInternalization = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{ moduleName: string }>();
|
||||
const [activeSuccess, setActiveSuccess] = useState<string | null>(null);
|
||||
const [successContent, setSuccessContent] = useState<string>("");
|
||||
const [resumeCallback, setResumeCallback] = useState<(() => void) | null>(
|
||||
null
|
||||
);
|
||||
// 拖拽题状态
|
||||
const [dragQuizValues, setDragQuizValues] = useState<
|
||||
Record<string, DragFillResult[]>
|
||||
>({});
|
||||
const [dragQuizErrors, setDragQuizErrors] = useState<Record<string, string>>(
|
||||
{}
|
||||
);
|
||||
|
||||
const anchors: VideoAnchor[] = useMemo(
|
||||
() =>
|
||||
QUIZ_ANCHORS.map((item) => {
|
||||
const correctAnswers = item.options
|
||||
.filter((opt) => opt.isCorrect)
|
||||
.map((opt) => opt.value);
|
||||
|
||||
// 拖拽题处理
|
||||
if (item.type === "drag" && item.blanks) {
|
||||
return {
|
||||
id: item.id,
|
||||
time: item.time,
|
||||
once: true,
|
||||
afterResume: "remove",
|
||||
render: ({ resume }) => {
|
||||
const handleDragChange = (val: DragFillResult[]) => {
|
||||
setDragQuizValues((prev) => ({ ...prev, [item.id]: val }));
|
||||
setDragQuizErrors((prev) => ({ ...prev, [item.id]: "" }));
|
||||
|
||||
if (val.length < correctAnswers.length) return;
|
||||
|
||||
// 检查是否包含了所有正确答案(不要求顺序)
|
||||
const filledAnswers = val.map((v) => v.optionValue);
|
||||
const answerSet = new Set(correctAnswers);
|
||||
const filledSet = new Set(filledAnswers);
|
||||
const isCorrect =
|
||||
answerSet.size === filledSet.size &&
|
||||
[...answerSet].every((answer) => filledSet.has(answer));
|
||||
|
||||
if (isCorrect) {
|
||||
setActiveSuccess(item.id);
|
||||
setSuccessContent("回答正确,请继续探索。");
|
||||
setResumeCallback(() => resume);
|
||||
} else {
|
||||
setDragQuizErrors((prev) => ({
|
||||
...prev,
|
||||
[item.id]: "再想一想,确保选择了所有正确答案~",
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DragQuizPanel
|
||||
visible={activeSuccess !== item.id}
|
||||
zIndex={10000}
|
||||
title={`${item.title}(拖拽)`}
|
||||
questionPrefix={`${item.prompt}(`}
|
||||
questionSuffix=")?"
|
||||
options={item.options.map((opt) => ({
|
||||
value: opt.value,
|
||||
label: opt.label,
|
||||
}))}
|
||||
blanks={item.blanks || []}
|
||||
value={dragQuizValues[item.id] || []}
|
||||
error={dragQuizErrors[item.id]}
|
||||
onChange={handleDragChange}
|
||||
/>
|
||||
<SuccessAlert
|
||||
visible={activeSuccess === item.id}
|
||||
title="恭喜你, 选择正确!"
|
||||
content={successContent}
|
||||
onContinue={() => {
|
||||
setActiveSuccess(null);
|
||||
setSuccessContent("");
|
||||
setDragQuizValues((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[item.id];
|
||||
return next;
|
||||
});
|
||||
setDragQuizErrors((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[item.id];
|
||||
return next;
|
||||
});
|
||||
const cb = resumeCallback;
|
||||
setResumeCallback(null);
|
||||
cb?.();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 多选题处理
|
||||
if (item.type === "multiple") {
|
||||
return {
|
||||
id: item.id,
|
||||
time: item.time,
|
||||
once: true,
|
||||
afterResume: "remove",
|
||||
render: ({ resume }) => (
|
||||
<>
|
||||
<MultipleChoiceQuizPanel
|
||||
visible={activeSuccess !== item.id}
|
||||
zIndex={10000}
|
||||
title={`${item.title}(多选题):${item.prompt}`}
|
||||
options={item.options.map((opt) => ({
|
||||
value: opt.value,
|
||||
label: `${opt.value}. ${opt.label}`,
|
||||
}))}
|
||||
correctAnswer={correctAnswers}
|
||||
errorMessage="再思考一下,注意选择所有正确答案。"
|
||||
onSubmit={(values: string[], isCorrect: boolean) => {
|
||||
if (isCorrect) {
|
||||
setActiveSuccess(item.id);
|
||||
setSuccessContent("回答正确,请继续探索。");
|
||||
setResumeCallback(() => resume);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<SuccessAlert
|
||||
visible={activeSuccess === item.id}
|
||||
title="恭喜你, 选择正确!"
|
||||
content={successContent}
|
||||
onContinue={() => {
|
||||
setActiveSuccess(null);
|
||||
setSuccessContent("");
|
||||
const cb = resumeCallback;
|
||||
setResumeCallback(null);
|
||||
cb?.();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// 单选题处理(保留原有逻辑,以防万一)
|
||||
return {
|
||||
id: item.id,
|
||||
time: item.time,
|
||||
once: true,
|
||||
afterResume: "remove",
|
||||
render: () => null,
|
||||
};
|
||||
}),
|
||||
[
|
||||
activeSuccess,
|
||||
resumeCallback,
|
||||
successContent,
|
||||
dragQuizValues,
|
||||
dragQuizErrors,
|
||||
]
|
||||
);
|
||||
|
||||
if (!moduleName) return null;
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full bg-black">
|
||||
{/* 离开按钮 */}
|
||||
<Image
|
||||
className="absolute right-8 top-8 z-10"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => navigate(`/${moduleName}/${starName}/home`)}
|
||||
/>
|
||||
|
||||
<Video
|
||||
src={getPublicPath("video/chenyun/chenyun_neihua.mp4")}
|
||||
className="h-full w-full"
|
||||
anchors={anchors}
|
||||
autoPlay
|
||||
muted
|
||||
playsInline
|
||||
controls
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChenyunInternalization;
|
||||
@@ -0,0 +1,273 @@
|
||||
import {
|
||||
ExhibitOverlay,
|
||||
GuideSteps,
|
||||
GuideStepsItem,
|
||||
Image,
|
||||
Title,
|
||||
Overlay,
|
||||
} from "@/components";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { useState } from "react";
|
||||
import Cursor from "@/components/cursor";
|
||||
|
||||
// starName 固定为 chenyun
|
||||
const starName = "chenyun";
|
||||
|
||||
// 展品 ID 与资源路径映射,ID 用来做唯一标识
|
||||
const exhibitSrcMap: Record<1 | 2 | 3 | 4, string> = {
|
||||
1: "chenyun/place1/场景1展品1",
|
||||
2: "chenyun/place1/场景1展品2",
|
||||
3: "chenyun/place1/场景1展品3",
|
||||
4: "chenyun/place1/场景1展品4",
|
||||
};
|
||||
const ChenyunPlace1 = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
// 学习说明对话框显示状态
|
||||
const [showIntroDialog, setShowIntroDialog] = useState(false);
|
||||
// 引导对话显示状态
|
||||
const [visible, setVisible] = useState(true);
|
||||
// 当前激活的展品 ID(只存 ID,不直接存路径,便于作为唯一标识)
|
||||
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | 3 | 4 | null>(
|
||||
null
|
||||
);
|
||||
// 已浏览展品的 ID 集合
|
||||
const [visited, setVisited] = useState<Set<number>>(new Set());
|
||||
|
||||
const handleOpenExhibit = (index: 1 | 2 | 3 | 4) => {
|
||||
setActiveExhibitId(index);
|
||||
};
|
||||
|
||||
const handleStartLearning = () => {
|
||||
setShowIntroDialog(false);
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
const progressCount = visited.size;
|
||||
const total = 4;
|
||||
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;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`chenyun/place1/场景1`),
|
||||
}}
|
||||
>
|
||||
{/* 标题 */}
|
||||
<Title
|
||||
title="水乡少年·艰辛成长"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
/>
|
||||
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/home`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 展品1 */}
|
||||
<Cursor
|
||||
className="absolute left-145 top-100"
|
||||
onClick={() => handleOpenExhibit(1)}
|
||||
read={visited.has(1)}
|
||||
/>
|
||||
{/* 展品2 */}
|
||||
<Cursor
|
||||
className="absolute left-198 top-100"
|
||||
onClick={() => handleOpenExhibit(2)}
|
||||
read={visited.has(2)}
|
||||
/>
|
||||
{/* 展品3 */}
|
||||
<Cursor
|
||||
className="absolute right-170 bottom-60"
|
||||
onClick={() => handleOpenExhibit(3)}
|
||||
read={visited.has(3)}
|
||||
/>
|
||||
{/* 展品4 */}
|
||||
<Cursor
|
||||
className="absolute right-98 top-130 cursor"
|
||||
onClick={() => handleOpenExhibit(4)}
|
||||
read={visited.has(4)}
|
||||
/>
|
||||
|
||||
{/* 继续前进 - 仅在完成所有展品后显示 */}
|
||||
{completed && (
|
||||
<Image
|
||||
className="absolute bottom-0 right-0"
|
||||
src={getStaticImage("chenyun/place1/继续前进")}
|
||||
button
|
||||
onClick={() => {
|
||||
navigate(`/${moduleName}/${starName}/place2`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 遮罩层 */}
|
||||
<ExhibitOverlay
|
||||
visible={activeExhibitId !== null}
|
||||
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
|
||||
onClose={() => {
|
||||
if (activeExhibitId !== null) {
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(activeExhibitId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
setActiveExhibitId(null);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 学习说明对话框 */}
|
||||
{showIntroDialog && (
|
||||
<Overlay
|
||||
selectors={[]}
|
||||
visible={showIntroDialog}
|
||||
maskColor="rgba(0, 0, 0, 0.6)"
|
||||
zIndex={50}
|
||||
>
|
||||
<LearningIntroduction handleStartLearning={handleStartLearning} />
|
||||
</Overlay>
|
||||
)}
|
||||
|
||||
{/* 引导 */}
|
||||
<GuideSteps
|
||||
visible={visible}
|
||||
onVisibleChange={setVisible}
|
||||
showMask={true}
|
||||
highlightSelectors={[]}
|
||||
>
|
||||
<GuideStepsItem speaker="student">
|
||||
小沪!这就是陈云爷爷小时候生活的房子吗?看起来和普通农家差不多,他童年过得艰苦吗?
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
这座房子就是陈云故居,位于上海市青浦区练塘镇下塘街95号,是一座坐南朝北、面向河畔的两层楼江南民居。...
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
实际上是陈云舅父母的家,也是少年陈云从4岁至14岁生活的地方。童年的陈云,家庭生活十分不幸。他两岁时父亲病故,四岁时,母亲也悲苦地死去...
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
父母去世后,由外婆抚养。陈云从小跟舅父廖文光和舅母廖倪氏一起生活。家里靠种田和做小生意勉强维持生计。
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 学习说明内容
|
||||
const LearningIntroduction = ({
|
||||
handleStartLearning,
|
||||
}: {
|
||||
handleStartLearning: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="relative w-full h-full bg-gradient-to-b from-[#f5f1e8] via-[#ede8dc] to-[#e8e0d1] shadow-[0_20px_60px_rgba(0,0,0,0.4)] border-4 border-[#d4c5a9] overflow-hidden">
|
||||
{/* 内容层 */}
|
||||
<div className="relative h-full w-full p-16 pb-20 overflow-hidden flex flex-col">
|
||||
{/* 装饰性顶部线条 */}
|
||||
<div className="absolute top-0 left-0 right-0 h-2 bg-gradient-to-r from-transparent via-[#c9b99b] to-transparent"></div>
|
||||
|
||||
{/* 标题 */}
|
||||
<div className="text-center mb-8 flex-shrink-0">
|
||||
<h1 className="text-6xl font-bold text-[#5c4a37] mb-4 tracking-wide">
|
||||
走进故居-对话模块学习说明
|
||||
</h1>
|
||||
<div className="w-32 h-1 bg-gradient-to-r from-transparent via-[#8b7355] to-transparent mx-auto"></div>
|
||||
</div>
|
||||
|
||||
{/* 介绍文字 */}
|
||||
<div className="mb-8 space-y-4 flex-shrink-0">
|
||||
<p className="text-3xl text-[#6b5d47] leading-relaxed text-center font-medium">
|
||||
同学你好,欢迎来到走进故居-对话学习模块!
|
||||
</p>
|
||||
<p className="text-3xl text-[#6b5d47] leading-relaxed text-center">
|
||||
进入陈云故居场景后,我们将通过参观陈云故居和陈云纪念馆的形式,带你游览陈云同志经历的7个阶段:
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 7个阶段列表 - 卡片式设计 */}
|
||||
<div className="mb-8 bg-[rgba(255,255,255,0.6)] rounded-2xl p-6 border-2 border-[#d4c5a9] flex-shrink-0">
|
||||
<div className="flex flex-wrap items-center justify-center gap-4 text-2xl">
|
||||
{[
|
||||
"水乡少年 艰辛成长",
|
||||
"商务启航 投身革命",
|
||||
"苏区淬炼 崭露头角",
|
||||
"延安担当经济破局",
|
||||
"恢复时期经济奠基",
|
||||
"建设时期 务实兴邦",
|
||||
"改革浪潮 擘画实业",
|
||||
].map((stage, index, array) => (
|
||||
<div key={index} className="flex items-center">
|
||||
<span className="px-4 py-2 bg-[rgba(139,115,85,0.15)] rounded-lg text-[#5c4a37] font-semibold">
|
||||
{stage}
|
||||
</span>
|
||||
{index < array.length - 1 && (
|
||||
<span className="mx-2 text-[#8b7355] font-bold text-3xl">
|
||||
→
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 功能说明列表 */}
|
||||
<div className="mb-8 space-y-4 flex-1 overflow-hidden">
|
||||
<div className="h-full overflow-hidden">
|
||||
{[
|
||||
"走进故居-对话学习模块会包含多个场景,场景中有蓝色动态光点的展品,表示未浏览,可通过点击光点来学习展品背后的故事,学习展品后蓝色动态光点会变为绿色静态光点,表示已浏览;",
|
||||
"场景中的展品有可能会包含一个或多个拼图,游览过程中如获得拼图,会自动掉落拼图并收入拼图盒中,拼图盒可以点开查看当前拼图收集情况,点击单个拼图可以回到获得拼图的场景再次学习;",
|
||||
"所有有蓝色动态光点的展品全部学习后才能进入下一个场景进行学习;",
|
||||
"每个阶段学习过后,会有学习回顾环节,以导图的形式总结了阶段的学习内容;",
|
||||
].map((feature, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-start gap-6 bg-[rgba(255,255,255,0.4)] rounded-xl p-6 border-l-4 border-[#8b7355] mb-4"
|
||||
>
|
||||
<span className="flex-shrink-0 w-12 h-12 bg-gradient-to-br from-[#8b7355] to-[#6b5d47] text-white text-3xl font-bold rounded-full flex items-center justify-center shadow-md">
|
||||
{index + 1}
|
||||
</span>
|
||||
<p className="text-2xl text-[#5c4a37] leading-relaxed flex-1 pt-1">
|
||||
{feature}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 开始学习按钮 */}
|
||||
<div className="flex justify-center flex-shrink-0">
|
||||
<button
|
||||
onClick={handleStartLearning}
|
||||
className="relative cursor-pointer px-20 py-5 bg-gradient-to-r from-[#8b7355] to-[#6b5d47] text-white text-4xl font-bold rounded-2xl shadow-[0_8px_20px_rgba(139,115,85,0.4)] hover:shadow-[0_12px_30px_rgba(139,115,85,0.6)] hover:from-[#9b8365] hover:to-[#7b6d57] transition-all duration-300 active:scale-95 border-2 border-[#a08f75]"
|
||||
>
|
||||
<span className="relative z-10">开始学习</span>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white to-transparent opacity-0 hover:opacity-20 rounded-2xl transition-opacity duration-300"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChenyunPlace1;
|
||||
@@ -0,0 +1,164 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
ExhibitOverlay,
|
||||
Title,
|
||||
Image,
|
||||
Cursor,
|
||||
GuideSteps,
|
||||
GuideStepsItem,
|
||||
} from "@/components";
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
// starName 固定为 chenyun
|
||||
const starName = "chenyun";
|
||||
|
||||
// 展品 ID 与资源路径映射
|
||||
const exhibitSrcMap: Record<1 | 2, string> = {
|
||||
1: "chenyun/place2/场景2展品1",
|
||||
2: "chenyun/place2/场景2展品2",
|
||||
};
|
||||
|
||||
// 展品描述映射
|
||||
const exhibitDescriptionMap: Record<1 | 2, string> = {
|
||||
1: "同学你看,这里有一幅陈云提写的颜安小学。陈云在青浦县乙种商业学校学习了一个多月后,又因经济困难不得不辍学回家。多亏当地知识分子、颜安小学校长杜枢,他免费保荐陈云入颜安小学高小部读书。颜安小学实行的新式教育开阔了陈云的视野,五四运动后,颜安小学师生积极投入反帝爱国运动,为陈云日后加入中国共产党埋下了思想的种子。",
|
||||
2: "",
|
||||
};
|
||||
const ChenyunPlace2 = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
// 当前激活展品 ID,只存 ID 做唯一标识
|
||||
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | null>(null);
|
||||
// 已浏览展品 ID 集合
|
||||
const [visited, setVisited] = useState<Set<number>>(new Set());
|
||||
// GuideSteps 显示状态
|
||||
const [guideVisible, setGuideVisible] = useState(false);
|
||||
// GuideSteps 当前步骤
|
||||
const [guideStep, setGuideStep] = useState(0);
|
||||
|
||||
const handleOpenExhibit = (index: 1 | 2) => {
|
||||
if (index === 1) {
|
||||
// 展品1:点击时同时打开展品和引导对话,从第1步开始
|
||||
setActiveExhibitId(1);
|
||||
setGuideVisible(true);
|
||||
setGuideStep(0);
|
||||
} else {
|
||||
// 展品2:直接打开
|
||||
setActiveExhibitId(index);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听GuideSteps步骤变化,仅同步当前步骤
|
||||
const handleGuideStepChange = (step: number) => {
|
||||
setGuideStep(step);
|
||||
};
|
||||
|
||||
const progressCount = visited.size;
|
||||
const total = 2;
|
||||
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;
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`chenyun/place2/场景2`),
|
||||
}}
|
||||
>
|
||||
{/* 标题 */}
|
||||
<Title
|
||||
title="水乡少年·艰辛成长"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
/>
|
||||
|
||||
{/* 返回上一章 */}
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/place1`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 展品1 */}
|
||||
<Cursor
|
||||
className="absolute left-100 top-100"
|
||||
onClick={() => handleOpenExhibit(1)}
|
||||
read={visited.has(1)}
|
||||
/>
|
||||
{/* 展品2 */}
|
||||
<Cursor
|
||||
className="absolute left-210 top-100"
|
||||
onClick={() => handleOpenExhibit(2)}
|
||||
read={visited.has(2)}
|
||||
/>
|
||||
|
||||
{/* 继续前进 - 仅在完成所有展品后显示 */}
|
||||
{completed && (
|
||||
<Image
|
||||
className="absolute bottom-0 right-160"
|
||||
src={getStaticImage("chenyun/place2/继续前进2")}
|
||||
button
|
||||
onClick={() => {
|
||||
navigate(`/${moduleName}/${starName}/place3`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 遮罩层 */}
|
||||
<ExhibitOverlay
|
||||
visible={activeExhibitId !== null}
|
||||
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
|
||||
description={
|
||||
activeExhibitId ? exhibitDescriptionMap[activeExhibitId] : undefined
|
||||
}
|
||||
autoPlayDescription={activeExhibitId === 1}
|
||||
onClose={() => {
|
||||
if (activeExhibitId !== null) {
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(activeExhibitId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
setActiveExhibitId(null);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 展品1的引导对话 */}
|
||||
<GuideSteps
|
||||
visible={guideVisible}
|
||||
onVisibleChange={setGuideVisible}
|
||||
currentStep={guideStep}
|
||||
onStepChange={handleGuideStepChange}
|
||||
showMask={false}
|
||||
highlightSelectors={[]}
|
||||
zIndex={10000}
|
||||
>
|
||||
<GuideStepsItem speaker="student">
|
||||
那陈云爷爷是怎么有机会到图中的青浦县乙种商业学校读书的呀?
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
因为家贫,初小读完的陈云便辍学在家。后在舅母亲戚的资助下,陈云才到青浦县乙种商业学校学习珠算和记账的,他在很短时间里就基本掌握了珠算,还初步学会写账,这为陈云日后从事经济工作打下了基础。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
陈云在党内以“铁算盘”著称,他在经济工作中能熟练使用算盘,就得益于少年时在青浦县立乙种商业学校的学习。
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChenyunPlace2;
|
||||
@@ -0,0 +1,171 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
ExhibitOverlay,
|
||||
Title,
|
||||
Image,
|
||||
Cursor,
|
||||
GuideSteps,
|
||||
GuideStepsItem,
|
||||
Video,
|
||||
} from "@/components";
|
||||
import { getBackgroundImage, getStaticImage, getPublicPath } from "@/lib/utils";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
// starName 固定为 chenyun
|
||||
const starName = "chenyun";
|
||||
|
||||
// 展品 ID 与资源路径映射
|
||||
const exhibitSrcMap: Record<1 | 2, string> = {
|
||||
1: "chenyun/place3/场景3展品1",
|
||||
2: "chenyun/place3/场景3展品2",
|
||||
};
|
||||
const ChenyunPlace3 = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
const [visited, setVisited] = useState<Set<number>>(new Set());
|
||||
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | null>(null);
|
||||
const [videoVisible, setVideoVisible] = useState(false);
|
||||
// GuideSteps 显示状态
|
||||
const [guideVisible, setGuideVisible] = useState(false);
|
||||
// GuideSteps 当前步骤
|
||||
const [guideStep, setGuideStep] = useState(0);
|
||||
|
||||
const handleOpenExhibit = (index: 1 | 2) => {
|
||||
if (index === 1) {
|
||||
// 展品1:点击时同时打开展品和引导对话,从第1步开始
|
||||
setActiveExhibitId(1);
|
||||
setGuideVisible(true);
|
||||
setGuideStep(0);
|
||||
} else {
|
||||
// 展品2:先播放视频,结束后再打开展品
|
||||
setVideoVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听GuideSteps步骤变化,仅同步当前步骤
|
||||
const handleGuideStepChange = (step: number) => {
|
||||
setGuideStep(step);
|
||||
};
|
||||
|
||||
const progressCount = visited.size;
|
||||
const total = 2;
|
||||
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;
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`chenyun/place3/场景3`),
|
||||
}}
|
||||
>
|
||||
{/* 标题 商务启航·投身革命*/}
|
||||
<Title
|
||||
title="商务启航·投身革命"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
/>
|
||||
|
||||
{/* 返回上一章 */}
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/place2`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 展品1 */}
|
||||
<Cursor
|
||||
className="absolute right-200 bottom-75"
|
||||
onClick={() => handleOpenExhibit(1)}
|
||||
read={visited.has(1)}
|
||||
/>
|
||||
{/* 展品2 */}
|
||||
<Cursor
|
||||
className="absolute right-130 bottom-75"
|
||||
onClick={() => handleOpenExhibit(2)}
|
||||
read={visited.has(2)}
|
||||
/>
|
||||
|
||||
{/* 继续前进 - 仅在完成所有展品后显示 */}
|
||||
{completed && (
|
||||
<Image
|
||||
className="absolute bottom-0 right-0"
|
||||
src={getStaticImage("chenyun/place3/继续前进3")}
|
||||
button
|
||||
onClick={() => {
|
||||
navigate(`/${moduleName}/${starName}/place4`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 展品2视频播放遮罩(全屏) */}
|
||||
{videoVisible && (
|
||||
<div className="fixed inset-0 z-[11001] bg-black">
|
||||
<Video
|
||||
src={getPublicPath("video/chenyun/chenyun_04.mp4")}
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
controls={false}
|
||||
onEnded={() => {
|
||||
setVideoVisible(false);
|
||||
setVisited((p) => new Set(p).add(2));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 遮罩层 */}
|
||||
<ExhibitOverlay
|
||||
visible={activeExhibitId !== null}
|
||||
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
|
||||
onClose={() => {
|
||||
if (activeExhibitId !== null) {
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(activeExhibitId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
setActiveExhibitId(null);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 展品1的引导对话 */}
|
||||
<GuideSteps
|
||||
visible={guideVisible}
|
||||
onVisibleChange={setGuideVisible}
|
||||
currentStep={guideStep}
|
||||
onStepChange={handleGuideStepChange}
|
||||
showMask={false}
|
||||
highlightSelectors={[]}
|
||||
zIndex={10000}
|
||||
>
|
||||
<GuideStepsItem speaker="student">
|
||||
陈云14岁就去了上海商务印书馆当学徒,他主要负责哪些工作呢?
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
从颜安小学毕业后,陈云进入了商务印书馆当学徒,开始在柜台接待顾客,打珠算、写信封、练包装、理货物、修文具。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
陈云的珠算能力在商务印书馆工作期间有了很大提高。由于工作认真负责,三年的学徒期,陈云提前两年就破格到商务印书馆虹口分店(现为1925书局)当店员。
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChenyunPlace3;
|
||||
@@ -0,0 +1,187 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
ExhibitOverlay,
|
||||
Title,
|
||||
Image,
|
||||
Cursor,
|
||||
GuideSteps,
|
||||
GuideStepsItem,
|
||||
} from "@/components";
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
// starName 固定为 chenyun
|
||||
const starName = "chenyun";
|
||||
|
||||
// 展品 ID 与资源路径映射
|
||||
const exhibitSrcMap: Record<1 | 2, string> = {
|
||||
1: "chenyun/place4/场景4展品1",
|
||||
2: "chenyun/place4/场景4展品2",
|
||||
};
|
||||
const ChenyunPlace4 = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
const [visited, setVisited] = useState<Set<number>>(new Set());
|
||||
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | null>(null);
|
||||
// 展品1引导对话
|
||||
const [guideVisible, setGuideVisible] = useState(false);
|
||||
const [guideStep, setGuideStep] = useState(0);
|
||||
// 展品2的引导对话
|
||||
const [guideVisible2, setGuideVisible2] = useState(false);
|
||||
const [guideStep2, setGuideStep2] = useState(0);
|
||||
|
||||
const handleOpenExhibit = (index: 1 | 2) => {
|
||||
if (index === 1) {
|
||||
// 展品1:点击时同时打开展品和引导对话
|
||||
setActiveExhibitId(1);
|
||||
setGuideVisible(true);
|
||||
setGuideStep(0);
|
||||
} else {
|
||||
// 展品2:点击时同时打开展品和引导对话
|
||||
setActiveExhibitId(2);
|
||||
setGuideVisible2(true);
|
||||
setGuideStep2(0);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听GuideSteps步骤变化,仅同步当前步骤
|
||||
const handleGuideStepChange = (step: number) => {
|
||||
setGuideStep(step);
|
||||
};
|
||||
|
||||
const handleGuideStepChange2 = (step: number) => {
|
||||
setGuideStep2(step);
|
||||
};
|
||||
|
||||
const progressCount = visited.size;
|
||||
const total = 2;
|
||||
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;
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`chenyun/place4/场景4`),
|
||||
}}
|
||||
>
|
||||
{/* 标题 */}
|
||||
<Title
|
||||
title="苏区淬炼·崭露头角"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
/>
|
||||
|
||||
{/* 返回上一章 */}
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/place3`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 展品1 */}
|
||||
<Cursor
|
||||
className="absolute left-180 top-120"
|
||||
onClick={() => handleOpenExhibit(1)}
|
||||
read={visited.has(1)}
|
||||
/>
|
||||
{/* 展品2 */}
|
||||
<Cursor
|
||||
className="absolute right-2 top-100"
|
||||
onClick={() => handleOpenExhibit(2)}
|
||||
read={visited.has(2)}
|
||||
/>
|
||||
|
||||
{/* 继续前进 - 仅在完成所有展品后显示 */}
|
||||
{completed && (
|
||||
<Image
|
||||
className="absolute bottom-0 right-0"
|
||||
src={getStaticImage("chenyun/place3/继续前进3")}
|
||||
button
|
||||
onClick={() => {
|
||||
navigate(`/${moduleName}/${starName}/place5`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 遮罩层 */}
|
||||
<ExhibitOverlay
|
||||
visible={activeExhibitId !== null}
|
||||
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
|
||||
onClose={() => {
|
||||
if (activeExhibitId !== null) {
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(activeExhibitId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
setActiveExhibitId(null);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 展品1的引导对话 */}
|
||||
<GuideSteps
|
||||
visible={guideVisible}
|
||||
onVisibleChange={setGuideVisible}
|
||||
currentStep={guideStep}
|
||||
onStepChange={handleGuideStepChange}
|
||||
showMask={false}
|
||||
highlightSelectors={[]}
|
||||
zIndex={10000}
|
||||
>
|
||||
<GuideStepsItem speaker="student">
|
||||
这组史料看起来陈云爷爷前往中央苏区了,他在中央苏区主要负责哪些工作呢?
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
1932年3月,陈云担任中共临时中央政治局常委,兼任全国总工会党团书记。1933年1月,陈云离开上海前往中央苏区首府瑞金,继续参加中共中央和全国总工会的领导工作。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
虽然陈云在中央苏区的任务主要是领导工人运动,但是陈云总是能针对一些棘手的经济问题提出较出色的建议。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
在中央苏区时期,陈云也关注和积极参与解决了许多经济难题。1934年,在中共六届五中全会上,陈云被选为中央政治局委员、常委,兼任中央白区工作部部长。
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
|
||||
{/* 展品2的引导对话 */}
|
||||
<GuideSteps
|
||||
visible={guideVisible2}
|
||||
onVisibleChange={setGuideVisible2}
|
||||
currentStep={guideStep2}
|
||||
onStepChange={handleGuideStepChange2}
|
||||
showMask={false}
|
||||
highlightSelectors={[]}
|
||||
zIndex={10000}
|
||||
>
|
||||
<GuideStepsItem speaker="student">
|
||||
苏区的经济工作一定不简单,陈云是如何开展工作的呢?
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
中央苏区时期的经济斗争,是陈云经济工作能力的“淬炼场”。陈云分析了“左”倾经济政策的表现、危害以及原因,他强调,当务之急是重新严格审查各个企业、工厂以及商店、大小作坊的实际承受力和经济支付能力,
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
从实际出发,实事求是地制定符合中央苏维埃区域每一个工厂、企业、作坊发展需求的劳动政策。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
陈云提议建立合作社,广泛吸收失业工人加入,多听取工人建议、增强企业管理的民主性,从而更好地建设苏区经济。这些做法为他日后主持全国财经工作积累了宝贵经验。
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChenyunPlace4;
|
||||
@@ -0,0 +1,159 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
ExhibitOverlay,
|
||||
Title,
|
||||
Image,
|
||||
Cursor,
|
||||
Video,
|
||||
Overlay,
|
||||
} from "@/components";
|
||||
import { getBackgroundImage, getStaticImage, getPublicPath } from "@/lib/utils";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
// starName 固定为 chenyun
|
||||
const starName = "chenyun";
|
||||
|
||||
// 展品 ID 与资源路径映射
|
||||
const exhibitSrcMap: Record<1 | 2 | 3, string> = {
|
||||
1: "chenyun/place5/场景5展品1",
|
||||
2: "chenyun/place5/场景5展品2",
|
||||
3: "chenyun/place5/场景5展品3",
|
||||
};
|
||||
// 展品描述映射
|
||||
const exhibitDescriptionMap: Record<1 | 2 | 3, string> = {
|
||||
1: "",
|
||||
2: "延安时期因为国民党的封锁,陕甘宁边区经济困难。陈云转战陕甘宁边区主持财经工作,他遇到了很多经济难题。1944年新春伊始,陈云开始担任西北财经办事处副主任,工作转到财经方面,这是陈云以主要精力从事领导经济工作的开始。当时,陕甘宁边区在国民党严密封锁下,财经状况严峻,边区物价猛涨,老百姓日常生活必需的布匹和粮食十分缺乏。国民党顽固派封锁边区,企图“不让一粒粮、一尺布进入边区”,同时千方百计阻止边区食盐出口。",
|
||||
3: "",
|
||||
};
|
||||
const ChenyunPlace5 = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
const [visited, setVisited] = useState<Set<number>>(new Set());
|
||||
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | 3 | null>(
|
||||
null
|
||||
);
|
||||
const [videoVisible3, setVideoVisible3] = useState(false);
|
||||
|
||||
const handleOpenExhibit = (index: 1 | 2 | 3) => {
|
||||
if (index === 3) {
|
||||
// 展品3:播放视频,不打开展品
|
||||
setVideoVisible3(true);
|
||||
return;
|
||||
}
|
||||
setActiveExhibitId(index);
|
||||
};
|
||||
|
||||
const progressCount = visited.size;
|
||||
const total = 3;
|
||||
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;
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`chenyun/place5/场景5`),
|
||||
}}
|
||||
>
|
||||
{/* 标题 */}
|
||||
<Title
|
||||
title="延安担当·经济破局"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
/>
|
||||
|
||||
{/* 返回上一章 */}
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/place4`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 展品1 */}
|
||||
<Cursor
|
||||
className="absolute left-30 top-70"
|
||||
onClick={() => handleOpenExhibit(1)}
|
||||
read={visited.has(1)}
|
||||
/>
|
||||
{/* 展品2 */}
|
||||
<Cursor
|
||||
className="absolute left-107 top-72"
|
||||
onClick={() => handleOpenExhibit(2)}
|
||||
read={visited.has(2)}
|
||||
/>
|
||||
|
||||
{/* 展品3 */}
|
||||
<Cursor
|
||||
className="absolute right-170 top-84"
|
||||
onClick={() => handleOpenExhibit(3)}
|
||||
read={visited.has(3)}
|
||||
/>
|
||||
|
||||
{/* 继续前进 - 仅在完成所有展品后显示 */}
|
||||
{completed && (
|
||||
<Image
|
||||
className="absolute bottom-5 right-20"
|
||||
src={getStaticImage("chenyun/place5/继续前进4")}
|
||||
button
|
||||
onClick={() => {
|
||||
navigate(`/${moduleName}/${starName}/place6`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 展品3视频播放(全屏) */}
|
||||
{videoVisible3 && (
|
||||
<Video
|
||||
src={getPublicPath("video/chenyun/chenyun_07.mp4")}
|
||||
className="w-full h-full"
|
||||
autoPlay
|
||||
playsInline
|
||||
showSkipButton
|
||||
onEnded={() => {
|
||||
setVideoVisible3(false);
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(3);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 遮罩层 */}
|
||||
<ExhibitOverlay
|
||||
visible={activeExhibitId !== null}
|
||||
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
|
||||
description={
|
||||
activeExhibitId ? exhibitDescriptionMap[activeExhibitId] : undefined
|
||||
}
|
||||
autoPlayDescription={activeExhibitId === 2}
|
||||
onClose={() => {
|
||||
if (activeExhibitId !== null) {
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(activeExhibitId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
setActiveExhibitId(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChenyunPlace5;
|
||||
@@ -0,0 +1,137 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
ExhibitOverlay,
|
||||
Title,
|
||||
Image,
|
||||
Cursor,
|
||||
GuideSteps,
|
||||
GuideStepsItem,
|
||||
} from "@/components";
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
// starName 固定为 chenyun
|
||||
const starName = "chenyun";
|
||||
|
||||
// 展品 ID 与资源路径映射
|
||||
const exhibitSrcMap: Record<1, string> = {
|
||||
1: "chenyun/place6/场景6展品1",
|
||||
};
|
||||
const ChenyunPlace6 = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
const [visited, setVisited] = useState<Set<number>>(new Set());
|
||||
const [activeExhibitId, setActiveExhibitId] = useState<1 | null>(null);
|
||||
const [guideVisible, setGuideVisible] = useState(false);
|
||||
const [guideStep, setGuideStep] = useState(0);
|
||||
|
||||
const handleOpenExhibit = () => {
|
||||
// 展品1:点击时同时打开展品和引导对话,从第1步开始
|
||||
setActiveExhibitId(1);
|
||||
setGuideVisible(true);
|
||||
setGuideStep(0);
|
||||
};
|
||||
|
||||
const handleGuideStepChange = (step: number) => {
|
||||
setGuideStep(step);
|
||||
};
|
||||
|
||||
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;
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`chenyun/place6/场景6`),
|
||||
}}
|
||||
>
|
||||
{/* 标题 */}
|
||||
<Title
|
||||
title="恢复时期·经济奠基"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
/>
|
||||
|
||||
{/* 返回上一章 */}
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/place5`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 展品1 */}
|
||||
<Cursor
|
||||
className="absolute left-230 bottom-80"
|
||||
onClick={handleOpenExhibit}
|
||||
read={visited.has(1)}
|
||||
/>
|
||||
|
||||
{/* 继续前进 - 仅在完成所有展品后显示 */}
|
||||
{completed && (
|
||||
<Image
|
||||
className="absolute bottom-0 right-0"
|
||||
src={getStaticImage("chenyun/place6/继续前进5")}
|
||||
button
|
||||
onClick={() => {
|
||||
navigate(`/${moduleName}/${starName}/place7`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 遮罩层 */}
|
||||
<ExhibitOverlay
|
||||
visible={activeExhibitId !== null}
|
||||
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
|
||||
onClose={() => {
|
||||
if (activeExhibitId !== null) {
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(activeExhibitId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
setActiveExhibitId(null);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 展品1的引导对话 */}
|
||||
<GuideSteps
|
||||
visible={guideVisible}
|
||||
onVisibleChange={setGuideVisible}
|
||||
currentStep={guideStep}
|
||||
onStepChange={handleGuideStepChange}
|
||||
showMask={false}
|
||||
highlightSelectors={[]}
|
||||
zIndex={10000}
|
||||
>
|
||||
<GuideStepsItem speaker="student">
|
||||
这是中央人民政府院财政经济委员会的印章,到这个时候陈云已经从地方到中央工作了吗?
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
是的,新中国刚成立时,中国共产党接管的是一个民穷财尽、物价飞涨的烂摊子。面对财政经济上的严重困难,陈云在1949年受命担任新成立的中央财政经济委员会(简称中财委)主任,主持领导全国财政经济工作。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
陈云挂帅后的首仗就是打击投机、平抑物价。通过“银元之战”“两白一黑”战役等几次严峻斗争,只用不到一年时间,就迅速稳定了金融物价。
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChenyunPlace6;
|
||||
@@ -0,0 +1,382 @@
|
||||
import { useState, useRef } from "react";
|
||||
import {
|
||||
ExhibitOverlay,
|
||||
Title,
|
||||
Image,
|
||||
Cursor,
|
||||
Video,
|
||||
GuideSteps,
|
||||
GuideStepsItem,
|
||||
type SingleChoiceOption,
|
||||
type DragFillResult,
|
||||
QuizPanel,
|
||||
} from "@/components";
|
||||
import { SuccessAlert, DragQuizPanel } from "@/components/layout";
|
||||
import { getBackgroundImage, getStaticImage, getPublicPath } from "@/lib/utils";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import type { VideoRef } from "@/components/video";
|
||||
|
||||
// starName 固定为 chenyun
|
||||
const starName = "chenyun";
|
||||
|
||||
// 展品 ID 与资源路径映射
|
||||
const exhibitSrcMap: Record<1, string> = {
|
||||
1: "chenyun/place5/场景5展品1",
|
||||
};
|
||||
|
||||
// 题目1选项
|
||||
const QUIZ1_OPTIONS: SingleChoiceOption[] = [
|
||||
{
|
||||
value: "A",
|
||||
label:
|
||||
"A. 推动了淮海战役的胜利(错误选项,选后提示:淮海战役于1949年1月胜利结束,发生在这次“银元之战”前,不存在因果关系)",
|
||||
},
|
||||
{
|
||||
value: "B",
|
||||
label:
|
||||
"B. 有利于稳定人民的发行和地位(正确选项,选后提示:“银元之战”属于当时人民政府整顿金融的重要举措,黄金、银元投机不利于人民币币值稳定和流通,因此“银元之战”打击贵属投机活动,有利于稳定人民币的发行和地位)",
|
||||
},
|
||||
{
|
||||
value: "C",
|
||||
label:
|
||||
"C. 巩固了新中国人民政权(错误选项,选后提示:1949年6月新中国政权尚未建立)",
|
||||
},
|
||||
];
|
||||
|
||||
const ChenyunPlace7 = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
const [visited, setVisited] = useState<Set<number>>(new Set());
|
||||
const [activeExhibitId, setActiveExhibitId] = useState<1 | null>(null);
|
||||
const [videoVisible1, setVideoVisible1] = useState(false);
|
||||
const [videoPlaying, setVideoPlaying] = useState(false);
|
||||
const videoRef = useRef<VideoRef>(null);
|
||||
// 单选题
|
||||
const [quizVisible, setQuizVisible] = useState(false);
|
||||
const [quizAnswer, setQuizAnswer] = useState<string>("");
|
||||
const [quizError, setQuizError] = useState<string>("");
|
||||
// 展品1引导对话
|
||||
const [guideVisible, setGuideVisible] = useState(false);
|
||||
const [guideStep, setGuideStep] = useState(0);
|
||||
// 第二阶段:对话2 + 视频2 + 题目2
|
||||
const [videoVisible2, setVideoVisible2] = useState(false);
|
||||
const [videoPlaying2, setVideoPlaying2] = useState(false);
|
||||
const videoRef2 = useRef<VideoRef>(null);
|
||||
const [guideVisible2, setGuideVisible2] = useState(false);
|
||||
const [guideStep2, setGuideStep2] = useState(0);
|
||||
const [quiz2Visible, setQuiz2Visible] = useState(false);
|
||||
const [quiz2Error, setQuiz2Error] = useState<string>("");
|
||||
const [quiz2Value, setQuiz2Value] = useState<DragFillResult[]>([]);
|
||||
// 成功提示
|
||||
const [successAlertVisible, setSuccessAlertVisible] = useState(false);
|
||||
// 标记拖拽题是否已完成
|
||||
const [quiz2Completed, setQuiz2Completed] = useState(false);
|
||||
|
||||
const handleOpenExhibit = (_index: 1) => {
|
||||
// 展品1:点击时显示视频1场景和引导对话1
|
||||
setVideoVisible1(true);
|
||||
setVideoPlaying(false); // 视频不自动播放
|
||||
setGuideVisible(true);
|
||||
setGuideStep(0);
|
||||
};
|
||||
|
||||
// 监听GuideSteps步骤变化
|
||||
const handleGuideStepChange = (step: number) => {
|
||||
setGuideStep(step);
|
||||
};
|
||||
|
||||
// 监听对话引导完成
|
||||
const handleGuideVisibleChange = (visible: boolean) => {
|
||||
setGuideVisible(visible);
|
||||
// 对话引导完成后,开始播放视频
|
||||
if (!visible && videoVisible1) {
|
||||
setVideoPlaying(true);
|
||||
videoRef.current?.play();
|
||||
}
|
||||
};
|
||||
|
||||
// 对话2步骤变化
|
||||
const handleGuideStepChange2 = (step: number) => {
|
||||
setGuideStep2(step);
|
||||
};
|
||||
|
||||
// 对话2完成 → 播放视频2
|
||||
const handleGuideVisibleChange2 = (visible: boolean) => {
|
||||
setGuideVisible2(visible);
|
||||
if (!visible && videoVisible2) {
|
||||
setVideoPlaying2(true);
|
||||
videoRef2.current?.play();
|
||||
}
|
||||
};
|
||||
|
||||
// 拖拽题配置
|
||||
const QUIZ2_BLANKS = [
|
||||
{ id: "blank-1", label: "____" },
|
||||
{ id: "blank-2", label: "____" },
|
||||
{ id: "blank-3", label: "____" },
|
||||
];
|
||||
const QUIZ2_OPTIONS = [
|
||||
{ value: "棉纱", label: "棉纱" },
|
||||
{ value: "大米", label: "大米" },
|
||||
{ value: "煤炭", label: "煤炭" },
|
||||
{ value: "石油", label: "石油" },
|
||||
];
|
||||
const QUIZ2_ANSWER = ["棉纱", "大米", "煤炭"];
|
||||
|
||||
const handleQuiz2Change = (val: DragFillResult[]) => {
|
||||
setQuiz2Value(val);
|
||||
setQuiz2Error("");
|
||||
if (val.length < QUIZ2_ANSWER.length) return;
|
||||
// 获取所有已填入的答案(不要求顺序)
|
||||
const filledAnswers = val.map((v) => v.optionValue);
|
||||
// 检查是否包含了所有正确答案(使用 Set 比较,不要求顺序)
|
||||
const answerSet = new Set(QUIZ2_ANSWER);
|
||||
const filledSet = new Set(filledAnswers);
|
||||
const isCorrect =
|
||||
answerSet.size === filledSet.size &&
|
||||
[...answerSet].every((answer) => filledSet.has(answer));
|
||||
if (isCorrect) {
|
||||
setQuiz2Visible(false);
|
||||
setVideoVisible1(false);
|
||||
setVideoPlaying(false);
|
||||
setVideoVisible2(false);
|
||||
setVideoPlaying2(false);
|
||||
// 标记拖拽题已完成
|
||||
setQuiz2Completed(true);
|
||||
// 显示成功提示
|
||||
setSuccessAlertVisible(true);
|
||||
} else {
|
||||
setQuiz2Error("再想一想,石油当时不是主要调控对象~");
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`chenyun/place7/场景7`),
|
||||
}}
|
||||
>
|
||||
{/* 标题 */}
|
||||
<Title
|
||||
title="恢复时期·经济奠基"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
/>
|
||||
|
||||
{/* 返回上一章 */}
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/place6`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 展品1 */}
|
||||
<Cursor
|
||||
className="absolute left-230 top-88"
|
||||
onClick={() => handleOpenExhibit(1)}
|
||||
read={visited.has(1)}
|
||||
/>
|
||||
|
||||
{/* 继续前进 - 仅在完成所有展品后显示 */}
|
||||
{completed && (
|
||||
<Image
|
||||
className="absolute bottom-0 right-0"
|
||||
src={getStaticImage("chenyun/place7/继续前进")}
|
||||
button
|
||||
onClick={() => {
|
||||
navigate(`/${moduleName}/${starName}/place8`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 展品1视频播放(全屏) */}
|
||||
{videoVisible1 && (
|
||||
<div className="fixed inset-0 z-[9999] bg-black">
|
||||
<Video
|
||||
ref={videoRef}
|
||||
src={getPublicPath("video/chenyun/chenyun_10.mp4")}
|
||||
className="w-full h-full"
|
||||
autoPlay={videoPlaying}
|
||||
playsInline
|
||||
preload="auto"
|
||||
muted={!videoPlaying}
|
||||
showSkipButton
|
||||
onEnded={() => {
|
||||
setVideoPlaying(false);
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(1);
|
||||
return next;
|
||||
});
|
||||
// 视频播放完成后,出现单选题
|
||||
setQuizVisible(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 展品1的引导对话 - 在视频上方显示 */}
|
||||
<GuideSteps
|
||||
visible={guideVisible}
|
||||
onVisibleChange={handleGuideVisibleChange}
|
||||
currentStep={guideStep}
|
||||
onStepChange={handleGuideStepChange}
|
||||
showMask={false}
|
||||
highlightSelectors={[]}
|
||||
zIndex={10001}
|
||||
>
|
||||
<GuideStepsItem speaker="student">
|
||||
那"银元之战"是如何取得胜利的呢?
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
由于通货膨胀,国民党发行的法币、金圆券变成了一堆废纸。当时坊间流传"大街过三道,物价跳三跳"。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
有人计算,一百元法币,在一九三七年可买两头牛,一九四五年只能买两个鸡蛋,一九四七年只能买一个煤球,到一九四九年五月连一粒米也买不到。可见,当时通货膨胀多么严重。
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
|
||||
{/* 单选题1:在视频1结束后出现(点击即判定) */}
|
||||
<QuizPanel
|
||||
visible={quizVisible}
|
||||
zIndex={10002}
|
||||
title="题目1(单选题):1949年6月,中央央指导打击银元投机活动,金条、银元一律由人民银行挂牌收兑,铁路交通事业、税收一律收入人民币。同时,上海军管会查封了证券大楼。这次“银元之战”()"
|
||||
options={QUIZ1_OPTIONS}
|
||||
value={quizAnswer}
|
||||
error={quizError}
|
||||
onChange={(val) => {
|
||||
setQuizAnswer(val);
|
||||
if (val === "B") {
|
||||
setQuizVisible(false);
|
||||
setQuizError("");
|
||||
// 显示成功提示
|
||||
setSuccessAlertVisible(true);
|
||||
} else {
|
||||
setQuizError("再想一想,注意题干中的时间和提示信息哦~");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 视频2播放(全屏) */}
|
||||
{videoVisible2 && (
|
||||
<div className="fixed inset-0 z-[10010] bg-black">
|
||||
<Video
|
||||
ref={videoRef2}
|
||||
src={getPublicPath("video/chenyun/chenyun_11.mp4")}
|
||||
className="w-full h-full"
|
||||
autoPlay={videoPlaying2}
|
||||
playsInline
|
||||
preload="auto"
|
||||
muted={!videoPlaying2}
|
||||
showSkipButton
|
||||
skipText="跳过"
|
||||
onEnded={() => {
|
||||
setVideoPlaying2(false);
|
||||
// 视频2结束后,停在最后一帧并出现题目2
|
||||
setQuiz2Visible(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 对话2:在视频2背景上显示 */}
|
||||
<GuideSteps
|
||||
visible={guideVisible2}
|
||||
onVisibleChange={handleGuideVisibleChange2}
|
||||
currentStep={guideStep2}
|
||||
onStepChange={handleGuideStepChange2}
|
||||
showMask={false}
|
||||
highlightSelectors={[]}
|
||||
zIndex={10011}
|
||||
>
|
||||
<GuideStepsItem speaker="student">
|
||||
那“两白一黑”战役是如何打赢的呢?
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
“银元之战”的胜利并没有遏制住投机者的嚣张气焰,物价稳定仅十几天后,投机者就由银元投机转向“两白一黑”的投机,即:棉纱、大米和煤炭,引发了又一次全国性涨价狂潮。
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
|
||||
{/* 拖拽题2:在视频2结束后出现 */}
|
||||
<DragQuizPanel
|
||||
visible={quiz2Visible}
|
||||
zIndex={10012}
|
||||
title="题目2(拖拽)"
|
||||
questionPrefix='新中国成立后,为了稳定物价,中央政府组织了"两白一黑"的集中调运,"两白一黑"指的是('
|
||||
questionSuffix=")。"
|
||||
options={QUIZ2_OPTIONS}
|
||||
blanks={QUIZ2_BLANKS}
|
||||
value={quiz2Value}
|
||||
error={quiz2Error}
|
||||
onChange={handleQuiz2Change}
|
||||
/>
|
||||
|
||||
{/* 遮罩层 */}
|
||||
<ExhibitOverlay
|
||||
visible={activeExhibitId !== null}
|
||||
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
|
||||
onClose={() => {
|
||||
if (activeExhibitId !== null) {
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(activeExhibitId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
setActiveExhibitId(null);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 成功提示 */}
|
||||
{successAlertVisible && (
|
||||
<div className="fixed inset-0" style={{ zIndex: 20000 }}>
|
||||
<SuccessAlert
|
||||
visible={successAlertVisible}
|
||||
title="恭喜你, 选择正确!"
|
||||
onClose={() => {
|
||||
setSuccessAlertVisible(false);
|
||||
// 如果是单选题答对且拖拽题还没开始,继续进入第二阶段
|
||||
if (quizAnswer === "B" && !quiz2Visible && !quiz2Completed) {
|
||||
setVideoVisible2(true);
|
||||
setVideoPlaying2(false);
|
||||
setGuideVisible2(true);
|
||||
setGuideStep2(0);
|
||||
}
|
||||
}}
|
||||
onContinue={() => {
|
||||
setSuccessAlertVisible(false);
|
||||
// 如果是单选题答对且拖拽题还没开始,继续进入第二阶段
|
||||
if (quizAnswer === "B" && !quiz2Visible && !quiz2Completed) {
|
||||
setVideoVisible2(true);
|
||||
setVideoPlaying2(false);
|
||||
setGuideVisible2(true);
|
||||
setGuideStep2(0);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChenyunPlace7;
|
||||
@@ -0,0 +1,158 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
ExhibitOverlay,
|
||||
Title,
|
||||
Image,
|
||||
Cursor,
|
||||
GuideSteps,
|
||||
GuideStepsItem,
|
||||
} from "@/components";
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
// starName 固定为 chenyun
|
||||
const starName = "chenyun";
|
||||
|
||||
// 展品 ID 与资源路径映射
|
||||
const exhibitSrcMap: Record<1 | 2, string> = {
|
||||
1: "chenyun/place8/场景8展品1",
|
||||
2: "chenyun/place8/场景8展品2",
|
||||
};
|
||||
const ChenyunPlace8 = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
const [visited, setVisited] = useState<Set<number>>(new Set());
|
||||
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | null>(null);
|
||||
// 展品2的对话引导
|
||||
const [guideVisible, setGuideVisible] = useState(false);
|
||||
const [guideStep, setGuideStep] = useState(0);
|
||||
|
||||
const handleOpenExhibit = (index: 1 | 2) => {
|
||||
setActiveExhibitId(index);
|
||||
// 如果是展品2,显示对话引导
|
||||
if (index === 2) {
|
||||
setGuideVisible(true);
|
||||
setGuideStep(0);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听对话引导步骤变化
|
||||
const handleGuideStepChange = (step: number) => {
|
||||
setGuideStep(step);
|
||||
};
|
||||
|
||||
// 监听对话引导完成
|
||||
const handleGuideVisibleChange = (visible: boolean) => {
|
||||
setGuideVisible(visible);
|
||||
};
|
||||
|
||||
const progressCount = visited.size;
|
||||
const total = 2;
|
||||
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;
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`chenyun/place8/场景8`),
|
||||
}}
|
||||
>
|
||||
{/* 标题 */}
|
||||
<Title
|
||||
title="建设时期·务实兴邦"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
/>
|
||||
|
||||
{/* 返回上一章 */}
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/place7`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 展品1 */}
|
||||
<Cursor
|
||||
className="absolute right-175 top-75"
|
||||
onClick={() => handleOpenExhibit(1)}
|
||||
read={visited.has(1)}
|
||||
/>
|
||||
{/* 展品2 */}
|
||||
<Cursor
|
||||
className="absolute right-100 bottom-55"
|
||||
onClick={() => handleOpenExhibit(2)}
|
||||
read={visited.has(2)}
|
||||
/>
|
||||
|
||||
{/* 继续前进 - 仅在完成所有展品后显示 */}
|
||||
{completed && (
|
||||
<Image
|
||||
className="absolute bottom-0 right-150"
|
||||
src={getStaticImage("chenyun/place8/继续前进6")}
|
||||
button
|
||||
onClick={() => {
|
||||
navigate(`/${moduleName}/${starName}/place9`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 遮罩层 */}
|
||||
<ExhibitOverlay
|
||||
visible={activeExhibitId !== null}
|
||||
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
|
||||
description={
|
||||
activeExhibitId === 1
|
||||
? `打赢"银元之战""两白一黑"战役的过程彰显了中国共产党治理经济的卓越能力!这里有一份"一五"计划报告的手稿,陈云在"一五"计划的编制中做出了重大贡献。一九五四年,陈云的主要工作之一,就是主持编制第一个五年计划纲要。这次编制工作是在前四次基础上进行的,陈云提出计划编制工作中要注意几个问题:首先是工业投资金额,其次是市场供应和人民生活,第三是农业发展速度,这三个问题对编制计划有指导意义。第一个五年计划纲要草案的重点是优先发展重工业,特别是156项重点工程。当时陈云重点抓重工业建设和粮食保障,通过"统购统销"稳定粮价,为重点工程项目筑牢根基,亲手铺就了我国工业化的起步之路。`
|
||||
: undefined
|
||||
}
|
||||
onClose={() => {
|
||||
if (activeExhibitId !== null) {
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(activeExhibitId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
setActiveExhibitId(null);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 展品2的对话引导 - 在展品遮罩层上方显示 */}
|
||||
<GuideSteps
|
||||
visible={guideVisible}
|
||||
onVisibleChange={handleGuideVisibleChange}
|
||||
currentStep={guideStep}
|
||||
onStepChange={handleGuideStepChange}
|
||||
showMask={false}
|
||||
highlightSelectors={[]}
|
||||
zIndex={10001}
|
||||
>
|
||||
<GuideStepsItem speaker="student">
|
||||
您讲的"统购统销"是不是和这里我们看到的粮票有直接关系啊?
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
不错,随着大规模经济建设的展开,全国粮、棉、油等主要农产品严重紧缺,供求矛盾尖锐。陈云经过调查研究,反复比较,权衡利弊得失,终于下决心在农村征购,在城市配售的解决粮食问题的办法,也就是后来所说的"统购统销"。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
陈云说:"只有采用这种办法,才能保证我国人民生活日益增长的需要,才能制止投机活动,保证市场物价的稳定,才能使发展国民经济第一个五年计划得以顺利地进行。"
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChenyunPlace8;
|
||||
@@ -0,0 +1,177 @@
|
||||
import { useState, useRef } from "react";
|
||||
import {
|
||||
ExhibitOverlay,
|
||||
Title,
|
||||
Image,
|
||||
Cursor,
|
||||
Video,
|
||||
GuideSteps,
|
||||
GuideStepsItem,
|
||||
} from "@/components";
|
||||
import { getBackgroundImage, getStaticImage, getPublicPath } from "@/lib/utils";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import type { VideoRef } from "@/components/video";
|
||||
|
||||
// starName 固定为 chenyun
|
||||
const starName = "chenyun";
|
||||
|
||||
// 展品 ID 与资源路径映射
|
||||
const exhibitSrcMap: Record<1, string> = {
|
||||
1: "chenyun/place9/场景9展品1",
|
||||
};
|
||||
const ChenyunPlace9 = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
const [visited, setVisited] = useState<Set<number>>(new Set());
|
||||
const [activeExhibitId, setActiveExhibitId] = useState<1 | null>(null);
|
||||
// 视频播放状态
|
||||
const [videoVisible, setVideoVisible] = useState(false);
|
||||
const [videoPlaying, setVideoPlaying] = useState(false);
|
||||
const videoRef = useRef<VideoRef>(null);
|
||||
// 对话引导状态
|
||||
const [guideVisible, setGuideVisible] = useState(false);
|
||||
const [guideStep, setGuideStep] = useState(0);
|
||||
|
||||
const handleOpenExhibit = (_index: 1) => {
|
||||
// 展品1:播放视频
|
||||
setVideoVisible(true);
|
||||
setVideoPlaying(true);
|
||||
};
|
||||
|
||||
// 监听对话引导步骤变化
|
||||
const handleGuideStepChange = (step: number) => {
|
||||
setGuideStep(step);
|
||||
};
|
||||
|
||||
// 监听对话引导完成
|
||||
const handleGuideVisibleChange = (visible: boolean) => {
|
||||
setGuideVisible(visible);
|
||||
if (!visible && videoVisible) {
|
||||
setVideoVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`chenyun/place9/场景9`),
|
||||
}}
|
||||
>
|
||||
{/* 标题 */}
|
||||
<Title
|
||||
title="改革时期·擘画实业"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
/>
|
||||
|
||||
{/* 返回上一章 */}
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/place8`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 展品1 */}
|
||||
<Cursor
|
||||
className="absolute right-26 top-88"
|
||||
onClick={() => handleOpenExhibit(1)}
|
||||
read={visited.has(1)}
|
||||
/>
|
||||
|
||||
{/* 离开场景 - 仅在完成所有展品后显示 */}
|
||||
{completed && (
|
||||
<Image
|
||||
className="absolute bottom-0 right-0"
|
||||
src={getStaticImage("chenyun/place9/离开场景")}
|
||||
button
|
||||
onClick={() => {
|
||||
navigate(`/${moduleName}/${starName}/home`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 展品1视频播放(全屏) */}
|
||||
{videoVisible && (
|
||||
<div className="fixed inset-0 z-[9999] bg-black">
|
||||
<Video
|
||||
ref={videoRef}
|
||||
src={getPublicPath("video/chenyun/chenyun_14.mp4")}
|
||||
className="w-full h-full"
|
||||
autoPlay={videoPlaying}
|
||||
playsInline
|
||||
preload="auto"
|
||||
muted={!videoPlaying}
|
||||
showSkipButton
|
||||
skipText="跳过"
|
||||
onEnded={() => {
|
||||
setVideoPlaying(false);
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(1);
|
||||
return next;
|
||||
});
|
||||
// 视频播放完成后,显示对话引导
|
||||
setGuideVisible(true);
|
||||
setGuideStep(0);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 视频播放完成后的对话引导 */}
|
||||
<GuideSteps
|
||||
visible={guideVisible}
|
||||
onVisibleChange={handleGuideVisibleChange}
|
||||
currentStep={guideStep}
|
||||
onStepChange={handleGuideStepChange}
|
||||
showMask={false}
|
||||
highlightSelectors={[]}
|
||||
zIndex={10001}
|
||||
>
|
||||
<GuideStepsItem speaker="guide">
|
||||
我们已经沿着陈云的足迹,从练塘水乡走到改革开放的前沿,读懂了他"适合老百姓的要求,做了一点事"的初心。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
接下来,请你跨越时空,走进1949年刚解放时期的上海,参与到当时那场轰轰烈烈的"银元之战"和"米棉之战"之中去,在实践决策中体会,陈云"多谋善断""敢闯务实"的卓越智慧与战略魄力,感受我国集中力量办大事的制度优势如何在经济战场中彰显威力。
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
|
||||
{/* 遮罩层 */}
|
||||
<ExhibitOverlay
|
||||
visible={activeExhibitId !== null}
|
||||
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
|
||||
onClose={() => {
|
||||
if (activeExhibitId !== null) {
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(activeExhibitId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
setActiveExhibitId(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChenyunPlace9;
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,150 @@
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { Image } from "@/components";
|
||||
//英文命名 "革命先驱" | "文化名人" | "艺术名家" | "经济巨擘"
|
||||
type ModuleName = "revolutionary" | "cultural" | "artistic" | "economic";
|
||||
|
||||
interface Star {
|
||||
image: string;
|
||||
name: string;
|
||||
starName: string;
|
||||
}
|
||||
|
||||
interface EntranceBackground {
|
||||
backgroundImage: string;
|
||||
//Star1
|
||||
Star: {
|
||||
Star1: Star;
|
||||
Star2: Star;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 故居入口页
|
||||
* 根据模块名称显示不同的背景图
|
||||
* 根据模块名称显示不同的进入按钮
|
||||
* 革命先驱:毛泽东、孙中山
|
||||
* 文化名人:鲁迅、黄炎培
|
||||
* 艺术名家:聂耳、刘海栗
|
||||
* 经济巨擘:荣德生、陈云
|
||||
*
|
||||
*/
|
||||
const EntranceBackgroundMap: Record<ModuleName, EntranceBackground> = {
|
||||
revolutionary: {
|
||||
backgroundImage: getBackgroundImage("entrance/革命先驱"),
|
||||
Star: {
|
||||
Star1: {
|
||||
image: getStaticImage("entrance/毛泽东故居入口"),
|
||||
name: "毛泽东",
|
||||
starName: "maozedong",
|
||||
},
|
||||
Star2: {
|
||||
image: getStaticImage("entrance/孙中山故居入口-解锁"),
|
||||
name: "孙中山",
|
||||
starName: "sunzhongshan",
|
||||
},
|
||||
},
|
||||
},
|
||||
cultural: {
|
||||
backgroundImage: getBackgroundImage("entrance/文化名人"),
|
||||
Star: {
|
||||
Star1: {
|
||||
image: getStaticImage("entrance/鲁迅故居-解锁"),
|
||||
name: "鲁迅",
|
||||
starName: "luxun",
|
||||
},
|
||||
Star2: {
|
||||
image: getStaticImage("entrance/黄元培-解锁"),
|
||||
name: "黄炎培",
|
||||
starName: "huangyanpei",
|
||||
},
|
||||
},
|
||||
},
|
||||
artistic: {
|
||||
backgroundImage: getBackgroundImage("entrance/艺术名家"),
|
||||
Star: {
|
||||
Star1: {
|
||||
image: "",
|
||||
name: "聂耳",
|
||||
starName: "nieer",
|
||||
},
|
||||
Star2: {
|
||||
image: "",
|
||||
name: "刘海栗",
|
||||
starName: "liuhaili",
|
||||
},
|
||||
},
|
||||
},
|
||||
economic: {
|
||||
backgroundImage: getBackgroundImage("entrance/经济"),
|
||||
Star: {
|
||||
Star1: {
|
||||
image: getStaticImage("entrance/荣德生-解锁"),
|
||||
name: "荣德生",
|
||||
starName: "rongdesheng",
|
||||
},
|
||||
Star2: {
|
||||
image: getStaticImage("entrance/陈云故居-解锁"),
|
||||
name: "陈云",
|
||||
starName: "chenyun",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Entrance = () => {
|
||||
const { moduleName } = useParams();
|
||||
const navigate = useNavigate();
|
||||
// const decodedModuleName = decodeURIComponent(moduleName || "");
|
||||
const { backgroundImage, Star } =
|
||||
EntranceBackgroundMap[moduleName as ModuleName] || {};
|
||||
if (!Star) {
|
||||
return null;
|
||||
}
|
||||
const { Star1, Star2 } = Star;
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{ backgroundImage }}
|
||||
>
|
||||
<Image
|
||||
className="absolute bottom-5 right-5"
|
||||
src={getStaticImage("entrance/入口-返回")}
|
||||
button
|
||||
onClick={() => navigate("/menu")}
|
||||
/>
|
||||
{/* 名人1 */}
|
||||
<Image
|
||||
className="absolute left-73 top-[calc(50%+38px)] -translate-y-1/2"
|
||||
src={Star1.image}
|
||||
/>
|
||||
{Star1.image && (
|
||||
<Image
|
||||
className="absolute left-90.5 bottom-40"
|
||||
src={getStaticImage("entrance/点击前往浏览")}
|
||||
button
|
||||
onClick={() => navigate(`/${moduleName}/${Star1.starName}/home`)}
|
||||
/>
|
||||
)}
|
||||
{/* 名人2 */}
|
||||
<Image
|
||||
className="absolute right-77.5 top-[calc(50%+39px)] -translate-y-1/2"
|
||||
src={Star2.image}
|
||||
/>
|
||||
{Star2.image && (
|
||||
<Image
|
||||
className="absolute right-90.5 bottom-40"
|
||||
src={getStaticImage("entrance/点击前往浏览")}
|
||||
button
|
||||
onClick={() => navigate(`/${moduleName}/${Star2.starName}/home`)}
|
||||
/>
|
||||
)}
|
||||
{/* <Image
|
||||
className="absolute right-5 top-1/2 -translate-y-1/2"
|
||||
src={Star2.image}
|
||||
/> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Entrance;
|
||||
@@ -0,0 +1,56 @@
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { Image } from "@/components";
|
||||
|
||||
const Huangyanpei = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
// starName 固定为 huangyanpei
|
||||
const starName = "huangyanpei";
|
||||
|
||||
if (!moduleName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`${starName}/背景`),
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
className="absolute right-37 top-22"
|
||||
src={getStaticImage("components/革命先驱故居-返回")}
|
||||
button
|
||||
onClick={() => navigate(`/entrance/${moduleName}`)}
|
||||
/>
|
||||
|
||||
<div className="absolute right-20 top-75 flex flex-col space-y-10">
|
||||
{/* 对话 */}
|
||||
<Image
|
||||
button
|
||||
src={getStaticImage("star/一-解锁")}
|
||||
onClick={() => navigate(`/${moduleName}/${starName}/place1`)}
|
||||
/>
|
||||
{/* 体悟 */}
|
||||
<Image
|
||||
button
|
||||
src={getStaticImage("star/二-解锁")}
|
||||
onClick={() => navigate(`/${moduleName}/${starName}/reflection`)}
|
||||
/>
|
||||
{/* 内化 */}
|
||||
<Image
|
||||
button
|
||||
src={getStaticImage("star/三-解锁")}
|
||||
onClick={() => navigate(`/${moduleName}/${starName}/internalization`)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Huangyanpei;
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { Image } from "@/components";
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
|
||||
const starName = "huangyanpei";
|
||||
|
||||
const HuangyanpeiInternalization = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`${starName}/internalization/底图`),
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/home`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-white text-4xl">黄炎培故居-内化模块(待完善)</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HuangyanpeiInternalization;
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import {
|
||||
ExhibitOverlay,
|
||||
GuideSteps,
|
||||
GuideStepsItem,
|
||||
Image,
|
||||
Title,
|
||||
} from "@/components";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { useState } from "react";
|
||||
import Cursor from "@/components/cursor";
|
||||
|
||||
// starName 固定为 huangyanpei
|
||||
const starName = "huangyanpei";
|
||||
|
||||
// 展品 ID 与资源路径映射,ID 用来做唯一标识
|
||||
const exhibitSrcMap: Record<1, string> = {
|
||||
1: "huangyanpei/place1/3_003",
|
||||
};
|
||||
|
||||
const HuangyanpeiPlace1 = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
// 引导对话显示状态
|
||||
const [visible, setVisible] = useState(true);
|
||||
// 当前激活的展品 ID(只存 ID,不直接存路径,便于作为唯一标识)
|
||||
const [activeExhibitId, setActiveExhibitId] = useState<1 | null>(null);
|
||||
// 已浏览展品的 ID 集合
|
||||
const [visited, setVisited] = useState<Set<number>>(new Set());
|
||||
|
||||
const handleOpenExhibit = (index: 1 | null) => {
|
||||
setActiveExhibitId(index);
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`huangyanpei/place1/3_001`),
|
||||
}}
|
||||
>
|
||||
{/* 标题 */}
|
||||
<Title
|
||||
title="思想启蒙"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
/>
|
||||
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/home`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 展品1 */}
|
||||
<Cursor
|
||||
className="absolute left-[calc(50%-8px)] bottom-100"
|
||||
onClick={() => handleOpenExhibit(1)}
|
||||
read={visited.has(1)}
|
||||
/>
|
||||
|
||||
{/* 继续前进 - 仅在完成所有展品后显示 */}
|
||||
{completed && (
|
||||
<Image
|
||||
className="absolute bottom-[calc(50%-140px)] right-80"
|
||||
src={getStaticImage("huangyanpei/place1/3_005")}
|
||||
button
|
||||
onClick={() => {
|
||||
navigate(`/${moduleName}/${starName}/place2`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 遮罩层 */}
|
||||
<ExhibitOverlay
|
||||
visible={activeExhibitId !== null}
|
||||
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
|
||||
onClose={() => {
|
||||
if (activeExhibitId !== null) {
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(activeExhibitId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
setActiveExhibitId(null);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 引导 */}
|
||||
<GuideSteps
|
||||
visible={visible}
|
||||
onVisibleChange={setVisible}
|
||||
showMask={true}
|
||||
highlightSelectors={[]}
|
||||
>
|
||||
<GuideStepsItem speaker="student">
|
||||
小沪!这就是黄炎培先生曾经生活过的地方吗?
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
是的,这里就是黄炎培故居。让我们一起来探索黄炎培先生的思想启蒙之路...
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HuangyanpeiPlace1;
|
||||
@@ -0,0 +1,101 @@
|
||||
import { useState } from "react";
|
||||
import { ExhibitOverlay, Title, Image, Cursor } from "@/components";
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
const starName = "huangyanpei";
|
||||
|
||||
const exhibitSrcMap: Record<1 | 2, string> = {
|
||||
1: "huangyanpei/place2/4_004",
|
||||
2: "huangyanpei/place2/4_005",
|
||||
};
|
||||
|
||||
const HuangyanpeiPlace2 = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | null>(null);
|
||||
const [visited, setVisited] = useState<Set<number>>(new Set());
|
||||
|
||||
const handleOpenExhibit = (index: 1 | 2) => {
|
||||
setActiveExhibitId(index);
|
||||
};
|
||||
|
||||
const progressCount = visited.size;
|
||||
const total = 2;
|
||||
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;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`huangyanpei/place2/4_001`),
|
||||
}}
|
||||
>
|
||||
<Title
|
||||
title="思想启蒙"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
/>
|
||||
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/place1`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Cursor
|
||||
className="absolute left-132 top-132"
|
||||
onClick={() => handleOpenExhibit(1)}
|
||||
read={visited.has(1)}
|
||||
/>
|
||||
<Cursor
|
||||
className="absolute left-240 top-132"
|
||||
onClick={() => handleOpenExhibit(2)}
|
||||
read={visited.has(2)}
|
||||
/>
|
||||
|
||||
{completed && (
|
||||
<Image
|
||||
className="absolute bottom-0 right-4"
|
||||
src={getStaticImage("components/place/继续前进")}
|
||||
button
|
||||
onClick={() => {
|
||||
navigate(`/${moduleName}/${starName}/place3`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ExhibitOverlay
|
||||
visible={activeExhibitId !== null}
|
||||
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
|
||||
onClose={() => {
|
||||
if (activeExhibitId !== null) {
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(activeExhibitId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
setActiveExhibitId(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HuangyanpeiPlace2;
|
||||
@@ -0,0 +1,109 @@
|
||||
import { useState } from "react";
|
||||
import { ExhibitOverlay, Title, Image, Cursor } from "@/components";
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
const starName = "huangyanpei";
|
||||
|
||||
const exhibitSrcMap: Record<1 | 2 | 3, string> = {
|
||||
1: "huangyanpei/place3/5_005",
|
||||
2: "huangyanpei/place3/5_006",
|
||||
3: "huangyanpei/place3/5_007",
|
||||
};
|
||||
|
||||
const HuangyanpeiPlace3 = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | 3 | null>(
|
||||
null
|
||||
);
|
||||
const [visited, setVisited] = useState<Set<number>>(new Set());
|
||||
|
||||
const handleOpenExhibit = (index: 1 | 2 | 3) => {
|
||||
setActiveExhibitId(index);
|
||||
};
|
||||
|
||||
const progressCount = visited.size;
|
||||
const total = 3;
|
||||
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;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`huangyanpei/place3/5_001`),
|
||||
}}
|
||||
>
|
||||
<Title
|
||||
title="教育实践"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
/>
|
||||
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/place2`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Cursor
|
||||
className="absolute left-32 top-105"
|
||||
onClick={() => handleOpenExhibit(1)}
|
||||
read={visited.has(1)}
|
||||
/>
|
||||
<Cursor
|
||||
className="absolute left-172 top-90"
|
||||
onClick={() => handleOpenExhibit(2)}
|
||||
read={visited.has(2)}
|
||||
/>
|
||||
<Cursor
|
||||
className="absolute right-92 top-108"
|
||||
onClick={() => handleOpenExhibit(3)}
|
||||
read={visited.has(3)}
|
||||
/>
|
||||
|
||||
{completed && (
|
||||
<Image
|
||||
className="absolute bottom-0 right-4"
|
||||
src={getStaticImage("components/place/继续前进")}
|
||||
button
|
||||
onClick={() => {
|
||||
navigate(`/${moduleName}/${starName}/place4`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ExhibitOverlay
|
||||
visible={activeExhibitId !== null}
|
||||
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
|
||||
onClose={() => {
|
||||
if (activeExhibitId !== null) {
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(activeExhibitId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
setActiveExhibitId(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HuangyanpeiPlace3;
|
||||
@@ -0,0 +1,115 @@
|
||||
import { useState } from "react";
|
||||
import { ExhibitOverlay, Title, Image, Cursor } from "@/components";
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
const starName = "huangyanpei";
|
||||
|
||||
const exhibitSrcMap: Record<1 | 2 | 3 | 4, string> = {
|
||||
1: "huangyanpei/place4/6_006",
|
||||
2: "huangyanpei/place4/6_007",
|
||||
3: "huangyanpei/place4/6_008",
|
||||
4: "huangyanpei/place4/6_009",
|
||||
};
|
||||
|
||||
const HuangyanpeiPlace4 = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | 3 | 4 | null>(
|
||||
null
|
||||
);
|
||||
const [visited, setVisited] = useState<Set<number>>(new Set());
|
||||
|
||||
const handleOpenExhibit = (index: 1 | 2 | 3 | 4) => {
|
||||
setActiveExhibitId(index);
|
||||
};
|
||||
|
||||
const progressCount = visited.size;
|
||||
const total = 4;
|
||||
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;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`huangyanpei/place4/6_001`),
|
||||
}}
|
||||
>
|
||||
<Title
|
||||
title="创立职教"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
/>
|
||||
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/place3`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Cursor
|
||||
className="absolute left-42 top-78"
|
||||
onClick={() => handleOpenExhibit(1)}
|
||||
read={visited.has(1)}
|
||||
/>
|
||||
<Cursor
|
||||
className="absolute left-40 top-132"
|
||||
onClick={() => handleOpenExhibit(2)}
|
||||
read={visited.has(2)}
|
||||
/>
|
||||
<Cursor
|
||||
className="absolute left-125 top-100"
|
||||
onClick={() => handleOpenExhibit(3)}
|
||||
read={visited.has(3)}
|
||||
/>
|
||||
<Cursor
|
||||
className="absolute right-62 top-102"
|
||||
onClick={() => handleOpenExhibit(4)}
|
||||
read={visited.has(4)}
|
||||
/>
|
||||
|
||||
{completed && (
|
||||
<Image
|
||||
className="absolute bottom-0 right-4"
|
||||
src={getStaticImage("components/place/继续前进")}
|
||||
button
|
||||
onClick={() => {
|
||||
navigate(`/${moduleName}/${starName}/place5`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ExhibitOverlay
|
||||
visible={activeExhibitId !== null}
|
||||
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
|
||||
onClose={() => {
|
||||
if (activeExhibitId !== null) {
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(activeExhibitId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
setActiveExhibitId(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HuangyanpeiPlace4;
|
||||
@@ -0,0 +1,136 @@
|
||||
import { useState, useRef } from "react";
|
||||
import { ExhibitOverlay, Title, Image, Cursor, Video } from "@/components";
|
||||
import { getBackgroundImage, getStaticImage, getPublicPath } from "@/lib/utils";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import type { VideoRef } from "@/components/video";
|
||||
|
||||
const starName = "huangyanpei";
|
||||
|
||||
const exhibitSrcMap: Record<1 | 2, string> = {
|
||||
1: "huangyanpei/place5/7_004",
|
||||
2: "huangyanpei/place5/场景5展品2",
|
||||
};
|
||||
|
||||
const HuangyanpeiPlace5 = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | null>(null);
|
||||
const [visited, setVisited] = useState<Set<number>>(new Set());
|
||||
const [videoVisible, setVideoVisible] = useState(false);
|
||||
const videoRef = useRef<VideoRef>(null);
|
||||
|
||||
const handleOpenExhibit = (index: 1 | 2) => {
|
||||
if (index === 2) {
|
||||
// 展品2:点击时播放视频,不展示展品
|
||||
setVideoVisible(true);
|
||||
} else {
|
||||
// 展品1:正常展示展品
|
||||
setActiveExhibitId(index);
|
||||
}
|
||||
};
|
||||
|
||||
const progressCount = visited.size;
|
||||
const total = 2;
|
||||
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;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`huangyanpei/place5/7_001`),
|
||||
}}
|
||||
>
|
||||
<Title
|
||||
title="延安对话"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
/>
|
||||
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/place4`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Cursor
|
||||
className="absolute left-35 top-108"
|
||||
onClick={() => handleOpenExhibit(1)}
|
||||
read={visited.has(1)}
|
||||
/>
|
||||
<Cursor
|
||||
className="absolute right-112 top-108"
|
||||
onClick={() => handleOpenExhibit(2)}
|
||||
read={visited.has(2)}
|
||||
/>
|
||||
|
||||
{completed && (
|
||||
<Image
|
||||
className="absolute bottom-0 right-4"
|
||||
src={getStaticImage("components/place/继续前进")}
|
||||
button
|
||||
onClick={() => {
|
||||
navigate(`/${moduleName}/${starName}/place6`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 展品2视频播放(全屏) */}
|
||||
{videoVisible && (
|
||||
<div className="fixed inset-0 z-[9999] bg-black">
|
||||
<Video
|
||||
ref={videoRef}
|
||||
src={getPublicPath(
|
||||
"video/chenyun/8陈云纪念馆16大米价格暴涨_x264.mp4"
|
||||
)}
|
||||
className="w-full h-full"
|
||||
autoPlay
|
||||
playsInline
|
||||
preload="auto"
|
||||
showSkipButton
|
||||
skipText="跳过"
|
||||
onEnded={() => {
|
||||
setVideoVisible(false);
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(2);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ExhibitOverlay
|
||||
visible={activeExhibitId !== null}
|
||||
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
|
||||
onClose={() => {
|
||||
if (activeExhibitId !== null) {
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(activeExhibitId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
setActiveExhibitId(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HuangyanpeiPlace5;
|
||||
@@ -0,0 +1,95 @@
|
||||
import { useState } from "react";
|
||||
import { ExhibitOverlay, Title, Image, Cursor } from "@/components";
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
const starName = "huangyanpei";
|
||||
|
||||
const exhibitSrcMap: Record<1, string> = {
|
||||
1: "huangyanpei/place6/8_003",
|
||||
};
|
||||
|
||||
const HuangyanpeiPlace6 = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
const [activeExhibitId, setActiveExhibitId] = useState<1 | null>(null);
|
||||
const [visited, setVisited] = useState<Set<number>>(new Set());
|
||||
|
||||
const handleOpenExhibit = (index: 1) => {
|
||||
setActiveExhibitId(index);
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`huangyanpei/place6/8_001`),
|
||||
}}
|
||||
>
|
||||
<Title
|
||||
title="执教传薪"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
/>
|
||||
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/place5`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Cursor
|
||||
className="absolute right-27 top-110"
|
||||
onClick={() => handleOpenExhibit(1)}
|
||||
read={visited.has(1)}
|
||||
/>
|
||||
|
||||
{completed && (
|
||||
<Image
|
||||
className="absolute bottom-0 right-4"
|
||||
src={getStaticImage("components/place/继续前进")}
|
||||
button
|
||||
onClick={() => {
|
||||
navigate(`/${moduleName}/${starName}/reflection`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ExhibitOverlay
|
||||
visible={activeExhibitId !== null}
|
||||
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
|
||||
onClose={() => {
|
||||
if (activeExhibitId !== null) {
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(activeExhibitId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
setActiveExhibitId(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HuangyanpeiPlace6;
|
||||
@@ -0,0 +1,155 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import {
|
||||
ExhibitOverlay,
|
||||
Title,
|
||||
Image,
|
||||
Cursor,
|
||||
BambooPuzzleGame,
|
||||
} from "@/components";
|
||||
import { SuccessAlert } from "@/components/layout";
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
const starName = "huangyanpei";
|
||||
|
||||
const exhibitSrcMap: Record<1 | 2, string> = {
|
||||
1: "huangyanpei/place7/9_004",
|
||||
2: "huangyanpei/place7/场景7展品2",
|
||||
};
|
||||
|
||||
const HuangyanpeiPlace7 = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | null>(null);
|
||||
const [visited, setVisited] = useState<Set<number>>(new Set());
|
||||
const [showGame, setShowGame] = useState(false);
|
||||
const [gameCompleted, setGameCompleted] = useState(false);
|
||||
const [showSuccessAlert, setShowSuccessAlert] = useState(false);
|
||||
|
||||
const handleOpenExhibit = (index: 1 | 2) => {
|
||||
setActiveExhibitId(index);
|
||||
};
|
||||
|
||||
const progressCount = visited.size;
|
||||
const total = 2;
|
||||
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;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`huangyanpei/place7/9_001`),
|
||||
}}
|
||||
>
|
||||
<Title
|
||||
title="跨越时空"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
/>
|
||||
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/place6`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Cursor
|
||||
className="absolute left-68 top-55"
|
||||
onClick={() => handleOpenExhibit(1)}
|
||||
read={visited.has(1)}
|
||||
/>
|
||||
<Cursor
|
||||
className="absolute left-160 bottom-50"
|
||||
onClick={() => {
|
||||
// 如果游戏已完成,不再打开
|
||||
if (!gameCompleted) {
|
||||
setShowGame(true);
|
||||
}
|
||||
}}
|
||||
read={gameCompleted}
|
||||
/>
|
||||
|
||||
{completed && (
|
||||
<Image
|
||||
className="absolute bottom-0 right-0"
|
||||
src={getStaticImage("huangyanpei/place7/9btn")}
|
||||
button
|
||||
onClick={() => {
|
||||
navigate(`/${moduleName}/${starName}/home`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ExhibitOverlay
|
||||
visible={activeExhibitId !== null}
|
||||
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
|
||||
onClose={() => {
|
||||
if (activeExhibitId !== null) {
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(activeExhibitId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
setActiveExhibitId(null);
|
||||
}}
|
||||
/>
|
||||
|
||||
<BambooPuzzleGame
|
||||
visible={showGame}
|
||||
onClose={() => {
|
||||
setShowGame(false);
|
||||
}}
|
||||
onComplete={() => {
|
||||
// 游戏完成后,显示成功提示
|
||||
setShowGame(false);
|
||||
setShowSuccessAlert(true);
|
||||
}}
|
||||
/>
|
||||
|
||||
<SuccessAlert
|
||||
visible={showSuccessAlert}
|
||||
title="拼单杠成功!"
|
||||
onContinue={useCallback(() => {
|
||||
console.log("onContinue called, showSuccessAlert:", showSuccessAlert);
|
||||
// 直接设置为 false,不使用函数式更新
|
||||
setShowSuccessAlert(false);
|
||||
setGameCompleted(true);
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(2);
|
||||
return next;
|
||||
});
|
||||
}, [showSuccessAlert])}
|
||||
onClose={useCallback(() => {
|
||||
console.log("onClose called, showSuccessAlert:", showSuccessAlert);
|
||||
// 直接设置为 false,不使用函数式更新
|
||||
setShowSuccessAlert(false);
|
||||
setGameCompleted(true);
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(2);
|
||||
return next;
|
||||
});
|
||||
}, [showSuccessAlert])}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HuangyanpeiPlace7;
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { Image } from "@/components";
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
|
||||
const starName = "huangyanpei";
|
||||
|
||||
const HuangyanpeiReflection = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`${starName}/reflection/底图`),
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/home`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-white text-4xl">黄炎培故居-体悟模块(待完善)</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HuangyanpeiReflection;
|
||||
@@ -0,0 +1,155 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import {
|
||||
ExhibitOverlay,
|
||||
Title,
|
||||
Image,
|
||||
Cursor,
|
||||
BambooPuzzleGame,
|
||||
} from "@/components";
|
||||
import { SuccessAlert } from "@/components/layout";
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
const starName = "huangyanpei";
|
||||
|
||||
const exhibitSrcMap: Record<1 | 2, string> = {
|
||||
1: "huangyanpei/reflection/scene1/9_004",
|
||||
2: "huangyanpei/reflection/scene1/场景7展品2",
|
||||
};
|
||||
|
||||
const HuangyanpeiReflectionScene1 = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | null>(null);
|
||||
const [visited, setVisited] = useState<Set<number>>(new Set());
|
||||
const [showGame, setShowGame] = useState(false);
|
||||
const [gameCompleted, setGameCompleted] = useState(false);
|
||||
const [showSuccessAlert, setShowSuccessAlert] = useState(false);
|
||||
|
||||
const handleOpenExhibit = (index: 1 | 2) => {
|
||||
setActiveExhibitId(index);
|
||||
};
|
||||
|
||||
const progressCount = visited.size;
|
||||
const total = 2;
|
||||
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;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(
|
||||
`huangyanpei/reflection/scene1/9_001`
|
||||
),
|
||||
}}
|
||||
>
|
||||
<Title
|
||||
title="跨越时空"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
/>
|
||||
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/home`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Cursor
|
||||
className="absolute left-68 top-55"
|
||||
onClick={() => handleOpenExhibit(1)}
|
||||
read={visited.has(1)}
|
||||
/>
|
||||
<Cursor
|
||||
className="absolute left-160 bottom-50"
|
||||
onClick={() => {
|
||||
// 如果游戏已完成,不再打开
|
||||
if (!gameCompleted) {
|
||||
setShowGame(true);
|
||||
}
|
||||
}}
|
||||
read={gameCompleted}
|
||||
/>
|
||||
|
||||
{completed && (
|
||||
<Image
|
||||
className="absolute bottom-0 right-0"
|
||||
src={getStaticImage("huangyanpei/reflection/scene1/9btn")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/reflection2`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ExhibitOverlay
|
||||
visible={activeExhibitId !== null}
|
||||
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
|
||||
onClose={() => {
|
||||
if (activeExhibitId !== null) {
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(activeExhibitId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
setActiveExhibitId(null);
|
||||
}}
|
||||
/>
|
||||
|
||||
<BambooPuzzleGame
|
||||
visible={showGame}
|
||||
onClose={() => {
|
||||
setShowGame(false);
|
||||
}}
|
||||
onComplete={() => {
|
||||
// 游戏完成后,显示成功提示
|
||||
setShowGame(false);
|
||||
setShowSuccessAlert(true);
|
||||
}}
|
||||
/>
|
||||
|
||||
<SuccessAlert
|
||||
visible={showSuccessAlert}
|
||||
title="拼单杠成功!"
|
||||
onContinue={useCallback(() => {
|
||||
setShowSuccessAlert(false);
|
||||
setGameCompleted(true);
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(2);
|
||||
return next;
|
||||
});
|
||||
}, [])}
|
||||
onClose={useCallback(() => {
|
||||
setShowSuccessAlert(false);
|
||||
setGameCompleted(true);
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(2);
|
||||
return next;
|
||||
});
|
||||
}, [])}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HuangyanpeiReflectionScene1;
|
||||
@@ -0,0 +1,98 @@
|
||||
import { useState } from "react";
|
||||
import { Title, Image, Cursor, ExhibitOverlay } from "@/components";
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
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 [visited, setVisited] = useState<Set<number>>(new Set());
|
||||
|
||||
if (!moduleName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(
|
||||
`huangyanpei/reflection/scene2/10_001`
|
||||
),
|
||||
}}
|
||||
>
|
||||
<Title
|
||||
title="跨越时空"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
/>
|
||||
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
navigate(`/${moduleName}/${starName}/home`);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 互动展品光标:后续可替换为拖拽/选择类互动 */}
|
||||
<Cursor
|
||||
className="absolute left-80 top-96"
|
||||
onClick={() => setActiveExhibitId(1)}
|
||||
read={visited.has(1)}
|
||||
/>
|
||||
|
||||
{/* 展品遮罩:当前使用单张图片,占位为后续互动内容 */}
|
||||
<ExhibitOverlay
|
||||
visible={activeExhibitId !== null}
|
||||
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
|
||||
onClose={() => {
|
||||
if (activeExhibitId !== null) {
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(activeExhibitId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
setActiveExhibitId(null);
|
||||
}}
|
||||
/>
|
||||
|
||||
{completed && (
|
||||
<Image
|
||||
className="absolute bottom-0 right-4"
|
||||
src={getStaticImage("components/place/继续前进")}
|
||||
button
|
||||
onClick={() => {
|
||||
navigate(`/${moduleName}/${starName}/reflection3`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HuangyanpeiReflectionScene2;
|
||||
@@ -0,0 +1,98 @@
|
||||
import { useState } from "react";
|
||||
import { Title, Image, Cursor, ExhibitOverlay } from "@/components";
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
const starName = "huangyanpei";
|
||||
|
||||
const exhibitSrcMap: Record<1, string> = {
|
||||
// 预留展品图片路径:请在 images 中放置对应资源
|
||||
1: "huangyanpei/reflection/scene3/11_002",
|
||||
};
|
||||
|
||||
const HuangyanpeiReflectionScene3 = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
const [activeExhibitId, setActiveExhibitId] = useState<1 | null>(null);
|
||||
const [visited, setVisited] = useState<Set<number>>(new Set());
|
||||
|
||||
if (!moduleName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(
|
||||
`huangyanpei/reflection/scene3_001`
|
||||
),
|
||||
}}
|
||||
>
|
||||
<Title
|
||||
title="跨越时空"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
/>
|
||||
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
navigate(`/${moduleName}/${starName}/home`);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 互动展品光标:后续可替换为拖拽/选择类互动 */}
|
||||
<Cursor
|
||||
className="absolute left-96 top-90"
|
||||
onClick={() => setActiveExhibitId(1)}
|
||||
read={visited.has(1)}
|
||||
/>
|
||||
|
||||
{/* 展品遮罩:当前使用单张图片,占位为后续互动内容 */}
|
||||
<ExhibitOverlay
|
||||
visible={activeExhibitId !== null}
|
||||
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
|
||||
onClose={() => {
|
||||
if (activeExhibitId !== null) {
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(activeExhibitId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
setActiveExhibitId(null);
|
||||
}}
|
||||
/>
|
||||
|
||||
{completed && (
|
||||
<Image
|
||||
className="absolute bottom-0 right-4"
|
||||
src={getStaticImage("components/place/继续前进")}
|
||||
button
|
||||
onClick={() => {
|
||||
navigate(`/${moduleName}/${starName}/reflection4`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HuangyanpeiReflectionScene3;
|
||||
@@ -0,0 +1,98 @@
|
||||
import { useState } from "react";
|
||||
import { Title, Image, Cursor, ExhibitOverlay } from "@/components";
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
const starName = "huangyanpei";
|
||||
|
||||
const exhibitSrcMap: Record<1, string> = {
|
||||
// 预留展品图片路径:请在 images 中放置对应资源
|
||||
1: "huangyanpei/reflection/scene4/12_002",
|
||||
};
|
||||
|
||||
const HuangyanpeiReflectionScene4 = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
const [activeExhibitId, setActiveExhibitId] = useState<1 | null>(null);
|
||||
const [visited, setVisited] = useState<Set<number>>(new Set());
|
||||
|
||||
if (!moduleName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(
|
||||
`huangyanpei/reflection/scene4_001`
|
||||
),
|
||||
}}
|
||||
>
|
||||
<Title
|
||||
title="跨越时空"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
/>
|
||||
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
navigate(`/${moduleName}/${starName}/home`);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 互动展品光标:后续可替换为拖拽/选择类互动 */}
|
||||
<Cursor
|
||||
className="absolute left-100 top-96"
|
||||
onClick={() => setActiveExhibitId(1)}
|
||||
read={visited.has(1)}
|
||||
/>
|
||||
|
||||
{/* 展品遮罩:当前使用单张图片,占位为后续互动内容 */}
|
||||
<ExhibitOverlay
|
||||
visible={activeExhibitId !== null}
|
||||
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
|
||||
onClose={() => {
|
||||
if (activeExhibitId !== null) {
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(activeExhibitId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
setActiveExhibitId(null);
|
||||
}}
|
||||
/>
|
||||
|
||||
{completed && (
|
||||
<Image
|
||||
className="absolute bottom-0 right-4"
|
||||
src={getStaticImage("components/place/继续前进")}
|
||||
button
|
||||
onClick={() => {
|
||||
navigate(`/${moduleName}/${starName}/home`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HuangyanpeiReflectionScene4;
|
||||
@@ -0,0 +1,56 @@
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { Image } from "@/components";
|
||||
|
||||
const Luxun = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
// starName 固定为 luxun
|
||||
const starName = "luxun";
|
||||
|
||||
if (!moduleName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`${starName}/首页`),
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
className="absolute right-37 top-22"
|
||||
src={getStaticImage("components/革命先驱故居-返回")}
|
||||
button
|
||||
onClick={() => navigate(`/entrance/${moduleName}`)}
|
||||
/>
|
||||
|
||||
<div className="absolute right-20 top-75 flex flex-col space-y-10">
|
||||
{/* 对话 */}
|
||||
<Image
|
||||
button
|
||||
src={getStaticImage("star/一-解锁")}
|
||||
onClick={() => navigate(`/${moduleName}/${starName}/place1`)}
|
||||
/>
|
||||
{/* 体悟 */}
|
||||
<Image
|
||||
button
|
||||
src={getStaticImage("star/二-解锁")}
|
||||
onClick={() => navigate(`/${moduleName}/${starName}/reflection`)}
|
||||
/>
|
||||
{/* 内化 */}
|
||||
<Image
|
||||
button
|
||||
src={getStaticImage("star/三-解锁")}
|
||||
onClick={() => navigate(`/${moduleName}/${starName}/internalization`)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Luxun;
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { Image } from "@/components";
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
|
||||
const starName = "luxun";
|
||||
|
||||
const LuxunInternalization = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`${starName}/internalization/底图`),
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/home`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-white text-4xl">鲁迅故居-内化模块(待完善)</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LuxunInternalization;
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import {
|
||||
ExhibitOverlay,
|
||||
GuideSteps,
|
||||
GuideStepsItem,
|
||||
Image,
|
||||
Title,
|
||||
Cursor,
|
||||
} from "@/components";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { useState } from "react";
|
||||
|
||||
// starName 固定为 luxun
|
||||
const starName = "luxun";
|
||||
|
||||
// 展品 ID 与资源路径映射,ID 用来做唯一标识
|
||||
const exhibitSrcMap: Record<1 | 2, string> = {
|
||||
1: "luxun/place1/展品1",
|
||||
2: "luxun/place1/展品2",
|
||||
};
|
||||
|
||||
const LuxunPlace1 = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
// 引导对话显示状态
|
||||
const [visible, setVisible] = useState(true);
|
||||
// 当前激活的展品 ID(只存 ID,不直接存路径,便于作为唯一标识)
|
||||
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | null>(null);
|
||||
// 已浏览展品的 ID 集合
|
||||
const [visited, setVisited] = useState<Set<number>>(new Set());
|
||||
|
||||
const handleOpenExhibit = (index: 1 | 2) => {
|
||||
setActiveExhibitId(index);
|
||||
};
|
||||
|
||||
const progressCount = visited.size;
|
||||
const total = 2;
|
||||
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;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`luxun/place1/背景`),
|
||||
}}
|
||||
>
|
||||
{/* 标题 */}
|
||||
<Title
|
||||
title="风雨前夕的思想淬炼"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
/>
|
||||
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/home`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 展品1 */}
|
||||
<Cursor
|
||||
className="absolute left-[calc(50%-48px)] top-80"
|
||||
onClick={() => handleOpenExhibit(1)}
|
||||
read={visited.has(1)}
|
||||
/>
|
||||
{/* 展品2 */}
|
||||
<Cursor
|
||||
className="absolute left-[calc(50%)] bottom-60"
|
||||
onClick={() => handleOpenExhibit(2)}
|
||||
read={visited.has(2)}
|
||||
/>
|
||||
|
||||
{/* 继续前进 - 仅在完成所有展品后显示 */}
|
||||
{completed && (
|
||||
<Image
|
||||
className="absolute bottom-0 left-130"
|
||||
src={getStaticImage("luxun/place1/前进")}
|
||||
button
|
||||
onClick={() => {
|
||||
navigate(`/${moduleName}/${starName}/place2`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 遮罩层 */}
|
||||
<ExhibitOverlay
|
||||
visible={activeExhibitId !== null}
|
||||
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
|
||||
onClose={() => {
|
||||
if (activeExhibitId !== null) {
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(activeExhibitId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
setActiveExhibitId(null);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 引导 */}
|
||||
<GuideSteps
|
||||
visible={visible}
|
||||
onVisibleChange={setVisible}
|
||||
showMask={true}
|
||||
highlightSelectors={[]}
|
||||
>
|
||||
<GuideStepsItem speaker="guide">
|
||||
同学你好,欢迎来到走进故居-对话学习模块!
|
||||
进入鲁迅故居纪念馆后,我们一起来了解鲁迅先生的思想历程和文学成就。
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
在这个场景中,我们将探索鲁迅故居一楼客厅,了解风雨前夕的思想淬炼。
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LuxunPlace1;
|
||||
@@ -0,0 +1,115 @@
|
||||
import { ExhibitOverlay, Image, Title, Cursor } from "@/components";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { useState } from "react";
|
||||
|
||||
// starName 固定为 luxun
|
||||
const starName = "luxun";
|
||||
|
||||
// 展品 ID 与资源路径映射
|
||||
const exhibitSrcMap: Record<1 | 2 | 3, string> = {
|
||||
1: "luxun/place2/展品3",
|
||||
2: "luxun/place2/展品4",
|
||||
3: "luxun/place2/展品5",
|
||||
};
|
||||
|
||||
const LuxunPlace2 = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | 3 | null>(
|
||||
null
|
||||
);
|
||||
const [visited, setVisited] = useState<Set<number>>(new Set());
|
||||
|
||||
const handleOpenExhibit = (index: 1 | 2 | 3) => {
|
||||
setActiveExhibitId(index);
|
||||
};
|
||||
|
||||
const progressCount = visited.size;
|
||||
const total = 3;
|
||||
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;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`luxun/place2/背景`),
|
||||
}}
|
||||
>
|
||||
<Title
|
||||
title="杂文集里的匕首与投枪"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
/>
|
||||
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/place1`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 展品 */}
|
||||
<Cursor
|
||||
className="absolute right-218 bottom-104"
|
||||
size={0.4}
|
||||
onClick={() => handleOpenExhibit(1)}
|
||||
read={visited.has(1)}
|
||||
/>
|
||||
<Cursor
|
||||
className="absolute right-225 bottom-95"
|
||||
size={0.4}
|
||||
onClick={() => handleOpenExhibit(2)}
|
||||
read={visited.has(2)}
|
||||
/>
|
||||
<Cursor
|
||||
className="absolute right-232 bottom-84"
|
||||
size={0.4}
|
||||
onClick={() => handleOpenExhibit(3)}
|
||||
read={visited.has(3)}
|
||||
/>
|
||||
|
||||
{completed && (
|
||||
<Image
|
||||
className="absolute bottom-10 left-42"
|
||||
src={getStaticImage("luxun/place2/前进")}
|
||||
button
|
||||
onClick={() => {
|
||||
navigate(`/${moduleName}/${starName}/place3`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ExhibitOverlay
|
||||
visible={activeExhibitId !== null}
|
||||
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
|
||||
onClose={() => {
|
||||
if (activeExhibitId !== null) {
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(activeExhibitId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
setActiveExhibitId(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LuxunPlace2;
|
||||
@@ -0,0 +1,115 @@
|
||||
import { ExhibitOverlay, Image, Title, Cursor } from "@/components";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { useState } from "react";
|
||||
|
||||
// starName 固定为 luxun
|
||||
const starName = "luxun";
|
||||
|
||||
// 展品 ID 与资源路径映射
|
||||
const exhibitSrcMap: Record<1 | 2 | 3, string> = {
|
||||
1: "luxun/place3/展品8",
|
||||
2: "luxun/place3/展品7",
|
||||
3: "luxun/place3/展品6",
|
||||
};
|
||||
|
||||
const LuxunPlace3 = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | 3 | null>(
|
||||
null
|
||||
);
|
||||
const [visited, setVisited] = useState<Set<number>>(new Set());
|
||||
|
||||
const handleOpenExhibit = (index: 1 | 2 | 3) => {
|
||||
setActiveExhibitId(index);
|
||||
};
|
||||
|
||||
const progressCount = visited.size;
|
||||
const total = 3;
|
||||
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;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`luxun/place3/背景`),
|
||||
}}
|
||||
>
|
||||
<Title
|
||||
title="历史新编中的现实呐喊"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
/>
|
||||
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/place2`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 展品 */}
|
||||
<Cursor
|
||||
className="absolute left-90 bottom-140"
|
||||
onClick={() => handleOpenExhibit(1)}
|
||||
size={0.4}
|
||||
read={visited.has(1)}
|
||||
/>
|
||||
<Cursor
|
||||
className="absolute right-225 bottom-120"
|
||||
onClick={() => handleOpenExhibit(2)}
|
||||
size={0.4}
|
||||
read={visited.has(2)}
|
||||
/>
|
||||
<Cursor
|
||||
className="absolute right-214 bottom-123"
|
||||
onClick={() => handleOpenExhibit(3)}
|
||||
size={0.4}
|
||||
read={visited.has(3)}
|
||||
/>
|
||||
|
||||
{completed && (
|
||||
<Image
|
||||
className="absolute bottom-25 right-[calc(50%-270px)]"
|
||||
src={getStaticImage("luxun/place3/前进")}
|
||||
button
|
||||
onClick={() => {
|
||||
navigate(`/${moduleName}/${starName}/place4`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ExhibitOverlay
|
||||
visible={activeExhibitId !== null}
|
||||
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
|
||||
onClose={() => {
|
||||
if (activeExhibitId !== null) {
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(activeExhibitId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
setActiveExhibitId(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LuxunPlace3;
|
||||
@@ -0,0 +1,106 @@
|
||||
import { ExhibitOverlay, Image, Title, Cursor } from "@/components";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { useState } from "react";
|
||||
|
||||
// starName 固定为 luxun
|
||||
const starName = "luxun";
|
||||
|
||||
// 展品 ID 与资源路径映射
|
||||
const exhibitSrcMap: Record<1 | 2, string> = {
|
||||
1: "luxun/place4/展品9_t",
|
||||
2: "luxun/place4/展品9_b",
|
||||
};
|
||||
|
||||
const LuxunPlace4 = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | null>(null);
|
||||
const [visited, setVisited] = useState<Set<number>>(new Set());
|
||||
|
||||
const handleOpenExhibit = (index: 1 | 2) => {
|
||||
setActiveExhibitId(index);
|
||||
};
|
||||
|
||||
const progressCount = visited.size;
|
||||
const total = 2;
|
||||
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;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`luxun/place4/背景`),
|
||||
}}
|
||||
>
|
||||
<Title
|
||||
title="为亡友编纂的精神丰碑"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
/>
|
||||
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/place3`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 展品 */}
|
||||
<Cursor
|
||||
className="absolute left-170 bottom-115"
|
||||
size={0.4}
|
||||
onClick={() => handleOpenExhibit(1)}
|
||||
read={visited.has(1)}
|
||||
/>
|
||||
<Cursor
|
||||
className="absolute left-183 bottom-115"
|
||||
size={0.4}
|
||||
onClick={() => handleOpenExhibit(2)}
|
||||
read={visited.has(2)}
|
||||
/>
|
||||
|
||||
{completed && (
|
||||
<Image
|
||||
className="absolute bottom-0 right-120"
|
||||
src={getStaticImage("luxun/place4/前进")}
|
||||
button
|
||||
onClick={() => {
|
||||
navigate(`/${moduleName}/${starName}/place5`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ExhibitOverlay
|
||||
visible={activeExhibitId !== null}
|
||||
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
|
||||
onClose={() => {
|
||||
if (activeExhibitId !== null) {
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(activeExhibitId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
setActiveExhibitId(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LuxunPlace4;
|
||||
@@ -0,0 +1,112 @@
|
||||
import { ExhibitOverlay, Image, Title, Cursor } from "@/components";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { useState } from "react";
|
||||
|
||||
// starName 固定为 luxun
|
||||
const starName = "luxun";
|
||||
|
||||
// 展品 ID 与资源路径映射
|
||||
const exhibitSrcMap: Record<1 | 2 | 3, string> = {
|
||||
1: "luxun/place5/展品11",
|
||||
2: "luxun/place5/展品12",
|
||||
3: "luxun/place5/展品13",
|
||||
};
|
||||
|
||||
const LuxunPlace5 = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | 3 | null>(
|
||||
null
|
||||
);
|
||||
const [visited, setVisited] = useState<Set<number>>(new Set());
|
||||
|
||||
const handleOpenExhibit = (index: 1 | 2 | 3) => {
|
||||
setActiveExhibitId(index);
|
||||
};
|
||||
|
||||
const progressCount = visited.size;
|
||||
const total = 3;
|
||||
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;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`luxun/place5/背景`),
|
||||
}}
|
||||
>
|
||||
<Title
|
||||
title="6平方米的红色庇护所"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
/>
|
||||
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/place4`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 展品 */}
|
||||
<Cursor
|
||||
className="absolute left-167 bottom-30"
|
||||
onClick={() => handleOpenExhibit(1)}
|
||||
read={visited.has(1)}
|
||||
/>
|
||||
<Cursor
|
||||
className="absolute left-228 bottom-30"
|
||||
onClick={() => handleOpenExhibit(2)}
|
||||
read={visited.has(2)}
|
||||
/>
|
||||
<Cursor
|
||||
className="absolute right-115 bottom-18 z-20"
|
||||
onClick={() => handleOpenExhibit(3)}
|
||||
read={visited.has(3)}
|
||||
/>
|
||||
|
||||
{completed && (
|
||||
<Image
|
||||
className="absolute bottom-0 right-0"
|
||||
src={getStaticImage("luxun/place5/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
navigate(`/${moduleName}/${starName}/home`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ExhibitOverlay
|
||||
visible={activeExhibitId !== null}
|
||||
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
|
||||
onClose={() => {
|
||||
if (activeExhibitId !== null) {
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(activeExhibitId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
setActiveExhibitId(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LuxunPlace5;
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { Image } from "@/components";
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
|
||||
const starName = "luxun";
|
||||
|
||||
const LuxunReflection = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`${starName}/reflection/底图`),
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/home`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-white text-4xl">鲁迅故居-体悟模块(待完善)</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LuxunReflection;
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import { Video, type VideoAnchor } from "@/components";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getPublicPath } from "@/lib/utils";
|
||||
import { useRef } from "react";
|
||||
|
||||
// 光标组件 - 带旋转动画
|
||||
// Safari 兼容:使用 onTouchEnd 和 -webkit-tap-highlight-color
|
||||
const Cursor = ({
|
||||
className,
|
||||
onClick,
|
||||
}: {
|
||||
className?: string;
|
||||
onClick: () => void;
|
||||
}) => (
|
||||
<img
|
||||
className={cn(
|
||||
"w-35 cursor-pointer animate-[spin_2s_linear_infinite] select-none",
|
||||
className
|
||||
)}
|
||||
onClick={onClick}
|
||||
onTouchEnd={(e) => {
|
||||
e.preventDefault(); // 防止 Safari 触发两次
|
||||
onClick();
|
||||
}}
|
||||
style={{
|
||||
WebkitTapHighlightColor: "transparent",
|
||||
WebkitTouchCallout: "none",
|
||||
WebkitUserSelect: "none",
|
||||
touchAction: "manipulation",
|
||||
}}
|
||||
src="http://101.132.152.157/hsyjgrl/images/gemingxianqu/mzd/congress/%E5%85%89%E6%A0%87.png"
|
||||
alt=""
|
||||
draggable={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const Home = () => {
|
||||
const bgmRef = useRef<HTMLAudioElement>(null);
|
||||
|
||||
// 播放 BGM
|
||||
const playBGM = () => {
|
||||
bgmRef.current?.play().catch(() => {});
|
||||
};
|
||||
|
||||
const anchors: VideoAnchor[] = [
|
||||
{
|
||||
id: "anchor-1",
|
||||
time: 8,
|
||||
afterResume: "hide", // 恢复后隐藏,seek 回来可重新触发
|
||||
onTrigger: () => console.log("埋点:锚点 1 触发"),
|
||||
render: ({ resume, unmute }) => (
|
||||
<Cursor
|
||||
className="absolute left-150 top-80"
|
||||
onClick={() => {
|
||||
unmute();
|
||||
playBGM(); // 第一个键点开始播放 BGM
|
||||
resume();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "anchor-2",
|
||||
time: 22,
|
||||
afterResume: "hide",
|
||||
render: ({ resume, unmute }) => (
|
||||
<Cursor
|
||||
className="absolute right-42 bottom-95"
|
||||
onClick={() => {
|
||||
unmute();
|
||||
resume();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "anchor-3",
|
||||
time: 32,
|
||||
afterResume: "hide",
|
||||
render: ({ resume, unmute }) => (
|
||||
<Cursor
|
||||
className="absolute left-50 bottom-100"
|
||||
onClick={() => {
|
||||
unmute();
|
||||
resume();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "anchor-4",
|
||||
time: 40,
|
||||
afterResume: "hide",
|
||||
render: ({ resume, unmute }) => (
|
||||
<Cursor
|
||||
className="absolute right-158 bottom-105"
|
||||
onClick={() => {
|
||||
unmute();
|
||||
resume();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "anchor-5",
|
||||
time: 56,
|
||||
afterResume: "hide",
|
||||
render: ({ resume, unmute }) => (
|
||||
<Cursor
|
||||
className="absolute right-58 bottom-115"
|
||||
onClick={() => {
|
||||
unmute();
|
||||
resume();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "anchor-6",
|
||||
time: 74,
|
||||
afterResume: "hide",
|
||||
render: ({ resume, unmute }) => (
|
||||
<Cursor
|
||||
className="absolute left-80 bottom-110"
|
||||
onClick={() => {
|
||||
unmute();
|
||||
resume();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* BGM 循环播放 */}
|
||||
<audio ref={bgmRef} src={getPublicPath("audio/BGM.mp3")} loop />
|
||||
<Video
|
||||
src={getPublicPath("video/maozedong/demo.mp4")}
|
||||
className="w-full h-full aspect-video"
|
||||
anchors={anchors}
|
||||
autoPlay
|
||||
muted
|
||||
playsInline
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
@@ -0,0 +1,43 @@
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { Image } from "@/components";
|
||||
|
||||
const Maozedong = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
// starName 固定为 maozedong
|
||||
const starName = "maozedong";
|
||||
|
||||
if (!moduleName) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`${starName}/毛泽东故居底图`),
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
className="absolute right-37 top-22"
|
||||
src={getStaticImage("components/革命先驱故居-返回")}
|
||||
button
|
||||
onClick={() => navigate(`/entrance/${moduleName}`)}
|
||||
/>
|
||||
|
||||
<div className="absolute right-20 top-75 flex flex-col space-y-10">
|
||||
<Image button src={getStaticImage("star/一-解锁")} />
|
||||
<Image
|
||||
button
|
||||
src={getStaticImage("star/二-解锁")}
|
||||
onClick={() => navigate(`/${moduleName}/${starName}/demo`)}
|
||||
/>
|
||||
<Image button src={getStaticImage("star/三-解锁")} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Maozedong;
|
||||
@@ -0,0 +1,56 @@
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { Image } from "@/components";
|
||||
|
||||
const Sunzhongshan = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
// starName 固定为 sunzhongshan
|
||||
const starName = "sunzhongshan";
|
||||
|
||||
if (!moduleName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`${starName}/孙中山-首页`),
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
className="absolute right-37 top-22"
|
||||
src={getStaticImage("components/革命先驱故居-返回")}
|
||||
button
|
||||
onClick={() => navigate(`/entrance/${moduleName}`)}
|
||||
/>
|
||||
|
||||
<div className="absolute right-20 top-75 flex flex-col space-y-10">
|
||||
{/* 对话 */}
|
||||
<Image
|
||||
button
|
||||
src={getStaticImage("star/一-解锁")}
|
||||
onClick={() => navigate(`/${moduleName}/${starName}/place1`)}
|
||||
/>
|
||||
{/* 体悟 */}
|
||||
<Image
|
||||
button
|
||||
src={getStaticImage("star/二-解锁")}
|
||||
onClick={() => navigate(`/${moduleName}/${starName}/reflection`)}
|
||||
/>
|
||||
{/* 内化 */}
|
||||
<Image
|
||||
button
|
||||
src={getStaticImage("star/三-解锁")}
|
||||
onClick={() => navigate(`/${moduleName}/${starName}/internalization`)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sunzhongshan;
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { Image } from "@/components";
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
|
||||
const starName = "sunzhongshan";
|
||||
|
||||
const SunzhongshanInternalization = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`${starName}/internalization/底图`),
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/home`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-white text-4xl">孙中山故居-内化模块(待完善)</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SunzhongshanInternalization;
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import {
|
||||
ExhibitOverlay,
|
||||
GuideSteps,
|
||||
GuideStepsItem,
|
||||
Image,
|
||||
Title,
|
||||
Overlay,
|
||||
} from "@/components";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { useState } from "react";
|
||||
import Cursor from "@/components/cursor";
|
||||
|
||||
// starName 固定为 sunzhongshan
|
||||
const starName = "sunzhongshan";
|
||||
|
||||
// 展品 ID 与资源路径映射,ID 用来做唯一标识
|
||||
const exhibitSrcMap: Record<1 | 2 | 3 | 4 | 5, string> = {
|
||||
1: "sunzhongshan/place1/“南北议和”纪念章",
|
||||
2: "sunzhongshan/place1/“四大寇”合影",
|
||||
3: "sunzhongshan/place1/民立报",
|
||||
4: "sunzhongshan/place1/申报",
|
||||
5: "sunzhongshan/place1/郑观应的推荐信",
|
||||
};
|
||||
|
||||
const SunzhongshanPlace1 = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
// 学习说明对话框显示状态
|
||||
const [showIntroDialog, setShowIntroDialog] = useState(false);
|
||||
// 引导对话显示状态
|
||||
const [visible, setVisible] = useState(true);
|
||||
// 当前激活的展品 ID(只存 ID,不直接存路径,便于作为唯一标识)
|
||||
const [activeExhibitId, setActiveExhibitId] = useState<
|
||||
1 | 2 | 3 | 4 | 5 | null
|
||||
>(null);
|
||||
// 已浏览展品的 ID 集合
|
||||
const [visited, setVisited] = useState<Set<number>>(new Set());
|
||||
|
||||
const handleOpenExhibit = (index: 1 | 2 | 3 | 4 | 5) => {
|
||||
setActiveExhibitId(index);
|
||||
};
|
||||
|
||||
const handleStartLearning = () => {
|
||||
setShowIntroDialog(false);
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
const progressCount = visited.size;
|
||||
const total = 5;
|
||||
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;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`sunzhongshan/place1/背景`),
|
||||
}}
|
||||
>
|
||||
{/* 标题 */}
|
||||
<Title
|
||||
title="立志救国·推翻帝制"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
/>
|
||||
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/home`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 展品1 */}
|
||||
<Cursor
|
||||
className="absolute left-75 bottom-80"
|
||||
onClick={() => handleOpenExhibit(1)}
|
||||
read={visited.has(1)}
|
||||
/>
|
||||
{/* 展品2 */}
|
||||
<Cursor
|
||||
className="absolute left-108 bottom-118"
|
||||
onClick={() => handleOpenExhibit(2)}
|
||||
read={visited.has(2)}
|
||||
/>
|
||||
{/* 展品3 */}
|
||||
<Cursor
|
||||
className="absolute left-165 bottom-90"
|
||||
onClick={() => handleOpenExhibit(3)}
|
||||
read={visited.has(3)}
|
||||
/>
|
||||
{/* 展品4 */}
|
||||
<Cursor
|
||||
className="absolute right-192 bottom-118"
|
||||
onClick={() => handleOpenExhibit(4)}
|
||||
read={visited.has(4)}
|
||||
/>
|
||||
{/* 展品5 */}
|
||||
<Cursor
|
||||
className="absolute right-188 bottom-88"
|
||||
onClick={() => handleOpenExhibit(5)}
|
||||
read={visited.has(5)}
|
||||
/>
|
||||
|
||||
{/* 继续前进 - 仅在完成所有展品后显示 */}
|
||||
{completed && (
|
||||
<Image
|
||||
className="absolute bottom-10 right-0"
|
||||
src={getStaticImage("components/place/r-3d前进")}
|
||||
button
|
||||
onClick={() => {
|
||||
navigate(`/${moduleName}/${starName}/place2`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 遮罩层 */}
|
||||
<ExhibitOverlay
|
||||
visible={activeExhibitId !== null}
|
||||
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
|
||||
onClose={() => {
|
||||
if (activeExhibitId !== null) {
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(activeExhibitId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
setActiveExhibitId(null);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 引导 */}
|
||||
<GuideSteps
|
||||
visible={visible}
|
||||
onVisibleChange={setVisible}
|
||||
showMask={true}
|
||||
highlightSelectors={[]}
|
||||
>
|
||||
<GuideStepsItem speaker="guide">
|
||||
同学你好,欢迎来到走进故居-对话学习模块!
|
||||
进入孙中山故居纪念馆后,我们一起来了解孙中山同志经历的7个阶段:“水乡少年
|
||||
艰辛成长”、“商务启航 投身革命” ...
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
“苏区淬炼 崭露头角”、“延安担当 经济破局”、“恢复时期 经济奠基”
|
||||
、“建设时期 务实兴邦”、“改革浪潮 擘画实业”
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SunzhongshanPlace1;
|
||||
@@ -0,0 +1,151 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
ExhibitOverlay,
|
||||
Title,
|
||||
Image,
|
||||
Cursor,
|
||||
GuideSteps,
|
||||
GuideStepsItem,
|
||||
} from "@/components";
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
const starName = "sunzhongshan";
|
||||
|
||||
const exhibitSrcMap: Record<1 | 2 | 3 | 4, string> = {
|
||||
1: "sunzhongshan/place2/铁路计划图",
|
||||
2: "sunzhongshan/place2/陆军大元帅时佩带的指挥刀",
|
||||
3: "sunzhongshan/place2/917.9.10孙中山就任中华民国军政府海陆军大元帅留影",
|
||||
4: "sunzhongshan/place2/实业计划",
|
||||
};
|
||||
|
||||
const SunzhongshanPlace2 = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | 3 | 4 | null>(
|
||||
null
|
||||
);
|
||||
const [visited, setVisited] = useState<Set<number>>(new Set());
|
||||
const [guideVisible, setGuideVisible] = useState(false);
|
||||
const [guideStep, setGuideStep] = useState(0);
|
||||
|
||||
const handleOpenExhibit = (index: 1 | 2 | 3 | 4) => {
|
||||
// if (index === 1) {
|
||||
// setActiveExhibitId(1);
|
||||
// setGuideVisible(true);
|
||||
// setGuideStep(0);
|
||||
// } else {
|
||||
setActiveExhibitId(index);
|
||||
// }
|
||||
};
|
||||
|
||||
const handleGuideStepChange = (step: number) => {
|
||||
setGuideStep(step);
|
||||
};
|
||||
|
||||
const progressCount = visited.size;
|
||||
const total = 4;
|
||||
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;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`sunzhongshan/place2/背景`),
|
||||
}}
|
||||
>
|
||||
<Title
|
||||
title="讨袁护法·捍卫共和"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
/>
|
||||
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/place1`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Cursor
|
||||
className="absolute left-160 top-114"
|
||||
onClick={() => handleOpenExhibit(1)}
|
||||
read={visited.has(1)}
|
||||
/>
|
||||
<Cursor
|
||||
className="absolute left-235 bottom-95"
|
||||
onClick={() => handleOpenExhibit(2)}
|
||||
read={visited.has(2)}
|
||||
/>
|
||||
<Cursor
|
||||
className="absolute left-241.5 top-119"
|
||||
onClick={() => handleOpenExhibit(3)}
|
||||
read={visited.has(3)}
|
||||
/>
|
||||
<Cursor
|
||||
className="absolute right-120 bottom-78"
|
||||
onClick={() => handleOpenExhibit(4)}
|
||||
read={visited.has(4)}
|
||||
/>
|
||||
|
||||
{completed && (
|
||||
<Image
|
||||
className="absolute bottom-5 right-115"
|
||||
src={getStaticImage("components/place/r-前进")}
|
||||
button
|
||||
onClick={() => {
|
||||
navigate(`/${moduleName}/${starName}/place3`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ExhibitOverlay
|
||||
visible={activeExhibitId !== null}
|
||||
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
|
||||
onClose={() => {
|
||||
if (activeExhibitId !== null) {
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(activeExhibitId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
setActiveExhibitId(null);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* <GuideSteps
|
||||
visible={guideVisible}
|
||||
onVisibleChange={setGuideVisible}
|
||||
currentStep={guideStep}
|
||||
onStepChange={handleGuideStepChange}
|
||||
showMask={false}
|
||||
highlightSelectors={[]}
|
||||
zIndex={10000}
|
||||
>
|
||||
<GuideStepsItem speaker="student">
|
||||
孙中山先生是如何讨袁护法、捍卫共和的?
|
||||
</GuideStepsItem>
|
||||
<GuideStepsItem speaker="guide">
|
||||
让我们继续探索孙中山先生讨袁护法、捍卫共和的历程...
|
||||
</GuideStepsItem>
|
||||
</GuideSteps> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SunzhongshanPlace2;
|
||||
@@ -0,0 +1,116 @@
|
||||
import { useState } from "react";
|
||||
import { ExhibitOverlay, Title, Image, Cursor } from "@/components";
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
const starName = "sunzhongshan";
|
||||
|
||||
const exhibitSrcMap: Record<1 | 2 | 3 | 4, string> = {
|
||||
1: "sunzhongshan/place3/李大钊给胡适的信",
|
||||
2: "sunzhongshan/place3/中国国民党第一次全国代表大会宣言",
|
||||
3: "sunzhongshan/place3/星期评论",
|
||||
4: "sunzhongshan/place3/社会主义的精义",
|
||||
};
|
||||
|
||||
const SunzhongshanPlace3 = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | 3 | 4 | null>(
|
||||
null
|
||||
);
|
||||
const [visited, setVisited] = useState<Set<number>>(new Set());
|
||||
|
||||
const handleOpenExhibit = (index: 1 | 2 | 3 | 4) => {
|
||||
setActiveExhibitId(index);
|
||||
};
|
||||
|
||||
const progressCount = visited.size;
|
||||
const total = 4;
|
||||
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;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`sunzhongshan/place3/背景`),
|
||||
}}
|
||||
>
|
||||
<Title
|
||||
title="与时俱进·探索新路"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
/>
|
||||
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/place2`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Cursor
|
||||
className="absolute left-198 top-118"
|
||||
onClick={() => handleOpenExhibit(1)}
|
||||
read={visited.has(1)}
|
||||
/>
|
||||
<Cursor
|
||||
className="absolute left-210 top-140"
|
||||
onClick={() => handleOpenExhibit(2)}
|
||||
read={visited.has(2)}
|
||||
/>
|
||||
|
||||
<Cursor
|
||||
className="absolute right-182 bottom-95"
|
||||
onClick={() => handleOpenExhibit(3)}
|
||||
read={visited.has(3)}
|
||||
/>
|
||||
<Cursor
|
||||
className="absolute right-158 bottom-95"
|
||||
onClick={() => handleOpenExhibit(4)}
|
||||
read={visited.has(4)}
|
||||
/>
|
||||
|
||||
{completed && (
|
||||
<Image
|
||||
className="absolute bottom-0 right-110"
|
||||
src={getStaticImage("components/place/r-前进")}
|
||||
button
|
||||
onClick={() => {
|
||||
navigate(`/${moduleName}/${starName}/place4`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ExhibitOverlay
|
||||
visible={activeExhibitId !== null}
|
||||
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
|
||||
onClose={() => {
|
||||
if (activeExhibitId !== null) {
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(activeExhibitId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
setActiveExhibitId(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SunzhongshanPlace3;
|
||||
@@ -0,0 +1,101 @@
|
||||
import { useState } from "react";
|
||||
import { ExhibitOverlay, Title, Image, Cursor } from "@/components";
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
const starName = "sunzhongshan";
|
||||
|
||||
const exhibitSrcMap: Record<1 | 2, string> = {
|
||||
1: "sunzhongshan/place4/国事遗嘱",
|
||||
2: "sunzhongshan/place4/上海各界人士在外滩码头欢迎孙中山",
|
||||
};
|
||||
|
||||
const SunzhongshanPlace4 = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
const [activeExhibitId, setActiveExhibitId] = useState<1 | 2 | null>(null);
|
||||
const [visited, setVisited] = useState<Set<number>>(new Set());
|
||||
|
||||
const handleOpenExhibit = (index: 1 | 2) => {
|
||||
setActiveExhibitId(index);
|
||||
};
|
||||
|
||||
const progressCount = visited.size;
|
||||
const total = 2;
|
||||
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;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`sunzhongshan/place4/背景`),
|
||||
}}
|
||||
>
|
||||
<Title
|
||||
title="抱病北上·鞠躬尽瘁"
|
||||
subtitle={subtitle}
|
||||
subtitleClassName={subtitleClassName}
|
||||
className="absolute top-10 left-1/2 -translate-x-1/2"
|
||||
/>
|
||||
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/place3`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Cursor
|
||||
className="absolute right-209 top-128"
|
||||
onClick={() => handleOpenExhibit(1)}
|
||||
read={visited.has(1)}
|
||||
/>
|
||||
<Cursor
|
||||
className="absolute right-150 top-88"
|
||||
onClick={() => handleOpenExhibit(2)}
|
||||
read={visited.has(2)}
|
||||
/>
|
||||
|
||||
{completed && (
|
||||
<Image
|
||||
className="absolute bottom-0 right-140"
|
||||
src={getStaticImage("components/place/l-前进")}
|
||||
button
|
||||
onClick={() => {
|
||||
navigate(`/${moduleName}/${starName}`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ExhibitOverlay
|
||||
visible={activeExhibitId !== null}
|
||||
src={activeExhibitId ? exhibitSrcMap[activeExhibitId] : ""}
|
||||
onClose={() => {
|
||||
if (activeExhibitId !== null) {
|
||||
setVisited((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(activeExhibitId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
setActiveExhibitId(null);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SunzhongshanPlace4;
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { Image } from "@/components";
|
||||
import { getBackgroundImage, getStaticImage } from "@/lib/utils";
|
||||
|
||||
const starName = "sunzhongshan";
|
||||
|
||||
const SunzhongshanReflection = () => {
|
||||
const navigate = useNavigate();
|
||||
const { moduleName } = useParams<{
|
||||
moduleName: string;
|
||||
}>();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: getBackgroundImage(`${starName}/reflection/底图`),
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
className="absolute right-5 top-5"
|
||||
src={getStaticImage("components/离开")}
|
||||
button
|
||||
onClick={() => {
|
||||
if (moduleName && starName) {
|
||||
navigate(`/${moduleName}/${starName}/home`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-white text-4xl">孙中山故居-体悟模块(待完善)</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SunzhongshanReflection;
|
||||
|
||||
Reference in new issue
Block a user