fix: persist PSD editor uploads and results
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-04-10 20:17:36 +08:00
parent ded97bef00
commit aa885a3d48
2 changed files with 341 additions and 22 deletions

View File

@ -1046,6 +1046,8 @@ export function AgentGenerationWorkspaceDarkV2({
const [isPsdTemplateDialogOpen, setIsPsdTemplateDialogOpen] = useState(false);
const [psdTemplateDialogSourceUrl, setPsdTemplateDialogSourceUrl] = useState<string | null>(null);
const [psdTemplateDialogMode, setPsdTemplateDialogMode] = useState<AgentPsdTemplateMode>("main");
const [psdTemplateDialogResultModeType, setPsdTemplateDialogResultModeType] =
useState<AgentGenerationResultModeType>("scene_generation");
const [batchDownloadState, setBatchDownloadState] = useState<null | "scene" | "detail" | "sku">(null);
const [detailReplacementSearchState, setDetailReplacementSearchState] = useState<{
isLoading: boolean;
@ -1078,11 +1080,13 @@ export function AgentGenerationWorkspaceDarkV2({
setIsPsdTemplateDialogOpen(false);
setPsdTemplateDialogSourceUrl(null);
setPsdTemplateDialogMode("main");
setPsdTemplateDialogResultModeType("scene_generation");
}, []);
const openPsdTemplateDialog = useCallback(
(params?: {
sourceImageUrl?: string | null;
mode?: AgentPsdTemplateMode | null;
resultModeType?: AgentGenerationResultModeType | null;
}) => {
setPsdTemplateDialogSourceUrl(
isValidChatHttpUrl(params?.sourceImageUrl)
@ -1092,9 +1096,14 @@ export function AgentGenerationWorkspaceDarkV2({
setPsdTemplateDialogMode(
params?.mode === "sku" ? "sku" : params?.mode === "main" ? "main" : "main"
);
setPsdTemplateDialogResultModeType(
params?.resultModeType ||
activeResultModeType ||
"scene_generation"
);
setIsPsdTemplateDialogOpen(true);
},
[]
[activeResultModeType]
);
const normalizedDeletedResultCollections = useMemo(
() => ({
@ -2952,6 +2961,39 @@ export function AgentGenerationWorkspaceDarkV2({
},
[mergedResultCollections.scene_generation, onResultPreviewUrlsChange, uploadBase64ResultToStorage]
);
const appendResultUrlToCollection = useCallback(
(modeType: AgentGenerationResultModeType, imageUrl: string) => {
const normalizedUrl = String(imageUrl || "").trim();
if (!normalizedUrl || !onResultPreviewUrlsChange) {
return false;
}
const nextUrls = Array.from(
new Set([
...normalizeResultCollectionUrls(mergedResultCollections[modeType]),
normalizedUrl,
])
);
onResultPreviewUrlsChange(modeType, nextUrls);
if (activeResultModeType === modeType) {
setSelectedResultUrl(normalizedUrl);
}
return true;
},
[activeResultModeType, mergedResultCollections, onResultPreviewUrlsChange]
);
const handlePsdDerivedImageCreated = useCallback(
(imageUrl: string) => {
appendResultUrlToCollection(psdTemplateDialogResultModeType, imageUrl);
},
[appendResultUrlToCollection, psdTemplateDialogResultModeType]
);
const handlePsdRenderedResultCreated = useCallback(
(imageUrl: string) => {
appendResultUrlToCollection(psdTemplateDialogResultModeType, imageUrl);
},
[appendResultUrlToCollection, psdTemplateDialogResultModeType]
);
const handleDeleteResultImage = useCallback(
(modeType: AgentGenerationResultModeType, imageUrl: string) => {
const normalizedUrl = String(imageUrl || "").trim();
@ -6085,6 +6127,12 @@ export function AgentGenerationWorkspaceDarkV2({
selectedCategoryNames={[workflowContext?.fifthCategory || ""].filter(Boolean)}
sizeInfoText={workflowContext?.summaryText || null}
templateMode={psdTemplateDialogMode}
onDerivedImageCreated={
onResultPreviewUrlsChange ? handlePsdDerivedImageCreated : undefined
}
onRenderedResultCreated={
onResultPreviewUrlsChange ? handlePsdRenderedResultCreated : undefined
}
/>
</ClientErrorBoundary>

View File

@ -429,6 +429,231 @@ function createInitialState(
return { imageAssignments, imagePlacements, textAssignments };
}
function normalizeLayerTransferKey(value: string) {
return String(value || "").trim().toLowerCase();
}
function getLayerTransferKeys(layer: AgentPsdTemplateLayerSlot) {
const keys = new Set<string>();
const addKey = (value?: string | null) => {
const normalized = normalizeLayerTransferKey(String(value || ""));
if (normalized) {
keys.add(normalized);
}
};
addKey(layer.id);
addKey(layer.layerId);
addKey(layer.layerPath);
addKey(layer.layerName);
if (isAgentPsdImageLayerSlot(layer)) {
addKey(`image:${layer.layerPath}`);
addKey(`image:${layer.layerName}`);
addKey(`image:${layer.layerPath}:${layer.width}x${layer.height}`);
addKey(`image:${layer.layerName}:${layer.width}x${layer.height}`);
if (layer.isDefaultResultSource) {
addKey("__default_result_source__");
}
}
if (isAgentPsdTextLayerSlot(layer)) {
addKey(`text:${layer.layerPath}`);
addKey(`text:${layer.layerName}`);
addKey(`text:${layer.layerPath}:${layer.width}x${layer.height}`);
addKey(`text:${layer.layerName}:${layer.width}x${layer.height}`);
}
return Array.from(keys);
}
function findMatchingLayer<TLayer extends AgentPsdTemplateLayerSlot>(params: {
nextLayer: TLayer;
previousLayers: TLayer[];
usedPreviousLayerIds?: Set<string>;
}) {
const { nextLayer, previousLayers, usedPreviousLayerIds } = params;
const nextLayerKeySet = new Set(getLayerTransferKeys(nextLayer));
for (const previousLayer of previousLayers) {
if (usedPreviousLayerIds?.has(previousLayer.id)) {
continue;
}
const previousKeys = getLayerTransferKeys(previousLayer);
if (previousKeys.some((key) => nextLayerKeySet.has(key))) {
return previousLayer;
}
}
return null;
}
function resolveTransferredSelectedLayerId(params: {
nextTemplate: AgentPsdTemplate | null;
previousTemplate: AgentPsdTemplate | null;
previousSelectedLayerId: string;
}) {
const { nextTemplate, previousTemplate, previousSelectedLayerId } = params;
if (!nextTemplate) {
return "";
}
if (!previousTemplate || !previousSelectedLayerId) {
return getPreferredSelectedLayerId(nextTemplate);
}
const previousLayer =
previousTemplate.layers.find((layer) => layer.id === previousSelectedLayerId) ||
null;
if (!previousLayer) {
return getPreferredSelectedLayerId(nextTemplate);
}
const nextCandidateLayers = nextTemplate.layers.filter(
(layer) =>
layer.isEditable &&
((isAgentPsdImageLayerSlot(previousLayer) && isAgentPsdImageLayerSlot(layer)) ||
(isAgentPsdTextLayerSlot(previousLayer) && isAgentPsdTextLayerSlot(layer)))
);
const previousLayerKeySet = new Set(getLayerTransferKeys(previousLayer));
const matchedLayer = nextCandidateLayers.find((layer) =>
getLayerTransferKeys(layer).some((key) => previousLayerKeySet.has(key))
);
return matchedLayer?.id || getPreferredSelectedLayerId(nextTemplate);
}
function createTemplateEditorState(params: {
nextTemplate: AgentPsdTemplate | null;
previousTemplate: AgentPsdTemplate | null;
sourceImageUrl: string;
previousImageAssignments: Record<string, string>;
previousImagePlacements: Record<string, AgentPsdImagePlacement>;
previousTextAssignments: Record<string, string>;
previousTextColorAssignments: Record<string, string>;
}) {
const {
nextTemplate,
previousTemplate,
sourceImageUrl,
previousImageAssignments,
previousImagePlacements,
previousTextAssignments,
previousTextColorAssignments,
} = params;
const initialState = createInitialState(nextTemplate, sourceImageUrl);
const nextState = {
imageAssignments: { ...initialState.imageAssignments },
imagePlacements: { ...initialState.imagePlacements },
textAssignments: { ...initialState.textAssignments },
textColorAssignments: {} as Record<string, string>,
};
if (!nextTemplate || !previousTemplate) {
return nextState;
}
const previousEditableImageLayers = previousTemplate.layers.filter(
(layer): layer is AgentPsdTemplateImageLayerSlot =>
layer.isEditable && isAgentPsdImageLayerSlot(layer)
);
const nextEditableImageLayers = nextTemplate.layers.filter(
(layer): layer is AgentPsdTemplateImageLayerSlot =>
layer.isEditable && isAgentPsdImageLayerSlot(layer)
);
const usedPreviousImageLayerIds = new Set<string>();
const usedNextImageLayerIds = new Set<string>();
for (const nextLayer of nextEditableImageLayers) {
const matchedPreviousLayer = findMatchingLayer({
nextLayer,
previousLayers: previousEditableImageLayers,
usedPreviousLayerIds: usedPreviousImageLayerIds,
});
if (!matchedPreviousLayer) {
continue;
}
const previousImageUrl = String(
previousImageAssignments[matchedPreviousLayer.id] || ""
).trim();
if (!previousImageUrl) {
continue;
}
nextState.imageAssignments[nextLayer.id] = previousImageUrl;
nextState.imagePlacements[nextLayer.id] = normalizeAgentPsdImagePlacement(
previousImagePlacements[matchedPreviousLayer.id]
);
usedPreviousImageLayerIds.add(matchedPreviousLayer.id);
usedNextImageLayerIds.add(nextLayer.id);
}
const unmatchedPreviousImageLayers = previousEditableImageLayers.filter(
(layer) =>
!usedPreviousImageLayerIds.has(layer.id) &&
Boolean(String(previousImageAssignments[layer.id] || "").trim())
);
if (unmatchedPreviousImageLayers.length === 1) {
const fallbackNextLayer =
nextEditableImageLayers.find(
(layer) =>
!usedNextImageLayerIds.has(layer.id) && layer.isDefaultResultSource
) ||
nextEditableImageLayers.find(
(layer) => !usedNextImageLayerIds.has(layer.id)
);
if (fallbackNextLayer) {
const fallbackPreviousLayer = unmatchedPreviousImageLayers[0];
nextState.imageAssignments[fallbackNextLayer.id] = String(
previousImageAssignments[fallbackPreviousLayer.id] || ""
).trim();
nextState.imagePlacements[fallbackNextLayer.id] =
normalizeAgentPsdImagePlacement(
previousImagePlacements[fallbackPreviousLayer.id]
);
}
}
const previousEditableTextLayers = previousTemplate.layers.filter(
(layer): layer is AgentPsdTemplateTextLayerSlot =>
layer.isEditable && isAgentPsdTextLayerSlot(layer)
);
const nextEditableTextLayers = nextTemplate.layers.filter(
(layer): layer is AgentPsdTemplateTextLayerSlot =>
layer.isEditable && isAgentPsdTextLayerSlot(layer)
);
const usedPreviousTextLayerIds = new Set<string>();
for (const nextLayer of nextEditableTextLayers) {
const matchedPreviousLayer = findMatchingLayer({
nextLayer,
previousLayers: previousEditableTextLayers,
usedPreviousLayerIds: usedPreviousTextLayerIds,
});
if (!matchedPreviousLayer) {
continue;
}
const previousTextValue = previousTextAssignments[matchedPreviousLayer.id];
if (typeof previousTextValue === "string") {
nextState.textAssignments[nextLayer.id] = previousTextValue;
}
const previousTextColor = String(
previousTextColorAssignments[matchedPreviousLayer.id] || ""
).trim();
if (previousTextColor) {
nextState.textColorAssignments[nextLayer.id] = previousTextColor;
}
usedPreviousTextLayerIds.add(matchedPreviousLayer.id);
}
return nextState;
}
function getLayerLabel(layer: AgentPsdTemplateLayerSlot) {
return isAgentPsdImageLayerSlot(layer) ? "可替换图片" : "可编辑文字";
}
@ -725,6 +950,8 @@ export function AgentPsdTemplateEditorDialog({
selectedCategoryNames,
sizeInfoText,
templateMode,
onDerivedImageCreated,
onRenderedResultCreated,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
@ -733,6 +960,8 @@ export function AgentPsdTemplateEditorDialog({
selectedCategoryNames?: string[] | null;
sizeInfoText?: string | null;
templateMode?: AgentPsdTemplateMode | null;
onDerivedImageCreated?: (imageUrl: string) => void;
onRenderedResultCreated?: (imageUrl: string) => void;
}) {
const normalizedBrandName = String(brandName || "").trim();
const normalizedSelectedCategoryNames = useMemo(
@ -831,6 +1060,7 @@ export function AgentPsdTemplateEditorDialog({
const previewViewportRef = useRef<HTMLDivElement | null>(null);
const uploadInputRef = useRef<HTMLInputElement | null>(null);
const inlineTextEditorRef = useRef<HTMLTextAreaElement | null>(null);
const previousSelectedTemplateRef = useRef<AgentPsdTemplate | null>(null);
const imageDragStateRef = useRef<{
layerId: string;
pointerId: number;
@ -839,6 +1069,18 @@ export function AgentPsdTemplateEditorDialog({
startPlacement: AgentPsdImagePlacement;
} | null>(null);
const imageAssignmentsRef = useRef(imageAssignments);
const imagePlacementsRef = useRef(imagePlacements);
const textAssignmentsRef = useRef(textAssignments);
const textColorAssignmentsRef = useRef(textColorAssignments);
const selectedLayerIdRef = useRef(selectedLayerId);
imageAssignmentsRef.current = imageAssignments;
imagePlacementsRef.current = imagePlacements;
textAssignmentsRef.current = textAssignments;
textColorAssignmentsRef.current = textColorAssignments;
selectedLayerIdRef.current = selectedLayerId;
const selectedTemplate = useMemo(
() =>
templates.find((template) => template.id === selectedTemplateId) || null,
@ -1269,6 +1511,14 @@ export function AgentPsdTemplateEditorDialog({
setSelectedTemplateId("");
setSelectedLayerId("");
setImageAssignments({});
setImagePlacements({});
setImageSizes({});
setTextAssignments({});
setTextColorAssignments({});
setRenderedResultUrl("");
setRightPanelView("editor");
previousSelectedTemplateRef.current = null;
}, [open]);
useEffect(() => {
@ -1291,15 +1541,26 @@ export function AgentPsdTemplateEditorDialog({
if (!open) return;
setRenderedResultUrl("");
setRightPanelView("editor");
const initialState = createInitialState(
selectedTemplate,
sourceImageUrl || ""
);
const initialState = createTemplateEditorState({
nextTemplate: selectedTemplate,
previousTemplate: previousSelectedTemplateRef.current,
sourceImageUrl: sourceImageUrl || "",
previousImageAssignments: imageAssignmentsRef.current,
previousImagePlacements: imagePlacementsRef.current,
previousTextAssignments: textAssignmentsRef.current,
previousTextColorAssignments: textColorAssignmentsRef.current,
});
setImageAssignments(initialState.imageAssignments);
setImagePlacements(initialState.imagePlacements);
setTextAssignments(initialState.textAssignments);
setTextColorAssignments({});
setSelectedLayerId(getPreferredSelectedLayerId(selectedTemplate));
setTextColorAssignments(initialState.textColorAssignments);
setSelectedLayerId(
resolveTransferredSelectedLayerId({
nextTemplate: selectedTemplate,
previousTemplate: previousSelectedTemplateRef.current,
previousSelectedLayerId: selectedLayerIdRef.current,
})
);
if (!selectedTemplate || typeof window === "undefined") {
return;
@ -1327,6 +1588,10 @@ export function AgentPsdTemplateEditorDialog({
};
}, [open, selectedTemplate?.id]);
useEffect(() => {
previousSelectedTemplateRef.current = selectedTemplate;
}, [selectedTemplate]);
useEffect(() => {
if (!selectedTemplate) {
setImageSizes((current) => {
@ -1596,7 +1861,12 @@ export function AgentPsdTemplateEditorDialog({
delete next[slotId];
return next;
});
toast.success("图片拓展完成,已替换当前槽位");
onDerivedImageCreated?.(nextImageUrl);
toast.success(
onDerivedImageCreated
? "图片拓展完成,已替换当前槽位并同步保存到结果区"
: "图片拓展完成,已替换当前槽位"
);
} catch (error) {
toast.error(error instanceof Error ? error.message : "图片拓展失败");
} finally {
@ -1727,7 +1997,12 @@ export function AgentPsdTemplateEditorDialog({
setRenderedResultUrl(payload.result.url);
setRightPanelView("export");
toast.success("模板替换结果已生成");
onRenderedResultCreated?.(payload.result.url);
toast.success(
onRenderedResultCreated
? "模板替换结果已生成,并已加入结果区"
: "模板替换结果已生成"
);
} catch (error) {
toast.error(error instanceof Error ? error.message : "PSD 模板渲染失败");
} finally {
@ -1787,6 +2062,15 @@ export function AgentPsdTemplateEditorDialog({
<DialogContent
className="flex h-[96vh] w-[96vw] max-w-[1700px] flex-col overflow-hidden rounded-[24px] border border-white/12 bg-[#0a0a0a] p-0 text-white shadow-[0_40px_100px_rgba(0,0,0,0.8)]"
hideCloseButton
onEscapeKeyDown={(event) => {
event.preventDefault();
}}
onInteractOutside={(event) => {
event.preventDefault();
}}
onPointerDownOutside={(event) => {
event.preventDefault();
}}
overlayClassName="bg-black/80 backdrop-blur-md"
>
{fontFaceCss ? <style>{fontFaceCss}</style> : null}
@ -2032,19 +2316,6 @@ export function AgentPsdTemplateEditorDialog({
</div>
</div>
<Button
className="h-12 w-full rounded-xl bg-gradient-to-r from-emerald-500 to-teal-400 font-medium text-white shadow-[0_4px_20px_rgba(16,185,129,0.25)] ring-1 ring-emerald-500/50 transition-all hover:from-emerald-400 hover:to-teal-300 hover:shadow-[0_6px_25px_rgba(16,185,129,0.35)] disabled:opacity-50"
disabled={isRendering}
onClick={handleRender}
>
{isRendering ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<Sparkles className="mr-2 h-4 w-4" />
)}
应用模板并导出
</Button>
{sourceImageUrl ? null : (
<div className="rounded-xl border border-white/10 border-dashed bg-white/[0.02] px-4 py-3 text-white/50 text-xs leading-relaxed shadow-inner">
当前未从生图结果带入图片。请先在下方图片图层中点击“上传替换图片”,再开始套用