From af97df13499634dd314a536b89a0a95bdc2a6dbb Mon Sep 17 00:00:00 2001 From: shy3130 <415333856@qq.com> Date: Sun, 6 Sep 2026 22:35:30 +0800 Subject: [PATCH] =?UTF-8?q?fix(frontend):=20=E5=88=86=E6=97=B6=E8=BD=AE?= =?UTF-8?q?=E8=AF=A2=E5=81=9C=E8=A1=A8=20=E2=80=94=20=E5=8E=86=E5=8F=B2?= =?UTF-8?q?=E6=97=A5/=E6=94=B6=E7=9B=98=E5=AE=9A=E7=89=88=E5=90=8E?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E6=97=A0=E6=95=88=E8=BD=AE=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 自选个股详情弹窗打开即按 6s 轮询分时接口 (StockPreviewDialog), 但两类 场景下数据不可变, 轮询纯属浪费: 周末/翻看历史日期 (后端回退出的历史日 本地分钟K不变), 以及当日收盘定版后。 新增共享停表工厂 minuteRefetchInterval (kline.ts 单一权威): - source=none / 响应日期 < 北京今天 → 停 - 当日: 收盘根 (≥15:00 的K) 已出现 且 已过 15:30 → 停。 完整性只认收盘根不按根数 —— 实测全天 241 根 (含 09:30 竞价根与 15:00 收盘根), 按根数阈值会在第 240 根 (14:59) 到位时误停, 错过 收盘根; 15:00 后盘后固定价成交可持续到 15:30, 其间量/额仍可能变, 与后端 quote 定版窗口终点 (15:30) 同口径 - 其余 (当日盘中) 照常轮询, 实时性不受影响 StockMultiDayIntradayChart 的最新日查询此前无条件轮询 (无 source=none 停表), 一并接入。判定扫全行取时间标签, 不依赖后端排序契约。 验证: pnpm build 通过。 --- .../src/components/StockIntradayChart.tsx | 4 +- .../components/StockMultiDayIntradayChart.tsx | 4 +- frontend/src/lib/kline.ts | 53 +++++++++++++++++++ 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/StockIntradayChart.tsx b/frontend/src/components/StockIntradayChart.tsx index ba6aed9..839ce01 100644 --- a/frontend/src/components/StockIntradayChart.tsx +++ b/frontend/src/components/StockIntradayChart.tsx @@ -3,7 +3,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { Loader2 } from 'lucide-react' import { api, type MinuteKlineRow } from '@/lib/api' import { QK } from '@/lib/queryKeys' -import { klineMinuteQueryOptions } from '@/lib/kline' +import { klineMinuteQueryOptions, minuteRefetchInterval } from '@/lib/kline' import { EChartsIntraday } from '@/components/EChartsIntraday' interface Props { @@ -40,7 +40,7 @@ export function StockIntradayChart({ // 避免读到分钟增量落盘的上一轮本地分区; 历史日期后端自行忽略 live。 ...klineMinuteQueryOptions(symbol, date ?? undefined, refetchIntervalMs != null), enabled: !!symbol && !!date, - refetchInterval: query => query.state.data?.source === 'none' ? false : refetchIntervalMs, + refetchInterval: minuteRefetchInterval(refetchIntervalMs), }) const fetchMinute = useMutation({ diff --git a/frontend/src/components/StockMultiDayIntradayChart.tsx b/frontend/src/components/StockMultiDayIntradayChart.tsx index 6f71c85..a108798 100644 --- a/frontend/src/components/StockMultiDayIntradayChart.tsx +++ b/frontend/src/components/StockMultiDayIntradayChart.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { Download, Loader2, RefreshCw } from 'lucide-react' import { api, type MinuteKlineSession } from '@/lib/api' -import { klineMinuteQueryOptions, klineMinuteRangeQueryOptions } from '@/lib/kline' +import { klineMinuteQueryOptions, klineMinuteRangeQueryOptions, minuteRefetchInterval } from '@/lib/kline' import { toast } from '@/components/Toast' import { EChartsMultiDayIntraday } from '@/components/EChartsMultiDayIntraday' @@ -36,7 +36,7 @@ export function StockMultiDayIntradayChart({ // live: 当日盘中直接实时拉取, 不被分钟增量落盘的本地分区(≥60s一轮)拖慢 ...klineMinuteQueryOptions(symbol, undefined, true), enabled: !!symbol, - refetchInterval: refetchIntervalMs, + refetchInterval: minuteRefetchInterval(refetchIntervalMs), }) const sessions = useMemo(() => { diff --git a/frontend/src/lib/kline.ts b/frontend/src/lib/kline.ts index 6bc7ac1..8d5e430 100644 --- a/frontend/src/lib/kline.ts +++ b/frontend/src/lib/kline.ts @@ -70,6 +70,59 @@ export function klineMinuteQueryOptions(symbol: string, date?: string, live?: bo } } +/** 收盘分钟标签: 连续竞价全天最后一根分钟K的时间。 */ +const CLOSE_MINUTE = '15:00' + +/** 全天真正定版时刻: 15:00 收盘后仍可按收盘价成交 (盘后固定价) 至 15:30, + * 其间成交量/额仍可能变化 —— 与后端 quote_service 定版重试窗口终点 + * (15:30) 同口径; 盘后管道默认 15:35 在此之后启动。 */ +const MARKET_ALL_OVER = '15:30' + +/** 北京时间今日 YYYY-MM-DD (与后端 cn_today 同口径; 不用 UTC toISOString, 避免傍晚错日)。 */ +function cnToday(): string { + return new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Shanghai' }).format(new Date()) +} + +/** 北京时间此刻 'HH:MM'。 */ +function cnNowHHMM(): string { + return new Intl.DateTimeFormat('en-GB', { timeZone: 'Asia/Shanghai', hour: '2-digit', minute: '2-digit', hour12: false }).format(new Date()) +} + +/** 行 datetime ('YYYY-MM-DD HH:MM:SS' 北京墙钟契约) → 'HH:MM'; 非常规格式返回 ''。 */ +function rowMinute(r: { datetime?: string } | undefined): string { + const s = String(r?.datetime ?? '') + return s.length >= 16 ? s.slice(11, 16) : '' +} + +/** + * 分时轮询停表 — 数据不可变时停止, 消除无效请求 (周末/盘后/翻看历史日期)。 + * + * - 未传间隔 → 不轮询 (调用方默认关闭) + * - 无数据 (source=none) → 停 + * - 响应日期 < 北京今天 → 停: 历史日的本地分钟K不可变 (周末"最新"会被 + * 后端回退到上一交易日, 同样命中此规则) + * - 当日: 收盘根 (≥15:00 的K) 已出现 **且** 已过 15:30 → 停。 + * 完整性**只认收盘根, 不按根数** —— 实测本系统全天 241 根 (首根 09:30 + * 竞价根 + 15:00 收盘根), 按根数阈值会在第 240 根 (14:59) 到位时误停, + * 恰好错过 15:00 收盘根; 收盘根判据还天然兼容盘中缺根与部分交易日。 + * 而 15:00 收盘根出现后**不能立即停**: 盘后按收盘价成交可持续到 15:30, + * 其间量/额仍可能更新 (当前数据源分钟K止于 15:00, 此窗口为防御性保留, + * 与后端"15:30 才允许定版/重建"的边界一致)。 + * - 其余 (当日 15:30 前) → 按间隔继续 + */ +export function minuteRefetchInterval(ms?: number) { + return (query: any): number | false => { + if (ms == null) return false + const d = query.state.data + if (!d) return ms + if (d.source === 'none') return false + if (typeof d.date === 'string' && d.date < cnToday()) return false + const closeBarArrived = (d.rows ?? []).some((r: { datetime?: string }) => rowMinute(r) >= CLOSE_MINUTE) + if (closeBarArrived && cnNowHHMM() >= MARKET_ALL_OVER) return false + return ms + } +} + /** 多日分时查询配置 — 分时 tab 的 StockMultiDayIntradayChart 与 邻近预取 共用。 * 内嵌「仅同 symbol 占位」守卫 (key 结构 ['kline-minute-range', symbol, days], index 1 为 symbol), * 与 klineDailyQueryOptions 同源, 调用点不再各自手写。 */