优化:多指交互

This commit is contained in:
iqudoo
2026-06-13 02:40:19 +08:00
parent 932ea4cc39
commit 4e31850dde
2 changed files with 94 additions and 13 deletions

View File

@@ -14,6 +14,8 @@ interface CanvasLabelImageProps {
variableDefaults?: Record<string, string> | null; variableDefaults?: Record<string, string> | null;
/** design=设计预览空变量显示占位output=排版/打印(空值隐藏图形元素) */ /** design=设计预览空变量显示占位output=排版/打印(空值隐藏图形元素) */
mode?: ContentResolveMode; mode?: ContentResolveMode;
/** 渲染倍率,设计画布应传 MM_TO_PX * zoom 与显示尺寸 1:1 对齐 */
renderScale?: number;
} }
export const CanvasLabelImage: React.FC<CanvasLabelImageProps> = ({ export const CanvasLabelImage: React.FC<CanvasLabelImageProps> = ({
@@ -26,6 +28,7 @@ export const CanvasLabelImage: React.FC<CanvasLabelImageProps> = ({
transparentBackground = false, transparentBackground = false,
variableDefaults = null, variableDefaults = null,
mode = 'design' as ContentResolveMode, mode = 'design' as ContentResolveMode,
renderScale = 16,
}) => { }) => {
const [dataUrl, setDataUrl] = useState<string>(''); const [dataUrl, setDataUrl] = useState<string>('');
const [initialLoading, setInitialLoading] = useState<boolean>(true); const [initialLoading, setInitialLoading] = useState<boolean>(true);
@@ -46,6 +49,7 @@ export const CanvasLabelImage: React.FC<CanvasLabelImageProps> = ({
transparentBackground, transparentBackground,
variableDefaults, variableDefaults,
mode, mode,
scale: renderScale,
}) })
.then((url) => { .then((url) => {
if (active) { if (active) {
@@ -61,7 +65,7 @@ export const CanvasLabelImage: React.FC<CanvasLabelImageProps> = ({
return () => { return () => {
active = false; 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) { if (initialLoading && !dataUrl) {
return ( return (
@@ -78,10 +82,8 @@ export const CanvasLabelImage: React.FC<CanvasLabelImageProps> = ({
<img <img
src={dataUrl} src={dataUrl}
alt="Label Preview" alt="Label Preview"
className="w-full h-full object-fill pointer-events-none select-none block" className="block w-full h-full pointer-events-none select-none"
style={{ style={{
width: '100%',
height: '100%',
imageRendering: 'auto', imageRendering: 'auto',
}} }}
/> />

View File

@@ -1,6 +1,6 @@
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react'; import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
import { LabelTemplate, TemplateElement } from '../types'; import { LabelTemplate, TemplateElement } from '../types';
import { mmToPx, shouldRenderElement } from '../utils'; import { mmToPx, MM_TO_PX, shouldRenderElement } from '../utils';
import { useIsNarrowScreen } from '../hooks/useIsMobile'; import { useIsNarrowScreen } from '../hooks/useIsMobile';
import { CanvasLabelImage } from './CanvasLabelImage'; import { CanvasLabelImage } from './CanvasLabelImage';
import { import {
@@ -181,6 +181,8 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
const ignoreCanvasClickRef = useRef(false); const ignoreCanvasClickRef = useRef(false);
const dragMovedRef = useRef(false); const dragMovedRef = useRef(false);
const multiTouchActiveRef = useRef(false);
const touchPointerMapRef = useRef<Map<number, { x: number; y: number }>>(new Map());
const marqueeRef = useRef(marqueeState); const marqueeRef = useRef(marqueeState);
marqueeRef.current = marqueeState; marqueeRef.current = marqueeState;
const selectedIdsRef = useRef(selectedElementIds); const selectedIdsRef = useRef(selectedElementIds);
@@ -395,6 +397,62 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
deleteSelectedElements, 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 applyModerateZoom = useCallback(() => {
const area = canvasAreaRef.current; const area = canvasAreaRef.current;
if (!area) return; if (!area) return;
@@ -434,6 +492,7 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
if (!marqueeActive) return; if (!marqueeActive) return;
const handlePointerMove = (e: PointerEvent) => { const handlePointerMove = (e: PointerEvent) => {
if (multiTouchActiveRef.current) return;
const canvas = canvasRef.current; const canvas = canvasRef.current;
if (!canvas) return; if (!canvas) return;
const rect = canvas.getBoundingClientRect(); const rect = canvas.getBoundingClientRect();
@@ -443,6 +502,10 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
}; };
const handlePointerUp = () => { const handlePointerUp = () => {
if (multiTouchActiveRef.current) {
setMarqueeState(null);
return;
}
const state = marqueeRef.current; const state = marqueeRef.current;
setMarqueeState(null); setMarqueeState(null);
@@ -502,12 +565,17 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
dragMovedRef.current = false; dragMovedRef.current = false;
const handlePointerMove = (e: PointerEvent) => { const handlePointerMove = (e: PointerEvent) => {
if (multiTouchActiveRef.current) return;
const deltaX = e.clientX - dragState.startX; const deltaX = e.clientX - dragState.startX;
const deltaY = e.clientY - dragState.startY; const deltaY = e.clientY - dragState.startY;
if ( const pastDragThreshold =
dragState.type === 'drag' && Math.abs(deltaX) > DRAG_START_THRESHOLD_PX || Math.abs(deltaY) > DRAG_START_THRESHOLD_PX;
(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; dragMovedRef.current = true;
} }
const MM_PER_PX = 1 / (3.78 * zoom); const MM_PER_PX = 1 / (3.78 * zoom);
@@ -584,6 +652,10 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
}; };
const handlePointerUp = () => { const handlePointerUp = () => {
if (multiTouchActiveRef.current) {
cancelActiveInteraction();
return;
}
if (dragRafRef.current !== null) { if (dragRafRef.current !== null) {
cancelAnimationFrame(dragRafRef.current); cancelAnimationFrame(dragRafRef.current);
dragRafRef.current = null; dragRafRef.current = null;
@@ -592,11 +664,11 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
localElementsRef.current = pendingDragElementsRef.current; localElementsRef.current = pendingDragElementsRef.current;
pendingDragElementsRef.current = null; pendingDragElementsRef.current = null;
} }
if (localElementsRef.current) { if (localElementsRef.current && dragMovedRef.current) {
onChange({ ...template, elements: localElementsRef.current }); onChange({ ...template, elements: localElementsRef.current });
localElementsRef.current = null;
setLocalElements(null);
} }
localElementsRef.current = null;
setLocalElements(null);
setFrozenCanvasElements(null); setFrozenCanvasElements(null);
setDragState(null); setDragState(null);
}; };
@@ -609,7 +681,7 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
window.removeEventListener('pointerup', handlePointerUp); window.removeEventListener('pointerup', handlePointerUp);
window.removeEventListener('pointercancel', handlePointerUp); window.removeEventListener('pointercancel', handlePointerUp);
}; };
}, [dragState, template, zoom, onChange]); }, [dragState, template, zoom, onChange, cancelActiveInteraction]);
const buildGroupStartPositions = (element: TemplateElement) => { const buildGroupStartPositions = (element: TemplateElement) => {
const moveIds = const moveIds =
@@ -628,6 +700,7 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
const startMarquee = (e: React.PointerEvent) => { const startMarquee = (e: React.PointerEvent) => {
if (activeTool !== 'move' || e.button !== 0) return; if (activeTool !== 'move' || e.button !== 0) return;
if (shouldIgnoreTouchPointer(e)) return;
const canvas = canvasRef.current; const canvas = canvasRef.current;
if (!canvas) return; if (!canvas) return;
@@ -653,6 +726,7 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
const handleWorkspacePointerDown = (e: React.PointerEvent<HTMLDivElement>) => { const handleWorkspacePointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
if (activeTool !== 'move' || e.button !== 0) return; if (activeTool !== 'move' || e.button !== 0) return;
if (shouldIgnoreTouchPointer(e)) return;
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
if (target.closest('[data-label-element]') || target.closest('.ps-handle')) return; if (target.closest('[data-label-element]') || target.closest('.ps-handle')) return;
@@ -665,6 +739,7 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
actionType: 'drag' | 'resize' actionType: 'drag' | 'resize'
) => { ) => {
if (e.button !== 0) return; if (e.button !== 0) return;
if (shouldIgnoreTouchPointer(e)) return;
e.stopPropagation(); e.stopPropagation();
if (e.shiftKey || e.ctrlKey || e.metaKey) { if (e.shiftKey || e.ctrlKey || e.metaKey) {
@@ -698,6 +773,7 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
element: TemplateElement, element: TemplateElement,
actionType: 'drag' | 'resize' actionType: 'drag' | 'resize'
) => { ) => {
if (shouldIgnoreTouchPointer(e)) return;
e.preventDefault(); e.preventDefault();
if (!selectedElementIds.includes(element.id)) { if (!selectedElementIds.includes(element.id)) {
onSelectElements([element.id]); onSelectElements([element.id]);
@@ -724,6 +800,7 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
element: TemplateElement, element: TemplateElement,
corner: ResizeCorner corner: ResizeCorner
) => { ) => {
if (shouldIgnoreTouchPointer(e)) return;
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
onSelectElements([element.id]); onSelectElements([element.id]);
@@ -778,6 +855,7 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
frozenCanvasElements ?? template.elements frozenCanvasElements ?? template.elements
); );
const overlayElements = filterVisibleElements(localElements ?? 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 }[] = [ const tools: { id: ActiveTool; icon: React.ReactNode; label: string; shortcut?: string }[] = [
{ id: 'move', icon: <Move className="w-[18px] h-[18px]" />, label: '移动工具', shortcut: 'V' }, { id: 'move', icon: <Move className="w-[18px] h-[18px]" />, label: '移动工具', shortcut: 'V' },
@@ -909,6 +987,7 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
rowIndex={0} rowIndex={0}
showBorder={false} showBorder={false}
variableDefaults={previewDefaults} variableDefaults={previewDefaults}
renderScale={canvasRenderScale}
/> />
</div> </div>