复制 local 代码到 serve

This commit is contained in:
2026-07-04 16:59:25 +08:00
parent cee04c1b46
commit 0474e5fb46
302 changed files with 78290 additions and 0 deletions
+494
View File
@@ -0,0 +1,494 @@
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<ProbeResult[]>([])
const [error, setError] = useState<string | null>(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 (
<div className="space-y-4">
<div>
<h2 className="text-sm font-semibold text-foreground">K数据探测</h2>
<p className="mt-1 text-xs text-muted">
<code className="px-1 rounded bg-elevated text-secondary">/api/kline/minute</code>
K数据是否齐全 TickFlow
</p>
</div>
<div className="flex flex-wrap items-end gap-3 rounded-btn bg-elevated p-4">
<div className="flex flex-col gap-1">
<label className="text-xs text-muted"></label>
<input
value={symbol}
onChange={(e) => 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()}
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs text-muted"></label>
<input
type="number"
min={1}
max={30}
value={days}
onChange={(e) => 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"
/>
</div>
<button
onClick={runProbe}
disabled={loading || !symbol.trim()}
className="flex items-center gap-1.5 rounded-btn bg-accent px-4 py-1.5 text-sm font-medium text-base hover:bg-accent/90 disabled:opacity-50 cursor-pointer"
>
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Search className="h-4 w-4" />}
{loading ? '探测中…' : '开始探测'}
</button>
</div>
{error && (
<div className="flex items-center gap-2 rounded-btn border border-danger/40 bg-danger/10 p-3 text-sm text-danger">
<AlertTriangle className="h-4 w-4 shrink-0" />
<span>{error}</span>
</div>
)}
{total > 0 && (
<div className="grid grid-cols-3 gap-3">
<div className="rounded-btn bg-elevated p-3">
<div className="text-xs text-muted"></div>
<div className="mt-1 text-lg font-semibold text-foreground">{total}</div>
</div>
<div className="rounded-btn bg-elevated p-3">
<div className="text-xs text-muted"></div>
<div className="mt-1 text-lg font-semibold text-emerald-400">{hasData}</div>
</div>
<div className="rounded-btn bg-elevated p-3">
<div className="text-xs text-muted"></div>
<div className="mt-1 text-lg font-semibold text-danger">{missing.length}</div>
</div>
</div>
)}
{results.length > 0 && (
<div className="overflow-hidden rounded-btn border border-border">
<table className="w-full text-sm">
<thead className="bg-elevated text-xs text-muted">
<tr>
<th className="px-4 py-2 text-left font-medium"></th>
<th className="px-4 py-2 text-right font-medium">K条数</th>
<th className="px-4 py-2 text-left font-medium"></th>
<th className="px-4 py-2 text-center font-medium"></th>
</tr>
</thead>
<tbody>
{results.map((r) => (
<tr key={r.date} className="border-t border-border/60">
<td className="px-4 py-2 text-foreground">{r.date}</td>
<td className="px-4 py-2 text-right tabular-nums text-foreground">{r.rows}</td>
<td className="px-4 py-2 text-secondary">
<span className="rounded bg-elevated px-1.5 py-0.5 text-xs">{r.source}</span>
</td>
<td className="px-4 py-2 text-center">
{r.ok ? (
<span className="inline-flex items-center gap-1 text-emerald-400">
<CheckCircle2 className="h-4 w-4" />
</span>
) : (
<span className="inline-flex items-center gap-1 text-danger">
<XCircle className="h-4 w-4" />
</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{missing.length > 0 && (
<div className="rounded-btn border border-warning/40 bg-warning/10 p-3 text-xs text-foreground">
<div className="mb-1 flex items-center gap-1.5 font-medium text-warning">
<AlertTriangle className="h-4 w-4" />
</div>
<p className="leading-relaxed text-secondary">
<span className="text-foreground">/</span>
<span className="text-foreground"></span> 0
<span className="text-foreground"></span>K有成交量K
TickFlow
</p>
</div>
)}
</div>
)
}
// ── 演示数据生成 ──────────────────────────────────────
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 (
<div className="space-y-4">
<div>
<h2 className="text-sm font-semibold text-foreground"></h2>
<p className="mt-1 text-xs text-muted">
,
</p>
</div>
<div className="space-y-3 rounded-btn bg-elevated p-4">
<div className="flex flex-wrap items-end gap-3">
<div className="flex flex-col gap-1">
<label className="text-xs text-muted"></label>
<input
type="number"
min={1}
max={50}
value={count}
onChange={(e) => 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"
/>
</div>
<label className="flex items-center gap-1.5 pb-1.5">
<input
type="checkbox"
checked={recent}
onChange={(e) => setRecent(e.target.checked)}
className="h-3.5 w-3.5 accent-accent"
/>
<span className="text-xs text-secondary">"刚刚"()</span>
</label>
</div>
<div className="flex flex-wrap gap-2">
<button
onClick={() => seedMut.mutate()}
disabled={seedMut.isPending}
className="flex items-center gap-1.5 rounded-btn bg-accent px-4 py-1.5 text-sm font-medium text-base hover:bg-accent/90 disabled:opacity-50 cursor-pointer"
>
{seedMut.isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : <FlaskConical className="h-4 w-4" />}
</button>
<button
onClick={() => clearMut.mutate()}
disabled={clearMut.isPending}
className="flex items-center gap-1.5 rounded-btn border border-danger/40 bg-danger/10 px-4 py-1.5 text-sm font-medium text-danger hover:bg-danger/20 disabled:opacity-50 cursor-pointer"
>
{clearMut.isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : <XCircle className="h-4 w-4" />}
</button>
</div>
</div>
{msg && (
<div className="rounded-btn border border-accent/40 bg-accent/10 p-3 text-sm text-accent">{msg}</div>
)}
{/* 监控规则生成 */}
<div className="space-y-3 rounded-btn bg-elevated p-4">
<div>
<h3 className="text-sm font-medium text-foreground"></h3>
<p className="mt-0.5 text-xs text-muted">
(///),
</p>
</div>
<div className="flex flex-wrap gap-2">
<button
onClick={() => ruleSeedMut.mutate()}
disabled={ruleSeedMut.isPending}
className="flex items-center gap-1.5 rounded-btn bg-accent px-4 py-1.5 text-sm font-medium text-base hover:bg-accent/90 disabled:opacity-50 cursor-pointer"
>
{ruleSeedMut.isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : <FlaskConical className="h-4 w-4" />}
</button>
</div>
</div>
<div className="rounded-btn border border-border/40 bg-surface/40 p-4 text-xs leading-relaxed text-muted">
<div className="mb-1 font-medium text-secondary">使</div>
<ul className="list-disc space-y-0.5 pl-4">
<li>,,</li>
<li></li>
<li>///</li>
<li></li>
</ul>
</div>
</div>
)
}
// ── 封单监控模拟触发 ──────────────────────────────────
function LadderTestPanel() {
const [result, setResult] = useState<Awaited<ReturnType<typeof api.monitorRuleTestLadder>> | 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 (
<div className="space-y-4">
<div>
<h2 className="text-sm font-semibold text-foreground"></h2>
<p className="mt-1 text-xs text-muted">
depth + enriched, ladder
</p>
</div>
<div className="flex flex-wrap items-center gap-3 rounded-btn bg-elevated p-4">
<button
onClick={() => testMut.mutate()}
disabled={testMut.isPending}
className="flex items-center gap-1.5 rounded-btn bg-accent px-4 py-1.5 text-sm font-medium text-base hover:bg-accent/90 disabled:opacity-50 cursor-pointer"
>
{testMut.isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : <Bell className="h-4 w-4" />}
</button>
<button
onClick={() => {
if (confirm('将真实推送飞书 + 写入监控中心 + 触发 SSE 通知。确认?')) {
triggerMut.mutate()
}
}}
disabled={triggerMut.isPending}
className="flex items-center gap-1.5 rounded-btn border border-amber-400/40 bg-amber-400/10 px-4 py-1.5 text-sm font-medium text-amber-400 hover:bg-amber-400/20 disabled:opacity-50 cursor-pointer"
>
{triggerMut.isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : <Bell className="h-4 w-4" />}
</button>
{result && (
<span className="text-xs text-muted">
{result.as_of} · {result.sealed_count} · {result.triggered.length} · {result.not_triggered.length}
</span>
)}
</div>
<div className="rounded-btn border border-amber-400/30 bg-amber-400/5 p-3 text-xs text-muted">
<span className="font-medium text-amber-400"></span>:,
<span className="font-medium text-amber-400 ml-2"></span>:( alerts.jsonl + + SSE ),
</div>
{pushMsg && (
<div className="rounded-btn border border-accent/40 bg-accent/10 p-3 text-sm text-accent">{pushMsg}</div>
)}
{error && (
<div className="flex items-center gap-2 rounded-btn border border-danger/40 bg-danger/10 p-3 text-sm text-danger">
<AlertTriangle className="h-4 w-4 shrink-0" />
<span>{error}</span>
</div>
)}
{result && result.triggered.length > 0 && (
<div className="space-y-2">
<h3 className="text-sm font-medium text-emerald-400"> ({result.triggered.length})</h3>
{result.triggered.map((ev) => (
<div key={ev.rule_id} className="rounded-btn border border-emerald-400/30 bg-emerald-400/5 p-3 text-sm">
<div className="flex items-center gap-2 mb-1">
<span className="font-medium text-foreground">{ev.symbol}</span>
{ev.name && <span className="text-secondary">{ev.name}</span>}
<span className="ml-auto rounded bg-emerald-400/15 px-1.5 py-0.5 text-xs text-emerald-400">{ev.type}</span>
</div>
<div className="text-xs text-secondary">{ev.message}</div>
<div className="mt-1 text-xs text-muted tabular-nums">
: {fmtVal(ev.sealed_metric === 'sealed_amount' ? ev.current_sealed_amount : ev.current_sealed_vol, ev.sealed_metric)}
</div>
</div>
))}
</div>
)}
{result && result.not_triggered.length > 0 && (
<div className="space-y-2">
<h3 className="text-sm font-medium text-muted"> ({result.not_triggered.length})</h3>
{result.not_triggered.map((r) => (
<div key={r.rule_id} className="rounded-btn border border-border bg-surface/40 p-3 text-sm">
<div className="flex items-center gap-2 mb-1">
<span className="font-medium text-foreground">{r.symbol}</span>
<span className="text-secondary text-xs">{r.rule_name}</span>
</div>
<div className="text-xs text-muted tabular-nums">
{fmtVal(r.threshold, r.metric)} · {fmtVal(r.current_value, r.metric)} · {r.reason}
</div>
</div>
))}
</div>
)}
{result && result.triggered.length === 0 && result.not_triggered.length === 0 && (
<div className="rounded-btn border border-border bg-surface/40 p-4 text-center text-sm text-muted">
ladder ,
</div>
)}
</div>
)
}
// ── Dev 主页面 ────────────────────────────────────────
export function Dev() {
const [tab, setTab] = useState<'minute' | 'seed' | 'ladder'>('seed')
return (
<div className="flex flex-col h-full">
<PageHeader
title="系统工具"
subtitle="数据诊断与预警调试"
right={
<div className="flex items-center gap-1 rounded-btn bg-elevated p-0.5">
<button
onClick={() => setTab('seed')}
className={cn(
'inline-flex items-center gap-1.5 rounded px-2.5 py-1 text-xs font-medium transition-colors cursor-pointer',
tab === 'seed' ? 'bg-surface text-foreground shadow-sm' : 'text-muted hover:text-secondary',
)}
>
<FlaskConical className="h-3.5 w-3.5" />
</button>
<button
onClick={() => setTab('ladder')}
className={cn(
'inline-flex items-center gap-1.5 rounded px-2.5 py-1 text-xs font-medium transition-colors cursor-pointer',
tab === 'ladder' ? 'bg-surface text-foreground shadow-sm' : 'text-muted hover:text-secondary',
)}
>
<Bell className="h-3.5 w-3.5" />
</button>
<button
onClick={() => setTab('minute')}
className={cn(
'inline-flex items-center gap-1.5 rounded px-2.5 py-1 text-xs font-medium transition-colors cursor-pointer',
tab === 'minute' ? 'bg-surface text-foreground shadow-sm' : 'text-muted hover:text-secondary',
)}
>
<Activity className="h-3.5 w-3.5" />K探测
</button>
</div>
}
/>
<div className="flex-1 overflow-auto px-5 py-4">
<div className="mx-auto max-w-3xl space-y-4">
{tab === 'minute' ? <MinuteProbePanel /> : tab === 'ladder' ? <LadderTestPanel /> : <SeedPanel />}
</div>
</div>
</div>
)
}