diff --git a/components/agents/AgentPsdTemplateEditorDialog.tsx b/components/agents/AgentPsdTemplateEditorDialog.tsx index 7e02a369c..6c47ad86d 100644 --- a/components/agents/AgentPsdTemplateEditorDialog.tsx +++ b/components/agents/AgentPsdTemplateEditorDialog.tsx @@ -1,10 +1,6 @@ "use client"; -import { useEffect, useMemo, useRef, useState, type CSSProperties, type ChangeEvent, type PointerEvent as ReactPointerEvent } from "react"; -import useSWR from "swr"; -import { toast } from "sonner"; import { - Download, Image as ImageIcon, Loader2, Move, @@ -15,12 +11,20 @@ import { ZoomIn, ZoomOut, } from "lucide-react"; -import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry"; import { - Dialog, - DialogContent, -} from "@/components/ui/dialog"; + type ChangeEvent, + type CSSProperties, + type PointerEvent as ReactPointerEvent, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { toast } from "sonner"; +import useSWR from "swr"; +import { AgentPsdTemplateExportPreview } from "@/components/agents/agent-psd-template-export-preview"; import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { ScrollArea } from "@/components/ui/scroll-area"; @@ -36,55 +40,69 @@ import { import { Slider } from "@/components/ui/slider"; import { Textarea } from "@/components/ui/textarea"; import { + type AgentPsdImagePlacement, + type AgentPsdImageSize, getDefaultAgentPsdImagePlacement, normalizeAgentPsdImagePlacement, resolveAgentPsdImageBaseFrame, - type AgentPsdImagePlacement, - type AgentPsdImageSize, } from "@/lib/agents/psd-image-placement"; import { - defaultAgentPsdTextColorConfig, - normalizeAgentPsdTextColorConfig, -} from "@/lib/agents/psd-text-color-config"; -import { - isAgentPsdImageLayerSlot, - isAgentPsdTextLayerSlot, - parseAgentPsdTemplates, - type AgentPsdTemplateMode, type AgentPsdTemplate, type AgentPsdTemplateImageLayerSlot, type AgentPsdTemplateLayerSlot, type AgentPsdTemplateListResponse, + type AgentPsdTemplateMode, type AgentPsdTemplateRenderResponse, type AgentPsdTemplateTextLayerSlot, + isAgentPsdImageLayerSlot, + isAgentPsdTextLayerSlot, + parseAgentPsdTemplates, } from "@/lib/agents/psd-template-config"; import { defaultAgentPsdTemplateSecondaryCategoryConfig, normalizeAgentPsdTemplateSecondaryCategoryConfig, } from "@/lib/agents/psd-template-secondary-category-config"; -import { normalizeHexColor, resolveTextLayout } from "@/lib/agents/psd-text-layout"; +import { + defaultAgentPsdTextColorConfig, + normalizeAgentPsdTextColorConfig, +} from "@/lib/agents/psd-text-color-config"; +import { + normalizeHexColor, + resolveTextLayout, +} from "@/lib/agents/psd-text-layout"; +import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry"; const fetcher = async (url: string): Promise => { try { const response = await fetch(url, { cache: "no-store" }); - const payload = (await response.json().catch(() => null)) as - | Partial - | null; + const payload = (await response + .json() + .catch(() => null)) as Partial | null; - if (!response.ok || !payload || payload.success !== true || !Array.isArray(payload.templates)) { + if ( + !response.ok || + !payload || + payload.success !== true || + !Array.isArray(payload.templates) + ) { return { success: false, templates: [], textColorConfig: defaultAgentPsdTextColorConfig, secondaryCategoryConfig: defaultAgentPsdTemplateSecondaryCategoryConfig, - error: typeof payload?.error === "string" ? payload.error : "PSD 模板加载失败", + error: + typeof payload?.error === "string" + ? payload.error + : "PSD 模板加载失败", }; } return { success: true, templates: parseAgentPsdTemplates(payload.templates), - textColorConfig: normalizeAgentPsdTextColorConfig(payload.textColorConfig), + textColorConfig: normalizeAgentPsdTextColorConfig( + payload.textColorConfig + ), secondaryCategoryConfig: normalizeAgentPsdTemplateSecondaryCategoryConfig( payload.secondaryCategoryConfig ), @@ -112,7 +130,9 @@ function getTemplateMetaLabel( ) { const metaParts = [ `${template.width} x ${template.height}`, - showSecondaryCategory ? String(template.secondaryCategory || "").trim() : "", + showSecondaryCategory + ? String(template.secondaryCategory || "").trim() + : "", ].filter(Boolean); return metaParts.join(" · "); } @@ -144,17 +164,29 @@ function renderTemplateSelectContent( } > {template.name}
-
+
{template.name}
{metaLabel ? ( -
+
{metaLabel}
) : null} @@ -178,23 +210,25 @@ function renderTemplatePickerCardContent(
{template.name}
{template.name}
{metaLabel ? ( -
{metaLabel}
+
+ {metaLabel} +
) : null}
@@ -208,7 +242,10 @@ function clampImageScale(value: number) { return Math.max(MIN_IMAGE_SCALE, Math.min(MAX_IMAGE_SCALE, value)); } -function createInitialState(template: AgentPsdTemplate | null, sourceImageUrl: string) { +function createInitialState( + template: AgentPsdTemplate | null, + sourceImageUrl: string +) { const imageAssignments: Record = {}; const imagePlacements: Record = {}; const textAssignments: Record = {}; @@ -218,7 +255,11 @@ function createInitialState(template: AgentPsdTemplate | null, sourceImageUrl: s } for (const layer of template.layers) { - if (isAgentPsdImageLayerSlot(layer) && layer.isDefaultResultSource && sourceImageUrl) { + if ( + isAgentPsdImageLayerSlot(layer) && + layer.isDefaultResultSource && + sourceImageUrl + ) { imageAssignments[layer.id] = sourceImageUrl; imagePlacements[layer.id] = getDefaultAgentPsdImagePlacement(); } @@ -245,7 +286,11 @@ function buildPreviewFontFaces(template: AgentPsdTemplate | null) { const rules = new Map(); for (const layer of template.layers) { - if (!isAgentPsdTextLayerSlot(layer) || !layer.fontFamily || !layer.fontFileUrl) { + if ( + !isAgentPsdTextLayerSlot(layer) || + !layer.fontFamily || + !layer.fontFileUrl + ) { continue; } const familyAliases = Array.from( @@ -320,7 +365,9 @@ function getPreferredSelectedLayerId(template: AgentPsdTemplate | null) { return editableLayers[0]?.id || ""; } -function getImageFit(replaceMode: AgentPsdTemplateImageLayerSlot["replaceMode"]): CSSProperties["objectFit"] { +function getImageFit( + replaceMode: AgentPsdTemplateImageLayerSlot["replaceMode"] +): CSSProperties["objectFit"] { if (replaceMode === "contain") return "contain"; if (replaceMode === "stretch") return "fill"; return "cover"; @@ -342,7 +389,10 @@ function getLayerFrameStyle( }; } -function getResolvedTextPreview(layer: AgentPsdTemplateTextLayerSlot, value: string) { +function getResolvedTextPreview( + layer: AgentPsdTemplateTextLayerSlot, + value: string +) { return value || layer.placeholder || layer.layerName; } @@ -415,7 +465,9 @@ function getTextLayerContentStyle( }; } -function getAssignedImageMaskStyle(layer: AgentPsdTemplateImageLayerSlot): CSSProperties { +function getAssignedImageMaskStyle( + layer: AgentPsdTemplateImageLayerSlot +): CSSProperties { const hasMask = Boolean(layer.maskUrl); return { WebkitMaskImage: hasMask ? `url(${layer.maskUrl})` : undefined, @@ -454,12 +506,20 @@ function getPlacedAssignedImageStyle(params: { transform: `translate(-50%, -50%) scale(${normalizedPlacement.scale})`, transformOrigin: "center center", objectFit: getImageFit(layer.replaceMode), + maxWidth: "none", + maxHeight: "none", willChange: "transform", }; } -function hasEditedTextValue(layer: AgentPsdTemplateTextLayerSlot, value: string) { - return String(value || "").replace(/\r\n/g, "\n") !== String(layer.defaultText || "").replace(/\r\n/g, "\n"); +function hasEditedTextValue( + layer: AgentPsdTemplateTextLayerSlot, + value: string +) { + return ( + String(value || "").replace(/\r\n/g, "\n") !== + String(layer.defaultText || "").replace(/\r\n/g, "\n") + ); } function hasCustomizedTextLayerAppearance( @@ -513,7 +573,8 @@ export function AgentPsdTemplateEditorDialog({ templateMode?: AgentPsdTemplateMode | null; }) { const normalizedBrandName = String(brandName || "").trim(); - const normalizedTemplateMode = templateMode === "sku" ? "sku" : templateMode === "main" ? "main" : ""; + const normalizedTemplateMode = + templateMode === "sku" ? "sku" : templateMode === "main" ? "main" : ""; const templateListRequestUrl = useMemo(() => { if (!open || !normalizedBrandName) { return null; @@ -527,27 +588,45 @@ export function AgentPsdTemplateEditorDialog({ templateListRequestUrl, fetcher ); - const templates = useMemo(() => (data?.success ? data.templates : []), [data]); + const templates = useMemo( + () => (data?.success ? data.templates : []), + [data] + ); const textColorConfig = useMemo( () => normalizeAgentPsdTextColorConfig(data?.textColorConfig), [data?.textColorConfig] ); const secondaryCategoryConfig = useMemo( - () => normalizeAgentPsdTemplateSecondaryCategoryConfig(data?.secondaryCategoryConfig), + () => + normalizeAgentPsdTemplateSecondaryCategoryConfig( + data?.secondaryCategoryConfig + ), [data?.secondaryCategoryConfig] ); const [selectedTemplateId, setSelectedTemplateId] = useState(""); const [selectedLayerId, setSelectedLayerId] = useState(""); - const [imageAssignments, setImageAssignments] = useState>({}); - const [imagePlacements, setImagePlacements] = useState>({}); - const [imageSizes, setImageSizes] = useState>({}); - const [textAssignments, setTextAssignments] = useState>({}); - const [textColorAssignments, setTextColorAssignments] = useState>({}); + const [imageAssignments, setImageAssignments] = useState< + Record + >({}); + const [imagePlacements, setImagePlacements] = useState< + Record + >({}); + const [imageSizes, setImageSizes] = useState< + Record + >({}); + const [textAssignments, setTextAssignments] = useState< + Record + >({}); + const [textColorAssignments, setTextColorAssignments] = useState< + Record + >({}); const [uploadingSlotId, setUploadingSlotId] = useState(null); const [isRendering, setIsRendering] = useState(false); const [renderedResultUrl, setRenderedResultUrl] = useState(""); - const [rightPanelView, setRightPanelView] = useState<"editor" | "export">("editor"); + const [rightPanelView, setRightPanelView] = useState<"editor" | "export">( + "editor" + ); const [stageScale, setStageScale] = useState(1); const previewViewportRef = useRef(null); @@ -562,33 +641,18 @@ export function AgentPsdTemplateEditorDialog({ } | null>(null); const selectedTemplate = useMemo( - () => templates.find((template) => template.id === selectedTemplateId) || null, + () => + templates.find((template) => template.id === selectedTemplateId) || null, [selectedTemplateId, templates] ); const shouldGroupTemplatesBySecondaryCategory = useMemo( () => secondaryCategoryConfig.enabled && - templates.some((template) => Boolean(String(template.secondaryCategory || "").trim())), + templates.some((template) => + Boolean(String(template.secondaryCategory || "").trim()) + ), [secondaryCategoryConfig.enabled, templates] ); - const groupedTemplates = useMemo(() => { - if (!shouldGroupTemplatesBySecondaryCategory) { - return [] as Array<{ label: string; templates: AgentPsdTemplate[] }>; - } - - const groups = new Map(); - for (const template of templates) { - const groupLabel = String(template.secondaryCategory || "").trim() || "未分组"; - const currentGroup = groups.get(groupLabel) || []; - currentGroup.push(template); - groups.set(groupLabel, currentGroup); - } - - return Array.from(groups.entries()).map(([label, grouped]) => ({ - label, - templates: grouped, - })); - }, [shouldGroupTemplatesBySecondaryCategory, templates]); const mainModeTemplates = useMemo( () => templates.filter((template) => template.mode === "main"), [templates] @@ -603,6 +667,32 @@ export function AgentPsdTemplateEditorDialog({ } return selectedTemplate.layers; }, [selectedTemplate]); + const selectedTemplateMode: AgentPsdTemplateMode = + selectedTemplate?.mode === "sku" ? "sku" : "main"; + const selectedModeTemplates = useMemo( + () => + selectedTemplateMode === "sku" ? skuModeTemplates : mainModeTemplates, + [mainModeTemplates, selectedTemplateMode, skuModeTemplates] + ); + const groupedSelectedModeTemplates = useMemo(() => { + if (!shouldGroupTemplatesBySecondaryCategory) { + return [] as Array<{ label: string; templates: AgentPsdTemplate[] }>; + } + + const groups = new Map(); + for (const template of selectedModeTemplates) { + const groupLabel = + String(template.secondaryCategory || "").trim() || "未分组"; + const currentGroup = groups.get(groupLabel) || []; + currentGroup.push(template); + groups.set(groupLabel, currentGroup); + } + + return Array.from(groups.entries()).map(([label, grouped]) => ({ + label, + templates: grouped, + })); + }, [selectedModeTemplates, shouldGroupTemplatesBySecondaryCategory]); const editableImageLayers = useMemo( () => @@ -631,19 +721,24 @@ export function AgentPsdTemplateEditorDialog({ const orderedEditableImageLayers = useMemo( () => orderedEditableLayers.filter( - (layer): layer is AgentPsdTemplateImageLayerSlot => isAgentPsdImageLayerSlot(layer) + (layer): layer is AgentPsdTemplateImageLayerSlot => + isAgentPsdImageLayerSlot(layer) ), [orderedEditableLayers] ); const orderedEditableTextLayers = useMemo( () => orderedEditableLayers.filter( - (layer): layer is AgentPsdTemplateTextLayerSlot => isAgentPsdTextLayerSlot(layer) + (layer): layer is AgentPsdTemplateTextLayerSlot => + isAgentPsdTextLayerSlot(layer) ), [orderedEditableLayers] ); const shouldShowCompositeFallback = useMemo( - () => orderedPreviewLayers.some((layer) => !String(layer.sourceUrl || "").trim()), + () => + orderedPreviewLayers.some( + (layer) => !String(layer.sourceUrl || "").trim() + ), [orderedPreviewLayers] ); @@ -677,7 +772,9 @@ export function AgentPsdTemplateEditorDialog({ for (const layer of editableTextLayers) { const explicit = textAssignments[layer.id]; nextAssignments[layer.id] = - typeof explicit === "string" ? explicit : String(layer.defaultText || ""); + typeof explicit === "string" + ? explicit + : String(layer.defaultText || ""); } return nextAssignments; @@ -708,177 +805,197 @@ export function AgentPsdTemplateEditorDialog({ if (!String(effectiveImageAssignments[layer.id] || "").trim()) { continue; } - nextPlacements[layer.id] = normalizeAgentPsdImagePlacement(imagePlacements[layer.id]); + nextPlacements[layer.id] = normalizeAgentPsdImagePlacement( + imagePlacements[layer.id] + ); } return nextPlacements; - }, [editableImageLayers, effectiveImageAssignments, imagePlacements, selectedTemplate]); + }, [ + editableImageLayers, + effectiveImageAssignments, + imagePlacements, + selectedTemplate, + ]); const selectedLayer = useMemo( - () => selectedTemplateLayers.find((layer) => layer.id === selectedLayerId) || null, + () => + selectedTemplateLayers.find((layer) => layer.id === selectedLayerId) || + null, [selectedLayerId, selectedTemplateLayers] ); - const selectedImageLayer = selectedLayer && isAgentPsdImageLayerSlot(selectedLayer) ? selectedLayer : null; - const selectedTextLayer = selectedLayer && isAgentPsdTextLayerSlot(selectedLayer) ? selectedLayer : null; + const selectedImageLayer = + selectedLayer && isAgentPsdImageLayerSlot(selectedLayer) + ? selectedLayer + : null; + const selectedTextLayer = + selectedLayer && isAgentPsdTextLayerSlot(selectedLayer) + ? selectedLayer + : null; const selectedImageAssignmentUrl = selectedImageLayer ? String(effectiveImageAssignments[selectedImageLayer.id] || "").trim() : ""; const selectedImagePlacement = selectedImageLayer - ? normalizeAgentPsdImagePlacement(effectiveImagePlacements[selectedImageLayer.id]) + ? normalizeAgentPsdImagePlacement( + effectiveImagePlacements[selectedImageLayer.id] + ) : getDefaultAgentPsdImagePlacement(); const selectedTextColor = selectedTextLayer - ? effectiveTextColorAssignments[selectedTextLayer.id] || selectedTextLayer.textColor + ? effectiveTextColorAssignments[selectedTextLayer.id] || + selectedTextLayer.textColor : "#111111"; - const selectedImageAdjustmentPanel = - selectedImageLayer ? ( -
-
- {sourceImageUrl ? ( + const selectedImageAdjustmentPanel = selectedImageLayer ? ( +
+
+ {sourceImageUrl ? ( + + ) : null} + +
+ + {selectedImageAssignmentUrl ? ( + <> +
- ) : null} - -
+
- {selectedImageAssignmentUrl ? ( - <> -
- -
+
-
- -
-
-
- - 缩放 -
- - {Math.round(selectedImagePlacement.scale * 100)}% - +
+
+
+ + 缩放
-
-
- - { - const newScale = clampImageScale(value[0] || 1); - updateImagePlacement(selectedImageLayer.id, (current) => ({ - ...current, - scale: newScale, - })); - }} - /> - -
-
- 40% - 100% - 300% -
+ + {Math.round(selectedImagePlacement.scale * 100)}% + +
+
+
+ + { + const newScale = clampImageScale(value[0] || 1); + updateImagePlacement(selectedImageLayer.id, (current) => ({ + ...current, + scale: newScale, + })); + }} + rangeClassName="bg-gradient-to-r from-emerald-300 via-green-400 to-teal-400 shadow-[0_0_12px_rgba(16,185,129,0.38)]" + step={0.01} + thumbClassName="h-5 w-5 border-2 border-emerald-100 bg-[#0a0a0d] shadow-[0_0_0_5px_rgba(16,185,129,0.2)]" + trackClassName="h-2.5 rounded-full border border-white/20 bg-[#0f1014]" + value={[selectedImagePlacement.scale]} + /> + +
+
+ 40% + 100% + 300%
+
-
- -
- - ) : null} -
- ) : null; - const fontFaceCss = useMemo(() => buildPreviewFontFaces(selectedTemplate), [selectedTemplate]); +
+ +
+ + ) : null} +
+ ) : null; + const fontFaceCss = useMemo( + () => buildPreviewFontFaces(selectedTemplate), + [selectedTemplate] + ); const selectedTemplateSelectValue = selectedTemplate?.id; const hasTemplateOptions = templates.length > 0; @@ -911,7 +1028,10 @@ export function AgentPsdTemplateEditorDialog({ if (!open) return; setRenderedResultUrl(""); setRightPanelView("editor"); - const initialState = createInitialState(selectedTemplate, sourceImageUrl || ""); + const initialState = createInitialState( + selectedTemplate, + sourceImageUrl || "" + ); setImageAssignments(initialState.imageAssignments); setImagePlacements(initialState.imagePlacements); setTextAssignments(initialState.textAssignments); @@ -942,7 +1062,7 @@ export function AgentPsdTemplateEditorDialog({ return () => { window.clearTimeout(timeoutId); }; - }, [open, selectedTemplate, sourceImageUrl]); + }, [open, selectedTemplate?.id]); useEffect(() => { if (!selectedTemplate) { @@ -960,7 +1080,9 @@ export function AgentPsdTemplateEditorDialog({ let cancelled = false; for (const layer of editableImageLayers) { - const assignedUrl = String(effectiveImageAssignments[layer.id] || "").trim(); + const assignedUrl = String( + effectiveImageAssignments[layer.id] || "" + ).trim(); if (!assignedUrl) { continue; } @@ -998,7 +1120,12 @@ export function AgentPsdTemplateEditorDialog({ return () => { cancelled = true; }; - }, [editableImageLayers, effectiveImageAssignments, imageSizes, selectedTemplate]); + }, [ + editableImageLayers, + effectiveImageAssignments, + imageSizes, + selectedTemplate, + ]); useEffect(() => { if (!selectedTemplate) { @@ -1013,24 +1140,35 @@ export function AgentPsdTemplateEditorDialog({ const rect = element.getBoundingClientRect(); const availableWidth = Math.max( 240, - Math.min(PREVIEW_STAGE_TARGET_WIDTH, rect.width - PREVIEW_VIEWPORT_PADDING) + Math.min( + PREVIEW_STAGE_TARGET_WIDTH, + rect.width - PREVIEW_VIEWPORT_PADDING + ) ); const availableHeight = Math.max( 240, - Math.min(PREVIEW_STAGE_TARGET_HEIGHT, rect.height - PREVIEW_VIEWPORT_PADDING) + Math.min( + PREVIEW_STAGE_TARGET_HEIGHT, + rect.height - PREVIEW_VIEWPORT_PADDING + ) ); const nextScale = Math.min( availableWidth / Math.max(1, selectedTemplate.width), availableHeight / Math.max(1, selectedTemplate.height), PREVIEW_STAGE_MAX_SCALE ); - setStageScale(Number.isFinite(nextScale) && nextScale > 0 ? nextScale : 1); + setStageScale( + Number.isFinite(nextScale) && nextScale > 0 ? nextScale : 1 + ); }; updateScale(); let animationFrameId: number | null = null; const scheduleScaleUpdate = () => { - if (typeof window !== "undefined" && typeof window.requestAnimationFrame === "function") { + if ( + typeof window !== "undefined" && + typeof window.requestAnimationFrame === "function" + ) { if (animationFrameId !== null) { window.cancelAnimationFrame(animationFrameId); } @@ -1085,11 +1223,17 @@ export function AgentPsdTemplateEditorDialog({ try { textarea.setSelectionRange(valueLength, valueLength); } catch (error) { - console.warn("[AgentPsdTemplateEditorDialog] setSelectionRange failed:", error); + console.warn( + "[AgentPsdTemplateEditorDialog] setSelectionRange failed:", + error + ); } }; - if (typeof window !== "undefined" && typeof window.requestAnimationFrame === "function") { + if ( + typeof window !== "undefined" && + typeof window.requestAnimationFrame === "function" + ) { const frameId = window.requestAnimationFrame(focusTextarea); return () => { window.cancelAnimationFrame(frameId); @@ -1102,7 +1246,10 @@ export function AgentPsdTemplateEditorDialog({ }; }, [selectedTextLayer?.id]); - async function handleSlotUpload(slotId: string, event: ChangeEvent) { + async function handleSlotUpload( + slotId: string, + event: ChangeEvent + ) { const file = event.target.files?.[0]; if (!file) return; @@ -1153,7 +1300,9 @@ export function AgentPsdTemplateEditorDialog({ ) { setImagePlacements((current) => ({ ...current, - [layerId]: normalizeAgentPsdImagePlacement(updater(normalizeAgentPsdImagePlacement(current[layerId]))), + [layerId]: normalizeAgentPsdImagePlacement( + updater(normalizeAgentPsdImagePlacement(current[layerId])) + ), })); } @@ -1184,14 +1333,19 @@ export function AgentPsdTemplateEditorDialog({ pointerId: event.pointerId, startClientX: event.clientX, startClientY: event.clientY, - startPlacement: normalizeAgentPsdImagePlacement(effectiveImagePlacements[layer.id]), + startPlacement: normalizeAgentPsdImagePlacement( + effectiveImagePlacements[layer.id] + ), }; const nextTarget = event.currentTarget; try { nextTarget.setPointerCapture?.(event.pointerId); } catch (error) { - console.warn("[AgentPsdTemplateEditorDialog] setPointerCapture failed:", error); + console.warn( + "[AgentPsdTemplateEditorDialog] setPointerCapture failed:", + error + ); } const handlePointerMove = (moveEvent: PointerEvent) => { @@ -1202,8 +1356,12 @@ export function AgentPsdTemplateEditorDialog({ updateImagePlacement(layer.id, () => ({ ...dragState.startPlacement, - offsetX: dragState.startPlacement.offsetX + (moveEvent.clientX - dragState.startClientX) / stageScale, - offsetY: dragState.startPlacement.offsetY + (moveEvent.clientY - dragState.startClientY) / stageScale, + offsetX: + dragState.startPlacement.offsetX + + (moveEvent.clientX - dragState.startClientX) / stageScale, + offsetY: + dragState.startPlacement.offsetY + + (moveEvent.clientY - dragState.startClientY) / stageScale, })); }; @@ -1259,14 +1417,30 @@ export function AgentPsdTemplateEditorDialog({ } } - function handleDownload() { - if (!renderedResultUrl) return; - window.open(renderedResultUrl, "_blank", "noopener,noreferrer"); + function handleSwitchSelectedTemplateMode(nextMode: AgentPsdTemplateMode) { + if (selectedTemplateMode === nextMode) { + return; + } + + const nextTemplates = + nextMode === "sku" ? skuModeTemplates : mainModeTemplates; + if (nextTemplates.length === 0) { + toast.error( + nextMode === "sku" + ? "当前品牌下暂无 SKU 图模式模板" + : "当前品牌下暂无主图模式模板" + ); + return; + } + + setSelectedTemplateId(nextTemplates[0].id); } return ( <> - : null} + `, + }} + /> + + + {fontFaceCss ? : null} -
-
- -
-
- - {selectedTemplate && hasTemplateOptions ? ( - + + {selectedTemplate ? ( +
+ {renderTemplateSelectContent(selectedTemplate, { + showSecondaryCategory: + shouldGroupTemplatesBySecondaryCategory, + })} +
+ ) : ( + + )} +
+ + {shouldGroupTemplatesBySecondaryCategory + ? groupedSelectedModeTemplates.map((group) => ( + + + {group.label} + + {group.templates.map((template) => ( + + {renderTemplateSelectContent(template, { + showSecondaryCategory: + shouldGroupTemplatesBySecondaryCategory, + compact: true, + })} + + ))} + + )) + : selectedModeTemplates.map((template) => ( {renderTemplateSelectContent(template, { - showSecondaryCategory: shouldGroupTemplatesBySecondaryCategory, compact: true, })} ))} - - )) - : templates.map((template) => ( - + +
+ ) : ( +
+ {isLoading ? ( + <> + {Array.from({ length: 2 }).map((_, index) => ( +
- {renderTemplateSelectContent(template, { compact: true })} - + 正在加载主图模式模板... +
))} - - - ) : ( -
- {isLoading ? ( - <> - {Array.from({ length: 2 }).map((_, index) => ( -
+ ) : mainModeTemplates.length > 0 ? ( + mainModeTemplates.map((template) => ( +
- ))} - - ) : mainModeTemplates.length > 0 ? ( - mainModeTemplates.map((template) => ( - - )) - ) : ( -
- {normalizedBrandName - ? `当前品牌“${normalizedBrandName}”下暂无主图模式模板` - : "请先在工作流中选择品牌后再打开 PSD 模板。"} + {renderTemplatePickerCardContent(template, { + showSecondaryCategory: + shouldGroupTemplatesBySecondaryCategory, + emphasize: true, + })} + + )) + ) : ( +
+ {normalizedBrandName + ? `当前品牌“${normalizedBrandName}”下暂无主图模式模板` + : "请先在工作流中选择品牌后再打开 PSD 模板。"} +
+ )} +
+ )} +
+ + {selectedTemplate ? ( + <> +
+
+ {selectedTemplate.name}
- )} +
+ + + + {selectedImageAdjustmentPanel} + + ) : ( +
+ {isLoading + ? "正在加载可用模板..." + : hasTemplateOptions + ? "请从左侧主图模式模板或右侧 SKU 图模式模板中选择一个模板开始编辑。" + : normalizedBrandName + ? `当前品牌“${normalizedBrandName}”下还没有可用 PSD 模板,请先去后台配置。` + : "当前还没有启用的 PSD 模板,请先去后台配置。"}
)}
+ +
+
+
{selectedTemplate ? ( - <> -
-
- {selectedTemplate.name} -
-
- - - - {selectedImageAdjustmentPanel} - - ) : ( -
- {isLoading - ? "正在加载可用模板..." - : normalizedBrandName - ? `当前品牌“${normalizedBrandName}”下还没有可用 PSD 模板,请先去后台配置。` - : "当前还没有启用的 PSD 模板,请先去后台配置。"} -
- )} -
- -
- -
-
- {selectedTemplate ? ( -
- {shouldShowCompositeFallback ? ( - {`${selectedTemplate.name} - ) : null} +
+ {shouldShowCompositeFallback ? ( + {`${selectedTemplate.name} + ) : null} -
- {orderedPreviewLayers.map((layer) => { - const layerBoxStyle = { - left: layer.x, - top: layer.y, - width: layer.width, - height: layer.height, - } satisfies CSSProperties; +
+ {orderedPreviewLayers.map((layer) => { + const layerBoxStyle = { + left: layer.x, + top: layer.y, + width: layer.width, + height: layer.height, + } satisfies CSSProperties; - if (isAgentPsdImageLayerSlot(layer)) { - const assignedUrl = effectiveImageAssignments[layer.id]; - const layerImageUrl = assignedUrl || layer.sourceUrl; - if (!layerImageUrl) { + if (isAgentPsdImageLayerSlot(layer)) { + const assignedUrl = + effectiveImageAssignments[layer.id]; + const layerImageUrl = + assignedUrl || layer.sourceUrl; + if (!layerImageUrl) { + return null; + } + const assignedImageContainerStyle = assignedUrl + ? { + ...layerBoxStyle, + ...getAssignedImageMaskStyle(layer), + } + : layerBoxStyle; + + return ( +
+ {layer.layerName} +
+ ); + } + + const isSelected = selectedLayerId === layer.id; + if (isSelected) { return null; } - const assignedImageContainerStyle = assignedUrl - ? { - ...layerBoxStyle, - ...getAssignedImageMaskStyle(layer), - } - : layerBoxStyle; - return ( -
- {layer.layerName} -
- ); - } + const value = + effectiveTextAssignments[layer.id] || ""; + const resolvedTextColor = + effectiveTextColorAssignments[layer.id] || + layer.textColor; + const shouldUseEditedPreview = + hasCustomizedTextLayerAppearance( + layer, + value, + resolvedTextColor + ); - const isSelected = selectedLayerId === layer.id; - if (isSelected) { - return null; - } - - const value = effectiveTextAssignments[layer.id] || ""; - const resolvedTextColor = effectiveTextColorAssignments[layer.id] || layer.textColor; - const shouldUseEditedPreview = hasCustomizedTextLayerAppearance( - layer, - value, - resolvedTextColor - ); - - if (!shouldUseEditedPreview && layer.sourceUrl) { - return ( -
- {layer.layerName} -
- ); - } - - if (!value && !layer.placeholder) { - return null; - } - - const textFrame = getTextLayerRenderFrame( - layer, - value || layer.placeholder || layer.layerName - ); - - return ( -
-
- {value || layer.placeholder || layer.layerName} -
-
- ); - })} -
- -
- {orderedEditableImageLayers.map((layer) => { - const isSelected = selectedLayerId === layer.id; - const frameStyle = getLayerFrameStyle(layer, isSelected); - const hasAssignedImage = Boolean( - String(effectiveImageAssignments[layer.id] || "").trim() - ); - - return isSelected && hasAssignedImage ? ( -
setSelectedLayerId(layer.id)} - onPointerDown={(event) => handleImageDragStart(layer, event)} - className="absolute overflow-hidden rounded-lg transition-all" - style={{ - ...frameStyle, - zIndex: 20, - background: "rgba(249,115,22,0.08)", - cursor: imageDragStateRef.current?.layerId === layer.id ? "grabbing" : "grab", - touchAction: "none", - }} - aria-label={`${getLayerLabel(layer)} ${layer.layerName}`} - title={`${getLayerLabel(layer)}:${layer.layerName}`} - > -
- {layer.layerName} -
-
- - 拖动调整 -
-
- ) : ( - - ); - })} + ); + } - {orderedEditableImageLayers.map((layer) => { - const isSelected = selectedLayerId === layer.id; - const hasAssignedImage = Boolean( - String(effectiveImageAssignments[layer.id] || "").trim() - ); - if (!isSelected || !hasAssignedImage) { - return null; - } + if (!value && !layer.placeholder) { + return null; + } - return ( -
- - - -
- ); - })} + const textFrame = getTextLayerRenderFrame( + layer, + value || layer.placeholder || layer.layerName + ); - {orderedEditableTextLayers.map((layer) => { - const isSelected = selectedLayerId === layer.id; - const frameStyle = getLayerFrameStyle(layer, isSelected); - const currentValue = effectiveTextAssignments[layer.id] || ""; - const resolvedTextColor = - effectiveTextColorAssignments[layer.id] || layer.textColor; - const preserveSourceAppearance = - !!layer.sourceUrl && - !hasCustomizedTextLayerAppearance(layer, currentValue, resolvedTextColor); - const editedTextFrame = getTextLayerRenderFrame(layer, currentValue); - - if (isSelected) { return (
- {preserveSourceAppearance && layer.sourceUrl ? ( - {layer.layerName} - ) : null}
-