From 152059b199936f2dc0d418174714764f03ae932f Mon Sep 17 00:00:00 2001 From: hzm <934585316@qq.com> Date: Sun, 8 Mar 2026 22:39:31 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9APC=E7=AB=AF=E8=A1=A8=E5=A4=B4?= =?UTF-8?q?=E5=9B=BA=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/PcFundTable.jsx | 207 ++++++++++++++++++++++++++++----- app/globals.css | 10 ++ app/page.jsx | 1 + 3 files changed, 186 insertions(+), 32 deletions(-) diff --git a/app/components/PcFundTable.jsx b/app/components/PcFundTable.jsx index 57fce32..3f3b826 100644 --- a/app/components/PcFundTable.jsx +++ b/app/components/PcFundTable.jsx @@ -1,6 +1,8 @@ 'use client'; +import ReactDOM from 'react-dom'; import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react'; +import { throttle } from 'lodash'; import { AnimatePresence, motion } from 'framer-motion'; import { flexRender, @@ -128,6 +130,7 @@ function SortableRow({ row, children, isTableDragging, disabled }) { * @param {(row: any) => Object} [props.getFundCardProps] - 给定行返回 FundCard 的 props;传入后点击基金名称将用弹框展示卡片详情 * @param {React.MutableRefObject<(() => void) | null>} [props.closeDialogRef] - 注入关闭弹框的方法,用于确认删除时关闭 * @param {boolean} [props.blockDialogClose] - 为 true 时阻止点击遮罩关闭弹框(如删除确认弹框打开时) + * @param {number} [props.stickyTop] - 表头固定时的 top 偏移(与 MobileFundTable 一致,用于适配导航栏、筛选栏等) */ export default function PcFundTable({ data = [], @@ -145,6 +148,7 @@ export default function PcFundTable({ getFundCardProps, closeDialogRef, blockDialogClose = false, + stickyTop = 0, }) { const sensors = useSensors( useSensor(PointerSensor, { @@ -157,6 +161,11 @@ export default function PcFundTable({ const [activeId, setActiveId] = useState(null); const [cardDialogRow, setCardDialogRow] = useState(null); + const tableContainerRef = useRef(null); + const portalHeaderRef = useRef(null); + const [showPortalHeader, setShowPortalHeader] = useState(false); + const [effectiveStickyTop, setEffectiveStickyTop] = useState(stickyTop); + const [portalHorizontal, setPortalHorizontal] = useState({ left: 0, right: 0 }); const handleDragStart = (event) => { setActiveId(event.active.id); @@ -374,6 +383,87 @@ export default function PcFundTable({ onHoldingAmountClick, ]); + useEffect(() => { + if (typeof window === 'undefined') return; + const getEffectiveStickyTop = () => { + const stickySummaryCard = document.querySelector('.group-summary-sticky .group-summary-card'); + if (!stickySummaryCard) return stickyTop; + + const stickySummaryWrapper = stickySummaryCard.closest('.group-summary-sticky'); + if (!stickySummaryWrapper) return stickyTop; + + const wrapperRect = stickySummaryWrapper.getBoundingClientRect(); + const isSummaryStuck = wrapperRect.top <= stickyTop + 1; + + return isSummaryStuck ? stickyTop + stickySummaryWrapper.offsetHeight : stickyTop; + }; + + const updateVerticalState = () => { + const nextStickyTop = getEffectiveStickyTop(); + setEffectiveStickyTop((prev) => (prev === nextStickyTop ? prev : nextStickyTop)); + + const tableEl = tableContainerRef.current; + const scrollEl = tableEl?.closest('.table-scroll-area'); + const targetEl = scrollEl || tableEl; + const rect = targetEl?.getBoundingClientRect(); + + if (!rect) { + setShowPortalHeader(window.scrollY >= nextStickyTop); + return; + } + + setShowPortalHeader(rect.top <= nextStickyTop); + + setPortalHorizontal((prev) => { + const next = { + left: rect.left, + right: typeof window !== 'undefined' ? Math.max(0, window.innerWidth - rect.right) : 0, + }; + if (prev.left === next.left && prev.right === next.right) return prev; + return next; + }); + }; + + const throttledVerticalUpdate = throttle(updateVerticalState, 1000 / 60, { leading: true, trailing: true }); + + updateVerticalState(); + window.addEventListener('scroll', throttledVerticalUpdate, { passive: true }); + window.addEventListener('resize', throttledVerticalUpdate, { passive: true }); + return () => { + window.removeEventListener('scroll', throttledVerticalUpdate); + window.removeEventListener('resize', throttledVerticalUpdate); + throttledVerticalUpdate.cancel(); + }; + }, [stickyTop]); + + useEffect(() => { + const tableEl = tableContainerRef.current; + const portalEl = portalHeaderRef.current; + const scrollEl = tableEl?.closest('.table-scroll-area'); + if (!scrollEl || !portalEl) return; + + const syncScrollToPortal = () => { + portalEl.scrollLeft = scrollEl.scrollLeft; + }; + + const syncScrollToTable = () => { + scrollEl.scrollLeft = portalEl.scrollLeft; + }; + + syncScrollToPortal(); + + const handleTableScroll = () => syncScrollToPortal(); + const handlePortalScroll = () => syncScrollToTable(); + + scrollEl.addEventListener('scroll', handleTableScroll, { passive: true }); + portalEl.addEventListener('scroll', handlePortalScroll, { passive: true }); + + return () => { + scrollEl.removeEventListener('scroll', handleTableScroll); + portalEl.removeEventListener('scroll', handlePortalScroll); + }; + }, [showPortalHeader]); + const FundNameCell = ({ info, showFullFundName, onOpenCardDialog }) => { const original = info.row.original || {}; const code = original.code; @@ -834,8 +924,47 @@ export default function PcFundTable({ }; }; + const renderTableHeader = (forPortal = false) => { + if (!headerGroup) return null; + return ( +