fix: PC 端基金详情弹框滚动问题

This commit is contained in:
hzm
2026-03-19 11:27:36 +08:00
parent 8d7f2d33df
commit 6557371f09
3 changed files with 170 additions and 154 deletions

View File

@@ -1077,8 +1077,9 @@ export default function PcFundTable({
const totalHeaderWidth = headerGroup?.headers?.reduce((acc, h) => acc + h.column.getSize(), 0) ?? 0; const totalHeaderWidth = headerGroup?.headers?.reduce((acc, h) => acc + h.column.getSize(), 0) ?? 0;
return ( return (
<div className="pc-fund-table" ref={tableContainerRef}> <>
<style>{` <div className="pc-fund-table" ref={tableContainerRef}>
<style>{`
.table-row-scroll { .table-row-scroll {
--row-bg: var(--bg); --row-bg: var(--bg);
background-color: var(--row-bg) !important; background-color: var(--row-bg) !important;
@@ -1175,87 +1176,134 @@ export default function PcFundTable({
opacity: 0; opacity: 0;
} }
`}</style> `}</style>
{/* 表头 */} {/* 表头 */}
{renderTableHeader(false)} {renderTableHeader(false)}
{/* 表体 */} {/* 表体 */}
<DndContext <DndContext
sensors={sensors} sensors={sensors}
collisionDetection={closestCenter} collisionDetection={closestCenter}
onDragStart={handleDragStart} onDragStart={handleDragStart}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel} onDragCancel={handleDragCancel}
modifiers={[restrictToVerticalAxis, restrictToParentElement]} modifiers={[restrictToVerticalAxis, restrictToParentElement]}
>
<SortableContext
items={data.map((item) => item.code)}
strategy={verticalListSortingStrategy}
> >
<AnimatePresence mode="popLayout"> <SortableContext
{table.getRowModel().rows.map((row, index) => ( items={data.map((item) => item.code)}
<SortableRow key={row.original.code || row.id} row={row} isTableDragging={!!activeId} disabled={sortBy !== 'default'}> strategy={verticalListSortingStrategy}
<div >
className={`table-row table-row-scroll ${index % 2 === 1 ? 'row-even' : ''}`} <AnimatePresence mode="popLayout">
> {table.getRowModel().rows.map((row, index) => (
{row.getVisibleCells().map((cell) => { <SortableRow key={row.original.code || row.id} row={row} isTableDragging={!!activeId} disabled={sortBy !== 'default'}>
const columnId = cell.column.id || cell.column.columnDef?.accessorKey; <div
const isNameColumn = columnId === 'fundName'; className={`table-row table-row-scroll ${index % 2 === 1 ? 'row-even' : ''}`}
const rightAlignedColumns = new Set([ >
'latestNav', {row.getVisibleCells().map((cell) => {
'estimateNav', const columnId = cell.column.id || cell.column.columnDef?.accessorKey;
'yesterdayChangePercent', const isNameColumn = columnId === 'fundName';
'estimateChangePercent', const rightAlignedColumns = new Set([
'totalChangePercent', 'latestNav',
'holdingAmount', 'estimateNav',
'todayProfit', 'yesterdayChangePercent',
'holdingProfit', 'estimateChangePercent',
]); 'totalChangePercent',
const align = isNameColumn 'holdingAmount',
? '' 'todayProfit',
: rightAlignedColumns.has(columnId) 'holdingProfit',
? 'text-right' ]);
: 'text-center'; const align = isNameColumn
const cellClassName = ? ''
(cell.column.columnDef.meta && cell.column.columnDef.meta.cellClassName) || ''; : rightAlignedColumns.has(columnId)
const style = getCommonPinningStyles(cell.column, false); ? 'text-right'
const isPinned = cell.column.getIsPinned(); : 'text-center';
return ( const cellClassName =
<div (cell.column.columnDef.meta && cell.column.columnDef.meta.cellClassName) || '';
key={cell.id} const style = getCommonPinningStyles(cell.column, false);
className={`table-cell ${align} ${cellClassName} ${isPinned ? 'pinned-cell' : ''}`} const isPinned = cell.column.getIsPinned();
style={style} return (
> <div
{flexRender( key={cell.id}
cell.column.columnDef.cell, className={`table-cell ${align} ${cellClassName} ${isPinned ? 'pinned-cell' : ''}`}
cell.getContext(), style={style}
)} >
</div> {flexRender(
); cell.column.columnDef.cell,
})} cell.getContext(),
</div> )}
</SortableRow> </div>
))} );
</AnimatePresence> })}
</SortableContext> </div>
</DndContext> </SortableRow>
))}
</AnimatePresence>
</SortableContext>
</DndContext>
{table.getRowModel().rows.length === 0 && ( {table.getRowModel().rows.length === 0 && (
<div className="table-row empty-row"> <div className="table-row empty-row">
<div className="table-cell" style={{ textAlign: 'center' }}> <div className="table-cell" style={{ textAlign: 'center' }}>
<span className="muted">暂无数据</span> <span className="muted">暂无数据</span>
</div>
</div> </div>
</div> )}
)} {resetConfirmOpen && (
{resetConfirmOpen && ( <ConfirmModal
<ConfirmModal title="重置列宽"
title="重置列宽" message="是否重置表格列宽为默认值?"
message="是否重置表格列宽为默认值?" icon={<ResetIcon width="20" height="20" className="shrink-0 text-[var(--primary)]" />}
icon={<ResetIcon width="20" height="20" className="shrink-0 text-[var(--primary)]" />} confirmVariant="primary"
confirmVariant="primary" onConfirm={handleResetSizing}
onConfirm={handleResetSizing} onCancel={() => setResetConfirmOpen(false)}
onCancel={() => setResetConfirmOpen(false)} confirmText="重置"
confirmText="重置" />
/> )}
{showPortalHeader && ReactDOM.createPortal(
<div
className="pc-fund-table pc-fund-table-portal-header"
ref={portalHeaderRef}
style={{
position: 'fixed',
top: effectiveStickyTop,
left: portalHorizontal.left,
right: portalHorizontal.right,
zIndex: 10,
overflowX: 'auto',
scrollbarWidth: 'none',
}}
>
<div
className="table-header-row table-header-row-scroll"
style={{ minWidth: totalHeaderWidth, width: 'fit-content' }}
>
{headerGroup?.headers.map((header) => {
const style = getCommonPinningStyles(header.column, true);
const isNameColumn =
header.column.id === 'fundName' ||
header.column.columnDef?.accessorKey === 'fundName';
const align = isNameColumn ? '' : 'text-center';
return (
<div
key={header.id}
className={`table-header-cell ${align}`}
style={style}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</div>
);
})}
</div>
</div>,
document.body
)}
</div>
{!!(cardDialogRow && getFundCardProps) && (
<FundDetailDialog blockDialogClose={blockDialogClose} cardDialogRow={cardDialogRow} getFundCardProps={getFundCardProps} setCardDialogRow={setCardDialogRow} />
)} )}
<PcTableSettingModal <PcTableSettingModal
open={settingModalOpen} open={settingModalOpen}
@@ -1272,74 +1320,36 @@ export default function PcFundTable({
showFullFundName={showFullFundName} showFullFundName={showFullFundName}
onToggleShowFullFundName={handleToggleShowFullFundName} onToggleShowFullFundName={handleToggleShowFullFundName}
/> />
<Dialog </>
open={!!(cardDialogRow && getFundCardProps)}
onOpenChange={(open) => {
if (!open && !blockDialogClose) setCardDialogRow(null);
}}
>
<DialogContent
className="sm:max-w-2xl max-h-[88vh] flex flex-col p-0 overflow-hidden"
onPointerDownOutside={blockDialogClose ? (e) => e.preventDefault() : undefined}
>
<DialogHeader className="flex-shrink-0 flex flex-row items-center justify-between gap-2 space-y-0 px-6 pb-4 pt-6 text-left border-b border-[var(--border)]">
<DialogTitle className="text-base font-semibold text-[var(--text)]">
基金详情
</DialogTitle>
</DialogHeader>
<div
className="flex-1 min-h-0 overflow-y-auto px-6 py-4 scrollbar-y-styled"
>
{cardDialogRow && getFundCardProps ? (
<FundCard {...getFundCardProps(cardDialogRow)} layoutMode="drawer" />
) : null}
</div>
</DialogContent>
</Dialog>
{showPortalHeader && ReactDOM.createPortal(
<div
className="pc-fund-table pc-fund-table-portal-header"
ref={portalHeaderRef}
style={{
position: 'fixed',
top: effectiveStickyTop,
left: portalHorizontal.left,
right: portalHorizontal.right,
zIndex: 10,
overflowX: 'auto',
scrollbarWidth: 'none',
}}
>
<div
className="table-header-row table-header-row-scroll"
style={{ minWidth: totalHeaderWidth, width: 'fit-content' }}
>
{headerGroup?.headers.map((header) => {
const style = getCommonPinningStyles(header.column, true);
const isNameColumn =
header.column.id === 'fundName' ||
header.column.columnDef?.accessorKey === 'fundName';
const align = isNameColumn ? '' : 'text-center';
return (
<div
key={header.id}
className={`table-header-cell ${align}`}
style={style}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</div>
);
})}
</div>
</div>,
document.body
)}
</div>
); );
} }
function FundDetailDialog({ blockDialogClose, cardDialogRow, getFundCardProps, setCardDialogRow}) {
return (
<Dialog
open
onOpenChange={(open) => {
if (!open && !blockDialogClose) setCardDialogRow(null);
}}
>
<DialogContent
className="sm:max-w-2xl max-h-[88vh] flex flex-col p-0 overflow-hidden"
onPointerDownOutside={blockDialogClose ? (e) => e.preventDefault() : undefined}
>
<DialogHeader className="flex-shrink-0 flex flex-row items-center justify-between gap-2 space-y-0 px-6 pb-4 pt-6 text-left border-b border-[var(--border)]">
<DialogTitle className="text-base font-semibold text-[var(--text)]">
基金详情
</DialogTitle>
</DialogHeader>
<div
className="flex-1 min-h-0 overflow-y-auto px-6 py-4 scrollbar-y-styled"
>
{cardDialogRow && getFundCardProps ? (
<FundCard {...getFundCardProps(cardDialogRow)} layoutMode="drawer" />
) : null}
</div>
</DialogContent>
</Dialog>
)
}

View File

@@ -15,9 +15,11 @@ function lockBodyScroll() {
originalBodyPosition = document.body.style.position || ""; originalBodyPosition = document.body.style.position || "";
originalBodyTop = document.body.style.top || ""; originalBodyTop = document.body.style.top || "";
document.body.style.position = "fixed"; requestAnimationFrame(() => {
document.body.style.top = `-${lockedScrollY}px`; document.body.style.top = `-${lockedScrollY}px`;
document.body.style.width = "100%"; document.body.style.width = "100%";
document.body.style.position = "fixed";
});
} }
} }
@@ -28,12 +30,15 @@ function unlockBodyScroll() {
// 只有全部弹框都关闭时才恢复滚动位置 // 只有全部弹框都关闭时才恢复滚动位置
if (scrollLockCount === 0) { if (scrollLockCount === 0) {
const scrollY = lockedScrollY;
document.body.style.position = originalBodyPosition; document.body.style.position = originalBodyPosition;
document.body.style.top = originalBodyTop; document.body.style.top = originalBodyTop;
document.body.style.width = ""; document.body.style.width = "";
// 恢复到锁定前的滚动位置,而不是跳到顶部 requestAnimationFrame(() => {
window.scrollTo(0, lockedScrollY); window.scrollTo(0, scrollY);
});
} }
} }
@@ -57,4 +62,4 @@ export function useBodyScrollLock(open) {
} }
}; };
}, [open]); }, [open]);
} }

View File

@@ -1,7 +1,6 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import { XIcon } from "lucide-react"
import { Dialog as DialogPrimitive } from "radix-ui" import { Dialog as DialogPrimitive } from "radix-ui"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
@@ -86,6 +85,8 @@ function DialogContent({
<DialogOverlay className={overlayClassName} style={overlayStyle} /> <DialogOverlay className={overlayClassName} style={overlayStyle} />
<DialogPrimitive.Content <DialogPrimitive.Content
data-slot="dialog-content" data-slot="dialog-content"
onOpenAutoFocus={(e) => e.preventDefault()}
onCloseAutoFocus={(e) => e.preventDefault()}
className={cn( className={cn(
"fixed top-[50%] left-[50%] z-50 w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-[16px] border border-[var(--border)] text-[var(--foreground)] p-6 dialog-content-shadow outline-none duration-200 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg", "fixed top-[50%] left-[50%] z-50 w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-[16px] border border-[var(--border)] text-[var(--foreground)] p-6 dialog-content-shadow outline-none duration-200 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg",
"mobile-dialog-glass", "mobile-dialog-glass",