feat:提交实验室环境配置
No files matched your search
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 963 KiB |
|
After Width: | Height: | Size: 339 KiB |
|
After Width: | Height: | Size: 311 KiB |
|
After Width: | Height: | Size: 293 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 106 KiB |
@@ -3,6 +3,7 @@ import { clsx } from "clsx";
|
|||||||
|
|
||||||
interface ImageProps {
|
interface ImageProps {
|
||||||
src: string;
|
src: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
alt?: string;
|
alt?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
width?: number | string;
|
width?: number | string;
|
||||||
@@ -23,6 +24,7 @@ interface ImageProps {
|
|||||||
|
|
||||||
export default function Image({
|
export default function Image({
|
||||||
src,
|
src,
|
||||||
|
children,
|
||||||
alt,
|
alt,
|
||||||
className,
|
className,
|
||||||
width,
|
width,
|
||||||
@@ -171,6 +173,7 @@ export default function Image({
|
|||||||
isLoaded ? "opacity-100" : "opacity-0"
|
isLoaded ? "opacity-100" : "opacity-0"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -18,6 +18,7 @@ export function useDragAndDrop<T = any>() {
|
|||||||
dragData: null,
|
dragData: null,
|
||||||
mousePosition: { x: 0, y: 0 },
|
mousePosition: { x: 0, y: 0 },
|
||||||
});
|
});
|
||||||
|
const isDraggingRef = useRef(false);
|
||||||
|
|
||||||
// 开始拖拽
|
// 开始拖拽
|
||||||
const startDrag = useCallback((data: T, event?: React.MouseEvent) => {
|
const startDrag = useCallback((data: T, event?: React.MouseEvent) => {
|
||||||
@@ -26,12 +27,14 @@ export function useDragAndDrop<T = any>() {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
// 立即获取鼠标初始位置
|
// 立即获取鼠标初始位置
|
||||||
|
isDraggingRef.current = true;
|
||||||
setState({
|
setState({
|
||||||
isDragging: true,
|
isDragging: true,
|
||||||
dragData: data,
|
dragData: data,
|
||||||
mousePosition: { x: event.clientX, y: event.clientY },
|
mousePosition: { x: event.clientX, y: event.clientY },
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
isDraggingRef.current = true;
|
||||||
setState({
|
setState({
|
||||||
isDragging: true,
|
isDragging: true,
|
||||||
dragData: data,
|
dragData: data,
|
||||||
@@ -42,6 +45,7 @@ export function useDragAndDrop<T = any>() {
|
|||||||
|
|
||||||
// 结束拖拽
|
// 结束拖拽
|
||||||
const endDrag = useCallback(() => {
|
const endDrag = useCallback(() => {
|
||||||
|
isDraggingRef.current = false;
|
||||||
setState({
|
setState({
|
||||||
isDragging: false,
|
isDragging: false,
|
||||||
dragData: null,
|
dragData: null,
|
||||||
@@ -49,11 +53,10 @@ export function useDragAndDrop<T = any>() {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 监听鼠标移动
|
// 监听鼠标移动 - 使用独立的事件监听器
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!state.isDragging) return;
|
|
||||||
|
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
if (!isDraggingRef.current) return;
|
||||||
setState((prev) => ({
|
setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
mousePosition: { x: e.clientX, y: e.clientY },
|
mousePosition: { x: e.clientX, y: e.clientY },
|
||||||
@@ -65,6 +68,7 @@ export function useDragAndDrop<T = any>() {
|
|||||||
// 这里只是提供一个安全机制
|
// 这里只是提供一个安全机制
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 始终添加事件监听器,在handleMouseMove内部检查拖拽状态
|
||||||
window.addEventListener("mousemove", handleMouseMove);
|
window.addEventListener("mousemove", handleMouseMove);
|
||||||
window.addEventListener("mouseup", handleMouseUp);
|
window.addEventListener("mouseup", handleMouseUp);
|
||||||
|
|
||||||
@@ -72,7 +76,7 @@ export function useDragAndDrop<T = any>() {
|
|||||||
window.removeEventListener("mousemove", handleMouseMove);
|
window.removeEventListener("mousemove", handleMouseMove);
|
||||||
window.removeEventListener("mouseup", handleMouseUp);
|
window.removeEventListener("mouseup", handleMouseUp);
|
||||||
};
|
};
|
||||||
}, [state.isDragging]);
|
}, []); // 空依赖数组,只在组件挂载/卸载时执行
|
||||||
|
|
||||||
// 将屏幕坐标转换为缩放后的页面坐标
|
// 将屏幕坐标转换为缩放后的页面坐标
|
||||||
const screenToScaledCoords = useCallback(
|
const screenToScaledCoords = useCallback(
|
||||||
@@ -131,7 +135,8 @@ export function useDropZone() {
|
|||||||
mouseY >= rect.top &&
|
mouseY >= rect.top &&
|
||||||
mouseY <= rect.bottom;
|
mouseY <= rect.bottom;
|
||||||
|
|
||||||
setIsOver(over);
|
// 只有当状态真正改变时才更新
|
||||||
|
setIsOver((prev) => (prev !== over ? over : prev));
|
||||||
return over;
|
return over;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export default function Home() {
|
|||||||
<Image
|
<Image
|
||||||
src={getStaticImage("home/按钮-开启实验.png")}
|
src={getStaticImage("home/按钮-开启实验.png")}
|
||||||
alt="开始实验按钮"
|
alt="开始实验按钮"
|
||||||
|
button
|
||||||
className="absolute bottom-10 left-1/2 transform -translate-x-1/2 -translate-y-1/2"
|
className="absolute bottom-10 left-1/2 transform -translate-x-1/2 -translate-y-1/2"
|
||||||
onClick={handleStartExperiment}
|
onClick={handleStartExperiment}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Image } from "@/components";
|
import { BackgroundImage, Image } from "@/components";
|
||||||
import { getStaticImage } from "@/utils";
|
import { getStaticImage } from "@/utils";
|
||||||
import ExperimentTitle from "./ExperimentTitle";
|
import ExperimentTitle from "./ExperimentTitle";
|
||||||
import { sensorImages } from "./constants";
|
import { sensorImages } from "./constants";
|
||||||
@@ -10,34 +10,22 @@ interface Step1IntroPageProps {
|
|||||||
export default function Step1IntroPage({ onNext }: Step1IntroPageProps) {
|
export default function Step1IntroPage({ onNext }: Step1IntroPageProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ExperimentTitle
|
<BackgroundImage
|
||||||
title="01实验工具介绍"
|
src={getStaticImage("experiment/01实验工具介绍.png")}
|
||||||
description="以下是实验所需要的所有工具,让我们来看看具体有哪些吧"
|
alt="实验工具介绍背景"
|
||||||
/>
|
className="h-full w-full relative"
|
||||||
|
backgroundSize="cover"
|
||||||
{/* img列表 */}
|
backgroundPosition="center"
|
||||||
<div className="grid grid-cols-4 gap-y-20 gap-x-20 mt-10">
|
onLoad={() => console.log("背景图片加载完成")}
|
||||||
{sensorImages.map((image, index) => (
|
onError={() => console.log("背景图片加载失败")}
|
||||||
<div key={index} className="flex flex-col items-center">
|
>
|
||||||
<Image
|
<Image
|
||||||
src={image.src}
|
className="absolute bottom-0 right-6"
|
||||||
className="w-80 h-50 rounded-lg shadow-md mb-4 object-cover"
|
|
||||||
/>
|
|
||||||
<div className="text-center text-xl">
|
|
||||||
<h3 className=" font-bold text-gray-800 mb-1">{image.name}</h3>
|
|
||||||
<p className=" text-gray-600">{image.description}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div onClick={onNext}>
|
|
||||||
<Image
|
|
||||||
className="absolute bottom-4 right-6 cursor-pointer"
|
|
||||||
src={getStaticImage("components/按钮-下一步.png")}
|
src={getStaticImage("components/按钮-下一步.png")}
|
||||||
button={true}
|
button={true}
|
||||||
|
onClick={onNext}
|
||||||
/>
|
/>
|
||||||
</div>
|
</BackgroundImage>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useRef, useMemo, useCallback } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { Image } from "@/components";
|
import { BackgroundImage, Image } from "@/components";
|
||||||
import ExperimentTitle from "./ExperimentTitle";
|
|
||||||
import { selectableTools, correctAnswers } from "./constants";
|
import { selectableTools, correctAnswers } from "./constants";
|
||||||
import { useDragAndDrop, useDropZone } from "@/hooks/useDragAndDrop";
|
import { useDragAndDrop, useDropZone } from "@/hooks/useDragAndDrop";
|
||||||
import { getStaticImage } from "@/utils";
|
import { getStaticImage } from "@/utils";
|
||||||
@@ -19,62 +18,125 @@ interface Step2SelectPageProps {
|
|||||||
onNext?: () => void;
|
onNext?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Step2SelectPage({ onNext }: Step2SelectPageProps) {
|
// ==================== 常量定义 ====================
|
||||||
const [selectedTools, setSelectedTools] = useState<SelectedTool[]>([]);
|
const MAX_TOOLS = 4; // 最大选择工具数量
|
||||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
const TOOL_GRID_COLS = 3; // 左侧工具网格列数
|
||||||
const [isCorrect, setIsCorrect] = useState(false);
|
const SELECTED_GRID_COLS = 2; // 右侧已选工具网格列数
|
||||||
const {
|
|
||||||
isDragging,
|
|
||||||
dragData,
|
|
||||||
mousePosition,
|
|
||||||
startDrag,
|
|
||||||
endDrag,
|
|
||||||
isPointInElement,
|
|
||||||
} = useDragAndDrop<Tool>();
|
|
||||||
const { dropZoneRef, isOver, checkIsOver } = useDropZone();
|
|
||||||
|
|
||||||
|
export default function Step2SelectPage({
|
||||||
|
onNext: _onNext,
|
||||||
|
}: Step2SelectPageProps) {
|
||||||
|
// ==================== 状态管理 ====================
|
||||||
|
const [selectedTools, setSelectedTools] = useState<SelectedTool[]>([]); // 已选择的工具列表
|
||||||
|
const [isSubmitted, setIsSubmitted] = useState(false); // 是否已提交
|
||||||
|
const [isCorrect, setIsCorrect] = useState(false); // 答案是否正确
|
||||||
|
|
||||||
|
// ==================== 引用和Hook ====================
|
||||||
|
const mousePositionRef = useRef({ x: 0, y: 0 }); // 鼠标位置缓存
|
||||||
|
// 拖拽功能Hook
|
||||||
|
const {
|
||||||
|
isDragging, // 是否正在拖拽
|
||||||
|
dragData, // 拖拽的数据
|
||||||
|
mousePosition, // 鼠标位置
|
||||||
|
startDrag, // 开始拖拽
|
||||||
|
endDrag, // 结束拖拽
|
||||||
|
isPointInElement, // 检查点是否在元素内
|
||||||
|
} = useDragAndDrop<Tool>();
|
||||||
|
|
||||||
|
// 放置区域Hook
|
||||||
|
const { dropZoneRef, isOver, checkIsOver } = useDropZone();
|
||||||
|
const checkIsOverRef = useRef(checkIsOver); // 缓存checkIsOver函数引用
|
||||||
|
|
||||||
|
// ==================== 计算属性 ====================
|
||||||
// 获取可用的工具列表(过滤掉已选择的)
|
// 获取可用的工具列表(过滤掉已选择的)
|
||||||
const availableTools = selectableTools.filter(
|
const availableTools = useMemo(
|
||||||
|
() =>
|
||||||
|
selectableTools.filter(
|
||||||
(tool) => !selectedTools.some((selected) => selected.name === tool.name)
|
(tool) => !selectedTools.some((selected) => selected.name === tool.name)
|
||||||
|
),
|
||||||
|
[selectedTools]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 开始拖拽工具
|
// ==================== 事件处理函数 ====================
|
||||||
const handleMouseDown = (tool: Tool, e: React.MouseEvent) => {
|
/**
|
||||||
// 如果已经提交且答案正确,则不允许拖拽
|
* 开始拖拽工具
|
||||||
|
* @param tool 要拖拽的工具
|
||||||
|
* @param e 鼠标事件
|
||||||
|
*/
|
||||||
|
const handleMouseDown = useCallback(
|
||||||
|
(tool: Tool, e: React.MouseEvent) => {
|
||||||
|
try {
|
||||||
|
// 检查是否允许拖拽
|
||||||
if (isSubmitted && isCorrect) {
|
if (isSubmitted && isCorrect) {
|
||||||
|
return; // 已提交且正确,不允许拖拽
|
||||||
|
}
|
||||||
|
if (selectedTools.length >= MAX_TOOLS) {
|
||||||
|
return; // 已达到最大数量限制
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证工具数据有效性
|
||||||
|
if (!tool || !tool.src || !tool.name) {
|
||||||
|
console.warn("Invalid tool data:", tool);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
startDrag(tool, e);
|
startDrag(tool, e);
|
||||||
};
|
} catch (error) {
|
||||||
|
console.error("Error in handleMouseDown:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isSubmitted, isCorrect, selectedTools.length, startDrag]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ==================== 副作用处理 ====================
|
||||||
|
// 更新checkIsOver函数引用
|
||||||
|
useEffect(() => {
|
||||||
|
checkIsOverRef.current = checkIsOver;
|
||||||
|
}, [checkIsOver]);
|
||||||
|
|
||||||
// 监听鼠标位置,检查是否在放置区域上
|
// 监听鼠标位置,检查是否在放置区域上
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isDragging) {
|
if (isDragging) {
|
||||||
checkIsOver(mousePosition.x, mousePosition.y);
|
mousePositionRef.current = { x: mousePosition.x, y: mousePosition.y };
|
||||||
|
checkIsOverRef.current(mousePosition.x, mousePosition.y);
|
||||||
}
|
}
|
||||||
}, [isDragging, mousePosition, checkIsOver]);
|
}, [isDragging, mousePosition.x, mousePosition.y]);
|
||||||
|
|
||||||
// 监听鼠标释放,处理拖拽结束
|
// 监听鼠标释放,处理拖拽结束
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isDragging) return;
|
if (!isDragging) return;
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
const handleMouseUp = () => {
|
||||||
|
// 检查是否在放置区域内
|
||||||
if (
|
if (
|
||||||
dragData &&
|
dragData &&
|
||||||
isPointInElement(mousePosition.x, mousePosition.y, dropZoneRef.current)
|
isPointInElement(
|
||||||
|
mousePositionRef.current.x,
|
||||||
|
mousePositionRef.current.y,
|
||||||
|
dropZoneRef.current
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
// 检查是否已经添加过这个工具
|
// 添加工具到选择列表
|
||||||
const isAlreadyAdded = selectedTools.some(
|
setSelectedTools((prev) => {
|
||||||
|
const isAlreadyAdded = prev.some(
|
||||||
(tool) => tool.name === dragData.name
|
(tool) => tool.name === dragData.name
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 检查数量限制
|
||||||
|
if (prev.length >= MAX_TOOLS) {
|
||||||
|
return prev; // 已达到最大数量,不添加新工具
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加新工具
|
||||||
if (!isAlreadyAdded) {
|
if (!isAlreadyAdded) {
|
||||||
const newTool: SelectedTool = {
|
const newTool: SelectedTool = {
|
||||||
...dragData,
|
...dragData,
|
||||||
id: `selected-${Date.now()}`,
|
id: `selected-${Date.now()}`,
|
||||||
};
|
};
|
||||||
setSelectedTools([...selectedTools, newTool]);
|
return [...prev, newTool];
|
||||||
}
|
}
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
endDrag();
|
endDrag();
|
||||||
};
|
};
|
||||||
@@ -85,27 +147,30 @@ export default function Step2SelectPage({ onNext }: Step2SelectPageProps) {
|
|||||||
isDragging,
|
isDragging,
|
||||||
dragData,
|
dragData,
|
||||||
mousePosition,
|
mousePosition,
|
||||||
selectedTools,
|
|
||||||
endDrag,
|
endDrag,
|
||||||
isPointInElement,
|
isPointInElement,
|
||||||
dropZoneRef,
|
dropZoneRef,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 清空选择
|
/**
|
||||||
const handleClear = () => {
|
* 清空所有选择
|
||||||
|
*/
|
||||||
|
const handleClear = useCallback(() => {
|
||||||
setSelectedTools([]);
|
setSelectedTools([]);
|
||||||
setIsSubmitted(false);
|
setIsSubmitted(false);
|
||||||
setIsCorrect(false);
|
setIsCorrect(false);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
// 提交选择
|
/**
|
||||||
const handleSubmit = () => {
|
* 提交选择并验证答案
|
||||||
|
*/
|
||||||
|
const handleSubmit = useCallback(() => {
|
||||||
|
try {
|
||||||
if (selectedTools.length === 0) {
|
if (selectedTools.length === 0) {
|
||||||
// alert("请至少选择一个工具!");
|
return; // 没有选择任何工具
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证答案
|
// 获取选择的工具名称
|
||||||
const selectedNames = selectedTools.map((tool) => tool.name);
|
const selectedNames = selectedTools.map((tool) => tool.name);
|
||||||
|
|
||||||
// 检查数量是否一致
|
// 检查数量是否一致
|
||||||
@@ -122,59 +187,82 @@ export default function Step2SelectPage({ onNext }: Step2SelectPageProps) {
|
|||||||
|
|
||||||
setIsSubmitted(true);
|
setIsSubmitted(true);
|
||||||
setIsCorrect(allCorrect);
|
setIsCorrect(allCorrect);
|
||||||
};
|
} catch (error) {
|
||||||
|
console.error("Error in handleSubmit:", error);
|
||||||
|
setIsSubmitted(true);
|
||||||
|
setIsCorrect(false);
|
||||||
|
}
|
||||||
|
}, [selectedTools]);
|
||||||
|
|
||||||
// 删除已选工具
|
/**
|
||||||
const handleRemoveTool = (id: string) => {
|
* 删除已选择的工具
|
||||||
// 如果已经提交且答案正确,则不允许删除
|
* @param id 工具ID
|
||||||
|
*/
|
||||||
|
const handleRemoveTool = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
try {
|
||||||
|
// 检查是否允许删除
|
||||||
if (isSubmitted && isCorrect) {
|
if (isSubmitted && isCorrect) {
|
||||||
|
return; // 已提交且正确,不允许删除
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证ID有效性
|
||||||
|
if (!id || typeof id !== "string") {
|
||||||
|
console.warn("Invalid tool ID:", id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSelectedTools(selectedTools.filter((tool) => tool.id !== id));
|
|
||||||
};
|
setSelectedTools((prev) => prev.filter((tool) => tool.id !== id));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in handleRemoveTool:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isSubmitted, isCorrect]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ExperimentTitle
|
<BackgroundImage
|
||||||
title="02实验工具选择"
|
src={getStaticImage("experiment/02实验工具选择.png")}
|
||||||
description="我们刚刚学习了实验所需要的所有工具,现在来手动选择一下哪些工具是需要应用在本次实验中的。"
|
alt="实验工具选择背景"
|
||||||
/>
|
className="h-full w-full relative pt-70"
|
||||||
|
backgroundSize="cover"
|
||||||
{/* 主要内容区域 */}
|
backgroundPosition="center"
|
||||||
<div className="flex gap-8 mt-10">
|
>
|
||||||
{/* 左侧:可拖拽选项 */}
|
{/* ==================== 主要内容区域 ==================== */}
|
||||||
|
<div className="flex gap-38 h-[580px] px-35 pr-45">
|
||||||
|
{/* ==================== 左侧:可拖拽工具列表 ==================== */}
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="bg-[#3d98cd] text-white text-center py-3 rounded-t-lg">
|
<div className="h-full p-4 pt-10">
|
||||||
<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 ? (
|
{availableTools.length === 0 ? (
|
||||||
<div className="flex items-center justify-center h-full text-gray-400 text-xl">
|
// 空状态:所有工具已选择完毕
|
||||||
|
<div className="flex flex-col items-center justify-center h-full text-gray-400">
|
||||||
|
<div className="text-6xl mb-4">🎯</div>
|
||||||
|
<div className="text-xl font-semibold">
|
||||||
所有工具已选择完毕
|
所有工具已选择完毕
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-sm mt-2">请检查右侧选择区域</div>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-3 gap-6">
|
<div className={`grid gap-4 grid-cols-${TOOL_GRID_COLS}`}>
|
||||||
{availableTools.map((tool, index) => {
|
{availableTools.map((tool, index) => {
|
||||||
const isBeingDragged =
|
const isBeingDragged =
|
||||||
isDragging && dragData?.name === tool.name;
|
isDragging && dragData?.name === tool.name;
|
||||||
const isDragDisabled = isSubmitted && isCorrect;
|
const isDragDisabled =
|
||||||
|
(isSubmitted && isCorrect) ||
|
||||||
|
selectedTools.length >= MAX_TOOLS;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
onMouseDown={(e) => handleMouseDown(tool, e)}
|
onMouseDown={(e) => handleMouseDown(tool, e)}
|
||||||
className={`flex flex-col items-center transition-transform select-none ${
|
className={`flex flex-col items-center transition-all duration-200 select-none p-4 rounded-lg ${
|
||||||
isDragDisabled
|
isDragDisabled
|
||||||
? "cursor-not-allowed opacity-50"
|
? "cursor-not-allowed opacity-50 grayscale"
|
||||||
: "cursor-move hover:scale-105"
|
: "cursor-move hover:scale-105 hover:shadow-lg hover:bg-white/10"
|
||||||
} ${isBeingDragged ? "opacity-30" : ""}`}
|
} ${isBeingDragged ? "opacity-30 scale-95" : ""}`}
|
||||||
>
|
>
|
||||||
<div className="rounded-lg p-4 w-full">
|
<Image src={tool.src} />
|
||||||
<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">
|
<p className="text-center text-lg font-semibold text-gray-700 mt-2">
|
||||||
{tool.name}
|
{tool.name}
|
||||||
</p>
|
</p>
|
||||||
@@ -186,27 +274,29 @@ export default function Step2SelectPage({ onNext }: Step2SelectPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 右侧:实验工具区域 */}
|
{/* ==================== 右侧:实验工具放置区域 ==================== */}
|
||||||
<div className="flex-1 relative">
|
<div className="w-[580px] relative">
|
||||||
<div className="bg-[#3d98cd] text-white text-center py-3 rounded-t-lg">
|
|
||||||
<h2 className="text-2xl font-bold">实验工具区域</h2>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
ref={dropZoneRef}
|
ref={dropZoneRef}
|
||||||
className={`backdrop-blur-sm border-2 border-[#3d98cd] rounded-b-lg p-6 min-h-[500px] transition-colors ${
|
className={`h-full transition-all duration-300 rounded-lg ${
|
||||||
isOver ? "bg-blue-50 border-blue-400" : ""
|
isOver
|
||||||
|
? "bg-blue-50/80 border-2 border-blue-400 border-dashed shadow-lg"
|
||||||
|
: "bg-white/5 border-2 border-transparent"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* 提交后的结果显示 */}
|
{/* ==================== 状态显示区域 ==================== */}
|
||||||
{isSubmitted ? (
|
{isSubmitted ? (
|
||||||
<div className="flex flex-col items-center justify-center h-full">
|
// 提交后的结果显示
|
||||||
|
<div className="flex flex-col items-center justify-center h-full bg-black/80">
|
||||||
{isCorrect ? (
|
{isCorrect ? (
|
||||||
|
// 答案正确
|
||||||
<>
|
<>
|
||||||
<p className="text-green-500 text-3xl font-bold mb-4">
|
<p className="text-green-500 text-3xl font-bold mb-4">
|
||||||
选择正确,继续实验吧
|
选择正确,继续实验吧
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
// 答案错误
|
||||||
<>
|
<>
|
||||||
<p className="text-red-500 text-3xl font-bold mb-8">
|
<p className="text-red-500 text-3xl font-bold mb-8">
|
||||||
选择错误,请重新选择
|
选择错误,请重新选择
|
||||||
@@ -223,22 +313,57 @@ export default function Step2SelectPage({ onNext }: Step2SelectPageProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : selectedTools.length === 0 ? (
|
) : selectedTools.length === 0 ? (
|
||||||
<div className="flex items-center justify-center h-full text-gray-400 text-xl">
|
// 空状态:未选择任何工具
|
||||||
拖拽工具到这里
|
<div className="flex flex-col items-center justify-center h-full text-gray-400">
|
||||||
|
<div className="text-6xl mb-4">📦</div>
|
||||||
|
<div className="text-xl font-semibold">拖拽工具到这里</div>
|
||||||
|
<div className="text-sm mt-2">
|
||||||
|
从左侧选择最多{MAX_TOOLS}个工具
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
<div className="grid grid-cols-3 gap-6">
|
) : selectedTools.length >= MAX_TOOLS ? (
|
||||||
|
// 达到最大数量限制
|
||||||
|
<div className="flex flex-col items-center justify-center h-full p-10">
|
||||||
|
<div
|
||||||
|
className={`grid gap-4 mb-4 grid-cols-${SELECTED_GRID_COLS}`}
|
||||||
|
>
|
||||||
{selectedTools.map((tool) => (
|
{selectedTools.map((tool) => (
|
||||||
<div
|
<div
|
||||||
key={tool.id}
|
key={tool.id}
|
||||||
className="flex flex-col items-center relative group"
|
className="flex flex-col items-center relative group p-4 rounded-lg hover:bg-white/10 transition-all duration-200"
|
||||||
>
|
>
|
||||||
<div className="rounded-lg p-4 w-full">
|
<Image src={tool.src} />
|
||||||
<Image
|
<p className="text-center text-lg font-semibold text-gray-700 mt-2">
|
||||||
src={tool.src}
|
{tool.name}
|
||||||
className="w-full h-32 object-contain mb-2"
|
</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-all duration-200 shadow-lg hover:scale-110"
|
||||||
|
title="删除"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="text-orange-500 text-2xl font-semibold text-center">
|
||||||
|
已选择{MAX_TOOLS}个工具,达到最大数量限制
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// 正常状态:显示已选择的工具
|
||||||
|
<div
|
||||||
|
className={`grid gap-4 p-10 grid-cols-${SELECTED_GRID_COLS}`}
|
||||||
|
>
|
||||||
|
{selectedTools.map((tool) => (
|
||||||
|
<div
|
||||||
|
key={tool.id}
|
||||||
|
className="flex flex-col items-center relative group p-4 rounded-lg hover:bg-white/10 transition-all duration-200"
|
||||||
|
>
|
||||||
|
<Image src={tool.src} />
|
||||||
<p className="text-center text-lg font-semibold text-gray-700 mt-2">
|
<p className="text-center text-lg font-semibold text-gray-700 mt-2">
|
||||||
{tool.name}
|
{tool.name}
|
||||||
</p>
|
</p>
|
||||||
@@ -258,48 +383,36 @@ export default function Step2SelectPage({ onNext }: Step2SelectPageProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 底部按钮 */}
|
{/* ==================== 底部操作按钮 ==================== */}
|
||||||
{!isSubmitted || !isCorrect ? (
|
{!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">
|
<div className="absolute bottom-0 left-1/2 translate-y-1/2 -translate-x-1/2 flex justify-center gap-8 mt-8">
|
||||||
<button
|
<Image
|
||||||
|
src={getStaticImage("components/按钮-清空.png")}
|
||||||
|
button
|
||||||
onClick={handleClear}
|
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"
|
/>
|
||||||
>
|
<Image
|
||||||
清空
|
src={getStaticImage("components/按钮-提交.png")}
|
||||||
</button>
|
button
|
||||||
<button
|
|
||||||
onClick={handleSubmit}
|
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>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 底部提示文字或下一步按钮 */}
|
{/* ==================== 下一步按钮 ==================== */}
|
||||||
<div className="text-center mt-20">
|
{isSubmitted && isCorrect && (
|
||||||
{isSubmitted && isCorrect ? (
|
|
||||||
<Image
|
<Image
|
||||||
onClick={onNext}
|
onClick={_onNext}
|
||||||
className="absolute bottom-4 right-6 cursor-pointer"
|
className="absolute bottom-4 right-6 cursor-pointer"
|
||||||
src={getStaticImage("components/按钮-下一步.png")}
|
src={getStaticImage("components/按钮-下一步.png")}
|
||||||
button={true}
|
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 影响 */}
|
{/* ==================== 拖拽预览 ==================== */}
|
||||||
|
{/* 使用 Portal 渲染到 body,避免受 scale 影响 */}
|
||||||
{isDragging &&
|
{isDragging &&
|
||||||
dragData &&
|
dragData &&
|
||||||
createPortal(
|
createPortal(
|
||||||
@@ -311,15 +424,23 @@ export default function Step2SelectPage({ onNext }: Step2SelectPageProps) {
|
|||||||
transform: "translate(-50%, -50%)",
|
transform: "translate(-50%, -50%)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col items-center opacity-80 bg-white rounded-lg shadow-xl p-4">
|
<div className="text-center opacity-90 bg-white rounded-lg shadow-2xl p-3 border border-gray-200">
|
||||||
<Image src={dragData.src} className="w-24 h-24 object-contain" />
|
<Image
|
||||||
<p className="text-sm font-semibold text-gray-700 mt-2">
|
src={dragData.src}
|
||||||
|
className="w-20 h-20 object-contain mx-auto"
|
||||||
|
/>
|
||||||
|
<div className="text-sm font-semibold text-gray-700 mt-2">
|
||||||
{dragData.name}
|
{dragData.name}
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
document.body
|
document.body
|
||||||
)}
|
)}
|
||||||
|
</BackgroundImage>
|
||||||
|
{/* <ExperimentTitle
|
||||||
|
title="02实验工具选择"
|
||||||
|
description="我们刚刚学习了实验所需要的所有工具,现在来手动选择一下哪些工具是需要应用在本次实验中的。"
|
||||||
|
/> */}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Image } from "@/components";
|
import { BackgroundImage, Image } from "@/components";
|
||||||
import { getStaticImage } from "@/utils";
|
import { getStaticImage } from "@/utils";
|
||||||
import ExperimentTitle from "./ExperimentTitle";
|
|
||||||
|
|
||||||
interface Step3ConfigPageProps {
|
interface Step3ConfigPageProps {
|
||||||
onNext?: () => void;
|
onNext?: () => void;
|
||||||
@@ -9,94 +8,36 @@ interface Step3ConfigPageProps {
|
|||||||
export default function Step3ConfigPage({ onNext }: Step3ConfigPageProps) {
|
export default function Step3ConfigPage({ onNext }: Step3ConfigPageProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ExperimentTitle
|
<BackgroundImage
|
||||||
title="03实验环境配置"
|
src={getStaticImage("experiment/03实验环境配置底图.png")}
|
||||||
description="配置实验所需的软件和硬件环境,确保实验能够顺利进行。"
|
alt="实验工具介绍背景"
|
||||||
/>
|
className="h-full w-full relative pt-51 pl-15"
|
||||||
|
backgroundSize="cover"
|
||||||
{/* 主要内容区域 */}
|
backgroundPosition="center"
|
||||||
<div className="mt-10">
|
>
|
||||||
<div className="bg-white/80 backdrop-blur-sm rounded-lg p-8 shadow-lg">
|
|
||||||
<h3 className="text-2xl font-bold text-gray-800 mb-6">
|
|
||||||
环境配置步骤
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<div className="flex-shrink-0 w-10 h-10 bg-[#3d98cd] text-white rounded-full flex items-center justify-center font-bold text-lg">
|
|
||||||
1
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<h4 className="text-xl font-semibold text-gray-800 mb-2">
|
|
||||||
安装 Keil 软件
|
|
||||||
</h4>
|
|
||||||
<p className="text-gray-600">
|
|
||||||
下载并安装 Keil μVision IDE,用于编写和编译 STM32 程序。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<div className="flex-shrink-0 w-10 h-10 bg-[#3d98cd] text-white rounded-full flex items-center justify-center font-bold text-lg">
|
|
||||||
2
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<h4 className="text-xl font-semibold text-gray-800 mb-2">
|
|
||||||
连接 STM32 主控板
|
|
||||||
</h4>
|
|
||||||
<p className="text-gray-600">
|
|
||||||
使用烧录线将 STM32 主控板连接到电脑,确保驱动正常安装。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<div className="flex-shrink-0 w-10 h-10 bg-[#3d98cd] text-white rounded-full flex items-center justify-center font-bold text-lg">
|
|
||||||
3
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<h4 className="text-xl font-semibold text-gray-800 mb-2">
|
|
||||||
配置传感器连接
|
|
||||||
</h4>
|
|
||||||
<p className="text-gray-600">
|
|
||||||
将力敏传感器正确连接到 STM32 主控板的指定引脚。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<div className="flex-shrink-0 w-10 h-10 bg-[#3d98cd] text-white rounded-full flex items-center justify-center font-bold text-lg">
|
|
||||||
4
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<h4 className="text-xl font-semibold text-gray-800 mb-2">
|
|
||||||
烧录程序
|
|
||||||
</h4>
|
|
||||||
<p className="text-gray-600">
|
|
||||||
在 Keil 中编译程序,并通过烧录线将程序下载到 STM32 主控板。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 底部提示和下一步按钮 */}
|
|
||||||
<div className="text-center mt-10">
|
|
||||||
<p className="text-blue-400 text-2xl font-semibold mb-6">
|
|
||||||
完成以上步骤后,即可进入下一阶段的实验操作。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{onNext && (
|
|
||||||
<div onClick={onNext}>
|
|
||||||
<Image
|
<Image
|
||||||
className="absolute bottom-4 right-6 cursor-pointer"
|
src={getStaticImage("experiment/1.png")}
|
||||||
src={getStaticImage("components/按钮-下一步.png")}
|
className="relative pointer-events-none"
|
||||||
button={true}
|
>
|
||||||
/>
|
<div className="w-full h-14 absolute bottom-45 right-0 hover:bg-[#3F84C5]/50 pointer-events-auto cursor-pointer"></div>
|
||||||
|
</Image>
|
||||||
|
{/* 文字提示 */}
|
||||||
|
<div className="absolute bottom-25 left-0 text-2xl font-bold text-[#3F84C5]">
|
||||||
|
<p className=" leading-relaxed">
|
||||||
|
打开<span className="text-[#E63638] font-bold">力敏传感器</span>
|
||||||
|
文件夹
|
||||||
|
<span className="text-[#E63638] font-bold">USER</span>
|
||||||
|
子文件夹,找到并打开
|
||||||
|
<span className="text-[#E63638] font-bold">
|
||||||
|
工程文件LED.uvprojx
|
||||||
|
</span>
|
||||||
|
。
|
||||||
|
</p>
|
||||||
|
<p className=" mt-4 leading-relaxed">
|
||||||
|
这一步骤是启动实验项目的基础,确保工程文件能够正确加载。
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</BackgroundImage>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ export const selectableTools = [
|
|||||||
name: "STM32主控板",
|
name: "STM32主控板",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: getStaticImage("experiment/serial-cable.png"),
|
src: getStaticImage("experiment/type-c.png"),
|
||||||
name: "TYPE-C",
|
name: "TYPE-C",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -67,7 +67,7 @@ export const selectableTools = [
|
|||||||
name: "烧录线",
|
name: "烧录线",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: getStaticImage("experiment/wireless.png"),
|
src: getStaticImage("experiment/arduino.png"),
|
||||||
name: "Arduino开发板",
|
name: "Arduino开发板",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,24 +1,42 @@
|
|||||||
import { BackgroundImage } from "@/components";
|
import { BackgroundImage } from "@/components";
|
||||||
import { getStaticImage } from "@/utils";
|
import { getStaticImage } from "@/utils";
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
import Step1IntroPage from "./Step1IntroPage";
|
import Step1IntroPage from "./Step1IntroPage";
|
||||||
import Step2SelectPage from "./Step2SelectPage";
|
import Step2SelectPage from "./Step2SelectPage";
|
||||||
import Step3ConfigPage from "./Step3ConfigPage";
|
import Step3ConfigPage from "./Step3ConfigPage";
|
||||||
|
|
||||||
export default function Experiment() {
|
export default function Experiment() {
|
||||||
const [step, setStep] = useState(1); // 1: 工具介绍, 2: 工具选择, 3: 实验环境配置
|
const { step: stepParam } = useParams<{ step?: string }>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// 从路由参数获取step,默认为1
|
||||||
|
const currentStep = stepParam ? parseInt(stepParam, 10) : 1;
|
||||||
|
|
||||||
|
// 验证step参数的有效性
|
||||||
|
const validStep = currentStep >= 1 && currentStep <= 3 ? currentStep : 1;
|
||||||
|
|
||||||
|
// 如果step参数无效,重定向到默认step
|
||||||
|
useEffect(() => {
|
||||||
|
if (stepParam && (currentStep < 1 || currentStep > 3)) {
|
||||||
|
navigate("/experiment/1", { replace: true });
|
||||||
|
}
|
||||||
|
}, [stepParam, currentStep, navigate]);
|
||||||
|
|
||||||
|
// 处理步骤切换
|
||||||
|
const handleNext = (nextStep: number) => {
|
||||||
|
if (nextStep <= 3) {
|
||||||
|
navigate(`/experiment/${nextStep}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BackgroundImage
|
<div className="h-full w-full">
|
||||||
src={getStaticImage("experiment/内容背景页.png")}
|
<div className="h-full">
|
||||||
alt="传感器实验首页"
|
{validStep === 1 && <Step1IntroPage onNext={() => handleNext(2)} />}
|
||||||
className="h-full w-full "
|
{validStep === 2 && <Step2SelectPage onNext={() => handleNext(3)} />}
|
||||||
>
|
{validStep === 3 && <Step3ConfigPage onNext={() => handleNext(4)} />}
|
||||||
<div className="h-full relative py-20 px-10">
|
</div>
|
||||||
{step === 1 && <Step1IntroPage onNext={() => setStep(2)} />}
|
|
||||||
{step === 2 && <Step2SelectPage onNext={() => setStep(3)} />}
|
|
||||||
{step === 3 && <Step3ConfigPage onNext={() => setStep(4)} />}
|
|
||||||
</div>
|
</div>
|
||||||
</BackgroundImage>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -36,6 +36,10 @@ const router = createBrowserRouter([
|
|||||||
path: "/experiment",
|
path: "/experiment",
|
||||||
element: <Experiment />,
|
element: <Experiment />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/experiment/:step",
|
||||||
|
element: <Experiment />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "*",
|
path: "*",
|
||||||
element: <NotFound />,
|
element: <NotFound />,
|
||||||
|
|||||||