优化:多指交互

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

View File

@@ -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<LabelDesignerProps> = ({
const ignoreCanvasClickRef = 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);
marqueeRef.current = marqueeState;
const selectedIdsRef = useRef(selectedElementIds);
@@ -395,6 +397,62 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
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<LabelDesignerProps> = ({
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<LabelDesignerProps> = ({
};
const handlePointerUp = () => {
if (multiTouchActiveRef.current) {
setMarqueeState(null);
return;
}
const state = marqueeRef.current;
setMarqueeState(null);
@@ -502,12 +565,17 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
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<LabelDesignerProps> = ({
};
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<LabelDesignerProps> = ({
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<LabelDesignerProps> = ({
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<LabelDesignerProps> = ({
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<LabelDesignerProps> = ({
const handleWorkspacePointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
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<LabelDesignerProps> = ({
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<LabelDesignerProps> = ({
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<LabelDesignerProps> = ({
element: TemplateElement,
corner: ResizeCorner
) => {
if (shouldIgnoreTouchPointer(e)) return;
e.stopPropagation();
e.preventDefault();
onSelectElements([element.id]);
@@ -778,6 +855,7 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
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: <Move className="w-[18px] h-[18px]" />, label: '移动工具', shortcut: 'V' },
@@ -909,6 +987,7 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
rowIndex={0}
showBorder={false}
variableDefaults={previewDefaults}
renderScale={canvasRenderScale}
/>
</div>