refactor: 前端视觉重构 — Layout/HeadMenu 骨架、SummaryCards 合并、Card 分组、Drawer 概览重设计
This commit is contained in:
@@ -1,27 +0,0 @@
|
||||
import { Tag, Typography } from "tdesign-react";
|
||||
|
||||
interface GroupHeaderProps {
|
||||
down: number;
|
||||
name: string;
|
||||
total: number;
|
||||
up: number;
|
||||
}
|
||||
|
||||
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" title="总数" variant="light">
|
||||
{total}
|
||||
</Tag>
|
||||
<Tag theme="success" title="正常" variant="light">
|
||||
{up}
|
||||
</Tag>
|
||||
<Tag theme="danger" title="异常" variant="light">
|
||||
{down}
|
||||
</Tag>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,19 @@
|
||||
import { Col, Descriptions, Divider, Row, Skeleton, Space, Statistic } from "tdesign-react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { Card, Col, Descriptions, Divider, Row, Skeleton, Space, Statistic, Typography } from "tdesign-react";
|
||||
|
||||
import type { TargetMetricsResponse, TargetStatus } from "../../shared/api";
|
||||
|
||||
import { formatDurationUnit } from "../utils/time";
|
||||
import { StatusDonut } from "./StatusDonut";
|
||||
import { TrendChart } from "./TrendChart";
|
||||
|
||||
interface OverviewStatItemProps {
|
||||
color?: string;
|
||||
suffix?: ReactNode;
|
||||
title: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface OverviewTabProps {
|
||||
metricsData: null | TargetMetricsResponse;
|
||||
metricsLoading: boolean;
|
||||
@@ -20,70 +28,6 @@ export function OverviewTab({ metricsData, metricsLoading, target }: OverviewTab
|
||||
|
||||
return (
|
||||
<Space className="full-width" direction="vertical" size={16}>
|
||||
<Divider align="left">统计</Divider>
|
||||
{metricsLoading ? (
|
||||
<Skeleton animation="gradient" />
|
||||
) : stats ? (
|
||||
<Space className="full-width" direction="vertical" size={16}>
|
||||
<Row gutter={16}>
|
||||
<Col span={3}>
|
||||
<Statistic color="green" suffix="%" title="可用率" value={stats.availability} />
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<Statistic
|
||||
suffix={stats.avgDurationMs === null ? "" : "ms"}
|
||||
title="平均延迟"
|
||||
value={stats.avgDurationMs ?? 0}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<Statistic
|
||||
suffix={stats.p95DurationMs === null ? "" : "ms"}
|
||||
title="P95 延迟"
|
||||
value={stats.p95DurationMs ?? 0}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<Statistic color="blue" title="检查总数" value={stats.totalChecks} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col span={3}>
|
||||
<Statistic suffix={mttr.suffix} title="MTTR" value={mttr.value} />
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<Statistic suffix={longestOutage.suffix} title="最长故障" value={longestOutage.value} />
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<Statistic color="red" suffix="次" title="故障次数" value={stats.incidentCount} />
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<Statistic color="green" suffix="次" title="连续正常" value={currentUpStreak} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Space>
|
||||
) : (
|
||||
<div className="trend-empty">暂无指标数据</div>
|
||||
)}
|
||||
|
||||
<Divider align="left">趋势</Divider>
|
||||
{metricsLoading ? (
|
||||
<Skeleton animation="gradient" />
|
||||
) : metricsData ? (
|
||||
<TrendChart data={metricsData.trend} />
|
||||
) : (
|
||||
<div className="trend-empty">暂无趋势数据</div>
|
||||
)}
|
||||
|
||||
<Divider align="left">状态分布</Divider>
|
||||
{metricsLoading ? (
|
||||
<Skeleton animation="gradient" />
|
||||
) : stats ? (
|
||||
<StatusDonut down={stats.downChecks} up={stats.upChecks} />
|
||||
) : (
|
||||
<div className="trend-empty">暂无状态数据</div>
|
||||
)}
|
||||
|
||||
<Divider align="left">基本信息</Divider>
|
||||
<Descriptions
|
||||
items={[
|
||||
@@ -96,6 +40,68 @@ export function OverviewTab({ metricsData, metricsLoading, target }: OverviewTab
|
||||
{ content: target.latestCheck?.statusDetail ?? "-", label: "状态详情" },
|
||||
]}
|
||||
/>
|
||||
|
||||
<Divider align="left">统计</Divider>
|
||||
{metricsLoading ? (
|
||||
<Skeleton animation="gradient" />
|
||||
) : stats ? (
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={3}>
|
||||
<OverviewStatItem color="green" suffix="%" title="可用率" value={stats.availability} />
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<OverviewStatItem
|
||||
suffix={stats.avgDurationMs === null ? "" : "ms"}
|
||||
title="平均延迟"
|
||||
value={stats.avgDurationMs ?? 0}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<OverviewStatItem
|
||||
suffix={stats.p95DurationMs === null ? "" : "ms"}
|
||||
title="P95 延迟"
|
||||
value={stats.p95DurationMs ?? 0}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<OverviewStatItem color="blue" title="检查总数" value={stats.totalChecks} />
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<OverviewStatItem suffix={mttr.suffix} title="MTTR" value={mttr.value} />
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<OverviewStatItem suffix={longestOutage.suffix} title="最长故障" value={longestOutage.value} />
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<OverviewStatItem color="red" suffix="次" title="故障次数" value={stats.incidentCount} />
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<OverviewStatItem color="green" suffix="次" title="连续正常" value={currentUpStreak} />
|
||||
</Col>
|
||||
</Row>
|
||||
) : (
|
||||
<div className="trend-empty">暂无指标数据</div>
|
||||
)}
|
||||
|
||||
<Divider align="left">趋势</Divider>
|
||||
{metricsLoading ? (
|
||||
<Skeleton animation="gradient" />
|
||||
) : metricsData ? (
|
||||
<TrendChart data={metricsData.trend} />
|
||||
) : (
|
||||
<div className="trend-empty">暂无趋势数据</div>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
function OverviewStatItem({ color, suffix, title, value }: OverviewStatItemProps) {
|
||||
return (
|
||||
<Card bordered={false} className="overview-stat-card" size="small">
|
||||
<div className="overview-stat-item">
|
||||
<Typography.Text theme="secondary">{title}</Typography.Text>
|
||||
<Statistic className="overview-stat-value" color={color} suffix={suffix} value={value} />
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { Tooltip } from "tdesign-react";
|
||||
|
||||
import type { RecentSample } from "../../shared/api";
|
||||
|
||||
import { formatRelativeTime } from "../utils/time";
|
||||
|
||||
interface StatusBarProps {
|
||||
maxSlots?: number;
|
||||
samples: Array<{ up: boolean }>;
|
||||
samples: RecentSample[];
|
||||
}
|
||||
|
||||
export function StatusBar({ maxSlots = 30, samples }: StatusBarProps) {
|
||||
@@ -9,10 +15,13 @@ export function StatusBar({ maxSlots = 30, samples }: StatusBarProps) {
|
||||
const sample = samples[i];
|
||||
if (sample) {
|
||||
blocks.push(
|
||||
<span
|
||||
className={`status-bar-block ${sample.up ? "status-bar-block--up" : "status-bar-block--down"}`}
|
||||
<Tooltip
|
||||
content={`${formatRelativeTime(sample.timestamp)},${sample.up ? "正常" : "异常"}`}
|
||||
key={i}
|
||||
/>,
|
||||
placement="top"
|
||||
>
|
||||
<span className={`status-bar-block ${sample.up ? "status-bar-block--up" : "status-bar-block--down"}`} />
|
||||
</Tooltip>,
|
||||
);
|
||||
} else {
|
||||
blocks.push(<span className="status-bar-block status-bar-block--empty" key={i} />);
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { Cell, Pie, PieChart, ResponsiveContainer } from "recharts";
|
||||
|
||||
interface StatusDonutProps {
|
||||
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({ down, up }: StatusDonutProps) {
|
||||
const total = up + down;
|
||||
const availability = total > 0 ? ((up / total) * 100).toFixed(1) : "-";
|
||||
|
||||
const data =
|
||||
total > 0
|
||||
? [
|
||||
{ name: "UP", value: up },
|
||||
{ name: "DOWN", value: down },
|
||||
]
|
||||
: [{ name: "EMPTY", value: 1 }];
|
||||
|
||||
const colors = total > 0 ? [UP_COLOR, DOWN_COLOR] : [EMPTY_COLOR];
|
||||
|
||||
return (
|
||||
<div className="status-donut">
|
||||
<ResponsiveContainer height={180} width="100%">
|
||||
<PieChart>
|
||||
<Pie cx="50%" cy="50%" data={data} dataKey="value" innerRadius={50} outerRadius={70} stroke="none">
|
||||
{data.map((item, index) => (
|
||||
<Cell fill={colors[index % colors.length]} key={item.name} />
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
<div className="donut-center-label">{total > 0 ? `${availability}%` : "-"}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,22 +1,12 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Card, Col, Row, Statistic, Typography } from "tdesign-react";
|
||||
import { Card, Col, Row, Statistic } from "tdesign-react";
|
||||
|
||||
import type { DashboardResponse } from "../../shared/api";
|
||||
|
||||
import { formatRelativeTime, isOlderThan } from "../utils/time";
|
||||
|
||||
interface SummaryCardsProps {
|
||||
summary: DashboardResponse["summary"] | null;
|
||||
}
|
||||
|
||||
export function SummaryCards({ summary }: SummaryCardsProps) {
|
||||
const [now, setNow] = useState(() => new Date());
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setInterval(() => setNow(new Date()), 1000);
|
||||
return () => window.clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
if (!summary) return null;
|
||||
|
||||
const cards = [
|
||||
@@ -25,25 +15,18 @@ export function SummaryCards({ summary }: SummaryCardsProps) {
|
||||
{ color: "red" as const, label: "异常", value: summary.down },
|
||||
{ color: "orange" as const, label: `${summary.window.label} 异常事件数`, value: summary.incidents },
|
||||
];
|
||||
const freshnessWarning = isOlderThan(summary.lastCheckTime, 60000, now);
|
||||
|
||||
return (
|
||||
<section className="summary-cards-row">
|
||||
<Row gutter={16}>
|
||||
{cards.map((card) => (
|
||||
<Col key={card.label} span={3}>
|
||||
<Card bordered>
|
||||
<Card bordered={false}>
|
||||
<Row gutter={16}>
|
||||
{cards.map((card) => (
|
||||
<Col className="summary-stat-col" key={card.label} span={3}>
|
||||
<Statistic color={card.color} title={card.label} value={card.value} />
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
<Typography.Text
|
||||
className={freshnessWarning ? "summary-freshness summary-freshness--warning" : "summary-freshness"}
|
||||
theme="secondary"
|
||||
>
|
||||
{summary.lastCheckTime ? `最后更新: ${formatRelativeTime(summary.lastCheckTime, now)}` : "尚无检查数据"}
|
||||
</Typography.Text>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Card>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export function TargetBoard({ onTargetClick, targets }: TargetBoardProps) {
|
||||
});
|
||||
|
||||
return (
|
||||
<Space className="full-width" direction="vertical" size={32}>
|
||||
<Space className="full-width" direction="vertical" size={24}>
|
||||
{sortedGroups.map(([name, groupTargets]) => (
|
||||
<TargetGroup columns={columns} key={name} name={name} onTargetClick={onTargetClick} targets={groupTargets} />
|
||||
))}
|
||||
|
||||
@@ -85,28 +85,30 @@ export function TargetDetailDrawer({
|
||||
}
|
||||
onClose={onClose}
|
||||
placement="right"
|
||||
size="60%"
|
||||
size="52%"
|
||||
visible={!!target}
|
||||
>
|
||||
<Space className="full-width" direction="vertical" size={16}>
|
||||
<RadioGroup
|
||||
onChange={handleShortcut}
|
||||
options={TIME_SHORTCUTS.map((s) => ({ label: s.label, value: s.value }))}
|
||||
theme="button"
|
||||
value={activeShortcut}
|
||||
variant="default-filled"
|
||||
/>
|
||||
<DateRangePicker
|
||||
className="full-width"
|
||||
defaultTime={["00:00:00", "23:59:00"]}
|
||||
enableTimePicker
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
mode="date"
|
||||
onChange={handleDateRangeChange}
|
||||
timePickerProps={{ format: "HH:mm", steps: [1, 1, 60] }}
|
||||
value={timeFrom && timeTo ? [timeFrom, timeTo] : undefined}
|
||||
valueType="YYYY-MM-DD HH:mm"
|
||||
/>
|
||||
<div className="drawer-time-controls">
|
||||
<RadioGroup
|
||||
onChange={handleShortcut}
|
||||
options={TIME_SHORTCUTS.map((s) => ({ label: s.label, value: s.value }))}
|
||||
theme="button"
|
||||
value={activeShortcut}
|
||||
variant="default-filled"
|
||||
/>
|
||||
<DateRangePicker
|
||||
className="drawer-date-range"
|
||||
defaultTime={["00:00:00", "23:59:00"]}
|
||||
enableTimePicker
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
mode="date"
|
||||
onChange={handleDateRangeChange}
|
||||
timePickerProps={{ format: "HH:mm", steps: [1, 1, 60] }}
|
||||
value={timeFrom && timeTo ? [timeFrom, timeTo] : undefined}
|
||||
valueType="YYYY-MM-DD HH:mm"
|
||||
/>
|
||||
</div>
|
||||
<Tabs onChange={(val: TabValue) => setActiveTab(val)} value={activeTab}>
|
||||
<Tabs.TabPanel className="tab-panel-padded" label="概览" value="overview">
|
||||
<OverviewTab metricsData={metricsData} metricsLoading={metricsLoading} target={target} />
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import type { PrimaryTableCol } from "tdesign-react";
|
||||
|
||||
import { PrimaryTable } from "tdesign-react";
|
||||
import { Card, PrimaryTable, Space, Tag } from "tdesign-react";
|
||||
|
||||
import type { TargetStatus } from "../../shared/api";
|
||||
|
||||
import { GroupHeader } from "./GroupHeader";
|
||||
|
||||
interface TargetGroupProps {
|
||||
columns: Array<PrimaryTableCol<TargetStatus>>;
|
||||
name: string;
|
||||
@@ -16,12 +14,24 @@ interface TargetGroupProps {
|
||||
export function TargetGroup({ columns, name, onTargetClick, targets }: TargetGroupProps) {
|
||||
const up = targets.filter((t) => t.latestCheck?.matched).length;
|
||||
const down = targets.length - up;
|
||||
const displayName = name === "default" ? "默认分组" : name;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<GroupHeader down={down} name={name} total={targets.length} up={up} />
|
||||
<Card
|
||||
actions={
|
||||
<Space size={8}>
|
||||
<Tag theme="success" title="正常" variant="light">
|
||||
{up}
|
||||
</Tag>
|
||||
<Tag theme="danger" title="异常" variant="light">
|
||||
{down}
|
||||
</Tag>
|
||||
</Space>
|
||||
}
|
||||
headerBordered
|
||||
title={displayName}
|
||||
>
|
||||
<PrimaryTable
|
||||
bordered
|
||||
className="clickable-table"
|
||||
columns={columns}
|
||||
data={targets}
|
||||
@@ -36,6 +46,6 @@ export function TargetGroup({ columns, name, onTargetClick, targets }: TargetGro
|
||||
size="small"
|
||||
stripe
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user