+
)}
{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}
/>
>
- )}
- )
- )}
+ ))}