From 687a18e769f396435705a5766e23eae2a60d2514 Mon Sep 17 00:00:00 2001 From: shy3130 <415333856@qq.com> Date: Tue, 18 Aug 2026 21:33:26 +0800 Subject: [PATCH] =?UTF-8?q?refactor(watchlist):=20=E5=88=86=E7=BB=84?= =?UTF-8?q?=E6=B6=A8=E8=B7=8C=E5=B9=85=E6=9C=80=E4=BC=98=E6=80=A7=E5=A4=8D?= =?UTF-8?q?=E5=AE=A1=20=E2=80=94=20=E5=8F=AF=E8=A7=81=E6=80=A7=E9=97=A8?= =?UTF-8?q?=E6=8E=A7=E4=B8=8E=E6=A0=BC=E5=BC=8F=E5=8C=96=E5=8E=BB=E9=87=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 复审发现两处可优化: 1. 侧栏分组涨跌幅查询原为 groupsInNav 开启即无条件拉取 — 改为 「侧栏展开 ∧ 自选二级菜单展开」才请求, 收起状态下不再为隐藏 UI 发 watchlist/enriched 请求(展开即时拉取, 与自选页共享缓存去重) 2. formatGroupPct 与全站 fmtPct(lib/format)逻辑完全重复 — 删除并 复用 fmtPct, 格式随全局统一; 顺带消除 Layout 中 navCollapsed 状态声明的位置耦合(上移至查询前) 验证: tsc 通过; 浏览器实测 — 子菜单收起 0 次 enriched 请求, 展开 后 1 次请求即显示; 分组 tab/侧栏数值与优化前逐位一致(全部+0.59% 半导体-1.55% 等), 悬停明细正常。 --- frontend/src/components/Layout.tsx | 31 ++++++++++++--------- frontend/src/components/WatchlistGroups.tsx | 4 +-- frontend/src/lib/watchlistGroupStats.ts | 15 ++++------ 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index f247ee8..8d36921 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -57,7 +57,8 @@ import { Logo } from './Logo' import { api, type IndexQuote } from '@/lib/api' import { cn } from '@/lib/cn' import { resolveWatchlistGroupColor } from '@/lib/watchlist-group-colors' -import { computeGroupPcts, formatGroupPct, groupPctColor, groupPctTitle } from '@/lib/watchlistGroupStats' +import { computeGroupPcts, groupPctColor, groupPctTitle } from '@/lib/watchlistGroupStats' +import { fmtPct } from '@/lib/format' import { toggleTheme, useTheme } from '@/lib/theme' import { setCurrentTotal as setAlertTotal, useUnreadAlerts } from '@/lib/monitorBadge' import { ExtensionSlot } from '@/extensions/ExtensionSlot' @@ -302,18 +303,29 @@ export function Layout() { staleTime: 60_000, }) const watchlistGroups = watchlistGroupsData?.groups ?? [] + // 自选二级菜单展开状态 — 默认当前在自选页时展开 + const [watchlistNavExpanded, setWatchlistNavExpanded] = useState(location.pathname === '/watchlist') + + // 侧边栏收起状态 — 持久化到 localStorage + const [navCollapsed, setNavCollapsed] = useState(() => { + if (typeof window !== 'undefined' && window.matchMedia('(max-width: 767px)').matches) return true + try { return localStorage.getItem('tf-nav-collapsed') === '1' } catch { return false } + }) + // 分组等权平均涨跌幅 — 复用 watchlist/enriched 查询缓存(与自选页同 key, - // 盘中随 SSE 刷新); 侧栏开启分组时才拉取 + // 盘中随 SSE 刷新)。可见性门控: 子菜单实际可见(侧栏展开 + 二级菜单展开) + // 时才拉取, 收起状态下不为隐藏 UI 发请求。 + const navGroupPctVisible = groupsInNav && !navCollapsed && watchlistNavExpanded const { data: navWatchlist } = useQuery({ queryKey: QK.watchlist, queryFn: api.watchlistList, - enabled: groupsInNav, + enabled: navGroupPctVisible, staleTime: 60_000, }) const { data: navEnriched } = useQuery({ queryKey: QK.watchlistEnriched(undefined), queryFn: () => api.watchlistEnriched(), - enabled: groupsInNav, + enabled: navGroupPctVisible, staleTime: 60_000, }) const navGroupPcts = useMemo( @@ -323,8 +335,6 @@ export function Layout() { ), [navWatchlist, navEnriched], ) - // 自选二级菜单展开状态 — 默认当前在自选页时展开 - const [watchlistNavExpanded, setWatchlistNavExpanded] = useState(location.pathname === '/watchlist') // 数据同步状态轮询: 有活跃 job 时「数据」菜单项显示转圈 const { data: pipelineJobs } = useQuery({ @@ -356,11 +366,6 @@ export function Layout() { const realtimeEnabled = prefs?.realtime_quotes_enabled ?? false // Free 档监控限制提示: 可手动关闭, 不持久化 (刷新后恢复显示) const [dismissFreeHint, setDismissFreeHint] = useState(false) - // 侧边栏收起状态 — 持久化到 localStorage - const [navCollapsed, setNavCollapsed] = useState(() => { - if (typeof window !== 'undefined' && window.matchMedia('(max-width: 767px)').matches) return true - try { return localStorage.getItem('tf-nav-collapsed') === '1' } catch { return false } - }) useEffect(() => { const compact = window.matchMedia('(max-width: 767px)') const syncSidebarWithViewport = (event: MediaQueryListEvent | MediaQueryList) => { @@ -676,7 +681,7 @@ export function Layout() { const info = navGroupPcts['all'] return info && info.pct != null ? ( - {formatGroupPct(info.pct)} + {fmtPct(info.pct)} ) : null })()} @@ -701,7 +706,7 @@ export function Layout() { {group.name} {pctInfo && pctInfo.pct != null && ( - {formatGroupPct(pctInfo.pct)} + {fmtPct(pctInfo.pct)} )} diff --git a/frontend/src/components/WatchlistGroups.tsx b/frontend/src/components/WatchlistGroups.tsx index 93cc84f..f636488 100644 --- a/frontend/src/components/WatchlistGroups.tsx +++ b/frontend/src/components/WatchlistGroups.tsx @@ -5,9 +5,9 @@ import { Modal } from '@/components/Modal' import { api } from '@/lib/api' import { QK } from '@/lib/queryKeys' import { usePreferences } from '@/lib/useSharedQueries' +import { fmtPct } from '@/lib/format' import type { WatchlistGroup, WatchlistGroupColor } from '@/lib/api' import { - formatGroupPct, groupPctColor, groupPctTitle, type GroupPctMap, @@ -91,7 +91,7 @@ export function WatchlistGroupBar({ className={`font-mono text-[10px] tabular-nums ${groupPctColor(info.pct)}`} title={groupPctTitle(info)} > - {formatGroupPct(info.pct)} + {fmtPct(info.pct)} ) })()} diff --git a/frontend/src/lib/watchlistGroupStats.ts b/frontend/src/lib/watchlistGroupStats.ts index b7e1ee0..4bab1e9 100644 --- a/frontend/src/lib/watchlistGroupStats.ts +++ b/frontend/src/lib/watchlistGroupStats.ts @@ -2,10 +2,12 @@ * 自选分组涨跌幅 — 等权平均口径。 * * 组内每只成员取「实时优先、收盘兜底」的涨跌幅(与自选表格展示同源: - * rt_pct ?? change_pct, 百分数单位), 算术平均即分组涨跌幅。等权最贴合 + * rt_pct ?? change_pct, 小数单位), 算术平均即分组涨跌幅。等权最贴合 * 自选的"个人组合"视角 — 透明且无需市值数据。 */ +import { fmtPct } from '@/lib/format' + export interface GroupPctInfo { /** 等权平均涨跌幅(小数, 0.0123 = +1.23%, 与 enriched change_pct 同单位); 无有效样本为 null */ pct: number | null @@ -52,21 +54,14 @@ export function computeGroupPcts( return out } -/** 涨跌幅文本: +1.23% / -0.50% / 0.00% / — (无样本)。入参为小数(0.0123) */ -export function formatGroupPct(pct: number | null): string { - if (pct == null) return '—' - const v = pct * 100 - return `${v > 0 ? '+' : ''}${v.toFixed(2)}%` -} - /** 涨跌色 (A 股惯例红涨绿跌) */ export function groupPctColor(pct: number | null): string { if (pct == null || pct === 0) return 'text-muted' return pct > 0 ? 'text-bull' : 'text-bear' } -/** 悬停明细: 等权平均 +1.23% · 上涨12 下跌5 平1 */ +/** 悬停明细: 等权平均 +1.23% · 上涨12 下跌5 平1 (格式化复用全站 fmtPct) */ export function groupPctTitle(info: GroupPctInfo | undefined): string { if (!info || info.pct == null) return '暂无涨跌幅数据' - return `等权平均 ${formatGroupPct(info.pct)} · 上涨${info.up} 下跌${info.down} 平${info.flat} (共${info.sampled}只)` + return `等权平均 ${fmtPct(info.pct)} · 上涨${info.up} 下跌${info.down} 平${info.flat} (共${info.sampled}只)` }