mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 16:44:15 +08:00
feat(frontend): 指数页固定核心四只, 删除全指数搜索与侧栏指数配置
- /indices 指数页保留: 左侧固定核心四只(核心指数卡片), 删除搜索框与 全量指数列表; 右侧日K/分时详情与同步指数日K按钮不变 - 侧栏指数条固定四只常驻显示, 点击跳指数详情 - 监控设置"左侧菜单指数"配置卡删除(四选多选+固定显示开关) - api.ts: 删 indexList/indexSearch, Preferences 移除 5 个指数偏好字段; queryKeys 同步清理
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'] })
|
||||
},
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
{/* ========== 右列 ========== */}
|
||||
|
||||
@@ -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 })))
|
||||
|
||||
Reference in New Issue
Block a user