diff --git a/src/assets/images/luxun/reflection/scene3/效果图1.jpg b/src/assets/images/luxun/reflection/scene3/效果图1.jpg new file mode 100644 index 0000000..8c67b05 Binary files /dev/null and b/src/assets/images/luxun/reflection/scene3/效果图1.jpg differ diff --git a/src/assets/images/luxun/reflection/scene3/效果图2.jpg b/src/assets/images/luxun/reflection/scene3/效果图2.jpg new file mode 100644 index 0000000..a656879 Binary files /dev/null and b/src/assets/images/luxun/reflection/scene3/效果图2.jpg differ diff --git a/src/components/game/BambooPuzzleGame.tsx b/src/components/game/BambooPuzzleGame.tsx index 39d0204..6af5e41 100644 --- a/src/components/game/BambooPuzzleGame.tsx +++ b/src/components/game/BambooPuzzleGame.tsx @@ -379,7 +379,7 @@ const BambooPuzzleGame = ({ /> ) : (
-
+
拖拽立柱到这里
@@ -432,7 +432,7 @@ const BambooPuzzleGame = ({ /> ) : (
-
+
拖拽立柱到这里
diff --git a/src/components/quiz/drag-fill/drag-fill.tsx b/src/components/quiz/drag-fill/drag-fill.tsx index 54fb662..107d10f 100644 --- a/src/components/quiz/drag-fill/drag-fill.tsx +++ b/src/components/quiz/drag-fill/drag-fill.tsx @@ -417,12 +417,13 @@ const DragFill = forwardRef(
2 + optionClassName, + !optionClassName && "grid gap-x-12 gap-y-16", + !optionClassName && normalizedOptions.length > 2 ? "grid-cols-2" - : "grid-cols-1" + : !optionClassName && "grid-cols-1" ) )} > @@ -439,7 +440,8 @@ const DragFill = forwardRef( } }} className={cn( - renderOption && "select-none flex-1", + renderOption && "select-none", + !optionClassName && renderOption && "flex-1", isDragging && "opacity-50" )} > diff --git a/src/index.css b/src/index.css index efd17ba..5d740e1 100644 --- a/src/index.css +++ b/src/index.css @@ -151,3 +151,76 @@ button:not([disabled]){ 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); +} + diff --git a/src/pages/luxun/reflection/scene1.tsx b/src/pages/luxun/reflection/scene1.tsx index bccd090..3b5da4b 100644 --- a/src/pages/luxun/reflection/scene1.tsx +++ b/src/pages/luxun/reflection/scene1.tsx @@ -1,17 +1,69 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { Image } from "@/components"; +import SuccessAlert from "@/components/layout/success-alert"; import { getBackgroundImage, getStaticImage } from "@/lib/utils"; const starName = "luxun"; -const cityMap: Record<1 | 2 | 3 | 4 | 5, string> = { - 1: "luxun/reflection/scene1/绍兴", - 2: "luxun/reflection/scene1/北京", - 3: "luxun/reflection/scene1/厦门", - 4: "luxun/reflection/scene1/广州", - 5: "luxun/reflection/scene1/上海", -}; +// 城市配置 - 根据效果图调整位置 +// 布局:北京(上) -> 上海(中上) -> 绍兴(中) -> 广州(左下) -> 厦门(右下) +const cityConfig = [ + { + id: 1, + 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 navigate = useNavigate(); @@ -19,17 +71,50 @@ const LuxunReflectionScene1 = () => { moduleName: string; }>(); + // 已点击的城市ID集合 + const [clickedCities, setClickedCities] = useState>(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 (
+ {/* 离开按钮 */} { @@ -39,60 +124,119 @@ const LuxunReflectionScene1 = () => { }} /> - {/* 左侧 */} + {/* 左侧:鲁迅的文学足迹 - 地图区域 */}
-
- - {/* 两个拖拽接收区域 */} -
-
-
-
- -
-
- -
-
- -
-
- + {/* 地图容器 */} +
+ {cityConfig.map((city) => { + const isClicked = clickedCities.has(city.id); + return ( +
handleCityClick(city.id)} + > + {/* 城市图片容器 */} +
+ {city.name} + + {/* 点击图标和提示 - 覆盖在城市图片上 */} +
+ {/* 点一点提示 - 始终显示 */} + {!isClicked && ( +
+ 点一点 + 点一点图标 +
+ )} +
+
+
+ ); + })}
- {/* 右侧 */} -
+ + {/* 右侧区域 */} +
{/* 任务进度 */} -
- - 任务进度:(0/8) +
+ + {allCompleted + ? "任务进度已完成" + : `任务进度:(${progress}/${totalTasks})`}
- {/* 鲁迅画廊-拖拽区域 */} -
{/* 任务展示窗口 */} -
-
任务展示窗口
-
+
+ {allCompleted + ? // 全部完成后显示详细信息 + cityConfig.map((city) => ( +
+
{city.location}
+
+ {city.time} +
+
+ {city.description} +
+
+ )) + : // 未完成时显示任务列表 + cityConfig.map((city, index) => { + const isCompleted = clickedCities.has(city.id); + return ( +
+ {index + 1}.点击图标{city.location} +
+ ); + })}
+ + {/* 成功提示弹窗 */} +
); }; diff --git a/src/pages/luxun/reflection/scene2.tsx b/src/pages/luxun/reflection/scene2.tsx index 89ad1f6..c106a5e 100644 --- a/src/pages/luxun/reflection/scene2.tsx +++ b/src/pages/luxun/reflection/scene2.tsx @@ -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 { createPortal } from "react-dom"; -import { AnimatePresence, motion } from "framer-motion"; -import { Title, Image, Cursor, DragFill } from "@/components"; +import { Image, DragFill } from "@/components"; import type { DragFillResult } from "@/components"; -import { cn, getBackgroundImage, getStaticImage } from "@/lib/utils"; -import { useScale } from "@/contexts/ScaleContext"; +import SuccessAlert from "@/components/layout/success-alert"; +import { getBackgroundImage, getStaticImage } from "@/lib/utils"; const starName = "luxun"; -// 文学作品拖拽选项 -const literatureOptions = [ - { value: "朝花夕拾", label: "朝花夕拾", image: "luxun/reflection/scene2/拖拽-朝花夕拾" }, - { value: "阿Q正传", label: "阿Q正传", image: "luxun/reflection/scene2/拖拽-阿Q正传" }, - { value: "故事新编", label: "故事新编", image: "luxun/reflection/scene2/拖拽-故事新编" }, - { value: "百草园到三味书屋", label: "百草园到三味书屋", image: "luxun/reflection/scene2/拖拽-百草园到三味书屋" }, - { value: "精神胜利法", label: "精神胜利法", image: "luxun/reflection/scene2/拖拽-精神胜利法" }, - { value: "润土", label: "润土", image: "luxun/reflection/scene2/拖拽-润土" }, +// 拖拽元素配置 - 每个元素对应一个特定的标签放置区域 +const dragItems = [ + { + id: "runtu", + name: "闰土", + imagePath: "luxun/reflection/scene2/拖拽-润土", + targetBlankId: "city-1-label-0", // 绍兴-人物 + }, + { + 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" }, - { id: "blank2", label: "位置2" }, - { id: "blank3", label: "位置3" }, +// 城市配置 - 根据效果图调整位置 +// 布局:北京(上) -> 上海(中上) -> 绍兴(中) -> 广州(左下) -> 厦门(右下) +const cityConfig = [ + { + 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 = () => { @@ -32,100 +135,89 @@ const LuxunReflectionScene2 = () => { moduleName: string; }>(); - const [showDialog, setShowDialog] = useState(false); - const [visited, setVisited] = useState>(new Set()); - const [dragValue, setDragValue] = useState([]); - const completedRef = useRef(false); - const { scale, offsetX, offsetY, DESIGN_WIDTH, DESIGN_HEIGHT } = useScale(); - const [mounted, setMounted] = useState(false); + // DragFill 的值 + const [dragFillValue, setDragFillValue] = useState([]); - useEffect(() => { - setMounted(true); - }, []); + // 是否显示成功提示 + const [showSuccessAlert, setShowSuccessAlert] = useState(false); + // 是否全部完成 + const [allCompleted, setAllCompleted] = useState(false); - // 重置游戏状态 - useEffect(() => { - if (showDialog) { - setDragValue([]); - completedRef.current = false; - } - }, [showDialog]); - - // 检查是否完成 - useEffect(() => { - if (dragValue.length === 3 && !completedRef.current) { - completedRef.current = true; - setTimeout(() => { - setVisited((prev) => { - const next = new Set(prev); - next.add(1); - return next; - }); - setShowDialog(false); - }, 500); - } - }, [dragValue]); - - const progressCount = visited.size; - const total = 1; - const completed = progressCount >= total; - const subtitle = completed - ? "探索文学作品·精神传承(已完成)" - : `探索文学作品·精神传承(${progressCount}/${total})`; - const subtitleClassName = completed - ? "text-[#fff998] [text-shadow:_0_8px_8px_rgba(0,0,0,0.35),_0_0_16px_rgba(0,0,0,0.25)]" - : undefined; - - const handleValueChange = useCallback((newValue: DragFillResult[]) => { - if (newValue.length <= 3) { - setDragValue(newValue); - } - }, []); - - const maskSrc = getStaticImage("components/蒙版遮罩"); - const backSrc = getStaticImage("components/关闭"); - - const overlayStyle = useMemo( - () => ({ - position: "fixed", - inset: 0, - zIndex: 9999, - pointerEvents: "none", - }), + // 转换为 DragFill 的 options + const options = useMemo( + () => + dragItems.map((item) => ({ + value: item.id, + label: item.name, + })), [] ); - const scaledContainerStyle = useMemo( - () => ({ - position: "absolute", - width: `${DESIGN_WIDTH}px`, - height: `${DESIGN_HEIGHT}px`, - left: `${offsetX}px`, - top: `${offsetY}px`, - transform: `scale(${scale})`, - transformOrigin: "top left", - overflow: "hidden", - }), - [scale, offsetX, offsetY, DESIGN_WIDTH, DESIGN_HEIGHT] - ); + // 转换为 DragFill 的 blanks - 为每个标签创建独立的空白区域 + const blanks = useMemo(() => { + const allBlanks: Array<{ id: string; label: string }> = []; + cityConfig.forEach((city) => { + city.labels.forEach((label, index) => { + const labelText = typeof label === "string" ? label : label.text; + allBlanks.push({ + id: `city-${city.id}-label-${index}`, + label: labelText, + }); + }); + }); + 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 (
- - + {/* 离开按钮 */} <Image - className="absolute right-5 top-5" - src={getStaticImage(`${starName}/reflection/scene2/离开`)} + className="absolute right-5 top-5 z-10" + src={getStaticImage(`${starName}/reflection/scene1/离开`)} button onClick={() => { if (moduleName && starName) { @@ -134,130 +226,218 @@ const LuxunReflectionScene2 = () => { }} /> - {/* 点击区域 - 打开拖拽弹窗 */} - <Cursor - className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" - onClick={() => setShowDialog(true)} - read={visited.has(1)} - /> - - {completed && ( - <Image - className="absolute bottom-0 right-0" - src={getStaticImage("components/place/继续前进")} - button - onClick={() => { - if (moduleName && starName) { - navigate(`/${moduleName}/${starName}/reflection3`); - } - }} - /> - )} - - {/* 拖拽弹窗 */} - {mounted && - createPortal( - <AnimatePresence> - {showDialog && ( - <motion.div - key="literature-drag-dialog" - style={overlayStyle} - initial={{ opacity: 0 }} - animate={{ opacity: 1 }} - exit={{ opacity: 0 }} - transition={{ duration: 0.3, ease: "easeInOut" }} + {/* 左侧:鲁迅的文学足迹 - 地图区域 */} + <div className="w-[44.5%] relative"> + {/* 地图容器 */} + <div className="relative w-full h-full"> + {cityConfig.map((city) => { + return ( + <div + key={city.id} + className="absolute flex flex-col items-center" + style={{ + top: city.position.top, + left: city.position.left, + transform: "translate(-50%, -50%)", + }} > - <div style={scaledContainerStyle}> - <div - className={cn("absolute inset-0 flex flex-col", "bg-black/70")} - style={{ pointerEvents: "auto" }} - > - {/* 背景蒙版 */} - {maskSrc && ( - <Image - src={maskSrc} - className="absolute inset-0 w-full h-full object-cover select-none pointer-events-none" - aria-hidden - /> - )} + {/* 城市图片容器 */} + <div className="relative"> + <img src={getStaticImage(city.imagePath)} alt={city.name} /> - {/* 返回按钮 */} - <Image - src={backSrc} - onClick={() => setShowDialog(false)} - alt="返回" - className="absolute top-5 right-5 z-20" - button - /> + {/* 标签区域 - 每个标签是一个独立的容器框 */} + <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; - {/* 内容区域 */} - <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"> - 将文学作品拖拽到对应位置 + 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> - - <DragFill - options={literatureOptions.map((opt) => ({ - value: opt.value, - label: opt.label, - }))} - blanks={targetBlanks} - value={dragValue} - onChange={handleValueChange} - allowDuplicate={false} - renderOption={(option) => { - const opt = literatureOptions.find( - (o) => o.value === option.value - ); - return ( - <div className="flex flex-col items-center gap-2"> - <Image - src={getStaticImage(opt?.image || "")} - className="w-32 h-32 object-contain" - /> - <span className="text-2xl text-white">{option.label}</span> - </div> - ); - }} - renderBlank={(blank, filledValue, isHovered) => { - const filledOption = filledValue - ? literatureOptions.find((opt) => opt.value === filledValue) - : null; - return ( - <div - className={cn( - "w-48 h-48 border-2 rounded-lg flex flex-col items-center justify-center gap-2 transition-all", - filledValue - ? "border-white bg-white/20" - : "border-dashed border-white/50 bg-white/10", - isHovered && "border-white bg-white/30" - )} - > - {filledOption ? ( - <> - <Image - src={getStaticImage(filledOption.image)} - className="w-24 h-24 object-contain" - /> - <span className="text-white text-lg">{filledOption.label}</span> - </> - ) : ( - <span className="text-white text-xl">{blank.label}</span> - )} - </div> - ); - }} - /> - </div> - </div> + ); + })} </div> </div> - </motion.div> - )} - </AnimatePresence>, - document.body - )} + </div> + ); + })} + </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 + options={options} + blanks={blanks} + value={dragFillValue} + onChange={handleValueChange} + allowDuplicate={false} + showSubmitButton={false} + layout="horizontal-options" + hideLabels={true} + className="w-full" + optionClassName="grid grid-cols-4 gap-10" + blankClassName="hidden" // 隐藏默认的空白区域,我们在地图上自定义显示 + renderOption={(option, isUsed) => { + const item = dragItems.find((item) => item.id === option.value); + if (!item) return null; + + return ( + <div + className={`relative transition-all duration-200 cursor-move ${ + isUsed ? "opacity-30" : "hover:scale-105" + }`} + > + <img + src={getStaticImage(item.imagePath)} + alt={item.name} + className="w-full h-auto" + draggable={false} + /> + </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 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> + + {/* 成功提示弹窗 */} + <SuccessAlert + visible={showSuccessAlert} + title="恭喜你, 完成所有任务!" + onClose={handleSuccessAlertClose} + onContinue={handleSuccessAlertClose} + /> </div> ); };