1
0

feat: 前端指标体系增强 — Dashboard/Metrics API、2×4 统计区、趋势图面积+异常标记、连续状态列

- 新增 GET /api/dashboard 合并原 summary+targets 首屏接口
- 新增 GET /api/targets/:id/metrics 合并原 stats+trend 概览接口
- 后端指标纯函数:可用率、百分位、故障段分析、连续状态、UTC 小时分桶
- ProbeStore 窗口取数方法替代全量历史查询
- SummaryCards 扩展为 4 卡片(新增异常事件数)+ 数据新鲜度展示
- 表格新增「连续」列(Tag 渲染 capped 状态)
- OverviewTab 重构为 2×4 Statistic 多维度布局
- TrendChart 改为延迟范围面积图 + 红色异常标记点
- 删除旧路由(summary/targets/trend)和 computeTrendStats
- 同步 delta specs 到主 specs 并归档变更
This commit is contained in:
2026-05-14 12:32:41 +08:00
parent e983e5d75d
commit 1c5cfafda6
47 changed files with 1768 additions and 1231 deletions

View File

@@ -1,14 +1,13 @@
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useCallback, useState } from "react";
import type { HistoryResponse, TargetStatus, TrendPoint } from "../../shared/api";
import type { HistoryResponse, TargetStatus } from "../../shared/api";
import { subtractHours } from "../utils/time";
import { fetchJson, useTargets } from "./use-queries";
import { fetchJson, useDashboard, useTargetMetrics } from "./use-queries";
const detailQueryKeys = {
history: (targetId: number, from: string, to: string, page: number) => ["history", targetId, from, to, page] as const,
trend: (targetId: number, from: string, to: string) => ["trend", targetId, from, to] as const,
};
export function useTargetDetail() {
@@ -18,28 +17,22 @@ export function useTargetDetail() {
const [timeTo, setTimeTo] = useState("");
const [historyPage, setHistoryPage] = useState(1);
const { data: targetsData } = useTargets();
const { data: dashboardData } = useDashboard();
const selectedTarget =
selectedTargetId !== null ? (targetsData?.find((target) => target.id === selectedTargetId) ?? null) : null;
selectedTargetId !== null
? (dashboardData?.targets.find((target) => target.id === selectedTargetId) ?? null)
: null;
const trend = useQuery({
enabled: selectedTargetId !== null && !!timeFrom && !!timeTo,
queryFn: () =>
fetchJson<TrendPoint[]>(
`/api/targets/${selectedTargetId}/trend?from=${encodeURIComponent(timeFrom)}&to=${encodeURIComponent(timeTo)}`,
),
queryKey:
selectedTargetId !== null && timeFrom && timeTo
? detailQueryKeys.trend(selectedTargetId, timeFrom, timeTo)
: ["trend", "disabled"],
});
const metrics = useTargetMetrics(selectedTargetId, timeFrom, timeTo, "1h");
const history = useQuery({
enabled: selectedTargetId !== null && !!timeFrom && !!timeTo,
queryFn: () =>
fetchJson<HistoryResponse>(
queryFn: () => {
if (selectedTargetId === null) throw new Error("未选择目标");
return fetchJson<HistoryResponse>(
`/api/targets/${selectedTargetId}/history?from=${encodeURIComponent(timeFrom)}&to=${encodeURIComponent(timeTo)}&page=${historyPage}&pageSize=20`,
),
);
},
queryKey:
selectedTargetId !== null && timeFrom && timeTo
? detailQueryKeys.history(selectedTargetId, timeFrom, timeTo, historyPage)
@@ -57,7 +50,7 @@ export function useTargetDetail() {
const closeDrawer = useCallback(() => {
setSelectedTargetId(null);
queryClient.removeQueries({ queryKey: ["trend"] });
queryClient.removeQueries({ queryKey: ["metrics"] });
queryClient.removeQueries({ queryKey: ["history"] });
}, [queryClient]);
@@ -77,11 +70,11 @@ export function useTargetDetail() {
handleTimeChange,
historyData: history.data ?? { items: [], page: 1, pageSize: 20, total: 0 },
historyLoading: history.isLoading,
metricsData: metrics.data ?? null,
metricsLoading: metrics.isLoading,
openDrawer,
selectedTarget,
timeFrom,
timeTo,
trendData: trend.data ?? [],
trendLoading: trend.isLoading,
};
}