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