diff --git a/app/components/MobileFundTable.jsx b/app/components/MobileFundTable.jsx new file mode 100644 index 0000000..24faffb --- /dev/null +++ b/app/components/MobileFundTable.jsx @@ -0,0 +1,588 @@ +'use client'; + +import { useEffect, useMemo, useRef, useState } from 'react'; +import { AnimatePresence, motion } from 'framer-motion'; +import { + flexRender, + getCoreRowModel, + useReactTable, +} from '@tanstack/react-table'; +import { + DndContext, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + closestCenter, +} from '@dnd-kit/core'; +import { restrictToVerticalAxis, restrictToParentElement } from '@dnd-kit/modifiers'; +import { + SortableContext, + verticalListSortingStrategy, + useSortable, +} from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import MobileSettingModal from './MobileSettingModal'; +import { ExitIcon, SettingsIcon, StarIcon } from './Icons'; + +const MOBILE_NON_FROZEN_COLUMN_IDS = [ + 'yesterdayChangePercent', + 'estimateChangePercent', + 'todayProfit', + 'holdingProfit', +]; +const MOBILE_COLUMN_HEADERS = { + yesterdayChangePercent: '昨日涨跌幅', + estimateChangePercent: '估值涨跌幅', + todayProfit: '当日收益', + holdingProfit: '持有收益', +}; + +function SortableRow({ row, children, isTableDragging, disabled }) { + const { + attributes, + listeners, + transform, + transition, + setNodeRef, + setActivatorNodeRef, + isDragging, + } = useSortable({ id: row.original.code, disabled }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + ...(isDragging ? { position: 'relative', zIndex: 9999, opacity: 0.8, boxShadow: '0 4px 12px rgba(0,0,0,0.15)' } : {}), + }; + + return ( + + {typeof children === 'function' ? children(setActivatorNodeRef, listeners) : children} + + ); +} + +/** + * 移动端基金列表表格组件(基于 @tanstack/react-table,与 PcFundTable 相同数据结构) + * + * @param {Object} props - 与 PcFundTable 一致 + * @param {Array} props.data - 表格数据(与 pcFundTableData 同结构) + * @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] - 是否刷新中 + * @param {string} [props.sortBy] - 排序方式,'default' 时长按行触发拖拽排序 + * @param {(oldIndex: number, newIndex: number) => void} [props.onReorder] - 拖拽排序回调 + */ +export default function MobileFundTable({ + data = [], + onRemoveFund, + currentTab, + favorites = new Set(), + onToggleFavorite, + onRemoveFromGroup, + onHoldingAmountClick, + onHoldingProfitClick, + refreshing = false, + sortBy = 'default', + onReorder, +}) { + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { delay: 400, tolerance: 5 }, + }), + useSensor(KeyboardSensor) + ); + + const [activeId, setActiveId] = useState(null); + + const onToggleFavoriteRef = useRef(onToggleFavorite); + const onRemoveFromGroupRef = useRef(onRemoveFromGroup); + const onHoldingAmountClickRef = useRef(onHoldingAmountClick); + const onHoldingProfitClickRef = useRef(onHoldingProfitClick); + + useEffect(() => { + onToggleFavoriteRef.current = onToggleFavorite; + onRemoveFromGroupRef.current = onRemoveFromGroup; + onHoldingAmountClickRef.current = onHoldingAmountClick; + onHoldingProfitClickRef.current = onHoldingProfitClick; + }, [ + onToggleFavorite, + onRemoveFromGroup, + onHoldingAmountClick, + onHoldingProfitClick, + ]); + + const handleDragStart = (e) => setActiveId(e.active.id); + const handleDragCancel = () => setActiveId(null); + const handleDragEnd = (e) => { + const { active, over } = e; + if (active && over && active.id !== over.id && onReorder) { + const oldIndex = data.findIndex((item) => item.code === active.id); + const newIndex = data.findIndex((item) => item.code === over.id); + if (oldIndex !== -1 && newIndex !== -1) onReorder(oldIndex, newIndex); + } + setActiveId(null); + }; + + const getStoredMobileColumnOrder = () => { + if (typeof window === 'undefined') return null; + try { + const raw = window.localStorage.getItem('customSettings'); + if (!raw) return null; + const parsed = JSON.parse(raw); + const order = parsed?.mobileTableColumnOrder; + if (!Array.isArray(order) || order.length === 0) return null; + const valid = order.filter((id) => MOBILE_NON_FROZEN_COLUMN_IDS.includes(id)); + const missing = MOBILE_NON_FROZEN_COLUMN_IDS.filter((id) => !valid.includes(id)); + return [...valid, ...missing]; + } catch { + return null; + } + }; + const persistMobileColumnOrder = (nextOrder) => { + 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, mobileTableColumnOrder: nextOrder } + : { mobileTableColumnOrder: nextOrder }; + window.localStorage.setItem('customSettings', JSON.stringify(nextSettings)); + } catch {} + }; + const getStoredMobileColumnVisibility = () => { + if (typeof window === 'undefined') return null; + try { + const raw = window.localStorage.getItem('customSettings'); + if (!raw) return null; + const parsed = JSON.parse(raw); + const visibility = parsed?.mobileTableColumnVisibility; + if (!visibility || typeof visibility !== 'object') return null; + const normalized = {}; + MOBILE_NON_FROZEN_COLUMN_IDS.forEach((id) => { + const value = visibility[id]; + if (typeof value === 'boolean') normalized[id] = value; + }); + return Object.keys(normalized).length ? normalized : null; + } catch { + return null; + } + }; + const persistMobileColumnVisibility = (nextVisibility) => { + 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, mobileTableColumnVisibility: nextVisibility } + : { mobileTableColumnVisibility: nextVisibility }; + window.localStorage.setItem('customSettings', JSON.stringify(nextSettings)); + } catch {} + }; + + const [mobileColumnOrder, setMobileColumnOrder] = useState( + () => getStoredMobileColumnOrder() ?? [...MOBILE_NON_FROZEN_COLUMN_IDS] + ); + const [mobileColumnVisibility, setMobileColumnVisibility] = useState(() => { + const stored = getStoredMobileColumnVisibility(); + if (stored) return stored; + const allVisible = {}; + MOBILE_NON_FROZEN_COLUMN_IDS.forEach((id) => { + allVisible[id] = true; + }); + return allVisible; + }); + const [settingModalOpen, setSettingModalOpen] = useState(false); + + const handleResetMobileColumnOrder = () => { + const defaultOrder = [...MOBILE_NON_FROZEN_COLUMN_IDS]; + setMobileColumnOrder(defaultOrder); + persistMobileColumnOrder(defaultOrder); + }; + const handleResetMobileColumnVisibility = () => { + const allVisible = {}; + MOBILE_NON_FROZEN_COLUMN_IDS.forEach((id) => { + allVisible[id] = true; + }); + setMobileColumnVisibility(allVisible); + persistMobileColumnVisibility(allVisible); + }; + const handleToggleMobileColumnVisibility = (columnId, visible) => { + setMobileColumnVisibility((prev = {}) => { + const next = { ...prev, [columnId]: visible }; + persistMobileColumnVisibility(next); + return next; + }); + }; + + // 移动端名称列:无拖拽把手,长按整行触发排序 + const MobileFundNameCell = ({ info }) => { + const original = info.row.original || {}; + const code = original.code; + const isUpdated = original.isUpdated; + const hasHoldingAmount = original.holdingAmountValue != null; + const holdingAmountDisplay = hasHoldingAmount ? (original.holdingAmount ?? '—') : null; + const isFavorites = favorites?.has?.(code); + const isGroupTab = currentTab && currentTab !== 'all' && currentTab !== 'fav'; + + return ( +
+ {isGroupTab ? ( + + ) : ( + + )} +
+ + {info.getValue() ?? '—'} + + {holdingAmountDisplay ? ( + { + e.stopPropagation?.(); + onHoldingAmountClickRef.current?.(original, { hasHolding: true }); + }} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onHoldingAmountClickRef.current?.(original, { hasHolding: true }); + } + }} + > + {holdingAmountDisplay} + {isUpdated && } + + ) : code ? ( + { + e.stopPropagation?.(); + onHoldingAmountClickRef.current?.(original, { hasHolding: false }); + }} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onHoldingAmountClickRef.current?.(original, { hasHolding: false }); + } + }} + > + #{code} + {isUpdated && } + + ) : null} +
+
+ ); + }; + + const columns = useMemo( + () => [ + { + accessorKey: 'fundName', + header: () => ( +
+ 基金名称 + +
+ ), + cell: (info) => , + meta: { align: 'left', cellClassName: 'name-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: '估值涨跌幅', + 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: '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: '持有收益', + 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' }, + }, + ], + [currentTab, favorites, refreshing] + ); + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + state: { + columnOrder: ['fundName', ...mobileColumnOrder], + columnVisibility: { fundName: true, ...mobileColumnVisibility }, + }, + onColumnOrderChange: (updater) => { + const next = typeof updater === 'function' ? updater(['fundName', ...mobileColumnOrder]) : updater; + const newNonFrozen = next.filter((id) => id !== 'fundName'); + if (newNonFrozen.length) { + setMobileColumnOrder(newNonFrozen); + persistMobileColumnOrder(newNonFrozen); + } + }, + onColumnVisibilityChange: (updater) => { + const next = typeof updater === 'function' ? updater({ fundName: true, ...mobileColumnVisibility }) : updater; + const rest = { ...next }; + delete rest.fundName; + setMobileColumnVisibility(rest); + persistMobileColumnVisibility(rest); + }, + initialState: { + columnPinning: { + left: ['fundName'], + }, + }, + defaultColumn: { + cell: (info) => info.getValue() ?? '—', + }, + }); + + const headerGroup = table.getHeaderGroups()[0]; + + const getPinClass = (columnId, isHeader) => { + if (columnId === 'fundName') return isHeader ? 'table-header-cell-pin-left' : 'table-cell-pin-left'; + return ''; + }; + + const getAlignClass = (columnId) => { + if (columnId === 'fundName') return ''; + if (['yesterdayChangePercent', 'estimateChangePercent', 'todayProfit', 'holdingProfit'].includes(columnId)) return 'text-right'; + return 'text-right'; + }; + + return ( +
+
+ {headerGroup && ( +
+ {headerGroup.headers.map((header) => { + const columnId = header.column.id; + const pinClass = getPinClass(columnId, true); + const alignClass = getAlignClass(columnId); + return ( +
+ {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} +
+ ); + })} +
+ )} + + + item.code)} + strategy={verticalListSortingStrategy} + > + + {table.getRowModel().rows.map((row) => ( + + {(setActivatorNodeRef, listeners) => ( +
+ {row.getVisibleCells().map((cell) => { + const columnId = cell.column.id; + const pinClass = getPinClass(columnId, false); + const alignClass = getAlignClass(columnId); + const cellClassName = cell.column.columnDef.meta?.cellClassName || ''; + return ( +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+ ); + })} +
+ )} +
+ ))} +
+
+
+
+ + {table.getRowModel().rows.length === 0 && ( +
+
+ 暂无数据 +
+
+ )} + + setSettingModalOpen(false)} + columns={mobileColumnOrder.map((id) => ({ id, header: MOBILE_COLUMN_HEADERS[id] ?? id }))} + columnVisibility={mobileColumnVisibility} + onColumnReorder={(newOrder) => { + setMobileColumnOrder(newOrder); + persistMobileColumnOrder(newOrder); + }} + onToggleColumnVisibility={handleToggleMobileColumnVisibility} + onResetColumnOrder={handleResetMobileColumnOrder} + onResetColumnVisibility={handleResetMobileColumnVisibility} + /> +
+ ); +} diff --git a/app/components/MobileSettingModal.jsx b/app/components/MobileSettingModal.jsx new file mode 100644 index 0000000..e4d7018 --- /dev/null +++ b/app/components/MobileSettingModal.jsx @@ -0,0 +1,220 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { AnimatePresence, motion, Reorder } from 'framer-motion'; +import { createPortal } from 'react-dom'; +import ConfirmModal from './ConfirmModal'; +import { CloseIcon, DragIcon, ResetIcon, SettingsIcon } from './Icons'; + +/** + * 移动端表格个性化设置弹框(底部抽屉) + * @param {Object} props + * @param {boolean} props.open - 是否打开 + * @param {() => void} props.onClose - 关闭回调 + * @param {Array<{id: string, header: string}>} props.columns - 非冻结列(id + 表头名称) + * @param {Record} [props.columnVisibility] - 列显示状态映射(id => 是否显示) + * @param {(newOrder: string[]) => void} props.onColumnReorder - 列顺序变更回调 + * @param {(id: string, visible: boolean) => void} props.onToggleColumnVisibility - 列显示/隐藏切换回调 + * @param {() => void} props.onResetColumnOrder - 重置列顺序回调 + * @param {() => void} props.onResetColumnVisibility - 重置列显示/隐藏回调 + */ +export default function MobileSettingModal({ + open, + onClose, + columns = [], + columnVisibility, + onColumnReorder, + onToggleColumnVisibility, + onResetColumnOrder, + onResetColumnVisibility, +}) { + const [resetConfirmOpen, setResetConfirmOpen] = useState(false); + + useEffect(() => { + if (!open) setResetConfirmOpen(false); + }, [open]); + + useEffect(() => { + if (open) { + const prev = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + return () => { + document.body.style.overflow = prev; + }; + } + }, [open]); + + const handleReorder = (newItems) => { + const newOrder = newItems.map((item) => item.id); + onColumnReorder?.(newOrder); + }; + + const content = ( + + {open && ( + + e.stopPropagation()} + > +
+
+ + 个性化设置 +
+ +
+ +
+

表头设置

+
+

+ 拖拽调整列顺序 +

+ {(onResetColumnOrder || onResetColumnVisibility) && ( + + )} +
+ {columns.length === 0 ? ( +
+ 暂无可配置列 +
+ ) : ( + + + {columns.map((item, index) => ( + +
+ +
+ {item.header} + {onToggleColumnVisibility && ( + + )} +
+ ))} +
+
+ )} +
+
+
+ )} + {resetConfirmOpen && ( + { + onResetColumnOrder?.(); + onResetColumnVisibility?.(); + setResetConfirmOpen(false); + }} + onCancel={() => setResetConfirmOpen(false)} + confirmText="重置" + /> + )} +
+ ); + + if (typeof document === 'undefined') return null; + return createPortal(content, document.body); +} diff --git a/app/components/PcFundTable.jsx b/app/components/PcFundTable.jsx index d95289f..9ca3008 100644 --- a/app/components/PcFundTable.jsx +++ b/app/components/PcFundTable.jsx @@ -504,7 +504,7 @@ export default function PcFundTable({ onHoldingAmountClickRef.current?.(original, { hasHolding: true }); }} title="编辑持仓" - style={{ border: 'none', width: '28px', height: '28px', marginLeft: -6 }} + style={{ border: 'none', width: '28px', height: '28px', marginLeft: -6, backgroundColor: 'transparent' }} > @@ -606,7 +606,7 @@ export default function PcFundTable({ }; return ( -
+
)} {viewMode === 'list' && isMobile && ( -
-
基金名称
-
净值/估值
-
涨跌幅
-
估值时间
-
持仓金额
-
当日收益
-
持有收益
-
操作
-
+ { + if (refreshing) return; + if (!row || !row.code) return; + requestRemoveFund({ code: row.code, name: row.fundName }); + }} + onToggleFavorite={(row) => { + if (!row || !row.code) return; + toggleFavorite(row.code); + }} + onRemoveFromGroup={(row) => { + if (!row || !row.code) return; + removeFundFromCurrentGroup(row.code); + }} + onHoldingAmountClick={(row, meta) => { + if (!row || !row.code) return; + const fund = row.rawFund || { code: row.code, name: row.fundName }; + if (meta?.hasHolding) { + setActionModal({ open: true, fund }); + } else { + setHoldingModal({ open: true, fund }); + } + }} + onHoldingProfitClick={(row) => { + if (!row || !row.code) return; + if (row.holdingProfitValue == null) return; + setPercentModes((prev) => ({ ...prev, [row.code]: !prev[row.code] })); + }} + /> )} - {displayFunds.map((f) => - (viewMode === 'list' && !isMobile) ? null : ( + {viewMode === 'card' && displayFunds.map((f) => ( - {viewMode === 'list' && isMobile && ( -
{ - e.stopPropagation(); // 阻止冒泡,防止触发全局收起导致状态混乱 - if (refreshing) return; - requestRemoveFund(f); - }} - style={{ pointerEvents: refreshing ? 'none' : 'auto', opacity: refreshing ? 0.6 : 1 }} - > - - 删除 -
- )} { - // 如果水平移动距离小于垂直移动距离,或者水平速度很小,视为垂直滚动意图,不进行拖拽处理 - // framer-motion 的 dragDirectionLock 已经处理了大部分情况,但可以进一步微调体验 - }} - // 如果当前行不是被选中的行,强制回到原点 (x: 0) - animate={viewMode === 'list' && isMobile ? { x: swipedFundCode === f.code ? -80 : 0 } : undefined} - onDragEnd={(e, { offset, velocity }) => { - if (viewMode === 'list' && isMobile) { - if (offset.x < -40) { - setSwipedFundCode(f.code); - } else { - setSwipedFundCode(null); - } - } - }} - onClick={(e) => { - // 阻止事件冒泡,避免触发全局的 click listener 导致立刻被收起 - // 只有在已经展开的情况下点击自身才需要阻止冒泡(或者根据需求调整) - // 这里我们希望:点击任何地方都收起。 - // 如果点击的是当前行,且不是拖拽操作,上面的全局 listener 会处理收起。 - // 但为了防止点击行内容触发收起后又立即触发行的其他点击逻辑(如果有的话), - // 可以在这里处理。不过当前需求是“点击其他区域收起”, - // 实际上全局 listener 已经覆盖了“点击任何区域(包括其他行)收起”。 - // 唯一的问题是:点击当前行的“删除按钮”时,会先触发全局 click 导致收起,然后触发删除吗? - // 删除按钮在底层,通常不会受影响,因为 React 事件和原生事件的顺序。 - // 但为了保险,删除按钮的 onClick 应该阻止冒泡。 - - // 如果当前行已展开,点击行内容(非删除按钮)应该收起 - if (viewMode === 'list' && isMobile && swipedFundCode === f.code) { - e.stopPropagation(); // 阻止冒泡,自己处理收起,避免触发全局再次处理 - setSwipedFundCode(null); - } - }} - style={{ - background: viewMode === 'list' ? 'var(--bg)' : undefined, - position: 'relative', - zIndex: 1 - }} + className="glass card" + style={{ position: 'relative', zIndex: 1 }} > - {viewMode === 'list' ? ( - <> -
- {currentTab !== 'all' && currentTab !== 'fav' ? ( - - ) : ( - - )} -
- - {f.name} - - - {(() => { - const holding = holdings[f.code]; - const profit = getHoldingProfit(f, holding); - return profit ? `¥${profit.amount.toFixed(2)}` : `#${f.code}`; - })()} - {f.jzrq === todayStr && } - -
-
- {(() => { - const hasTodayData = f.jzrq === todayStr; - const shouldHideChange = isTradingDay && !hasTodayData; - - if (!shouldHideChange) { - // 如果涨跌幅列显示(即非交易时段或今日净值已更新),则显示单位净值和真实涨跌幅 - return ( - <> -
- {f.dwjz ?? '—'} -
-
- 0 ? 'up' : f.zzl < 0 ? 'down' : ''} style={{ fontWeight: 700 }}> - {f.zzl !== undefined ? `${f.zzl > 0 ? '+' : ''}${Number(f.zzl).toFixed(2)}%` : ''} - -
- - ); - } else { - // 否则显示估值净值和估值涨跌幅 - // 如果是无估值数据的基金,直接显示净值数据 - if (f.noValuation) { - return ( - <> -
- {f.dwjz ?? '—'} -
-
- 0 ? 'up' : f.zzl < 0 ? 'down' : ''} style={{ fontWeight: 700 }}> - {f.zzl !== undefined && f.zzl !== null ? `${f.zzl > 0 ? '+' : ''}${Number(f.zzl).toFixed(2)}%` : '—'} - -
- - ); - } - return ( - <> -
- {f.estPricedCoverage > 0.05 ? f.estGsz.toFixed(4) : (f.gsz ?? '—')} -
-
- 0.05 ? (f.estGszzl > 0 ? 'up' : f.estGszzl < 0 ? 'down' : '') : (Number(f.gszzl) > 0 ? 'up' : Number(f.gszzl) < 0 ? 'down' : '')} style={{ fontWeight: 700 }}> - {f.estPricedCoverage > 0.05 ? `${f.estGszzl > 0 ? '+' : ''}${f.estGszzl.toFixed(2)}%` : (isNumber(f.gszzl) ? `${f.gszzl > 0 ? '+' : ''}${f.gszzl.toFixed(2)}%` : f.gszzl ?? '—')} - -
- - ); - } - })()} -
- {f.noValuation ? (f.jzrq || '-') : (f.gztime || f.time || '-')} -
- {!isMobile && (() => { - const holding = holdings[f.code]; - const profit = getHoldingProfit(f, holding); - const amount = profit ? profit.amount : null; - if (amount === null) { - return ( -
{ e.stopPropagation(); setHoldingModal({ open: true, fund: f }); }} - > - - 未设置 - -
- ); - } - return ( -
{ e.stopPropagation(); setActionModal({ open: true, fund: f }); }} - > - ¥{amount.toFixed(2)} - -
- ); - })()} - {(() => { - const holding = holdings[f.code]; - const profit = getHoldingProfit(f, holding); - const profitValue = profit ? profit.profitToday : null; - const hasProfit = profitValue !== null; - - return ( -
- 0 ? 'up' : profitValue < 0 ? 'down' : '') : 'muted'} - style={{ fontWeight: 700 }} - > - {hasProfit - ? `${profitValue > 0 ? '+' : profitValue < 0 ? '-' : ''}¥${Math.abs(profitValue).toFixed(2)}` - : ''} - -
- ); - })()} - {!isMobile && (() => { - const holding = holdings[f.code]; - const profit = getHoldingProfit(f, holding); - const total = profit ? profit.profitTotal : null; - const principal = holding && holding.cost && holding.share ? holding.cost * holding.share : 0; - const asPercent = percentModes[f.code]; - const hasTotal = total !== null; - const formatted = hasTotal - ? (asPercent && principal > 0 - ? `${total > 0 ? '+' : total < 0 ? '-' : ''}${Math.abs((total / principal) * 100).toFixed(2)}%` - : `${total > 0 ? '+' : total < 0 ? '-' : ''}¥${Math.abs(total).toFixed(2)}`) - : ''; - const cls = hasTotal ? (total > 0 ? 'up' : total < 0 ? 'down' : '') : 'muted'; - return ( -
{ - e.stopPropagation(); - if (hasTotal) { - setPercentModes(prev => ({ ...prev, [f.code]: !prev[f.code] })); - } - }} - style={{ cursor: hasTotal ? 'pointer' : 'default' }} - > - {formatted} -
- ); - })()} -
- -
- - ) : ( - <> + <>
{currentTab !== 'all' && currentTab !== 'fav' ? ( @@ -4491,11 +4266,9 @@ export default function HomePage() { theme={theme} /> - )} - ) - )} + ))}