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() {
首次使用请点击右侧按钮同步数据
)}