feat: 重构 Dashboard 为卡片式分组布局
表格布局替换为按分组展示的卡片式布局,新增 group 字段配置和 TargetBoard/TargetCard 等组件。模态框详情页支持时间范围筛选和分页,SummaryCards 减为 3 个。API 端点变更:trend/history 改用 from/to 参数,history 支持分页。recentSampleCount 硬编码为 30。
This commit is contained in:
@@ -3,6 +3,7 @@ import type {
|
||||
CheckFailure,
|
||||
CheckResult,
|
||||
HealthResponse,
|
||||
HistoryResponse,
|
||||
RuntimeMode,
|
||||
SummaryResponse,
|
||||
TargetStatus,
|
||||
@@ -97,20 +98,47 @@ function handleHistory(idStr: string, url: URL, method: string, store: ProbeStor
|
||||
return jsonResponse(createApiError("Target not found", 404), { method, mode, status: 404 });
|
||||
}
|
||||
|
||||
const limitParam = url.searchParams.get("limit");
|
||||
let limit = 20;
|
||||
const from = url.searchParams.get("from");
|
||||
const to = url.searchParams.get("to");
|
||||
|
||||
if (limitParam !== null) {
|
||||
limit = Number(limitParam);
|
||||
if (!Number.isInteger(limit) || limit <= 0) {
|
||||
return jsonResponse(createApiError("Invalid limit parameter", 400), { method, mode, status: 400 });
|
||||
if (!from || !to) {
|
||||
return jsonResponse(createApiError("from and to parameters are required", 400), { method, mode, status: 400 });
|
||||
}
|
||||
|
||||
const fromDate = new Date(from);
|
||||
const toDate = new Date(to);
|
||||
if (isNaN(fromDate.getTime()) || isNaN(toDate.getTime())) {
|
||||
return jsonResponse(createApiError("Invalid from or to parameter format", 400), { method, mode, status: 400 });
|
||||
}
|
||||
|
||||
const pageParam = url.searchParams.get("page");
|
||||
const pageSizeParam = url.searchParams.get("pageSize");
|
||||
let page = 1;
|
||||
let pageSize = 20;
|
||||
|
||||
if (pageParam !== null) {
|
||||
page = Number(pageParam);
|
||||
if (!Number.isInteger(page) || page <= 0) {
|
||||
return jsonResponse(createApiError("Invalid page parameter", 400), { method, mode, status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
const rows = store.getHistory(id, limit);
|
||||
const results: CheckResult[] = rows.map(mapCheckResult);
|
||||
if (pageSizeParam !== null) {
|
||||
pageSize = Number(pageSizeParam);
|
||||
if (!Number.isInteger(pageSize) || pageSize <= 0) {
|
||||
return jsonResponse(createApiError("Invalid pageSize parameter", 400), { method, mode, status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
return jsonResponse(results, { method, mode });
|
||||
const result = store.getHistory(id, from, to, page, pageSize);
|
||||
const response: HistoryResponse = {
|
||||
items: result.items.map(mapCheckResult),
|
||||
total: result.total,
|
||||
page: result.page,
|
||||
pageSize: result.pageSize,
|
||||
};
|
||||
|
||||
return jsonResponse(response, { method, mode });
|
||||
}
|
||||
|
||||
function handleTrend(idStr: string, url: URL, method: string, store: ProbeStore, mode: RuntimeMode): Response {
|
||||
@@ -125,17 +153,20 @@ function handleTrend(idStr: string, url: URL, method: string, store: ProbeStore,
|
||||
return jsonResponse(createApiError("Target not found", 404), { method, mode, status: 404 });
|
||||
}
|
||||
|
||||
const hoursParam = url.searchParams.get("hours");
|
||||
let hours = 24;
|
||||
const from = url.searchParams.get("from");
|
||||
const to = url.searchParams.get("to");
|
||||
|
||||
if (hoursParam !== null) {
|
||||
hours = Number(hoursParam);
|
||||
if (!Number.isInteger(hours) || hours <= 0) {
|
||||
return jsonResponse(createApiError("Invalid hours parameter", 400), { method, mode, status: 400 });
|
||||
}
|
||||
if (!from || !to) {
|
||||
return jsonResponse(createApiError("from and to parameters are required", 400), { method, mode, status: 400 });
|
||||
}
|
||||
|
||||
const trend: TrendPoint[] = store.getTrend(id, hours).map((row) => ({
|
||||
const fromDate = new Date(from);
|
||||
const toDate = new Date(to);
|
||||
if (isNaN(fromDate.getTime()) || isNaN(toDate.getTime())) {
|
||||
return jsonResponse(createApiError("Invalid from or to parameter format", 400), { method, mode, status: 400 });
|
||||
}
|
||||
|
||||
const trend: TrendPoint[] = store.getTrend(id, from, to).map((row) => ({
|
||||
hour: row.hour,
|
||||
avgDurationMs: row.avgDurationMs,
|
||||
availability: Math.round(row.availability * 100) / 100,
|
||||
@@ -151,7 +182,6 @@ function createSummaryResponse(store: ProbeStore): SummaryResponse {
|
||||
total: summary.total,
|
||||
up: summary.up,
|
||||
down: summary.down,
|
||||
avgDurationMs: summary.avgDurationMs,
|
||||
lastCheckTime: summary.lastCheckTime,
|
||||
};
|
||||
}
|
||||
@@ -162,20 +192,24 @@ function createTargetsResponse(store: ProbeStore): TargetStatus[] {
|
||||
return targets.map((target) => {
|
||||
const latest = store.getLatestCheck(target.id);
|
||||
const stats = store.getTargetStats(target.id);
|
||||
const recentSamples = store.getRecentSamples(target.id, 30);
|
||||
|
||||
return {
|
||||
id: target.id,
|
||||
name: target.name,
|
||||
type: target.type,
|
||||
target: target.target,
|
||||
group: target.grp,
|
||||
interval: formatDuration(target.interval_ms),
|
||||
latestCheck: latest ? mapCheckResult(latest) : null,
|
||||
sparkline: store.getSparkline(target.id),
|
||||
recentSamples: recentSamples.map((s) => ({
|
||||
timestamp: s.timestamp,
|
||||
durationMs: s.duration_ms,
|
||||
up: s.success === 1 && s.matched === 1,
|
||||
})),
|
||||
stats: {
|
||||
totalChecks: stats.totalChecks,
|
||||
availability: stats.availability,
|
||||
avgDurationMs: stats.avgDurationMs,
|
||||
p99DurationMs: stats.p99DurationMs,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user