diff --git a/app/components/MobileFundCardDrawer.jsx b/app/components/MobileFundCardDrawer.jsx new file mode 100644 index 0000000..f3f6435 --- /dev/null +++ b/app/components/MobileFundCardDrawer.jsx @@ -0,0 +1,83 @@ +'use client'; + +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerTitle, DrawerTrigger, +} from '@/components/ui/drawer'; +import FundCard from './FundCard'; +import { CloseIcon } from './Icons'; + +/** + * 移动端基金详情底部 Drawer 弹框 + * + * @param {Object} props + * @param {boolean} props.open - 是否打开 + * @param {(open: boolean) => void} props.onOpenChange - 打开状态变化回调 + * @param {boolean} [props.blockDrawerClose] - 是否禁止关闭(如上层有弹框时) + * @param {React.MutableRefObject} [props.ignoreNextDrawerCloseRef] - 忽略下一次关闭(用于点击到内部 dialog 时) + * @param {Object|null} props.cardSheetRow - 当前选中的行数据,用于 getFundCardProps + * @param {(row: any) => Object} [props.getFundCardProps] - 根据行数据返回 FundCard 的 props + */ +export default function MobileFundCardDrawer({ + open, + onOpenChange, + blockDrawerClose = false, + ignoreNextDrawerCloseRef, + cardSheetRow, + getFundCardProps, + children, +}) { + return ( + { + if (!nextOpen) { + if (ignoreNextDrawerCloseRef?.current) { + ignoreNextDrawerCloseRef.current = false; + return; + } + if (!blockDrawerClose) onOpenChange(false); + } + }} + > + + {children} + + { + if (blockDrawerClose) return; + if (e?.target?.closest?.('[data-slot="dialog-content"], [role="dialog"]')) { + if (ignoreNextDrawerCloseRef) ignoreNextDrawerCloseRef.current = true; + return; + } + onOpenChange(false); + }} + > + + + 基金详情 + + + + + +
+ {cardSheetRow && getFundCardProps ? ( + + ) : null} +
+
+
+ ); +} diff --git a/app/components/MobileFundTable.jsx b/app/components/MobileFundTable.jsx index 0eaf475..5e9fd74 100644 --- a/app/components/MobileFundTable.jsx +++ b/app/components/MobileFundTable.jsx @@ -24,17 +24,10 @@ import { } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { throttle } from 'lodash'; -import { - Drawer, - DrawerClose, - DrawerContent, - DrawerHeader, - DrawerTitle, -} from '@/components/ui/drawer'; import FitText from './FitText'; -import FundCard from './FundCard'; +import MobileFundCardDrawer from './MobileFundCardDrawer'; import MobileSettingModal from './MobileSettingModal'; -import { CloseIcon, DragIcon, ExitIcon, SettingsIcon, SortIcon, StarIcon } from './Icons'; +import { DragIcon, ExitIcon, SettingsIcon, SortIcon, StarIcon } from './Icons'; const MOBILE_NON_FROZEN_COLUMN_IDS = [ 'yesterdayChangePercent', @@ -1082,51 +1075,14 @@ export default function MobileFundTable({ /> )} - { - if (!open) { - if (ignoreNextDrawerCloseRef.current) { - ignoreNextDrawerCloseRef.current = false; - return; - } - if (!blockDrawerClose) setCardSheetRow(null); - } - }} - > - { - if (blockDrawerClose) return; - if (e?.target?.closest?.('[data-slot="dialog-content"], [role="dialog"]')) { - ignoreNextDrawerCloseRef.current = true; - return; - } - setCardSheetRow(null); - }} - > - - - 基金详情 - - - - - -
- {cardSheetRow && getFundCardProps ? ( - - ) : null} -
-
-
+ onOpenChange={(open) => { if (!open) setCardSheetRow(null); }} + blockDrawerClose={blockDrawerClose} + ignoreNextDrawerCloseRef={ignoreNextDrawerCloseRef} + cardSheetRow={cardSheetRow} + getFundCardProps={getFundCardProps} + /> {!onlyShowHeader && showPortalHeader && ReactDOM.createPortal(renderContent(true), document.body)} diff --git a/app/globals.css b/app/globals.css index 4b6f46a..79a41cd 100644 --- a/app/globals.css +++ b/app/globals.css @@ -106,8 +106,10 @@ html, body { + overscroll-behavior-y: none; height: 100%; overflow-x: clip; + will-change: auto; /* 或者移除任何 will-change: transform */ } body { diff --git a/app/page.jsx b/app/page.jsx index 62b6c54..f34ef94 100644 --- a/app/page.jsx +++ b/app/page.jsx @@ -3356,13 +3356,13 @@ export default function HomePage() { isScanImporting; if (isAnyModalOpen) { - document.body.style.overflow = 'hidden'; + containerRef.current.style.overflow = 'hidden'; } else { - document.body.style.overflow = ''; + containerRef.current.style.overflow = ''; } return () => { - document.body.style.overflow = ''; + containerRef.current.style.overflow = ''; }; }, [ settingsOpen, diff --git a/components/ui/drawer.jsx b/components/ui/drawer.jsx index aa95895..e9c15f8 100644 --- a/components/ui/drawer.jsx +++ b/components/ui/drawer.jsx @@ -5,6 +5,50 @@ import { Drawer as DrawerPrimitive } from "vaul" import { cn } from "@/lib/utils" +const DrawerScrollLockContext = React.createContext(null) + +/** + * 移动端滚动锁定:仅将 body 设为 position:fixed,用负值 top 把页面“拉”回当前视口位置, + * 既锁定滚动又保留视觉位置;overlay 上 ontouchmove preventDefault 防止背景触摸滚动。 + */ +function useScrollLock(open) { + const savedScrollYRef = React.useRef(0) + const onOverlayTouchMove = React.useCallback((e) => { + e.preventDefault() + }, []) + + React.useEffect(() => { + if (!open || typeof document === "undefined") return + const scrollY = window.scrollY ?? window.pageYOffset + savedScrollYRef.current = scrollY + const prev = { + position: document.body.style.position, + top: document.body.style.top, + left: document.body.style.left, + right: document.body.style.right, + width: document.body.style.width, + } + document.body.style.position = "fixed" + document.body.style.top = `-${scrollY}px` + document.body.style.left = "0" + document.body.style.right = "0" + document.body.style.width = "100%" + return () => { + document.body.style.position = prev.position + document.body.style.top = prev.top + document.body.style.left = prev.left + document.body.style.right = prev.right + document.body.style.width = prev.width + window.scrollTo(0, savedScrollYRef.current) + } + }, [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$/) @@ -12,10 +56,17 @@ function parseVhToPx(vhStr) { return (window.innerHeight * Number(match[1])) / 100 } -function Drawer({ - ...props -}) { - return ; +function Drawer({ open, ...props }) { + const scrollLock = useScrollLock(open) + const contextValue = React.useMemo( + () => ({ ...scrollLock, open: !!open }), + [scrollLock, open] + ) + return ( + + + + ) } function DrawerTrigger({ @@ -40,14 +91,26 @@ function DrawerOverlay({ className, ...props }) { + const ctx = React.useContext(DrawerScrollLockContext) + const { open = false, ...scrollLockProps } = ctx || {} + // modal={false} 时 vaul 不渲染/隐藏 Overlay,用自定义遮罩 div 保证始终有遮罩;点击遮罩关闭 return ( - + +
+ ); }