1
0

feat: 将 demo 项目转化为 HTTP 拨测监控工具

新增 YAML 配置解析(Bun 内置 YAML)、SQLite 数据存储(bun:sqlite)、按 interval 分组并发拨测引擎、REST API(/api/summary、/api/targets、/api/targets/:id/history、/api/targets/:id/trend)、React 前端 Dashboard(统计卡片、目标表格、可展开详情面板、recharts 趋势图)。CLI 简化为仅接受配置文件路径。移除 /api/demo 路由和相关 demo 代码。保留 /health、静态资源服务和 SPA fallback。
This commit is contained in:
2026-05-09 17:04:25 +08:00
parent 9267f6585c
commit 57d3a5cfb4
43 changed files with 2910 additions and 525 deletions

View File

@@ -1,91 +1,29 @@
import { useEffect, useState } from "react";
import type { DemoResponse } from "../shared/api";
type DemoState =
| { status: "loading" }
| { status: "success"; data: DemoResponse }
| { status: "error"; message: string };
import { useSummary } from "./hooks/useSummary";
import { useTargets } from "./hooks/useTargets";
import { SummaryCards } from "./components/SummaryCards";
import { TargetTable } from "./components/TargetTable";
export function App() {
const [demoState, setDemoState] = useState<DemoState>({ status: "loading" });
const { data: summary, loading: summaryLoading, error: summaryError } = useSummary();
const { data: targets, loading: targetsLoading, error: targetsError } = useTargets();
useEffect(() => {
const abortController = new AbortController();
async function loadDemo() {
try {
const response = await fetch("/api/demo", { signal: abortController.signal });
if (!response.ok) {
throw new Error(`请求失败: ${response.status}`);
}
const data = (await response.json()) as DemoResponse;
setDemoState({ status: "success", data });
} catch (error) {
if (abortController.signal.aborted) return;
setDemoState({
status: "error",
message: error instanceof Error ? error.message : "未知错误",
});
}
}
void loadDemo();
return () => abortController.abort();
}, []);
const error = summaryError || targetsError;
return (
<main className="shell">
<section className="hero" aria-labelledby="page-title">
<p className="eyebrow">Vite + React + Bun</p>
<h1 id="page-title">Gateway Checker Demo</h1>
<p className="summary">Bun API </p>
</section>
<main className="dashboard">
<header className="dashboard-header">
<h1>Gateway Checker</h1>
<p className="dashboard-subtitle">HTTP </p>
</header>
<section className="card" aria-live="polite">
<div className="card-header">
<span className="status-dot" data-state={demoState.status} />
<h2></h2>
{error && (
<div className="error-banner">
: {error}
</div>
)}
{demoState.status === "loading" ? <p> /api/demo...</p> : null}
{demoState.status === "error" ? (
<div className="error">
<strong></strong>
<p>{demoState.message}</p>
</div>
) : null}
{demoState.status === "success" ? (
<div className="result">
<p className="message">{demoState.data.message}</p>
<dl>
<div>
<dt></dt>
<dd>{demoState.data.runtime.mode}</dd>
</div>
<div>
<dt>Bun </dt>
<dd>{demoState.data.runtime.bunVersion}</dd>
</div>
<div>
<dt></dt>
<dd>
{demoState.data.runtime.platform}/{demoState.data.runtime.arch}
</dd>
</div>
<div>
<dt></dt>
<dd>{demoState.data.runtime.timestamp}</dd>
</div>
</dl>
</div>
) : null}
</section>
<SummaryCards summary={summary} loading={summaryLoading} />
<TargetTable targets={targets} loading={targetsLoading} />
</main>
);
}