From bfe01e06d90cc380009b12b3c1e9ffa3a38fd9c5 Mon Sep 17 00:00:00 2001 From: shy3130 <415333856@qq.com> Date: Mon, 31 Aug 2026 20:34:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(data):=20=E6=97=A0=E9=99=A4=E6=9D=83?= =?UTF-8?q?=E5=9B=A0=E5=AD=90=E8=83=BD=E5=8A=9B=E6=97=B6=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E5=89=8D=E7=BD=AE=E4=BA=8C=E6=AC=A1=E7=A1=AE=E8=AE=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 除权因子不可用时同步静默降级 (日K不复权入库、enriched 直接用 raw 价), 此前无任何提示。现于两个同步入口 (数据页同步按钮、首用欢迎弹窗开始获取) 检测能力矩阵 usable 标志, 不可用先弹确认: 告知不复权口径与除权事件后 需重新校准, 支持不再提示 (localStorage)。矩阵未加载完成时 fail-open 不拦截。 --- frontend/src/components/AdjFactorSyncGate.tsx | 105 ++++++++++++++++++ frontend/src/pages/Dashboard.tsx | 11 +- frontend/src/pages/Data.tsx | 7 +- 3 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/AdjFactorSyncGate.tsx diff --git a/frontend/src/components/AdjFactorSyncGate.tsx b/frontend/src/components/AdjFactorSyncGate.tsx new file mode 100644 index 0000000..eec2aba --- /dev/null +++ b/frontend/src/components/AdjFactorSyncGate.tsx @@ -0,0 +1,105 @@ +import { useState } from 'react' +import { useQuery } from '@tanstack/react-query' +import { motion } from 'framer-motion' +import { AlertTriangle, Info } from 'lucide-react' +import { api } from '@/lib/api' +import { QK } from '@/lib/queryKeys' + +// 无除权因子能力时的同步前置确认。 +// 背景: 除权因子不可用时同步静默降级 —— 日K按不复权价入库、enriched 直接用 +// raw 价算指标 (pipeline 契约), 后续发生除权事件已有数据需重新校准。 +// 该降级无任何运行期提示, 故在同步入口处前置拦截告知一次。 + +const NO_REMIND_KEY = 'tsp_adj_factor_no_remind' + +/** + * 同步入口的除权因子门控。 + * guard(run): 除权因子能力可用 (或矩阵未加载完成 fail-open / 用户已选不再提示) 时直接执行, + * 否则弹二次确认; dialog 需渲染在调用方页面根部。 + */ +export function useAdjFactorSyncGate() { + const matrix = useQuery({ + queryKey: QK.capabilityMatrix, + queryFn: api.capabilityMatrix, + staleTime: 60_000, + }) + const [pending, setPending] = useState<{ run: () => void } | null>(null) + const [noRemind, setNoRemind] = useState(() => { + try { return localStorage.getItem(NO_REMIND_KEY) === '1' } catch { return false } + }) + + const adjRoute = matrix.data?.capabilities.find(c => c.id === 'adj_factor') + // 矩阵未加载完成时不拦截 (fail-open): 首次同步流程不被慢请求卡住 + const adjUsable = matrix.isLoading || !matrix.data ? true : (adjRoute?.usable ?? true) + + const guard = (run: () => void) => { + if (adjUsable || noRemind) { run(); return } + setPending({ run }) + } + + const dialog = pending ? ( +
+ setPending(null)} + /> + +
+
+ +
+
+

当前无除权因子能力

+

+ 本次同步的日K将以不复权价格入库并计算指标, + K 线可能包含除权跳空。后续配置除权因子能力后重新同步, 才能得到前复权口径的数据。 +

+
+ + 若期间发生除权(分红/送转)事件, 已有数据需要重新校准。 +
+ +
+
+
+ + +
+
+
+ ) : null + + return { guard, dialog } +} diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 98405b0..702968a 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -12,6 +12,7 @@ import { useDataStatus, useCapabilities, useSettings, usePreferences } from '@/l import { SealedBadge } from '@/components/SealedBadge' import { StockPreviewDialog } from '@/components/StockPreviewDialog' import { SettingsModal } from '@/components/data/SettingsModal' +import { useAdjFactorSyncGate } from '@/components/AdjFactorSyncGate' import { STAGE_LABELS } from '@/components/data/ActiveJobCard' import { cn } from '@/lib/cn' import { cnSignal } from '@/lib/signals' @@ -628,6 +629,8 @@ export function Dashboard() { const fetchSucceeded = fetchStatus.data?.status === 'succeeded' // 首次使用且无数据 → 自动弹一次引导弹窗(同会话只弹一次) + // 「开始获取」前若无除权因子能力, 先弹前置确认 (adjGate.guard) + const adjGate = useAdjFactorSyncGate() useEffect(() => { if (!hasNoData) return if (settings.data?.onboarding_completed === false) return // 还在引导流程中,不重复弹 @@ -722,12 +725,16 @@ export function Dashboard() { providerLabel={providerLabel} onClose={() => setShowWelcomeModal(false)} onStart={() => { - startFetch.mutate() - setShowWelcomeModal(false) + adjGate.guard(() => { + startFetch.mutate() + setShowWelcomeModal(false) + }) }} /> )} + {/* 无除权因子能力时的同步前置确认 */} + {adjGate.dialog}
diff --git a/frontend/src/pages/Data.tsx b/frontend/src/pages/Data.tsx index c2a88b2..99c122b 100644 --- a/frontend/src/pages/Data.tsx +++ b/frontend/src/pages/Data.tsx @@ -33,6 +33,7 @@ import { useToggleRealtimeQuotes, useUpdateQuoteInterval } from '@/lib/useShared import { MissingCapChip, routeCapUsable, routeProviderDisplay, type RouteCapId } from '@/lib/capability-labels' import { QK } from '@/lib/queryKeys' import { PageHeader } from '@/components/PageHeader' +import { useAdjFactorSyncGate } from '@/components/AdjFactorSyncGate' import { formatScheduleDatePart, formatScheduleTimePart, isToday } from '@/lib/format' // 拆分出的子组件 @@ -105,6 +106,9 @@ export function Data() { }, }) + // 无除权因子能力时同步前置确认 (静默降级告知) + const adjGate = useAdjFactorSyncGate() + const [showClearConfirm, setShowClearConfirm] = useState(false) const clearData = useMutation({ mutationFn: api.dataClear, @@ -581,7 +585,7 @@ export function Data() { 首次使用请点击右侧按钮同步数据 )}