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 <cursoragent@cursor.com>
This commit is contained in:
2026-05-29 13:53:52 +08:00
parent 44fa5d3d82
commit f6fa4bb1ca
3 changed files with 87 additions and 1 deletions

View File

@@ -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<Build, "createdAt" | "updatedAt" | "status">,
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();
}

View File

@@ -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<Build[]>([]);
@@ -32,7 +33,10 @@ export default function BuildList() {
{build.appName} ({build.appNameEn})
</Link>
<div className="meta-line">
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()}
</div>
</li>

View File

@@ -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<Build | null>(null);
const [error, setError] = useState<string | null>(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 <p className="error-text"> ID</p>;
}
@@ -90,6 +104,22 @@ export default function JobStatus() {
</span>
</dd>
</div>
<div>
<dt></dt>
<dd>{formatDateTime(build.createdAt)}</dd>
</div>
{duration?.finished ? (
<div>
<dt></dt>
<dd>{formatDateTime(build.updatedAt)}</dd>
</div>
) : null}
<div>
<dt>{duration?.finished ? "构建总耗时" : "已用时间"}</dt>
<dd>
<strong>{duration?.text ?? "—"}</strong>
</dd>
</div>
</dl>
{build.status === "completed" ? (