import { useRef, useState } from "react"; import { GuideSpotlight } from "@/components/common"; import { Button } from "@/components/ui/button"; import { Overlay, type OverlayProps } from "@/components/ui/overlay"; import { Portal } from "@/components/ui/portal"; import { useToast } from "@/components/ui/toast"; import { getAppPortalRoot, useCourseCanvas } from "@/course/canvas"; import { useSpotlightStore } from "@/stores"; import { DragDropDemo } from "./DragDropDemo"; type OverlayDemoKey = | "dim" | "body" | "custom" | "light" | "gradient" | "image" | "locked"; const overlayCards: Array<{ key: OverlayDemoKey; title: string; description: string; buttonText: string; overlayProps?: Pick< OverlayProps, | "variant" | "backgroundImage" | "backgroundClassName" | "closeOnBackgroundClick" >; }> = [ { key: "dim", title: "课程画布遮罩", description: "挂载到 App 画布,适合课程弹窗、确认提示和答题反馈。", buttonText: "打开画布 Overlay", overlayProps: {}, }, { key: "body", title: "body 挂载", description: "默认挂载到 body,适合作为浏览器视口全局层。", buttonText: "打开 body Overlay", overlayProps: {}, }, { key: "custom", title: "ref 容器挂载", description: "通过 container={ref} 挂到上方蓝色虚线容器内,适合局部弹层测试。", buttonText: "打开 ref Overlay", overlayProps: { backgroundClassName: "bg-black/50" }, }, { key: "light", title: "浅色背景", description: "适合浅色视觉风格或需要弱化遮罩的课程页面。", buttonText: "打开浅色 Overlay", overlayProps: { variant: "light" }, }, { key: "gradient", title: "Tailwind 自定义背景", description: "通过 backgroundClassName 使用 Tailwind class 控制背景层。", buttonText: "打开渐变 Overlay", overlayProps: { variant: "transparent", backgroundClassName: "bg-gradient-to-br from-blue-950/80 via-indigo-900/70 to-purple-900/80", }, }, { key: "image", title: "背景图片示例", description: "backgroundImage 接收合法的 CSS background-image 值。", buttonText: "打开图片背景 Overlay", overlayProps: { variant: "transparent", backgroundImage: "linear-gradient(135deg, rgb(30 58 138 / 0.88), rgb(88 28 135 / 0.8))", backgroundClassName: "bg-cover bg-center", }, }, { key: "locked", title: "禁止点击背景关闭", description: "适合必须点击按钮确认的课程提示或关键操作。", buttonText: "打开锁定 Overlay", overlayProps: { closeOnBackgroundClick: false, backgroundClassName: "bg-black/70", }, }, ]; export default function Components() { const customRootRef = useRef(null); const [portalVisible, setPortalVisible] = useState(false); const [customPortalVisible, setCustomPortalVisible] = useState(false); const [activeOverlay, setActiveOverlay] = useState( null, ); const currentOverlay = overlayCards.find( (item) => item.key === activeOverlay, ); const isCustomOverlay = currentOverlay?.key === "custom"; const isBodyOverlay = currentOverlay?.key === "body"; const overlayContainer = isBodyOverlay ? undefined : isCustomOverlay ? customRootRef : getAppPortalRoot; const toast = useToast(); const { scale } = useCourseCanvas(); const startGuideSpotlight = () => { useSpotlightStore.getState().startSpotlight({ courseId: "components-playground", spotlightId: "spotlight-demo", steps: [ { id: "guide-card", target: { dataGuide: "spotlight-card" }, title: "引导目标区域", description: "通过 data-guide 标记页面元素,GuideSpotlight 会自动定位并高亮目标。", placement: "bottom", }, { id: "guide-actions", target: { dataGuide: "spotlight-actions" }, title: "引导操作按钮", description: "可以为不同步骤设置 placement、文案和高亮间距。", placement: "top", padding: 12, }, { id: "guide-note", target: { dataGuide: "spotlight-note" }, title: "完成引导", description: "示例页中可以手动触发引导,便于调试课程页面的新手提示流程。", placement: "left", doneLabel: "知道了", }, ], }); }; return (

Components Playground

组件示例页

用于验证组件在模板中正常使用,展示与组件库(yuxiang-kit)集成效果。

📐 本模板使用固定 1920 × 1080 设计画布 + CSS transform 整体缩放

当前缩放比例 scale = {scale.toFixed(3)}。 DragDrop 等组件通过 scale 参数修正坐标, GuideSpotlight 通过 viewport + container 定位画布内元素。

Button

现有按钮组件的 variant 和 size 示例。

Toast

通过 useToast 显示轻提示,支持 info、success、warning、error 四种类型。

GuideSpotlight

引导组件会根据 data-guide 或 selector 高亮页面元素,适合课程新手引导、功能提示和操作说明。

Guide Demo

课程引导示例区域

点击右侧按钮后,会依次高亮这个卡片、操作按钮和这段说明文字。

Portal

Portal 默认挂载到 document.body,也可以通过 container 指定自定义挂载位置。

这是自定义 ref 容器。Portal 或 Overlay 都可以通过 container 挂载到这个容器里。

{portalVisible && (
这是通过 Portal 挂载到 body 的内容
)} {customPortalVisible && (
这是通过 container={ref} 挂载的内容
)}

Overlay

Overlay 内部已使用 Portal,可同时测试背景、关闭行为和挂载位置。

{overlayCards.map((item) => (

{item.title}

{item.description}

))}
{currentOverlay && ( setActiveOverlay(null)} container={overlayContainer} className={isCustomOverlay ? "z-10" : undefined} contentClassName="flex items-center justify-center p-8" {...currentOverlay.overlayProps} >

Overlay Demo

{currentOverlay.title}

{currentOverlay.description}

)}
); }