优化
This commit is contained in:
@@ -3,6 +3,12 @@ import { TemplateElement } from '../types';
|
||||
import { mmToPx } from '../utils';
|
||||
import { renderLabelToDataUrl } from '../labelRenderer';
|
||||
|
||||
/** 离屏单元素渲染时预留描边空间,避免画布边缘裁切表格线 */
|
||||
function getFloatPreviewPaddingMm(element: TemplateElement): number {
|
||||
if (element.type !== 'table') return 0;
|
||||
return (element.tableBorderWidth ?? 0.2) / 2 + 0.1;
|
||||
}
|
||||
|
||||
interface ElementFloatPreviewProps {
|
||||
element: TemplateElement;
|
||||
renderScale: number;
|
||||
@@ -17,10 +23,11 @@ export async function renderElementFloatPreview(
|
||||
renderScale: number,
|
||||
variableDefaults?: Record<string, string> | null
|
||||
): Promise<string> {
|
||||
const padMm = getFloatPreviewPaddingMm(element);
|
||||
return renderLabelToDataUrl({
|
||||
widthMm: element.width,
|
||||
heightMm: element.height,
|
||||
elements: [{ ...element, x: 0, y: 0 }],
|
||||
widthMm: element.width + padMm * 2,
|
||||
heightMm: element.height + padMm * 2,
|
||||
elements: [{ ...element, x: padMm, y: padMm }],
|
||||
rowData: null,
|
||||
rowIndex: 0,
|
||||
showBorder: false,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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 { mmToPx, MM_TO_PX, shouldRenderElement, centerElementOnLabel } from '../utils';
|
||||
import type { ElementClipboardActions } from '../hooks/useElementClipboard';
|
||||
import {
|
||||
createDefaultTableElement,
|
||||
@@ -327,8 +327,7 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
id,
|
||||
type: 'image',
|
||||
name: `图片 ${n}`,
|
||||
x: 5,
|
||||
y: 5,
|
||||
...centerElementOnLabel(template.width, template.height, 30, 30),
|
||||
width: 30,
|
||||
height: 30,
|
||||
content: src,
|
||||
@@ -421,6 +420,14 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
||||
};
|
||||
}
|
||||
|
||||
const centered = centerElementOnLabel(
|
||||
template.width,
|
||||
template.height,
|
||||
newElement.width,
|
||||
newElement.height
|
||||
);
|
||||
newElement = { ...newElement, ...centered };
|
||||
|
||||
onChange({
|
||||
...template,
|
||||
elements: [...template.elements, newElement],
|
||||
|
||||
@@ -997,7 +997,9 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
||||
|
||||
const renderPropertiesPanelBody = () => (
|
||||
<>
|
||||
<div className="pb-3 border-b border-[#3a3a3a]">{renderPreviewReferenceBackgroundFields()}</div>
|
||||
{!selectedElem && (
|
||||
<div className="pb-3 border-b border-[#3a3a3a]">{renderPreviewReferenceBackgroundFields()}</div>
|
||||
)}
|
||||
{/* Label Canvas Dimensions */}
|
||||
{!selectedElem && (
|
||||
<div className="space-y-4">
|
||||
@@ -1684,13 +1686,15 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
||||
value={selectedElem.width}
|
||||
onCommit={(val) => {
|
||||
const wVal = Math.max(2, Number(val) || 2);
|
||||
let updated = { ...selectedElem, width: wVal };
|
||||
if (selectedElem.type === 'qrcode') {
|
||||
updated.height = wVal;
|
||||
handleElemChange({ ...selectedElem, width: wVal, height: wVal });
|
||||
} else if (selectedElem.type === 'table') {
|
||||
updated = scaleTableElementSize(updated, wVal, updated.height);
|
||||
handleElemChange(
|
||||
scaleTableElementSize(selectedElem, wVal, selectedElem.height)
|
||||
);
|
||||
} else {
|
||||
handleElemChange({ ...selectedElem, width: wVal });
|
||||
}
|
||||
handleElemChange(updated);
|
||||
}}
|
||||
className="ps-field font-mono"
|
||||
/>
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
updateTableCells,
|
||||
updateTableColWidth,
|
||||
updateTableRowHeight,
|
||||
equalizeTableColWidths,
|
||||
equalizeTableRowHeights,
|
||||
getTableColWidths,
|
||||
getTableRowHeights,
|
||||
getCellRawContent,
|
||||
@@ -90,7 +92,17 @@ export const TablePropertyFields: React.FC<TablePropertyFieldsProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<span className="text-[10px] font-semibold text-[#aaa]">行高 (mm)</span>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-[10px] font-semibold text-[#aaa]">行高 (mm)</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange(equalizeTableRowHeights(elem))}
|
||||
className="text-[9px] text-[#31a8ff] hover:underline cursor-pointer shrink-0"
|
||||
title="将各行高度恢复为相等"
|
||||
>
|
||||
等分行高
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-1.5">
|
||||
{rowHeights.map((h, i) => (
|
||||
<div key={`rh-${i}`}>
|
||||
@@ -111,7 +123,17 @@ export const TablePropertyFields: React.FC<TablePropertyFieldsProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<span className="text-[10px] font-semibold text-[#aaa]">列宽 (mm)</span>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-[10px] font-semibold text-[#aaa]">列宽 (mm)</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange(equalizeTableColWidths(elem))}
|
||||
className="text-[9px] text-[#31a8ff] hover:underline cursor-pointer shrink-0"
|
||||
title="将各列宽度恢复为相等"
|
||||
>
|
||||
等分列宽
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-1.5">
|
||||
{colWidths.map((w, i) => (
|
||||
<div key={`cw-${i}`}>
|
||||
@@ -287,7 +309,9 @@ export const TableCellContentFields: React.FC<TableCellContentFieldsProps> = ({
|
||||
export function scaleTableElementSize(
|
||||
elem: TemplateElement,
|
||||
newWidth: number,
|
||||
newHeight: number
|
||||
newHeight: number,
|
||||
startWidth = elem.width,
|
||||
startHeight = elem.height
|
||||
): TemplateElement {
|
||||
if (elem.type !== 'table') {
|
||||
return { ...elem, width: newWidth, height: newHeight };
|
||||
@@ -296,8 +320,8 @@ export function scaleTableElementSize(
|
||||
elem,
|
||||
newWidth,
|
||||
newHeight,
|
||||
elem.width,
|
||||
elem.height,
|
||||
startWidth,
|
||||
startHeight,
|
||||
getTableRowHeights(elem),
|
||||
getTableColWidths(elem)
|
||||
);
|
||||
|
||||
@@ -422,12 +422,17 @@ function drawTableGridLines(
|
||||
ctx.beginPath();
|
||||
|
||||
const inset = borderW / 2;
|
||||
ctx.rect(
|
||||
alignStrokeCoord(left + inset),
|
||||
alignStrokeCoord(top + inset),
|
||||
Math.max(0, width - borderW),
|
||||
Math.max(0, height - borderW)
|
||||
);
|
||||
const right = left + width;
|
||||
const bottom = top + height;
|
||||
// 外框:描边完全落在网格区域内,避免 clip / 画布边缘裁切右、下边
|
||||
ctx.moveTo(alignStrokeCoord(left + inset), alignStrokeCoord(top + inset));
|
||||
ctx.lineTo(alignStrokeCoord(right - inset), alignStrokeCoord(top + inset));
|
||||
ctx.moveTo(alignStrokeCoord(right - inset), alignStrokeCoord(top + inset));
|
||||
ctx.lineTo(alignStrokeCoord(right - inset), alignStrokeCoord(bottom - inset));
|
||||
ctx.moveTo(alignStrokeCoord(right - inset), alignStrokeCoord(bottom - inset));
|
||||
ctx.lineTo(alignStrokeCoord(left + inset), alignStrokeCoord(bottom - inset));
|
||||
ctx.moveTo(alignStrokeCoord(left + inset), alignStrokeCoord(bottom - inset));
|
||||
ctx.lineTo(alignStrokeCoord(left + inset), alignStrokeCoord(top + inset));
|
||||
|
||||
for (let r = 1; r < rows; r++) {
|
||||
const y = alignStrokeCoord(rowBounds[r]);
|
||||
@@ -532,8 +537,8 @@ async function drawTableElement(
|
||||
);
|
||||
}
|
||||
|
||||
drawTableGridLines(ctx, elem, ex, ey, scale);
|
||||
ctx.restore();
|
||||
drawTableGridLines(ctx, elem, ex, ey, scale);
|
||||
}
|
||||
|
||||
function drawImagePlaceholder(
|
||||
|
||||
@@ -56,6 +56,36 @@ export function getTableColWidths(elem: TemplateElement): number[] {
|
||||
return Array.from({ length: cols }, () => even);
|
||||
}
|
||||
|
||||
/** 调整末项(必要时向前)使轨道总和精确等于 targetTotal */
|
||||
function absorbTrackSumDiff(tracks: number[], targetTotal: number, min = 0.5): number[] {
|
||||
const result = tracks.map((t) => round1(Math.max(min, t)));
|
||||
let diff = round1(targetTotal - result.reduce((a, b) => a + b, 0));
|
||||
for (let i = result.length - 1; i >= 0 && diff !== 0; i--) {
|
||||
const adjusted = round1(Math.max(min, result[i] + diff));
|
||||
const applied = round1(adjusted - result[i]);
|
||||
result[i] = adjusted;
|
||||
diff = round1(diff - applied);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 按比例缩放轨道,严格使总和等于 targetTotal(不放大目标总量) */
|
||||
function scaleTracksToExactTotal(tracks: number[], targetTotal: number, min = 0.5): number[] {
|
||||
if (tracks.length === 0) return [];
|
||||
const count = tracks.length;
|
||||
const floor = count * min;
|
||||
if (targetTotal <= floor + 0.01) {
|
||||
return absorbTrackSumDiff(Array.from({ length: count }, () => min), targetTotal, min);
|
||||
}
|
||||
const current = tracks.reduce((a, b) => a + b, 0);
|
||||
if (current <= 0) {
|
||||
const even = round1(targetTotal / count);
|
||||
return absorbTrackSumDiff(Array.from({ length: count }, () => even), targetTotal, min);
|
||||
}
|
||||
const scaled = tracks.map((t) => round1(Math.max(min, (t / current) * targetTotal)));
|
||||
return absorbTrackSumDiff(scaled, targetTotal, min);
|
||||
}
|
||||
|
||||
/** 将轨道尺寸总和调整为 targetTotal(保持相对比例,末项吸收取整误差) */
|
||||
function scaleTracksToTotal(tracks: number[], targetTotal: number, min = 0.5): number[] {
|
||||
if (tracks.length === 0) return [];
|
||||
@@ -73,6 +103,48 @@ function scaleTracksToTotal(tracks: number[], targetTotal: number, min = 0.5): n
|
||||
return scaled;
|
||||
}
|
||||
|
||||
/** 调整单条轨道尺寸,保持轨道总和不变;优先由 index 之后的轨道吸收差值 */
|
||||
function resizeTrackKeepingTotal(
|
||||
tracks: number[],
|
||||
index: number,
|
||||
sizeMm: number,
|
||||
min = 0.5
|
||||
): number[] {
|
||||
if (index < 0 || index >= tracks.length) return tracks;
|
||||
const total = round1(tracks.reduce((a, b) => a + b, 0));
|
||||
if (tracks.length === 1) {
|
||||
return [round1(Math.max(min, Math.min(total, sizeMm)))];
|
||||
}
|
||||
|
||||
const beforeSum = round1(tracks.slice(0, index).reduce((a, b) => a + b, 0));
|
||||
const afterCount = tracks.length - index - 1;
|
||||
const beforeCount = index;
|
||||
|
||||
const maxForIndex =
|
||||
afterCount > 0
|
||||
? round1(total - beforeSum - afterCount * min)
|
||||
: round1(total - beforeCount * min);
|
||||
const newSize = round1(Math.max(min, Math.min(maxForIndex, sizeMm)));
|
||||
const result = [...tracks];
|
||||
result[index] = newSize;
|
||||
|
||||
if (afterCount > 0) {
|
||||
const afterRemaining = round1(total - beforeSum - newSize);
|
||||
const scaledAfter = scaleTracksToExactTotal(tracks.slice(index + 1), afterRemaining, min);
|
||||
for (let i = 0; i < scaledAfter.length; i++) {
|
||||
result[index + 1 + i] = scaledAfter[i];
|
||||
}
|
||||
} else if (beforeCount > 0) {
|
||||
const beforeRemaining = round1(total - newSize);
|
||||
const scaledBefore = scaleTracksToExactTotal(tracks.slice(0, index), beforeRemaining, min);
|
||||
for (let i = 0; i < scaledBefore.length; i++) {
|
||||
result[i] = scaledBefore[i];
|
||||
}
|
||||
}
|
||||
|
||||
return absorbTrackSumDiff(result, total, min);
|
||||
}
|
||||
|
||||
/** 将 inner 区域均分给 count 条轨道(末项吸收取整误差) */
|
||||
function distributeTracks(total: number, count: number, min = 0.5): number[] {
|
||||
if (count <= 0) return [];
|
||||
@@ -132,8 +204,14 @@ export function scaleTableTracks(
|
||||
const scaleW = newInnerW / startInnerW;
|
||||
const scaleH = newInnerH / startInnerH;
|
||||
|
||||
const tableRowHeights = startRowHeights.map((h) => round1(Math.max(0.5, h * scaleH)));
|
||||
const tableColWidths = startColWidths.map((w) => round1(Math.max(0.5, w * scaleW)));
|
||||
const tableRowHeights = scaleTracksToExactTotal(
|
||||
startRowHeights.map((h) => h * scaleH),
|
||||
newInnerH
|
||||
);
|
||||
const tableColWidths = scaleTracksToExactTotal(
|
||||
startColWidths.map((w) => w * scaleW),
|
||||
newInnerW
|
||||
);
|
||||
|
||||
return {
|
||||
...elem,
|
||||
@@ -456,14 +534,27 @@ export function updateTableRowHeight(
|
||||
rowIndex: number,
|
||||
heightMm: number
|
||||
): TemplateElement {
|
||||
const rowHeights = [...getTableRowHeights(elem)];
|
||||
if (rowIndex < 0 || rowIndex >= rowHeights.length) return elem;
|
||||
rowHeights[rowIndex] = Math.max(0.5, round1(heightMm));
|
||||
if (rowIndex < 0 || rowIndex >= getTableRows(elem)) return elem;
|
||||
const pad = resolvePaddingMm(elem);
|
||||
const innerH = getTableInnerSizeMm(elem).height;
|
||||
const rowHeights = scaleTracksToExactTotal(getTableRowHeights(elem), innerH);
|
||||
const isLast = rowIndex === rowHeights.length - 1;
|
||||
|
||||
if (isLast) {
|
||||
const nextHeights = [...rowHeights];
|
||||
nextHeights[rowIndex] = round1(Math.max(0.5, heightMm));
|
||||
const newInnerH = round1(nextHeights.reduce((a, b) => a + b, 0));
|
||||
return {
|
||||
...elem,
|
||||
tableRowHeights: nextHeights,
|
||||
height: round1(newInnerH + pad.top + pad.bottom),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...elem,
|
||||
tableRowHeights: rowHeights,
|
||||
height: round1(rowHeights.reduce((a, b) => a + b, 0) + pad.top + pad.bottom),
|
||||
tableRowHeights: resizeTrackKeepingTotal(rowHeights, rowIndex, heightMm),
|
||||
height: elem.height,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -472,14 +563,55 @@ export function updateTableColWidth(
|
||||
colIndex: number,
|
||||
widthMm: number
|
||||
): TemplateElement {
|
||||
const colWidths = [...getTableColWidths(elem)];
|
||||
if (colIndex < 0 || colIndex >= colWidths.length) return elem;
|
||||
colWidths[colIndex] = Math.max(0.5, round1(widthMm));
|
||||
if (colIndex < 0 || colIndex >= getTableCols(elem)) return elem;
|
||||
const pad = resolvePaddingMm(elem);
|
||||
const innerW = getTableInnerSizeMm(elem).width;
|
||||
const colWidths = scaleTracksToExactTotal(getTableColWidths(elem), innerW);
|
||||
const isLast = colIndex === colWidths.length - 1;
|
||||
|
||||
if (isLast) {
|
||||
const nextWidths = [...colWidths];
|
||||
nextWidths[colIndex] = round1(Math.max(0.5, widthMm));
|
||||
const newInnerW = round1(nextWidths.reduce((a, b) => a + b, 0));
|
||||
return {
|
||||
...elem,
|
||||
tableColWidths: nextWidths,
|
||||
width: round1(newInnerW + pad.left + pad.right),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...elem,
|
||||
tableColWidths: colWidths,
|
||||
width: round1(colWidths.reduce((a, b) => a + b, 0) + pad.left + pad.right),
|
||||
tableColWidths: resizeTrackKeepingTotal(colWidths, colIndex, widthMm),
|
||||
width: elem.width,
|
||||
};
|
||||
}
|
||||
|
||||
/** 在现有元素高度内均分行高 */
|
||||
export function equalizeTableRowHeights(elem: TemplateElement): TemplateElement {
|
||||
const rows = getTableRows(elem);
|
||||
const innerH = getTableInnerSizeMm(elem).height;
|
||||
return {
|
||||
...elem,
|
||||
tableRowHeights: scaleTracksToExactTotal(
|
||||
Array.from({ length: rows }, () => innerH / rows),
|
||||
innerH
|
||||
),
|
||||
height: elem.height,
|
||||
};
|
||||
}
|
||||
|
||||
/** 在现有元素宽度内均分列宽 */
|
||||
export function equalizeTableColWidths(elem: TemplateElement): TemplateElement {
|
||||
const cols = getTableCols(elem);
|
||||
const innerW = getTableInnerSizeMm(elem).width;
|
||||
return {
|
||||
...elem,
|
||||
tableColWidths: scaleTracksToExactTotal(
|
||||
Array.from({ length: cols }, () => innerW / cols),
|
||||
innerW
|
||||
),
|
||||
width: elem.width,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
15
src/utils.ts
15
src/utils.ts
@@ -30,6 +30,21 @@ export const VARIABLE_MAPPING_ROW_INDEX_LABEL = '数据序号';
|
||||
/** 粘贴副本时相对原位置的偏移 (mm) */
|
||||
export const PASTE_ELEMENT_OFFSET_MM = 2;
|
||||
|
||||
/** 在标签画布上居中放置元素(mm,保留 1 位小数) */
|
||||
export function centerElementOnLabel(
|
||||
labelWidth: number,
|
||||
labelHeight: number,
|
||||
elementWidth: number,
|
||||
elementHeight: number
|
||||
): { x: number; y: number } {
|
||||
const x = Math.round(((labelWidth - elementWidth) / 2) * 10) / 10;
|
||||
const y = Math.round(((labelHeight - elementHeight) / 2) * 10) / 10;
|
||||
return {
|
||||
x: Math.max(0, x),
|
||||
y: Math.max(0, y),
|
||||
};
|
||||
}
|
||||
|
||||
/** 深拷贝元素并生成新 id,用于复制/粘贴 */
|
||||
export function cloneTemplateElementsForPaste(
|
||||
elements: TemplateElement[],
|
||||
|
||||
Reference in New Issue
Block a user