1
0

chore: 强化代码质量与风格检查体系

ESLint 升级到 recommended-type-checked + stylistic-type-checked,
引入 perfectionist 导入排序和 import 插件导入验证。

Prettier 显式声明全部格式化参数,消除跨环境差异。
TypeScript 启用 noUnusedLocals 和 noPropertyAccessFromIndexSignature。
完善 ignore 列表,排除 .agents/、bun.lock、data/ 等。
引入 husky + lint-staged(pre-commit)+ commitlint(commit-msg)。
更新 DEVELOPMENT.md 代码质量章节。
修复所有新增规则检测到的类型和风格违规。
This commit is contained in:
2026-05-12 18:44:59 +08:00
parent ce8baae3d1
commit a5cf6065c2
83 changed files with 2654 additions and 1824 deletions

View File

@@ -1,25 +1,25 @@
import { Tag, Typography } from "tdesign-react";
interface GroupHeaderProps {
down: number;
name: string;
total: number;
up: number;
down: number;
}
export function GroupHeader({ name, total, up, down }: GroupHeaderProps) {
export function GroupHeader({ down, name, total, up }: GroupHeaderProps) {
const displayName = name === "default" ? "默认分组" : name;
return (
<div className="group-header">
<Typography.Title level="h4">{displayName}</Typography.Title>
<Tag theme="primary" variant="light" title="总数">
<Tag theme="primary" title="总数" variant="light">
{total}
</Tag>
<Tag theme="success" variant="light" title="正常">
<Tag theme="success" title="正常" variant="light">
{up}
</Tag>
<Tag theme="danger" variant="light" title="异常">
<Tag theme="danger" title="异常" variant="light">
{down}
</Tag>
</div>

View File

@@ -9,12 +9,12 @@ export function StatusBar({ samples }: StatusBarProps) {
if (sample) {
blocks.push(
<span
key={i}
className={`status-bar-block ${sample.up ? "status-bar-block--up" : "status-bar-block--down"}`}
key={i}
/>,
);
} else {
blocks.push(<span key={i} className="status-bar-block status-bar-block--empty" />);
blocks.push(<span className="status-bar-block status-bar-block--empty" key={i} />);
}
}

View File

@@ -1,15 +1,15 @@
import { PieChart, Pie, Cell, ResponsiveContainer } from "recharts";
import { Cell, Pie, PieChart, ResponsiveContainer } from "recharts";
interface StatusDonutProps {
up: number;
down: number;
up: number;
}
const UP_COLOR = "var(--td-success-color)";
const DOWN_COLOR = "var(--td-error-color)";
const EMPTY_COLOR = "var(--td-bg-color-component-disabled)";
export function StatusDonut({ up, down }: StatusDonutProps) {
export function StatusDonut({ down, up }: StatusDonutProps) {
const total = up + down;
const availability = total > 0 ? ((up / total) * 100).toFixed(1) : "-";
@@ -25,11 +25,11 @@ export function StatusDonut({ up, down }: StatusDonutProps) {
return (
<div className="status-donut">
<ResponsiveContainer width="100%" height={180}>
<ResponsiveContainer height={180} width="100%">
<PieChart>
<Pie data={data} cx="50%" cy="50%" innerRadius={50} outerRadius={70} dataKey="value" stroke="none">
<Pie cx="50%" cy="50%" data={data} dataKey="value" innerRadius={50} outerRadius={70} stroke="none">
{data.map((_, index) => (
<Cell key={index} fill={colors[index % colors.length]!} />
<Cell fill={colors[index % colors.length]} key={index} />
))}
</Pie>
</PieChart>

View File

@@ -1,25 +1,26 @@
import { Row, Col, Card, Statistic } from "tdesign-react";
import { Card, Col, Row, Statistic } from "tdesign-react";
import type { SummaryResponse } from "../../shared/api";
interface SummaryCardsProps {
summary: SummaryResponse | null;
summary: null | SummaryResponse;
}
export function SummaryCards({ summary }: SummaryCardsProps) {
if (!summary) return null;
const cards = [
{ label: "全部目标", value: summary.total, color: "blue" as const },
{ label: "正常", value: summary.up, color: "green" as const },
{ label: "异常", value: summary.down, color: "red" as const },
{ color: "blue" as const, label: "全部目标", value: summary.total },
{ color: "green" as const, label: "正常", value: summary.up },
{ color: "red" as const, label: "异常", value: summary.down },
];
return (
<Row gutter={16} className="summary-cards-row">
<Row className="summary-cards-row" gutter={16}>
{cards.map((card) => (
<Col key={card.label} span={4}>
<Card bordered>
<Statistic title={card.label} value={card.value} color={card.color} />
<Statistic color={card.color} title={card.label} value={card.value} />
</Card>
</Col>
))}

View File

@@ -1,13 +1,15 @@
import { Space } from "tdesign-react";
import type { TargetStatus } from "../../shared/api";
import { TargetGroup } from "./TargetGroup";
interface TargetBoardProps {
targets: TargetStatus[];
onTargetClick: (target: TargetStatus) => void;
targets: TargetStatus[];
}
export function TargetBoard({ targets, onTargetClick }: TargetBoardProps) {
export function TargetBoard({ onTargetClick, targets }: TargetBoardProps) {
const groups = new Map<string, TargetStatus[]>();
for (const target of targets) {
const group = target.group;
@@ -25,9 +27,9 @@ export function TargetBoard({ targets, onTargetClick }: TargetBoardProps) {
});
return (
<Space direction="vertical" size={32} className="full-width">
<Space className="full-width" direction="vertical" size={32}>
{sortedGroups.map(([name, groupTargets]) => (
<TargetGroup key={name} name={name} targets={groupTargets} onTargetClick={onTargetClick} />
<TargetGroup key={name} name={name} onTargetClick={onTargetClick} targets={groupTargets} />
))}
</Space>
);

View File

@@ -1,96 +1,99 @@
import { useState, useCallback } from "react";
import type { TabValue } from "tdesign-react";
import { useCallback, useState } from "react";
import {
Drawer,
Tabs,
RadioGroup,
DateRangePicker,
Tag,
Row,
Col,
Statistic,
DateRangePicker,
Descriptions,
Skeleton,
PrimaryTable,
Divider,
Drawer,
PrimaryTable,
RadioGroup,
Row,
Skeleton,
Space,
Statistic,
Tabs,
Tag,
Typography,
} from "tdesign-react";
import type { TabValue } from "tdesign-react";
import type { CheckResult, TargetStatus, TrendPoint, HistoryResponse } from "../../shared/api";
import { StatusDot } from "./StatusDot";
import { StatusDonut } from "./StatusDonut";
import { TrendChart } from "./TrendChart";
import type { CheckResult, HistoryResponse, TargetStatus, TrendPoint } from "../../shared/api";
import { getTargetTypeDisplay } from "../constants/target-type-display";
import { subtractHours } from "../utils/time";
import { StatusDonut } from "./StatusDonut";
import { StatusDot } from "./StatusDot";
import { TrendChart } from "./TrendChart";
interface TargetDetailDrawerProps {
target: TargetStatus | null;
trendData: TrendPoint[];
trendLoading: boolean;
historyData: HistoryResponse;
historyLoading: boolean;
onClose: () => void;
onPageChange: (page: number) => void;
onTimeChange: (from: string, to: string) => void;
target: null | TargetStatus;
timeFrom: string;
timeTo: string;
onTimeChange: (from: string, to: string) => void;
onPageChange: (page: number) => void;
onClose: () => void;
trendData: TrendPoint[];
trendLoading: boolean;
}
const TIME_SHORTCUTS = [
{ label: "1小时", hours: 1, value: "1h" },
{ label: "6小时", hours: 6, value: "6h" },
{ label: "24小时", hours: 24, value: "24h" },
{ label: "7天", hours: 168, value: "7d" },
{ hours: 1, label: "1小时", value: "1h" },
{ hours: 6, label: "6小时", value: "6h" },
{ hours: 24, label: "24小时", value: "24h" },
{ hours: 168, label: "7天", value: "7d" },
] as const;
const HISTORY_COLUMNS = [
{
cell: ({ row }: { col: unknown; colIndex: number; row: CheckResult; rowIndex: number }) => (
<StatusDot up={!!row.matched} />
),
colKey: "matched",
title: "#",
width: 40,
cell: ({ row }: { row: CheckResult; rowIndex: number; col: unknown; colIndex: number }) => (
<StatusDot up={!!row.matched} />
),
},
{
colKey: "timestamp",
title: "时间",
width: 180,
cell: ({ row }: { row: CheckResult; rowIndex: number; col: unknown; colIndex: number }) => {
cell: ({ row }: { col: unknown; colIndex: number; row: CheckResult; rowIndex: number }) => {
const d = new Date(row.timestamp);
const pad = (n: number) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
},
colKey: "timestamp",
title: "时间",
width: 180,
},
{
align: "center" as const,
cell: ({ row }: { col: unknown; colIndex: number; row: CheckResult; rowIndex: number }) =>
row.durationMs !== null ? Math.round(row.durationMs) : "-",
colKey: "durationMs",
title: "耗时(ms)",
width: 96,
align: "center" as const,
cell: ({ row }: { row: CheckResult; rowIndex: number; col: unknown; colIndex: number }) =>
row.durationMs !== null ? Math.round(row.durationMs) : "-",
},
{
colKey: "statusDetail",
title: "详情",
cell: ({ row }: { row: CheckResult; rowIndex: number; col: unknown; colIndex: number }) => {
cell: ({ row }: { col: unknown; colIndex: number; row: CheckResult; rowIndex: number }) => {
const parts = [row.statusDetail, row.failure?.message].filter(Boolean);
return parts.length > 0 ? parts.join("") : "-";
},
colKey: "statusDetail",
title: "详情",
},
];
export function TargetDetailDrawer({
target,
trendData,
trendLoading,
historyData,
historyLoading,
onClose,
onPageChange,
onTimeChange,
target,
timeFrom,
timeTo,
onTimeChange,
onPageChange,
onClose,
trendData,
trendLoading,
}: TargetDetailDrawerProps) {
const [activeShortcut, setActiveShortcut] = useState<string>("24h");
const [activeTab, setActiveTab] = useState<TabValue>("overview");
@@ -108,8 +111,8 @@ export function TargetDetailDrawer({
);
const handleDateRangeChange = useCallback(
(value: Array<string | number | Date>) => {
if (value && value.length === 2) {
(value: Array<Date | number | string>) => {
if (value?.length === 2) {
onTimeChange(new Date(value[0]!).toISOString(), new Date(value[1]!).toISOString());
setActiveShortcut("");
}
@@ -126,11 +129,7 @@ export function TargetDetailDrawer({
return (
<Drawer
visible={!!target}
placement="right"
size="60%"
footer={false}
onClose={onClose}
header={
<Space align="center" size={8}>
<StatusDot up={!!isUp} />
@@ -140,44 +139,48 @@ export function TargetDetailDrawer({
</Tag>
</Space>
}
onClose={onClose}
placement="right"
size="60%"
visible={!!target}
>
<Space direction="vertical" size={16} className="full-width">
<Space className="full-width" direction="vertical" size={16}>
<RadioGroup
theme="button"
variant="default-filled"
value={activeShortcut}
options={TIME_SHORTCUTS.map((s) => ({ label: s.label, value: s.value }))}
onChange={handleShortcut}
options={TIME_SHORTCUTS.map((s) => ({ label: s.label, value: s.value }))}
theme="button"
value={activeShortcut}
variant="default-filled"
/>
<DateRangePicker
mode="date"
className="full-width"
defaultTime={["00:00:00", "23:59:00"]}
enableTimePicker
format="YYYY-MM-DD HH:mm"
valueType="YYYY-MM-DD HH:mm"
defaultTime={["00:00:00", "23:59:00"]}
timePickerProps={{ format: "HH:mm", steps: [1, 1, 60] }}
className="full-width"
value={timeFrom && timeTo ? [timeFrom, timeTo] : undefined}
mode="date"
onChange={handleDateRangeChange}
timePickerProps={{ format: "HH:mm", steps: [1, 1, 60] }}
value={timeFrom && timeTo ? [timeFrom, timeTo] : undefined}
valueType="YYYY-MM-DD HH:mm"
/>
</Space>
<Tabs value={activeTab} onChange={(val: TabValue) => setActiveTab(val)}>
<Tabs.TabPanel value="overview" label="概览" className="tab-panel-padded">
<Space direction="vertical" size={16} className="full-width">
<Tabs onChange={(val: TabValue) => setActiveTab(val)} value={activeTab}>
<Tabs.TabPanel className="tab-panel-padded" label="概览" value="overview">
<Space className="full-width" direction="vertical" size={16}>
<Divider align="left"></Divider>
<Row gutter={16}>
<Col span={3}>
<Statistic title="总检查" value={totalChecks} color="blue" />
<Statistic color="blue" title="总检查" value={totalChecks} />
</Col>
<Col span={3}>
<Statistic title="正常" value={upChecks} color="green" />
<Statistic color="green" title="正常" value={upChecks} />
</Col>
<Col span={3}>
<Statistic title="异常" value={downChecks} color="red" />
<Statistic color="red" title="异常" value={downChecks} />
</Col>
<Col span={3}>
<Statistic title="可用率" value={target.stats?.availability ?? 0} color="green" suffix="%" />
<Statistic color="green" suffix="%" title="可用率" value={target.stats?.availability ?? 0} />
</Col>
</Row>
@@ -185,38 +188,38 @@ export function TargetDetailDrawer({
{trendLoading ? <Skeleton animation="gradient" /> : <TrendChart data={trendData} loading={false} />}
<Divider align="left"></Divider>
<StatusDonut up={upChecks} down={downChecks} />
<StatusDonut down={downChecks} up={upChecks} />
<Divider align="left"></Divider>
<Descriptions
items={[
{ label: "目标地址", content: target.target },
{ label: "检查间隔", content: target.interval },
{ content: target.target, label: "目标地址" },
{ content: target.interval, label: "检查间隔" },
{
label: "最新检查时间",
content: target.latestCheck ? new Date(target.latestCheck.timestamp).toLocaleString("zh-CN") : "-",
label: "最新检查时间",
},
{ label: "状态详情", content: target.latestCheck?.statusDetail ?? "-" },
{ content: target.latestCheck?.statusDetail ?? "-", label: "状态详情" },
]}
/>
</Space>
</Tabs.TabPanel>
<Tabs.TabPanel value="history" label="记录" className="tab-panel-padded">
<Tabs.TabPanel className="tab-panel-padded" label="记录" value="history">
<PrimaryTable
columns={HISTORY_COLUMNS}
data={historyData.items}
rowKey="timestamp"
loading={historyLoading}
disableDataPage
loading={historyLoading}
onPageChange={({ current }) => {
if (current) onPageChange(current);
}}
pagination={{
current: historyData.page,
pageSize: historyData.pageSize,
total: historyData.total,
}}
onPageChange={({ current }) => {
if (current) onPageChange(current);
}}
rowKey="timestamp"
/>
</Tabs.TabPanel>
</Tabs>

View File

@@ -1,36 +1,38 @@
import type { TargetStatus } from "../../shared/api";
import { GroupHeader } from "./GroupHeader";
import { PrimaryTable } from "tdesign-react";
import type { TargetStatus } from "../../shared/api";
import { TARGET_TABLE_COLUMNS } from "../constants/target-table-columns";
import { GroupHeader } from "./GroupHeader";
interface TargetGroupProps {
name: string;
targets: TargetStatus[];
onTargetClick: (target: TargetStatus) => void;
targets: TargetStatus[];
}
export function TargetGroup({ name, targets, onTargetClick }: TargetGroupProps) {
export function TargetGroup({ name, onTargetClick, targets }: TargetGroupProps) {
const up = targets.filter((t) => t.latestCheck?.matched).length;
const down = targets.length - up;
return (
<div>
<GroupHeader name={name} total={targets.length} up={up} down={down} />
<GroupHeader down={down} name={name} total={targets.length} up={up} />
<PrimaryTable
bordered
className="clickable-table"
columns={TARGET_TABLE_COLUMNS}
data={targets}
defaultSort={[{ descending: true, sortBy: "latestCheck.matched" }]}
hover
onRowClick={({ row }) => onTargetClick(row)}
rowClassName={({ row }) => {
const target = row;
return target.latestCheck?.matched === false ? "row-down" : "";
}}
rowKey="id"
size="small"
stripe
hover
bordered
defaultSort={[{ sortBy: "latestCheck.matched", descending: true }]}
onRowClick={({ row }) => onTargetClick(row as TargetStatus)}
rowClassName={({ row }) => {
const target = row as TargetStatus;
return target.latestCheck?.matched === false ? "row-down" : "";
}}
className="clickable-table"
/>
</div>
);

View File

@@ -1,4 +1,5 @@
import { Line, LineChart, ResponsiveContainer, XAxis, YAxis, Tooltip, CartesianGrid } from "recharts";
import { CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
import type { TrendPoint } from "../../shared/api";
interface TrendChartProps {
@@ -22,23 +23,23 @@ export function TrendChart({ data, loading }: TrendChartProps) {
return (
<div className="trend-chart">
<ResponsiveContainer width="100%" height={240}>
<ResponsiveContainer height={240} width="100%">
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke="var(--td-border-level-2-color)" />
<XAxis dataKey="hour" tick={{ fontSize: 12 }} stroke="var(--td-text-color-secondary)" />
<CartesianGrid stroke="var(--td-border-level-2-color)" strokeDasharray="3 3" />
<XAxis dataKey="hour" stroke="var(--td-text-color-secondary)" tick={{ fontSize: 12 }} />
<YAxis
yAxisId="duration"
tick={{ fontSize: 12 }}
label={{ fontSize: 11, position: "insideTopRight", value: "ms" }}
stroke="var(--td-text-color-secondary)"
label={{ value: "ms", position: "insideTopRight", fontSize: 11 }}
tick={{ fontSize: 12 }}
yAxisId="duration"
/>
<YAxis
yAxisId="availability"
orientation="right"
domain={[0, 100]}
tick={{ fontSize: 12 }}
label={{ fontSize: 11, position: "insideTopLeft", value: "%" }}
orientation="right"
stroke="var(--td-text-color-secondary)"
label={{ value: "%", position: "insideTopLeft", fontSize: 11 }}
tick={{ fontSize: 12 }}
yAxisId="availability"
/>
<Tooltip
formatter={(value: unknown, name: unknown) => {
@@ -50,22 +51,22 @@ export function TrendChart({ data, loading }: TrendChartProps) {
}}
/>
<Line
yAxisId="duration"
type="monotone"
dataKey="avgDurationMs"
stroke="var(--td-brand-color)"
strokeWidth={2}
dot={false}
name="avgDurationMs"
stroke="var(--td-brand-color)"
strokeWidth={2}
type="monotone"
yAxisId="duration"
/>
<Line
yAxisId="availability"
type="monotone"
dataKey="availability"
stroke="var(--td-success-color)"
strokeWidth={2}
dot={false}
name="availability"
stroke="var(--td-success-color)"
strokeWidth={2}
type="monotone"
yAxisId="availability"
/>
</LineChart>
</ResponsiveContainer>