From 4e31850dde650fc7dd28064e82880d3049b9ff33 Mon Sep 17 00:00:00 2001 From: iqudoo Date: Sat, 13 Jun 2026 02:40:19 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=9A=E5=A4=9A=E6=8C=87?= =?UTF-8?q?=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/CanvasLabelImage.tsx | 10 +-- src/components/LabelDesigner.tsx | 97 ++++++++++++++++++++++++++--- 2 files changed, 94 insertions(+), 13 deletions(-) diff --git a/src/components/CanvasLabelImage.tsx b/src/components/CanvasLabelImage.tsx index 4867160..872b7e4 100644 --- a/src/components/CanvasLabelImage.tsx +++ b/src/components/CanvasLabelImage.tsx @@ -14,6 +14,8 @@ interface CanvasLabelImageProps { variableDefaults?: Record | null; /** design=设计预览(空变量显示占位);output=排版/打印(空值隐藏图形元素) */ mode?: ContentResolveMode; + /** 渲染倍率,设计画布应传 MM_TO_PX * zoom 与显示尺寸 1:1 对齐 */ + renderScale?: number; } export const CanvasLabelImage: React.FC = ({ @@ -26,6 +28,7 @@ export const CanvasLabelImage: React.FC = ({ transparentBackground = false, variableDefaults = null, mode = 'design' as ContentResolveMode, + renderScale = 16, }) => { const [dataUrl, setDataUrl] = useState(''); const [initialLoading, setInitialLoading] = useState(true); @@ -46,6 +49,7 @@ export const CanvasLabelImage: React.FC = ({ transparentBackground, variableDefaults, mode, + scale: renderScale, }) .then((url) => { if (active) { @@ -61,7 +65,7 @@ export const CanvasLabelImage: React.FC = ({ return () => { active = false; }; - }, [widthMm, heightMm, elementsKey, rowDataKey, rowIndex, showBorder, transparentBackground, mode, elements, variableDefaults]); + }, [widthMm, heightMm, elementsKey, rowDataKey, rowIndex, showBorder, transparentBackground, mode, renderScale, elements, variableDefaults]); if (initialLoading && !dataUrl) { return ( @@ -78,10 +82,8 @@ export const CanvasLabelImage: React.FC = ({ Label Preview diff --git a/src/components/LabelDesigner.tsx b/src/components/LabelDesigner.tsx index df12294..d1ed605 100644 --- a/src/components/LabelDesigner.tsx +++ b/src/components/LabelDesigner.tsx @@ -1,6 +1,6 @@ import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react'; import { LabelTemplate, TemplateElement } from '../types'; -import { mmToPx, shouldRenderElement } from '../utils'; +import { mmToPx, MM_TO_PX, shouldRenderElement } from '../utils'; import { useIsNarrowScreen } from '../hooks/useIsMobile'; import { CanvasLabelImage } from './CanvasLabelImage'; import { @@ -181,6 +181,8 @@ export const LabelDesigner: React.FC = ({ const ignoreCanvasClickRef = useRef(false); const dragMovedRef = useRef(false); + const multiTouchActiveRef = useRef(false); + const touchPointerMapRef = useRef>(new Map()); const marqueeRef = useRef(marqueeState); marqueeRef.current = marqueeState; const selectedIdsRef = useRef(selectedElementIds); @@ -395,6 +397,62 @@ export const LabelDesigner: React.FC = ({ deleteSelectedElements, ]); + const cancelActiveInteraction = useCallback(() => { + if (dragRafRef.current !== null) { + cancelAnimationFrame(dragRafRef.current); + dragRafRef.current = null; + } + pendingDragElementsRef.current = null; + localElementsRef.current = null; + setLocalElements(null); + setFrozenCanvasElements(null); + setDragState(null); + dragMovedRef.current = false; + setMarqueeState(null); + }, []); + + const shouldIgnoreTouchPointer = (e: { pointerType: string; isPrimary: boolean }) => { + if (multiTouchActiveRef.current) return true; + if (e.pointerType === 'touch' && !e.isPrimary) return true; + return false; + }; + + useEffect(() => { + if (!isMobileLayout && !isNarrow) return; + + const onPointerDown = (e: PointerEvent) => { + if (e.pointerType !== 'touch') return; + const target = e.target as Node | null; + if (!canvasAreaRef.current?.contains(target)) return; + + touchPointerMapRef.current.set(e.pointerId, { x: e.clientX, y: e.clientY }); + + if (touchPointerMapRef.current.size > 1) { + multiTouchActiveRef.current = true; + cancelActiveInteraction(); + } + }; + + const onPointerUp = (e: PointerEvent) => { + if (e.pointerType !== 'touch') return; + touchPointerMapRef.current.delete(e.pointerId); + if (touchPointerMapRef.current.size <= 1) { + multiTouchActiveRef.current = false; + } + }; + + window.addEventListener('pointerdown', onPointerDown, true); + window.addEventListener('pointerup', onPointerUp, true); + window.addEventListener('pointercancel', onPointerUp, true); + return () => { + window.removeEventListener('pointerdown', onPointerDown, true); + window.removeEventListener('pointerup', onPointerUp, true); + window.removeEventListener('pointercancel', onPointerUp, true); + touchPointerMapRef.current.clear(); + multiTouchActiveRef.current = false; + }; + }, [isMobileLayout, isNarrow, cancelActiveInteraction]); + const applyModerateZoom = useCallback(() => { const area = canvasAreaRef.current; if (!area) return; @@ -434,6 +492,7 @@ export const LabelDesigner: React.FC = ({ if (!marqueeActive) return; const handlePointerMove = (e: PointerEvent) => { + if (multiTouchActiveRef.current) return; const canvas = canvasRef.current; if (!canvas) return; const rect = canvas.getBoundingClientRect(); @@ -443,6 +502,10 @@ export const LabelDesigner: React.FC = ({ }; const handlePointerUp = () => { + if (multiTouchActiveRef.current) { + setMarqueeState(null); + return; + } const state = marqueeRef.current; setMarqueeState(null); @@ -502,12 +565,17 @@ export const LabelDesigner: React.FC = ({ dragMovedRef.current = false; const handlePointerMove = (e: PointerEvent) => { + if (multiTouchActiveRef.current) return; + const deltaX = e.clientX - dragState.startX; const deltaY = e.clientY - dragState.startY; - if ( - dragState.type === 'drag' && - (Math.abs(deltaX) > DRAG_START_THRESHOLD_PX || Math.abs(deltaY) > DRAG_START_THRESHOLD_PX) - ) { + const pastDragThreshold = + Math.abs(deltaX) > DRAG_START_THRESHOLD_PX || Math.abs(deltaY) > DRAG_START_THRESHOLD_PX; + + if (dragState.type === 'drag' && !pastDragThreshold) { + return; + } + if (dragState.type === 'drag' && pastDragThreshold) { dragMovedRef.current = true; } const MM_PER_PX = 1 / (3.78 * zoom); @@ -584,6 +652,10 @@ export const LabelDesigner: React.FC = ({ }; const handlePointerUp = () => { + if (multiTouchActiveRef.current) { + cancelActiveInteraction(); + return; + } if (dragRafRef.current !== null) { cancelAnimationFrame(dragRafRef.current); dragRafRef.current = null; @@ -592,11 +664,11 @@ export const LabelDesigner: React.FC = ({ localElementsRef.current = pendingDragElementsRef.current; pendingDragElementsRef.current = null; } - if (localElementsRef.current) { + if (localElementsRef.current && dragMovedRef.current) { onChange({ ...template, elements: localElementsRef.current }); - localElementsRef.current = null; - setLocalElements(null); } + localElementsRef.current = null; + setLocalElements(null); setFrozenCanvasElements(null); setDragState(null); }; @@ -609,7 +681,7 @@ export const LabelDesigner: React.FC = ({ window.removeEventListener('pointerup', handlePointerUp); window.removeEventListener('pointercancel', handlePointerUp); }; - }, [dragState, template, zoom, onChange]); + }, [dragState, template, zoom, onChange, cancelActiveInteraction]); const buildGroupStartPositions = (element: TemplateElement) => { const moveIds = @@ -628,6 +700,7 @@ export const LabelDesigner: React.FC = ({ const startMarquee = (e: React.PointerEvent) => { if (activeTool !== 'move' || e.button !== 0) return; + if (shouldIgnoreTouchPointer(e)) return; const canvas = canvasRef.current; if (!canvas) return; @@ -653,6 +726,7 @@ export const LabelDesigner: React.FC = ({ const handleWorkspacePointerDown = (e: React.PointerEvent) => { if (activeTool !== 'move' || e.button !== 0) return; + if (shouldIgnoreTouchPointer(e)) return; const target = e.target as HTMLElement; if (target.closest('[data-label-element]') || target.closest('.ps-handle')) return; @@ -665,6 +739,7 @@ export const LabelDesigner: React.FC = ({ actionType: 'drag' | 'resize' ) => { if (e.button !== 0) return; + if (shouldIgnoreTouchPointer(e)) return; e.stopPropagation(); if (e.shiftKey || e.ctrlKey || e.metaKey) { @@ -698,6 +773,7 @@ export const LabelDesigner: React.FC = ({ element: TemplateElement, actionType: 'drag' | 'resize' ) => { + if (shouldIgnoreTouchPointer(e)) return; e.preventDefault(); if (!selectedElementIds.includes(element.id)) { onSelectElements([element.id]); @@ -724,6 +800,7 @@ export const LabelDesigner: React.FC = ({ element: TemplateElement, corner: ResizeCorner ) => { + if (shouldIgnoreTouchPointer(e)) return; e.stopPropagation(); e.preventDefault(); onSelectElements([element.id]); @@ -778,6 +855,7 @@ export const LabelDesigner: React.FC = ({ frozenCanvasElements ?? template.elements ); const overlayElements = filterVisibleElements(localElements ?? template.elements); + const canvasRenderScale = Math.max(1, Math.round(MM_TO_PX * zoom * 100) / 100); const tools: { id: ActiveTool; icon: React.ReactNode; label: string; shortcut?: string }[] = [ { id: 'move', icon: , label: '移动工具', shortcut: 'V' }, @@ -909,6 +987,7 @@ export const LabelDesigner: React.FC = ({ rowIndex={0} showBorder={false} variableDefaults={previewDefaults} + renderScale={canvasRenderScale} />