326 lines
11 KiB
TypeScript
326 lines
11 KiB
TypeScript
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
|
||
)}
|
||
</>
|
||
);
|
||
}
|