From f6fa4bb1cad0a1677bdc1745772a5168a8b6e5a4 Mon Sep 17 00:00:00 2001 From: shumengya Date: Fri, 29 May 2026 13:53:52 +0800 Subject: [PATCH] feat(frontend): show build duration on job status and list Display elapsed time while building and total duration from start to completion for success and failure. Co-authored-by: Cursor --- frontend/src/lib/duration.ts | 52 ++++++++++++++++++++++++++++++++ frontend/src/pages/BuildList.tsx | 6 +++- frontend/src/pages/JobStatus.tsx | 30 ++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 frontend/src/lib/duration.ts diff --git a/frontend/src/lib/duration.ts b/frontend/src/lib/duration.ts new file mode 100644 index 0000000..e963b3a --- /dev/null +++ b/frontend/src/lib/duration.ts @@ -0,0 +1,52 @@ +import type { Build, BuildStatus } from "../api/client"; + +function parseDbDate(value: string): number { + const normalized = value.includes("T") + ? value + : `${value.replace(" ", "T")}Z`; + const ms = Date.parse(normalized); + return Number.isNaN(ms) ? Date.now() : ms; +} + +export function formatDurationMs(ms: number): string { + const totalSec = Math.max(0, Math.floor(ms / 1000)); + if (totalSec < 60) { + return `${totalSec} 秒`; + } + + const min = Math.floor(totalSec / 60); + const sec = totalSec % 60; + if (min < 60) { + return sec > 0 ? `${min} 分 ${sec} 秒` : `${min} 分`; + } + + const hour = Math.floor(min / 60); + const remMin = min % 60; + return remMin > 0 ? `${hour} 小时 ${remMin} 分` : `${hour} 小时`; +} + +export function isBuildFinished(status: BuildStatus): boolean { + return status === "completed" || status === "failed"; +} + +export function getBuildDuration( + build: Pick, + now = Date.now(), +): { ms: number; text: string; finished: boolean } { + const start = parseDbDate(build.createdAt); + const finished = isBuildFinished(build.status); + const end = finished ? parseDbDate(build.updatedAt) : now; + const ms = Math.max(0, end - start); + + return { + ms, + finished, + text: finished + ? formatDurationMs(ms) + : `${formatDurationMs(ms)}(进行中)`, + }; +} + +export function formatDateTime(value: string): string { + return new Date(parseDbDate(value)).toLocaleString(); +} diff --git a/frontend/src/pages/BuildList.tsx b/frontend/src/pages/BuildList.tsx index 13f3f46..03322b2 100644 --- a/frontend/src/pages/BuildList.tsx +++ b/frontend/src/pages/BuildList.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import { fetchBuilds, statusLabel, type Build } from "../api/client"; +import { getBuildDuration, isBuildFinished } from "../lib/duration"; export default function BuildList() { const [builds, setBuilds] = useState([]); @@ -32,7 +33,10 @@ export default function BuildList() { {build.appName} ({build.appNameEn})
- v{build.appVersion} · {build.id} · {statusLabel(build.status)}{" "} + v{build.appVersion} · {statusLabel(build.status)} + {isBuildFinished(build.status) + ? ` · 耗时 ${getBuildDuration(build).text}` + : null}{" "} · {new Date(build.createdAt).toLocaleString()}
diff --git a/frontend/src/pages/JobStatus.tsx b/frontend/src/pages/JobStatus.tsx index 6d101c9..bc04130 100644 --- a/frontend/src/pages/JobStatus.tsx +++ b/frontend/src/pages/JobStatus.tsx @@ -1,11 +1,17 @@ import { useEffect, useState } from "react"; import { Link, useParams } from "react-router-dom"; import { fetchBuild, statusLabel, type Build } from "../api/client"; +import { + formatDateTime, + getBuildDuration, + isBuildFinished, +} from "../lib/duration"; export default function JobStatus() { const { id } = useParams<{ id: string }>(); const [build, setBuild] = useState(null); const [error, setError] = useState(null); + const [now, setNow] = useState(() => Date.now()); useEffect(() => { if (!id) return; @@ -37,6 +43,14 @@ export default function JobStatus() { }; }, [id]); + useEffect(() => { + if (!build || isBuildFinished(build.status)) return; + const timer = window.setInterval(() => setNow(Date.now()), 1000); + return () => window.clearInterval(timer); + }, [build?.status, build?.id]); + + const duration = build ? getBuildDuration(build, now) : null; + if (!id) { return

缺少任务 ID

; } @@ -90,6 +104,22 @@ export default function JobStatus() { +
+
开始时间
+
{formatDateTime(build.createdAt)}
+
+ {duration?.finished ? ( +
+
结束时间
+
{formatDateTime(build.updatedAt)}
+
+ ) : null} +
+
{duration?.finished ? "构建总耗时" : "已用时间"}
+
+ {duration?.text ?? "—"} +
+
{build.status === "completed" ? (