Files
auto-virtual-tryon-frontend/src/features/reviews/components/review-image-panel.tsx

182 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}