import { useState } from 'react' import { useMutation, useQueryClient } from '@tanstack/react-query' import { Loader2, Search, AlertTriangle, CheckCircle2, XCircle, FlaskConical, Activity, Bell } from 'lucide-react' import { PageHeader } from '@/components/PageHeader' import { api } from '@/lib/api' import { cn } from '@/lib/cn' import { resetBadge } from '@/lib/monitorBadge' // ── 分钟K探测 (迁移自 MinuteDataProbe) ───────────────── interface ProbeResult { date: string rows: number source: string ok: boolean } function MinuteProbePanel() { const [symbol, setSymbol] = useState('603261.SH') const [days, setDays] = useState(10) const [loading, setLoading] = useState(false) const [results, setResults] = useState([]) const [error, setError] = useState(null) const runProbe = async () => { const sym = symbol.trim().toUpperCase() if (!sym) return setLoading(true) setError(null) setResults([]) const dates: string[] = [] const today = new Date() for (let i = 0; i < days; i++) { const d = new Date(today) d.setDate(d.getDate() - i) dates.push(d.toISOString().slice(0, 10)) } const out: ProbeResult[] = [] try { for (const date of dates) { const r = await api.klineMinute(sym, date) const rows = r.rows?.length ?? 0 out.push({ date, rows, source: r.source ?? (rows > 0 ? 'local' : 'none'), ok: rows > 0, }) setResults([...out]) } } catch (e: any) { setError(e?.message ?? String(e)) } finally { setLoading(false) } } const total = results.length const hasData = results.filter((r) => r.ok).length const missing = results.filter((r) => !r.ok) return (

分钟K数据探测

逐日调用 /api/kline/minute 接口, 检测每只股票最近若干天的分钟K数据是否齐全。本地无数据时会自动走 TickFlow 实时拉取。

setSymbol(e.target.value)} placeholder="603261.SH" className="w-44 rounded-btn border border-border bg-base px-3 py-1.5 text-sm text-foreground outline-none focus:border-accent" onKeyDown={(e) => e.key === 'Enter' && !loading && runProbe()} />
setDays(Math.max(1, Math.min(30, Number(e.target.value) || 1)))} className="w-24 rounded-btn border border-border bg-base px-3 py-1.5 text-sm text-foreground outline-none focus:border-accent" />
{error && (
{error}
)} {total > 0 && (
检测天数
{total}
有数据
{hasData}
缺失
{missing.length}
)} {results.length > 0 && (
{results.map((r) => ( ))}
日期 分钟K条数 数据来源 状态
{r.date} {r.rows} {r.source} {r.ok ? ( ) : ( 缺失 )}
)} {missing.length > 0 && (
缺失日期的诊断

缺失日期若为周末/节假日属正常; 若为停牌日(成交量为 0)也属正常; 若为正常交易日(日K有成交量)却缺失分钟K, 则是 TickFlow 数据源未提供该日分钟数据。

)}
) } // ── 演示数据生成 ────────────────────────────────────── function SeedPanel() { const qc = useQueryClient() const [count, setCount] = useState(12) const [recent, setRecent] = useState(true) const [msg, setMsg] = useState('') const seedMut = useMutation({ mutationFn: () => api.alertSeed(count, recent), onSuccess: (data) => { setMsg(`已生成 ${data.generated} 条触发记录`) qc.invalidateQueries({ queryKey: ['alerts'] }) qc.invalidateQueries({ queryKey: ['alerts-total'] }) setTimeout(() => setMsg(''), 4000) }, onError: () => { setMsg('生成失败') setTimeout(() => setMsg(''), 4000) }, }) const clearMut = useMutation({ mutationFn: () => api.alertsClear(), onSuccess: (data) => { setMsg(`已清空 ${data.cleared} 条触发记录`) qc.invalidateQueries({ queryKey: ['alerts'] }) qc.invalidateQueries({ queryKey: ['alerts-total'] }) resetBadge() setTimeout(() => setMsg(''), 4000) }, }) const ruleSeedMut = useMutation({ mutationFn: () => api.monitorRuleSeed(), onSuccess: (data) => { setMsg(`已生成 ${data.generated} 条监控规则`) qc.invalidateQueries({ queryKey: ['monitor-rules'] }) setTimeout(() => setMsg(''), 4000) }, onError: () => { setMsg('规则生成失败') setTimeout(() => setMsg(''), 4000) }, }) return (

监控触发记录演示数据

生成模拟的触发记录,用于测试监控中心页面的展示效果、未读徽标、新增闪烁等功能。生成的数据可随时清空。

setCount(Math.max(1, Math.min(50, Number(e.target.value) || 1)))} className="w-24 rounded-btn border border-border bg-base px-3 py-1.5 text-sm text-foreground outline-none focus:border-accent" />
{msg && (
{msg}
)} {/* 监控规则生成 */}

监控规则

生成多种类型的演示监控规则 (个股信号/价格/市场异动/策略变更),用于测试监控中心规则列表展示。

使用说明
  • 勾选「时间戳设为刚刚」后,切到其他页面再回监控中心,新记录会闪烁高亮
  • 生成后菜单「监控中心」会出现红色未读徽标
  • 数据覆盖策略/信号/价格/市场异动四种来源
  • 清空操作不可撤销
) } // ── 封单监控模拟触发 ────────────────────────────────── function LadderTestPanel() { const [result, setResult] = useState> | null>(null) const [error, setError] = useState('') const [pushMsg, setPushMsg] = useState('') const testMut = useMutation({ mutationFn: () => api.monitorRuleTestLadder(), onSuccess: (data) => { setResult(data); setError('') }, onError: (e: any) => { setError(e?.message ?? String(e)); setResult(null) }, }) const triggerMut = useMutation({ mutationFn: () => api.monitorRuleTriggerLadder(), onSuccess: (data) => { setPushMsg(`✅ 已真实触发 ${data.triggered} 条预警 (落盘 + 飞书 + SSE)`) setTimeout(() => setPushMsg(''), 6000) }, onError: (e: any) => { setPushMsg(`❌ 触发失败: ${e?.message ?? String(e)}`) setTimeout(() => setPushMsg(''), 6000) }, }) const fmtVal = (v: number | null | undefined, metric: string) => { if (v == null) return '—' if (metric === 'sealed_amount') return `${(v / 1e8).toFixed(4)} 亿` return `${v.toLocaleString()} 手` } return (

封单监控模拟触发

用当前 depth 封单数据 + 最新日 enriched, 评估所有 ladder 规则。

{result && ( 日期 {result.as_of} · 封单股 {result.sealed_count} · 触发 {result.triggered.length} · 未触发 {result.not_triggered.length} )}
模拟触发:纯条件判断,不落盘不推送。 真实触发:走完整链路(落盘 alerts.jsonl + 推送飞书 + SSE 通知),会在监控中心和飞书看到预警。
{pushMsg && (
{pushMsg}
)} {error && (
{error}
)} {result && result.triggered.length > 0 && (

✅ 会触发 ({result.triggered.length})

{result.triggered.map((ev) => (
{ev.symbol} {ev.name && {ev.name}} {ev.type}
{ev.message}
当前封单: {fmtVal(ev.sealed_metric === 'sealed_amount' ? ev.current_sealed_amount : ev.current_sealed_vol, ev.sealed_metric)}
))}
)} {result && result.not_triggered.length > 0 && (

⚪ 未触发 ({result.not_triggered.length})

{result.not_triggered.map((r) => (
{r.symbol} {r.rule_name}
阈值 {fmtVal(r.threshold, r.metric)} · 当前 {fmtVal(r.current_value, r.metric)} · {r.reason}
))}
)} {result && result.triggered.length === 0 && result.not_triggered.length === 0 && (
无 ladder 监控规则,请先在连板梯队页设置封单监控
)}
) } // ── Dev 主页面 ──────────────────────────────────────── export function Dev() { const [tab, setTab] = useState<'minute' | 'seed' | 'ladder'>('seed') return (
} />
{tab === 'minute' ? : tab === 'ladder' ? : }
) }