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}
)}
); }