import { useEffect, useState, type ReactNode } from "react"; import { createPortal } from "react-dom"; import { CheckCircle2, Download, Inbox, Music, Shield, Trash2, X } from "lucide-react"; // 抽屉 / 弹窗 / 全屏播放器必须挂到 document.body。 // 写在页面树里会被顶栏(sticky + z-index)盖住:搜索、余额、铃铛会浮在抽屉上。 export function OverlayPortal({ children }: { children: ReactNode }) { return createPortal(children, document.body); } // 浮层打开时锁住 body 滚动(否则滚轮会滚动遮罩后面的页面,体感"遮罩没盖住")。 // 多浮层叠开用计数,最后一个关闭才解锁。 let scrollLockCount = 0; export function useBodyScrollLock(active: boolean) { useEffect(() => { if (!active) return; scrollLockCount += 1; document.body.style.overflow = "hidden"; return () => { scrollLockCount -= 1; if (scrollLockCount <= 0) { scrollLockCount = 0; document.body.style.overflow = ""; } }; }, [active]); } // 浮层进出场过渡 + ESC 统一接管。 // 进场:挂载后「下一帧」再上 .show,让 CSS 的 scale(.96→1)/translateX 过渡真正跑起来 // (此前直接挂 .show,首帧即终态 → 没有进场动画)。 // 退场:先撤 .show 播退场过渡,等过渡时长后再卸载。 // ESC:open 时监听 Escape → onClose(对齐 MediaLightbox 既有行为)。 export function useOverlayTransition(open: boolean, onClose?: () => void, durationMs = 260) { const [mounted, setMounted] = useState(open); const [show, setShow] = useState(false); useEffect(() => { let raf1 = 0; let raf2 = 0; let timer = 0; if (open) { setMounted(true); raf1 = requestAnimationFrame(() => { raf2 = requestAnimationFrame(() => setShow(true)); }); } else { setShow(false); timer = window.setTimeout(() => setMounted(false), durationMs); } return () => { cancelAnimationFrame(raf1); cancelAnimationFrame(raf2); window.clearTimeout(timer); }; }, [open, durationMs]); useEffect(() => { if (!open || !onClose) return; const onKey = (event: KeyboardEvent) => { if (event.key === "Escape") onClose(); }; document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); }, [open, onClose]); return { mounted, show }; } // 通用媒体预览灯箱:点击图片放大 / 点击视频弹窗播放 / 点击音频弹窗试听(复用 .np-lightbox 样式) // 背景点击 / Esc / 关闭键都可关闭;点媒体本身不关闭。 export function MediaLightbox({ open, src, kind, name, close, onDownload }: { open: boolean; src: string; kind?: "image" | "video" | "audio"; name?: string; close: () => void; onDownload?: () => void; }) { useBodyScrollLock(open && !!src); useEffect(() => { if (!open) return; const onKey = (event: KeyboardEvent) => { if (event.key === "Escape") close(); }; document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); }, [open, close]); if (!open || !src) return null; // 挂到 body:浮层才能盖住顶栏(搜索 / 余额 / 铃铛)和侧栏 return createPortal(
event.stopPropagation()}> {onDownload ? ( ) : null}
{kind === "video" ? (
, document.body, ); } export function SettingRow({ title, desc, action, toggle, checked }: { title: string; desc: string; action?: string; toggle?: boolean; checked?: boolean }) { return (
{title}{desc}
{toggle ? : }
); } export function TeamModal({ open, title, subtitle = "", icon, close, children, footer, dismissable = true }: { open: boolean; title: string; subtitle?: string; icon: ReactNode; close: () => void; children: ReactNode; footer?: ReactNode; /** 是否允许点击遮罩关闭弹窗。默认 true(保持原有行为);传 false 则点遮罩不响应。 */ dismissable?: boolean; }) { useBodyScrollLock(open); const { mounted, show } = useOverlayTransition(open, close); if (!mounted) return null; // 挂到 body:浮层才能盖住顶栏(搜索 / 余额 / 铃铛)和侧栏 return createPortal(
event.stopPropagation()}> ++
{icon}
{title}{subtitle}
{children}
{footer || }
, document.body, ); } export function ConfirmModal({ open, title, detail, confirmText, subtitle = "", icon, onCancel, onConfirm, dismissable = true, showCancel = true, priority = false }: { open: boolean; title: string; detail: ReactNode; confirmText: string; subtitle?: string; icon?: ReactNode; onCancel: () => void; onConfirm: () => void | Promise; /** 是否允许点击遮罩关闭弹窗。默认 true(保持原有行为);传 false 则点遮罩不响应。 */ dismissable?: boolean; /** 强制确认场景可隐藏取消按钮;默认显示,保持现有弹窗行为。 */ showCancel?: boolean; /** 系统级强制提示置于灯箱、抽屉和普通确认框之上。 */ priority?: boolean; }) { useBodyScrollLock(open); const { mounted, show } = useOverlayTransition(open, onCancel); if (!mounted) return null; // 挂到 body:浮层才能盖住顶栏(搜索 / 余额 / 铃铛)和侧栏 return createPortal(
event.stopPropagation()}> ++
{icon ?? }
{title}{subtitle ? {subtitle} : null}
{detail}
{showCancel ? : null}
, document.body, ); } /* 成功提示弹窗(复用 .modal 设计系统)· 标题 + 说明 + 自定义动作按钮组 */ export function SuccessModal({ open, title, detail, actions, close }: { open: boolean; title: string; detail: string; actions: ReactNode; close: () => void; }) { useBodyScrollLock(open); const { mounted, show } = useOverlayTransition(open, close); if (!mounted) return null; return createPortal(
event.stopPropagation()}> ++
{title}
{detail}
{actions}
, document.body, ); } export function Drawer({ title, open, close, children, className }: { title: string; open: boolean; close: () => void; children: ReactNode; className?: string }) { useBodyScrollLock(open); const { mounted, show } = useOverlayTransition(open, close); if (!mounted) return null; return createPortal( <>
, document.body, ); } export function EmptyPanel({ title, action, onAction }: { title: string; action: string; onAction: () => void }) { return

{title}

先创建资料后进入下一步

; }