Files
ai-video-fullstack/frontend/src/components/batch-test/batch-run-completed.tsx
T

249 lines
8.1 KiB
TypeScript
Raw Normal View History

"use client";
/**
* 批量测试 — 已完成结果视图(MVP mock)。
*/
2026-08-08 19:08:48 +08:00
import { ChevronRight } from "lucide-react";
import { useState } from "react";
2026-08-08 19:08:48 +08:00
import { BatchCaseDetailDrawer } from "@/components/batch-test/batch-case-detail-drawer";
import {
BatchCaseStatusBadge,
BatchPhasePill,
} from "@/components/batch-test/batch-case-status";
import {
countByStatus,
formatRunTime,
type BatchRunSnapshot,
} from "@/data/batch-run";
import { cn } from "@/lib/utils";
export function BatchRunCompletedView({ run }: { run: BatchRunSnapshot }) {
2026-08-08 19:08:48 +08:00
const [selectedCaseId, setSelectedCaseId] = useState<string | null>(null);
const selectedCase =
run.cases.find((item) => item.id === selectedCaseId) ?? null;
const counts = countByStatus(run.cases);
const judged = counts.pass + counts.fail;
const total = run.cases.length;
const passRate = judged === 0 ? 0 : Math.round((counts.pass / judged) * 100);
const failRate = judged === 0 ? 0 : 100 - passRate;
return (
2026-08-08 19:08:48 +08:00
<>
<div className="mx-auto flex w-full max-w-[960px] flex-col gap-4">
{/* 结果总览 */}
<section className="rounded-2xl border border-hairline bg-card p-5 shadow-sm">
<div className="flex flex-wrap items-center gap-2">
<h2 className="text-base font-medium text-foreground">{run.title}</h2>
<BatchPhasePill phase="completed" />
</div>
2026-08-08 19:08:48 +08:00
{run.finishedAt && (
<p className="mt-1.5 text-xs text-muted-soft">
完成时间:{formatRunTime(run.finishedAt)}
{run.stopped ? " · 已手动停止" : ""}
</p>
)}
2026-08-08 19:08:48 +08:00
<div className="mt-5 flex flex-col items-center gap-6 sm:flex-row sm:justify-between sm:gap-8">
<div className="text-center sm:text-left">
<div className="font-display text-3xl text-ink">
{counts.pass} / {judged || total} 通过
</div>
<div className="mt-1 text-sm font-medium text-success">
通过率 {passRate}%
</div>
</div>
2026-08-08 19:08:48 +08:00
<PassRateDonut pass={counts.pass} fail={counts.fail} />
<div className="space-y-2 text-sm">
<LegendRow
2026-08-08 19:08:48 +08:00
tone="success"
label="通过"
count={counts.pass}
percent={passRate}
/>
2026-08-08 19:08:48 +08:00
<LegendRow
tone="destructive"
label="失败"
count={counts.fail}
percent={failRate}
/>
{counts.skipped > 0 && (
<LegendRow
tone="muted"
label="未执行"
count={counts.skipped}
percent={
total === 0
? 0
: Math.round((counts.skipped / total) * 100)
}
/>
)}
</div>
</div>
2026-08-08 19:08:48 +08:00
</section>
2026-08-08 19:08:48 +08:00
{/* 用例列表 */}
<section className="rounded-2xl border border-hairline bg-card shadow-sm">
<div className="border-b border-hairline px-5 py-3.5">
<h3 className="text-sm font-medium text-foreground">
用例列表(共 {total} 个)
</h3>
</div>
2026-08-08 19:08:48 +08:00
<div className="overflow-x-auto">
<table className="w-full min-w-[560px] text-left text-sm">
<thead>
<tr className="border-b border-hairline text-xs text-muted-soft">
<th className="w-12 px-5 py-2.5 font-medium">#</th>
<th className="px-3 py-2.5 font-medium">测试用例</th>
<th className="w-28 px-3 py-2.5 font-medium">状态</th>
<th className="w-24 px-3 py-2.5 font-medium">详情</th>
</tr>
</thead>
<tbody>
{run.cases.map((item, index) => {
return (
<tr
key={item.id}
className="border-b border-hairline last:border-b-0"
>
<td colSpan={4} className="p-0">
<button
type="button"
onClick={() => setSelectedCaseId(item.id)}
aria-haspopup="dialog"
className={cn(
"flex w-full items-center gap-0 px-5 py-3 text-left transition-colors hover:bg-canvas-soft/70",
selectedCaseId === item.id && "bg-canvas-soft",
)}
>
<span className="w-12 shrink-0 pt-0.5 tabular-nums text-muted-soft">
{index + 1}
</span>
2026-08-08 19:08:48 +08:00
<span className="min-w-0 flex-1 px-3">
<span className="block font-medium text-foreground">
{item.name}
</span>
2026-08-08 19:08:48 +08:00
</span>
<span className="w-28 shrink-0 px-3">
<BatchCaseStatusBadge status={item.status} />
</span>
<span className="flex w-24 shrink-0 items-center gap-1 px-3 text-muted-foreground">
查看
<ChevronRight size={14} />
</span>
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</section>
</div>
2026-08-08 19:08:48 +08:00
<BatchCaseDetailDrawer
item={selectedCase}
assistantName={run.assistantName}
onClose={() => setSelectedCaseId(null)}
/>
</>
);
}
function PassRateDonut({ pass, fail }: { pass: number; fail: number }) {
const total = pass + fail;
const passRatio = total === 0 ? 0 : pass / total;
const size = 112;
const stroke = 14;
const radius = (size - stroke) / 2;
const circumference = 2 * Math.PI * radius;
const passLength = circumference * passRatio;
const failLength = circumference - passLength;
return (
<div
className="relative shrink-0"
style={{ width: size, height: size }}
aria-hidden
>
<svg width={size} height={size} className="-rotate-90">
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke="currentColor"
strokeWidth={stroke}
className="text-surface-strong"
/>
{total > 0 && (
<>
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke="currentColor"
strokeWidth={stroke}
strokeDasharray={`${passLength} ${circumference}`}
strokeLinecap="butt"
className="text-success"
/>
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke="currentColor"
strokeWidth={stroke}
strokeDasharray={`${failLength} ${circumference}`}
strokeDashoffset={-passLength}
strokeLinecap="butt"
className="text-destructive"
/>
</>
)}
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center">
<span className="text-lg font-medium tabular-nums text-foreground">
{total === 0 ? 0 : Math.round((pass / total) * 100)}%
</span>
</div>
</div>
);
}
function LegendRow({
tone,
label,
count,
percent,
}: {
tone: "success" | "destructive" | "muted";
label: string;
count: number;
percent: number;
}) {
const dotClass = {
success: "bg-success",
destructive: "bg-destructive",
muted: "bg-muted-soft",
}[tone];
return (
<div className="flex items-center gap-2 text-muted-foreground">
<span className={cn("size-2.5 rounded-full", dotClass)} />
<span>
{label} {count}({percent}%)
</span>
</div>
);
}