fix:修复移动端drawer 自动滚动到顶部的行为
This commit is contained in:
83
app/components/MobileFundCardDrawer.jsx
Normal file
83
app/components/MobileFundCardDrawer.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -106,8 +106,10 @@
|
||||
|
||||
html,
|
||||
body {
|
||||
overscroll-behavior-y: none;
|
||||
height: 100%;
|
||||
overflow-x: clip;
|
||||
will-change: auto; /* 或者移除任何 will-change: transform */
|
||||
}
|
||||
|
||||
body {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user