优化:多指交互
This commit is contained in:
@@ -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',
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user