import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { Overlay } from "@/components/ui/overlay"; import { useToast } from "@/components/ui/toast"; import { getAppPortalRoot, useCourseCanvas } from "@/course/canvas"; import { DragDropProvider, type DragDropProviderProps, Draggable, Droppable, } from "@/course/interactions/drag-drop"; import { cn } from "@/lib/cn"; import { useUserStore } from "@/stores"; import { getUserStorage, setUserStorage } from "@/utils/yiban"; import { courseCategories, courseResources, createEmptyProgress, parseProgress, } from "./CourseDemo.data"; import { courseDemoDialog } from "./CourseDemo.dialog"; import { courseDemoSpotlight } from "./CourseDemo.spotlight"; import type { CourseFlowProgress } from "./CourseDemo.types"; import { CourseDemoGuideDialog } from "./CourseDemoGuideDialog"; import { CourseDemoGuideSpotlight } from "./CourseDemoGuideSpotlight"; const LESSON_ID = "course-flow"; const PROGRESS_KEY = "resource-classification"; export default function CourseDemo() { const { scale } = useCourseCanvas(); const toast = useToast(); const { courseId, userId, yibanConnectionStatus } = useUserStore(); const [progress, setProgress] = useState(createEmptyProgress); const [restored, setRestored] = useState(false); const [completionOpen, setCompletionOpen] = useState(false); const placedIds = Object.values(progress.placed).flat(); const progressPercent = Math.round( (placedIds.length / courseResources.length) * 100, ); useEffect(() => { let active = true; getUserStorage(LESSON_ID, PROGRESS_KEY) .then((result) => { if (!active || result.status !== "success") return; const savedProgress = parseProgress(result.content); if (savedProgress) setProgress(savedProgress); }) .catch(() => { if (active) toast.warning("暂时无法恢复进度,将从本节开始学习。"); }) .finally(() => { if (active) setRestored(true); }); return () => { active = false; }; }, [toast]); const persistProgress = async (nextProgress: CourseFlowProgress) => { try { await setUserStorage( LESSON_ID, PROGRESS_KEY, JSON.stringify(nextProgress), ); } catch { toast.warning("学习结果已保留在当前页面,远端同步稍后重试。"); } }; const handleDragEnd: DragDropProviderProps["onDragEnd"] = (event) => { const resource = courseResources.find( (item) => item.id === String(event.active.id), ); const categoryId = event.over ? String(event.over.id) : null; if (!resource || !categoryId) { toast.error("请把资源放入一个分类区域。"); return; } if (resource.category !== categoryId) { toast.error(`“${resource.title}”不属于这个分类,再想一想。`); return; } if (placedIds.includes(resource.id)) return; const placed = { ...progress.placed, [resource.category]: [...progress.placed[resource.category], resource.id], }; const completed = Object.values(placed).flat().length === courseResources.length; const nextProgress = { placed, completed, updatedAt: new Date().toISOString(), }; setProgress(nextProgress); void persistProgress(nextProgress); toast.success( completed ? "全部资源分类正确,本节练习已完成。" : `分类正确:${resource.title}`, ); if (completed) setCompletionOpen(true); }; const resetProgress = () => { const nextProgress = createEmptyProgress(); setProgress(nextProgress); setCompletionOpen(false); void persistProgress(nextProgress); toast.info("练习已重置,可以重新完成分类。"); }; return ( <>

学生资助政策 · 互动学习

建立你的课程资源地图

阅读每张资源卡片,将它拖入“政策学习、互动实践、学习支持”中的正确位置。

本节进度 {progressPercent}%
已完成 {placedIds.length}/{courseResources.length} {restored ? "进度已恢复" : "正在恢复进度"} scale {scale.toFixed(3)} {yibanConnectionStatus}
{ const resource = courseResources.find( (item) => item.id === String(activeId), ); return resource ? ( ) : null; }} >
{courseCategories.map((category) => ( {({ isOver }) => (
{category.icon}

{category.title}

{category.description}

{progress.placed[category.id].map((resourceId) => { const resource = courseResources.find( (item) => item.id === resourceId, ); return resource ? ( ) : null; })}

拖到这里完成分类

)}
))}
课程 {courseId ?? "本地预览"} · 学员 {userId ?? "访客"} 每次正确分类都会自动保存,刷新页面后继续学习。
setCompletionOpen(false)} container={getAppPortalRoot} contentClassName="flex items-center justify-center p-8" >
🎉

本节完成

资源地图已建立

你已经能够区分政策材料、互动练习和学习支持资源。学习结果已保存,可以进入下一节课程。

); } function ResourceCard({ resource, overlay = false, placed = false, }: { resource: (typeof courseResources)[number]; overlay?: boolean; placed?: boolean; }) { return (
{resource.icon} {resource.title} {resource.description}
); }