'use client'; import { motion, AnimatePresence } from 'framer-motion'; import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; import timezone from 'dayjs/plugin/timezone'; import isSameOrAfter from 'dayjs/plugin/isSameOrAfter'; import { isNumber, isString } from 'lodash'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Stat } from './Common'; import FundTrendChart from './FundTrendChart'; import FundIntradayChart from './FundIntradayChart'; import { ChevronIcon, ExitIcon, SettingsIcon, StarIcon, SwitchIcon, TrashIcon, } from './Icons'; dayjs.extend(utc); dayjs.extend(timezone); dayjs.extend(isSameOrAfter); const DEFAULT_TZ = 'Asia/Shanghai'; const getBrowserTimeZone = () => { if (typeof Intl !== 'undefined' && Intl.DateTimeFormat) { const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; return tz || DEFAULT_TZ; } return DEFAULT_TZ; }; const TZ = getBrowserTimeZone(); const toTz = (input) => (input ? dayjs.tz(input, TZ) : dayjs().tz(TZ)); export default function FundCard({ fund: f, todayStr, currentTab, favorites, dcaPlans, holdings, percentModes, valuationSeries, collapsedCodes, collapsedTrends, transactions, theme, isTradingDay, refreshing, getHoldingProfit, onRemoveFromGroup, onToggleFavorite, onRemoveFund, onHoldingClick, onActionClick, onPercentModeToggle, onToggleCollapse, onToggleTrendCollapse, layoutMode = 'card', // 'card' | 'drawer',drawer 时前10重仓与业绩走势以 Tabs 展示 masked = false, }) { const holding = holdings[f?.code]; const profit = getHoldingProfit?.(f, holding) ?? null; const hasHoldings = f.holdingsIsLastQuarter && Array.isArray(f.holdings) && f.holdings.length > 0; const style = layoutMode === 'drawer' ? { border: 'none', boxShadow: 'none', paddingLeft: 0, paddingRight: 0, background: 'transparent', } : {}; return (
{currentTab !== 'all' && currentTab !== 'fav' ? ( ) : ( )}
{f.name} #{f.code} {dcaPlans?.[f.code]?.enabled === true && } {f.jzrq === todayStr && }
{f.noValuation ? '净值日期' : '估值时间'} {f.noValuation ? (f.jzrq || '-') : (f.gztime || f.time || '-')}
{f.noValuation ? ( 0 ? '+' : ''}${Number(f.zzl).toFixed(2)}%` : '—' } delta={f.zzl} /> ) : ( <> {(() => { const hasTodayData = f.jzrq === todayStr; let isYesterdayChange = false; let isPreviousTradingDay = false; if (!hasTodayData && isString(f.jzrq)) { const today = toTz(todayStr).startOf('day'); const jzDate = toTz(f.jzrq).startOf('day'); const yesterday = today.clone().subtract(1, 'day'); if (jzDate.isSame(yesterday, 'day')) { isYesterdayChange = true; } else if (jzDate.isBefore(yesterday, 'day')) { isPreviousTradingDay = true; } } const shouldHideChange = isTradingDay && !hasTodayData && !isYesterdayChange && !isPreviousTradingDay; if (shouldHideChange) return null; const changeLabel = hasTodayData ? '涨跌幅' : '昨日涨幅'; return ( 0 ? '+' : ''}${Number(f.zzl).toFixed(2)}%` : '' } delta={f.zzl} /> ); })()} 0.05 ? f.estGsz.toFixed(4) : (f.gsz ?? '—') } /> 0.05 ? `${f.estGszzl > 0 ? '+' : ''}${f.estGszzl.toFixed(2)}%` : isNumber(f.gszzl) ? `${f.gszzl > 0 ? '+' : ''}${f.gszzl.toFixed(2)}%` : f.gszzl ?? '—' } delta={f.estPricedCoverage > 0.05 ? f.estGszzl : Number(f.gszzl) || 0} /> )}
{!profit ? (
持仓金额
onHoldingClick?.(f)} > 未设置
) : ( <>
onActionClick?.(f)} > 持仓金额 {masked ? '******' : `¥${profit.amount.toFixed(2)}`}
当日收益 0 ? 'up' : profit.profitToday < 0 ? 'down' : '' : 'muted' }`} > {profit.profitToday != null ? masked ? '******' : `${profit.profitToday > 0 ? '+' : profit.profitToday < 0 ? '-' : ''}¥${Math.abs(profit.profitToday).toFixed(2)}` : '--'}
{profit.profitTotal !== null && (
{ e.stopPropagation(); onPercentModeToggle?.(f.code); }} style={{ cursor: 'pointer', flexDirection: 'column', gap: 4, alignItems: 'flex-end' }} title="点击切换金额/百分比" > 持有收益{percentModes?.[f.code] ? '(%)' : ''} 0 ? 'up' : profit.profitTotal < 0 ? 'down' : '' }`} > {masked ? '******' : <> {profit.profitTotal > 0 ? '+' : profit.profitTotal < 0 ? '-' : ''} {percentModes?.[f.code] ? `${Math.abs( holding?.cost * holding?.share ? (profit.profitTotal / (holding.cost * holding.share)) * 100 : 0, ).toFixed(2)}%` : `¥${Math.abs(profit.profitTotal).toFixed(2)}`} }
)} )}
{f.estPricedCoverage > 0.05 && (
基于 {Math.round(f.estPricedCoverage * 100)}% 持仓估算
)} {(() => { const showIntraday = Array.isArray(valuationSeries?.[f.code]) && valuationSeries[f.code].length >= 2; if (!showIntraday) return null; if ( f.gztime && toTz(todayStr).startOf('day').isAfter(toTz(f.gztime).startOf('day')) ) { return null; } if ( f.jzrq && f.gztime && toTz(f.jzrq).startOf('day').isSameOrAfter(toTz(f.gztime).startOf('day')) ) { return null; } return ( ); })()} {layoutMode === 'drawer' ? ( {hasHoldings && ( 前10重仓股票 )} 业绩走势 {hasHoldings && (
{f.holdings.map((h, idx) => (
{h.name}
{isNumber(h.change) && ( 0 ? 'up' : h.change < 0 ? 'down' : ''}`} style={{ marginRight: 8 }} > {h.change > 0 ? '+' : ''} {h.change.toFixed(2)}% )} {h.weight}
))}
)} onToggleTrendCollapse?.(f.code)} transactions={transactions?.[f.code] || []} theme={theme} hideHeader />
) : ( <> {hasHoldings && ( <>
onToggleCollapse?.(f.code)} >
前10重仓股票
涨跌幅 / 占比
{!collapsedCodes?.has(f.code) && (
{f.holdings.map((h, idx) => (
{h.name}
{isNumber(h.change) && ( 0 ? 'up' : h.change < 0 ? 'down' : ''}`} style={{ marginRight: 8 }} > {h.change > 0 ? '+' : ''} {h.change.toFixed(2)}% )} {h.weight}
))}
)}
)} onToggleTrendCollapse?.(f.code)} transactions={transactions?.[f.code] || []} theme={theme} /> )}
); }