fix: guard material search client crashes
This commit is contained in:
parent
531a23d15f
commit
e8796c74a6
@ -63,6 +63,40 @@ const MATERIAL_GRID_MIN_CARD_HEIGHT = 258;
|
||||
const AGENT_STYLE_OPTIMIZED_RENDER_ENABLED =
|
||||
process.env.NEXT_PUBLIC_AGENT_STYLE_OPTIMIZED_RENDER !== "false";
|
||||
|
||||
function normalizeImageIdList(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
return value.map((item) => String(item || "").trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
function normalizeMaterialImages(value: unknown): AgentWorkflowMaterialImage[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
return value.filter((item): item is AgentWorkflowMaterialImage => {
|
||||
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
||||
return false;
|
||||
}
|
||||
const image = item as Partial<AgentWorkflowMaterialImage>;
|
||||
return Boolean(String(image.id || "").trim());
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeSkuOptions(
|
||||
value: AgentWorkflowMaterialSearchResult["skuOptions"] | unknown
|
||||
): NonNullable<AgentWorkflowMaterialSearchResult["skuOptions"]> {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
return value.filter((item): item is NonNullable<AgentWorkflowMaterialSearchResult["skuOptions"]>[number] => {
|
||||
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
||||
return false;
|
||||
}
|
||||
return Boolean(String(item.skuName || "").trim());
|
||||
});
|
||||
}
|
||||
|
||||
const MaterialImageCard = memo(function MaterialImageCard({
|
||||
image,
|
||||
selected,
|
||||
@ -327,10 +361,14 @@ export function AgentWorkflowMaterialPicker({
|
||||
const canUploadMaterialImage = Boolean(onUploadImage);
|
||||
const [previewDisplayMode, setPreviewDisplayMode] = useState<"selected" | "all">("all");
|
||||
const [hasManualSelectionInteraction, setHasManualSelectionInteraction] = useState(false);
|
||||
const selectedSet = useMemo(
|
||||
() => new Set((selectedImageIds || []).map((item) => String(item || "").trim()).filter(Boolean)),
|
||||
const normalizedSelectedImageIds = useMemo(
|
||||
() => normalizeImageIdList(selectedImageIds),
|
||||
[selectedImageIds]
|
||||
);
|
||||
const selectedSet = useMemo(
|
||||
() => new Set(normalizedSelectedImageIds),
|
||||
[normalizedSelectedImageIds]
|
||||
);
|
||||
const recommendedImageIds = useMemo(
|
||||
() =>
|
||||
Array.isArray(result?.recommendedImageIds)
|
||||
@ -338,7 +376,14 @@ export function AgentWorkflowMaterialPicker({
|
||||
: [],
|
||||
[result?.recommendedImageIds]
|
||||
);
|
||||
const allImages = result?.images || [];
|
||||
const allImages = useMemo(
|
||||
() => normalizeMaterialImages(result?.images),
|
||||
[result?.images]
|
||||
);
|
||||
const skuOptions = useMemo(
|
||||
() => normalizeSkuOptions(result?.skuOptions),
|
||||
[result?.skuOptions]
|
||||
);
|
||||
const uploadedImages = useMemo(
|
||||
() =>
|
||||
allImages
|
||||
@ -458,7 +503,7 @@ export function AgentWorkflowMaterialPicker({
|
||||
|
||||
const handleToggleImage = useCallback((imageId: string) => {
|
||||
setHasManualSelectionInteraction(true);
|
||||
const next = new Set(selectedImageIds);
|
||||
const next = new Set(normalizedSelectedImageIds);
|
||||
if (next.has(imageId)) {
|
||||
next.delete(imageId);
|
||||
} else {
|
||||
@ -475,7 +520,7 @@ export function AgentWorkflowMaterialPicker({
|
||||
}, [
|
||||
onSelectedImageIdsChange,
|
||||
resolvedSelectionLimit,
|
||||
selectedImageIds,
|
||||
normalizedSelectedImageIds,
|
||||
selectionLimitLabel,
|
||||
]);
|
||||
|
||||
@ -1042,9 +1087,9 @@ export function AgentWorkflowMaterialPicker({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{result.skuOptions?.length ? (
|
||||
{skuOptions.length ? (
|
||||
<div className="grid grid-cols-1 gap-3 xl:grid-cols-2">
|
||||
{result.skuOptions.map((option) => (
|
||||
{skuOptions.map((option) => (
|
||||
<div
|
||||
key={`${option.skuName}-${option.skuId || "sku"}`}
|
||||
className={cn(
|
||||
@ -1183,7 +1228,7 @@ export function AgentWorkflowMaterialPicker({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(result?.images?.length || result?.skuOptions?.length) ? (
|
||||
{(allImages.length || skuOptions.length) ? (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-start gap-2 rounded-[20px] px-3 py-2 text-xs",
|
||||
@ -1191,7 +1236,7 @@ export function AgentWorkflowMaterialPicker({
|
||||
)}
|
||||
>
|
||||
<RefreshCw className="mt-0.5 h-3.5 w-3.5 shrink-0" />
|
||||
{result?.skuOptions?.length
|
||||
{skuOptions.length
|
||||
? "当前是 SPU 命中结果,请先选择具体 SKU,再继续拉取白底图。"
|
||||
: totalImageCount > 0
|
||||
? "白底图会先按权重自动选入槽位,如需微调仍可手动调整。"
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Plus, Sparkles } from "lucide-react";
|
||||
import { AgentWorkflowMaterialPicker } from "@/components/agents/AgentWorkflowMaterialPicker";
|
||||
import { ClientErrorBoundary } from "@/components/ui/client-error-boundary";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { createEmptyAgentWorkflowMaterialSelection } from "@/lib/agents/workflow-material-config";
|
||||
import { cn } from "@/lib/utils";
|
||||
@ -31,6 +32,58 @@ interface AgentWorkflowMaterialSelectionPanelProps {
|
||||
theme?: "light" | "dark";
|
||||
}
|
||||
|
||||
function normalizeImageIdList(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
return value.map((item) => String(item || "").trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
function normalizeMaterialImages(value: unknown): AgentWorkflowMaterialImage[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
return value.filter((item): item is AgentWorkflowMaterialImage => {
|
||||
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
||||
return false;
|
||||
}
|
||||
const image = item as Partial<AgentWorkflowMaterialImage>;
|
||||
return Boolean(String(image.id || "").trim());
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeMaterialSearchResult(
|
||||
result?: AgentWorkflowMaterialSearchResult | null
|
||||
): AgentWorkflowMaterialSearchResult | null {
|
||||
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...result,
|
||||
images: normalizeMaterialImages(result.images),
|
||||
recommendedImageIds: normalizeImageIdList(result.recommendedImageIds),
|
||||
skuOptions: Array.isArray(result.skuOptions)
|
||||
? result.skuOptions.filter((item) => Boolean(String(item?.skuName || "").trim()))
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeMaterialSelections(
|
||||
value: AgentWorkflowMaterialSelections
|
||||
): AgentWorkflowMaterialSelections {
|
||||
return {
|
||||
primary: {
|
||||
result: normalizeMaterialSearchResult(value?.primary?.result),
|
||||
selectedImageIds: normalizeImageIdList(value?.primary?.selectedImageIds),
|
||||
},
|
||||
secondary: {
|
||||
result: normalizeMaterialSearchResult(value?.secondary?.result),
|
||||
selectedImageIds: normalizeImageIdList(value?.secondary?.selectedImageIds),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getUploadedWorkflowMaterialImages(
|
||||
result?: AgentWorkflowMaterialSearchResult | null
|
||||
): AgentWorkflowMaterialImage[] {
|
||||
@ -47,13 +100,15 @@ function getSelectedWorkflowMaterialImages(params: {
|
||||
selectedImageIds: string[];
|
||||
}): AgentWorkflowMaterialImage[] {
|
||||
const selectedIds = new Set(
|
||||
(params.selectedImageIds || []).map((item) => String(item || "").trim()).filter(Boolean)
|
||||
normalizeImageIdList(params.selectedImageIds)
|
||||
);
|
||||
if (!params.result || selectedIds.size === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (params.result.images || []).filter((image) => selectedIds.has(image.id));
|
||||
return normalizeMaterialImages(params.result.images).filter((image) =>
|
||||
selectedIds.has(image.id)
|
||||
);
|
||||
}
|
||||
|
||||
function syncWorkflowMaterialAppearanceSummary(params: {
|
||||
@ -101,7 +156,7 @@ function syncWorkflowMaterialAppearanceSummary(params: {
|
||||
}
|
||||
|
||||
let changed = false;
|
||||
const nextImages = result.images.map((image) => {
|
||||
const nextImages = normalizeMaterialImages(result.images).map((image) => {
|
||||
if (image.id !== targetImage.id) {
|
||||
return image;
|
||||
}
|
||||
@ -217,7 +272,9 @@ function mergeSearchResultWithUploadedImages(params: {
|
||||
currentResult?: AgentWorkflowMaterialSearchResult | null;
|
||||
}): AgentWorkflowMaterialSearchResult {
|
||||
const uploadedImages = getUploadedWorkflowMaterialImages(params.currentResult);
|
||||
const searchImages = markSearchWorkflowMaterialImages(params.searchResult.images || []);
|
||||
const searchImages = markSearchWorkflowMaterialImages(
|
||||
normalizeMaterialImages(params.searchResult.images)
|
||||
);
|
||||
return buildWorkflowMaterialResult({
|
||||
currentResult: {
|
||||
...params.searchResult,
|
||||
@ -229,10 +286,34 @@ function mergeSearchResultWithUploadedImages(params: {
|
||||
});
|
||||
}
|
||||
|
||||
function MaterialPickerErrorFallback({
|
||||
isDark,
|
||||
title,
|
||||
}: {
|
||||
isDark: boolean;
|
||||
title: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-[24px] border px-4 py-5 text-sm leading-6",
|
||||
isDark
|
||||
? "border-amber-300/20 bg-amber-300/[0.08] text-amber-50"
|
||||
: "border-amber-200 bg-amber-50 text-amber-900"
|
||||
)}
|
||||
>
|
||||
<div className="font-medium">{title}暂时无法渲染</div>
|
||||
<div className={cn("mt-1 text-xs", isDark ? "text-amber-50/70" : "text-amber-800/75")}>
|
||||
已拦截本次客户端异常,避免整个风格化工作台崩溃。请修改搜索词或刷新后重试。
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AgentWorkflowMaterialSelectionPanel({
|
||||
inputs,
|
||||
onInputsChange,
|
||||
materials,
|
||||
materials: rawMaterials,
|
||||
onMaterialsChange,
|
||||
appearanceRecognitionEnabled = false,
|
||||
onAppearanceRecognitionEnabledChange,
|
||||
@ -245,6 +326,10 @@ export function AgentWorkflowMaterialSelectionPanel({
|
||||
disabledHint = "请先在下方选择一个生图模式,再启用 SKU / SPU 搜图。",
|
||||
theme = "light",
|
||||
}: AgentWorkflowMaterialSelectionPanelProps) {
|
||||
const materials = useMemo(
|
||||
() => normalizeMaterialSelections(rawMaterials),
|
||||
[rawMaterials]
|
||||
);
|
||||
const hasSecondaryMaterialContent =
|
||||
Boolean(String(inputs.secondarySku || "").trim()) ||
|
||||
Boolean(materials.secondary.result) ||
|
||||
@ -890,174 +975,75 @@ export function AgentWorkflowMaterialSelectionPanel({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AgentWorkflowMaterialPicker
|
||||
value={inputs.primarySku}
|
||||
queryMode={materialSearchMode.primary}
|
||||
placeholder={materialSearchMode.primary === "spu" ? "输入主 SPU" : "输入主 SKU"}
|
||||
buttonLabel={materialSearchMode.primary === "spu" ? "查SKU列表" : "搜索主图"}
|
||||
searchNote={
|
||||
materialSearchMode.primary === "spu"
|
||||
? "按 SPU 搜索时会先返回该 SPU 下的 SKU 列表,点选某个 SKU 后再继续拉取白底图。"
|
||||
: "按 SKU 搜索会直接拉取白底图,适合你已经知道具体 SKU 的情况。"
|
||||
}
|
||||
disabled={disabled}
|
||||
disabledHint={disabledHint}
|
||||
isLoading={materialSearchState.primary.isLoading}
|
||||
errorText={materialSearchState.primary.errorText}
|
||||
result={materials.primary.result}
|
||||
selectedImageIds={materials.primary.selectedImageIds}
|
||||
appearanceRecognitionEnabled={appearanceRecognitionEnabled}
|
||||
selectionLimit={primarySelectionLimit}
|
||||
selectionLimitLabel={selectionLimitLabel}
|
||||
onValueChange={(value) => handleMaterialInputChange("primary", value)}
|
||||
onQueryModeChange={(nextMode) =>
|
||||
setMaterialSearchMode((prev) => ({
|
||||
...prev,
|
||||
primary: nextMode,
|
||||
}))
|
||||
}
|
||||
onSearch={() => {
|
||||
void handleSearchMaterials("primary");
|
||||
}}
|
||||
onUploadImage={(file, options) => handleUploadMaterialImage("primary", file, options)}
|
||||
onRemoveUploadedImage={(imageId) =>
|
||||
handleRemoveUploadedMaterialImage("primary", imageId)
|
||||
}
|
||||
isUploadingNewImage={materialUploadState.primary.isUploadingNew}
|
||||
uploadingImageId={materialUploadState.primary.uploadingImageId}
|
||||
onSkuOptionSelect={(skuName) => {
|
||||
const selectedOption = materials.primary.result?.skuOptions?.find(
|
||||
(option) => option.skuName === skuName
|
||||
);
|
||||
const placeholderCount = selectedOption?.whiteBackgroundCount || 8;
|
||||
const uploadedImages = getUploadedWorkflowMaterialImages(materials.primary.result);
|
||||
onInputsChange({
|
||||
...inputs,
|
||||
primarySku: skuName,
|
||||
});
|
||||
onMaterialsChange({
|
||||
...materials,
|
||||
primary: {
|
||||
result: buildWorkflowMaterialResult({
|
||||
currentResult: {
|
||||
...buildLoadingMaterialResult({
|
||||
keyword: skuName,
|
||||
currentResult: materials.primary.result,
|
||||
placeholderCount,
|
||||
}),
|
||||
images: [
|
||||
...uploadedImages,
|
||||
...buildLoadingMaterialResult({
|
||||
keyword: skuName,
|
||||
currentResult: materials.primary.result,
|
||||
placeholderCount,
|
||||
}).images.map((image) => ({
|
||||
...image,
|
||||
sourceType: "search" as const,
|
||||
})),
|
||||
],
|
||||
},
|
||||
images: [
|
||||
...uploadedImages,
|
||||
...buildLoadingMaterialResult({
|
||||
keyword: skuName,
|
||||
currentResult: materials.primary.result,
|
||||
placeholderCount,
|
||||
}).images.map((image) => ({
|
||||
...image,
|
||||
sourceType: "search" as const,
|
||||
})),
|
||||
],
|
||||
fallbackQuery: skuName,
|
||||
fallbackQueryType: "sku",
|
||||
}),
|
||||
selectedImageIds: materials.primary.selectedImageIds.filter((id) =>
|
||||
uploadedImages.some((image) => image.id === id)
|
||||
),
|
||||
},
|
||||
});
|
||||
setMaterialSearchMode((prev) => ({
|
||||
...prev,
|
||||
primary: "sku",
|
||||
}));
|
||||
void handleSearchMaterials("primary", {
|
||||
keyword: skuName,
|
||||
queryType: "sku",
|
||||
});
|
||||
}}
|
||||
theme={theme}
|
||||
onSelectedImageIdsChange={(next) =>
|
||||
onMaterialsChange({
|
||||
...materials,
|
||||
primary: {
|
||||
...materials.primary,
|
||||
selectedImageIds: next,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
{isSecondaryMaterialVisible ? (
|
||||
<ClientErrorBoundary
|
||||
resetKeys={[
|
||||
"primary",
|
||||
inputs.primarySku,
|
||||
materials.primary.result?.matchedSku || "",
|
||||
materials.primary.selectedImageIds.join("|"),
|
||||
]}
|
||||
fallback={<MaterialPickerErrorFallback isDark={isDark} title="主商品素材搜索" />}
|
||||
>
|
||||
<AgentWorkflowMaterialPicker
|
||||
value={inputs.secondarySku}
|
||||
queryMode={materialSearchMode.secondary}
|
||||
placeholder={materialSearchMode.secondary === "spu" ? "输入搭配 SPU" : "输入搭配 SKU"}
|
||||
buttonLabel={materialSearchMode.secondary === "spu" ? "查SKU列表" : "搜索搭配图"}
|
||||
value={inputs.primarySku}
|
||||
queryMode={materialSearchMode.primary}
|
||||
placeholder={materialSearchMode.primary === "spu" ? "输入主 SPU" : "输入主 SKU"}
|
||||
buttonLabel={materialSearchMode.primary === "spu" ? "查SKU列表" : "搜索主图"}
|
||||
searchNote={
|
||||
materialSearchMode.secondary === "spu"
|
||||
? "按 SPU 搜索时会先返回该 SPU 下的 SKU 列表,再选具体 SKU 获取白底图。"
|
||||
: "选中的搭配图会进入第二个产品槽位,你也可以在下面继续手动替换。"
|
||||
materialSearchMode.primary === "spu"
|
||||
? "按 SPU 搜索时会先返回该 SPU 下的 SKU 列表,点选某个 SKU 后再继续拉取白底图。"
|
||||
: "按 SKU 搜索会直接拉取白底图,适合你已经知道具体 SKU 的情况。"
|
||||
}
|
||||
disabled={disabled}
|
||||
disabledHint={disabledHint}
|
||||
isLoading={materialSearchState.secondary.isLoading}
|
||||
errorText={materialSearchState.secondary.errorText}
|
||||
result={materials.secondary.result}
|
||||
selectedImageIds={materials.secondary.selectedImageIds}
|
||||
isLoading={materialSearchState.primary.isLoading}
|
||||
errorText={materialSearchState.primary.errorText}
|
||||
result={materials.primary.result}
|
||||
selectedImageIds={materials.primary.selectedImageIds}
|
||||
appearanceRecognitionEnabled={appearanceRecognitionEnabled}
|
||||
selectionLimit={secondarySelectionLimit}
|
||||
selectionLimit={primarySelectionLimit}
|
||||
selectionLimitLabel={selectionLimitLabel}
|
||||
onValueChange={(value) => handleMaterialInputChange("secondary", value)}
|
||||
onValueChange={(value) => handleMaterialInputChange("primary", value)}
|
||||
onQueryModeChange={(nextMode) =>
|
||||
setMaterialSearchMode((prev) => ({
|
||||
...prev,
|
||||
secondary: nextMode,
|
||||
primary: nextMode,
|
||||
}))
|
||||
}
|
||||
onSearch={() => {
|
||||
void handleSearchMaterials("secondary");
|
||||
void handleSearchMaterials("primary");
|
||||
}}
|
||||
onUploadImage={(file, options) => handleUploadMaterialImage("secondary", file, options)}
|
||||
onUploadImage={(file, options) => handleUploadMaterialImage("primary", file, options)}
|
||||
onRemoveUploadedImage={(imageId) =>
|
||||
handleRemoveUploadedMaterialImage("secondary", imageId)
|
||||
handleRemoveUploadedMaterialImage("primary", imageId)
|
||||
}
|
||||
isUploadingNewImage={materialUploadState.secondary.isUploadingNew}
|
||||
uploadingImageId={materialUploadState.secondary.uploadingImageId}
|
||||
isUploadingNewImage={materialUploadState.primary.isUploadingNew}
|
||||
uploadingImageId={materialUploadState.primary.uploadingImageId}
|
||||
onSkuOptionSelect={(skuName) => {
|
||||
const selectedOption = materials.secondary.result?.skuOptions?.find(
|
||||
const selectedOption = materials.primary.result?.skuOptions?.find(
|
||||
(option) => option.skuName === skuName
|
||||
);
|
||||
const placeholderCount = selectedOption?.whiteBackgroundCount || 8;
|
||||
const uploadedImages = getUploadedWorkflowMaterialImages(materials.secondary.result);
|
||||
const uploadedImages = getUploadedWorkflowMaterialImages(materials.primary.result);
|
||||
onInputsChange({
|
||||
...inputs,
|
||||
secondarySku: skuName,
|
||||
primarySku: skuName,
|
||||
});
|
||||
onMaterialsChange({
|
||||
...materials,
|
||||
secondary: {
|
||||
primary: {
|
||||
result: buildWorkflowMaterialResult({
|
||||
currentResult: {
|
||||
...buildLoadingMaterialResult({
|
||||
keyword: skuName,
|
||||
currentResult: materials.secondary.result,
|
||||
currentResult: materials.primary.result,
|
||||
placeholderCount,
|
||||
}),
|
||||
images: [
|
||||
...uploadedImages,
|
||||
...buildLoadingMaterialResult({
|
||||
keyword: skuName,
|
||||
currentResult: materials.secondary.result,
|
||||
currentResult: materials.primary.result,
|
||||
placeholderCount,
|
||||
}).images.map((image) => ({
|
||||
...image,
|
||||
@ -1069,7 +1055,7 @@ export function AgentWorkflowMaterialSelectionPanel({
|
||||
...uploadedImages,
|
||||
...buildLoadingMaterialResult({
|
||||
keyword: skuName,
|
||||
currentResult: materials.secondary.result,
|
||||
currentResult: materials.primary.result,
|
||||
placeholderCount,
|
||||
}).images.map((image) => ({
|
||||
...image,
|
||||
@ -1079,16 +1065,16 @@ export function AgentWorkflowMaterialSelectionPanel({
|
||||
fallbackQuery: skuName,
|
||||
fallbackQueryType: "sku",
|
||||
}),
|
||||
selectedImageIds: materials.secondary.selectedImageIds.filter((id) =>
|
||||
selectedImageIds: materials.primary.selectedImageIds.filter((id) =>
|
||||
uploadedImages.some((image) => image.id === id)
|
||||
),
|
||||
},
|
||||
});
|
||||
setMaterialSearchMode((prev) => ({
|
||||
...prev,
|
||||
secondary: "sku",
|
||||
primary: "sku",
|
||||
}));
|
||||
void handleSearchMaterials("secondary", {
|
||||
void handleSearchMaterials("primary", {
|
||||
keyword: skuName,
|
||||
queryType: "sku",
|
||||
});
|
||||
@ -1097,13 +1083,132 @@ export function AgentWorkflowMaterialSelectionPanel({
|
||||
onSelectedImageIdsChange={(next) =>
|
||||
onMaterialsChange({
|
||||
...materials,
|
||||
secondary: {
|
||||
...materials.secondary,
|
||||
primary: {
|
||||
...materials.primary,
|
||||
selectedImageIds: next,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</ClientErrorBoundary>
|
||||
|
||||
{isSecondaryMaterialVisible ? (
|
||||
<ClientErrorBoundary
|
||||
resetKeys={[
|
||||
"secondary",
|
||||
inputs.secondarySku,
|
||||
materials.secondary.result?.matchedSku || "",
|
||||
materials.secondary.selectedImageIds.join("|"),
|
||||
]}
|
||||
fallback={<MaterialPickerErrorFallback isDark={isDark} title="搭配商品素材搜索" />}
|
||||
>
|
||||
<AgentWorkflowMaterialPicker
|
||||
value={inputs.secondarySku}
|
||||
queryMode={materialSearchMode.secondary}
|
||||
placeholder={materialSearchMode.secondary === "spu" ? "输入搭配 SPU" : "输入搭配 SKU"}
|
||||
buttonLabel={materialSearchMode.secondary === "spu" ? "查SKU列表" : "搜索搭配图"}
|
||||
searchNote={
|
||||
materialSearchMode.secondary === "spu"
|
||||
? "按 SPU 搜索时会先返回该 SPU 下的 SKU 列表,再选具体 SKU 获取白底图。"
|
||||
: "选中的搭配图会进入第二个产品槽位,你也可以在下面继续手动替换。"
|
||||
}
|
||||
disabled={disabled}
|
||||
disabledHint={disabledHint}
|
||||
isLoading={materialSearchState.secondary.isLoading}
|
||||
errorText={materialSearchState.secondary.errorText}
|
||||
result={materials.secondary.result}
|
||||
selectedImageIds={materials.secondary.selectedImageIds}
|
||||
appearanceRecognitionEnabled={appearanceRecognitionEnabled}
|
||||
selectionLimit={secondarySelectionLimit}
|
||||
selectionLimitLabel={selectionLimitLabel}
|
||||
onValueChange={(value) => handleMaterialInputChange("secondary", value)}
|
||||
onQueryModeChange={(nextMode) =>
|
||||
setMaterialSearchMode((prev) => ({
|
||||
...prev,
|
||||
secondary: nextMode,
|
||||
}))
|
||||
}
|
||||
onSearch={() => {
|
||||
void handleSearchMaterials("secondary");
|
||||
}}
|
||||
onUploadImage={(file, options) => handleUploadMaterialImage("secondary", file, options)}
|
||||
onRemoveUploadedImage={(imageId) =>
|
||||
handleRemoveUploadedMaterialImage("secondary", imageId)
|
||||
}
|
||||
isUploadingNewImage={materialUploadState.secondary.isUploadingNew}
|
||||
uploadingImageId={materialUploadState.secondary.uploadingImageId}
|
||||
onSkuOptionSelect={(skuName) => {
|
||||
const selectedOption = materials.secondary.result?.skuOptions?.find(
|
||||
(option) => option.skuName === skuName
|
||||
);
|
||||
const placeholderCount = selectedOption?.whiteBackgroundCount || 8;
|
||||
const uploadedImages = getUploadedWorkflowMaterialImages(materials.secondary.result);
|
||||
onInputsChange({
|
||||
...inputs,
|
||||
secondarySku: skuName,
|
||||
});
|
||||
onMaterialsChange({
|
||||
...materials,
|
||||
secondary: {
|
||||
result: buildWorkflowMaterialResult({
|
||||
currentResult: {
|
||||
...buildLoadingMaterialResult({
|
||||
keyword: skuName,
|
||||
currentResult: materials.secondary.result,
|
||||
placeholderCount,
|
||||
}),
|
||||
images: [
|
||||
...uploadedImages,
|
||||
...buildLoadingMaterialResult({
|
||||
keyword: skuName,
|
||||
currentResult: materials.secondary.result,
|
||||
placeholderCount,
|
||||
}).images.map((image) => ({
|
||||
...image,
|
||||
sourceType: "search" as const,
|
||||
})),
|
||||
],
|
||||
},
|
||||
images: [
|
||||
...uploadedImages,
|
||||
...buildLoadingMaterialResult({
|
||||
keyword: skuName,
|
||||
currentResult: materials.secondary.result,
|
||||
placeholderCount,
|
||||
}).images.map((image) => ({
|
||||
...image,
|
||||
sourceType: "search" as const,
|
||||
})),
|
||||
],
|
||||
fallbackQuery: skuName,
|
||||
fallbackQueryType: "sku",
|
||||
}),
|
||||
selectedImageIds: materials.secondary.selectedImageIds.filter((id) =>
|
||||
uploadedImages.some((image) => image.id === id)
|
||||
),
|
||||
},
|
||||
});
|
||||
setMaterialSearchMode((prev) => ({
|
||||
...prev,
|
||||
secondary: "sku",
|
||||
}));
|
||||
void handleSearchMaterials("secondary", {
|
||||
keyword: skuName,
|
||||
queryType: "sku",
|
||||
});
|
||||
}}
|
||||
theme={theme}
|
||||
onSelectedImageIdsChange={(next) =>
|
||||
onMaterialsChange({
|
||||
...materials,
|
||||
secondary: {
|
||||
...materials.secondary,
|
||||
selectedImageIds: next,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</ClientErrorBoundary>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user