"use client"; import { Download, Loader2, Ruler, Trash2 } from "lucide-react"; import Image from "next/image"; import { type PointerEvent as ReactPointerEvent, useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { buildImageProxyUrl } from "@/lib/image-delivery"; import { cn } from "@/lib/utils"; type AgentPsdTemplateExportPreviewProps = { imageUrl: string; imageAlt?: string; enableDimensionAnnotations?: boolean; downloadName?: string; sizeInfoText?: string | null; }; type ImageSize = { width: number; height: number; }; type PreviewRect = { left: number; top: number; width: number; height: number; }; type DimensionAnnotation = { id: string; x1: number; y1: number; x2: number; y2: number; label: string; }; type Point = { x: number; y: number; }; type CoordinateSpace = { containerLeft: number; containerTop: number; previewRect: PreviewRect; imageSize: ImageSize; }; type DragState = | { kind: "drawing"; pointerId: number; annotationId: string; coordinateSpace: CoordinateSpace; } | { kind: "move-start" | "move-end"; pointerId: number; annotationId: string; coordinateSpace: CoordinateSpace; } | { kind: "move-line"; pointerId: number; annotationId: string; coordinateSpace: CoordinateSpace; startClientX: number; startClientY: number; startAnnotation: DimensionAnnotation; }; const PREVIEW_MIN_HEIGHT = 340; const DIMENSION_UNIT = "CM"; const LABEL_PLACEHOLDER = "例如:120"; const IMAGE_EXTENSION_REGEX = /\.(png|jpg|jpeg|webp)$/i; const DIMENSION_SUFFIX_REGEX = /-尺寸标注(?=\.)/; const DIMENSION_LINE_COLOR = "#666b73"; const DIMENSION_TEXT_COLOR = "#2a2f35"; const DIMENSION_TEXT_HALO_COLOR = "rgba(255,255,255,0.92)"; const PREVIEW_DIMENSION_FONT_SIZE = 10.4; const PREVIEW_DIMENSION_LABEL_OFFSET = 13; const PREVIEW_DIMENSION_STROKE_WIDTH = 1.1; const PREVIEW_DIMENSION_STROKE_WIDTH_ACTIVE = 1.5; const PREVIEW_DIMENSION_CAP_STROKE_WIDTH = 1.55; const PREVIEW_DIMENSION_CAP_STROKE_WIDTH_ACTIVE = 1.9; const PREVIEW_DIMENSION_TICK_LENGTH = 5.2; 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 SIZE_INFO_LINE_REGEX = /尺寸信息[::]\s*([^\n]+)/i; const NUMBER_REGEX = /\d+(?:\.\d+)?/g; type AutoDimensionValues = { widthCm?: number; depthCm?: number; heightCm?: number; rawValues: number[]; }; function clamp(value: number, min: number, max: number) { return Math.max(min, Math.min(max, value)); } function createAnnotationId() { if ( typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ) { return crypto.randomUUID(); } return `psd-dimension-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; } function getContainPreviewRect( container: ImageSize, image: ImageSize ): PreviewRect { const safeContainerWidth = Math.max(1, container.width); const safeContainerHeight = Math.max(1, container.height); const safeImageWidth = Math.max(1, image.width); const safeImageHeight = Math.max(1, image.height); const scale = Math.min( safeContainerWidth / safeImageWidth, safeContainerHeight / safeImageHeight ); const width = safeImageWidth * scale; const height = safeImageHeight * scale; return { left: (safeContainerWidth - width) / 2, top: (safeContainerHeight - height) / 2, width, height, }; } function getDimensionLength(annotation: DimensionAnnotation) { return Math.hypot( annotation.x2 - annotation.x1, annotation.y2 - annotation.y1 ); } function formatDimensionValue(value: number) { return Number.isInteger(value) ? `${value}${DIMENSION_UNIT}` : `${value.toFixed(1)}${DIMENSION_UNIT}`; } function normalizeDimensionInput(value: string) { return String(value || "") .trim() .replace(/厘米|公分/gi, "") .replace(/cm/gi, "") .replace(/\s+/g, "") .replace(/[^0-9.]/g, ""); } function extractAutoDimensionValues( sizeInfoText: string | null | undefined ): AutoDimensionValues | null { const normalizedText = String(sizeInfoText || "").trim(); if (!normalizedText) { return null; } const matchedLine = normalizedText.match(SIZE_INFO_LINE_REGEX)?.[1] || normalizedText; const values = Array.from(matchedLine.matchAll(NUMBER_REGEX)) .map((item) => Number(item[0])) .filter((item) => Number.isFinite(item) && item > 0) .slice(0, 3); if (values.length === 0) { return null; } if (values.length === 1) { return { heightCm: values[0], rawValues: values, widthCm: values[0], }; } if (values.length === 2) { const [larger, smaller] = [...values].sort((left, right) => right - left); return { heightCm: larger, rawValues: values, widthCm: smaller, }; } const sortedValues = [...values].sort((left, right) => right - left); const [heightCm, widthCm, depthCm] = sortedValues; return { depthCm, heightCm, rawValues: values, widthCm, }; } function getAnnotationOrientation(annotation: DimensionAnnotation) { const deltaX = Math.abs(annotation.x2 - annotation.x1); const deltaY = Math.abs(annotation.y2 - annotation.y1); if (deltaY > deltaX * 1.25) { return "vertical"; } if (deltaX > deltaY * 1.25) { return "horizontal"; } return "diagonal"; } function getAutoDimensionLabel(params: { annotation: DimensionAnnotation; annotations: DimensionAnnotation[]; autoDimensionValues?: AutoDimensionValues | null; }) { const { annotation, annotations, autoDimensionValues } = params; if (!autoDimensionValues) { return ""; } if (autoDimensionValues.rawValues.length === 1) { return formatDimensionValue(autoDimensionValues.rawValues[0]); } const orientation = getAnnotationOrientation(annotation); if (orientation === "vertical" && autoDimensionValues.heightCm) { return formatDimensionValue(autoDimensionValues.heightCm); } if (orientation === "diagonal" && autoDimensionValues.depthCm) { return formatDimensionValue(autoDimensionValues.depthCm); } if (orientation === "horizontal") { const horizontalAnnotations = annotations .filter((item) => getAnnotationOrientation(item) === "horizontal") .sort( (left, right) => getDimensionLength(right) - getDimensionLength(left) ); const currentIndex = horizontalAnnotations.findIndex( (item) => item.id === annotation.id ); if (currentIndex <= 0 && autoDimensionValues.widthCm) { return formatDimensionValue(autoDimensionValues.widthCm); } if (autoDimensionValues.depthCm) { return formatDimensionValue(autoDimensionValues.depthCm); } if (autoDimensionValues.widthCm) { return formatDimensionValue(autoDimensionValues.widthCm); } } if (autoDimensionValues.widthCm) { return formatDimensionValue(autoDimensionValues.widthCm); } if (autoDimensionValues.heightCm) { return formatDimensionValue(autoDimensionValues.heightCm); } return ""; } function getDimensionLabel(params: { annotation: DimensionAnnotation; annotations: DimensionAnnotation[]; autoDimensionValues?: AutoDimensionValues | null; }) { const { annotation, annotations, autoDimensionValues } = params; const customLabel = normalizeDimensionInput(annotation.label); if (customLabel) { return `${customLabel}${DIMENSION_UNIT}`; } return getAutoDimensionLabel({ annotation, annotations, autoDimensionValues, }); } function mapImagePointToPreviewPoint( point: Point, previewRect: PreviewRect, imageSize: ImageSize ) { return { x: previewRect.left + (point.x / Math.max(1, imageSize.width)) * previewRect.width, y: previewRect.top + (point.y / Math.max(1, imageSize.height)) * previewRect.height, }; } function mapClientPointToImagePoint( clientX: number, clientY: number, coordinateSpace: CoordinateSpace, options?: { requireInside?: boolean } ) { const localX = clientX - coordinateSpace.containerLeft; const localY = clientY - coordinateSpace.containerTop; const { left, top, width, height } = coordinateSpace.previewRect; const isInside = localX >= left && localX <= left + width && localY >= top && localY <= top + height; if (options?.requireInside && !isInside) { return null; } return { x: clamp( ((localX - left) / Math.max(1, width)) * coordinateSpace.imageSize.width, 0, coordinateSpace.imageSize.width ), y: clamp( ((localY - top) / Math.max(1, height)) * coordinateSpace.imageSize.height, 0, coordinateSpace.imageSize.height ), }; } function getAnnotationVector(x1: number, y1: number, x2: number, y2: number) { const dx = x2 - x1; const dy = y2 - y1; const length = Math.max(1, Math.hypot(dx, dy)); return { dx, dy, length, ux: dx / length, uy: dy / length, nx: -dy / length, ny: dx / length, }; } function getPreviewAnnotationGeometry(params: { annotation: DimensionAnnotation; annotations: DimensionAnnotation[]; previewRect: PreviewRect; imageSize: ImageSize; autoDimensionValues?: AutoDimensionValues | null; }) { const { annotation, annotations, previewRect, imageSize, autoDimensionValues, } = params; const start = mapImagePointToPreviewPoint( { x: annotation.x1, y: annotation.y1 }, previewRect, imageSize ); const end = mapImagePointToPreviewPoint( { x: annotation.x2, y: annotation.y2 }, previewRect, imageSize ); const vector = getAnnotationVector(start.x, start.y, end.x, end.y); const label = getDimensionLabel({ annotation, annotations, autoDimensionValues, }); const labelOffset = PREVIEW_DIMENSION_LABEL_OFFSET; const rawAngleRadians = Math.atan2(vector.dy, vector.dx); const angleRadians = rawAngleRadians > Math.PI / 2 ? rawAngleRadians - Math.PI : rawAngleRadians < -Math.PI / 2 ? rawAngleRadians + Math.PI : rawAngleRadians; return { start, end, vector, label, angleDegrees: (angleRadians * 180) / Math.PI, labelCenterX: (start.x + end.x) / 2 + vector.nx * labelOffset, labelCenterY: (start.y + end.y) / 2 + vector.ny * labelOffset, }; } function buildDownloadName(downloadName: string | undefined, imageUrl: string) { const explicitName = String(downloadName || "").trim(); if (explicitName) { return explicitName; } try { const pathname = new URL(imageUrl).pathname; const rawName = pathname.split("/").pop() || ""; const decoded = decodeURIComponent(rawName).trim(); if (!decoded) { return `psd-template-${Date.now()}.png`; } if (IMAGE_EXTENSION_REGEX.test(decoded)) { return decoded.replace(IMAGE_EXTENSION_REGEX, "-尺寸标注.png"); } return `${decoded}-尺寸标注.png`; } catch { return `psd-template-${Date.now()}.png`; } } async function loadImageFromCandidates(candidates: string[]) { const uniqueCandidates = Array.from( new Set(candidates.map((item) => String(item || "").trim()).filter(Boolean)) ); for (const candidate of uniqueCandidates) { try { const response = await fetch(candidate); if (!response.ok) { continue; } const blob = await response.blob(); const objectUrl = URL.createObjectURL(blob); const image = await new Promise((resolve, reject) => { const nextImage = new window.Image(); nextImage.onload = () => resolve(nextImage); nextImage.onerror = () => reject(new Error("图片加载失败")); nextImage.src = objectUrl; }); return { image, dispose: () => URL.revokeObjectURL(objectUrl), }; } catch { // continue to next candidate } } throw new Error("结果图加载失败"); } function downloadBlob(blob: Blob, fileName: string) { const objectUrl = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = objectUrl; link.download = fileName; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(objectUrl); } function drawDimensionCap(params: { context: CanvasRenderingContext2D; point: Point; vector: ReturnType; size: number; }) { const { context, point, vector, size } = params; context.beginPath(); context.moveTo(point.x + vector.nx * size, point.y + vector.ny * size); context.lineTo(point.x - vector.nx * size, point.y - vector.ny * size); context.stroke(); } function drawAnnotationToCanvas(params: { context: CanvasRenderingContext2D; annotation: DimensionAnnotation; imageSize: ImageSize; annotations: DimensionAnnotation[]; autoDimensionValues?: AutoDimensionValues | null; }) { const { context, annotation, imageSize, annotations, autoDimensionValues } = params; const label = getDimensionLabel({ annotation, annotations, autoDimensionValues, }); const vector = getAnnotationVector( annotation.x1, annotation.y1, annotation.x2, annotation.y2 ); const rawAngleRadians = Math.atan2(vector.dy, vector.dx); const angleRadians = rawAngleRadians > Math.PI / 2 ? rawAngleRadians - Math.PI : rawAngleRadians < -Math.PI / 2 ? rawAngleRadians + Math.PI : rawAngleRadians; const strokeWidth = clamp( Math.min(imageSize.width, imageSize.height) * 0.001_35, 1, 2.4 ); const capLength = strokeWidth * 5.3; const fontSize = clamp( Math.round(Math.min(imageSize.width, imageSize.height) * 0.0176), 11, 27 ); 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; context.save(); context.strokeStyle = DIMENSION_LINE_COLOR; context.lineWidth = strokeWidth; context.lineCap = "round"; context.lineJoin = "round"; context.beginPath(); context.moveTo(annotation.x1, annotation.y1); context.lineTo(annotation.x2, annotation.y2); context.stroke(); drawDimensionCap({ context, point: { x: annotation.x1, y: annotation.y1 }, size: capLength, vector, }); drawDimensionCap({ context, point: { x: annotation.x2, y: annotation.y2 }, size: capLength, vector, }); context.translate(centerX, centerY); context.rotate(angleRadians); 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.strokeText(label, 0, 0); context.fillStyle = DIMENSION_TEXT_COLOR; context.fillText(label, 0, 0); context.restore(); } export function AgentPsdTemplateExportPreview({ imageUrl, imageAlt = "PSD 模板导出结果", enableDimensionAnnotations = false, downloadName, sizeInfoText, }: AgentPsdTemplateExportPreviewProps) { const previewViewportRef = useRef(null); const dragStateRef = useRef(null); const cleanupRef = useRef<(() => void) | null>(null); const [viewportSize, setViewportSize] = useState({ width: 0, height: 0, }); const [imageSize, setImageSize] = useState(null); const [annotations, setAnnotations] = useState([]); const [selectedAnnotationId, setSelectedAnnotationId] = useState(""); const [isDrawMode, setIsDrawMode] = useState(false); const [isDownloading, setIsDownloading] = useState(false); const previewImageUrl = useMemo( () => buildImageProxyUrl(imageUrl), [imageUrl] ); const autoDimensionValues = useMemo( () => extractAutoDimensionValues(sizeInfoText), [sizeInfoText] ); const selectedAnnotation = useMemo( () => annotations.find( (annotation) => annotation.id === selectedAnnotationId ) || null, [annotations, selectedAnnotationId] ); const previewRect = useMemo(() => { if (!imageSize || viewportSize.width <= 0 || viewportSize.height <= 0) { return null; } return getContainPreviewRect(viewportSize, imageSize); }, [imageSize, viewportSize]); const clearPointerSession = useCallback(() => { cleanupRef.current?.(); cleanupRef.current = null; dragStateRef.current = null; }, []); useEffect(() => { const element = previewViewportRef.current; if (!element) { return; } const updateViewportSize = () => { setViewportSize({ width: element.clientWidth, height: element.clientHeight, }); }; updateViewportSize(); if (typeof ResizeObserver !== "undefined") { const observer = new ResizeObserver(updateViewportSize); observer.observe(element); return () => observer.disconnect(); } window.addEventListener("resize", updateViewportSize); return () => window.removeEventListener("resize", updateViewportSize); }, []); useEffect(() => clearPointerSession, [clearPointerSession]); const getCoordinateSpace = useCallback(() => { const element = previewViewportRef.current; if (!element || !previewRect || !imageSize) { return null; } const bounds = element.getBoundingClientRect(); return { containerLeft: bounds.left, containerTop: bounds.top, previewRect, imageSize, } satisfies CoordinateSpace; }, [imageSize, previewRect]); const attachPointerSession = useCallback( ( state: DragState, event: ReactPointerEvent, onMove: (dragState: DragState, moveEvent: PointerEvent) => void, onEnd?: (dragState: DragState) => void ) => { clearPointerSession(); dragStateRef.current = state; try { event.currentTarget.setPointerCapture?.(event.pointerId); } catch { // ignore capture failures in unsupported environments } const handlePointerMove = (moveEvent: PointerEvent) => { const activeState = dragStateRef.current; if (!activeState || activeState.pointerId !== moveEvent.pointerId) { return; } onMove(activeState, moveEvent); }; const handlePointerUp = (upEvent: PointerEvent) => { const activeState = dragStateRef.current; if (!activeState || activeState.pointerId !== upEvent.pointerId) { return; } onEnd?.(activeState); clearPointerSession(); }; cleanupRef.current = () => { window.removeEventListener("pointermove", handlePointerMove); window.removeEventListener("pointerup", handlePointerUp); window.removeEventListener("pointercancel", handlePointerUp); }; window.addEventListener("pointermove", handlePointerMove); window.addEventListener("pointerup", handlePointerUp); window.addEventListener("pointercancel", handlePointerUp); }, [clearPointerSession] ); const handleOverlayPointerDown = useCallback( (event: ReactPointerEvent) => { if (!enableDimensionAnnotations) { return; } if (!isDrawMode) { setSelectedAnnotationId(""); return; } const coordinateSpace = getCoordinateSpace(); if (!coordinateSpace) { return; } const startPoint = mapClientPointToImagePoint( event.clientX, event.clientY, coordinateSpace, { requireInside: true } ); if (!startPoint) { return; } event.preventDefault(); event.stopPropagation(); const annotationId = createAnnotationId(); setAnnotations((current) => [ ...current, { id: annotationId, x1: startPoint.x, y1: startPoint.y, x2: startPoint.x, y2: startPoint.y, label: "", }, ]); setSelectedAnnotationId(annotationId); attachPointerSession( { kind: "drawing", pointerId: event.pointerId, annotationId, coordinateSpace, }, event, (dragState, moveEvent) => { const nextPoint = mapClientPointToImagePoint( moveEvent.clientX, moveEvent.clientY, dragState.coordinateSpace ); if (!nextPoint) { return; } setAnnotations((current) => current.map((annotation) => annotation.id === dragState.annotationId ? { ...annotation, x2: nextPoint.x, y2: nextPoint.y, } : annotation ) ); }, (dragState) => { setAnnotations((current) => current.filter((annotation) => { if (annotation.id !== dragState.annotationId) { return true; } return getDimensionLength(annotation) >= 12; }) ); setIsDrawMode(false); } ); }, [ attachPointerSession, enableDimensionAnnotations, getCoordinateSpace, isDrawMode, ] ); const handleAnnotationHandlePointerDown = useCallback( ( annotationId: string, handle: "move-start" | "move-end", event: ReactPointerEvent ) => { const coordinateSpace = getCoordinateSpace(); if (!coordinateSpace) { return; } event.preventDefault(); event.stopPropagation(); setSelectedAnnotationId(annotationId); attachPointerSession( { kind: handle, pointerId: event.pointerId, annotationId, coordinateSpace, }, event, (dragState, moveEvent) => { const nextPoint = mapClientPointToImagePoint( moveEvent.clientX, moveEvent.clientY, dragState.coordinateSpace ); if (!nextPoint) { return; } setAnnotations((current) => current.map((annotation) => { if (annotation.id !== dragState.annotationId) { return annotation; } return dragState.kind === "move-start" ? { ...annotation, x1: nextPoint.x, y1: nextPoint.y } : { ...annotation, x2: nextPoint.x, y2: nextPoint.y }; }) ); } ); }, [attachPointerSession, getCoordinateSpace] ); const handleAnnotationBodyPointerDown = useCallback( ( annotation: DimensionAnnotation, event: ReactPointerEvent ) => { const coordinateSpace = getCoordinateSpace(); if (!coordinateSpace) { return; } event.preventDefault(); event.stopPropagation(); setSelectedAnnotationId(annotation.id); attachPointerSession( { kind: "move-line", pointerId: event.pointerId, annotationId: annotation.id, coordinateSpace, startClientX: event.clientX, startClientY: event.clientY, startAnnotation: annotation, }, event, (dragState, moveEvent) => { if (dragState.kind !== "move-line") { return; } const deltaImageX = ((moveEvent.clientX - dragState.startClientX) / Math.max(1, dragState.coordinateSpace.previewRect.width)) * dragState.coordinateSpace.imageSize.width; const deltaImageY = ((moveEvent.clientY - dragState.startClientY) / Math.max(1, dragState.coordinateSpace.previewRect.height)) * dragState.coordinateSpace.imageSize.height; const minDeltaX = -Math.min( dragState.startAnnotation.x1, dragState.startAnnotation.x2 ); const maxDeltaX = dragState.coordinateSpace.imageSize.width - Math.max( dragState.startAnnotation.x1, dragState.startAnnotation.x2 ); const minDeltaY = -Math.min( dragState.startAnnotation.y1, dragState.startAnnotation.y2 ); const maxDeltaY = dragState.coordinateSpace.imageSize.height - Math.max( dragState.startAnnotation.y1, dragState.startAnnotation.y2 ); const safeDeltaX = clamp(deltaImageX, minDeltaX, maxDeltaX); const safeDeltaY = clamp(deltaImageY, minDeltaY, maxDeltaY); setAnnotations((current) => current.map((item) => item.id === dragState.annotationId ? { ...item, x1: dragState.startAnnotation.x1 + safeDeltaX, y1: dragState.startAnnotation.y1 + safeDeltaY, x2: dragState.startAnnotation.x2 + safeDeltaX, y2: dragState.startAnnotation.y2 + safeDeltaY, } : item ) ); } ); }, [attachPointerSession, getCoordinateSpace] ); const handleDeleteSelected = useCallback(() => { if (!selectedAnnotation) { return; } setAnnotations((current) => current.filter((annotation) => annotation.id !== selectedAnnotation.id) ); setSelectedAnnotationId(""); }, [selectedAnnotation]); const handleDownload = useCallback(async () => { const fileName = buildDownloadName(downloadName, imageUrl); const shouldCompose = enableDimensionAnnotations && annotations.length > 0; try { setIsDownloading(true); if (!shouldCompose) { const response = await fetch(buildImageProxyUrl(imageUrl)); if (!response.ok) { throw new Error(`下载失败(${response.status})`); } const blob = await response.blob(); downloadBlob(blob, fileName.replace(DIMENSION_SUFFIX_REGEX, "")); return; } const { image, dispose } = await loadImageFromCandidates([ buildImageProxyUrl(imageUrl), imageUrl, ]); try { const canvas = document.createElement("canvas"); canvas.width = Math.max(1, image.naturalWidth || image.width); canvas.height = Math.max(1, image.naturalHeight || image.height); const context = canvas.getContext("2d"); if (!context) { throw new Error("无法创建导出画布"); } context.drawImage(image, 0, 0, canvas.width, canvas.height); for (const annotation of annotations) { drawAnnotationToCanvas({ annotation, annotations, autoDimensionValues, context, imageSize: { width: canvas.width, height: canvas.height, }, }); } const blob = await new Promise((resolve, reject) => { canvas.toBlob( (nextBlob) => { if (nextBlob) { resolve(nextBlob); return; } reject(new Error("导出结果生成失败")); }, "image/png", 1 ); }); downloadBlob(blob, fileName); } finally { dispose(); } } catch (error) { toast.error(error instanceof Error ? error.message : "下载结果图失败"); } finally { setIsDownloading(false); } }, [ annotations, autoDimensionValues, downloadName, enableDimensionAnnotations, imageUrl, ]); return (
{enableDimensionAnnotations ? (
{isDrawMode ? "在预览图上按住拖动即可拉出一条尺寸线" : "支持拖动整条线、拖动两端端点,并可手动改尺寸文字"}
) : null}
{previewRect && imageSize ? ( <>
{imageAlt}
{annotations.map((annotation) => { const isSelected = annotation.id === selectedAnnotationId; const geometry = getPreviewAnnotationGeometry({ annotation, annotations, autoDimensionValues, imageSize, previewRect, }); const strokeWidth = isSelected ? PREVIEW_DIMENSION_STROKE_WIDTH_ACTIVE : PREVIEW_DIMENSION_STROKE_WIDTH; const tickLength = isSelected ? PREVIEW_DIMENSION_TICK_LENGTH_ACTIVE : PREVIEW_DIMENSION_TICK_LENGTH; return ( handleAnnotationBodyPointerDown(annotation, event) } stroke="transparent" strokeWidth={24} x1={geometry.start.x} x2={geometry.end.x} y1={geometry.start.y} y2={geometry.end.y} /> handleAnnotationBodyPointerDown(annotation, event) } > {geometry.label} {isSelected ? ( <> handleAnnotationHandlePointerDown( annotation.id, "move-start", event ) } r={PREVIEW_DIMENSION_HANDLE_RADIUS} stroke={DIMENSION_LINE_COLOR} strokeWidth={PREVIEW_DIMENSION_HANDLE_STROKE_WIDTH} /> handleAnnotationHandlePointerDown( annotation.id, "move-end", event ) } r={PREVIEW_DIMENSION_HANDLE_RADIUS} stroke={DIMENSION_LINE_COLOR} strokeWidth={PREVIEW_DIMENSION_HANDLE_STROKE_WIDTH} /> ) : null} ); })} {enableDimensionAnnotations && isDrawMode ? (
按住并拖动,拉出尺寸线
) : null} ) : ( {imageAlt} { const nextWidth = Math.max( 1, event.currentTarget.naturalWidth || 0 ); const nextHeight = Math.max( 1, event.currentTarget.naturalHeight || 0 ); if (nextWidth > 0 && nextHeight > 0) { setImageSize({ width: nextWidth, height: nextHeight }); } }} sizes="100vw" src={previewImageUrl} style={{ objectFit: "contain" }} unoptimized /> )}
{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, label: normalizeDimensionInput( event.target.value ), } : annotation ) ) } placeholder={LABEL_PLACEHOLDER} value={normalizeDimensionInput(selectedAnnotation.label)} /> {DIMENSION_UNIT}
) : (
选中一条尺寸线后,可以在这里输入真实尺寸文案。
)}
) : null}
); }