182 lines
6.7 KiB
TypeScript
182 lines
6.7 KiB
TypeScript
import { Card, CardContent, CardDescription, CardEyebrow, CardHeader, CardTitle } from "@/components/ui/card";
|
||
import { EmptyState } from "@/components/ui/empty-state";
|
||
import { StatusBadge } from "@/components/ui/status-badge";
|
||
import type { AssetViewModel, OrderDetailVM } from "@/lib/types/view-models";
|
||
|
||
type ReviewImagePanelProps = {
|
||
error: string | null;
|
||
isLoading: boolean;
|
||
order: OrderDetailVM | null;
|
||
selectedAssetId: number | null;
|
||
onSelectAsset: (assetId: number) => void;
|
||
};
|
||
|
||
function joinClasses(...values: Array<string | false | null | undefined>) {
|
||
return values.filter(Boolean).join(" ");
|
||
}
|
||
|
||
function collectAssets(order: OrderDetailVM): AssetViewModel[] {
|
||
const uniqueAssets = new Map<number, AssetViewModel>();
|
||
|
||
if (order.finalAsset) {
|
||
uniqueAssets.set(order.finalAsset.id, order.finalAsset);
|
||
}
|
||
|
||
for (const asset of order.assets) {
|
||
uniqueAssets.set(asset.id, asset);
|
||
}
|
||
|
||
return Array.from(uniqueAssets.values());
|
||
}
|
||
|
||
export function ReviewImagePanel({
|
||
error,
|
||
isLoading,
|
||
order,
|
||
selectedAssetId,
|
||
onSelectAsset,
|
||
}: ReviewImagePanelProps) {
|
||
if (isLoading) {
|
||
return (
|
||
<Card className="h-full">
|
||
<CardContent className="px-6 py-8 text-sm text-[var(--ink-muted)]">
|
||
正在加载订单详情与预览资产…
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
if (error) {
|
||
return (
|
||
<Card className="h-full">
|
||
<CardContent className="px-6 py-8">
|
||
<div className="rounded-[24px] border border-[#b88472] bg-[#f8ece5] px-5 py-4 text-sm text-[#7f4b3b]">
|
||
{error}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
if (!order) {
|
||
return (
|
||
<Card className="h-full">
|
||
<CardContent className="px-6 py-8">
|
||
<EmptyState
|
||
eyebrow="No active order"
|
||
title="选择一个待审核订单"
|
||
description="左侧队列选中订单后,这里会展示当前审核所需的结果图和过程资产。"
|
||
/>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
const assets = collectAssets(order);
|
||
const selectedAsset =
|
||
assets.find((asset) => asset.id === selectedAssetId) ??
|
||
order.finalAsset ??
|
||
assets[0] ??
|
||
null;
|
||
const emptyStateTitle =
|
||
order.finalAssetState.kind === "business-empty"
|
||
? order.finalAssetState.title
|
||
: "暂无可用预览";
|
||
const emptyStateDescription =
|
||
order.finalAssetState.kind === "business-empty"
|
||
? order.finalAssetState.description
|
||
: "当前订单还没有能用于审核的结果图或过程资产。";
|
||
|
||
return (
|
||
<Card className="h-full">
|
||
<CardHeader className="gap-4">
|
||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||
<div className="space-y-2">
|
||
<CardEyebrow>Review target</CardEyebrow>
|
||
<div className="space-y-1">
|
||
<CardTitle>订单 #{order.orderId}</CardTitle>
|
||
<CardDescription>
|
||
当前服务模式为 {order.serviceMode},步骤停留在 {order.currentStepLabel}。
|
||
</CardDescription>
|
||
</div>
|
||
</div>
|
||
<div className="flex flex-wrap gap-2">
|
||
<StatusBadge status={order.status} />
|
||
<StatusBadge variant="workflowStep" status={order.currentStep} />
|
||
</div>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="space-y-6">
|
||
{selectedAsset ? (
|
||
<div className="rounded-[28px] border border-[var(--border-soft)] bg-[linear-gradient(180deg,rgba(250,247,242,0.96),rgba(238,231,221,0.92))] p-5">
|
||
<div className="flex min-h-[320px] items-center justify-center rounded-[22px] border border-dashed border-[var(--border-strong)] bg-[rgba(255,255,255,0.55)] p-6 text-center">
|
||
<div className="space-y-3">
|
||
<p className="font-[var(--font-mono)] text-[11px] uppercase tracking-[0.22em] text-[var(--ink-faint)]">
|
||
{selectedAsset.isMock ? "Mock preview asset" : "Preview asset"}
|
||
</p>
|
||
<h3 className="text-xl font-semibold tracking-[-0.02em] text-[var(--ink-strong)]">
|
||
{selectedAsset.label}
|
||
</h3>
|
||
<p className="mx-auto max-w-lg text-sm leading-7 text-[var(--ink-muted)]">
|
||
当前阶段优先保证审核流程可用,因此预览面板直接展示资产信息卡;真实图片 URI 可在后续 Task 7 的详情页复用。
|
||
</p>
|
||
<code className="inline-flex rounded-full bg-[rgba(74,64,53,0.08)] px-4 py-2 text-xs text-[var(--ink-muted)]">
|
||
{selectedAsset.uri}
|
||
</code>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<EmptyState
|
||
eyebrow="Asset empty"
|
||
title={emptyStateTitle}
|
||
description={emptyStateDescription}
|
||
/>
|
||
)}
|
||
|
||
{order.hasMockAssets ? (
|
||
<div className="rounded-[24px] border border-[rgba(145,104,46,0.2)] bg-[rgba(202,164,97,0.12)] px-5 py-4 text-sm text-[#7a5323]">
|
||
当前订单包含 mock 资产,审核结论仅用于前后台联调,不代表真实生产结果。
|
||
</div>
|
||
) : null}
|
||
|
||
{assets.length ? (
|
||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||
{assets.map((asset) => {
|
||
const isSelected = asset.id === selectedAsset?.id;
|
||
|
||
return (
|
||
<button
|
||
key={asset.id}
|
||
type="button"
|
||
onClick={() => onSelectAsset(asset.id)}
|
||
className={joinClasses(
|
||
"rounded-[24px] border px-4 py-4 text-left transition duration-150",
|
||
isSelected
|
||
? "border-[var(--accent-primary)] bg-[rgba(110,127,82,0.09)]"
|
||
: "border-[var(--border-soft)] bg-[var(--surface-muted)] hover:bg-[var(--surface)]",
|
||
)}
|
||
>
|
||
<div className="flex items-start justify-between gap-3">
|
||
<div className="space-y-1">
|
||
<p className="text-sm font-semibold text-[var(--ink-strong)]">
|
||
{asset.label}
|
||
</p>
|
||
<p className="text-xs text-[var(--ink-muted)]">{asset.stepLabel}</p>
|
||
</div>
|
||
{asset.isMock ? (
|
||
<span className="rounded-full bg-[rgba(145,104,46,0.12)] px-2.5 py-1 font-[var(--font-mono)] text-[11px] uppercase tracking-[0.18em] text-[#7a5323]">
|
||
Mock
|
||
</span>
|
||
) : null}
|
||
</div>
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
) : null}
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|