"use client" import * as React from "react" import { Drawer as DrawerPrimitive } from "vaul" import { cn } from "@/lib/utils" import { useBodyScrollLock } from "../../app/hooks/useBodyScrollLock" const DrawerScrollLockContext = React.createContext(null) /** * 移动端滚动锁定:仅将 body 设为 position:fixed,用负值 top 把页面“拉”回当前视口位置, * 既锁定滚动又保留视觉位置;overlay 上 ontouchmove preventDefault 防止背景触摸滚动。 */ function useScrollLock(open) { const onOverlayTouchMove = React.useCallback((e) => { e.preventDefault() }, []) // 统一使用 app 级 hook 处理 body 滚动锁定 & 恢复,避免多处实现导致位移/跳顶问题 useBodyScrollLock(open) return React.useMemo( () => (open ? { onTouchMove: onOverlayTouchMove } : null), [open, onOverlayTouchMove] ) } function parseVhToPx(vhStr) { if (typeof vhStr === "number") return vhStr const match = String(vhStr).match(/^([\d.]+)\s*vh$/) if (!match) return null return (window.innerHeight * Number(match[1])) / 100 } function Drawer({ open, ...props }) { const scrollLock = useScrollLock(open) const contextValue = React.useMemo( () => ({ ...scrollLock, open: !!open }), [scrollLock, open] ) return ( ) } function DrawerTrigger({ ...props }) { return ; } function DrawerPortal({ ...props }) { return ; } function DrawerClose({ ...props }) { return ; } function DrawerOverlay({ className, ...props }) { const ctx = React.useContext(DrawerScrollLockContext) const { open = false, ...scrollLockProps } = ctx || {} // modal={false} 时 vaul 不渲染/隐藏 Overlay,用自定义遮罩 div 保证始终有遮罩;点击遮罩关闭 return (
); } function DrawerContent({ className, children, defaultHeight = "77vh", minHeight = "20vh", maxHeight = "90vh", ...props }) { const [heightPx, setHeightPx] = React.useState(() => typeof window !== "undefined" ? parseVhToPx(defaultHeight) : null ); const [isDragging, setIsDragging] = React.useState(false); const dragRef = React.useRef({ startY: 0, startHeight: 0 }); const minPx = React.useMemo(() => parseVhToPx(minHeight), [minHeight]); const maxPx = React.useMemo(() => parseVhToPx(maxHeight), [maxHeight]); React.useEffect(() => { const px = parseVhToPx(defaultHeight); if (px != null) setHeightPx(px); }, [defaultHeight]); React.useEffect(() => { const sync = () => { const max = parseVhToPx(maxHeight); const min = parseVhToPx(minHeight); setHeightPx((prev) => { if (prev == null) return parseVhToPx(defaultHeight); const clamped = Math.min(prev, max ?? prev); return Math.max(clamped, min ?? clamped); }); }; window.addEventListener("resize", sync); return () => window.removeEventListener("resize", sync); }, [defaultHeight, minHeight, maxHeight]); const handlePointerDown = React.useCallback( (e) => { e.preventDefault(); setIsDragging(true); dragRef.current = { startY: e.clientY ?? e.touches?.[0]?.clientY, startHeight: heightPx ?? parseVhToPx(defaultHeight) ?? 0 }; }, [heightPx, defaultHeight] ); React.useEffect(() => { if (!isDragging) return; const move = (e) => { const clientY = e.clientY ?? e.touches?.[0]?.clientY; const { startY, startHeight } = dragRef.current; const delta = startY - clientY; const next = Math.min(maxPx ?? Infinity, Math.max(minPx ?? 0, startHeight + delta)); setHeightPx(next); }; const up = () => setIsDragging(false); document.addEventListener("mousemove", move, { passive: true }); document.addEventListener("mouseup", up); document.addEventListener("touchmove", move, { passive: true }); document.addEventListener("touchend", up); return () => { document.removeEventListener("mousemove", move); document.removeEventListener("mouseup", up); document.removeEventListener("touchmove", move); document.removeEventListener("touchend", up); }; }, [isDragging, minPx, maxPx]); const contentStyle = React.useMemo(() => { if (heightPx == null) return undefined; return { height: `${heightPx}px`, maxHeight: maxPx != null ? `${maxPx}px` : undefined }; }, [heightPx, maxPx]); return (