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}只)`
}