feat: bootstrap auto virtual tryon admin frontend

This commit is contained in:
afei A
2026-03-27 23:38:50 +08:00
commit 98c6b741d6
119 changed files with 19046 additions and 0 deletions

View File

@@ -0,0 +1,181 @@
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>
);
}