feat(frontend): 指数页固定核心四只, 删除全指数搜索与侧栏指数配置

- /indices 指数页保留: 左侧固定核心四只(核心指数卡片), 删除搜索框与
  全量指数列表; 右侧日K/分时详情与同步指数日K按钮不变
- 侧栏指数条固定四只常驻显示, 点击跳指数详情
- 监控设置"左侧菜单指数"配置卡删除(四选多选+固定显示开关)
- api.ts: 删 indexList/indexSearch, Preferences 移除 5 个指数偏好字段;
  queryKeys 同步清理
This commit is contained in:
shy3130
2026-08-31 16:50:28 +08:00
parent 3c6ed99922
commit 58b86a962b
8 changed files with 20 additions and 147 deletions
+6 -9
View File
@@ -141,7 +141,7 @@ function MonitorBadge({ active }: { active: boolean }) {
)
}
function SidebarIndexQuotes({ rows, items }: { rows: IndexQuote[] | undefined; items: CoreIndex[] }) {
function SidebarIndexQuotes({ rows, items }: { rows: IndexQuote[] | undefined; items: readonly CoreIndex[] }) {
if (items.length === 0) return null
const quoteBySymbol = new Map((rows ?? []).map(q => [q.symbol, q]))
return (
@@ -445,15 +445,12 @@ export function Layout() {
return next
})
}
const indicesPinned = prefs?.indices_nav_pinned ?? true
const sidebarIndexSymbols = prefs?.sidebar_index_symbols ?? CORE_INDEXES.map(p => p.symbol)
const sidebarIndexes = CORE_INDEXES.filter(item => sidebarIndexSymbols.includes(item.symbol))
// 卡片数据:固定显示时也拉取(即使实时行情关闭)
const showSidebarQuotes = indicesPinned || realtimeEnabled
// 指数条: 固定核心四只 (产品契约, 不再可配置), 常驻显示
const sidebarIndexes = CORE_INDEXES
const { data: sidebarIndexQuotes } = useQuery({
queryKey: [...QK.indexQuotes, 'sidebar', sidebarIndexSymbols.join(',')] as const,
queryKey: [...QK.indexQuotes, 'sidebar', 'core'] as const,
queryFn: () => api.indexQuotes(sidebarIndexes.map(p => p.symbol)),
enabled: showSidebarQuotes && sidebarIndexes.length > 0,
enabled: sidebarIndexes.length > 0,
placeholderData: (prev) => prev,
})
@@ -877,7 +874,7 @@ export function Layout() {
)}
</div>
)}
{showSidebarQuotes && !isWatchlistMode && (!realtimeUnavailable || !!realtimeProviderName) && (
{!isWatchlistMode && (!realtimeUnavailable || !!realtimeProviderName) && (
<SidebarIndexQuotes rows={sidebarIndexQuotes?.rows} items={sidebarIndexes} />
)}
</div>
+1 -19
View File
@@ -1595,7 +1595,6 @@ export interface WecomBotStatus {
export interface Preferences {
realtime_quotes_enabled: boolean
indices_nav_pinned: boolean
watchlist_groups_in_nav: boolean
minute_sync_enabled: boolean
minute_sync_days: number
@@ -1614,9 +1613,6 @@ export interface Preferences {
data_source_long_job_timeout_s: number
realtime_pull_stock?: boolean
realtime_pull_etf?: boolean
realtime_pull_index?: boolean
realtime_index_mode?: 'core' | 'all'
realtime_index_symbols?: string[]
pipeline_pull_a_share: boolean
pipeline_pull_etf: boolean
pipeline_pull_index: boolean
@@ -1645,7 +1641,6 @@ export interface Preferences {
wecom_bot_enabled?: boolean
webhook_enabled_default?: boolean
webhook_default_channels?: string[]
sidebar_index_symbols: string[]
nav_order: string[]
nav_hidden: string[]
screener_auto_run: boolean
@@ -1851,16 +1846,11 @@ export const api = {
method: 'PUT',
body: JSON.stringify({ realtime_quotes_enabled: enabled }),
}),
updateRealtimeQuoteScope: (cfg: Partial<Pick<Preferences, 'realtime_pull_stock' | 'realtime_pull_etf' | 'realtime_pull_index' | 'realtime_index_mode' | 'realtime_index_symbols'>>) =>
updateRealtimeQuoteScope: (cfg: Partial<Pick<Preferences, 'realtime_pull_stock' | 'realtime_pull_etf'>>) =>
request<Partial<Preferences>>('/api/settings/preferences/realtime-quote-scope', {
method: 'PUT',
body: JSON.stringify(cfg),
}),
updateIndicesNavPinned: (pinned: boolean) =>
request<{ indices_nav_pinned: boolean }>('/api/settings/preferences/indices-nav-pinned', {
method: 'PUT',
body: JSON.stringify({ indices_nav_pinned: pinned }),
}),
updateWatchlistGroupsInNav: (enabled: boolean) =>
request<{ watchlist_groups_in_nav: boolean }>('/api/settings/preferences/watchlist-groups-in-nav', {
method: 'PUT',
@@ -1904,7 +1894,6 @@ export const api = {
sse_refresh_pages?: Record<string, boolean>
strategy_monitor_enabled?: boolean
strategy_monitor_ids?: string[]
sidebar_index_symbols?: string[]
screener_auto_run?: boolean
minute_intraday_refresh?: boolean
minute_intraday_refresh_interval?: number
@@ -1914,7 +1903,6 @@ export const api = {
sse_refresh_pages: Record<string, boolean>
strategy_monitor_enabled: boolean
strategy_monitor_ids: string[]
sidebar_index_symbols: string[]
screener_auto_run: boolean
minute_intraday_refresh: boolean
minute_intraday_refresh_interval: number
@@ -2105,11 +2093,6 @@ export const api = {
}>(
`/api/kline/minute-range?symbol=${encodeURIComponent(symbol)}&days=${days}`,
),
indexList: () => request<{ results: IndexInstrument[]; count: number }>('/api/index/list'),
indexSearch: (q: string, limit = 20) =>
request<{ results: IndexInstrument[] }>(
`/api/index/search?q=${encodeURIComponent(q)}&limit=${limit}`,
),
indexDaily: (symbol: string, days = 120, dateRange?: { start: string; end: string }) =>
request<{
symbol: string
@@ -2126,7 +2109,6 @@ export const api = {
request<{
symbol: string
name?: string
index_info?: IndexInstrument
date: string | null
rows: MinuteKlineRow[]
source?: string
+2 -3
View File
@@ -20,7 +20,6 @@ export const QK = {
quoteInterval: ['quote-interval'] as const,
overviewMarket: (asOf?: string) => ['overview-market', asOf ?? 'latest'] as const,
indexQuotes: ['index-quotes'] as const,
indexList: ['index-list'] as const,
// Watchlist
watchlist: ['watchlist'] as const,
@@ -86,9 +85,9 @@ export const QK = {
klineMinuteRange: (symbol: string, days: number) =>
['kline-minute-range', symbol, days] as const,
indexDaily: (symbol: string, start: string, end: string) =>
['index-daily', symbol, start, end] as const,
['index-daily', symbol, start, end] as const,
indexMinute: (symbol: string, date: string) =>
['index-minute', symbol, date] as const,
['index-minute', symbol, date] as const,
// Schema
extDataSchemaAll: ['ext-data-schema-all'] as const,
-1
View File
@@ -162,7 +162,6 @@ export function Data() {
mutationFn: () => api.syncIndexDaily(indexSyncDays),
onSuccess: () => {
qc.invalidateQueries({ queryKey: QK.dataStatus })
qc.invalidateQueries({ queryKey: QK.indexList })
qc.invalidateQueries({ queryKey: QK.indexQuotes })
qc.invalidateQueries({ queryKey: ['index-daily'] })
},
+9 -65
View File
@@ -1,7 +1,7 @@
import { useEffect, useMemo, useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { Activity, Loader2, Lock, RefreshCw, Search } from 'lucide-react'
import { Activity, Loader2, Lock, RefreshCw } from 'lucide-react'
import { api, type IndexInstrument, type KlineRow, type MinuteKlineRow } from '@/lib/api'
import { QK } from '@/lib/queryKeys'
import { useCapabilities } from '@/lib/useSharedQueries'
@@ -61,14 +61,9 @@ const PINNED_INDEXES = [
{ symbol: '000680.SH', name: '科创综指' },
]
function pinnedRank(item: IndexInstrument) {
return PINNED_INDEXES.findIndex(p => item.symbol === p.symbol || item.name === p.name)
}
export function Indices() {
const qc = useQueryClient()
const [searchParams, setSearchParams] = useSearchParams()
const [keyword, setKeyword] = useState('')
const symbolParam = searchParams.get('symbol') ?? ''
const [selected, setSelected] = useState<string>(symbolParam)
const [range, setRange] = useState(defaultRange)
@@ -79,29 +74,12 @@ export function Indices() {
const caps = useCapabilities()
const hasMinuteCap = !!caps.data?.capabilities?.['kline.minute.batch']
const list = useQuery({
queryKey: QK.indexList,
queryFn: api.indexList,
})
// 指数标的固定核心四只 (产品契约, 不再提供全指数搜索/浏览)
const topRows: IndexInstrument[] = PINNED_INDEXES.map(p => ({
symbol: p.symbol, name: p.name, asset_type: 'index' as const,
}))
const search = useQuery({
queryKey: ['index-search', keyword],
queryFn: () => api.indexSearch(keyword, 50),
enabled: keyword.trim().length > 0,
})
const rows: IndexInstrument[] = keyword.trim()
? (search.data?.results ?? [])
: (list.data?.results ?? [])
const topRows = useMemo(() => {
const all = list.data?.results ?? []
return PINNED_INDEXES.map(p => (
all.find(item => item.symbol === p.symbol || item.name === p.name) ?? { symbol: p.symbol, name: p.name, asset_type: 'index' as const }
))
}, [list.data?.results])
const listRows = useMemo(() => rows.filter(item => pinnedRank(item) < 0), [rows])
const selectedSymbol = selected || topRows[0]?.symbol || listRows[0]?.symbol || ''
const selectedSymbol = selected || topRows[0]?.symbol || ''
useEffect(() => {
if (symbolParam && symbolParam !== selected) setSelected(symbolParam)
@@ -132,18 +110,9 @@ export function Indices() {
placeholderData: (prev) => prev,
})
const syncInstruments = useMutation({
mutationFn: api.syncIndexInstruments,
onSuccess: () => {
qc.invalidateQueries({ queryKey: QK.indexList })
qc.invalidateQueries({ queryKey: QK.indexQuotes })
},
})
const syncDaily = useMutation({
mutationFn: () => api.syncIndexDaily(365),
onSuccess: () => {
qc.invalidateQueries({ queryKey: QK.indexList })
qc.invalidateQueries({ queryKey: QK.indexQuotes })
qc.invalidateQueries({ queryKey: ['index-daily'] })
},
@@ -159,7 +128,7 @@ export function Indices() {
const selectedQuotePct = selectedQuote?.change_pct ?? selectedQuote?.pct
const chartRows = useMemo(() => toOHLC(daily.data?.rows ?? []), [daily.data?.rows])
const selectedInfo = [...topRows, ...listRows].find(r => r.symbol === selectedSymbol) || daily.data?.index_info
const selectedInfo = topRows.find(r => r.symbol === selectedSymbol) || daily.data?.index_info
const minuteRows: MinuteKlineRow[] = minute.data?.rows ?? []
const selectedIdx = selectedDate ? chartRows.findIndex(r => r.date === selectedDate) : -1
const prevClose = selectedIdx > 0
@@ -211,14 +180,6 @@ export function Indices() {
</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => syncInstruments.mutate()}
disabled={syncInstruments.isPending}
className="inline-flex items-center gap-1.5 rounded-btn bg-elevated px-3 py-1.5 text-xs text-secondary hover:text-foreground disabled:opacity-50"
>
{syncInstruments.isPending ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <RefreshCw className="h-3.5 w-3.5" />}
</button>
<button
onClick={() => syncDaily.mutate()}
disabled={syncDaily.isPending}
@@ -232,27 +193,10 @@ export function Indices() {
<div className="grid grid-cols-[15rem_1fr] gap-4">
<aside className="rounded-card border border-border bg-surface p-3">
<div className="relative mb-3">
<Search className="pointer-events-none absolute left-2 top-2 h-3.5 w-3.5 text-muted" />
<input
value={keyword}
onChange={e => setKeyword(e.target.value)}
placeholder="搜索指数代码/名称"
className="w-full rounded-btn border border-border bg-base py-1.5 pl-7 pr-2 text-xs text-foreground outline-none focus:border-accent"
/>
</div>
<div className="mb-3 space-y-1 border-b border-border/60 pb-3">
<div className="mb-2 px-1 text-[11px] uppercase tracking-wider text-muted"></div>
<div className="space-y-1">
{topRows.map(renderIndexItem)}
</div>
<div className="max-h-[calc(100vh-24rem)] space-y-1 overflow-auto pr-1">
{(list.isLoading || search.isLoading) && <div className="py-4 text-center text-xs text-muted"></div>}
{!list.isLoading && listRows.length === 0 && (
<div className="rounded-btn bg-elevated p-3 text-xs text-muted">
{keyword.trim() ? '无匹配指数。' : '暂无更多指数,先点击“同步指数列表”。'}
</div>
)}
{listRows.map(renderIndexItem)}
</div>
</aside>
<main className="min-w-0 rounded-card border border-border bg-surface p-3">
+1 -1
View File
@@ -37,6 +37,7 @@ const BUILTIN_PAGES: NavEntry[] = [
{ id: '/backtest', label: '回测', type: 'builtin', visible: true },
{ id: '/mining', label: '挖掘', type: 'builtin', visible: true },
{ id: '/limit-ladder', label: '连板梯队', type: 'builtin', visible: true },
{ id: '/indices', label: '指数', type: 'builtin', visible: true },
{ id: '/concept-analysis', label: '概念分析', type: 'builtin', visible: true },
{ id: '/industry-analysis', label: '行业分析', type: 'builtin', visible: true },
{ id: '/stock-analysis', label: '个股分析', type: 'builtin', visible: true },
@@ -44,7 +45,6 @@ const BUILTIN_PAGES: NavEntry[] = [
{ id: '/abnormal', label: '异动监控', type: 'builtin', visible: true },
{ id: '/review', label: '复盘', type: 'builtin', visible: true },
{ id: '/financials', label: '财务分析', type: 'builtin', visible: true },
{ id: '/indices', label: '指数', type: 'builtin', visible: true },
{ id: '/monitor', label: '监控中心', type: 'builtin', visible: true },
{ id: '/data', label: '数据', type: 'builtin', visible: true },
]
@@ -3,7 +3,6 @@ import { useQueryClient, useMutation, useQuery } from '@tanstack/react-query'
import {
Activity,
Wifi,
BarChart3,
Flame,
Zap,
Webhook,
@@ -33,13 +32,6 @@ const PAGE_LABELS: Record<string, string> = {
'limit-ladder': '连板梯队',
}
const SIDEBAR_INDEX_OPTIONS = [
{ symbol: '000001.SH', name: '上证指数' },
{ symbol: '399001.SZ', name: '深证成指' },
{ symbol: '399006.SZ', name: '创业板指' },
{ symbol: '000680.SH', name: '科创综指' },
]
// ===== 导出为 Panel 组件 (由 Settings.tsx 嵌入) =====
export function SettingsMonitoringPanel({ highlight }: { highlight?: string } = {}) {
@@ -74,8 +66,6 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } =
const rs = refreshStatus.data
// 新建监控规则时默认勾选的推送渠道 (全局默认值数组, 单条规则可独立修改)
const webhookDefaultChannels = prefs?.webhook_default_channels ?? []
const sidebarIndexSymbols = prefs?.sidebar_index_symbols ?? SIDEBAR_INDEX_OPTIONS.map(i => i.symbol)
const indicesPinned = prefs?.indices_nav_pinned ?? true
const isRunning = quoteStatus?.running ?? false
const isTrading = quoteStatus?.is_trading_hours ?? false
// 管道/数据修正运行期间实时行情被临时暂停 — 此时禁止开启
@@ -134,20 +124,6 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } =
qc.invalidateQueries({ queryKey: QK.quoteStatus })
}, [toggleQuote, qc])
const toggleSidebarIndex = useCallback((symbol: string, visible: boolean) => {
const selected = new Set(sidebarIndexSymbols)
if (visible) selected.add(symbol)
else selected.delete(symbol)
const next = SIDEBAR_INDEX_OPTIONS
.map(item => item.symbol)
.filter(s => selected.has(s))
save({ sidebar_index_symbols: next })
}, [save, sidebarIndexSymbols])
const toggleIndicesPin = useCallback((pinned: boolean) => {
api.updateIndicesNavPinned(pinned).then(() => qc.invalidateQueries({ queryKey: QK.preferences }))
}, [qc])
const toggleLimitLadderMonitor = useCallback(async (enabled: boolean) => {
await api.updateLimitLadderMonitor(enabled)
qc.invalidateQueries({ queryKey: QK.preferences })
@@ -386,30 +362,6 @@ export function SettingsMonitoringPanel({ highlight }: { highlight?: string } =
</div>
</Card>
<Card icon={BarChart3} title="左侧菜单指数">
<p className="text-xs text-secondary mb-4">
</p>
<div className="space-y-2">
{SIDEBAR_INDEX_OPTIONS.map(item => (
<ToggleRow
key={item.symbol}
label={item.name}
desc={item.symbol}
checked={sidebarIndexSymbols.includes(item.symbol)}
onChange={(v) => toggleSidebarIndex(item.symbol, v)}
/>
))}
</div>
<div className="mt-3 pt-3 border-t border-border">
<ToggleRow
label="固定显示"
desc={indicesPinned ? '指数卡片常驻显示(即使实时行情关闭)' : '跟随实时行情开关(仅实时开时显示)'}
checked={indicesPinned}
onChange={toggleIndicesPin}
/>
</div>
</Card>
</div>
{/* ========== 右列 ========== */}
+1 -1
View File
@@ -29,9 +29,9 @@ const IndustryAnalysis = lazy(() => import('./pages/IndustryAnalysis').then(m =>
const StockAnalysis = lazy(() => import('./pages/StockAnalysis').then(m => ({ default: m.StockAnalysis })))
const Review = lazy(() => import('./pages/Review').then(m => ({ default: m.Review })))
const LimitUpLadder = lazy(() => import('./pages/LimitUpLadder').then(m => ({ default: m.LimitUpLadder })))
const Indices = lazy(() => import('./pages/Indices').then(m => ({ default: m.Indices })))
const Branding = lazy(() => import('./pages/Branding').then(m => ({ default: m.Branding })))
const Settings = lazy(() => import('./pages/Settings').then(m => ({ default: m.Settings })))
const Indices = lazy(() => import('./pages/Indices').then(m => ({ default: m.Indices })))
const Regime = lazy(() => import('./pages/Regime').then(m => ({ default: m.Regime })))
const AbnormalMoves = lazy(() => import('./pages/AbnormalMoves').then(m => ({ default: m.AbnormalMoves })))
const Dev = lazy(() => import('./pages/Dev').then(m => ({ default: m.Dev })))