diff --git a/app/(admin)/settings/agent-workflow/psd-templates/psd-templates-settings-client.tsx b/app/(admin)/settings/agent-workflow/psd-templates/psd-templates-settings-client.tsx index 5571c4a63..37d7021ee 100644 --- a/app/(admin)/settings/agent-workflow/psd-templates/psd-templates-settings-client.tsx +++ b/app/(admin)/settings/agent-workflow/psd-templates/psd-templates-settings-client.tsx @@ -109,6 +109,7 @@ export function PsdTemplatesSettingsClient({ const [isCreating, setIsCreating] = useState(false); const [isSaving, setIsSaving] = useState(false); const [isDeleting, setIsDeleting] = useState(false); + const [deletingTemplateId, setDeletingTemplateId] = useState(null); const [isUploadingFont, setIsUploadingFont] = useState(false); const [deletingFontId, setDeletingFontId] = useState(null); @@ -331,15 +332,16 @@ export function PsdTemplatesSettingsClient({ } } - async function handleDeleteTemplate() { - if (!selectedTemplate) return; - const confirmed = window.confirm(`确认删除模板“${selectedTemplate.name}”吗?`); + async function handleDeleteTemplate(template = selectedTemplate) { + if (!template) return; + const confirmed = window.confirm(`确认删除模板“${template.name}”吗?`); if (!confirmed) return; try { setIsDeleting(true); + setDeletingTemplateId(template.id); const response = await fetch( - `/api/admin/psd-templates/${encodeURIComponent(selectedTemplate.id)}`, + `/api/admin/psd-templates/${encodeURIComponent(template.id)}`, { method: "DELETE", } @@ -350,12 +352,15 @@ export function PsdTemplatesSettingsClient({ } toast.success("PSD 模板已删除"); - setSelectedTemplateId(null); + if (selectedTemplateId === template.id) { + setSelectedTemplateId(null); + } await mutate(); } catch (error) { toast.error(error instanceof Error ? error.message : "PSD 模板删除失败"); } finally { setIsDeleting(false); + setDeletingTemplateId(null); } } @@ -604,10 +609,17 @@ export function PsdTemplatesSettingsClient({ templates.map((template) => { const isActive = template.id === selectedTemplateId; return ( - - - - {template.status === "active" ? "启用" : "停用"} - {template.description ? (
{template.description}
) : null} - + ); }) )} @@ -649,10 +680,15 @@ export function PsdTemplatesSettingsClient({ 模板基础信息 启用前请至少配置一个可编辑槽位,文字槽位建议明确字体和字号。 - +
-
-
+
+
{draftTemplate.name} { + void handleDeleteTemplate(); + }} disabled={isDeleting} > {isDeleting ? ( @@ -819,23 +857,36 @@ export function PsdTemplatesSettingsClient({ filteredLayers.map((layer, index) => (
-
-
- #{index + 1} - {layer.layerName} - - {layer.width} x {layer.height} - - - {layer.slotType === "text" ? "文字槽位" : "图片槽位"} - - {layer.slotType === "image" && layer.maskUrl ? ( - 已生成蒙版 - ) : null} +
+
+ {layer.previewUrl ? ( + {`${layer.layerName} + ) : ( +
无预览
+ )}
-
{layer.layerPath}
-
- 坐标:({layer.x}, {layer.y}) · layerId: {layer.layerId} +
+
+ #{index + 1} + {layer.layerName} + + {layer.width} x {layer.height} + + + {layer.slotType === "text" ? "文字槽位" : "图片槽位"} + + {layer.slotType === "image" && layer.maskUrl ? ( + 已生成蒙版 + ) : null} +
+
{layer.layerPath}
+
+ 坐标:({layer.x}, {layer.y}) · layerId: {layer.layerId} +
diff --git a/app/api/admin/psd-templates/route.ts b/app/api/admin/psd-templates/route.ts index 54aade067..d3df3234b 100644 --- a/app/api/admin/psd-templates/route.ts +++ b/app/api/admin/psd-templates/route.ts @@ -12,7 +12,10 @@ import { type AgentPsdTemplateListResponse, type AgentPsdTemplateMutationResponse, } from "@/lib/agents/psd-template-config"; -import { parsePsdTemplateFile } from "@/lib/agents/psd-template-parser"; +import { + parsePsdTemplateFile, + preparePsdTemplateRenderAssets, +} from "@/lib/agents/psd-template-parser"; import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings"; export const runtime = "nodejs"; @@ -136,9 +139,54 @@ export async function POST(request: NextRequest) { ), ]); + const templateId = randomUUID(); + const initialLayers = parsedTemplate.layers.map((layer): AgentPsdTemplateLayerSlot => { + if (layer.slotType === "text") { + return { + ...layer, + slotType: "text", + isEditable: false, + isRequired: false, + placeholder: layer.defaultText || layer.layerName, + fontSize: Math.max(16, Math.min(layer.height, Math.round(layer.height * 0.78))), + lineHeight: 1.2, + letterSpacing: 0, + fontWeight: 500, + textColor: "#111111", + textAlign: "left", + paddingX: 0, + paddingY: 0, + maxLines: 4, + }; + } + + return { + ...layer, + slotType: "image", + isEditable: false, + isRequired: false, + isDefaultResultSource: false, + allowUserUploadOverride: true, + replaceMode: "cover", + }; + }); + + const preparedAssets = await preparePsdTemplateRenderAssets({ + templateId, + fileUrl: uploadedFile.url, + layers: initialLayers, + }); + const uploadedRenderBase = await uploadFile( + `agent-psd-base-${templateId}-${Date.now()}.png`, + preparedAssets.renderBaseBuffer.buffer.slice( + preparedAssets.renderBaseBuffer.byteOffset, + preparedAssets.renderBaseBuffer.byteOffset + preparedAssets.renderBaseBuffer.byteLength + ) as ArrayBuffer + ); + const now = new Date().toISOString(); const nextTemplate: AgentPsdTemplate = { - id: randomUUID(), + id: templateId, name, brandName, description: description || undefined, @@ -148,41 +196,12 @@ export async function POST(request: NextRequest) { filePath: uploadedFile.pathname, previewUrl: uploadedPreview.url, previewPath: uploadedPreview.pathname, - renderBaseUrl: uploadedPreview.url, - renderBasePath: uploadedPreview.pathname, + renderBaseUrl: uploadedRenderBase.url, + renderBasePath: uploadedRenderBase.pathname, width: parsedTemplate.width, height: parsedTemplate.height, layerCount: parsedTemplate.layers.length, - layers: parsedTemplate.layers.map((layer): AgentPsdTemplateLayerSlot => { - if (layer.slotType === "text") { - return { - ...layer, - slotType: "text", - isEditable: false, - isRequired: false, - placeholder: layer.defaultText || layer.layerName, - fontSize: Math.max(16, Math.min(layer.height, Math.round(layer.height * 0.78))), - lineHeight: 1.2, - letterSpacing: 0, - fontWeight: 500, - textColor: "#111111", - textAlign: "left", - paddingX: 0, - paddingY: 0, - maxLines: 4, - }; - } - - return { - ...layer, - slotType: "image", - isEditable: false, - isRequired: false, - isDefaultResultSource: false, - allowUserUploadOverride: true, - replaceMode: "cover", - }; - }), + layers: preparedAssets.layers, createdAt: now, updatedAt: now, uploadedById: session.user.id, diff --git a/components/agents/AgentPsdTemplateEditorDialog.tsx b/components/agents/AgentPsdTemplateEditorDialog.tsx index 853c00f34..1ec7933fd 100644 --- a/components/agents/AgentPsdTemplateEditorDialog.tsx +++ b/components/agents/AgentPsdTemplateEditorDialog.tsx @@ -206,8 +206,12 @@ export function AgentPsdTemplateEditorDialog({ {selectedTemplate ? ( <> -
-
+
+
{selectedTemplate.name} -
-
+
+
PSD 模板渲染结果 layer.id === targetLayer.id); + if (layerIndex >= 0) { + nextLayers[layerIndex] = { + ...nextLayers[layerIndex], + previewUrl: uploadedPreview.url, + previewPath: uploadedPreview.pathname, + }; + } + } + + if (!isAgentPsdImageLayerSlot(targetLayer) || !targetLayer.isEditable || targetLayer.maskUrl) { + continue; + } + const pixelCount = entry.layer.width * entry.layer.height; const maskRgba = new Uint8ClampedArray(pixelCount * 4); let hasVisiblePixel = false; diff --git a/lib/agents/psd-template-settings.ts b/lib/agents/psd-template-settings.ts index cf0061969..351b68fe4 100644 --- a/lib/agents/psd-template-settings.ts +++ b/lib/agents/psd-template-settings.ts @@ -20,6 +20,7 @@ export async function tryDeleteAgentPsdTemplateAssets(template: AgentPsdTemplate template.filePath, template.previewPath, template.renderBasePath, + ...template.layers.map((layer) => layer.previewPath), ...template.layers .filter(isAgentPsdImageLayerSlot) .map((layer) => layer.maskPath),