diff --git a/frontend/src/components/analysis-shared.tsx b/frontend/src/components/analysis-shared.tsx index 5a1ad33..6680f2b 100644 --- a/frontend/src/components/analysis-shared.tsx +++ b/frontend/src/components/analysis-shared.tsx @@ -12,9 +12,12 @@ import { useMemo, useState } from 'react' import { useQuery } from '@tanstack/react-query' import { motion } from 'framer-motion' import { + AlertCircle, BarChart3, ChevronDown, Database, + DownloadCloud, + RefreshCw, Search, Settings2, Tags, @@ -449,3 +452,50 @@ export function ConfigButton({ onClick }: { onClick: () => void }) { ) } + +/** + * 内置预设 (概念/行业) 数据获取空状态。 + * + * 当检测到内置预设存在但无数据时, 展示图标 + 提示 + 「获取数据」按钮, + * 让用户手动触发拉取 (POST /api/ext-data/presets/{id}/fetch), 而非自动拉取。 + */ +export function PresetFetchState({ + title, + hint, + isLoading, + error, + onFetch, +}: { + title: string + hint: string + isLoading: boolean + error: unknown + onFetch: () => void +}) { + const errMsg = error instanceof Error ? error.message : error ? String(error) : '' + return ( +
+
+ +

{title}

+

{hint}

+ + {errMsg && ( +

+ {errMsg} +

+ )} +
+
+ ) +} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index ad164d4..4e3cb15 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1242,6 +1242,13 @@ export const api = { { method: 'POST' }, ), + // 内置预设 (概念/行业) 手动获取数据: 走结构转换, 保证 schema 一致 + extDataPresetFetch: (id: string) => + request<{ status: string; rows: number }>( + `/api/ext-data/presets/${id}/fetch`, + { method: 'POST' }, + ), + extDataDetectFields: (file: File) => { const fd = new FormData() fd.append('file', file) diff --git a/frontend/src/pages/ConceptAnalysis.tsx b/frontend/src/pages/ConceptAnalysis.tsx index fd8829a..0d3e4ff 100644 --- a/frontend/src/pages/ConceptAnalysis.tsx +++ b/frontend/src/pages/ConceptAnalysis.tsx @@ -1,10 +1,9 @@ import { useMemo, useState, type ReactNode } from 'react' -import { useQuery } from '@tanstack/react-query' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { AnimatePresence } from 'framer-motion' import { Activity, Crown, - Database, Layers3, RefreshCw, Search, @@ -14,7 +13,7 @@ import { } from 'lucide-react' import { PageHeader } from '@/components/PageHeader' import { EmptyState } from '@/components/EmptyState' -import { AnalysisConfigDialog, type AnalysisFieldConfig } from '@/components/analysis-shared' +import { AnalysisConfigDialog, PresetFetchState, type AnalysisFieldConfig } from '@/components/analysis-shared' import { StockPreviewDialog } from '@/components/StockPreviewDialog' import { api, type MarketSnapshotRow } from '@/lib/api' import { QK } from '@/lib/queryKeys' @@ -256,6 +255,21 @@ export function ConceptAnalysis() { enabled: !!activeConfigId, }) + // 内置概念预设 (ext_gn_ths) 手动获取数据 + const PRESET_CONCEPT_ID = 'ext_gn_ths' + const queryClient = useQueryClient() + const fetchMutation = useMutation({ + mutationFn: () => api.extDataPresetFetch(PRESET_CONCEPT_ID), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: QK.extData }) + queryClient.invalidateQueries({ queryKey: QK.extDataRows(PRESET_CONCEPT_ID, undefined, PAGE_LIMIT) }) + }, + }) + // 是否处于「内置概念预设存在但无数据」状态 → 显示获取按钮 + const needsConceptFetch = + !!activeConfig && activeConfig.id === PRESET_CONCEPT_ID && + !rowsQuery.isLoading && (rowsQuery.data?.total ?? 0) === 0 + const marketQuery = useQuery({ queryKey: QK.marketSnapshot, queryFn: api.marketSnapshot, @@ -310,6 +324,7 @@ export function ConceptAnalysis() { } if (!activeConfig) { + // 极端情况: 无任何概念配置。仍提供一键获取内置概念数据入口 return ( <>
@@ -321,7 +336,13 @@ export function ConceptAnalysis() { } /> - + fetchMutation.mutate()} + />
{showConfig && setShowConfig(false)} />} @@ -379,6 +400,14 @@ export function ConceptAnalysis() { ) : rowsQuery.isLoading ? (
正在计算概念强度...
+ ) : needsConceptFetch ? ( + fetchMutation.mutate()} + /> ) : ( )} diff --git a/frontend/src/pages/IndustryAnalysis.tsx b/frontend/src/pages/IndustryAnalysis.tsx index f96362e..ecfe5fb 100644 --- a/frontend/src/pages/IndustryAnalysis.tsx +++ b/frontend/src/pages/IndustryAnalysis.tsx @@ -1,10 +1,9 @@ import { useMemo, useState, type ReactNode } from 'react' -import { useQuery } from '@tanstack/react-query' +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { AnimatePresence } from 'framer-motion' import { Activity, Crown, - Database, Layers3, RefreshCw, Search, @@ -14,7 +13,7 @@ import { } from 'lucide-react' import { PageHeader } from '@/components/PageHeader' import { EmptyState } from '@/components/EmptyState' -import { AnalysisConfigDialog, DimensionHeatmap, type AnalysisFieldConfig } from '@/components/analysis-shared' +import { AnalysisConfigDialog, DimensionHeatmap, PresetFetchState, type AnalysisFieldConfig } from '@/components/analysis-shared' import { StockPreviewDialog } from '@/components/StockPreviewDialog' import { api, type MarketSnapshotRow } from '@/lib/api' import { QK } from '@/lib/queryKeys' @@ -291,6 +290,21 @@ export function IndustryAnalysis() { enabled: !!activeConfigId, }) + // 内置行业预设 (ext_hy_ths) 手动获取数据 + const PRESET_INDUSTRY_ID = 'ext_hy_ths' + const queryClient = useQueryClient() + const fetchMutation = useMutation({ + mutationFn: () => api.extDataPresetFetch(PRESET_INDUSTRY_ID), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: QK.extData }) + queryClient.invalidateQueries({ queryKey: QK.extDataRows(PRESET_INDUSTRY_ID, undefined, PAGE_LIMIT) }) + }, + }) + // 是否处于「内置行业预设存在但无数据」状态 → 显示获取按钮 + const needsIndustryFetch = + !!activeConfig && activeConfig.id === PRESET_INDUSTRY_ID && + !rowsQuery.isLoading && (rowsQuery.data?.total ?? 0) === 0 + const marketQuery = useQuery({ queryKey: QK.marketSnapshot, queryFn: api.marketSnapshot, @@ -361,6 +375,7 @@ export function IndustryAnalysis() { } if (!activeConfig) { + // 极端情况: 无任何行业配置。仍提供一键获取内置行业数据入口 return ( <>
@@ -372,7 +387,13 @@ export function IndustryAnalysis() { } /> - + fetchMutation.mutate()} + />
{showConfig && setShowConfig(false)} showHierarchyLevel />} @@ -443,6 +464,14 @@ export function IndustryAnalysis() { ) : rowsQuery.isLoading ? (
正在计算行业强度...
+ ) : needsIndustryFetch ? ( + fetchMutation.mutate()} + /> ) : ( )}