Files
shanghai-residences/src/pages/experiment/Step2SelectPage.tsx
T
2025-10-20 17:50:42 +08:00

326 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from "react";
import { createPortal } from "react-dom";
import { Image } from "@/components";
import ExperimentTitle from "./ExperimentTitle";
import { selectableTools, correctAnswers } from "./constants";
import { useDragAndDrop, useDropZone } from "@/hooks/useDragAndDrop";
import { getStaticImage } from "@/utils";
interface Tool {
src: string;
name: string;
}
interface SelectedTool extends Tool {
id: string;
}
interface Step2SelectPageProps {
onNext?: () => void;
}
export default function Step2SelectPage({ onNext }: Step2SelectPageProps) {
const [selectedTools, setSelectedTools] = useState<SelectedTool[]>([]);
const [isSubmitted, setIsSubmitted] = useState(false);
const [isCorrect, setIsCorrect] = useState(false);
const {
isDragging,
dragData,
mousePosition,
startDrag,
endDrag,
isPointInElement,
} = useDragAndDrop<Tool>();
const { dropZoneRef, isOver, checkIsOver } = useDropZone();
// 获取可用的工具列表(过滤掉已选择的)
const availableTools = selectableTools.filter(
(tool) => !selectedTools.some((selected) => selected.name === tool.name)
);
// 开始拖拽工具
const handleMouseDown = (tool: Tool, e: React.MouseEvent) => {
// 如果已经提交且答案正确,则不允许拖拽
if (isSubmitted && isCorrect) {
return;
}
startDrag(tool, e);
};
// 监听鼠标位置,检查是否在放置区域上
useEffect(() => {
if (isDragging) {
checkIsOver(mousePosition.x, mousePosition.y);
}
}, [isDragging, mousePosition, checkIsOver]);
// 监听鼠标释放,处理拖拽结束
useEffect(() => {
if (!isDragging) return;
const handleMouseUp = () => {
if (
dragData &&
isPointInElement(mousePosition.x, mousePosition.y, dropZoneRef.current)
) {
// 检查是否已经添加过这个工具
const isAlreadyAdded = selectedTools.some(
(tool) => tool.name === dragData.name
);
if (!isAlreadyAdded) {
const newTool: SelectedTool = {
...dragData,
id: `selected-${Date.now()}`,
};
setSelectedTools([...selectedTools, newTool]);
}
}
endDrag();
};
window.addEventListener("mouseup", handleMouseUp);
return () => window.removeEventListener("mouseup", handleMouseUp);
}, [
isDragging,
dragData,
mousePosition,
selectedTools,
endDrag,
isPointInElement,
dropZoneRef,
]);
// 清空选择
const handleClear = () => {
setSelectedTools([]);
setIsSubmitted(false);
setIsCorrect(false);
};
// 提交选择
const handleSubmit = () => {
if (selectedTools.length === 0) {
// alert("请至少选择一个工具!");
return;
}
// 验证答案
const selectedNames = selectedTools.map((tool) => tool.name);
// 检查数量是否一致
if (selectedNames.length !== correctAnswers.length) {
setIsSubmitted(true);
setIsCorrect(false);
return;
}
// 检查每个选择的工具是否都在正确答案中
const allCorrect = selectedNames.every((name) =>
correctAnswers.includes(name)
);
setIsSubmitted(true);
setIsCorrect(allCorrect);
};
// 删除已选工具
const handleRemoveTool = (id: string) => {
// 如果已经提交且答案正确,则不允许删除
if (isSubmitted && isCorrect) {
return;
}
setSelectedTools(selectedTools.filter((tool) => tool.id !== id));
};
return (
<>
<ExperimentTitle
title="02实验工具选择"
description="我们刚刚学习了实验所需要的所有工具,现在来手动选择一下哪些工具是需要应用在本次实验中的。"
/>
{/* 主要内容区域 */}
<div className="flex gap-8 mt-10">
{/* 左侧:可拖拽选项 */}
<div className="flex-1">
<div className="bg-[#3d98cd] text-white text-center py-3 rounded-t-lg">
<h2 className="text-2xl font-bold">可拖拽选项</h2>
</div>
<div className="backdrop-blur-sm border-2 border-[#3d98cd] rounded-b-lg p-6 min-h-[500px]">
{availableTools.length === 0 ? (
<div className="flex items-center justify-center h-full text-gray-400 text-xl">
所有工具已选择完毕
</div>
) : (
<div className="grid grid-cols-3 gap-6">
{availableTools.map((tool, index) => {
const isBeingDragged =
isDragging && dragData?.name === tool.name;
const isDragDisabled = isSubmitted && isCorrect;
return (
<div
key={index}
onMouseDown={(e) => handleMouseDown(tool, e)}
className={`flex flex-col items-center transition-transform select-none ${
isDragDisabled
? "cursor-not-allowed opacity-50"
: "cursor-move hover:scale-105"
} ${isBeingDragged ? "opacity-30" : ""}`}
>
<div className="rounded-lg p-4 w-full">
<Image
src={tool.src}
className="w-full h-32 object-contain mb-2 pointer-events-none"
/>
</div>
<p className="text-center text-lg font-semibold text-gray-700 mt-2">
{tool.name}
</p>
</div>
);
})}
</div>
)}
</div>
</div>
{/* 右侧:实验工具区域 */}
<div className="flex-1 relative">
<div className="bg-[#3d98cd] text-white text-center py-3 rounded-t-lg">
<h2 className="text-2xl font-bold">实验工具区域</h2>
</div>
<div
ref={dropZoneRef}
className={`backdrop-blur-sm border-2 border-[#3d98cd] rounded-b-lg p-6 min-h-[500px] transition-colors ${
isOver ? "bg-blue-50 border-blue-400" : ""
}`}
>
{/* 提交后的结果显示 */}
{isSubmitted ? (
<div className="flex flex-col items-center justify-center h-full">
{isCorrect ? (
<>
<p className="text-green-500 text-3xl font-bold mb-4">
选择正确,继续实验吧
</p>
</>
) : (
<>
<p className="text-red-500 text-3xl font-bold mb-8">
选择错误,请重新选择
</p>
<div className="text-center">
<p className="text-blue-400 text-2xl font-semibold mb-4">
答案提示
</p>
<p className="text-blue-400 text-xl">
{correctAnswers.join("、")}
</p>
</div>
</>
)}
</div>
) : selectedTools.length === 0 ? (
<div className="flex items-center justify-center h-full text-gray-400 text-xl">
拖拽工具到这里
</div>
) : (
<div className="grid grid-cols-3 gap-6">
{selectedTools.map((tool) => (
<div
key={tool.id}
className="flex flex-col items-center relative group"
>
<div className="rounded-lg p-4 w-full">
<Image
src={tool.src}
className="w-full h-32 object-contain mb-2"
/>
</div>
<p className="text-center text-lg font-semibold text-gray-700 mt-2">
{tool.name}
</p>
{/* 删除按钮 - 答案正确后隐藏 */}
{!(isSubmitted && isCorrect) && (
<button
onClick={() => handleRemoveTool(tool.id)}
className="absolute top-2 right-2 bg-red-500 hover:bg-red-600 text-white rounded-full w-8 h-8 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity shadow-lg"
title="删除"
>
×
</button>
)}
</div>
))}
</div>
)}
</div>
{/* 底部按钮 */}
{!isSubmitted || !isCorrect ? (
<div className="absolute bottom-0 left-1/2 translate-y-1/2 -translate-x-1/2 flex justify-center gap-8 mt-8">
<button
onClick={handleClear}
className="bg-gray-400 hover:bg-gray-500 text-white px-12 py-4 rounded-full text-2xl font-bold shadow-lg transition-colors"
>
清空
</button>
<button
onClick={handleSubmit}
className="bg-[#3d98cd] hover:bg-[#2d7aad] text-white px-12 py-4 rounded-full text-2xl font-bold shadow-lg transition-colors"
>
提交
</button>
</div>
) : null}
</div>
</div>
{/* 底部提示文字或下一步按钮 */}
<div className="text-center mt-20">
{isSubmitted && isCorrect ? (
<Image
onClick={onNext}
className="absolute bottom-4 right-6 cursor-pointer"
src={getStaticImage("components/按钮-下一步.png")}
button={true}
/>
) : (
// <button
// className="bg-blue-400 hover:bg-blue-500 text-white px-20 py-5 rounded-full text-3xl font-bold shadow-2xl transition-all hover:scale-105"
// >
// 下一步
// </button>
<p className="text-blue-400 text-3xl font-semibold">
通过拖拽的方式,将正确选择拖拽到实验工具区域内吧。
</p>
)}
</div>
{/* 拖拽预览 - 使用 Portal 渲染到 body,避免受 scale 影响 */}
{isDragging &&
dragData &&
createPortal(
<div
className="fixed pointer-events-none z-[9999]"
style={{
left: mousePosition.x,
top: mousePosition.y,
transform: "translate(-50%, -50%)",
}}
>
<div className="flex flex-col items-center opacity-80 bg-white rounded-lg shadow-xl p-4">
<Image src={dragData.src} className="w-24 h-24 object-contain" />
<p className="text-sm font-semibold text-gray-700 mt-2">
{dragData.name}
</p>
</div>
</div>,
document.body
)}
</>
);
}