refactor: 全面重构前端样式,消除内联 style 和硬编码色值,统一 TDesign 规范
- 重写 styles.css:CSS 变量化可用率色阶、状态色类、工具类、安全选择器 - 组件改造:StatusDot/StatusBar/TargetDetailDrawer/GroupHeader 等改用 CSS 类和 Typography - color-threshold 移除 getLatencyColor 死代码,保留 getAvailabilityProgressColor 返回 CSS 变量 - target-table-columns 状态列和延迟列切换为 CSS 类 - 新增 css-utility-classes spec,更新 4 个 main specs(probe/card/table/drawer) - README 和 config.yaml 写入前端样式开发规范
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Alert, Loading } from "tdesign-react";
|
||||
import { Alert, Loading, Typography } from "tdesign-react";
|
||||
import { useSummary, useTargets, useTargetDetail } from "./hooks/useTargetDetail";
|
||||
import { SummaryCards } from "./components/SummaryCards";
|
||||
import { TargetBoard } from "./components/TargetBoard";
|
||||
@@ -26,8 +26,8 @@ export function App() {
|
||||
return (
|
||||
<main className="dashboard">
|
||||
<header className="dashboard-header">
|
||||
<h1>DiAL</h1>
|
||||
<p className="dashboard-subtitle">统一拨测平台</p>
|
||||
<Typography.Title level="h1">DiAL</Typography.Title>
|
||||
<Typography.Text theme="secondary">统一拨测平台</Typography.Text>
|
||||
</header>
|
||||
|
||||
{error && <Alert theme="error" message={`请求失败: ${error.message}`} closeBtn />}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Space, Tag } from "tdesign-react";
|
||||
import { Tag, Typography } from "tdesign-react";
|
||||
|
||||
interface GroupHeaderProps {
|
||||
name: string;
|
||||
@@ -11,8 +11,8 @@ export function GroupHeader({ name, total, up, down }: GroupHeaderProps) {
|
||||
const displayName = name === "default" ? "默认分组" : name;
|
||||
|
||||
return (
|
||||
<Space align="center" size={8} style={{ marginBottom: 12 }}>
|
||||
<h2 style={{ margin: 0, fontSize: "1.1rem", fontWeight: 600 }}>{displayName}</h2>
|
||||
<div className="group-header">
|
||||
<Typography.Title level="h4">{displayName}</Typography.Title>
|
||||
<Tag theme="primary" variant="light" title="总数">
|
||||
{total}
|
||||
</Tag>
|
||||
@@ -22,6 +22,6 @@ export function GroupHeader({ name, total, up, down }: GroupHeaderProps) {
|
||||
<Tag theme="danger" variant="light" title="异常">
|
||||
{down}
|
||||
</Tag>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,14 +10,11 @@ export function StatusBar({ samples }: StatusBarProps) {
|
||||
blocks.push(
|
||||
<span
|
||||
key={i}
|
||||
className="status-bar-block"
|
||||
style={{ background: sample.up ? "var(--td-success-color)" : "var(--td-error-color)" }}
|
||||
className={`status-bar-block ${sample.up ? "status-bar-block--up" : "status-bar-block--down"}`}
|
||||
/>,
|
||||
);
|
||||
} else {
|
||||
blocks.push(
|
||||
<span key={i} className="status-bar-block" style={{ background: "var(--td-bg-color-component-disabled)" }} />,
|
||||
);
|
||||
blocks.push(<span key={i} className="status-bar-block status-bar-block--empty" />);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,5 @@ interface StatusDotProps {
|
||||
}
|
||||
|
||||
export function StatusDot({ up }: StatusDotProps) {
|
||||
const color = up ? "var(--td-success-color)" : "var(--td-error-color)";
|
||||
const shadow = up ? "var(--td-success-color)" : "var(--td-error-color)";
|
||||
return (
|
||||
<span
|
||||
className="status-dot"
|
||||
style={{ background: color, boxShadow: `0 0 0 6px color-mix(in srgb, ${shadow} 14%, transparent)` }}
|
||||
/>
|
||||
);
|
||||
return <span className={`status-dot ${up ? "status-dot--up" : "status-dot--down"}`} />;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export function SummaryCards({ summary }: SummaryCardsProps) {
|
||||
];
|
||||
|
||||
return (
|
||||
<Row gutter={16} style={{ marginBottom: 32 }}>
|
||||
<Row gutter={16} className="summary-cards-row">
|
||||
{cards.map((card) => (
|
||||
<Col key={card.label} span={4}>
|
||||
<Card bordered>
|
||||
|
||||
@@ -25,7 +25,7 @@ export function TargetBoard({ targets, onTargetClick }: TargetBoardProps) {
|
||||
});
|
||||
|
||||
return (
|
||||
<Space direction="vertical" size={32} style={{ width: "100%" }}>
|
||||
<Space direction="vertical" size={32} className="full-width">
|
||||
{sortedGroups.map(([name, groupTargets]) => (
|
||||
<TargetGroup key={name} name={name} targets={groupTargets} onTargetClick={onTargetClick} />
|
||||
))}
|
||||
|
||||
@@ -11,6 +11,9 @@ import {
|
||||
Descriptions,
|
||||
Skeleton,
|
||||
PrimaryTable,
|
||||
Divider,
|
||||
Space,
|
||||
Typography,
|
||||
} from "tdesign-react";
|
||||
import type { TabValue } from "tdesign-react";
|
||||
import type { CheckResult, TargetStatus, TrendPoint, HistoryResponse } from "../../shared/api";
|
||||
@@ -40,13 +43,6 @@ const TIME_SHORTCUTS = [
|
||||
{ label: "7天", hours: 168, value: "7d" },
|
||||
] as const;
|
||||
|
||||
const SECTION_TITLE_STYLE: React.CSSProperties = {
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: "var(--td-text-color-primary)",
|
||||
margin: "16px 0 8px",
|
||||
};
|
||||
|
||||
const HISTORY_COLUMNS = [
|
||||
{
|
||||
colKey: "matched",
|
||||
@@ -136,16 +132,16 @@ export function TargetDetailDrawer({
|
||||
footer={false}
|
||||
onClose={onClose}
|
||||
header={
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<Space align="center" size={8}>
|
||||
<StatusDot up={!!isUp} />
|
||||
<span style={{ fontWeight: 600 }}>{target.name}</span>
|
||||
<Typography.Text strong>{target.name}</Typography.Text>
|
||||
<Tag size="small" theme="primary" variant="light-outline">
|
||||
{getTargetTypeDisplay(target.type)}
|
||||
</Tag>
|
||||
</div>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Space direction="vertical" size={16} className="full-width">
|
||||
<RadioGroup
|
||||
theme="button"
|
||||
variant="default-filled"
|
||||
@@ -153,8 +149,6 @@ export function TargetDetailDrawer({
|
||||
options={TIME_SHORTCUTS.map((s) => ({ label: s.label, value: s.value }))}
|
||||
onChange={handleShortcut}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<DateRangePicker
|
||||
mode="date"
|
||||
enableTimePicker
|
||||
@@ -162,51 +156,53 @@ export function TargetDetailDrawer({
|
||||
valueType="YYYY-MM-DD HH:mm"
|
||||
defaultTime={["00:00:00", "23:59:00"]}
|
||||
timePickerProps={{ format: "HH:mm", steps: [1, 1, 60] }}
|
||||
style={{ width: "100%" }}
|
||||
className="full-width"
|
||||
value={timeFrom && timeTo ? [timeFrom, timeTo] : undefined}
|
||||
onChange={handleDateRangeChange}
|
||||
/>
|
||||
</div>
|
||||
</Space>
|
||||
|
||||
<Tabs className="drawer-tabs" value={activeTab} onChange={(val: TabValue) => setActiveTab(val)}>
|
||||
<Tabs.TabPanel value="overview" label="概览">
|
||||
<h4 style={{ ...SECTION_TITLE_STYLE, marginTop: 0 }}>统计</h4>
|
||||
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||||
<Col span={3}>
|
||||
<Statistic title="总检查" value={totalChecks} color="blue" />
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<Statistic title="正常" value={upChecks} color="green" />
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<Statistic title="异常" value={downChecks} color="red" />
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<Statistic title="可用率" value={target.stats?.availability ?? 0} color="green" suffix="%" />
|
||||
</Col>
|
||||
</Row>
|
||||
<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">
|
||||
<Divider align="left">统计</Divider>
|
||||
<Row gutter={16}>
|
||||
<Col span={3}>
|
||||
<Statistic title="总检查" value={totalChecks} color="blue" />
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<Statistic title="正常" value={upChecks} color="green" />
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<Statistic title="异常" value={downChecks} color="red" />
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<Statistic title="可用率" value={target.stats?.availability ?? 0} color="green" suffix="%" />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<h4 style={SECTION_TITLE_STYLE}>趋势</h4>
|
||||
{trendLoading ? <Skeleton animation="gradient" /> : <TrendChart data={trendData} loading={false} />}
|
||||
<Divider align="left">趋势</Divider>
|
||||
{trendLoading ? <Skeleton animation="gradient" /> : <TrendChart data={trendData} loading={false} />}
|
||||
|
||||
<h4 style={SECTION_TITLE_STYLE}>状态分布</h4>
|
||||
<StatusDonut up={upChecks} down={downChecks} />
|
||||
<Divider align="left">状态分布</Divider>
|
||||
<StatusDonut up={upChecks} down={downChecks} />
|
||||
|
||||
<h4 style={SECTION_TITLE_STYLE}>基本信息</h4>
|
||||
<Descriptions
|
||||
items={[
|
||||
{ label: "目标地址", content: target.target },
|
||||
{ label: "检查间隔", content: target.interval },
|
||||
{
|
||||
label: "最新检查时间",
|
||||
content: target.latestCheck ? new Date(target.latestCheck.timestamp).toLocaleString("zh-CN") : "-",
|
||||
},
|
||||
{ label: "状态详情", content: target.latestCheck?.statusDetail ?? "-" },
|
||||
]}
|
||||
/>
|
||||
<Divider align="left">基本信息</Divider>
|
||||
<Descriptions
|
||||
items={[
|
||||
{ label: "目标地址", content: target.target },
|
||||
{ label: "检查间隔", content: target.interval },
|
||||
{
|
||||
label: "最新检查时间",
|
||||
content: target.latestCheck ? new Date(target.latestCheck.timestamp).toLocaleString("zh-CN") : "-",
|
||||
},
|
||||
{ label: "状态详情", content: target.latestCheck?.statusDetail ?? "-" },
|
||||
]}
|
||||
/>
|
||||
</Space>
|
||||
</Tabs.TabPanel>
|
||||
|
||||
<Tabs.TabPanel value="history" label="记录">
|
||||
<Tabs.TabPanel value="history" label="记录" className="tab-panel-padded">
|
||||
<PrimaryTable
|
||||
columns={HISTORY_COLUMNS}
|
||||
data={historyData.items}
|
||||
|
||||
@@ -30,7 +30,7 @@ export function TargetGroup({ name, targets, onTargetClick }: TargetGroupProps)
|
||||
const target = row as TargetStatus;
|
||||
return target.latestCheck?.matched === false ? "row-down" : "";
|
||||
}}
|
||||
style={{ cursor: "pointer" }}
|
||||
className="clickable-table"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,24 +1,4 @@
|
||||
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)";
|
||||
return `var(--avail-${index})`;
|
||||
}
|
||||
|
||||
@@ -4,15 +4,15 @@ 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 { getAvailabilityProgressColor } 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,
|
||||
title: "#",
|
||||
width: 60,
|
||||
fixed: "left",
|
||||
align: "center",
|
||||
filter: statusFilter,
|
||||
@@ -72,9 +72,9 @@ export const TARGET_TABLE_COLUMNS: PrimaryTableCol<TargetStatus>[] = [
|
||||
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>;
|
||||
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>;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
:root {
|
||||
--avail-0: #d54941;
|
||||
--avail-1: #d96241;
|
||||
--avail-2: #e37318;
|
||||
--avail-3: #e89318;
|
||||
--avail-4: #d9a818;
|
||||
--avail-5: #b8b020;
|
||||
--avail-6: #8dba30;
|
||||
--avail-7: #6dba3f;
|
||||
--avail-8: #4dba50;
|
||||
--avail-9: #3dba60;
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
padding: 32px 24px;
|
||||
padding: var(--td-comp-paddingTB-xl) var(--td-comp-paddingLR-xl);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
margin-bottom: 32px;
|
||||
margin-bottom: var(--td-comp-margin-l);
|
||||
}
|
||||
|
||||
.dashboard-header h1 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 1.75rem;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.dashboard-subtitle {
|
||||
.dashboard-header .t-typography {
|
||||
margin: 0;
|
||||
color: var(--td-text-color-secondary);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
@@ -27,6 +33,16 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-dot--up {
|
||||
background: var(--td-success-color);
|
||||
box-shadow: 0 0 0 6px color-mix(in srgb, var(--td-success-color) 14%, transparent);
|
||||
}
|
||||
|
||||
.status-dot--down {
|
||||
background: var(--td-error-color);
|
||||
box-shadow: 0 0 0 6px color-mix(in srgb, var(--td-error-color) 14%, transparent);
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
@@ -41,6 +57,18 @@
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.status-bar-block--up {
|
||||
background: var(--td-success-color);
|
||||
}
|
||||
|
||||
.status-bar-block--down {
|
||||
background: var(--td-error-color);
|
||||
}
|
||||
|
||||
.status-bar-block--empty {
|
||||
background: var(--td-bg-color-component-disabled);
|
||||
}
|
||||
|
||||
.status-donut {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@@ -71,10 +99,60 @@
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.drawer-tabs .t-tab-panel {
|
||||
.tab-panel-padded {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.row-down {
|
||||
background: color-mix(in srgb, var(--td-error-color) 6%, transparent) !important;
|
||||
.t-table tr.row-down {
|
||||
background: color-mix(in srgb, var(--td-error-color) 6%, transparent);
|
||||
}
|
||||
|
||||
.t-table--hoverable tbody tr.row-down:hover {
|
||||
background: color-mix(in srgb, var(--td-error-color) 10%, transparent);
|
||||
}
|
||||
|
||||
.text-disabled {
|
||||
color: var(--td-text-color-disabled);
|
||||
}
|
||||
|
||||
.tabular-nums {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.latency-ok {
|
||||
color: var(--td-success-color);
|
||||
}
|
||||
|
||||
.latency-warn {
|
||||
color: var(--td-warning-color);
|
||||
}
|
||||
|
||||
.latency-error {
|
||||
color: var(--td-error-color);
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.clickable-table {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.group-header {
|
||||
margin-bottom: var(--td-comp-margin-m);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.group-header .t-typography {
|
||||
margin: 0;
|
||||
font-size: var(--td-font-size-title-medium);
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.summary-cards-row {
|
||||
margin-bottom: var(--td-comp-margin-xl);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user