From 063be7d08e48ff746125f5a8c1ee241d05313927 Mon Sep 17 00:00:00 2001 From: hzm <934585316@qq.com> Date: Thu, 12 Mar 2026 08:45:39 +0800 Subject: [PATCH] =?UTF-8?q?fix=EF=BC=9A=E4=BF=AE=E5=A4=8D=E7=A7=BB?= =?UTF-8?q?=E5=8A=A8=E7=AB=AFdrawer=20=E8=87=AA=E5=8A=A8=E6=BB=9A=E5=8A=A8?= =?UTF-8?q?=E5=88=B0=E9=A1=B6=E9=83=A8=E7=9A=84=E8=A1=8C=E4=B8=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/MobileFundCardDrawer.jsx | 83 ++++++++++++++++++++++++ app/components/MobileFundTable.jsx | 62 +++--------------- app/globals.css | 2 + app/page.jsx | 6 +- components/ui/drawer.jsx | 85 +++++++++++++++++++++---- 5 files changed, 171 insertions(+), 67 deletions(-) create mode 100644 app/components/MobileFundCardDrawer.jsx 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 ( - + +
+ ); }