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