1
0

refactor: 全面重构前端 Dashboard 为 TDesign + TanStack Query 分组表格布局

- 卡片式布局改为分组 PrimaryTable,Modal 改为 Drawer
- 手写 hooks 替换为 TanStack Query(轮询/缓存/条件查询)
- CSS 607行精简至73行,颜色迁移至 TDesign tokens
- 可用率进度条颜色按 10% 一档红→绿渐变
- 新增纯函数测试 34 项全通过(排序/筛选/色阶阈值)
- 同步更新主 specs 并归档变更文档
This commit is contained in:
2026-05-12 01:06:53 +08:00
parent 48b40238b8
commit f48e39a615
41 changed files with 1314 additions and 1302 deletions

View File

@@ -0,0 +1,24 @@
const AVAILABILITY_COLORS = [
"#d54941", // 0-10%
"#d96241", // 10-20%
"#e37318", // 20-30%
"#e89318", // 30-40%
"#d9a818", // 40-50%
"#b8b020", // 50-60%
"#8dba30", // 60-70%
"#6dba3f", // 70-80%
"#4dba50", // 80-90%
"#3dba60", // 90-100%
];
export function getAvailabilityProgressColor(availability: number): string {
const index = Math.min(Math.floor(availability / 10), 9);
return AVAILABILITY_COLORS[index]!;
}
export function getLatencyColor(ms: number): string {
if (ms <= 100) return "var(--td-success-color)";
if (ms <= 500) return "var(--td-warning-color)";
return "var(--td-error-color)";
}

View File

@@ -0,0 +1,89 @@
import type { PrimaryTableCol, PrimaryTableCellParams } from "tdesign-react";
import { Tag, Progress } from "tdesign-react";
import type { TargetStatus } from "../../shared/api";
import { StatusDot } from "../components/StatusDot";
import { StatusBar } from "../components/StatusBar";
import { getTargetTypeDisplay } from "./target-type-display";
import { getAvailabilityProgressColor, getLatencyColor } from "./color-threshold";
import { availabilitySorter, latencySorter, nameSorter } from "./target-table-sorters";
import { statusFilter, typeFilter } from "./target-table-filters";
export const TARGET_TABLE_COLUMNS: PrimaryTableCol<TargetStatus>[] = [
{
colKey: "latestCheck.matched",
title: "状态",
width: 80,
fixed: "left",
align: "center",
filter: statusFilter,
cell: ({ row }: PrimaryTableCellParams<TargetStatus>) => <StatusDot up={!!row.latestCheck?.matched} />,
},
{
colKey: "name",
title: "名称",
ellipsis: true,
sorter: nameSorter,
sortType: "all",
},
{
colKey: "type",
title: "类型",
width: 80,
filter: typeFilter,
cell: ({ row }: PrimaryTableCellParams<TargetStatus>) => (
<Tag size="small" theme="primary" variant="light-outline">
{getTargetTypeDisplay(row.type)}
</Tag>
),
},
{
colKey: "stats.availability",
title: "可用率",
width: 160,
sorter: availabilitySorter,
sortType: "all",
cell: ({ row }: PrimaryTableCellParams<TargetStatus>) => {
const availability = row.stats?.availability;
if (availability === undefined || availability === null) return "-";
const color = getAvailabilityProgressColor(availability);
return (
<Progress
theme="line"
size="small"
percentage={availability}
color={color}
label={`${availability.toFixed(1)}%`}
/>
);
},
},
{
colKey: "recentSamples",
title: "最近状态",
width: 220,
cell: ({ row }: PrimaryTableCellParams<TargetStatus>) => <StatusBar samples={row.recentSamples} />,
},
{
colKey: "latestCheck.durationMs",
title: "延迟",
width: 80,
align: "right",
sorter: latencySorter,
sortType: "all",
cell: ({ row }: PrimaryTableCellParams<TargetStatus>) => {
const ms = row.latestCheck?.durationMs;
if (ms === null || ms === undefined) return <span style={{ color: "var(--td-text-color-disabled)" }}>-</span>;
const color = getLatencyColor(ms);
return <span style={{ color, fontVariantNumeric: "tabular-nums" }}>{Math.round(ms)}ms</span>;
},
},
{
colKey: "interval",
title: "间隔",
width: 72,
align: "center",
},
];
export { statusSorter, availabilitySorter, latencySorter, nameSorter } from "./target-table-sorters";
export { statusFilter, typeFilter } from "./target-table-filters";

View File

@@ -0,0 +1,19 @@
import type { PrimaryTableCol } from "tdesign-react";
export const statusFilter: PrimaryTableCol["filter"] = {
type: "single",
list: [
{ label: "全部", value: "" },
{ label: "UP", value: "up" },
{ label: "DOWN", value: "down" },
],
};
export const typeFilter: PrimaryTableCol["filter"] = {
type: "single",
list: [
{ label: "全部", value: "" },
{ label: "HTTP", value: "http" },
{ label: "CMD", value: "command" },
],
};

View File

@@ -0,0 +1,27 @@
import type { TargetStatus } from "../../shared/api";
const STATUS_ORDER: Record<string, number> = {
down: 0,
up: 1,
};
function getStatusRank(target: TargetStatus): number {
if (!target.latestCheck) return 2;
return target.latestCheck.matched ? STATUS_ORDER["up"]! : STATUS_ORDER["down"]!;
}
export function statusSorter(a: TargetStatus, b: TargetStatus): number {
return getStatusRank(a) - getStatusRank(b);
}
export function availabilitySorter(a: TargetStatus, b: TargetStatus): number {
return (a.stats?.availability ?? 0) - (b.stats?.availability ?? 0);
}
export function latencySorter(a: TargetStatus, b: TargetStatus): number {
return (a.latestCheck?.durationMs ?? Infinity) - (b.latestCheck?.durationMs ?? Infinity);
}
export function nameSorter(a: TargetStatus, b: TargetStatus): number {
return a.name.localeCompare(b.name, "zh-CN");
}