1
0

refactor: 前端架构重构 — hook拆分、组件拆分、类型筛选器动态化、Meta API

- 后端新增 GET /api/meta 端点(checkerRegistry.supportedTypes)及 MetaResponse 类型
- 前端 hook 拆分为 use-queries.ts(全局查询+useMeta)和 use-target-detail.ts(Drawer状态)
- TargetDetailDrawer 拆分为 OverviewTab + HistoryTab + history-table-columns + stats.ts
- 类型筛选器由 meta API 动态驱动,删除 target-type-display 静态映射
- 列定义改为工厂函数 createTargetTableColumns(checkerTypes),TargetGroup 新增 columns prop
- 修复 StatusDonut key、StatusBar maxSlots prop、TrendChart 移除 loading prop
- 补充 utils/time、utils/stats、动态列工厂测试,删除旧 mapping 测试
- 同步 delta specs 到主 specs,归档 frontend-architecture-refactor change
This commit is contained in:
2026-05-13 20:55:42 +08:00
parent a62007083d
commit 31aeee6d60
41 changed files with 713 additions and 902 deletions

View File

@@ -0,0 +1,42 @@
import type { PrimaryTableCellParams, PrimaryTableCol } from "tdesign-react";
import type { CheckResult } from "../../shared/api";
import { StatusDot } from "../components/StatusDot";
export const HISTORY_COLUMNS: Array<PrimaryTableCol<CheckResult>> = [
{
cell: ({ row }: PrimaryTableCellParams<CheckResult>) => <StatusDot up={!!row.matched} />,
colKey: "matched",
title: "#",
width: 40,
},
{
cell: ({ row }: PrimaryTableCellParams<CheckResult>) => formatTimestamp(row.timestamp),
colKey: "timestamp",
title: "时间",
width: 180,
},
{
align: "center",
cell: ({ row }: PrimaryTableCellParams<CheckResult>) =>
row.durationMs !== null ? Math.round(row.durationMs) : "-",
colKey: "durationMs",
title: "耗时(ms)",
width: 96,
},
{
cell: ({ row }: PrimaryTableCellParams<CheckResult>) => {
const parts = [row.statusDetail, row.failure?.message].filter(Boolean);
return parts.length > 0 ? parts.join("") : "-";
},
colKey: "statusDetail",
title: "详情",
},
];
function formatTimestamp(timestamp: string): string {
const date = new Date(timestamp);
const pad = (value: number) => String(value).padStart(2, "0");
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
}

View File

@@ -7,86 +7,91 @@ import type { TargetStatus } from "../../shared/api";
import { StatusBar } from "../components/StatusBar";
import { StatusDot } from "../components/StatusDot";
import { getAvailabilityProgressColor } from "./color-threshold";
import { statusFilter, typeFilter } from "./target-table-filters";
import { statusFilter } from "./target-table-filters";
import { availabilitySorter, latencySorter, nameSorter } from "./target-table-sorters";
import { getTargetTypeDisplay } from "./target-type-display";
export const TARGET_TABLE_COLUMNS: Array<PrimaryTableCol<TargetStatus>> = [
{
align: "center",
cell: ({ row }: PrimaryTableCellParams<TargetStatus>) => <StatusDot up={!!row.latestCheck?.matched} />,
colKey: "latestCheck.matched",
filter: statusFilter,
fixed: "left",
title: "#",
width: 60,
},
{
colKey: "name",
ellipsis: true,
sorter: nameSorter,
sortType: "all",
title: "名称",
},
{
cell: ({ row }: PrimaryTableCellParams<TargetStatus>) => (
<Tag size="small" theme="primary" variant="light-outline">
{getTargetTypeDisplay(row.type)}
</Tag>
),
colKey: "type",
filter: typeFilter,
title: "类型",
width: 80,
},
{
cell: ({ row }: PrimaryTableCellParams<TargetStatus>) => {
const availability = row.stats?.availability;
if (availability === undefined || availability === null) return "-";
const color = getAvailabilityProgressColor(availability);
return (
<Progress
color={color}
label={`${availability.toFixed(1)}%`}
percentage={availability}
size="small"
theme="line"
/>
);
export function createTargetTableColumns(checkerTypes: string[]): Array<PrimaryTableCol<TargetStatus>> {
return [
{
align: "center",
cell: ({ row }: PrimaryTableCellParams<TargetStatus>) => <StatusDot up={!!row.latestCheck?.matched} />,
colKey: "latestCheck.matched",
filter: statusFilter,
fixed: "left",
title: "#",
width: 60,
},
colKey: "stats.availability",
sorter: availabilitySorter,
sortType: "all",
title: "可用率",
width: 160,
},
{
cell: ({ row }: PrimaryTableCellParams<TargetStatus>) => <StatusBar samples={row.recentSamples} />,
colKey: "recentSamples",
title: "最近状态",
width: 220,
},
{
align: "right",
cell: ({ row }: PrimaryTableCellParams<TargetStatus>) => {
const ms = row.latestCheck?.durationMs;
if (ms === null || ms === undefined) return <span className="text-disabled">-</span>;
const colorClass = ms <= 100 ? "latency-ok" : ms <= 500 ? "latency-warn" : "latency-error";
return <span className={`${colorClass} tabular-nums`}>{Math.round(ms)}ms</span>;
{
colKey: "name",
ellipsis: true,
sorter: nameSorter,
sortType: "all",
title: "名称",
},
colKey: "latestCheck.durationMs",
sorter: latencySorter,
sortType: "all",
title: "延迟",
width: 80,
},
{
align: "center",
colKey: "interval",
title: "间隔",
width: 72,
},
];
{
cell: ({ row }: PrimaryTableCellParams<TargetStatus>) => (
<Tag size="small" theme="primary" variant="light-outline">
{row.type}
</Tag>
),
colKey: "type",
filter: createTypeFilter(checkerTypes),
title: "类型",
width: 80,
},
{
cell: ({ row }: PrimaryTableCellParams<TargetStatus>) => {
const availability = row.stats?.availability;
if (availability === undefined || availability === null) return "-";
const color = getAvailabilityProgressColor(availability);
return (
<Progress
color={color}
label={`${availability.toFixed(1)}%`}
percentage={availability}
size="small"
theme="line"
/>
);
},
colKey: "stats.availability",
sorter: availabilitySorter,
sortType: "all",
title: "可用率",
width: 160,
},
{
cell: ({ row }: PrimaryTableCellParams<TargetStatus>) => <StatusBar samples={row.recentSamples} />,
colKey: "recentSamples",
title: "最近状态",
width: 220,
},
{
align: "right",
cell: ({ row }: PrimaryTableCellParams<TargetStatus>) => {
const ms = row.latestCheck?.durationMs;
if (ms === null || ms === undefined) return <span className="text-disabled">-</span>;
const colorClass = ms <= 100 ? "latency-ok" : ms <= 500 ? "latency-warn" : "latency-error";
return <span className={`${colorClass} tabular-nums`}>{Math.round(ms)}ms</span>;
},
colKey: "latestCheck.durationMs",
sorter: latencySorter,
sortType: "all",
title: "延迟",
width: 80,
},
{
align: "center",
colKey: "interval",
title: "间隔",
width: 72,
},
];
}
export { statusFilter, typeFilter } from "./target-table-filters";
export { availabilitySorter, latencySorter, nameSorter, statusSorter } from "./target-table-sorters";
function createTypeFilter(checkerTypes: string[]): PrimaryTableCol["filter"] {
return {
list: [{ label: "全部", value: "" }, ...checkerTypes.map((type) => ({ label: type, value: type }))],
type: "single",
};
}

View File

@@ -8,12 +8,3 @@ export const statusFilter: PrimaryTableCol["filter"] = {
],
type: "single",
};
export const typeFilter: PrimaryTableCol["filter"] = {
list: [
{ label: "全部", value: "" },
{ label: "HTTP", value: "http" },
{ label: "CMD", value: "command" },
],
type: "single",
};

View File

@@ -1,10 +0,0 @@
export const TARGET_TYPE_DISPLAY = {
command: "CMD",
http: "HTTP",
} as const;
export type TargetType = keyof typeof TARGET_TYPE_DISPLAY;
export function getTargetTypeDisplay(type: string): string {
return TARGET_TYPE_DISPLAY[type as TargetType] || type.toUpperCase();
}