feat: 鲁迅体悟场景1、场景2

This commit is contained in:
zhongyidian committed 2026-01-04 01:39:39 +08:00
1 parent dd1cabfb87
commit 3611a0857a
7 files changed
+656 -257

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 KiB

+2 -2
View File
@@ -379,7 +379,7 @@ const BambooPuzzleGame = ({
/> />
) : ( ) : (
<div className="w-full h-full flex items-center justify-center"> <div className="w-full h-full flex items-center justify-center">
<div className="text-white/30 text-xs"> <div className="text-white/30 text-4xl">
拖拽立柱到这里 拖拽立柱到这里
</div> </div>
</div> </div>
@@ -432,7 +432,7 @@ const BambooPuzzleGame = ({
/> />
) : ( ) : (
<div className="w-full h-full flex items-center justify-center"> <div className="w-full h-full flex items-center justify-center">
<div className="text-white/30 text-xs"> <div className="text-white/30 text-4xl">
拖拽立柱到这里 拖拽立柱到这里
</div> </div>
</div> </div>
+7 -5
View File
@@ -417,12 +417,13 @@ const DragFill = forwardRef<HTMLDivElement, DragFillProps>(
<div <div
className={cn( className={cn(
layout === "horizontal-options" layout === "horizontal-options"
? "flex flex-row gap-6 flex-wrap w-full" ? optionClassName || "flex flex-row gap-6 flex-wrap w-full"
: cn( : cn(
"grid gap-x-12 gap-y-16", optionClassName,
normalizedOptions.length > 2 !optionClassName && "grid gap-x-12 gap-y-16",
!optionClassName && normalizedOptions.length > 2
? "grid-cols-2" ? "grid-cols-2"
: "grid-cols-1" : !optionClassName && "grid-cols-1"
) )
)} )}
> >
@@ -439,7 +440,8 @@ const DragFill = forwardRef<HTMLDivElement, DragFillProps>(
} }
}} }}
className={cn( className={cn(
renderOption && "select-none flex-1", renderOption && "select-none",
!optionClassName && renderOption && "flex-1",
isDragging && "opacity-50" isDragging && "opacity-50"
)} )}
> >
+73
View File
@@ -151,3 +151,76 @@ button:not([disabled]){
opacity: 0; opacity: 0;
} }
/* 鲁迅页面滚动条样式 - 古典文化风格 */
.luxun-scrollbar {
scrollbar-width: thin;
scrollbar-color: rgba(212, 175, 55, 0.85) rgba(250, 245, 235, 0.08);
}
.luxun-scrollbar::-webkit-scrollbar {
width: 14px;
}
.luxun-scrollbar::-webkit-scrollbar-track {
background: linear-gradient(
to bottom,
rgba(250, 245, 235, 0.06),
rgba(240, 235, 225, 0.04)
);
border-radius: 12px;
border: 1.5px solid rgba(139, 115, 71, 0.2);
margin: 6px 2px;
box-shadow:
inset 0 0 8px rgba(139, 115, 71, 0.08),
inset 0 0 2px rgba(255, 255, 255, 0.1);
}
.luxun-scrollbar::-webkit-scrollbar-thumb {
background: linear-gradient(
to bottom,
rgba(240, 190, 117, 0.95),
rgba(212, 175, 55, 0.95),
rgba(184, 134, 11, 0.95)
);
border-radius: 12px;
border: 2px solid rgba(250, 245, 235, 0.4);
box-shadow:
inset 0 1px 3px rgba(255, 255, 255, 0.25),
inset 0 -1px 2px rgba(139, 115, 71, 0.2),
0 2px 6px rgba(139, 115, 71, 0.25),
0 0 10px rgba(212, 175, 55, 0.15);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
}
/* 滚动条滑块内部装饰线效果 */
.luxun-scrollbar::-webkit-scrollbar-thumb:hover {
background: linear-gradient(
to bottom,
rgba(255, 215, 140, 1),
rgba(240, 190, 117, 1),
rgba(212, 175, 55, 1)
);
box-shadow:
inset 0 1px 4px rgba(255, 255, 255, 0.35),
inset 0 -1px 3px rgba(139, 115, 71, 0.25),
0 3px 8px rgba(139, 115, 71, 0.35),
0 0 14px rgba(212, 175, 55, 0.3);
border-color: rgba(250, 245, 235, 0.6);
transform: scaleX(1.05);
}
.luxun-scrollbar::-webkit-scrollbar-thumb:active {
background: linear-gradient(
to bottom,
rgba(184, 134, 11, 1),
rgba(139, 115, 71, 1),
rgba(107, 93, 71, 1)
);
box-shadow:
inset 0 2px 5px rgba(0, 0, 0, 0.4),
inset 0 -1px 2px rgba(139, 115, 71, 0.3),
0 1px 3px rgba(139, 115, 71, 0.3);
border-color: rgba(250, 245, 235, 0.2);
}
+191 -47
View File
@@ -1,17 +1,69 @@
import { useState } from "react"; import { useState, useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { Image } from "@/components"; import { Image } from "@/components";
import SuccessAlert from "@/components/layout/success-alert";
import { getBackgroundImage, getStaticImage } from "@/lib/utils"; import { getBackgroundImage, getStaticImage } from "@/lib/utils";
const starName = "luxun"; const starName = "luxun";
const cityMap: Record<1 | 2 | 3 | 4 | 5, string> = { // 城市配置 - 根据效果图调整位置
1: "luxun/reflection/scene1/绍兴", // 布局:北京(上) -> 上海(中上) -> 绍兴(中) -> 广州(左下) -> 厦门(右下)
2: "luxun/reflection/scene1/北京", const cityConfig = [
3: "luxun/reflection/scene1/厦门", {
4: "luxun/reflection/scene1/广州", id: 1,
5: "luxun/reflection/scene1/上海", name: "绍兴",
}; location: "绍兴三味书屋",
imagePath: "luxun/reflection/scene1/绍兴",
position: { top: "58%", left: "60%" }, // 中间
iconPosition: { top: "0%", left: "0%" },
time: "1892年-1898年",
description: "少年鲁迅在此求学,为《从百草园到三味书屋》埋下创作种子。",
},
{
id: 2,
name: "北京",
location: "北京八道湾胡同",
imagePath: "luxun/reflection/scene1/北京",
position: { top: "14%", left: "50%" }, // 顶部
iconPosition: { top: "43%", left: "0%" },
time: "1919-1923年",
description:
"鲁迅参与新文化运动的核心时期,《阿Q正传》等经典作品在此酝酿创作。",
},
{
id: 3,
name: "厦门",
location: "厦门大学",
imagePath: "luxun/reflection/scene1/厦门",
position: { top: "80%", left: "70%" }, // 右下
iconPosition: { top: "16%", left: "40%" },
time: "1926年9-12月",
description:
"鲁迅应邀前往厦门大学任教,期间写下多篇批判现实的杂文,关注底层民众命运。",
},
{
id: 4,
name: "广州",
location: "广州白云楼",
imagePath: "luxun/reflection/scene1/广州",
position: { top: "80%", left: "30%" }, // 左下
iconPosition: { top: "16%", left: "30%" },
time: "1927年3-9月",
description:
"先生在此整理旧作,思考民族觉醒之路,《朝花夕拾》部分篇章定稿于此。",
},
{
id: 5,
name: "上海",
location: "上海鲁迅故居",
imagePath: "luxun/reflection/scene1/上海",
position: { top: "40%", left: "60%" }, // 中上
iconPosition: { top: "0%", left: "0%" },
time: "1933-1936年",
description:
"鲁迅晚年定居地,先生在此写下《故事新编》等作品,与进步文人并肩,以文字为刃批判黑暗。",
},
];
const LuxunReflectionScene1 = () => { const LuxunReflectionScene1 = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -19,17 +71,50 @@ const LuxunReflectionScene1 = () => {
moduleName: string; moduleName: string;
}>(); }>();
// 已点击的城市ID集合
const [clickedCities, setClickedCities] = useState<Set<number>>(new Set());
// 是否显示成功提示
const [showSuccessAlert, setShowSuccessAlert] = useState(false);
// 是否全部完成
const [allCompleted, setAllCompleted] = useState(false);
const progress = clickedCities.size;
const totalTasks = cityConfig.length;
// 检查是否全部完成
useEffect(() => {
if (progress === totalTasks && !allCompleted) {
setAllCompleted(true);
setShowSuccessAlert(true);
}
}, [progress, totalTasks, allCompleted]);
const handleCityClick = (cityId: number) => {
if (clickedCities.has(cityId)) return; // 已点击过的不再处理
setClickedCities((prev) => {
const next = new Set(prev);
next.add(cityId);
return next;
});
};
const handleSuccessAlertClose = () => {
setShowSuccessAlert(false);
};
return ( return (
<div <div
className="flex justify-between w-full h-full bg-cover bg-center bg-no-repeat gap-8" className="flex justify-between w-full h-full bg-cover bg-center bg-no-repeat gap-8 relative"
style={{ style={{
backgroundImage: getBackgroundImage( backgroundImage: getBackgroundImage(
`${starName}/reflection/scene1/底图` `${starName}/reflection/scene1/底图`
), ),
}} }}
> >
{/* 离开按钮 */}
<Image <Image
className="absolute right-5 top-5" className="absolute right-5 top-5 z-10"
src={getStaticImage(`${starName}/reflection/scene1/离开`)} src={getStaticImage(`${starName}/reflection/scene1/离开`)}
button button
onClick={() => { onClick={() => {
@@ -39,60 +124,119 @@ const LuxunReflectionScene1 = () => {
}} }}
/> />
{/* 左侧 */} {/* 左侧:鲁迅的文学足迹 - 地图区域 */}
<div className="w-[45%] relative"> <div className="w-[45%] relative">
<div className="absolute"> {/* 地图容器 */}
<img <div className="relative w-full h-full">
className="absolute inset-0" {cityConfig.map((city) => {
src={getStaticImage(`${starName}/reflection/scene1/北京`)} const isClicked = clickedCities.has(city.id);
alt="" return (
/> <div
{/* 两个拖拽接收区域 */} key={city.id}
<div className=""></div> className="absolute flex flex-col items-center cursor-pointer hover:scale-110 transition-all duration-300"
<div className="border border-red-500"></div> style={{
</div> top: city.position.top,
left: city.position.left,
transform: "translate(-50%, -50%)",
}}
onClick={() => handleCityClick(city.id)}
>
{/* 城市图片容器 */}
<div className="relative"> <div className="relative">
<img <img
className="absolute inset-0" src={getStaticImage(city.imagePath)}
src={getStaticImage(`${starName}/reflection/scene1/上海`)} alt={city.name}
alt="" className={`transition-all duration-300 ${
isClicked ? "brightness-100" : "brightness-50 grayscale"
}`}
style={{
filter: isClicked
? "none"
: "grayscale(100%) brightness(0.5)",
}}
/> />
</div>
<div> {/* 点击图标和提示 - 覆盖在城市图片上 */}
<div
className="absolute -translate-x-full -translate-y-full flex flex-col items-center z-10 pointer-events-none"
style={{
left: city.iconPosition.left,
top: city.iconPosition.top,
}}
>
{/* 点一点提示 - 始终显示 */}
{!isClicked && (
<div className="flex items-center">
<img <img
src={getStaticImage(`${starName}/reflection/scene1/绍兴`)} src={getStaticImage(
alt="" `${starName}/reflection/scene1/鲁迅体悟改版点一点`
)}
alt="点一点"
/> />
</div>
<div>
<img <img
src={getStaticImage(`${starName}/reflection/scene1/厦门`)} className="rotate-y-[180deg] rotate-z-[180deg]"
alt="" src={getStaticImage(
`${starName}/reflection/scene1/鲁迅体悟点一点图标`
)}
alt="点一点图标"
/> />
</div> </div>
<div> )}
<img
src={getStaticImage(`${starName}/reflection/scene1/广州`)}
alt=""
/>
</div> </div>
</div> </div>
{/* 右侧 */} </div>
<div className="flex-1"> );
})}
</div>
</div>
{/* 右侧区域 */}
<div className="flex-1 relative">
{/* 任务进度 */} {/* 任务进度 */}
<div> <div className="absolute top-5 left-[24%] w-[445px] h-55 flex justify-center items-center">
<span className="text-white text-2xl font-bold [text-shadow:_6px_6px_4px_#b37921,_6px_6px_4px_#b37921,_6px_6px_4px_#b37921,_6px_6px_4px_#b37921]"> <span className="text-white text-[54px] font-bold [text-shadow:_0px_0px_4px_#b37921,_6px_6px_4px_#b37921,_6px_6px_4px_#b37921,_6px_6px_4px_#b37921]">
任务进度:(0/8) {allCompleted
? "任务进度已完成"
: `任务进度:(${progress}/${totalTasks})`}
</span> </span>
</div> </div>
{/* 鲁迅画廊-拖拽区域 */}
<div></div>
{/* 任务展示窗口 */} {/* 任务展示窗口 */}
<div className="flex justify-between"> <div className="absolute bottom-0 right-7 w-[995px] h-[728px] font-bold text-7xl space-y-16 px-10 py-10 overflow-y-auto luxun-scrollbar">
<div>任务展示窗口</div> {allCompleted
<div></div> ? // 全部完成后显示详细信息
cityConfig.map((city) => (
<div key={city.id} className="space-y-4">
<div className="text-red-600 text-6xl">{city.location}</div>
<div className="text-gray-600 text-4xl font-normal">
{city.time}
</div>
<div className="text-black text-4xl font-normal leading-relaxed">
{city.description}
</div> </div>
</div> </div>
))
: // 未完成时显示任务列表
cityConfig.map((city, index) => {
const isCompleted = clickedCities.has(city.id);
return (
<div
key={city.id}
className={isCompleted ? "text-green-600 line-through" : ""}
>
{index + 1}.点击图标{city.location}
</div>
);
})}
</div>
</div>
{/* 成功提示弹窗 */}
<SuccessAlert
visible={showSuccessAlert}
title="恭喜你, 完成所有任务!"
onClose={handleSuccessAlertClose}
onContinue={handleSuccessAlertClose}
/>
</div> </div>
); );
}; };
+383 -203
View File
@@ -1,29 +1,132 @@
import { useState, useCallback, useMemo, useEffect, useRef } from "react"; import { useState, useEffect, useMemo, useCallback } from "react";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { createPortal } from "react-dom"; import { Image, DragFill } from "@/components";
import { AnimatePresence, motion } from "framer-motion";
import { Title, Image, Cursor, DragFill } from "@/components";
import type { DragFillResult } from "@/components"; import type { DragFillResult } from "@/components";
import { cn, getBackgroundImage, getStaticImage } from "@/lib/utils"; import SuccessAlert from "@/components/layout/success-alert";
import { useScale } from "@/contexts/ScaleContext"; import { getBackgroundImage, getStaticImage } from "@/lib/utils";
const starName = "luxun"; const starName = "luxun";
// 文学作品拖拽选项 // 拖拽元素配置 - 每个元素对应一个特定的标签放置区域
const literatureOptions = [ const dragItems = [
{ value: "朝花夕拾", label: "朝花夕拾", image: "luxun/reflection/scene2/拖拽-朝花夕拾" }, {
{ value: "阿Q正传", label: "阿Q正传", image: "luxun/reflection/scene2/拖拽-阿Q正传" }, id: "runtu",
{ value: "故事新编", label: "故事新编", image: "luxun/reflection/scene2/拖拽-故事新编" }, name: "闰土",
{ value: "百草园到三味书屋", label: "百草园到三味书屋", image: "luxun/reflection/scene2/拖拽-百草园到三味书屋" }, imagePath: "luxun/reflection/scene2/拖拽-润土",
{ value: "精神胜利法", label: "精神胜利法", image: "luxun/reflection/scene2/拖拽-精神胜利法" }, targetBlankId: "city-1-label-0", // 绍兴-人物
{ value: "润土", label: "润土", image: "luxun/reflection/scene2/拖拽-润土" }, },
{
id: "baicaoyuan",
name: "《从百草园到三味书屋》",
imagePath: "luxun/reflection/scene2/拖拽-百草园到三味书屋",
targetBlankId: "city-1-label-1", // 绍兴-作品
},
{
id: "aqzhengzhuan",
name: "《阿Q正传》",
imagePath: "luxun/reflection/scene2/拖拽-阿Q正传",
targetBlankId: "city-2-label-0", // 北京-作品
},
{
id: "jingshenshengli",
name: "“精神胜利法”",
imagePath: "luxun/reflection/scene2/拖拽-精神胜利法",
targetBlankId: "city-2-label-1", // 北京-核心主题
},
{
id: "guanzhudiceng",
name: "“关注底层”",
imagePath: "luxun/reflection/scene2/关注底层",
targetBlankId: "city-3-label-0", // 厦门-创作方向
},
{
id: "zhaohuaxishi",
name: "《朝花夕拾》",
imagePath: "luxun/reflection/scene2/拖拽-朝花夕拾",
targetBlankId: "city-4-label-0", // 广州-作品集
},
{
id: "gushixinbian",
name: "《故事新编》",
imagePath: "luxun/reflection/scene2/拖拽-故事新编",
targetBlankId: "city-5-label-0", // 上海-作品
},
{
id: "biweidaoqiang",
name: "“笔为刀枪”",
imagePath: "luxun/reflection/scene2/笔为刀枪",
targetBlankId: "city-5-label-1", // 上海-创作中心
},
]; ];
// 目标位置 // 城市配置 - 根据效果图调整位置
const targetBlanks = [ // 布局:北京(上) -> 上海(中上) -> 绍兴(中) -> 广州(左下) -> 厦门(右下)
{ id: "blank1", label: "位置1" }, const cityConfig = [
{ id: "blank2", label: "位置2" }, {
{ id: "blank3", label: "位置3" }, id: 1,
name: "绍兴",
location: "绍兴三味书屋",
imagePath: "luxun/reflection/scene2/绍兴",
position: { top: "58%", left: "60%" }, // 中间
labels: [
{ text: "人物", containerPosition: { top: "0%", left: "-20%" } },
{ text: "作品", containerPosition: { top: "0%", left: "20%" } },
],
time: "1892年-1898年",
description: "少年鲁迅在此求学,为《从百草园到三味书屋》埋下创作种子。",
},
{
id: 2,
name: "北京",
location: "北京八道湾胡同",
imagePath: "luxun/reflection/scene2/北京",
position: { top: "14%", left: "50%" }, // 顶部
labels: [
{ text: "作品", containerPosition: { top: "0%", left: "-20%" } },
{ text: "核心主题", containerPosition: { top: "0%", left: "20%" } },
],
time: "1919-1923年",
description:
"鲁迅参与新文化运动的核心时期,《阿Q正传》等经典作品在此酝酿创作。",
},
{
id: 3,
name: "厦门",
location: "厦门大学",
imagePath: "luxun/reflection/scene2/厦门",
position: { top: "80%", left: "70%" }, // 右下
labels: [
{ text: "创作方向", containerPosition: { top: "0%", left: "0%" } },
],
time: "1926年9-12月",
description:
"鲁迅应邀前往厦门大学任教,期间写下多篇批判现实的杂文,关注底层民众命运。",
},
{
id: 4,
name: "广州",
location: "广州白云楼",
imagePath: "luxun/reflection/scene2/广州",
position: { top: "80%", left: "30%" }, // 左下
labels: [{ text: "作品集", containerPosition: { top: "0%", left: "0%" } }],
time: "1927年3-9月",
description:
"先生在此整理旧作,思考民族觉醒之路,《朝花夕拾》部分篇章定稿于此。",
},
{
id: 5,
name: "上海",
location: "上海鲁迅故居",
imagePath: "luxun/reflection/scene2/上海",
position: { top: "40%", left: "60%" }, // 中上
labels: [
{ text: "作品", containerPosition: { top: "0%", left: "-20%" } },
{ text: "创作中心", containerPosition: { top: "0%", left: "20%" } },
],
time: "1933-1936年",
description:
"鲁迅晚年定居地,先生在此写下《故事新编》等作品,与进步文人并肩,以文字为刃批判黑暗。",
},
]; ];
const LuxunReflectionScene2 = () => { const LuxunReflectionScene2 = () => {
@@ -32,100 +135,89 @@ const LuxunReflectionScene2 = () => {
moduleName: string; moduleName: string;
}>(); }>();
const [showDialog, setShowDialog] = useState(false); // DragFill 的值
const [visited, setVisited] = useState<Set<number>>(new Set()); const [dragFillValue, setDragFillValue] = useState<DragFillResult[]>([]);
const [dragValue, setDragValue] = useState<DragFillResult[]>([]);
const completedRef = useRef(false);
const { scale, offsetX, offsetY, DESIGN_WIDTH, DESIGN_HEIGHT } = useScale();
const [mounted, setMounted] = useState(false);
useEffect(() => { // 是否显示成功提示
setMounted(true); const [showSuccessAlert, setShowSuccessAlert] = useState(false);
}, []); // 是否全部完成
const [allCompleted, setAllCompleted] = useState(false);
// 重置游戏状态 // 转换为 DragFill 的 options
useEffect(() => { const options = useMemo(
if (showDialog) { () =>
setDragValue([]); dragItems.map((item) => ({
completedRef.current = false; value: item.id,
} label: item.name,
}, [showDialog]); })),
// 检查是否完成
useEffect(() => {
if (dragValue.length === 3 && !completedRef.current) {
completedRef.current = true;
setTimeout(() => {
setVisited((prev) => {
const next = new Set(prev);
next.add(1);
return next;
});
setShowDialog(false);
}, 500);
}
}, [dragValue]);
const progressCount = visited.size;
const total = 1;
const completed = progressCount >= total;
const subtitle = completed
? "探索文学作品·精神传承(已完成)"
: `探索文学作品·精神传承(${progressCount}/${total})`;
const subtitleClassName = completed
? "text-[#fff998] [text-shadow:_0_8px_8px_rgba(0,0,0,0.35),_0_0_16px_rgba(0,0,0,0.25)]"
: undefined;
const handleValueChange = useCallback((newValue: DragFillResult[]) => {
if (newValue.length <= 3) {
setDragValue(newValue);
}
}, []);
const maskSrc = getStaticImage("components/蒙版遮罩");
const backSrc = getStaticImage("components/关闭");
const overlayStyle = useMemo<React.CSSProperties>(
() => ({
position: "fixed",
inset: 0,
zIndex: 9999,
pointerEvents: "none",
}),
[] []
); );
const scaledContainerStyle = useMemo<React.CSSProperties>( // 转换为 DragFill 的 blanks - 为每个标签创建独立的空白区域
() => ({ const blanks = useMemo(() => {
position: "absolute", const allBlanks: Array<{ id: string; label: string }> = [];
width: `${DESIGN_WIDTH}px`, cityConfig.forEach((city) => {
height: `${DESIGN_HEIGHT}px`, city.labels.forEach((label, index) => {
left: `${offsetX}px`, const labelText = typeof label === "string" ? label : label.text;
top: `${offsetY}px`, allBlanks.push({
transform: `scale(${scale})`, id: `city-${city.id}-label-${index}`,
transformOrigin: "top left", label: labelText,
overflow: "hidden", });
}), });
[scale, offsetX, offsetY, DESIGN_WIDTH, DESIGN_HEIGHT] });
); return allBlanks;
}, []);
// 处理拖拽结果变化 - 验证是否正确放置
const handleValueChange = useCallback((newValue: DragFillResult[]) => {
// 验证每个元素是否放置到正确的标签区域
const filteredValue = newValue.filter((result) => {
const itemId = result.optionValue;
const blankId = result.blankId;
const item = dragItems.find((item) => item.id === itemId);
return item && item.targetBlankId === blankId;
});
setDragFillValue(filteredValue);
}, []);
const progress = dragFillValue.length;
const totalTasks = dragItems.length; // 8个拖拽元素
// 检查是否全部完成
useEffect(() => {
if (progress === totalTasks && !allCompleted) {
setAllCompleted(true);
setShowSuccessAlert(true);
}
}, [progress, totalTasks, allCompleted]);
const handleSuccessAlertClose = () => {
setShowSuccessAlert(false);
};
// 获取每个标签区域已放置的元素
const getLabelPlacedItem = (cityId: number, labelIndex: number) => {
const blankId = `city-${cityId}-label-${labelIndex}`;
const result = dragFillValue.find((r) => r.blankId === blankId);
if (!result) return null;
return dragItems.find((item) => item.id === result.optionValue) || null;
};
return ( return (
<div <div
className="w-full h-full bg-cover bg-center bg-no-repeat" className="flex justify-between w-full h-full bg-cover bg-center bg-no-repeat gap-10 relative"
style={{ style={{
backgroundImage: getBackgroundImage(`${starName}/reflection/scene2/底图`), backgroundImage: getBackgroundImage(
`${starName}/reflection/scene2/底图`
),
}} }}
> >
<Title {/* 离开按钮 */}
title="跨越时空"
subtitle={subtitle}
subtitleClassName={subtitleClassName}
className="absolute top-10 left-1/2 -translate-x-1/2"
/>
<Image <Image
className="absolute right-5 top-5" className="absolute right-5 top-5 z-10"
src={getStaticImage(`${starName}/reflection/scene2/离开`)} src={getStaticImage(`${starName}/reflection/scene1/离开`)}
button button
onClick={() => { onClick={() => {
if (moduleName && starName) { if (moduleName && starName) {
@@ -134,131 +226,219 @@ const LuxunReflectionScene2 = () => {
}} }}
/> />
{/* 点击区域 - 打开拖拽弹窗 */} {/* 左侧:鲁迅的文学足迹 - 地图区域 */}
<Cursor <div className="w-[44.5%] relative">
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" {/* 地图容器 */}
onClick={() => setShowDialog(true)} <div className="relative w-full h-full">
read={visited.has(1)} {cityConfig.map((city) => {
/> return (
{completed && (
<Image
className="absolute bottom-0 right-0"
src={getStaticImage("components/place/继续前进")}
button
onClick={() => {
if (moduleName && starName) {
navigate(`/${moduleName}/${starName}/reflection3`);
}
}}
/>
)}
{/* 拖拽弹窗 */}
{mounted &&
createPortal(
<AnimatePresence>
{showDialog && (
<motion.div
key="literature-drag-dialog"
style={overlayStyle}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3, ease: "easeInOut" }}
>
<div style={scaledContainerStyle}>
<div <div
className={cn("absolute inset-0 flex flex-col", "bg-black/70")} key={city.id}
style={{ pointerEvents: "auto" }} className="absolute flex flex-col items-center"
style={{
top: city.position.top,
left: city.position.left,
transform: "translate(-50%, -50%)",
}}
> >
{/* 背景蒙版 */} {/* 城市图片容器 */}
{maskSrc && ( <div className="relative">
<Image <img src={getStaticImage(city.imagePath)} alt={city.name} />
src={maskSrc}
className="absolute inset-0 w-full h-full object-cover select-none pointer-events-none" {/* 标签区域 - 每个标签是一个独立的容器框 */}
aria-hidden <div className="absolute inset-0">
{city.labels.map((label, index) => {
const blankId = `city-${city.id}-label-${index}`;
const placedItem = getLabelPlacedItem(city.id, index);
const isFilled = !!placedItem;
const labelText =
typeof label === "string" ? label : label.text;
const containerPosition =
typeof label === "string"
? { top: "0%", left: "0%" }
: label.containerPosition;
return (
<div
key={index}
data-blank-id={blankId}
className={`absolute border-2 border-dashed rounded-lg transition-all min-w-[120px] min-h-[120px] flex flex-col items-center justify-center ${
isFilled
? "border-green-500 bg-green-500/20"
: "border-white/60 bg-white/10 hover:border-blue-400 hover:bg-blue-400/20"
}`}
style={{
top: containerPosition.top,
left: containerPosition.left,
transform: "translate(-50%, -50%)",
}}
>
{/* 已放置的元素显示在容器框内 */}
{placedItem ? (
<img
src={getStaticImage(placedItem.imagePath)}
alt={placedItem.name}
className="w-20 h-20 object-contain"
/> />
) : (
<div className="text-white/30 text-sm text-center">
{labelText}
</div>
)} )}
</div>
{/* 返回按钮 */} );
<Image })}
src={backSrc} </div>
onClick={() => setShowDialog(false)} </div>
alt="返回" </div>
className="absolute top-5 right-5 z-20" );
button })}
/> </div>
{/* 内容区域 */}
<div className="relative z-10 w-full h-full flex items-center justify-center px-30 py-16 pt-30">
<div className="w-full max-w-6xl">
<div className="text-5xl font-bold text-white mb-8 text-center">
将文学作品拖拽到对应位置
</div> </div>
{/* 右侧区域 */}
<div className="flex-1 relative">
{/* 任务进度 */}
<div className="absolute top-4 left-[27%] w-[430px] h-51 flex justify-center items-center">
<span className="text-white text-[54px] font-bold [text-shadow:_0px_0px_4px_#b37921,_6px_6px_4px_#b37921,_6px_6px_4px_#b37921,_6px_6px_4px_#b37921]">
{allCompleted
? "任务进度已完成"
: `任务进度:(${progress}/${totalTasks})`}
</span>
</div>
{/* 鲁迅画廊 */}
<div className="absolute top-[calc(2.4rem+204px)] left-0 w-[995px] h-[467px] pt-16 pl-70 pr-10">
{/* 提示图标 */}
<div className="absolute left-10 top-30 flex items-center z-10 pointer-events-none">
<img
src={getStaticImage(
`${starName}/reflection/scene2/鲁迅体悟拖一拖`
)}
alt="拖一拖"
/>
<img
src={getStaticImage(
`${starName}/reflection/scene2/鲁迅体悟拖一拖图标`
)}
alt="拖一拖图标"
/>
</div>
{/* 使用 DragFill 组件 */}
<div className="mt-10">
<DragFill <DragFill
options={literatureOptions.map((opt) => ({ options={options}
value: opt.value, blanks={blanks}
label: opt.label, value={dragFillValue}
}))}
blanks={targetBlanks}
value={dragValue}
onChange={handleValueChange} onChange={handleValueChange}
allowDuplicate={false} allowDuplicate={false}
renderOption={(option) => { showSubmitButton={false}
const opt = literatureOptions.find( layout="horizontal-options"
(o) => o.value === option.value hideLabels={true}
); className="w-full"
return ( optionClassName="grid grid-cols-4 gap-10"
<div className="flex flex-col items-center gap-2"> blankClassName="hidden" // 隐藏默认的空白区域,我们在地图上自定义显示
<Image renderOption={(option, isUsed) => {
src={getStaticImage(opt?.image || "")} const item = dragItems.find((item) => item.id === option.value);
className="w-32 h-32 object-contain" if (!item) return null;
/>
<span className="text-2xl text-white">{option.label}</span>
</div>
);
}}
renderBlank={(blank, filledValue, isHovered) => {
const filledOption = filledValue
? literatureOptions.find((opt) => opt.value === filledValue)
: null;
return ( return (
<div <div
className={cn( className={`relative transition-all duration-200 cursor-move ${
"w-48 h-48 border-2 rounded-lg flex flex-col items-center justify-center gap-2 transition-all", isUsed ? "opacity-30" : "hover:scale-105"
filledValue }`}
? "border-white bg-white/20"
: "border-dashed border-white/50 bg-white/10",
isHovered && "border-white bg-white/30"
)}
> >
{filledOption ? ( <img
<> src={getStaticImage(item.imagePath)}
<Image alt={item.name}
src={getStaticImage(filledOption.image)} className="w-full h-auto"
className="w-24 h-24 object-contain" draggable={false}
/> />
<span className="text-white text-lg">{filledOption.label}</span>
</>
) : (
<span className="text-white text-xl">{blank.label}</span>
)}
</div> </div>
); );
}} }}
renderDragging={(option) => {
const item = dragItems.find((item) => item.id === option.value);
if (!item) return null;
return (
<img
src={getStaticImage(item.imagePath)}
alt={item.name}
className="w-20 h-20 object-contain shadow-lg"
draggable={false}
/>
);
}}
renderBlank={(blank, _filledValue, _isHovered) => {
// 空白区域在地图上显示,返回一个隐藏的占位元素
// DragFill 会通过 data-blank-id 属性来识别空白区域
return (
<div
data-blank-id={blank.id}
className="hidden"
style={{ display: "none" }}
/>
);
}}
/> />
</div> </div>
</div> </div>
{/* 任务展示窗口 */}
<div className="absolute bottom-3.5 right-8 w-[995px] h-[335px] px-10 py-4 overflow-y-auto luxun-scrollbar">
{allCompleted ? (
// 全部完成后显示详细信息
<div className="space-y-8">
{cityConfig.map((city) => (
<div key={city.id} className="space-y-4">
<div className="text-red-600 text-6xl font-bold">
{city.location}
</div>
<div className="text-gray-600 text-4xl font-normal">
{city.time}
</div>
<div className="text-black text-4xl font-normal leading-relaxed">
{city.description}
</div>
</div>
))}
</div>
) : (
// 未完成时显示提示内容
<div className="space-y-8 text-center">
<div className="text-black text-5xl font-bold">任务展示窗口</div>
<div className="text-red-600 text-4xl font-bold">
请将以下内容拖拽至正确位置
</div>
<div className="grid grid-cols-4 gap-4 text-3xl font-bold">
{dragItems.map((item) => {
const isPlaced = dragFillValue.some(
(result) => result.optionValue === item.id
);
return (
<div
key={item.id}
className={`${
isPlaced ? "text-green-600 line-through" : "text-black"
}`}
>
{item.name}
</div>
);
})}
</div> </div>
</div> </div>
</motion.div>
)}
</AnimatePresence>,
document.body
)} )}
</div> </div>
</div>
{/* 成功提示弹窗 */}
<SuccessAlert
visible={showSuccessAlert}
title="恭喜你, 完成所有任务!"
onClose={handleSuccessAlertClose}
onContinue={handleSuccessAlertClose}
/>
</div>
); );
}; };