'use client'; import { useEffect, useMemo, useRef, useState } from 'react'; import { AnimatePresence, motion } from 'framer-motion'; import { flexRender, getCoreRowModel, useReactTable, } from '@tanstack/react-table'; import { ExitIcon, SettingsIcon, StarIcon, TrashIcon } from './Icons'; /** * PC 端基金列表表格组件(基于 @tanstack/react-table) * * @param {Object} props * @param {Array} props.data - 表格数据 * 每一行推荐结构(字段命名与 page.jsx 中的数据一致): * { * fundName: string; // 基金名称 * code?: string; // 基金代码(可选,只用于展示在名称下方) * navOrEstimate: string|number; // 净值/估值 * yesterdayChangePercent: string|number; // 昨日涨跌幅 * estimateChangePercent: string|number; // 估值涨跌幅 * holdingAmount: string|number; // 持仓金额 * todayProfit: string|number; // 当日收益 * holdingProfit: string|number; // 持有收益 * } * @param {(row: any) => void} [props.onRemoveFund] - 删除基金的回调 * @param {string} [props.currentTab] - 当前分组 * @param {Set} [props.favorites] - 自选集合 * @param {(row: any) => void} [props.onToggleFavorite] - 添加/取消自选 * @param {(row: any) => void} [props.onRemoveFromGroup] - 从当前分组移除 * @param {(row: any, meta: { hasHolding: boolean }) => void} [props.onHoldingAmountClick] - 点击持仓金额 * @param {(row: any) => void} [props.onHoldingProfitClick] - 点击持有收益 * @param {boolean} [props.refreshing] - 是否处于刷新状态(控制删除按钮禁用态) */ export default function PcFundTable({ data = [], onRemoveFund, currentTab, favorites = new Set(), onToggleFavorite, onRemoveFromGroup, onHoldingAmountClick, onHoldingProfitClick, refreshing = false, }) { const getStoredColumnSizing = () => { if (typeof window === 'undefined') return {}; try { const raw = window.localStorage.getItem('customSettings'); if (!raw) return {}; const parsed = JSON.parse(raw); const sizing = parsed?.pcTableColumns; if (!sizing || typeof sizing !== 'object') return {}; return Object.fromEntries( Object.entries(sizing).filter(([, value]) => Number.isFinite(value)), ); } catch { return {}; } }; const persistColumnSizing = (nextSizing) => { if (typeof window === 'undefined') return; try { const raw = window.localStorage.getItem('customSettings'); const parsed = raw ? JSON.parse(raw) : {}; const nextSettings = parsed && typeof parsed === 'object' ? { ...parsed, pcTableColumns: nextSizing } : { pcTableColumns: nextSizing }; window.localStorage.setItem('customSettings', JSON.stringify(nextSettings)); } catch { } }; const [columnSizing, setColumnSizing] = useState(() => { const stored = getStoredColumnSizing(); if (stored.actions) { const { actions, ...rest } = stored; return rest; } return stored; }); const onRemoveFundRef = useRef(onRemoveFund); const onToggleFavoriteRef = useRef(onToggleFavorite); const onRemoveFromGroupRef = useRef(onRemoveFromGroup); const onHoldingAmountClickRef = useRef(onHoldingAmountClick); const onHoldingProfitClickRef = useRef(onHoldingProfitClick); useEffect(() => { onRemoveFundRef.current = onRemoveFund; onToggleFavoriteRef.current = onToggleFavorite; onRemoveFromGroupRef.current = onRemoveFromGroup; onHoldingAmountClickRef.current = onHoldingAmountClick; onHoldingProfitClickRef.current = onHoldingProfitClick; }, [ onRemoveFund, onToggleFavorite, onRemoveFromGroup, onHoldingAmountClick, onHoldingProfitClick, ]); const columns = useMemo( () => [ { accessorKey: 'fundName', header: '基金名称', size: 240, minSize: 100, enablePinning: true, cell: (info) => { const original = info.row.original || {}; const code = original.code; const isUpdated = original.isUpdated; const isFavorites = favorites?.has?.(code); const isGroupTab = currentTab && currentTab !== 'all' && currentTab !== 'fav'; return (
{isGroupTab ? ( ) : ( )}
{info.getValue() ?? '—'} {code ? #{code} : null}
); }, meta: { align: 'left', cellClassName: 'name-cell', }, }, { accessorKey: 'navOrEstimate', header: '净值/估值', size: 100, minSize: 80, cell: (info) => ( {info.getValue() ?? '—'} ), meta: { align: 'right', cellClassName: 'value-cell', }, }, { accessorKey: 'yesterdayChangePercent', header: '昨日涨跌幅', cell: (info) => { const original = info.row.original || {}; const value = original.yesterdayChangeValue; const date = original.yesterdayDate ?? '-'; const cls = value > 0 ? 'up' : value < 0 ? 'down' : ''; return (
{info.getValue() ?? '—'} {date}
); }, meta: { align: 'right', cellClassName: 'change-cell', }, }, { accessorKey: 'estimateChangePercent', header: '估值涨跌幅', size: 100, minSize: 80, cell: (info) => { const original = info.row.original || {}; const value = original.estimateChangeValue; const isMuted = original.estimateChangeMuted; const time = original.estimateTime ?? '-'; const cls = isMuted ? 'muted' : value > 0 ? 'up' : value < 0 ? 'down' : ''; return (
{info.getValue() ?? '—'} {time}
); }, meta: { align: 'right', cellClassName: 'est-change-cell', }, }, { accessorKey: 'holdingAmount', header: '持仓金额', cell: (info) => { const original = info.row.original || {}; if (original.holdingAmountValue == null) { return (
{ e.stopPropagation?.(); onHoldingAmountClickRef.current?.(original, { hasHolding: false }); }} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onHoldingAmountClickRef.current?.(original, { hasHolding: false }); } }} > 未设置
); } return (
{ e.stopPropagation?.(); onHoldingAmountClickRef.current?.(original, { hasHolding: true }); }} > {info.getValue() ?? '—'}
); }, meta: { align: 'right', cellClassName: 'holding-amount-cell', }, }, { accessorKey: 'todayProfit', header: '当日收益', cell: (info) => { const original = info.row.original || {}; const value = original.todayProfitValue; const hasProfit = value != null; const cls = hasProfit ? (value > 0 ? 'up' : value < 0 ? 'down' : '') : 'muted'; return ( {hasProfit ? (info.getValue() ?? '') : ''} ); }, meta: { align: 'right', cellClassName: 'profit-cell', }, }, { accessorKey: 'holdingProfit', header: '持有收益', size: 140, minSize: 100, cell: (info) => { const original = info.row.original || {}; const value = original.holdingProfitValue; const hasTotal = value != null; const cls = hasTotal ? (value > 0 ? 'up' : value < 0 ? 'down' : '') : 'muted'; return (
{ if (!hasTotal) return; e.stopPropagation?.(); onHoldingProfitClickRef.current?.(original); }} > {hasTotal ? (info.getValue() ?? '') : ''}
); }, meta: { align: 'right', cellClassName: 'holding-cell', }, }, { id: 'actions', header: '操作', size: 80, minSize: 80, maxSize: 80, enableResizing: false, enablePinning: true, meta: { align: 'center', isAction: true, cellClassName: 'action-cell', }, cell: (info) => { const original = info.row.original || {}; const handleClick = (e) => { e.stopPropagation?.(); if (refreshing) return; onRemoveFundRef.current?.(original); }; return (
); }, }, ], [currentTab, favorites, refreshing], ); const table = useReactTable({ data, columns, enableColumnPinning: true, enableColumnResizing: true, columnResizeMode: 'onChange', onColumnSizingChange: (updater) => { setColumnSizing((prev) => { const next = typeof updater === 'function' ? updater(prev) : updater; const { actions, ...rest } = next || {}; persistColumnSizing(rest || {}); return rest || {}; }); }, state: { columnSizing, }, initialState: { columnPinning: { left: ['fundName'], right: ['actions'], }, }, getCoreRowModel: getCoreRowModel(), defaultColumn: { cell: (info) => info.getValue() ?? '—', }, }); const headerGroup = table.getHeaderGroups()[0]; const getCommonPinningStyles = (column, isHeader) => { const isPinned = column.getIsPinned(); const isNameColumn = column.id === 'fundName' || column.columnDef?.accessorKey === 'fundName'; const style = { width: `${column.getSize()}px`, }; if (!isPinned) return style; const isLeft = isPinned === 'left'; const isRight = isPinned === 'right'; return { ...style, position: 'sticky', left: isLeft ? `${column.getStart('left')}px` : undefined, right: isRight ? `${column.getAfter('right')}px` : undefined, zIndex: isHeader ? 11 : 10, backgroundColor: isHeader ? '#2a394b' : 'var(--row-bg)', boxShadow: 'none', textAlign: isNameColumn ? 'left' : 'center', justifyContent: isNameColumn ? 'flex-start' : 'center', }; }; return ( <> {/* 表头 */} {headerGroup && (
{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 (
{header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext(), )}
); })}
)} {/* 表体 */} {table.getRowModel().rows.map((row) => (
{row.getVisibleCells().map((cell) => { const columnId = cell.column.id || cell.column.columnDef?.accessorKey; const isNameColumn = columnId === 'fundName'; const rightAlignedColumns = new Set([ 'yesterdayChangePercent', 'estimateChangePercent', 'holdingAmount', 'todayProfit', 'holdingProfit', ]); const align = isNameColumn ? '' : rightAlignedColumns.has(columnId) ? 'text-right' : 'text-center'; const cellClassName = (cell.column.columnDef.meta && cell.column.columnDef.meta.cellClassName) || ''; const style = getCommonPinningStyles(cell.column, false); return (
{flexRender( cell.column.columnDef.cell, cell.getContext(), )}
); })}
))}
{table.getRowModel().rows.length === 0 && (
暂无数据
)} ); }