diff --git a/frontend/src/pages/dashboard/DashboardPage.tsx b/frontend/src/pages/dashboard/DashboardPage.tsx
index f661665..6386711 100644
--- a/frontend/src/pages/dashboard/DashboardPage.tsx
+++ b/frontend/src/pages/dashboard/DashboardPage.tsx
@@ -1,7 +1,7 @@
-import { useEffect, useState } from "react";
+import { useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import ReactECharts from "echarts-for-react";
-import { Card, Col, Row, Spin, Statistic } from "antd";
+import { Card, Col, Row, Spin, Tag, Typography } from "antd";
import dayjs from "dayjs";
import {
dashboardApi,
@@ -9,10 +9,44 @@ import {
type LatestBatchItem,
} from "../../services";
+const pageBg = "#f4f8fc";
+/** 与 `src/styles/global.css` body、侧边栏 DT-Report 一致的系统字体栈 */
+const appFontFamily =
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif';
+/**
+ * 环形图:与侧栏 #2e3b7c、菜单高亮 #667eea 同系蓝紫,
+ * 通过偏浅亮(接近高光),失败偏深靛(略向紫偏),色相一致、主要靠明度与饱和度区分。
+ */
+const donutPassColor = "#a8b9ee";
+const donutFailColor = "#4a5a9a";
+
+function renderBatchResultTag(result: string | null | undefined) {
+ const label = (result || "").trim();
+ if (!label) {
+ return (
+
+ —
+
+ );
+ }
+ const raw = label.toLowerCase();
+ const fail = raw.includes("fail") || raw.includes("失败") || raw.includes("error");
+ const ok = raw.includes("pass") || raw.includes("成功") || raw === "success";
+ return (
+
+ {label}
+
+ );
+}
+
function buildChartOption(items: BatchTrendItem[]) {
return {
tooltip: {
trigger: "axis" as const,
+ axisPointer: { type: "cross" as const, label: { backgroundColor: "#6a7985" } },
formatter: (params: unknown) => {
const arr = Array.isArray(params) ? params : [];
if (arr.length === 0 || !arr[0]) return "";
@@ -34,32 +68,150 @@ function buildChartOption(items: BatchTrendItem[]) {
},
legend: {
data: ["失败用例数", "总用例数"],
- bottom: 0,
+ top: 4,
+ itemGap: 20,
},
- grid: { left: "3%", right: "4%", bottom: "15%", top: "8%", containLabel: true },
+ grid: { left: "2%", right: "3%", bottom: "12%", top: "16%", containLabel: true },
xAxis: {
type: "category" as const,
+ boundaryGap: false,
data: items.map((i) => i.batch),
- axisLabel: { rotate: 45 },
+ axisLine: { lineStyle: { color: "#d9d9d9" } },
+ axisLabel: {
+ rotate: 38,
+ fontSize: 11,
+ color: "#595959",
+ margin: 12,
+ hideOverlap: true,
+ },
},
yAxis: {
type: "value" as const,
- splitLine: { show: false },
+ minInterval: 1,
+ splitLine: {
+ show: true,
+ lineStyle: { color: "#ebeef2", type: "dashed" as const },
+ },
+ axisLabel: { color: "#8c8c8c", fontSize: 11 },
},
series: [
{
name: "失败用例数",
type: "line" as const,
+ smooth: true,
+ symbol: "circle",
+ symbolSize: 7,
data: items.map((i) => i.failed_num),
itemStyle: { color: "#ff4d4f" },
- label: { show: true, position: "top" as const },
+ lineStyle: { width: 2 },
+ areaStyle: { color: "rgba(255, 77, 79, 0.12)" },
+ label: { show: true, position: "top" as const, fontSize: 11, color: "#595959" },
+ emphasis: { focus: "series" as const },
},
{
name: "总用例数",
type: "line" as const,
+ smooth: true,
+ symbol: "circle",
+ symbolSize: 7,
data: items.map((i) => i.total_case_num),
- itemStyle: { color: "#1890ff" },
- label: { show: true, position: "top" as const },
+ itemStyle: { color: "#1677ff" },
+ lineStyle: { width: 2 },
+ areaStyle: { color: "rgba(22, 119, 255, 0.14)" },
+ label: { show: true, position: "top" as const, fontSize: 11, color: "#595959" },
+ emphasis: { focus: "series" as const },
+ },
+ ],
+ };
+}
+
+function buildDonutOption(passed: number, failed: number, totalCaseNum: number) {
+ const sum = passed + failed;
+ const centerTotal =
+ typeof totalCaseNum === "number" && totalCaseNum >= 0 ? totalCaseNum : sum;
+
+ const centerGraphic = [
+ {
+ type: "text" as const,
+ left: "center",
+ top: "40%",
+ style: {
+ text: sum === 0 && centerTotal === 0 ? "—" : String(centerTotal),
+ textAlign: "center" as const,
+ textVerticalAlign: "middle" as const,
+ fill: "#102a43",
+ fontSize: 32,
+ fontWeight: 700,
+ fontFamily: appFontFamily,
+ },
+ },
+ {
+ type: "text" as const,
+ left: "center",
+ top: "50%",
+ style: {
+ text: "总用例数",
+ textAlign: "center" as const,
+ fill: "#8c8c8c",
+ fontSize: 14,
+ fontFamily: appFontFamily,
+ },
+ },
+ ];
+
+ if (sum === 0) {
+ return {
+ graphic: centerGraphic,
+ title: {
+ text: "暂无通过/失败构成数据",
+ left: "center",
+ top: "62%",
+ textStyle: { color: "#bfbfbf", fontSize: 13, fontWeight: 400, fontFamily: appFontFamily },
+ },
+ };
+ }
+
+ return {
+ tooltip: {
+ trigger: "item" as const,
+ formatter: "{b}
数量:{c}
占比:{d}%",
+ },
+ legend: {
+ orient: "horizontal" as const,
+ bottom: 4,
+ itemGap: 16,
+ textStyle: { color: "#595959", fontSize: 13, fontFamily: appFontFamily },
+ },
+ graphic: centerGraphic,
+ color: [donutPassColor, donutFailColor],
+ series: [
+ {
+ name: "本批次",
+ type: "pie" as const,
+ radius: ["46%", "74%"],
+ center: ["50%", "46%"],
+ avoidLabelOverlap: true,
+ itemStyle: {
+ borderRadius: 6,
+ borderColor: "#fff",
+ borderWidth: 2,
+ },
+ label: {
+ show: true,
+ formatter: "{b}\n{c} ({d}%)",
+ fontSize: 13,
+ color: "#434343",
+ fontFamily: appFontFamily,
+ },
+ labelLine: {
+ length: 14,
+ length2: 10,
+ lineStyle: { color: "#98a6c4", width: 1 },
+ },
+ data: [
+ { value: passed, name: "通过", itemStyle: { color: donutPassColor } },
+ { value: failed, name: "失败", itemStyle: { color: donutFailColor } },
+ ],
},
],
};
@@ -135,116 +287,238 @@ export default function DashboardPage() {
const hasLatestBatch = latestBatch && Object.keys(latestBatch).length > 0;
+ const donutOption = useMemo(() => {
+ if (!hasLatestBatch || !latestBatch) return null;
+ const p = latestBatch.passed_num ?? 0;
+ const f = latestBatch.failed_num ?? 0;
+ const total = latestBatch.total_case_num ?? 0;
+ return buildDonutOption(p, f, total);
+ }, [hasLatestBatch, latestBatch]);
+
+ const masterOption = useMemo(
+ () => (masterTrendItems.length ? buildChartOption(masterTrendItems) : null),
+ [masterTrendItems]
+ );
+ const bugfixOption = useMemo(
+ () => (bugfixTrendItems.length ? buildChartOption(bugfixTrendItems) : null),
+ [bugfixTrendItems]
+ );
+
+ const branchChartsLoading = masterTrendLoading || bugfixTrendLoading;
+
return (
-
首页大盘
+
+ 首页大盘
+
- {/* 最新批次状态卡片 */}
-
+
{loading ? (
-
+
) : !hasLatestBatch ? (
-
- 暂无批次数据
-
+
暂无批次数据
) : (
-
handleCardClick(latestBatch!.batch)}
- style={{ cursor: "pointer" }}
- >
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ {donutOption && (
+ handleCardClick(latestBatch!.batch),
+ }}
+ />
+ )}
+
-
-
+
+
+
+
+
+
+ 批次
+
+
+ {latestBatch!.batch}
+
+
+
+
+
+
+ 执行时间
+
+
+ {latestBatch!.batch_start
+ ? dayjs(latestBatch!.batch_start).format("YYYY-MM-DD HH:mm:ss")
+ : "—"}
+
+
+
+
+
+
+ 批次结果
+
+
{renderBatchResultTag(latestBatch!.result)}
+
+
+
+
)}
- {/* master 分支趋势折线图 */}
master 分支最近 batch 执行情况
}
- style={{ marginBottom: 24 }}
+ title="各分支最近 batch 执行情况"
+ variant="borderless"
+ style={{
+ borderRadius: 10,
+ boxShadow: "0 1px 3px rgba(15, 37, 64, 0.06)",
+ }}
+ styles={{ body: { paddingTop: 8 } }}
>
- {masterTrendLoading ? (
-
+ {branchChartsLoading ? (
+
- ) : masterTrendItems.length === 0 ? (
-
- 暂无趋势数据
-
+ ) : masterTrendItems.length === 0 && bugfixTrendItems.length === 0 ? (
+
暂无趋势数据
) : (
-
- )}
-
-
- {/* bugfix 分支趋势折线图 */}
-
bugfix 分支最近 batch 执行情况 }
- >
- {bugfixTrendLoading ? (
-
-
-
- ) : bugfixTrendItems.length === 0 ? (
-
- 暂无趋势数据
-
- ) : (
-
+
+
+
+ master
+
+ {masterOption ? (
+
+ ) : (
+ 暂无 master 数据
+ )}
+
+
+
+ bugfix
+
+ {bugfixOption ? (
+
+ ) : (
+ 暂无 bugfix 数据
+ )}
+
+
)}