feat:移动端表头吸附效果
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import {
|
||||
flexRender,
|
||||
@@ -275,8 +276,10 @@ export default function MobileFundTable({
|
||||
|
||||
const [settingModalOpen, setSettingModalOpen] = useState(false);
|
||||
const tableContainerRef = useRef(null);
|
||||
const portalHeaderRef = useRef(null);
|
||||
const [tableContainerWidth, setTableContainerWidth] = useState(0);
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const [showPortalHeader, setShowPortalHeader] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const el = tableContainerRef.current;
|
||||
@@ -289,16 +292,63 @@ export default function MobileFundTable({
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const el = tableContainerRef.current;
|
||||
if (!el) return;
|
||||
const handleScroll = () => {
|
||||
setIsScrolled(el.scrollLeft > 0);
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const handleVerticalScroll = () => {
|
||||
console.log('scrollY', window.scrollY);
|
||||
setShowPortalHeader(window.scrollY >= 100);
|
||||
};
|
||||
handleScroll();
|
||||
el.addEventListener('scroll', handleScroll, { passive: true });
|
||||
return () => el.removeEventListener('scroll', handleScroll);
|
||||
|
||||
handleVerticalScroll();
|
||||
window.addEventListener('scroll', handleVerticalScroll, { passive: true });
|
||||
return () => window.removeEventListener('scroll', handleVerticalScroll);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const tableEl = tableContainerRef.current;
|
||||
if (!tableEl) return;
|
||||
|
||||
const handleScroll = () => {
|
||||
setIsScrolled(tableEl.scrollLeft > 0);
|
||||
};
|
||||
|
||||
handleScroll();
|
||||
tableEl.addEventListener('scroll', handleScroll, { passive: true });
|
||||
|
||||
return () => {
|
||||
tableEl.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const tableEl = tableContainerRef.current;
|
||||
const portalEl = portalHeaderRef.current;
|
||||
console.log('portalEl', portalEl)
|
||||
if (!tableEl || !portalEl) return;
|
||||
|
||||
const syncScrollToPortal = () => {
|
||||
console.log('tableEl.scrollLeft', tableEl.scrollLeft)
|
||||
portalEl.scrollLeft = tableEl.scrollLeft;
|
||||
};
|
||||
|
||||
const syncScrollToTable = () => {
|
||||
tableEl.scrollLeft = portalEl.scrollLeft;
|
||||
};
|
||||
|
||||
syncScrollToPortal();
|
||||
|
||||
const handleTableScroll = () => syncScrollToPortal();
|
||||
const handlePortalScroll = () => syncScrollToTable();
|
||||
|
||||
tableEl.addEventListener('scroll', handleTableScroll, { passive: true });
|
||||
// portalEl.addEventListener('scroll', handlePortalScroll, { passive: true });
|
||||
|
||||
return () => {
|
||||
tableEl.removeEventListener('scroll', handleTableScroll);
|
||||
// portalEl.removeEventListener('scroll', handlePortalScroll);
|
||||
};
|
||||
}, [showPortalHeader]);
|
||||
|
||||
const NAME_CELL_WIDTH = 140;
|
||||
const GAP = 12;
|
||||
const LAST_COLUMN_EXTRA = 12;
|
||||
@@ -742,13 +792,9 @@ export default function MobileFundTable({
|
||||
return 'text-right';
|
||||
};
|
||||
|
||||
const renderTableHeader = ()=>{
|
||||
if(!headerGroup) return null;
|
||||
return (
|
||||
<div className="mobile-fund-table" ref={tableContainerRef}>
|
||||
<div
|
||||
className="mobile-fund-table-scroll"
|
||||
style={mobileGridLayout.minWidth != null ? { minWidth: mobileGridLayout.minWidth } : undefined}
|
||||
>
|
||||
{headerGroup && (
|
||||
<div
|
||||
className="table-header-row mobile-fund-table-header"
|
||||
style={mobileGridLayout.gridTemplateColumns ? { gridTemplateColumns: mobileGridLayout.gridTemplateColumns } : undefined}
|
||||
@@ -771,8 +817,32 @@ export default function MobileFundTable({
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
}
|
||||
|
||||
const renderContent = (onlyShowHeader) => {
|
||||
if (onlyShowHeader) {
|
||||
return (
|
||||
<div style={{position: 'fixed', top: 175}} className="mobile-fund-table mobile-fund-table-portal-header" ref={portalHeaderRef}>
|
||||
<div
|
||||
className="mobile-fund-table-scroll"
|
||||
style={mobileGridLayout.minWidth != null ? { minWidth: mobileGridLayout.minWidth } : undefined}
|
||||
>
|
||||
{renderTableHeader()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mobile-fund-table" ref={tableContainerRef}>
|
||||
<div
|
||||
className="mobile-fund-table-scroll"
|
||||
style={mobileGridLayout.minWidth != null ? { minWidth: mobileGridLayout.minWidth } : undefined}
|
||||
>
|
||||
{renderTableHeader()}
|
||||
|
||||
{!onlyShowHeader && (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
@@ -828,9 +898,11 @@ export default function MobileFundTable({
|
||||
</AnimatePresence>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
|
||||
)}
|
||||
</div>
|
||||
|
||||
{table.getRowModel().rows.length === 0 && (
|
||||
{table.getRowModel().rows.length === 0 && !onlyShowHeader && (
|
||||
<div className="table-row empty-row">
|
||||
<div className="table-cell" style={{ textAlign: 'center' }}>
|
||||
<span className="muted">暂无数据</span>
|
||||
@@ -838,6 +910,7 @@ export default function MobileFundTable({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!onlyShowHeader && (
|
||||
<MobileSettingModal
|
||||
open={settingModalOpen}
|
||||
onClose={() => setSettingModalOpen(false)}
|
||||
@@ -852,6 +925,16 @@ export default function MobileFundTable({
|
||||
showFullFundName={showFullFundName}
|
||||
onToggleShowFullFundName={handleToggleShowFullFundName}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!onlyShowHeader && showPortalHeader && ReactDOM.createPortal(renderContent(true), document.body)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderContent()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1535,6 +1535,22 @@ input[type="number"] {
|
||||
/* min-width 由 MobileFundTable 根据 columns meta.width 动态设置 */
|
||||
}
|
||||
|
||||
.mobile-fund-table-portal-header {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.mobile-fund-table-portal-header::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.mobile-fund-table-portal-header .mobile-fund-table-scroll {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mobile-fund-table .table-header-row {
|
||||
display: grid;
|
||||
/* grid-template-columns 由 MobileFundTable 根据当前列顺序动态设置 */
|
||||
|
||||
@@ -427,6 +427,7 @@ export default function HomePage() {
|
||||
// 动态计算 Navbar 和 FilterBar 高度
|
||||
const navbarRef = useRef(null);
|
||||
const filterBarRef = useRef(null);
|
||||
const containerRef = useRef(null);
|
||||
const [navbarHeight, setNavbarHeight] = useState(0);
|
||||
const [filterBarHeight, setFilterBarHeight] = useState(0);
|
||||
// 主题初始固定为 dark,避免 SSR 与客户端首屏不一致导致 hydration 报错;真实偏好由 useLayoutEffect 在首帧前恢复
|
||||
@@ -3598,7 +3599,7 @@ export default function HomePage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container content" style={{ width: containerWidth }}>
|
||||
<div ref={containerRef} className="container content" style={{ width: containerWidth }}>
|
||||
<AnimatePresence>
|
||||
{showThemeTransition && (
|
||||
<motion.div
|
||||
|
||||
Reference in New Issue
Block a user