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