83 行
3.6 KiB
TypeScript
83 行
3.6 KiB
TypeScript
import { AlertCircle, Check, Circle, Clock, ExternalLink } from "lucide-react";
|
|
import { Link } from "react-router-dom";
|
|
import type { ReimbursementProcessingProgress as ProcessingProgress } from "../../api/reimbursements";
|
|
import { formatDisplayDateTime } from "../../lib/displayFormat";
|
|
import { cn } from "../../lib/utils";
|
|
|
|
type Props = {
|
|
progress: ProcessingProgress;
|
|
mobile?: boolean;
|
|
};
|
|
|
|
const STATE_TEXT = {
|
|
completed: "已完成",
|
|
current: "处理中",
|
|
pending: "待处理",
|
|
exception: "需处理",
|
|
} as const;
|
|
|
|
export function ReimbursementProcessingProgress({ progress, mobile = false }: Props) {
|
|
return (
|
|
<ol
|
|
className="space-y-0"
|
|
data-testid={mobile ? "mobile-reimbursement-processing-progress" : "reimbursement-workflow-progress"}
|
|
aria-label="报销处理进度"
|
|
>
|
|
{progress.stages.map((stage, index) => {
|
|
const expanded = stage.state === "current" || stage.state === "exception" || Boolean(stage.comment);
|
|
const Icon = stage.state === "completed"
|
|
? Check
|
|
: stage.state === "current"
|
|
? Clock
|
|
: stage.state === "exception"
|
|
? AlertCircle
|
|
: Circle;
|
|
const facts = [stage.actorName, stage.occurredAt ? formatDisplayDateTime(stage.occurredAt) : null, stage.result]
|
|
.filter(Boolean);
|
|
|
|
return (
|
|
<li className="relative flex gap-3" data-state={stage.state} key={stage.key}>
|
|
{index < progress.stages.length - 1 ? (
|
|
<span className="absolute left-[15px] top-8 h-[calc(100%-1rem)] w-px bg-zinc-200" aria-hidden="true" />
|
|
) : null}
|
|
<span
|
|
className={cn(
|
|
"relative z-10 mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-full border bg-white",
|
|
stage.state === "completed" && "border-emerald-300 text-emerald-700",
|
|
stage.state === "current" && "border-blue-300 bg-blue-50 text-blue-700",
|
|
stage.state === "exception" && "border-red-300 bg-red-50 text-red-700",
|
|
stage.state === "pending" && "border-zinc-300 text-zinc-400",
|
|
)}
|
|
aria-hidden="true"
|
|
>
|
|
<Icon className="size-4" />
|
|
</span>
|
|
<div className={cn("min-w-0 flex-1 pb-5", mobile ? "pt-1" : "pt-1.5")}>
|
|
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-sm">
|
|
<span className="font-semibold text-zinc-900">{stage.label}</span>
|
|
{facts.length ? <span className="text-zinc-500">· {facts.join(" · ")}</span> : null}
|
|
{!stage.result ? <span className="text-xs text-zinc-500">· {STATE_TEXT[stage.state]}</span> : null}
|
|
</div>
|
|
{expanded && stage.detail ? <p className="mt-1 text-sm text-zinc-600">{stage.detail}</p> : null}
|
|
{stage.comment ? (
|
|
<blockquote className="mt-2 rounded-md border-l-2 border-zinc-300 bg-zinc-50 px-3 py-2 text-sm leading-5 text-zinc-700">
|
|
{stage.comment}
|
|
</blockquote>
|
|
) : null}
|
|
{stage.targetHref ? (
|
|
<Link
|
|
className="mt-2 inline-flex min-h-11 items-center gap-1 text-sm font-medium text-primary hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2"
|
|
to={stage.targetHref}
|
|
>
|
|
查看所在付款批次
|
|
<ExternalLink aria-hidden="true" className="size-4" />
|
|
</Link>
|
|
) : null}
|
|
</div>
|
|
</li>
|
|
);
|
|
})}
|
|
</ol>
|
|
);
|
|
}
|