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,84 @@
import type { PrimaryTableCellParams, PrimaryTableCol } from "tdesign-react";
import { describe, expect, test } from "bun:test";
import type { TargetStatus } from "../../../src/shared/api";
import { createTargetTableColumns } from "../../../src/web/constants/target-table-columns";
interface TableFilter {
list?: Array<{ label: string; value: string }>;
type?: string;
}
function getColumn(columns: Array<PrimaryTableCol<TargetStatus>>, colKey: string): PrimaryTableCol<TargetStatus> {
const column = columns.find((item) => item.colKey === colKey);
expect(column).toBeDefined();
return column!;
}
function makeTarget(overrides: Partial<TargetStatus> = {}): TargetStatus {
return {
group: "default",
id: 1,
interval: "5s",
latestCheck: null,
name: "test",
recentSamples: [],
stats: { availability: 100, totalChecks: 0 },
target: "https://example.com",
type: "http",
...overrides,
};
}
describe("createTargetTableColumns", () => {
test("生成 7 个目标表格列", () => {
const columns = createTargetTableColumns(["http", "command"]);
expect(columns.map((column) => column.colKey)).toEqual([
"latestCheck.matched",
"name",
"type",
"stats.availability",
"recentSamples",
"latestCheck.durationMs",
"interval",
]);
});
test("根据 checkerTypes 生成类型筛选器", () => {
const typeColumn = getColumn(createTargetTableColumns(["http", "command", "tcp"]), "type");
const filter = typeColumn.filter as TableFilter;
expect(filter.type).toBe("single");
expect(filter.list).toEqual([
{ label: "全部", value: "" },
{ label: "http", value: "http" },
{ label: "command", value: "command" },
{ label: "tcp", value: "tcp" },
]);
});
test("checkerTypes 为空时只保留全部选项", () => {
const typeColumn = getColumn(createTargetTableColumns([]), "type");
const filter = typeColumn.filter as TableFilter;
expect(filter.list).toEqual([{ label: "全部", value: "" }]);
});
test("类型列直接渲染原始 type 文本", () => {
const typeColumn = getColumn(createTargetTableColumns(["tcp"]), "type");
const renderCell = typeColumn.cell as (params: PrimaryTableCellParams<TargetStatus>) => {
props: { children: unknown };
};
const element = renderCell({
col: typeColumn,
colIndex: 2,
row: makeTarget({ type: "tcp" }),
rowIndex: 0,
});
expect(element.props.children).toBe("tcp");
});
});

View File

@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test";
import { statusFilter, typeFilter } from "../../../src/web/constants/target-table-filters";
import { statusFilter } from "../../../src/web/constants/target-table-filters";
describe("target-table-filters", () => {
describe("statusFilter", () => {
@@ -14,16 +14,4 @@ describe("target-table-filters", () => {
expect(list[2]!.label).toBe("DOWN");
});
});
describe("typeFilter", () => {
test("包含全部选项", () => {
expect(typeFilter).toBeDefined();
expect(typeFilter!.type).toBe("single");
const list = typeFilter!.list!;
expect(list).toHaveLength(3);
expect(list[0]!.label).toBe("全部");
expect(list[1]!.label).toBe("HTTP");
expect(list[2]!.label).toBe("CMD");
});
});
});

View File

@@ -1,40 +0,0 @@
import { describe, expect, test } from "bun:test";
import { getTargetTypeDisplay, TARGET_TYPE_DISPLAY } from "../../../src/web/constants/target-type-display";
describe("target-type-display", () => {
describe("TARGET_TYPE_DISPLAY 常量", () => {
test("定义了 http 类型映射", () => {
expect(TARGET_TYPE_DISPLAY.http).toBe("HTTP");
});
test("定义了 command 类型映射", () => {
expect(TARGET_TYPE_DISPLAY.command).toBe("CMD");
});
});
describe("getTargetTypeDisplay 函数", () => {
test("http 类型返回 HTTP", () => {
expect(getTargetTypeDisplay("http")).toBe("HTTP");
});
test("command 类型返回 CMD", () => {
expect(getTargetTypeDisplay("command")).toBe("CMD");
});
test("未知类型返回大写形式", () => {
expect(getTargetTypeDisplay("tcp")).toBe("TCP");
expect(getTargetTypeDisplay("dns")).toBe("DNS");
expect(getTargetTypeDisplay("grpc")).toBe("GRPC");
});
test("空字符串返回空字符串", () => {
expect(getTargetTypeDisplay("")).toBe("");
});
test("已大写的类型保持大写", () => {
expect(getTargetTypeDisplay("HTTP")).toBe("HTTP");
expect(getTargetTypeDisplay("CMD")).toBe("CMD");
});
});
});

View File

@@ -0,0 +1,29 @@
import { describe, expect, test } from "bun:test";
import type { TrendPoint } from "../../../src/shared/api";
import { computeTrendStats } from "../../../src/web/utils/stats";
describe("computeTrendStats", () => {
test("空趋势返回 0 统计", () => {
expect(computeTrendStats([])).toEqual({ downChecks: 0, totalChecks: 0, upChecks: 0 });
});
test("汇总总检查、正常和异常数量", () => {
const points: TrendPoint[] = [
{ availability: 80, avgDurationMs: 100, hour: "2025-01-01T00:00:00.000Z", totalChecks: 10 },
{ availability: 40, avgDurationMs: 200, hour: "2025-01-01T01:00:00.000Z", totalChecks: 5 },
];
expect(computeTrendStats(points)).toEqual({ downChecks: 5, totalChecks: 15, upChecks: 10 });
});
test("按每个趋势点四舍五入正常数量", () => {
const points: TrendPoint[] = [
{ availability: 33.3, avgDurationMs: null, hour: "2025-01-01T00:00:00.000Z", totalChecks: 3 },
{ availability: 66.7, avgDurationMs: null, hour: "2025-01-01T01:00:00.000Z", totalChecks: 3 },
];
expect(computeTrendStats(points)).toEqual({ downChecks: 3, totalChecks: 6, upChecks: 3 });
});
});

View File

@@ -0,0 +1,29 @@
import { describe, expect, test } from "bun:test";
import { subtractHours } from "../../../src/web/utils/time";
describe("subtractHours", () => {
test("正常扣减小时", () => {
const result = subtractHours(new Date("2025-01-15T12:00:00.000Z"), 3);
expect(result.toISOString()).toBe("2025-01-15T09:00:00.000Z");
});
test("跨天扣减", () => {
const result = subtractHours(new Date("2025-01-15T02:00:00.000Z"), 6);
expect(result.toISOString()).toBe("2025-01-14T20:00:00.000Z");
});
test("跨月扣减", () => {
const result = subtractHours(new Date("2025-03-01T01:00:00.000Z"), 2);
expect(result.toISOString()).toBe("2025-02-28T23:00:00.000Z");
});
test("扣减 0 小时返回相同时间", () => {
const result = subtractHours(new Date("2025-01-15T12:00:00.000Z"), 0);
expect(result.toISOString()).toBe("2025-01-15T12:00:00.000Z");
});
});