This commit is contained in:
iqudoo
2026-06-12 21:17:24 +08:00
parent 239e2eeebd
commit 220841444a
7 changed files with 240 additions and 81 deletions

View File

@@ -1,6 +1,7 @@
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
import { LabelTemplate, TemplateElement } from '../types';
import { mmToPx, shouldRenderElement } from '../utils';
import { useIsNarrowScreen } from '../hooks/useIsMobile';
import { CanvasLabelImage } from './CanvasLabelImage';
import {
ZoomIn,
@@ -70,6 +71,7 @@ function applyCornerResize(
const MARQUEE_MIN_PX = 4;
const DRAG_START_THRESHOLD_PX = 4;
const MODERATE_ZOOM_MAX = 2.0;
function rectFullyContains(
outer: { left: number; top: number; right: number; bottom: number },
@@ -123,7 +125,8 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
onSelectElements,
}) => {
const selectedElementId = selectedElementIds[0] || null;
const [zoom, setZoom] = useState<number>(2.5);
const isNarrow = useIsNarrowScreen();
const [zoom, setZoom] = useState<number>(MODERATE_ZOOM_MAX);
const [activeTool, setActiveTool] = useState<ActiveTool>('move');
const canvasAreaRef = useRef<HTMLDivElement | null>(null);
const canvasRef = useRef<HTMLDivElement | null>(null);
@@ -378,6 +381,23 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
deleteSelectedElements,
]);
const applyModerateZoom = useCallback(() => {
const area = canvasAreaRef.current;
if (!area) return;
const pad = isNarrow ? 32 : 80;
const availW = area.clientWidth - pad;
const availH = area.clientHeight - pad;
if (availW <= 0 || availH <= 0) return;
const docW = mmToPx(template.width, 1);
const docH = mmToPx(template.height, 1);
const fitZoom = Math.min(availW / docW, availH / docH, MODERATE_ZOOM_MAX);
setZoom(Math.max(0.5, Math.round(fitZoom * 100) / 100));
}, [template.width, template.height, isNarrow]);
useEffect(() => {
applyModerateZoom();
}, [template.id, applyModerateZoom]);
useEffect(() => {
const area = canvasAreaRef.current;
if (!area) return;
@@ -399,7 +419,7 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
useEffect(() => {
if (!marqueeActive) return;
const handleMouseMove = (e: MouseEvent) => {
const handlePointerMove = (e: PointerEvent) => {
const canvas = canvasRef.current;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
@@ -408,7 +428,7 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
setMarqueeState((prev) => (prev ? { ...prev, currentX, currentY } : null));
};
const handleMouseUp = () => {
const handlePointerUp = () => {
const state = marqueeRef.current;
setMarqueeState(null);
@@ -453,11 +473,13 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
}
};
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp);
window.addEventListener('pointercancel', handlePointerUp);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp);
window.removeEventListener('pointercancel', handlePointerUp);
};
}, [marqueeActive, zoom, onSelectElements]);
@@ -465,7 +487,7 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
if (!dragState) return;
dragMovedRef.current = false;
const handleMouseMove = (e: MouseEvent) => {
const handlePointerMove = (e: PointerEvent) => {
const deltaX = e.clientX - dragState.startX;
const deltaY = e.clientY - dragState.startY;
if (
@@ -547,7 +569,7 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
}
};
const handleMouseUp = () => {
const handlePointerUp = () => {
if (dragRafRef.current !== null) {
cancelAnimationFrame(dragRafRef.current);
dragRafRef.current = null;
@@ -565,11 +587,13 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
setDragState(null);
};
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp);
window.addEventListener('pointercancel', handlePointerUp);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp);
window.removeEventListener('pointercancel', handlePointerUp);
};
}, [dragState, template, zoom, onChange]);
@@ -588,7 +612,7 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
return groupStartPositions;
};
const startMarquee = (e: React.MouseEvent) => {
const startMarquee = (e: React.PointerEvent) => {
if (activeTool !== 'move' || e.button !== 0) return;
const canvas = canvasRef.current;
@@ -600,6 +624,7 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
e.preventDefault();
e.stopPropagation();
e.currentTarget.setPointerCapture(e.pointerId);
setMarqueeState({
startX,
@@ -610,7 +635,7 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
});
};
const handleWorkspaceMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
const handleWorkspacePointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
if (activeTool !== 'move' || e.button !== 0) return;
const target = e.target as HTMLElement;
if (target.closest('[data-label-element]') || target.closest('.ps-handle')) return;
@@ -618,8 +643,8 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
startMarquee(e);
};
const handleElementMouseDown = (
e: React.MouseEvent,
const handleElementPointerDown = (
e: React.PointerEvent,
element: TemplateElement,
actionType: 'drag' | 'resize'
) => {
@@ -652,13 +677,18 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
}
};
const handleDragStart = (e: React.MouseEvent, element: TemplateElement, actionType: 'drag' | 'resize') => {
const handleDragStart = (
e: React.PointerEvent,
element: TemplateElement,
actionType: 'drag' | 'resize'
) => {
e.preventDefault();
if (!selectedElementIds.includes(element.id)) {
onSelectElements([element.id]);
}
if (element.locked) return;
e.currentTarget.setPointerCapture(e.pointerId);
setFrozenCanvasElements(template.elements);
setDragState({
elementId: element.id,
@@ -674,7 +704,7 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
};
const handleResizeStart = (
e: React.MouseEvent,
e: React.PointerEvent,
element: TemplateElement,
corner: ResizeCorner
) => {
@@ -683,6 +713,7 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
onSelectElements([element.id]);
if (element.locked) return;
e.currentTarget.setPointerCapture(e.pointerId);
setFrozenCanvasElements(template.elements);
setDragState({
elementId: element.id,
@@ -775,8 +806,8 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
<div className="flex flex-col flex-1 min-w-0 min-h-0">
<div
ref={canvasAreaRef}
className="flex-1 overflow-auto ps-workspace-bg flex items-center justify-center"
onMouseDown={handleWorkspaceMouseDown}
className="flex-1 overflow-auto ps-workspace-bg ps-design-canvas-touch flex items-center justify-center"
onPointerDown={handleWorkspacePointerDown}
onClick={(e) => {
if (ignoreCanvasClickRef.current) {
ignoreCanvasClickRef.current = false;
@@ -820,9 +851,9 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
{activeTool === 'move' && (
<div
className="absolute inset-0 z-[1] cursor-crosshair"
className="absolute inset-0 z-[1] cursor-crosshair touch-none"
aria-hidden
onMouseDown={startMarquee}
onPointerDown={startMarquee}
/>
)}
@@ -851,7 +882,7 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
<div
key={element.id}
data-label-element
className={`absolute group ${
className={`absolute group touch-none ${
isLocked
? 'pointer-events-none'
: isSelected
@@ -867,8 +898,8 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
transform: element.rotation ? `rotate(${element.rotation}deg)` : undefined,
transformOrigin: 'center center',
}}
onMouseDown={
isLocked ? undefined : (e) => handleElementMouseDown(e, element, 'drag')
onPointerDown={
isLocked ? undefined : (e) => handleElementPointerDown(e, element, 'drag')
}
onClick={isLocked ? undefined : (e) => handleElementClick(e, element)}
>
@@ -886,22 +917,22 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
<>
<div
className="ps-handle top-0 left-0 -translate-x-1/2 -translate-y-1/2 cursor-nw-resize"
onMouseDown={(e) => handleResizeStart(e, element, 'nw')}
onPointerDown={(e) => handleResizeStart(e, element, 'nw')}
title="拖拽调整尺寸"
/>
<div
className="ps-handle top-0 right-0 translate-x-1/2 -translate-y-1/2 cursor-ne-resize"
onMouseDown={(e) => handleResizeStart(e, element, 'ne')}
onPointerDown={(e) => handleResizeStart(e, element, 'ne')}
title="拖拽调整尺寸"
/>
<div
className="ps-handle bottom-0 left-0 -translate-x-1/2 translate-y-1/2 cursor-sw-resize"
onMouseDown={(e) => handleResizeStart(e, element, 'sw')}
onPointerDown={(e) => handleResizeStart(e, element, 'sw')}
title="拖拽调整尺寸"
/>
<div
className="ps-handle bottom-0 right-0 translate-x-1/2 translate-y-1/2 cursor-se-resize"
onMouseDown={(e) => handleResizeStart(e, element, 'se')}
onPointerDown={(e) => handleResizeStart(e, element, 'se')}
title="拖拽调整尺寸"
/>
</>
@@ -918,32 +949,44 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
</div>
</div>
<div className="ps-status-bar">
<div className="flex items-center gap-3">
<span>
: {template.width} × {template.height} mm
</span>
{selectedElemObj && (
<span className="text-[#31a8ff]">
| {selectedElemObj.name} X:{selectedElemObj.x} Y:{selectedElemObj.y} W:
{selectedElemObj.width} H:{selectedElemObj.height} mm
</span>
)}
{selectedElementIds.length > 1 && (
<span className="text-emerald-400">
| {selectedElementIds.length}
</span>
)}
{activeTool === 'move' && (
<span className="text-[#888] hidden sm:inline">
| · Shift/Ctrl+ · Ctrl+A
</span>
<div className={`ps-status-bar ${isNarrow ? 'ps-status-bar-compact' : ''}`}>
<div className="flex items-center gap-2 min-w-0 truncate">
{isNarrow ? (
<>
<span className="truncate">
{template.width}×{template.height}
{selectedElemObj ? ` · ${selectedElemObj.name}` : ''}
{selectedElementIds.length > 1 ? ` · ${selectedElementIds.length}` : ''}
</span>
</>
) : (
<>
<span>
: {template.width} × {template.height} mm
</span>
{selectedElemObj && (
<span className="text-[#31a8ff]">
| {selectedElemObj.name} X:{selectedElemObj.x} Y:{selectedElemObj.y} W:
{selectedElemObj.width} H:{selectedElemObj.height} mm
</span>
)}
{selectedElementIds.length > 1 && (
<span className="text-emerald-400">| {selectedElementIds.length} </span>
)}
{activeTool === 'move' && (
<span className="text-[#888] hidden sm:inline">
| · Shift/Ctrl+ · Ctrl+A
</span>
)}
</>
)}
</div>
<div className="flex items-center gap-3">
<span className="hidden sm:inline text-[10px]">
使 · · Ctrl+
</span>
<div className="flex items-center gap-2 shrink-0">
{!isNarrow && (
<span className="hidden sm:inline text-[10px]">
使 · · Ctrl+
</span>
)}
<div className="ps-zoom-control">
<button className="ps-zoom-btn" onClick={() => setZoom(Math.max(0.5, zoom - 0.25))} title="缩小">
<ZoomOut className="w-3 h-3" />
@@ -955,6 +998,13 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
<ZoomIn className="w-3 h-3" />
</button>
</div>
<button
onClick={applyModerateZoom}
className="text-[10px] text-[#aaa] hover:text-white px-1.5 py-0.5 border border-[#4a4a4a] hover:border-[#666] transition cursor-pointer"
title="适中缩放"
>
</button>
<button
onClick={fitToView}
className="text-[10px] text-[#aaa] hover:text-white px-1.5 py-0.5 border border-[#4a4a4a] hover:border-[#666] transition cursor-pointer"