fix:修复移动端drawer 自动滚动到顶部的行为

This commit is contained in:
hzm
2026-03-12 08:45:39 +08:00
parent 613b5f02e8
commit 063be7d08e
5 changed files with 171 additions and 67 deletions

View File

@@ -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<boolean>} [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 (
<Drawer
open={open}
onOpenChange={(nextOpen) => {
if (!nextOpen) {
if (ignoreNextDrawerCloseRef?.current) {
ignoreNextDrawerCloseRef.current = false;
return;
}
if (!blockDrawerClose) onOpenChange(false);
}
}}
>
<DrawerTrigger asChild>
{children}
</DrawerTrigger>
<DrawerContent
className="h-[77vh] max-h-[88vh] mt-0 flex flex-col"
onPointerDownOutside={(e) => {
if (blockDrawerClose) return;
if (e?.target?.closest?.('[data-slot="dialog-content"], [role="dialog"]')) {
if (ignoreNextDrawerCloseRef) ignoreNextDrawerCloseRef.current = true;
return;
}
onOpenChange(false);
}}
>
<DrawerHeader className="flex-shrink-0 flex flex-row items-center justify-between gap-2 space-y-0 px-5 pb-4 pt-2 text-left">
<DrawerTitle className="text-base font-semibold text-[var(--text)]">
基金详情
</DrawerTitle>
<DrawerClose
className="icon-button border-none bg-transparent p-1"
title="关闭"
style={{ borderColor: 'transparent', backgroundColor: 'transparent' }}
>
<CloseIcon width="20" height="20" />
</DrawerClose>
</DrawerHeader>
<div
className="flex-1 min-h-0 overflow-y-auto px-5 pb-8 pt-0"
style={{ paddingBottom: 'calc(24px + env(safe-area-inset-bottom, 0px))' }}
>
{cardSheetRow && getFundCardProps ? (
<FundCard {...getFundCardProps(cardSheetRow)} />
) : null}
</div>
</DrawerContent>
</Drawer>
);
}

View File

@@ -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({
/>
)}
<Drawer
<MobileFundCardDrawer
open={!!(cardSheetRow && getFundCardProps)}
onOpenChange={(open) => {
if (!open) {
if (ignoreNextDrawerCloseRef.current) {
ignoreNextDrawerCloseRef.current = false;
return;
}
if (!blockDrawerClose) setCardSheetRow(null);
}
}}
>
<DrawerContent
className="h-[77vh] max-h-[88vh] mt-0 flex flex-col"
onPointerDownOutside={(e) => {
if (blockDrawerClose) return;
if (e?.target?.closest?.('[data-slot="dialog-content"], [role="dialog"]')) {
ignoreNextDrawerCloseRef.current = true;
return;
}
setCardSheetRow(null);
}}
>
<DrawerHeader className="flex-shrink-0 flex flex-row items-center justify-between gap-2 space-y-0 px-5 pb-4 pt-2 text-left">
<DrawerTitle className="text-base font-semibold text-[var(--text)]">
基金详情
</DrawerTitle>
<DrawerClose
className="icon-button border-none bg-transparent p-1"
title="关闭"
style={{ borderColor: 'transparent', backgroundColor: 'transparent' }}
>
<CloseIcon width="20" height="20" />
</DrawerClose>
</DrawerHeader>
<div
className="flex-1 min-h-0 overflow-y-auto px-5 pb-8 pt-0"
style={{ paddingBottom: 'calc(24px + env(safe-area-inset-bottom, 0px))' }}
>
{cardSheetRow && getFundCardProps ? (
<FundCard {...getFundCardProps(cardSheetRow)} />
) : null}
</div>
</DrawerContent>
</Drawer>
onOpenChange={(open) => { if (!open) setCardSheetRow(null); }}
blockDrawerClose={blockDrawerClose}
ignoreNextDrawerCloseRef={ignoreNextDrawerCloseRef}
cardSheetRow={cardSheetRow}
getFundCardProps={getFundCardProps}
/>
{!onlyShowHeader && showPortalHeader && ReactDOM.createPortal(renderContent(true), document.body)}
</div>

View File

@@ -106,8 +106,10 @@
html,
body {
overscroll-behavior-y: none;
height: 100%;
overflow-x: clip;
will-change: auto; /* 或者移除任何 will-change: transform */
}
body {

View File

@@ -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,

View File

@@ -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 <DrawerPrimitive.Root data-slot="drawer" {...props} />;
function Drawer({ open, ...props }) {
const scrollLock = useScrollLock(open)
const contextValue = React.useMemo(
() => ({ ...scrollLock, open: !!open }),
[scrollLock, open]
)
return (
<DrawerScrollLockContext.Provider value={contextValue}>
<DrawerPrimitive.Root modal={false} data-slot="drawer" open={open} {...props} />
</DrawerScrollLockContext.Provider>
)
}
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 (
<DrawerPrimitive.Overlay
data-slot="drawer-overlay"
className={cn(
"fixed inset-0 z-50 bg-[var(--drawer-overlay)] backdrop-blur-[4px] data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
className
)}
{...props} />
<DrawerPrimitive.Close asChild>
<div
data-slot="drawer-overlay"
data-state={open ? "open" : "closed"}
role="button"
tabIndex={-1}
aria-label="关闭"
className={cn(
"fixed inset-0 z-50 cursor-default bg-[var(--drawer-overlay,rgba(0,0,0,0.45))] backdrop-blur-[6px]",
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
className
)}
{...scrollLockProps}
{...props}
/>
</DrawerPrimitive.Close>
);
}