概念/行业分析添加日期选择功能
- backend: market_snapshot 支持可选 as_of 参数 - frontend: 两页加 DatePicker,按选定日期查询数据 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -296,12 +296,13 @@ def get_cached(
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/market-snapshot")
|
@router.get("/market-snapshot")
|
||||||
def market_snapshot(request: Request):
|
def market_snapshot(request: Request, as_of: date | None = None):
|
||||||
"""最新全市场轻量行情快照,供板块/概念聚合分析使用。"""
|
"""全市场轻量行情快照(可选指定日期),供板块/概念聚合分析使用。"""
|
||||||
import polars as pl
|
import polars as pl
|
||||||
|
|
||||||
repo = request.app.state.repo
|
repo = request.app.state.repo
|
||||||
svc = ScreenerService(repo)
|
svc = ScreenerService(repo)
|
||||||
|
if as_of is None:
|
||||||
as_of = svc.latest_date()
|
as_of = svc.latest_date()
|
||||||
if not as_of:
|
if not as_of:
|
||||||
return {"as_of": None, "rows": []}
|
return {"as_of": None, "rows": []}
|
||||||
|
|||||||
@@ -882,8 +882,8 @@ export const api = {
|
|||||||
? `/api/screener/cached?ext_columns=${encodeURIComponent(extColumns)}`
|
? `/api/screener/cached?ext_columns=${encodeURIComponent(extColumns)}`
|
||||||
: '/api/screener/cached',
|
: '/api/screener/cached',
|
||||||
),
|
),
|
||||||
marketSnapshot: () =>
|
marketSnapshot: (asOf?: string) =>
|
||||||
request<{ as_of: string | null; rows: MarketSnapshotRow[] }>('/api/screener/market-snapshot'),
|
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}` : ''}`),
|
overviewMarket: (asOf?: string) => request<OverviewMarket>(`/api/overview/market${asOf ? `?as_of=${asOf}` : ''}`),
|
||||||
|
|
||||||
// 概念涨幅轮动矩阵: 每列(日期)各自把所有概念按当天涨幅从高到低排序
|
// 概念涨幅轮动矩阵: 每列(日期)各自把所有概念按当天涨幅从高到低排序
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { PageHeader } from '@/components/PageHeader'
|
|||||||
import { EmptyState } from '@/components/EmptyState'
|
import { EmptyState } from '@/components/EmptyState'
|
||||||
import { type AnalysisFieldConfig } from '@/components/analysis-shared'
|
import { type AnalysisFieldConfig } from '@/components/analysis-shared'
|
||||||
import { StockPreviewDialog } from '@/components/StockPreviewDialog'
|
import { StockPreviewDialog } from '@/components/StockPreviewDialog'
|
||||||
|
import { DatePicker } from '@/components/DatePicker'
|
||||||
import { RpsRotationDialog } from '@/components/RpsRotationDialog'
|
import { RpsRotationDialog } from '@/components/RpsRotationDialog'
|
||||||
import { api, type MarketSnapshotRow } from '@/lib/api'
|
import { api, type MarketSnapshotRow } from '@/lib/api'
|
||||||
import { QK } from '@/lib/queryKeys'
|
import { QK } from '@/lib/queryKeys'
|
||||||
@@ -238,6 +239,7 @@ export function ConceptAnalysis() {
|
|||||||
const [previewSymbol, setPreviewSymbol] = useState<string | null>(null)
|
const [previewSymbol, setPreviewSymbol] = useState<string | null>(null)
|
||||||
const [previewName, setPreviewName] = useState<string>('')
|
const [previewName, setPreviewName] = useState<string>('')
|
||||||
const [showRps, setShowRps] = useState(false)
|
const [showRps, setShowRps] = useState(false)
|
||||||
|
const [asOf, setAsOf] = useState('')
|
||||||
|
|
||||||
const configsQuery = useQuery({ queryKey: QK.extData, queryFn: api.extDataList })
|
const configsQuery = useQuery({ queryKey: QK.extData, queryFn: api.extDataList })
|
||||||
const availableConfigs = configsQuery.data?.items ?? []
|
const availableConfigs = configsQuery.data?.items ?? []
|
||||||
@@ -249,14 +251,14 @@ export function ConceptAnalysis() {
|
|||||||
const activeConfig = availableConfigs.find(c => c.id === activeConfigId)
|
const activeConfig = availableConfigs.find(c => c.id === activeConfigId)
|
||||||
|
|
||||||
const rowsQuery = useQuery({
|
const rowsQuery = useQuery({
|
||||||
queryKey: QK.extDataRows(activeConfigId, undefined, PAGE_LIMIT),
|
queryKey: QK.extDataRows(activeConfigId, asOf || undefined, PAGE_LIMIT),
|
||||||
queryFn: () => api.extDataRows(activeConfigId, { limit: PAGE_LIMIT }),
|
queryFn: () => api.extDataRows(activeConfigId, { date: asOf || undefined, limit: PAGE_LIMIT }),
|
||||||
enabled: !!activeConfigId,
|
enabled: !!activeConfigId,
|
||||||
})
|
})
|
||||||
|
|
||||||
const marketQuery = useQuery({
|
const marketQuery = useQuery({
|
||||||
queryKey: QK.marketSnapshot,
|
queryKey: ['market-snapshot', asOf],
|
||||||
queryFn: api.marketSnapshot,
|
queryFn: () => api.marketSnapshot(asOf || undefined),
|
||||||
staleTime: 60_000,
|
staleTime: 60_000,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -317,9 +319,10 @@ export function ConceptAnalysis() {
|
|||||||
<>
|
<>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="概念分析"
|
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={
|
right={
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
|
<DatePicker value={asOf} onChange={setAsOf} className="w-28" />
|
||||||
{/* RPS 轮动: 打开涨幅轮动矩阵对话框 */}
|
{/* RPS 轮动: 打开涨幅轮动矩阵对话框 */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowRps(true)}
|
onClick={() => setShowRps(true)}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
TrendingDown,
|
TrendingDown,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
import { DatePicker } from '@/components/DatePicker'
|
||||||
import { PageHeader } from '@/components/PageHeader'
|
import { PageHeader } from '@/components/PageHeader'
|
||||||
import { EmptyState } from '@/components/EmptyState'
|
import { EmptyState } from '@/components/EmptyState'
|
||||||
import { DimensionHeatmap, type AnalysisFieldConfig } from '@/components/analysis-shared'
|
import { DimensionHeatmap, type AnalysisFieldConfig } from '@/components/analysis-shared'
|
||||||
@@ -269,6 +270,7 @@ export function IndustryAnalysis() {
|
|||||||
const [sortMode, setSortMode] = useState<SortMode>('heat')
|
const [sortMode, setSortMode] = useState<SortMode>('heat')
|
||||||
const [previewSymbol, setPreviewSymbol] = useState<string | null>(null)
|
const [previewSymbol, setPreviewSymbol] = useState<string | null>(null)
|
||||||
const [previewName, setPreviewName] = useState<string>('')
|
const [previewName, setPreviewName] = useState<string>('')
|
||||||
|
const [asOf, setAsOf] = useState('')
|
||||||
|
|
||||||
const configsQuery = useQuery({ queryKey: QK.extData, queryFn: api.extDataList })
|
const configsQuery = useQuery({ queryKey: QK.extData, queryFn: api.extDataList })
|
||||||
const availableConfigs = configsQuery.data?.items ?? []
|
const availableConfigs = configsQuery.data?.items ?? []
|
||||||
@@ -280,14 +282,14 @@ export function IndustryAnalysis() {
|
|||||||
const activeConfig = availableConfigs.find(c => c.id === activeConfigId)
|
const activeConfig = availableConfigs.find(c => c.id === activeConfigId)
|
||||||
|
|
||||||
const rowsQuery = useQuery({
|
const rowsQuery = useQuery({
|
||||||
queryKey: QK.extDataRows(activeConfigId, undefined, PAGE_LIMIT),
|
queryKey: QK.extDataRows(activeConfigId, asOf || undefined, PAGE_LIMIT),
|
||||||
queryFn: () => api.extDataRows(activeConfigId, { limit: PAGE_LIMIT }),
|
queryFn: () => api.extDataRows(activeConfigId, { date: asOf || undefined, limit: PAGE_LIMIT }),
|
||||||
enabled: !!activeConfigId,
|
enabled: !!activeConfigId,
|
||||||
})
|
})
|
||||||
|
|
||||||
const marketQuery = useQuery({
|
const marketQuery = useQuery({
|
||||||
queryKey: QK.marketSnapshot,
|
queryKey: ['market-snapshot', asOf],
|
||||||
queryFn: api.marketSnapshot,
|
queryFn: () => api.marketSnapshot(asOf || undefined),
|
||||||
staleTime: 60_000,
|
staleTime: 60_000,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -367,7 +369,10 @@ export function IndustryAnalysis() {
|
|||||||
<>
|
<>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="行业分析"
|
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">
|
<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">
|
||||||
|
|||||||
Reference in New Issue
Block a user