表格支持
This commit is contained in:
@@ -2,6 +2,17 @@ import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react'
|
||||
import { createPortal } from 'react-dom';
|
||||
import { LabelTemplate, TemplateElement } from '../types';
|
||||
import { mmToPx, MM_TO_PX, shouldRenderElement } from '../utils';
|
||||
import {
|
||||
createDefaultTableElement,
|
||||
getTableColWidths,
|
||||
getTableRowHeights,
|
||||
iterTableCells,
|
||||
getCellRectMm,
|
||||
toggleTableCellSelection,
|
||||
scaleTableTracks,
|
||||
getCellAnchor,
|
||||
type TableCellCoord,
|
||||
} from '../tableUtils';
|
||||
import { useIsNarrowScreen } from '../hooks/useIsMobile';
|
||||
import { CanvasLabelImage } from './CanvasLabelImage';
|
||||
import { ElementFloatPreview, renderElementFloatPreview } from './ElementFloatPreview';
|
||||
@@ -16,6 +27,7 @@ import {
|
||||
Image as ImageIcon,
|
||||
Move,
|
||||
Trash2,
|
||||
Table2,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface LabelDesignerProps {
|
||||
@@ -23,12 +35,14 @@ interface LabelDesignerProps {
|
||||
onChange: (updated: LabelTemplate) => void;
|
||||
selectedElementIds: string[];
|
||||
onSelectElements: (ids: string[]) => void;
|
||||
selectedTableCells?: TableCellCoord[];
|
||||
onSelectTableCells?: (cells: TableCellCoord[] | ((prev: TableCellCoord[]) => TableCellCoord[])) => void;
|
||||
variant?: 'desktop' | 'mobile';
|
||||
/** 微调等场景下隐藏选区框与缩放手柄 */
|
||||
suppressSelectionChrome?: boolean;
|
||||
}
|
||||
|
||||
type ActiveTool = 'move' | 'text' | 'barcode' | 'qrcode';
|
||||
type ActiveTool = 'move' | 'text' | 'barcode' | 'qrcode' | 'table';
|
||||
type ResizeCorner = 'nw' | 'ne' | 'sw' | 'se';
|
||||
|
||||
const MIN_ELEM_W = 2;
|
||||
@@ -194,6 +208,8 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
onChange,
|
||||
selectedElementIds,
|
||||
onSelectElements,
|
||||
selectedTableCells = [],
|
||||
onSelectTableCells,
|
||||
variant = 'desktop',
|
||||
suppressSelectionChrome = false,
|
||||
}) => {
|
||||
@@ -256,6 +272,8 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
startElemY: number;
|
||||
startElemW: number;
|
||||
startElemH: number;
|
||||
startTableRowHeights?: number[];
|
||||
startTableColWidths?: number[];
|
||||
groupStartPositions?: Record<string, { x: number; y: number }>;
|
||||
} | null>(null);
|
||||
|
||||
@@ -269,6 +287,15 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
|
||||
const ignoreCanvasClickRef = useRef(false);
|
||||
const dragMovedRef = useRef(false);
|
||||
const tableCellPointerRef = useRef<{
|
||||
row: number;
|
||||
col: number;
|
||||
elementId: string;
|
||||
startX: number;
|
||||
startY: number;
|
||||
pointerId: number;
|
||||
dragStarted: boolean;
|
||||
} | null>(null);
|
||||
const multiTouchActiveRef = useRef(false);
|
||||
const touchPointerMapRef = useRef<Map<number, { x: number; y: number }>>(new Map());
|
||||
const marqueeRef = useRef(marqueeState);
|
||||
@@ -297,7 +324,8 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
barcodeFormat: 'CODE128',
|
||||
showText: false,
|
||||
fontFamily: 'sans',
|
||||
backgroundColor: 'transparent',
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundOpacity: 0,
|
||||
imageFit: 'contain',
|
||||
};
|
||||
onChange({
|
||||
@@ -311,12 +339,14 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
);
|
||||
|
||||
const addElement = useCallback(
|
||||
(type: 'text' | 'barcode' | 'qrcode') => {
|
||||
(type: 'text' | 'barcode' | 'qrcode' | 'table') => {
|
||||
let newElement: TemplateElement;
|
||||
const id = `${type}_${Date.now()}`;
|
||||
const n = template.elements.length + 1;
|
||||
|
||||
if (type === 'text') {
|
||||
if (type === 'table') {
|
||||
newElement = createDefaultTableElement(id, n);
|
||||
} else if (type === 'text') {
|
||||
newElement = {
|
||||
id,
|
||||
type: 'text',
|
||||
@@ -334,7 +364,8 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
barcodeFormat: 'CODE128',
|
||||
showText: false,
|
||||
fontFamily: 'sans',
|
||||
backgroundColor: 'transparent',
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundOpacity: 0,
|
||||
};
|
||||
} else if (type === 'barcode') {
|
||||
newElement = {
|
||||
@@ -352,7 +383,8 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
barcodeFormat: 'CODE128',
|
||||
showText: true,
|
||||
fontFamily: 'mono',
|
||||
backgroundColor: 'transparent',
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundOpacity: 0,
|
||||
};
|
||||
} else {
|
||||
newElement = {
|
||||
@@ -370,7 +402,8 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
barcodeFormat: 'CODE128',
|
||||
showText: false,
|
||||
fontFamily: 'sans',
|
||||
backgroundColor: 'transparent',
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundOpacity: 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -883,11 +916,25 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
corner
|
||||
);
|
||||
|
||||
const updated = template.elements.map((el) =>
|
||||
el.id === dragState.elementId
|
||||
? { ...el, x: rect.x, y: rect.y, width: rect.w, height: rect.h }
|
||||
: el
|
||||
);
|
||||
const updated = template.elements.map((el) => {
|
||||
if (el.id !== dragState.elementId) return el;
|
||||
if (
|
||||
el.type === 'table' &&
|
||||
dragState.startTableRowHeights &&
|
||||
dragState.startTableColWidths
|
||||
) {
|
||||
return scaleTableTracks(
|
||||
{ ...el, x: rect.x, y: rect.y },
|
||||
rect.w,
|
||||
rect.h,
|
||||
dragState.startElemW,
|
||||
dragState.startElemH,
|
||||
dragState.startTableRowHeights,
|
||||
dragState.startTableColWidths
|
||||
);
|
||||
}
|
||||
return { ...el, x: rect.x, y: rect.y, width: rect.w, height: rect.h };
|
||||
});
|
||||
pendingDragElementsRef.current = updated;
|
||||
|
||||
const paintInteractionFloat = () => {
|
||||
@@ -1109,6 +1156,71 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleTableCellPointerDown = (
|
||||
e: React.PointerEvent,
|
||||
element: TemplateElement,
|
||||
row: number,
|
||||
col: number
|
||||
) => {
|
||||
if (e.button !== 0 || shouldIgnoreTouchPointer(e) || element.locked) return;
|
||||
e.stopPropagation();
|
||||
e.currentTarget.setPointerCapture(e.pointerId);
|
||||
tableCellPointerRef.current = {
|
||||
row,
|
||||
col,
|
||||
elementId: element.id,
|
||||
startX: e.clientX,
|
||||
startY: e.clientY,
|
||||
pointerId: e.pointerId,
|
||||
dragStarted: false,
|
||||
};
|
||||
};
|
||||
|
||||
const handleTableCellPointerMove = (
|
||||
e: React.PointerEvent,
|
||||
element: TemplateElement
|
||||
) => {
|
||||
const pending = tableCellPointerRef.current;
|
||||
if (!pending || pending.pointerId !== e.pointerId || pending.elementId !== element.id) {
|
||||
return;
|
||||
}
|
||||
if (pending.dragStarted || element.locked) return;
|
||||
|
||||
const dx = e.clientX - pending.startX;
|
||||
const dy = e.clientY - pending.startY;
|
||||
if (
|
||||
Math.abs(dx) > DRAG_START_THRESHOLD_PX ||
|
||||
Math.abs(dy) > DRAG_START_THRESHOLD_PX
|
||||
) {
|
||||
pending.dragStarted = true;
|
||||
dragMovedRef.current = true;
|
||||
tableCellPointerRef.current = null;
|
||||
try {
|
||||
(e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
handleDragStart(e, element, 'drag');
|
||||
}
|
||||
};
|
||||
|
||||
const handleTableCellPointerUp = (e: React.PointerEvent) => {
|
||||
const pending = tableCellPointerRef.current;
|
||||
if (!pending || pending.pointerId !== e.pointerId) return;
|
||||
|
||||
if (!pending.dragStarted && onSelectTableCells) {
|
||||
onSelectTableCells((prev) =>
|
||||
toggleTableCellSelection(prev, { row: pending.row, col: pending.col })
|
||||
);
|
||||
}
|
||||
tableCellPointerRef.current = null;
|
||||
try {
|
||||
(e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
|
||||
const handleResizeStart = (
|
||||
e: React.PointerEvent,
|
||||
element: TemplateElement,
|
||||
@@ -1135,6 +1247,10 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
startElemY: element.y,
|
||||
startElemW: element.width,
|
||||
startElemH: element.height,
|
||||
startTableRowHeights:
|
||||
element.type === 'table' ? getTableRowHeights(element) : undefined,
|
||||
startTableColWidths:
|
||||
element.type === 'table' ? getTableColWidths(element) : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1300,8 +1416,9 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
const overlayElements = filterVisibleElements(template.elements);
|
||||
|
||||
const tools: { id: ActiveTool; icon: React.ReactNode; label: string; shortcut?: string }[] = [
|
||||
{ id: 'move', icon: <Move className="w-[18px] h-[18px]" />, label: '移动工具', shortcut: 'V' },
|
||||
{ id: 'text', icon: <Type className="w-[18px] h-[18px]" />, label: '文本工具', shortcut: 'T' },
|
||||
{ id: 'move', icon: <Move className="w-[18px] h-[18px]" />, label: '移动', shortcut: 'V' },
|
||||
{ id: 'text', icon: <Type className="w-[18px] h-[18px]" />, label: '文本', shortcut: 'T' },
|
||||
{ id: 'table', icon: <Table2 className="w-[18px] h-[18px]" />, label: '表格' },
|
||||
{ id: 'barcode', icon: <Barcode className="w-[18px] h-[18px]" />, label: '条形码' },
|
||||
{ id: 'qrcode', icon: <QrCode className="w-[18px] h-[18px]" />, label: '二维码' },
|
||||
];
|
||||
@@ -1734,6 +1851,52 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
{element.name}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSelected &&
|
||||
!hideSelectionChrome &&
|
||||
!isLocked &&
|
||||
selectedElementIds.length === 1 &&
|
||||
overlayElement.type === 'table' &&
|
||||
onSelectTableCells && (
|
||||
<div className="absolute inset-0 z-10 pointer-events-none">
|
||||
{iterTableCells(overlayElement).map(({ row, col }) => {
|
||||
const rect = getCellRectMm(overlayElement, row, col);
|
||||
const cellX = mmToPx(rect.x, displayZoom);
|
||||
const cellY = mmToPx(rect.y, displayZoom);
|
||||
const cellW = mmToPx(rect.width, displayZoom);
|
||||
const cellH = mmToPx(rect.height, displayZoom);
|
||||
const isCellSelected = selectedTableCells.some((c) => {
|
||||
const anchor = getCellAnchor(overlayElement, c.row, c.col);
|
||||
return anchor.row === row && anchor.col === col;
|
||||
});
|
||||
return (
|
||||
<div
|
||||
key={`cell-${row}-${col}`}
|
||||
className={`absolute pointer-events-auto cursor-grab active:cursor-grabbing border touch-none ${
|
||||
isCellSelected
|
||||
? 'bg-[#31a8ff]/25 border-[#31a8ff]'
|
||||
: 'border-transparent hover:bg-[#31a8ff]/10 hover:border-[#31a8ff]/40'
|
||||
}`}
|
||||
style={{
|
||||
left: `${cellX}px`,
|
||||
top: `${cellY}px`,
|
||||
width: `${cellW}px`,
|
||||
height: `${cellH}px`,
|
||||
}}
|
||||
onPointerDown={(e) =>
|
||||
handleTableCellPointerDown(e, overlayElement, row, col)
|
||||
}
|
||||
onPointerMove={(e) =>
|
||||
handleTableCellPointerMove(e, overlayElement)
|
||||
}
|
||||
onPointerUp={(e) => handleTableCellPointerUp(e)}
|
||||
onPointerCancel={(e) => handleTableCellPointerUp(e)}
|
||||
title={`单元格 ${row + 1},${col + 1}(拖动移动表格)`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user