feat: 概念/行业分析页数据缺失时显示「获取数据」按钮

数据缺失时引导用户手动触发拉取, 而非静默无数据。

前端:
- analysis-shared.tsx: 新增 PresetFetchState 共享组件 (图标+提示+按钮+错误展示)
- api.ts: 新增 extDataPresetFetch(id) 封装
- ConceptAnalysis/IndustryAnalysis: 两个空状态接入 PresetFetchState,
  新增 fetchMutation + needsXxxFetch 判断 (内置预设存在但无数据时显示按钮)
This commit is contained in:
shy3130
2026-06-28 16:01:46 +08:00
parent 76431b588e
commit 94e871a48b
4 changed files with 123 additions and 8 deletions
@@ -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 }) {
</button>
)
}
/**
* 内置预设 (概念/行业) 数据获取空状态。
*
* 当检测到内置预设存在但无数据时, 展示图标 + 提示 + 「获取数据」按钮,
* 让用户手动触发拉取 (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 (
<div className="h-full grid place-items-center px-8 py-16">
<div className="text-center max-w-md">
<DownloadCloud className="mx-auto h-10 w-10 text-muted" strokeWidth={1.5} />
<h2 className="mt-4 text-base font-medium text-foreground">{title}</h2>
<p className="mt-2 text-sm text-secondary leading-relaxed">{hint}</p>
<button
onClick={onFetch}
disabled={isLoading}
className="mt-5 inline-flex items-center gap-2 rounded-lg bg-accent px-4 py-2 text-sm font-medium text-white transition-colors hover:brightness-110 disabled:opacity-60"
>
{isLoading ? (
<><RefreshCw className="h-4 w-4 animate-spin" /> ...</>
) : (
<><DownloadCloud className="h-4 w-4" /> </>
)}
</button>
{errMsg && (
<p className="mt-3 flex items-center justify-center gap-1.5 text-xs text-bear">
<AlertCircle className="h-3.5 w-3.5" /> {errMsg}
</p>
)}
</div>
</div>
)
}
+7
View File
@@ -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)
+33 -4
View File
@@ -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 (
<>
<div className="flex h-full flex-col">
@@ -321,7 +336,13 @@ export function ConceptAnalysis() {
</button>
}
/>
<EmptyState icon={Database} title="暂无概念数据" hint={'请先在"数据"页面创建包含概念/题材字段的扩展数据源'} />
<PresetFetchState
title="暂无概念数据"
hint="从同花顺获取概念分类数据后即可使用概念分析"
isLoading={fetchMutation.isPending}
error={fetchMutation.error}
onFetch={() => fetchMutation.mutate()}
/>
</div>
<AnimatePresence>
{showConfig && <AnalysisConfigDialog currentConfig={fieldConfig} onSave={handleSaveConfig} onClose={() => setShowConfig(false)} />}
@@ -379,6 +400,14 @@ export function ConceptAnalysis() {
</div>
) : rowsQuery.isLoading ? (
<div className="rounded-2xl border border-border bg-surface px-6 py-16 text-center text-sm text-muted">...</div>
) : needsConceptFetch ? (
<PresetFetchState
title="未获取概念数据"
hint="内置概念数据源已就绪,点击下方按钮从同花顺获取概念分类数据"
isLoading={fetchMutation.isPending}
error={fetchMutation.error}
onFetch={() => fetchMutation.mutate()}
/>
) : (
<EmptyState icon={Layers3} title="未匹配到概念数据" hint={resolved.hint || '请检查扩展数据是否包含概念/题材相关字段'} />
)}
+33 -4
View File
@@ -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 (
<>
<div className="flex h-full flex-col">
@@ -372,7 +387,13 @@ export function IndustryAnalysis() {
</button>
}
/>
<EmptyState icon={Database} title="暂无行业数据" hint={'请先在"数据"页面创建包含行业/板块字段的扩展数据源'} />
<PresetFetchState
title="暂无行业数据"
hint="从同花顺获取行业分类数据后即可使用行业分析"
isLoading={fetchMutation.isPending}
error={fetchMutation.error}
onFetch={() => fetchMutation.mutate()}
/>
</div>
<AnimatePresence>
{showConfig && <AnalysisConfigDialog currentConfig={fieldConfig} onSave={handleSaveConfig} onClose={() => setShowConfig(false)} showHierarchyLevel />}
@@ -443,6 +464,14 @@ export function IndustryAnalysis() {
</div>
) : rowsQuery.isLoading ? (
<div className="rounded-2xl border border-border bg-surface px-6 py-16 text-center text-sm text-muted">...</div>
) : needsIndustryFetch ? (
<PresetFetchState
title="未获取行业数据"
hint="内置行业数据源已就绪,点击下方按钮从同花顺获取行业分类数据"
isLoading={fetchMutation.isPending}
error={fetchMutation.error}
onFetch={() => fetchMutation.mutate()}
/>
) : (
<EmptyState icon={Layers3} title="未匹配到行业数据" hint={resolved.hint || '请检查扩展数据是否包含行业/板块相关字段'} />
)}