feat:移动端表格宽度动态计算

This commit is contained in:
hzm
2026-03-01 19:07:14 +08:00
parent e7661e7b38
commit 4fcb076d99
3 changed files with 131 additions and 36 deletions

View File

@@ -27,12 +27,16 @@ import MobileSettingModal from './MobileSettingModal';
import { ExitIcon, SettingsIcon, StarIcon } from './Icons'; import { ExitIcon, SettingsIcon, StarIcon } from './Icons';
const MOBILE_NON_FROZEN_COLUMN_IDS = [ const MOBILE_NON_FROZEN_COLUMN_IDS = [
'latestNav',
'estimateNav',
'yesterdayChangePercent', 'yesterdayChangePercent',
'estimateChangePercent', 'estimateChangePercent',
'todayProfit', 'todayProfit',
'holdingProfit', 'holdingProfit',
]; ];
const MOBILE_COLUMN_HEADERS = { const MOBILE_COLUMN_HEADERS = {
latestNav: '最新净值',
estimateNav: '估算净值',
yesterdayChangePercent: '昨日涨跌幅', yesterdayChangePercent: '昨日涨跌幅',
estimateChangePercent: '估值涨跌幅', estimateChangePercent: '估值涨跌幅',
todayProfit: '当日收益', todayProfit: '当日收益',
@@ -214,6 +218,48 @@ export default function MobileFundTable({
return allVisible; return allVisible;
}); });
const [settingModalOpen, setSettingModalOpen] = useState(false); const [settingModalOpen, setSettingModalOpen] = useState(false);
const tableContainerRef = useRef(null);
const [tableContainerWidth, setTableContainerWidth] = useState(0);
useEffect(() => {
const el = tableContainerRef.current;
if (!el) return;
const updateWidth = () => setTableContainerWidth(el.clientWidth || 0);
updateWidth();
const ro = new ResizeObserver(updateWidth);
ro.observe(el);
return () => ro.disconnect();
}, []);
const NAME_CELL_WIDTH = 140;
const GAP = 12;
const LAST_COLUMN_EXTRA = 12;
const FALLBACK_WIDTHS = {
fundName: 140,
latestNav: 64,
estimateNav: 64,
yesterdayChangePercent: 72,
estimateChangePercent: 80,
todayProfit: 80,
holdingProfit: 80,
};
const columnWidthMap = useMemo(() => {
const visibleNonNameIds = mobileColumnOrder.filter((id) => mobileColumnVisibility[id] !== false);
const nonNameCount = visibleNonNameIds.length;
if (tableContainerWidth > 0 && nonNameCount > 0) {
const gapTotal = nonNameCount >= 3 ? 3 * GAP : (nonNameCount) * GAP;
const remaining = tableContainerWidth - NAME_CELL_WIDTH - gapTotal - LAST_COLUMN_EXTRA;
const divisor = nonNameCount >= 3 ? 3 : nonNameCount;
const otherColumnWidth = Math.max(48, Math.floor(remaining / divisor));
const map = { fundName: NAME_CELL_WIDTH };
MOBILE_NON_FROZEN_COLUMN_IDS.forEach((id) => {
map[id] = otherColumnWidth;
});
return map;
}
return { ...FALLBACK_WIDTHS };
}, [tableContainerWidth, mobileColumnOrder, mobileColumnVisibility]);
const handleResetMobileColumnOrder = () => { const handleResetMobileColumnOrder = () => {
const defaultOrder = [...MOBILE_NON_FROZEN_COLUMN_IDS]; const defaultOrder = [...MOBILE_NON_FROZEN_COLUMN_IDS];
@@ -356,7 +402,31 @@ export default function MobileFundTable({
</div> </div>
), ),
cell: (info) => <MobileFundNameCell info={info} />, cell: (info) => <MobileFundNameCell info={info} />,
meta: { align: 'left', cellClassName: 'name-cell', width: 140 }, meta: { align: 'left', cellClassName: 'name-cell', width: columnWidthMap.fundName },
},
{
accessorKey: 'latestNav',
header: '最新净值',
cell: (info) => (
<span style={{ display: 'block', width: '100%', fontWeight: 700 }}>
<FitText maxFontSize={14} minFontSize={10}>
{info.getValue() ?? '—'}
</FitText>
</span>
),
meta: { align: 'right', cellClassName: 'value-cell', width: columnWidthMap.latestNav },
},
{
accessorKey: 'estimateNav',
header: '估算净值',
cell: (info) => (
<span style={{ display: 'block', width: '100%', fontWeight: 700 }}>
<FitText maxFontSize={14} minFontSize={10}>
{info.getValue() ?? '—'}
</FitText>
</span>
),
meta: { align: 'right', cellClassName: 'value-cell', width: columnWidthMap.estimateNav },
}, },
{ {
accessorKey: 'yesterdayChangePercent', accessorKey: 'yesterdayChangePercent',
@@ -375,7 +445,7 @@ export default function MobileFundTable({
</div> </div>
); );
}, },
meta: { align: 'right', cellClassName: 'change-cell', width: 72 }, meta: { align: 'right', cellClassName: 'change-cell', width: columnWidthMap.yesterdayChangePercent },
}, },
{ {
accessorKey: 'estimateChangePercent', accessorKey: 'estimateChangePercent',
@@ -395,7 +465,7 @@ export default function MobileFundTable({
</div> </div>
); );
}, },
meta: { align: 'right', cellClassName: 'est-change-cell', width: 80 }, meta: { align: 'right', cellClassName: 'est-change-cell', width: columnWidthMap.estimateChangePercent },
}, },
{ {
accessorKey: 'todayProfit', accessorKey: 'todayProfit',
@@ -413,7 +483,7 @@ export default function MobileFundTable({
</span> </span>
); );
}, },
meta: { align: 'right', cellClassName: 'profit-cell', width: 80 }, meta: { align: 'right', cellClassName: 'profit-cell', width: columnWidthMap.todayProfit },
}, },
{ {
accessorKey: 'holdingProfit', accessorKey: 'holdingProfit',
@@ -441,10 +511,10 @@ export default function MobileFundTable({
</div> </div>
); );
}, },
meta: { align: 'right', cellClassName: 'holding-cell', width: 80 }, meta: { align: 'right', cellClassName: 'holding-cell', width: columnWidthMap.holdingProfit },
}, },
], ],
[currentTab, favorites, refreshing] [currentTab, favorites, refreshing, columnWidthMap]
); );
const table = useReactTable({ const table = useReactTable({
@@ -482,7 +552,6 @@ export default function MobileFundTable({
const headerGroup = table.getHeaderGroups()[0]; const headerGroup = table.getHeaderGroups()[0];
const LAST_COLUMN_EXTRA = 12;
const mobileGridLayout = (() => { const mobileGridLayout = (() => {
if (!headerGroup?.headers?.length) return { gridTemplateColumns: '', minWidth: undefined }; if (!headerGroup?.headers?.length) return { gridTemplateColumns: '', minWidth: undefined };
const gap = 12; const gap = 12;
@@ -501,12 +570,12 @@ export default function MobileFundTable({
const getAlignClass = (columnId) => { const getAlignClass = (columnId) => {
if (columnId === 'fundName') return ''; if (columnId === 'fundName') return '';
if (['yesterdayChangePercent', 'estimateChangePercent', 'todayProfit', 'holdingProfit'].includes(columnId)) return 'text-right'; if (['latestNav', 'estimateNav', 'yesterdayChangePercent', 'estimateChangePercent', 'todayProfit', 'holdingProfit'].includes(columnId)) return 'text-right';
return 'text-right'; return 'text-right';
}; };
return ( return (
<div className="mobile-fund-table"> <div className="mobile-fund-table" ref={tableContainerRef}>
<div <div
className="mobile-fund-table-scroll" className="mobile-fund-table-scroll"
style={mobileGridLayout.minWidth != null ? { minWidth: mobileGridLayout.minWidth } : undefined} style={mobileGridLayout.minWidth != null ? { minWidth: mobileGridLayout.minWidth } : undefined}

View File

@@ -23,11 +23,13 @@ import {
} from '@dnd-kit/sortable'; } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities'; import { CSS } from '@dnd-kit/utilities';
import ConfirmModal from './ConfirmModal'; import ConfirmModal from './ConfirmModal';
import FitText from './FitText';
import PcTableSettingModal from './PcTableSettingModal'; import PcTableSettingModal from './PcTableSettingModal';
import { DragIcon, ExitIcon, SettingsIcon, StarIcon, TrashIcon } from './Icons'; import { DragIcon, ExitIcon, SettingsIcon, StarIcon, TrashIcon } from './Icons';
const NON_FROZEN_COLUMN_IDS = [ const NON_FROZEN_COLUMN_IDS = [
'navOrEstimate', 'latestNav',
'estimateNav',
'yesterdayChangePercent', 'yesterdayChangePercent',
'estimateChangePercent', 'estimateChangePercent',
'holdingAmount', 'holdingAmount',
@@ -35,7 +37,8 @@ const NON_FROZEN_COLUMN_IDS = [
'holdingProfit', 'holdingProfit',
]; ];
const COLUMN_HEADERS = { const COLUMN_HEADERS = {
navOrEstimate: '净值/估值', latestNav: '最新净值',
estimateNav: '估算净值',
yesterdayChangePercent: '昨日涨跌幅', yesterdayChangePercent: '昨日涨跌幅',
estimateChangePercent: '估值涨跌幅', estimateChangePercent: '估值涨跌幅',
holdingAmount: '持仓金额', holdingAmount: '持仓金额',
@@ -98,7 +101,8 @@ function SortableRow({ row, children, isTableDragging, disabled }) {
* { * {
* fundName: string; // 基金名称 * fundName: string; // 基金名称
* code?: string; // 基金代码(可选,只用于展示在名称下方) * code?: string; // 基金代码(可选,只用于展示在名称下方)
* navOrEstimate: string|number; // 净值/估 * latestNav: string|number; // 最新净
* estimateNav: string|number; // 估算净值
* yesterdayChangePercent: string|number; // 昨日涨跌幅 * yesterdayChangePercent: string|number; // 昨日涨跌幅
* estimateChangePercent: string|number; // 估值涨跌幅 * estimateChangePercent: string|number; // 估值涨跌幅
* holdingAmount: string|number; // 持仓金额 * holdingAmount: string|number; // 持仓金额
@@ -396,12 +400,29 @@ export default function PcFundTable({
}, },
}, },
{ {
accessorKey: 'navOrEstimate', accessorKey: 'latestNav',
header: '净值/估值', header: '最新净值',
size: 100, size: 100,
minSize: 80, minSize: 80,
cell: (info) => ( cell: (info) => (
<span style={{ fontWeight: 700 }}>{info.getValue() ?? '—'}</span> <FitText style={{ fontWeight: 700 }} maxFontSize={14} minFontSize={10}>
{info.getValue() ?? '—'}
</FitText>
),
meta: {
align: 'right',
cellClassName: 'value-cell',
},
},
{
accessorKey: 'estimateNav',
header: '估算净值',
size: 100,
minSize: 80,
cell: (info) => (
<FitText style={{ fontWeight: 700 }} maxFontSize={14} minFontSize={10}>
{info.getValue() ?? '—'}
</FitText>
), ),
meta: { meta: {
align: 'right', align: 'right',
@@ -420,9 +441,9 @@ export default function PcFundTable({
const cls = value > 0 ? 'up' : value < 0 ? 'down' : ''; const cls = value > 0 ? 'up' : value < 0 ? 'down' : '';
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 0 }}> <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 0 }}>
<span className={cls} style={{ fontWeight: 700 }}> <FitText className={cls} style={{ fontWeight: 700 }} maxFontSize={14} minFontSize={10} as="div">
{info.getValue() ?? '—'} {info.getValue() ?? '—'}
</span> </FitText>
<span className="muted" style={{ fontSize: '11px' }}> <span className="muted" style={{ fontSize: '11px' }}>
{date} {date}
</span> </span>
@@ -447,9 +468,9 @@ export default function PcFundTable({
const cls = isMuted ? 'muted' : value > 0 ? 'up' : value < 0 ? 'down' : ''; const cls = isMuted ? 'muted' : value > 0 ? 'up' : value < 0 ? 'down' : '';
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 0 }}> <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 0 }}>
<span className={cls} style={{ fontWeight: 700 }}> <FitText className={cls} style={{ fontWeight: 700 }} maxFontSize={14} minFontSize={10} as="div">
{info.getValue() ?? '—'} {info.getValue() ?? '—'}
</span> </FitText>
<span className="muted" style={{ fontSize: '11px' }}> <span className="muted" style={{ fontSize: '11px' }}>
{time} {time}
</span> </span>
@@ -494,13 +515,17 @@ export default function PcFundTable({
return ( return (
<div <div
title="点击设置持仓" title="点击设置持仓"
style={{ display: 'inline-flex', alignItems: 'center', cursor: 'pointer' }} style={{ display: 'flex', alignItems: 'center', cursor: 'pointer', width: '100%', minWidth: 0 }}
onClick={(e) => { onClick={(e) => {
e.stopPropagation?.(); e.stopPropagation?.();
onHoldingAmountClickRef.current?.(original, { hasHolding: true }); onHoldingAmountClickRef.current?.(original, { hasHolding: true });
}} }}
> >
<span style={{ fontWeight: 700, marginRight: 6 }}>{info.getValue() ?? '—'}</span> <div style={{ flex: '1 1 0', minWidth: 0 }}>
<FitText style={{ fontWeight: 700 }} maxFontSize={14} minFontSize={10}>
{info.getValue() ?? '—'}
</FitText>
</div>
<button <button
className="icon-button no-hover" className="icon-button no-hover"
onClick={(e) => { onClick={(e) => {
@@ -508,7 +533,7 @@ export default function PcFundTable({
onHoldingAmountClickRef.current?.(original, { hasHolding: true }); onHoldingAmountClickRef.current?.(original, { hasHolding: true });
}} }}
title="编辑持仓" title="编辑持仓"
style={{ border: 'none', width: '28px', height: '28px', marginLeft: -6, backgroundColor: 'transparent' }} style={{ border: 'none', width: '28px', height: '28px', marginLeft: 4, flexShrink: 0, backgroundColor: 'transparent' }}
> >
<SettingsIcon width="14" height="14" /> <SettingsIcon width="14" height="14" />
</button> </button>
@@ -531,9 +556,9 @@ export default function PcFundTable({
const hasProfit = value != null; const hasProfit = value != null;
const cls = hasProfit ? (value > 0 ? 'up' : value < 0 ? 'down' : '') : 'muted'; const cls = hasProfit ? (value > 0 ? 'up' : value < 0 ? 'down' : '') : 'muted';
return ( return (
<span className={cls} style={{ fontWeight: 700 }}> <FitText className={cls} style={{ fontWeight: 700 }} maxFontSize={14} minFontSize={10}>
{hasProfit ? (info.getValue() ?? '') : ''} {hasProfit ? (info.getValue() ?? '') : ''}
</span> </FitText>
); );
}, },
meta: { meta: {
@@ -554,16 +579,16 @@ export default function PcFundTable({
return ( return (
<div <div
title="点击切换金额/百分比" title="点击切换金额/百分比"
style={{ cursor: hasTotal ? 'pointer' : 'default' }} style={{ cursor: hasTotal ? 'pointer' : 'default', width: '100%' }}
onClick={(e) => { onClick={(e) => {
if (!hasTotal) return; if (!hasTotal) return;
e.stopPropagation?.(); e.stopPropagation?.();
onHoldingProfitClickRef.current?.(original); onHoldingProfitClickRef.current?.(original);
}} }}
> >
<span className={cls} style={{ fontWeight: 700 }}> <FitText className={cls} style={{ fontWeight: 700 }} maxFontSize={14} minFontSize={10}>
{hasTotal ? (info.getValue() ?? '') : ''} {hasTotal ? (info.getValue() ?? '') : ''}
</span> </FitText>
</div> </div>
); );
}, },
@@ -836,6 +861,8 @@ export default function PcFundTable({
const columnId = cell.column.id || cell.column.columnDef?.accessorKey; const columnId = cell.column.id || cell.column.columnDef?.accessorKey;
const isNameColumn = columnId === 'fundName'; const isNameColumn = columnId === 'fundName';
const rightAlignedColumns = new Set([ const rightAlignedColumns = new Set([
'latestNav',
'estimateNav',
'yesterdayChangePercent', 'yesterdayChangePercent',
'estimateChangePercent', 'estimateChangePercent',
'holdingAmount', 'holdingAmount',

View File

@@ -728,14 +728,12 @@ export default function HomePage() {
() => () =>
displayFunds.map((f) => { displayFunds.map((f) => {
const hasTodayData = f.jzrq === todayStr; const hasTodayData = f.jzrq === todayStr;
const shouldHideChange = isTradingDay && !hasTodayData; const latestNav = f.dwjz != null && f.dwjz !== '' ? (typeof f.dwjz === 'number' ? Number(f.dwjz).toFixed(4) : String(f.dwjz)) : '—';
const navOrEstimate = !shouldHideChange const estimateNav = f.noValuation
? (f.dwjz ?? '—') ? '—'
: (f.noValuation : (f.estPricedCoverage > 0.05
? (f.dwjz ?? '—') ? (f.estGsz != null ? Number(f.estGsz).toFixed(4) : '—')
: (f.estPricedCoverage > 0.05 : (f.gsz != null ? (typeof f.gsz === 'number' ? Number(f.gsz).toFixed(4) : String(f.gsz)) : '—'));
? (f.estGsz != null ? Number(f.estGsz).toFixed(4) : '—')
: (f.gsz ?? '—')));
const yesterdayChangePercent = const yesterdayChangePercent =
f.zzl != null && f.zzl !== '' f.zzl != null && f.zzl !== ''
@@ -794,7 +792,8 @@ export default function HomePage() {
code: f.code, code: f.code,
fundName: f.name, fundName: f.name,
isUpdated: f.jzrq === todayStr, isUpdated: f.jzrq === todayStr,
navOrEstimate, latestNav,
estimateNav,
yesterdayChangePercent, yesterdayChangePercent,
yesterdayChangeValue, yesterdayChangeValue,
yesterdayDate, yesterdayDate,