feat: improve sku material loading feedback
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-03-19 13:30:01 +08:00
parent 5d1d2eb653
commit ae5f1309b1
2 changed files with 119 additions and 31 deletions

View File

@ -61,6 +61,9 @@ export function AgentWorkflowMaterialPicker({
const selectedCount = result?.images
? result.images.filter((item) => selectedSet.has(item.id)).length
: 0;
const hasSelectableImages = Boolean(
result?.images?.some((item) => Boolean(String(item.imageUrl || "").trim()))
);
const handleToggleImage = (imageId: string) => {
const next = new Set(selectedImageIds);
@ -73,7 +76,11 @@ export function AgentWorkflowMaterialPicker({
};
const handleSelectAll = () => {
onSelectedImageIdsChange(result?.images?.map((item) => item.id) || []);
onSelectedImageIdsChange(
result?.images
?.filter((item) => Boolean(String(item.imageUrl || "").trim()))
.map((item) => item.id) || []
);
};
return (
@ -229,7 +236,7 @@ export function AgentWorkflowMaterialPicker({
variant="outline"
size="sm"
onClick={handleSelectAll}
disabled={!result.images?.length}
disabled={!hasSelectableImages}
className={cn(
isDark &&
"border-white/12 bg-white/[0.04] text-white/82 hover:bg-white/[0.10] hover:text-white disabled:text-white/35"
@ -242,7 +249,7 @@ export function AgentWorkflowMaterialPicker({
variant="outline"
size="sm"
onClick={() => onSelectedImageIdsChange([])}
disabled={selectedCount === 0}
disabled={selectedCount === 0 && !hasSelectableImages}
className={cn(
isDark &&
"border-white/12 bg-white/[0.04] text-white/82 hover:bg-white/[0.10] hover:text-white disabled:text-white/35"
@ -316,16 +323,22 @@ export function AgentWorkflowMaterialPicker({
) : null}
{result.images?.length ? (
<div className="grid grid-cols-2 gap-3 xl:grid-cols-3">
<div className="grid grid-cols-2 gap-3 lg:grid-cols-3 xl:grid-cols-4">
{result.images.map((image) => {
const selected = selectedSet.has(image.id);
const isPlaceholder = !String(image.imageUrl || "").trim();
return (
<button
key={image.id}
type="button"
onClick={() => handleToggleImage(image.id)}
onClick={() => {
if (isPlaceholder) return;
handleToggleImage(image.id);
}}
disabled={isPlaceholder}
className={cn(
"group overflow-hidden rounded-[24px] border bg-background text-left transition-all",
isPlaceholder && "cursor-wait",
selected
? isDark
? "border-emerald-400/70 bg-white/[0.03] shadow-[0_0_0_1px_rgba(52,211,153,0.2)]"
@ -341,43 +354,55 @@ export function AgentWorkflowMaterialPicker({
isDark ? "bg-white/[0.04]" : "bg-muted/40"
)}
>
<img
src={image.imageUrl}
alt={image.label}
className="h-full w-full object-cover"
loading="lazy"
decoding="async"
onError={(event) => {
event.currentTarget.style.display = "none";
}}
/>
{isPlaceholder ? (
<div className="flex h-full w-full flex-col items-center justify-center gap-3 bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.08),transparent_55%),linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02))]">
<Loader2 className="h-6 w-6 animate-spin text-white/55" />
<div className="h-2 w-20 rounded-full bg-white/10" />
<div className="h-2 w-14 rounded-full bg-white/10" />
</div>
) : (
<img
src={image.imageUrl}
alt={image.label}
className="h-full w-full object-cover"
loading="lazy"
decoding="async"
onError={(event) => {
event.currentTarget.style.display = "none";
}}
/>
)}
<div className="absolute inset-x-0 top-0 flex items-center justify-between px-2 py-2">
<span className="rounded-full bg-black/60 px-2 py-1 text-[11px] text-white">
{image.group === "fixed" ? "固定角度" : "其他图片"}
</span>
<span
className={cn(
"flex h-6 w-6 items-center justify-center rounded-full border text-white transition-colors",
selected
? isDark
? "border-emerald-400 bg-emerald-400 text-black"
: "border-primary bg-primary"
: "border-white/60 bg-black/35 opacity-0 group-hover:opacity-100"
)}
>
<Check className="h-3.5 w-3.5" />
{isPlaceholder ? "加载中" : image.group === "fixed" ? "固定角度" : "其他图片"}
</span>
{!isPlaceholder ? (
<span
className={cn(
"flex h-6 w-6 items-center justify-center rounded-full border text-white transition-colors",
selected
? isDark
? "border-emerald-400 bg-emerald-400 text-black"
: "border-primary bg-primary"
: "border-white/60 bg-black/35 opacity-0 group-hover:opacity-100"
)}
>
<Check className="h-3.5 w-3.5" />
</span>
) : null}
</div>
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/75 via-black/25 to-transparent px-3 py-2 text-[11px] text-white">
{image.label}
{isPlaceholder ? "白底图加载中..." : image.label}
</div>
</div>
<div className="space-y-1 px-3 py-2">
<div className={cn("truncate text-xs font-medium", isDark ? "text-white/88" : "")}>
{image.fileName}
{isPlaceholder ? "正在获取 SKU 图片..." : image.fileName}
</div>
<div className={cn("text-[11px]", isDark ? "text-white/60" : "text-muted-foreground")}>
点击{selected ? "取消" : "选择"},可多选带入生图模式
{isPlaceholder
? "已先展示预览卡位,图片会逐步加载出来。"
: `点击${selected ? "取消" : "选择"},可多选带入生图模式`}
</div>
</div>
</button>

View File

@ -7,6 +7,7 @@ import { createEmptyAgentWorkflowMaterialSelection } from "@/lib/agents/workflow
import { cn } from "@/lib/utils";
import type { AgentWorkflowInputs } from "@/components/agents/AgentWorkflowPanel";
import type {
AgentWorkflowMaterialSearchResult,
AgentWorkflowMaterialQueryType,
AgentWorkflowMaterialSelections,
} from "@/types/agent";
@ -34,6 +35,38 @@ export function AgentWorkflowMaterialSelectionPanel({
disabledHint = "请先在下方选择一个生图模式,再启用 SKU / SPU 搜图。",
theme = "light",
}: AgentWorkflowMaterialSelectionPanelProps) {
const buildLoadingMaterialResult = (params: {
keyword: string;
currentResult?: AgentWorkflowMaterialSearchResult | null;
placeholderCount: number;
}): AgentWorkflowMaterialSearchResult => {
const count = Math.max(4, Math.min(params.placeholderCount || 8, 16));
return {
query: params.keyword,
queryType: "sku",
matchedSku: params.keyword,
matchedSpu: params.currentResult?.matchedSpu || null,
materialId: params.currentResult?.materialId || null,
productModel: params.currentResult?.productModel || null,
zipUrl: null,
fixedCount: count,
otherCount: 0,
totalCount: count,
skuOptions: [],
images: Array.from({ length: count }, (_, index) => ({
id: `loading-${params.keyword}-${index + 1}`,
key: `loading-${params.keyword}-${index + 1}`,
label: `加载中 ${index + 1}`,
fileName: `正在加载白底图 ${index + 1}`,
imageUrl: "",
assetId: null,
mimeType: null,
group: "fixed",
sortOrder: index,
})),
};
};
const [materialSearchState, setMaterialSearchState] = useState<{
primary: { isLoading: boolean; errorText: string | null };
secondary: { isLoading: boolean; errorText: string | null };
@ -186,10 +219,25 @@ export function AgentWorkflowMaterialSelectionPanel({
void handleSearchMaterials("primary");
}}
onSkuOptionSelect={(skuName) => {
const selectedOption = materials.primary.result?.skuOptions?.find(
(option) => option.skuName === skuName
);
const placeholderCount = selectedOption?.whiteBackgroundCount || 8;
onInputsChange({
...inputs,
primarySku: skuName,
});
onMaterialsChange({
...materials,
primary: {
result: buildLoadingMaterialResult({
keyword: skuName,
currentResult: materials.primary.result,
placeholderCount,
}),
selectedImageIds: [],
},
});
setMaterialSearchMode((prev) => ({
...prev,
primary: "sku",
@ -240,10 +288,25 @@ export function AgentWorkflowMaterialSelectionPanel({
void handleSearchMaterials("secondary");
}}
onSkuOptionSelect={(skuName) => {
const selectedOption = materials.secondary.result?.skuOptions?.find(
(option) => option.skuName === skuName
);
const placeholderCount = selectedOption?.whiteBackgroundCount || 8;
onInputsChange({
...inputs,
secondarySku: skuName,
});
onMaterialsChange({
...materials,
secondary: {
result: buildLoadingMaterialResult({
keyword: skuName,
currentResult: materials.secondary.result,
placeholderCount,
}),
selectedImageIds: [],
},
});
setMaterialSearchMode((prev) => ({
...prev,
secondary: "sku",