fix: guard psd template dialog runtime errors
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-01 13:20:29 +08:00
parent 89dfc39284
commit 49b54c2387

View File

@ -412,11 +412,20 @@ export function AgentPsdTemplateEditorDialog({
? normalizeAgentPsdImagePlacement(effectiveImagePlacements[selectedImageLayer.id])
: getDefaultAgentPsdImagePlacement();
const fontFaceCss = useMemo(() => buildPreviewFontFaces(selectedTemplate), [selectedTemplate]);
const selectedTemplateSelectValue = selectedTemplate?.id;
useEffect(() => {
if (!open) return;
if (templates.length > 0 && !templates.some((template) => template.id === selectedTemplateId)) {
if (templates.length === 0) {
if (selectedTemplateId) {
setSelectedTemplateId("");
}
setSelectedLayerId("");
return;
}
if (!templates.some((template) => template.id === selectedTemplateId)) {
setSelectedTemplateId(templates[0].id);
}
}, [open, selectedTemplateId, templates]);
@ -436,6 +445,9 @@ export function AgentPsdTemplateEditorDialog({
setImageSizes({});
return;
}
if (typeof window === "undefined" || typeof window.Image !== "function") {
return;
}
let cancelled = false;
for (const layer of editableImageLayers) {
@ -496,10 +508,44 @@ export function AgentPsdTemplateEditorDialog({
};
updateScale();
const observer = new ResizeObserver(updateScale);
observer.observe(element);
let animationFrameId: number | null = null;
const scheduleScaleUpdate = () => {
if (typeof window !== "undefined" && typeof window.requestAnimationFrame === "function") {
if (animationFrameId !== null) {
window.cancelAnimationFrame(animationFrameId);
}
animationFrameId = window.requestAnimationFrame(() => {
animationFrameId = null;
updateScale();
});
return;
}
updateScale();
};
if (typeof ResizeObserver !== "undefined") {
const observer = new ResizeObserver(scheduleScaleUpdate);
observer.observe(element);
return () => {
observer.disconnect();
if (animationFrameId !== null && typeof window !== "undefined") {
window.cancelAnimationFrame(animationFrameId);
}
};
}
if (typeof window !== "undefined") {
window.addEventListener("resize", scheduleScaleUpdate);
}
return () => {
observer.disconnect();
if (typeof window !== "undefined") {
window.removeEventListener("resize", scheduleScaleUpdate);
if (animationFrameId !== null) {
window.cancelAnimationFrame(animationFrameId);
}
}
};
}, [selectedTemplate]);
@ -513,11 +559,23 @@ export function AgentPsdTemplateEditorDialog({
return;
}
requestAnimationFrame(() => {
const focusTextarea = () => {
textarea.focus();
const valueLength = textarea.value.length;
textarea.setSelectionRange(valueLength, valueLength);
});
};
if (typeof window !== "undefined" && typeof window.requestAnimationFrame === "function") {
const frameId = window.requestAnimationFrame(focusTextarea);
return () => {
window.cancelAnimationFrame(frameId);
};
}
const timeoutId = window.setTimeout(focusTextarea, 0);
return () => {
window.clearTimeout(timeoutId);
};
}, [selectedTextLayer?.id]);
async function handleSlotUpload(slotId: string, event: ChangeEvent<HTMLInputElement>) {
@ -635,21 +693,6 @@ export function AgentPsdTemplateEditorDialog({
window.addEventListener("pointercancel", handlePointerUp);
}
function handleImageWheel(
layer: AgentPsdTemplateImageLayerSlot,
event: ReactPointerEvent<HTMLDivElement>
) {
if (!String(effectiveImageAssignments[layer.id] || "").trim()) {
return;
}
event.preventDefault();
event.stopPropagation();
const delta = event.deltaY < 0 ? 0.08 : -0.08;
nudgeImageScale(layer.id, delta);
}
async function handleRender() {
if (!selectedTemplate) {
toast.error("请先选择 PSD 模板");
@ -715,7 +758,7 @@ export function AgentPsdTemplateEditorDialog({
<div className="space-y-4">
<div className="space-y-2">
<Label>选择模板</Label>
<Select value={selectedTemplateId} onValueChange={setSelectedTemplateId}>
<Select value={selectedTemplateSelectValue} onValueChange={setSelectedTemplateId}>
<SelectTrigger>
<SelectValue placeholder={isLoading ? "加载模板中..." : "请选择模板"} />
</SelectTrigger>
@ -937,7 +980,6 @@ export function AgentPsdTemplateEditorDialog({
tabIndex={0}
onClick={() => setSelectedLayerId(layer.id)}
onPointerDown={(event) => handleImageDragStart(layer, event)}
onWheel={(event) => handleImageWheel(layer, event)}
className="absolute overflow-hidden rounded-lg transition-all"
style={{
...frameStyle,
@ -952,50 +994,9 @@ export function AgentPsdTemplateEditorDialog({
<div className="absolute left-2 top-2 rounded-full bg-black/70 px-2 py-1 text-[11px] text-white">
{layer.layerName}
</div>
<div className="absolute right-2 top-2 flex items-center gap-1 rounded-full bg-black/70 p-1 text-white shadow-lg">
<button
type="button"
className="rounded-full p-1 transition hover:bg-white/10"
onPointerDown={(event) => event.stopPropagation()}
onClick={(event) => {
event.stopPropagation();
nudgeImageScale(layer.id, -0.1);
}}
aria-label="缩小图片"
title="缩小"
>
<ZoomOut className="h-3.5 w-3.5" />
</button>
<button
type="button"
className="rounded-full p-1 transition hover:bg-white/10"
onPointerDown={(event) => event.stopPropagation()}
onClick={(event) => {
event.stopPropagation();
nudgeImageScale(layer.id, 0.1);
}}
aria-label="放大图片"
title="放大"
>
<ZoomIn className="h-3.5 w-3.5" />
</button>
<button
type="button"
className="rounded-full p-1 transition hover:bg-white/10"
onPointerDown={(event) => event.stopPropagation()}
onClick={(event) => {
event.stopPropagation();
resetImagePlacement(layer.id);
}}
aria-label="重置图片位置和缩放"
title="重置"
>
<RefreshCcw className="h-3.5 w-3.5" />
</button>
</div>
<div className="pointer-events-none absolute bottom-2 right-2 flex items-center gap-1 rounded-full bg-orange-500/85 px-2 py-1 text-[11px] text-white">
<Move className="h-3 w-3" />
拖动或滚轮缩放
拖动调整
</div>
</div>
) : (
@ -1025,6 +1026,56 @@ export function AgentPsdTemplateEditorDialog({
);
})}
{orderedEditableImageLayers.map((layer) => {
const isSelected = selectedLayerId === layer.id;
const hasAssignedImage = Boolean(
String(effectiveImageAssignments[layer.id] || "").trim()
);
if (!isSelected || !hasAssignedImage) {
return null;
}
return (
<div
key={`${layer.id}-controls`}
className="absolute flex items-center gap-1 rounded-full bg-black/72 p-1 text-white shadow-lg"
style={{
left: layer.x + layer.width - 112,
top: Math.max(8, layer.y - 40),
zIndex: 25,
}}
>
<button
type="button"
className="rounded-full p-1.5 transition hover:bg-white/10"
onClick={() => nudgeImageScale(layer.id, -0.1)}
aria-label="缩小图片"
title="缩小"
>
<ZoomOut className="h-3.5 w-3.5" />
</button>
<button
type="button"
className="rounded-full p-1.5 transition hover:bg-white/10"
onClick={() => nudgeImageScale(layer.id, 0.1)}
aria-label="放大图片"
title="放大"
>
<ZoomIn className="h-3.5 w-3.5" />
</button>
<button
type="button"
className="rounded-full p-1.5 transition hover:bg-white/10"
onClick={() => resetImagePlacement(layer.id)}
aria-label="重置图片位置和缩放"
title="重置"
>
<RefreshCcw className="h-3.5 w-3.5" />
</button>
</div>
);
})}
{orderedEditableTextLayers.map((layer) => {
const isSelected = selectedLayerId === layer.id;
const frameStyle = getLayerFrameStyle(layer, isSelected);