mirror of
https://github.com/shy3130/tick-stock-panel.git
synced 2026-11-02 10:26:13 +00:00
fix(ui): 看板预热期不再误弹"立即获取数据"引导
v0.3.20 回归: /api/data/status 忙时降级 (977248ce) 在启动指标预热期
(indicators_ready=false, 低配机 50s+) 且 stale 缓存为空时, daily/enriched
返回 null; 看板 hasNoData 判定把 null 当 trading_days=0, 导致升级后有
数据的老用户打开客户端即被误弹"当前暂无数据/立即获取数据"卡片与欢迎
弹窗, 且 staleTime 60s 无轮询, 预热完成后提示也不会自动消失。
修复: 判定提取为 resolveHasNoData — 预热中视为"未知"而非"无数据";
预热期间每 5s 轮询 /status, 预热完成 busy 解除后自动恢复正常判定并停
轮询。老后端无 indicators_ready 字段时行为不变; 新用户引导不受影响。
验证: Dashboard.test.tsx 7 条回归 (修复前 7 failed → 修复后 passed,
覆盖预热 null/预热完成/无字段兼容/部分数据); pnpm build 通过; 全量
vitest 328 passed; 后端契约 test_data_status_busy.py 7 passed。
Signed-off-by: shy3130 <shy3130@qq.com>
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
// 无本地数据判定 — 预热期不得误判"无数据" (v0.3.20 回归)
|
||||
//
|
||||
// 回归复现: 启动时 enriched 指标缓存异步预热 (低配机 50s+), 期间后端
|
||||
// /api/data/status 走 busy 降级 — stale 缓存为空时 daily/enriched 返回 null。
|
||||
// 旧判定把 null 一律当 trading_days=0, 导致升级后有数据的老用户打开客户端
|
||||
// 即被误弹"当前暂无数据 / 立即获取数据"引导卡与欢迎弹窗。
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { resolveHasNoData } from './Dashboard'
|
||||
import type { DataStatus } from '@/lib/api'
|
||||
|
||||
function ds(partial: Partial<DataStatus>): DataStatus {
|
||||
return partial as DataStatus
|
||||
}
|
||||
|
||||
describe('resolveHasNoData', () => {
|
||||
it('status 未返回 → 不判无数据', () => {
|
||||
expect(resolveHasNoData(undefined)).toBe(false)
|
||||
})
|
||||
|
||||
it('预热中 (indicators_ready=false) 两表为 null → 不判无数据 (回归复现)', () => {
|
||||
const s = ds({ indicators_ready: false, daily: undefined, enriched: undefined })
|
||||
expect(resolveHasNoData(s)).toBe(false)
|
||||
})
|
||||
|
||||
it('预热中即使 stale 缓存有旧值也保持未知态 → 不判无数据', () => {
|
||||
const s = ds({
|
||||
indicators_ready: false,
|
||||
daily: { trading_days: 0 } as DataStatus['daily'],
|
||||
enriched: { trading_days: 0 } as DataStatus['enriched'],
|
||||
})
|
||||
expect(resolveHasNoData(s)).toBe(false)
|
||||
})
|
||||
|
||||
it('预热完成且两表交易日为 0 → 判无数据 (新用户正常引导)', () => {
|
||||
const s = ds({
|
||||
indicators_ready: true,
|
||||
daily: { trading_days: 0 } as DataStatus['daily'],
|
||||
enriched: { trading_days: 0 } as DataStatus['enriched'],
|
||||
})
|
||||
expect(resolveHasNoData(s)).toBe(true)
|
||||
})
|
||||
|
||||
it('老版本后端无 indicators_ready 字段: 两表 0 仍判无数据 (兼容)', () => {
|
||||
const s = ds({
|
||||
daily: { trading_days: 0 } as DataStatus['daily'],
|
||||
enriched: { trading_days: 0 } as DataStatus['enriched'],
|
||||
})
|
||||
expect(resolveHasNoData(s)).toBe(true)
|
||||
})
|
||||
|
||||
it('enriched 有交易日 → 有数据', () => {
|
||||
const s = ds({
|
||||
indicators_ready: true,
|
||||
enriched: { trading_days: 243 } as DataStatus['enriched'],
|
||||
daily: undefined,
|
||||
})
|
||||
expect(resolveHasNoData(s)).toBe(false)
|
||||
})
|
||||
|
||||
it('daily 有交易日 (enriched 未出) → 有数据', () => {
|
||||
const s = ds({
|
||||
indicators_ready: true,
|
||||
daily: { trading_days: 240 } as DataStatus['daily'],
|
||||
enriched: undefined,
|
||||
})
|
||||
expect(resolveHasNoData(s)).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -4,7 +4,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { ArrowUpRight, Check, Database, Gauge, GripVertical, Info, Loader2, Play, RefreshCw, RotateCcw, Sparkles, Timer } from 'lucide-react'
|
||||
import { DatePicker } from '@/components/DatePicker'
|
||||
import { api, type AlertEvent } from '@/lib/api'
|
||||
import { api, type AlertEvent, type DataStatus } from '@/lib/api'
|
||||
import { QK } from '@/lib/queryKeys'
|
||||
import { DimensionMembersDialog, type DimensionMembersTarget } from '@/components/DimensionMembersDialog'
|
||||
import { useDataStatus, useCapabilities, useSettings, usePreferences } from '@/lib/useSharedQueries'
|
||||
@@ -20,6 +20,21 @@ import { useDashboardLayout } from '@/components/dashboard/useDashboardLayout'
|
||||
import { DEFAULT_LAYOUT, widgetDef, type WidgetCtx } from '@/components/dashboard/registry'
|
||||
import { cloneItems, GRID_COLS, type WidgetType } from '@/components/dashboard/layout'
|
||||
|
||||
/** 判断是否处于"指标预热中" — 后端 busy 降级会把 status 表统计置 null。 */
|
||||
export function isWarmupPending(ds?: DataStatus): boolean {
|
||||
return ds?.indicators_ready === false
|
||||
}
|
||||
|
||||
/** 无本地数据判定: enriched/daily 交易日均为 0 才算无数据。
|
||||
* 预热中 (indicators_ready=false) 后端 /status 走 busy 降级, stale 缓存为空时
|
||||
* 两表返回 null — 那是"未知"而非"无数据", 必须等预热完成再判; 否则升级后
|
||||
* 有数据的老用户启动预热期即被误弹"立即获取数据" (v0.3.20 回归)。 */
|
||||
export function resolveHasNoData(ds?: DataStatus): boolean {
|
||||
if (!ds) return false
|
||||
if (isWarmupPending(ds)) return false
|
||||
return (ds.enriched?.trading_days ?? 0) === 0 && (ds.daily?.trading_days ?? 0) === 0
|
||||
}
|
||||
|
||||
/** 打开个股预览的来源榜 (用于行高亮与切股导航列表) */
|
||||
type PreviewSource = 'gain' | 'loss' | 'amount' | 'active' | 'concept' | 'industry' | 'alert'
|
||||
|
||||
@@ -59,7 +74,12 @@ export function Dashboard() {
|
||||
}, [dashItems, setDashItems])
|
||||
// 首次使用(无数据 + 未完成引导)自动弹窗: 同一会话只弹一次
|
||||
const [showWelcomeModal, setShowWelcomeModal] = useState(false)
|
||||
const dataStatus = useDataStatus({ staleTime: 60_000 })
|
||||
// 预热中每 5s 轮询: 预热完成后 busy 解除, /status 恢复真实聚合,
|
||||
// 前端才能拿到非 null 的 daily/enriched 结束"未知"态 (staleTime 不拦 interval)
|
||||
const dataStatus = useDataStatus({
|
||||
staleTime: 60_000,
|
||||
refetchInterval: (q: any) => (isWarmupPending(q.state.data) ? 5_000 : false),
|
||||
})
|
||||
const overview = useQuery({
|
||||
queryKey: QK.overviewMarket(selectedDate),
|
||||
queryFn: () => api.overviewMarket(selectedDate),
|
||||
@@ -89,11 +109,10 @@ export function Dashboard() {
|
||||
].find(s => s.name === activeProvider)?.display_name
|
||||
?.replace(/(.*?)|\(.*?\)/g, '').trim() || activeProvider
|
||||
// 无本地数据(enriched/daily 都没有)→ 常驻引导卡片
|
||||
// 注: 后端 status 的 rows 为性能刻意返回 0, 用 trading_days 判断是否有数据
|
||||
// 注: 后端 status 的 rows 为性能刻意返回 0, 用 trading_days 判断是否有数据;
|
||||
// 预热中两表为 null 时不算无数据 (见 resolveHasNoData)
|
||||
const ds = dataStatus.data
|
||||
const hasNoData = !!ds
|
||||
&& (ds.enriched?.trading_days ?? 0) === 0
|
||||
&& (ds.daily?.trading_days ?? 0) === 0
|
||||
const hasNoData = resolveHasNoData(ds)
|
||||
|
||||
// ===== 盘后管道触发(看板内一键获取数据) =====
|
||||
const [fetchJobId, setFetchJobId] = useState<string | null>(null)
|
||||
|
||||
Reference in New Issue
Block a user