概念/行业分析添加日期选择功能

- backend: market_snapshot 支持可选 as_of 参数
- frontend: 两页加 DatePicker,按选定日期查询数据

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-06 14:58:31 +08:00
parent 4cd74b06f1
commit 267b8557e8
4 changed files with 24 additions and 15 deletions
+3 -2
View File
@@ -296,12 +296,13 @@ def get_cached(
@router.get("/market-snapshot")
def market_snapshot(request: Request):
"""最新全市场轻量行情快照,供板块/概念聚合分析使用。"""
def market_snapshot(request: Request, as_of: date | None = None):
"""全市场轻量行情快照(可选指定日期),供板块/概念聚合分析使用。"""
import polars as pl
repo = request.app.state.repo
svc = ScreenerService(repo)
if as_of is None:
as_of = svc.latest_date()
if not as_of:
return {"as_of": None, "rows": []}
+2 -2
View File
@@ -882,8 +882,8 @@ export const api = {
? `/api/screener/cached?ext_columns=${encodeURIComponent(extColumns)}`
: '/api/screener/cached',
),
marketSnapshot: () =>
request<{ as_of: string | null; rows: MarketSnapshotRow[] }>('/api/screener/market-snapshot'),
marketSnapshot: (asOf?: string) =>
request<{ as_of: string | null; rows: MarketSnapshotRow[] }>(`/api/screener/market-snapshot${asOf ? `?as_of=${asOf}` : ''}`),
overviewMarket: (asOf?: string) => request<OverviewMarket>(`/api/overview/market${asOf ? `?as_of=${asOf}` : ''}`),
// 概念涨幅轮动矩阵: 每列(日期)各自把所有概念按当天涨幅从高到低排序
+8 -5
View File
@@ -15,6 +15,7 @@ import { PageHeader } from '@/components/PageHeader'
import { EmptyState } from '@/components/EmptyState'
import { type AnalysisFieldConfig } from '@/components/analysis-shared'
import { StockPreviewDialog } from '@/components/StockPreviewDialog'
import { DatePicker } from '@/components/DatePicker'
import { RpsRotationDialog } from '@/components/RpsRotationDialog'
import { api, type MarketSnapshotRow } from '@/lib/api'
import { QK } from '@/lib/queryKeys'
@@ -238,6 +239,7 @@ export function ConceptAnalysis() {
const [previewSymbol, setPreviewSymbol] = useState<string | null>(null)
const [previewName, setPreviewName] = useState<string>('')
const [showRps, setShowRps] = useState(false)
const [asOf, setAsOf] = useState('')
const configsQuery = useQuery({ queryKey: QK.extData, queryFn: api.extDataList })
const availableConfigs = configsQuery.data?.items ?? []
@@ -249,14 +251,14 @@ export function ConceptAnalysis() {
const activeConfig = availableConfigs.find(c => c.id === activeConfigId)
const rowsQuery = useQuery({
queryKey: QK.extDataRows(activeConfigId, undefined, PAGE_LIMIT),
queryFn: () => api.extDataRows(activeConfigId, { limit: PAGE_LIMIT }),
queryKey: QK.extDataRows(activeConfigId, asOf || undefined, PAGE_LIMIT),
queryFn: () => api.extDataRows(activeConfigId, { date: asOf || undefined, limit: PAGE_LIMIT }),
enabled: !!activeConfigId,
})
const marketQuery = useQuery({
queryKey: QK.marketSnapshot,
queryFn: api.marketSnapshot,
queryKey: ['market-snapshot', asOf],
queryFn: () => api.marketSnapshot(asOf || undefined),
staleTime: 60_000,
})
@@ -317,9 +319,10 @@ export function ConceptAnalysis() {
<>
<PageHeader
title="概念分析"
subtitle={`${marketQuery.data?.as_of ?? rowsQuery.data?.date ?? '最新'} · ${stats.length} 个概念 · ${totalSymbols} 只标的`}
subtitle={`${asOf || marketQuery.data?.as_of || rowsQuery.data?.date || '最新'} · ${stats.length} 个概念 · ${totalSymbols} 只标的`}
right={
<div className="flex items-center gap-1">
<DatePicker value={asOf} onChange={setAsOf} className="w-28" />
{/* RPS 轮动: 打开涨幅轮动矩阵对话框 */}
<button
onClick={() => setShowRps(true)}
+10 -5
View File
@@ -9,6 +9,7 @@ import {
TrendingDown,
TrendingUp,
} from 'lucide-react'
import { DatePicker } from '@/components/DatePicker'
import { PageHeader } from '@/components/PageHeader'
import { EmptyState } from '@/components/EmptyState'
import { DimensionHeatmap, type AnalysisFieldConfig } from '@/components/analysis-shared'
@@ -269,6 +270,7 @@ export function IndustryAnalysis() {
const [sortMode, setSortMode] = useState<SortMode>('heat')
const [previewSymbol, setPreviewSymbol] = useState<string | null>(null)
const [previewName, setPreviewName] = useState<string>('')
const [asOf, setAsOf] = useState('')
const configsQuery = useQuery({ queryKey: QK.extData, queryFn: api.extDataList })
const availableConfigs = configsQuery.data?.items ?? []
@@ -280,14 +282,14 @@ export function IndustryAnalysis() {
const activeConfig = availableConfigs.find(c => c.id === activeConfigId)
const rowsQuery = useQuery({
queryKey: QK.extDataRows(activeConfigId, undefined, PAGE_LIMIT),
queryFn: () => api.extDataRows(activeConfigId, { limit: PAGE_LIMIT }),
queryKey: QK.extDataRows(activeConfigId, asOf || undefined, PAGE_LIMIT),
queryFn: () => api.extDataRows(activeConfigId, { date: asOf || undefined, limit: PAGE_LIMIT }),
enabled: !!activeConfigId,
})
const marketQuery = useQuery({
queryKey: QK.marketSnapshot,
queryFn: api.marketSnapshot,
queryKey: ['market-snapshot', asOf],
queryFn: () => api.marketSnapshot(asOf || undefined),
staleTime: 60_000,
})
@@ -367,7 +369,10 @@ export function IndustryAnalysis() {
<>
<PageHeader
title="行业分析"
subtitle={`${industryLevelLabel} · ${marketQuery.data?.as_of ?? rowsQuery.data?.date ?? '最新'} · ${stats.length} 个行业 · ${totalSymbols} 只标的`}
subtitle={`${industryLevelLabel} · ${asOf || marketQuery.data?.as_of || rowsQuery.data?.date || '最新'} · ${stats.length} 个行业 · ${totalSymbols} 只标的`}
right={
<DatePicker value={asOf} onChange={setAsOf} className="w-28" />
}
/>
<div className="min-h-full bg-[radial-gradient(circle_at_12%_0%,rgba(245,158,11,0.12),transparent_28%),radial-gradient(circle_at_85%_8%,rgba(244,63,94,0.08),transparent_28%)] px-6 py-5">