fix: harden agent PSD template dialog
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 21:52:25 +08:00
parent d02416d1b2
commit 8853bfae77
3 changed files with 118 additions and 39 deletions

View File

@ -33,11 +33,19 @@ import {
} from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { ClientErrorBoundary } from "@/components/ui/client-error-boundary";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
@ -5823,16 +5831,46 @@ export function AgentGenerationWorkspaceDarkV2({
/>
) : null}
<AgentPsdTemplateEditorDialog
open={Boolean(psdTemplateDialogSourceUrl)}
onOpenChange={(open) => {
if (!open) {
setPsdTemplateDialogSourceUrl(null);
}
}}
sourceImageUrl={psdTemplateDialogSourceUrl}
brandName={workflowContext?.brand || null}
/>
<ClientErrorBoundary
resetKeys={[psdTemplateDialogSourceUrl || "", workflowContext?.brand || ""]}
fallback={
<Dialog
open={Boolean(psdTemplateDialogSourceUrl)}
onOpenChange={(open) => {
if (!open) {
setPsdTemplateDialogSourceUrl(null);
}
}}
>
<DialogContent
overlayClassName="bg-black/85 backdrop-blur-sm"
className="max-w-lg border border-white/10 bg-[#09090b] text-white"
>
<DialogHeader>
<DialogTitle>PSD 模板暂时无法打开</DialogTitle>
<DialogDescription>
已拦截本次客户端异常,避免整个智能体工作台被一起打断。
</DialogDescription>
</DialogHeader>
<div className="space-y-3 text-sm leading-6 text-white/72">
<p>请关闭弹窗后重试一次。</p>
<p>如果仍然出现,优先检查当前品牌下的 PSD 模板配置、图层数据或字体资源是否完整。</p>
</div>
</DialogContent>
</Dialog>
}
>
<AgentPsdTemplateEditorDialog
open={Boolean(psdTemplateDialogSourceUrl)}
onOpenChange={(open) => {
if (!open) {
setPsdTemplateDialogSourceUrl(null);
}
}}
sourceImageUrl={psdTemplateDialogSourceUrl}
brandName={workflowContext?.brand || null}
/>
</ClientErrorBoundary>
{/* Fullscreen Preview */}
<AnimatePresence>

View File

@ -455,6 +455,7 @@ export function AgentPsdTemplateEditorDialog({
: getDefaultAgentPsdImagePlacement();
const fontFaceCss = useMemo(() => buildPreviewFontFaces(selectedTemplate), [selectedTemplate]);
const selectedTemplateSelectValue = selectedTemplate?.id;
const hasTemplateOptions = templates.length > 0;
useEffect(() => {
if (!open) return;
@ -481,24 +482,30 @@ export function AgentPsdTemplateEditorDialog({
setTextAssignments(initialState.textAssignments);
setSelectedLayerId(getPreferredSelectedLayerId(selectedTemplate));
// 自动适配缩放比例(延迟执行确保 DOM 已渲染)
if (selectedTemplate) {
setTimeout(() => {
if (previewViewportRef.current) {
const viewport = previewViewportRef.current;
const padding = 32;
const availableWidth = viewport.clientWidth - padding;
const availableHeight = viewport.clientHeight - padding;
if (availableWidth > 0 && availableHeight > 0) {
const scaleX = availableWidth / selectedTemplate.width;
const scaleY = availableHeight / selectedTemplate.height;
const fitScale = Math.min(scaleX, scaleY, 1);
setStageScale(fitScale);
}
}
}, 100);
if (!selectedTemplate || typeof window === "undefined") {
return;
}
// 自动适配缩放比例(延迟执行确保 DOM 已渲染)
const timeoutId = window.setTimeout(() => {
if (previewViewportRef.current) {
const viewport = previewViewportRef.current;
const padding = 32;
const availableWidth = viewport.clientWidth - padding;
const availableHeight = viewport.clientHeight - padding;
if (availableWidth > 0 && availableHeight > 0) {
const scaleX = availableWidth / selectedTemplate.width;
const scaleY = availableHeight / selectedTemplate.height;
const fitScale = Math.min(scaleX, scaleY, 1);
setStageScale(fitScale);
}
}
}, 100);
return () => {
window.clearTimeout(timeoutId);
};
}, [open, selectedTemplate, sourceImageUrl]);
useEffect(() => {
@ -844,18 +851,28 @@ export function AgentPsdTemplateEditorDialog({
<div className="space-y-4">
<div className="space-y-2">
<Label>选择模板</Label>
<Select value={selectedTemplateSelectValue} onValueChange={setSelectedTemplateId}>
<SelectTrigger>
<SelectValue placeholder={isLoading ? "加载模板中..." : "请选择模板"} />
</SelectTrigger>
<SelectContent>
{templates.map((template) => (
<SelectItem key={template.id} value={template.id}>
{template.name}
</SelectItem>
))}
</SelectContent>
</Select>
{hasTemplateOptions ? (
<Select value={selectedTemplateSelectValue} onValueChange={setSelectedTemplateId}>
<SelectTrigger>
<SelectValue placeholder={isLoading ? "加载模板中..." : "请选择模板"} />
</SelectTrigger>
<SelectContent>
{templates.map((template) => (
<SelectItem key={template.id} value={template.id}>
{template.name}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<div className="rounded-xl border border-dashed border-white/15 bg-white/[0.02] px-3 py-2 text-sm text-muted-foreground">
{isLoading
? "正在加载可用模板..."
: normalizedBrandName
? `当前品牌“${normalizedBrandName}”下暂无可用模板`
: "请先在工作流中选择品牌后再打开 PSD 模板。"}
</div>
)}
</div>
{selectedTemplate ? (

View File

@ -5,12 +5,27 @@ import React from "react";
interface ClientErrorBoundaryProps {
children: React.ReactNode;
fallback: React.ReactNode;
resetKeys?: unknown[];
}
interface ClientErrorBoundaryState {
hasError: boolean;
}
function areResetKeysEqual(nextKeys?: unknown[], prevKeys?: unknown[]) {
if (nextKeys === prevKeys) {
return true;
}
if (!nextKeys || !prevKeys) {
return false;
}
if (nextKeys.length !== prevKeys.length) {
return false;
}
return nextKeys.every((key, index) => Object.is(key, prevKeys[index]));
}
export class ClientErrorBoundary extends React.Component<
ClientErrorBoundaryProps,
ClientErrorBoundaryState
@ -24,6 +39,15 @@ export class ClientErrorBoundary extends React.Component<
return { hasError: true };
}
componentDidUpdate(prevProps: ClientErrorBoundaryProps) {
if (
this.state.hasError &&
!areResetKeysEqual(this.props.resetKeys, prevProps.resetKeys)
) {
this.setState({ hasError: false });
}
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error("[ClientErrorBoundary] Caught client error:", error, errorInfo);
}