import { useState } from "react";
import {
DragDropProvider,
type DragDropProviderProps,
Draggable,
Droppable,
} from "@/components/course-interactions/drag-drop";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/cn";
import { useScale } from "@/utils";
import type {
DragDemoItem,
DragDemoZone,
DragDropDemoResult,
} from "./DragDropDemo.types";
const dragItems = [
{
id: "apple",
label: "苹果",
description: "富含维生素的水果",
icon: "🍎",
tone: "rose",
targetZoneId: "fruit-zone",
},
{
id: "banana",
label: "香蕉",
description: "常见的热带水果",
icon: "🍌",
tone: "lime",
targetZoneId: "fruit-zone",
},
{
id: "car",
label: "汽车",
description: "陆地出行工具",
icon: "🚗",
tone: "blue",
targetZoneId: "traffic-zone",
},
{
id: "bike",
label: "自行车",
description: "绿色低碳出行工具",
icon: "🚲",
tone: "cyan",
targetZoneId: "traffic-zone",
},
{
id: "book",
label: "书本",
description: "阅读和学习资料",
icon: "📘",
tone: "amber",
targetZoneId: "study-zone",
},
{
id: "pencil",
label: "铅笔",
description: "书写和绘画工具",
icon: "✏️",
tone: "indigo",
targetZoneId: "study-zone",
},
] satisfies DragDemoItem[];
const dropZones = [
{
id: "fruit-zone",
title: "水果类",
description: "把可以食用的水果放到这里",
icon: "🍓",
tone: "emerald",
},
{
id: "traffic-zone",
title: "交通工具",
description: "把出行相关物品放到这里",
icon: "🚌",
tone: "sky",
},
{
id: "study-zone",
title: "学习用品",
description: "把学习和书写用品放到这里",
icon: "🎒",
tone: "violet",
},
] satisfies DragDemoZone[];
const dragItemClassNames = {
rose: "border-rose-200 bg-rose-50 text-rose-700 shadow-rose-100",
blue: "border-blue-200 bg-blue-50 text-blue-700 shadow-blue-100",
amber: "border-amber-200 bg-amber-50 text-amber-700 shadow-amber-100",
lime: "border-lime-200 bg-lime-50 text-lime-700 shadow-lime-100",
cyan: "border-cyan-200 bg-cyan-50 text-cyan-700 shadow-cyan-100",
indigo: "border-indigo-200 bg-indigo-50 text-indigo-700 shadow-indigo-100",
};
const zoneIconClassNames = {
emerald: "bg-emerald-100 text-emerald-600",
sky: "bg-sky-100 text-sky-600",
violet: "bg-violet-100 text-violet-600",
};
const zoneOverClassNames = {
emerald:
"border-emerald-400 bg-emerald-50 shadow-[0_0_0_6px_rgba(16,185,129,0.10),0_18px_45px_rgba(16,185,129,0.16)]",
sky: "border-sky-400 bg-sky-50 shadow-[0_0_0_6px_rgba(14,165,233,0.10),0_18px_45px_rgba(14,165,233,0.16)]",
violet:
"border-violet-400 bg-violet-50 shadow-[0_0_0_6px_rgba(139,92,246,0.10),0_18px_45px_rgba(139,92,246,0.16)]",
};
const findDragItem = (itemId: string) =>
dragItems.find((item) => item.id === itemId);
const getZoneTotal = (zoneId: string) =>
dragItems.filter((item) => item.targetZoneId === zoneId).length;
const renderDragItem = (item: DragDemoItem, className?: string) => (
{item.icon}
{item.label}
{item.description}
);
const renderPlacedItem = (item: DragDemoItem) => (
{item.icon}
{item.label}
);
export function DragDropDemo() {
const scale = useScale();
const [dropResult, setDropResult] = useState(null);
const [placedItemIds, setPlacedItemIds] = useState>(
{},
);
const placedIds = Object.values(placedItemIds).flat();
const placedCount = placedIds.length;
const progress = Math.round((placedCount / dragItems.length) * 100);
const isCompleted = placedCount === dragItems.length;
const handleDragEnd: DragDropProviderProps["onDragEnd"] = (event) => {
if (isCompleted) {
return;
}
const draggedId = String(event.active.id);
const droppedId = event.over ? String(event.over.id) : null;
const draggedItem = findDragItem(draggedId);
if (!droppedId || !draggedItem) {
setDropResult({
type: "error",
message: "没有放入目标区域,请再试一次。",
});
return;
}
if (placedIds.includes(draggedId)) {
return;
}
if (draggedItem.targetZoneId !== droppedId) {
setDropResult({
type: "error",
message: `还不对:${draggedItem.label} 不属于这个分类。`,
});
return;
}
const nextPlacedItemIds = {
...placedItemIds,
[droppedId]: [...(placedItemIds[droppedId] ?? []), draggedId],
};
const nextPlacedCount = Object.values(nextPlacedItemIds).flat().length;
setPlacedItemIds(nextPlacedItemIds);
setDropResult({
type: "success",
message:
nextPlacedCount === dragItems.length
? "全部分类完成,做得很好!"
: `答对了:${draggedItem.label} 已放入正确分类。`,
});
};
const handleDragStart = () => {
if (!isCompleted) {
setDropResult(null);
}
};
const handleDragCancel = () => {
if (!isCompleted) {
setDropResult(null);
}
};
const handleRetry = () => {
setDropResult(null);
setPlacedItemIds({});
};
return (
Interactive Course Demo
Drag and Drop 分类挑战
将左侧物品拖入右侧正确分类。拖拽预览使用自定义
DragOverlay,释放区根据是否匹配给出颜色反馈。
完成 {placedCount}/{dragItems.length}
scale {scale.toFixed(3)}
{
const activeItem = findDragItem(String(activeId));
return activeItem
? renderDragItem(
activeItem,
"pointer-events-none w-[300px] shadow-xl ring-4 ring-white/70",
)
: null;
}}
>
{dragItems.length - placedCount} left
{dragItems
.filter((item) => !placedIds.includes(item.id))
.map((item) => (
{renderDragItem(item)}
))}
{isCompleted && (
所有物品都已完成分类
)}
{dropZones.map((zone) => {
const zonePlacedIds = placedItemIds[zone.id] ?? [];
const placedItems = zonePlacedIds
.map((itemId) => findDragItem(itemId))
.filter((item): item is DragDemoItem => Boolean(item));
const zoneTotal = getZoneTotal(zone.id);
return (
{
const activeItem = findDragItem(String(activeId));
return (
activeItem?.targetZoneId === zone.id &&
!zonePlacedIds.includes(String(activeId))
);
}}
className={({ isOver, canDrop }) =>
cn(
"min-h-80 rounded-3xl border-2 border-dashed border-slate-300 bg-white p-5 transition-[background-color,border-color,box-shadow] duration-150",
isOver && canDrop && zoneOverClassNames[zone.tone],
isOver &&
!canDrop &&
"border-rose-300 bg-rose-50 shadow-[0_0_0_6px_rgba(244,63,94,0.08)]",
)
}
>
{zone.icon}
{zone.title}
{zone.description}
{placedItems.length}/{zoneTotal}
{placedItems.length > 0 ? (
placedItems.map((item) => (
{renderPlacedItem(item)}
))
) : (
等待放入正确物品
)}
);
})}
{dropResult && (
{dropResult.message}
)}
);
}