From 0d3d5a9507bf492c9dee50c57dd45444441b9eab Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 11 Apr 2026 00:08:29 +0800 Subject: [PATCH] fix: polish PSD editor scrolling and dimension styles --- .../agents/AgentPsdTemplateEditorDialog.tsx | 123 ++- .../agent-psd-template-export-preview.tsx | 704 +++++++++--------- 2 files changed, 487 insertions(+), 340 deletions(-) diff --git a/components/agents/AgentPsdTemplateEditorDialog.tsx b/components/agents/AgentPsdTemplateEditorDialog.tsx index f1cc4ece6..09ddca37d 100644 --- a/components/agents/AgentPsdTemplateEditorDialog.tsx +++ b/components/agents/AgentPsdTemplateEditorDialog.tsx @@ -1,6 +1,8 @@ "use client"; import { + ChevronDown, + ChevronUp, Eye, EyeOff, Expand, @@ -244,6 +246,7 @@ const MIN_IMAGE_SCALE = 0.4; const MAX_IMAGE_SCALE = 3; const UNASSIGNED_BRAND_LABEL = "未分类"; const IMAGE_EXTENSION_REGEX = /\.([a-z0-9]{2,5})$/i; +const HIDDEN_LAYER_SCROLL_STEP = 180; function buildTemplatePreviewThumbUrl( previewUrl: string, @@ -267,6 +270,12 @@ function buildTemplatePreviewThumbUrl( return `/api/image-proxy?${searchParams.toString()}`; } +function getScrollAreaViewportElement(root: HTMLDivElement | null) { + return root?.querySelector( + "[data-radix-scroll-area-viewport]" + ) as HTMLDivElement | null; +} + function isLikelyStableRenderImageUrl(url: string) { const normalized = String(url || "").trim(); if (!normalized) { @@ -1312,8 +1321,11 @@ export function AgentPsdTemplateEditorDialog({ "editor" ); const [stageScale, setStageScale] = useState(1); + const [canScrollHiddenLayersUp, setCanScrollHiddenLayersUp] = useState(false); + const [canScrollHiddenLayersDown, setCanScrollHiddenLayersDown] = useState(false); const previewViewportRef = useRef(null); + const hiddenLayerScrollAreaRef = useRef(null); const uploadInputRef = useRef(null); const previousSelectedTemplateRef = useRef(null); const imageDragStateRef = useRef<{ @@ -1343,6 +1355,47 @@ export function AgentPsdTemplateEditorDialog({ } }, [open]); + useEffect(() => { + if (!open || typeof document === "undefined") { + return; + } + + const html = document.documentElement; + const body = document.body; + const previousHtmlOverflow = html.style.overflow; + const previousBodyOverflow = body.style.overflow; + const previousHtmlOverscrollBehavior = html.style.overscrollBehavior; + const previousBodyOverscrollBehavior = body.style.overscrollBehavior; + const previousHtmlBackgroundColor = html.style.backgroundColor; + const previousBodyBackgroundColor = body.style.backgroundColor; + const previousBodyPaddingRight = body.style.paddingRight; + const scrollbarWidth = Math.max(0, window.innerWidth - html.clientWidth); + const currentBodyPaddingRight = Number.parseFloat( + window.getComputedStyle(body).paddingRight || "0" + ); + + html.style.overflow = "hidden"; + body.style.overflow = "hidden"; + html.style.overscrollBehavior = "none"; + body.style.overscrollBehavior = "none"; + html.style.backgroundColor = "#050505"; + body.style.backgroundColor = "#050505"; + + if (scrollbarWidth > 0) { + body.style.paddingRight = `${currentBodyPaddingRight + scrollbarWidth}px`; + } + + return () => { + html.style.overflow = previousHtmlOverflow; + body.style.overflow = previousBodyOverflow; + html.style.overscrollBehavior = previousHtmlOverscrollBehavior; + body.style.overscrollBehavior = previousBodyOverscrollBehavior; + html.style.backgroundColor = previousHtmlBackgroundColor; + body.style.backgroundColor = previousBodyBackgroundColor; + body.style.paddingRight = previousBodyPaddingRight; + }; + }, [open]); + const selectedTemplateSummary = useMemo( () => templates.find((template) => template.id === selectedTemplateId) || null, @@ -1487,6 +1540,31 @@ export function AgentPsdTemplateEditorDialog({ new Set(hiddenLayerIds.map((layerId) => String(layerId || "").trim()).filter(Boolean)), [hiddenLayerIds] ); + useEffect(() => { + const viewport = getScrollAreaViewportElement(hiddenLayerScrollAreaRef.current); + if (!viewport) { + setCanScrollHiddenLayersUp(false); + setCanScrollHiddenLayersDown(false); + return; + } + + const updateScrollButtons = () => { + const nextCanScrollUp = viewport.scrollTop > 4; + const nextCanScrollDown = + viewport.scrollTop + viewport.clientHeight < viewport.scrollHeight - 4; + setCanScrollHiddenLayersUp(nextCanScrollUp); + setCanScrollHiddenLayersDown(nextCanScrollDown); + }; + + updateScrollButtons(); + viewport.addEventListener("scroll", updateScrollButtons, { passive: true }); + const animationFrameId = window.requestAnimationFrame(updateScrollButtons); + + return () => { + window.cancelAnimationFrame(animationFrameId); + viewport.removeEventListener("scroll", updateScrollButtons); + }; + }, [hiddenLayerIds.length, open, orderedPreviewLayers.length, selectedTemplate?.id]); const orderedVisiblePreviewLayers = useMemo( () => orderedPreviewLayers.filter((layer) => !hiddenLayerIdSet.has(layer.id)), [hiddenLayerIdSet, orderedPreviewLayers] @@ -2258,6 +2336,18 @@ export function AgentPsdTemplateEditorDialog({ })); } + function handleHiddenLayerListScroll(direction: "up" | "down") { + const viewport = getScrollAreaViewportElement(hiddenLayerScrollAreaRef.current); + if (!viewport) { + return; + } + + viewport.scrollBy({ + top: direction === "up" ? -HIDDEN_LAYER_SCROLL_STEP : HIDDEN_LAYER_SCROLL_STEP, + behavior: "smooth", + }); + } + async function archiveRenderImageUrl( rawUrl: string, label: string @@ -2528,7 +2618,7 @@ export function AgentPsdTemplateEditorDialog({ /> { event.preventDefault(); @@ -3549,11 +3639,36 @@ export function AgentPsdTemplateEditorDialog({ 临时删除只影响这次套版预览和导出,不会改动 PSD 模板本身。 -
- 已隐藏 {hiddenLayerIds.length} / {selectedTemplateLayers.length} +
+
+ + +
+
+ 已隐藏 {hiddenLayerIds.length} / {selectedTemplateLayers.length} +
- +
{orderedPreviewLayers.map((layer) => { const isHidden = hiddenLayerIdSet.has(layer.id); diff --git a/components/agents/agent-psd-template-export-preview.tsx b/components/agents/agent-psd-template-export-preview.tsx index 230ef9a18..1df0367d6 100644 --- a/components/agents/agent-psd-template-export-preview.tsx +++ b/components/agents/agent-psd-template-export-preview.tsx @@ -99,6 +99,7 @@ const DIMENSION_COLOR_OPTIONS = [ "#facc15", ]; const DIMENSION_TEXT_HALO_COLOR = "rgba(255,255,255,0.92)"; +const DIMENSION_TEXT_OUTLINE_COLOR = "rgba(15,18,24,0.28)"; const PREVIEW_DIMENSION_FONT_SIZE = 10.4; const PREVIEW_DIMENSION_LABEL_OFFSET = 13; const PREVIEW_DIMENSION_STROKE_WIDTH = 1.1; @@ -110,6 +111,7 @@ const PREVIEW_DIMENSION_TICK_LENGTH_ACTIVE = 6.5; const PREVIEW_DIMENSION_HANDLE_RADIUS = 3.2; const PREVIEW_DIMENSION_HANDLE_STROKE_WIDTH = 1.2; const PREVIEW_DIMENSION_TEXT_HALO_WIDTH = 2.2; +const PREVIEW_DIMENSION_TEXT_OUTLINE_WIDTH = 0.85; const SIZE_INFO_LINE_REGEX = /尺寸信息[::]\s*([^\n]+)/i; const NUMBER_REGEX = /\d+(?:\.\d+)?/g; const HEX_COLOR_REGEX = /^#(?:[0-9a-fA-F]{3}){1,2}$/; @@ -202,13 +204,25 @@ function getAnnotationColor(annotation: DimensionAnnotation) { function getAnnotationGlowFilter(annotation: DimensionAnnotation) { if (!annotation.glow) { - return; + return "none"; } const color = getAnnotationColor(annotation); return `drop-shadow(0 0 4px ${colorWithAlpha(color, 0.42)}) drop-shadow(0 0 10px ${colorWithAlpha(color, 0.3)})`; } +function getAnnotationTextOutlineColor(annotation: DimensionAnnotation) { + return annotation.glow + ? DIMENSION_TEXT_HALO_COLOR + : DIMENSION_TEXT_OUTLINE_COLOR; +} + +function getAnnotationTextOutlineWidth(annotation: DimensionAnnotation) { + return annotation.glow + ? PREVIEW_DIMENSION_TEXT_HALO_WIDTH + : PREVIEW_DIMENSION_TEXT_OUTLINE_WIDTH; +} + function normalizeDimensionInput(value: string) { return String(value || "") .trim() @@ -592,6 +606,10 @@ function drawAnnotationToCanvas(params: { const labelOffset = fontSize * 0.95; const centerX = (annotation.x1 + annotation.x2) / 2 + vector.nx * labelOffset; const centerY = (annotation.y1 + annotation.y2) / 2 + vector.ny * labelOffset; + const textOutlineColor = getAnnotationTextOutlineColor(annotation); + const textOutlineWidth = annotation.glow + ? Math.max(1.5, fontSize * 0.18) + : Math.max(0.85, fontSize * 0.08); context.save(); context.strokeStyle = annotationColor; @@ -626,8 +644,8 @@ function drawAnnotationToCanvas(params: { context.font = `500 ${fontSize}px "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif`; context.textAlign = "center"; context.textBaseline = "middle"; - context.lineWidth = Math.max(1.5, fontSize * 0.18); - context.strokeStyle = DIMENSION_TEXT_HALO_COLOR; + context.lineWidth = textOutlineWidth; + context.strokeStyle = textOutlineColor; context.strokeText(label, 0, 0); context.fillStyle = annotationColor; context.fillText(label, 0, 0); @@ -1104,7 +1122,7 @@ export function AgentPsdTemplateExportPreview({ ]); return ( -
+
{enableDimensionAnnotations ? (
+ + {enableDimensionAnnotations ? ( +
+ {selectedAnnotation ? ( +
+
+
+
+
+ 尺寸文字 +
+
+ {autoDimensionValues + ? "留空时会优先使用商品尺寸信息自动显示 CM;手动时只输入数值即可。" + : "当前没有拿到商品尺寸信息,请直接输入数值,系统会自动补 CM。"} +
+
+ {getDimensionLabel({ + annotation: selectedAnnotation, + annotations, + autoDimensionValues, + }) ? ( +
+ 建议值{" "} + {getDimensionLabel({ + annotation: selectedAnnotation, + annotations, + autoDimensionValues, + })} +
+ ) : null} +
+
+ setAnnotations((current) => current.map((annotation) => annotation.id === selectedAnnotation.id ? { ...annotation, - color: nextColor, + label: normalizeDimensionInput( + event.target.value + ), } : annotation ) - ); - }} - type="color" - value={getAnnotationColor(selectedAnnotation)} + ) + } + placeholder={LABEL_PLACEHOLDER} + value={normalizeDimensionInput(selectedAnnotation.label)} /> - + + {DIMENSION_UNIT} + +
+
+ +
+
+
+
+ 线条样式 +
+
+ 可为当前尺寸线设置颜色,并控制是否开启外发光。 +
+
+
+ 外发光 + + setAnnotations((current) => + current.map((annotation) => + annotation.id === selectedAnnotation.id + ? { ...annotation, glow: checked } + : annotation + ) + ) + } + /> +
+
+
+ {DIMENSION_COLOR_OPTIONS.map((color) => { + const normalizedColor = normalizeDimensionColor(color); + const isActive = + getAnnotationColor(selectedAnnotation) === + normalizedColor; + + return ( +
-
- ) : annotations.length > 0 ? ( -
- 当前已有尺寸线,正在为你自动保留最近一条为选中状态。 - 如果样式面板没刷新,点一下对应尺寸线即可继续编辑。 -
- ) : ( -
- 选中一条尺寸线后,可以在这里输入真实尺寸文案。 -
- )} -
- ) : null} + ) : annotations.length > 0 ? ( +
+ 当前已有尺寸线,正在为你自动保留最近一条为选中状态。 + 如果样式面板没刷新,点一下对应尺寸线即可继续编辑。 +
+ ) : ( +
+ 选中一条尺寸线后,可以在这里输入真实尺寸文案。 +
+ )} +
+ ) : null} +