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()}
+ />
) : (
)}