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:
52
frontend/src/lib/duration.ts
Normal file
52
frontend/src/lib/duration.ts
Normal 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();
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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" ? (
|
||||
|
||||
Reference in New Issue
Block a user