优化
This commit is contained in:
@@ -7,10 +7,10 @@ import {
|
|||||||
getTableColWidths,
|
getTableColWidths,
|
||||||
getTableRowHeights,
|
getTableRowHeights,
|
||||||
iterTableCells,
|
iterTableCells,
|
||||||
getCellRectMm,
|
|
||||||
toggleTableCellSelection,
|
toggleTableCellSelection,
|
||||||
scaleTableTracks,
|
scaleTableTracks,
|
||||||
getCellAnchor,
|
getCellAnchor,
|
||||||
|
getCellRectPx,
|
||||||
type TableCellCoord,
|
type TableCellCoord,
|
||||||
} from '../tableUtils';
|
} from '../tableUtils';
|
||||||
import { useIsNarrowScreen } from '../hooks/useIsMobile';
|
import { useIsNarrowScreen } from '../hooks/useIsMobile';
|
||||||
@@ -1860,11 +1860,8 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
|||||||
onSelectTableCells && (
|
onSelectTableCells && (
|
||||||
<div className="absolute inset-0 z-10 pointer-events-none">
|
<div className="absolute inset-0 z-10 pointer-events-none">
|
||||||
{iterTableCells(overlayElement).map(({ row, col }) => {
|
{iterTableCells(overlayElement).map(({ row, col }) => {
|
||||||
const rect = getCellRectMm(overlayElement, row, col);
|
const { x: cellX, y: cellY, width: cellW, height: cellH } =
|
||||||
const cellX = mmToPx(rect.x, displayZoom);
|
getCellRectPx(overlayElement, row, col, canvasRenderScale);
|
||||||
const cellY = mmToPx(rect.y, displayZoom);
|
|
||||||
const cellW = mmToPx(rect.width, displayZoom);
|
|
||||||
const cellH = mmToPx(rect.height, displayZoom);
|
|
||||||
const isCellSelected = selectedTableCells.some((c) => {
|
const isCellSelected = selectedTableCells.some((c) => {
|
||||||
const anchor = getCellAnchor(overlayElement, c.row, c.col);
|
const anchor = getCellAnchor(overlayElement, c.row, c.col);
|
||||||
return anchor.row === row && anchor.col === col;
|
return anchor.row === row && anchor.col === col;
|
||||||
@@ -1872,16 +1869,19 @@ export const LabelDesigner: React.FC<LabelDesignerProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`cell-${row}-${col}`}
|
key={`cell-${row}-${col}`}
|
||||||
className={`absolute pointer-events-auto cursor-grab active:cursor-grabbing border touch-none ${
|
className={`absolute pointer-events-auto cursor-grab active:cursor-grabbing touch-none ${
|
||||||
isCellSelected
|
isCellSelected
|
||||||
? 'bg-[#31a8ff]/25 border-[#31a8ff]'
|
? 'bg-[#31a8ff]/25'
|
||||||
: 'border-transparent hover:bg-[#31a8ff]/10 hover:border-[#31a8ff]/40'
|
: 'hover:bg-[#31a8ff]/10'
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
left: `${cellX}px`,
|
left: `${cellX}px`,
|
||||||
top: `${cellY}px`,
|
top: `${cellY}px`,
|
||||||
width: `${cellW}px`,
|
width: `${cellW}px`,
|
||||||
height: `${cellH}px`,
|
height: `${cellH}px`,
|
||||||
|
boxShadow: isCellSelected
|
||||||
|
? 'inset 0 0 0 1px #31a8ff'
|
||||||
|
: undefined,
|
||||||
}}
|
}}
|
||||||
onPointerDown={(e) =>
|
onPointerDown={(e) =>
|
||||||
handleTableCellPointerDown(e, overlayElement, row, col)
|
handleTableCellPointerDown(e, overlayElement, row, col)
|
||||||
|
|||||||
@@ -482,7 +482,7 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="text-[10px] font-bold text-[#31a8ff]">变量列表</div>
|
<div className="text-[10px] font-bold text-[#31a8ff]">变量列表</div>
|
||||||
<p className="text-[10px] text-[#666] leading-relaxed mb-3">
|
<p className="text-[10px] text-[#666] leading-relaxed mb-3">
|
||||||
可编辑变量默认值,未被内容或显示条件引用的变量可删除。
|
可编辑变量预览值,未被内容或显示条件引用的变量可删除。
|
||||||
</p>
|
</p>
|
||||||
{templateVariables.map((v) => {
|
{templateVariables.map((v) => {
|
||||||
const inUse = isTemplateVariableInUse(template, v);
|
const inUse = isTemplateVariableInUse(template, v);
|
||||||
@@ -501,7 +501,7 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
|||||||
type="text"
|
type="text"
|
||||||
value={template.variableDefaults?.[v] ?? ''}
|
value={template.variableDefaults?.[v] ?? ''}
|
||||||
onChange={(e) => setVariableDefault(v, e.target.value)}
|
onChange={(e) => setVariableDefault(v, e.target.value)}
|
||||||
placeholder="预览默认值"
|
placeholder="预览值"
|
||||||
className="ps-field font-mono text-[10px] w-full min-w-0"
|
className="ps-field font-mono text-[10px] w-full min-w-0"
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center justify-center w-[44px] shrink-0">
|
<div className="flex items-center justify-center w-[44px] shrink-0">
|
||||||
@@ -661,6 +661,17 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
|||||||
)}
|
)}
|
||||||
{selectedElem.type !== 'table' && (
|
{selectedElem.type !== 'table' && (
|
||||||
<>
|
<>
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<label className="ps-field-label">内容</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={!selectedElem.content}
|
||||||
|
onClick={() => handleElemChange({ ...selectedElem, content: '' })}
|
||||||
|
className="text-[9px] text-[#31a8ff] hover:underline cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed disabled:no-underline shrink-0"
|
||||||
|
>
|
||||||
|
清空内容
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<PropTextarea
|
<PropTextarea
|
||||||
value={selectedElem.content}
|
value={selectedElem.content}
|
||||||
onCommit={(val) => handleElemChange({ ...selectedElem, content: val })}
|
onCommit={(val) => handleElemChange({ ...selectedElem, content: val })}
|
||||||
@@ -691,6 +702,31 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{(selectedElem.type === 'barcode' ||
|
||||||
|
selectedElem.type === 'qrcode' ||
|
||||||
|
selectedElem.type === 'image') && (
|
||||||
|
<label className="flex items-start gap-2 pt-1">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedElem.showWhenContentEmpty ?? false}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleElemChange({
|
||||||
|
...selectedElem,
|
||||||
|
showWhenContentEmpty: e.target.checked ? true : undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="ps-checkbox mt-0.5"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-[#ccc] leading-normal">
|
||||||
|
输出模式下内容为空仍显示
|
||||||
|
{selectedElem.type === 'image' && (
|
||||||
|
<span className="block text-[9px] text-[#777] mt-0.5">
|
||||||
|
开启后绑定数据为空时可显示默认图片
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
{selectedElem.type === 'table' && (
|
{selectedElem.type === 'table' && (
|
||||||
<p className="text-[10px] text-[#777] leading-normal">
|
<p className="text-[10px] text-[#777] leading-normal">
|
||||||
表格各单元格内容独立设置,默认为空。点击画布上的单元格后在属性面板编辑。
|
表格各单元格内容独立设置,默认为空。点击画布上的单元格后在属性面板编辑。
|
||||||
@@ -1407,6 +1443,45 @@ export const PropSidebar: React.FC<PropSidebarProps> = ({
|
|||||||
<option value="fill">拉伸 (fill)</option>
|
<option value="fill">拉伸 (fill)</option>
|
||||||
</FieldSelect>
|
</FieldSelect>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="ps-field-label">默认图片</label>
|
||||||
|
<p className="text-[9px] text-[#777] leading-normal mb-1.5">
|
||||||
|
主图地址加载失败时使用;需同时开启「内容为空仍显示」才能在绑定数据为空时展示
|
||||||
|
</p>
|
||||||
|
<label className="ps-btn text-[10px] cursor-pointer mb-1.5 inline-flex">
|
||||||
|
<Upload className="w-3.5 h-3.5" />
|
||||||
|
选择默认图片
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
className="hidden"
|
||||||
|
onChange={(e) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () =>
|
||||||
|
handleElemChange({
|
||||||
|
...selectedElem,
|
||||||
|
defaultImage: reader.result as string,
|
||||||
|
});
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
e.target.value = '';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<PropTextarea
|
||||||
|
value={selectedElem.defaultImage ?? ''}
|
||||||
|
onCommit={(val) =>
|
||||||
|
handleElemChange({
|
||||||
|
...selectedElem,
|
||||||
|
defaultImage: val.trim() || undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
rows={2}
|
||||||
|
className="ps-field resize-none"
|
||||||
|
placeholder="默认图片 URL 或 data URI"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,17 @@ export const TableCellTextFields: React.FC<TableCellTextFieldsProps> = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="ps-field-label">内容</label>
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<label className="ps-field-label">内容</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={!textElem.content}
|
||||||
|
onClick={() => patchText({ ...textElem, content: '' })}
|
||||||
|
className="text-[9px] text-[#31a8ff] hover:underline cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed disabled:no-underline shrink-0"
|
||||||
|
>
|
||||||
|
清空内容
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
value={textElem.content}
|
value={textElem.content}
|
||||||
rows={2}
|
rows={2}
|
||||||
|
|||||||
@@ -242,7 +242,17 @@ export const TableCellContentFields: React.FC<TableCellContentFieldsProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2 pt-2 border-t border-[#3a3a3a]">
|
<div className="space-y-2 pt-2 border-t border-[#3a3a3a]">
|
||||||
<label className="ps-field-label">批量设置内容({selectedCells.length} 个单元格)</label>
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<label className="ps-field-label">批量设置内容({selectedCells.length} 个单元格)</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={!content}
|
||||||
|
onClick={() => onChange(updateTableCells(elem, selectedCells, { content: '' }))}
|
||||||
|
className="text-[9px] text-[#31a8ff] hover:underline cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed disabled:no-underline shrink-0"
|
||||||
|
>
|
||||||
|
清空内容
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
value={content}
|
value={content}
|
||||||
rows={2}
|
rows={2}
|
||||||
|
|||||||
@@ -19,14 +19,13 @@ import {
|
|||||||
} from './elementBox';
|
} from './elementBox';
|
||||||
import {
|
import {
|
||||||
iterTableCells,
|
iterTableCells,
|
||||||
getCellRectMm,
|
|
||||||
getCellAnchor,
|
getCellAnchor,
|
||||||
getCellData,
|
getCellData,
|
||||||
getEffectiveCellStyle,
|
getEffectiveCellStyle,
|
||||||
getTableRows,
|
getTableRows,
|
||||||
getTableCols,
|
getTableCols,
|
||||||
getTableRowHeights,
|
buildTableGridBoundariesPx,
|
||||||
getTableColWidths,
|
getCellRectPx,
|
||||||
} from './tableUtils';
|
} from './tableUtils';
|
||||||
|
|
||||||
const GENERATOR_TRANSPARENT_BG = '#00000000';
|
const GENERATOR_TRANSPARENT_BG = '#00000000';
|
||||||
@@ -90,6 +89,34 @@ function loadImage(src: string): Promise<HTMLImageElement> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadImageOrNull(src: string): Promise<HTMLImageElement | null> {
|
||||||
|
const trimmed = src.trim();
|
||||||
|
if (!trimmed) return Promise.resolve(null);
|
||||||
|
return loadImage(trimmed).catch(() => null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveElementImage(
|
||||||
|
elem: TemplateElement,
|
||||||
|
primarySrc: string,
|
||||||
|
mode: ContentResolveMode
|
||||||
|
): Promise<{ img: HTMLImageElement | null; showPlaceholder: boolean }> {
|
||||||
|
const primary = primarySrc.trim();
|
||||||
|
const fallback = (elem.defaultImage ?? '').trim();
|
||||||
|
|
||||||
|
let img = await loadImageOrNull(primary);
|
||||||
|
if (!img && fallback) {
|
||||||
|
img = await loadImageOrNull(fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (img) return { img, showPlaceholder: false };
|
||||||
|
|
||||||
|
if (mode === 'design' && !primary && !fallback) {
|
||||||
|
return { img: null, showPlaceholder: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { img: null, showPlaceholder: false };
|
||||||
|
}
|
||||||
|
|
||||||
function drawImageWithFit(
|
function drawImageWithFit(
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
img: HTMLImageElement,
|
img: HTMLImageElement,
|
||||||
@@ -226,7 +253,7 @@ function layoutVerticalTextColumns(
|
|||||||
return columns;
|
return columns;
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawTextInRect(
|
function drawTextContentInBox(
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
elem: TemplateElement,
|
elem: TemplateElement,
|
||||||
boxX: number,
|
boxX: number,
|
||||||
@@ -236,9 +263,15 @@ function drawTextInRect(
|
|||||||
value: string,
|
value: string,
|
||||||
scale: number
|
scale: number
|
||||||
) {
|
) {
|
||||||
const pad = 2 * scale;
|
const pad = resolvePaddingMm(elem);
|
||||||
const writableW = Math.max(1, boxW - pad * 2);
|
const padPx = {
|
||||||
const writableH = Math.max(1, boxH - pad * 2);
|
top: Math.round(pad.top * scale),
|
||||||
|
right: Math.round(pad.right * scale),
|
||||||
|
bottom: Math.round(pad.bottom * scale),
|
||||||
|
left: Math.round(pad.left * scale),
|
||||||
|
};
|
||||||
|
const writableW = Math.max(1, boxW - padPx.left - padPx.right);
|
||||||
|
const writableH = Math.max(1, boxH - padPx.top - padPx.bottom);
|
||||||
|
|
||||||
const fontStyle = elem.fontStyle === 'italic' ? 'italic ' : '';
|
const fontStyle = elem.fontStyle === 'italic' ? 'italic ' : '';
|
||||||
const fontWeight = elem.fontWeight === 'bold' ? 'bold ' : '';
|
const fontWeight = elem.fontWeight === 'bold' ? 'bold ' : '';
|
||||||
@@ -267,8 +300,8 @@ function drawTextInRect(
|
|||||||
|
|
||||||
const textScaleX = Math.max(0.1, Math.min(5, elem.textScaleX ?? 1));
|
const textScaleX = Math.max(0.1, Math.min(5, elem.textScaleX ?? 1));
|
||||||
const textScaleY = Math.max(0.1, Math.min(5, elem.textScaleY ?? 1));
|
const textScaleY = Math.max(0.1, Math.min(5, elem.textScaleY ?? 1));
|
||||||
const transformOriginX = boxX + boxW / 2;
|
const transformOriginX = boxX + padPx.left + writableW / 2;
|
||||||
const transformOriginY = boxY + boxH / 2;
|
const transformOriginY = boxY + padPx.top + writableH / 2;
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
if (textScaleX !== 1 || textScaleY !== 1) {
|
if (textScaleX !== 1 || textScaleY !== 1) {
|
||||||
@@ -285,11 +318,11 @@ function drawTextInRect(
|
|||||||
|
|
||||||
let startX: number;
|
let startX: number;
|
||||||
if (elem.textAlign === 'center') {
|
if (elem.textAlign === 'center') {
|
||||||
startX = boxX + pad + Math.max(0, (writableW - totalColumnsWidth) / 2) + columnWidth / 2;
|
startX = boxX + padPx.left + Math.max(0, (writableW - totalColumnsWidth) / 2) + columnWidth / 2;
|
||||||
} else if (elem.textAlign === 'right') {
|
} else if (elem.textAlign === 'right') {
|
||||||
startX = boxX + pad + Math.max(0, writableW - totalColumnsWidth) + columnWidth / 2;
|
startX = boxX + padPx.left + Math.max(0, writableW - totalColumnsWidth) + columnWidth / 2;
|
||||||
} else {
|
} else {
|
||||||
startX = boxX + pad + columnWidth / 2;
|
startX = boxX + padPx.left + columnWidth / 2;
|
||||||
}
|
}
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
|
|
||||||
@@ -298,17 +331,18 @@ function drawTextInRect(
|
|||||||
const colTextHeight = column.length * lineHeight;
|
const colTextHeight = column.length * lineHeight;
|
||||||
let colStartY: number;
|
let colStartY: number;
|
||||||
if (vAlign === 'top') {
|
if (vAlign === 'top') {
|
||||||
colStartY = boxY + pad + lineHeight / 2;
|
colStartY = boxY + padPx.top + lineHeight / 2;
|
||||||
} else if (vAlign === 'bottom') {
|
} else if (vAlign === 'bottom') {
|
||||||
colStartY = boxY + boxH - pad - colTextHeight + lineHeight / 2;
|
colStartY = boxY + boxH - padPx.bottom - colTextHeight + lineHeight / 2;
|
||||||
} else {
|
} else {
|
||||||
colStartY = boxY + pad + Math.max(0, (writableH - colTextHeight) / 2) + lineHeight / 2;
|
colStartY =
|
||||||
|
boxY + padPx.top + Math.max(0, (writableH - colTextHeight) / 2) + lineHeight / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
const colX = startX + col * columnWidth;
|
const colX = startX + col * columnWidth;
|
||||||
for (let i = 0; i < column.length; i++) {
|
for (let i = 0; i < column.length; i++) {
|
||||||
const charY = colStartY + i * lineHeight;
|
const charY = colStartY + i * lineHeight;
|
||||||
if (charY - lineHeight / 2 <= boxY + boxH - pad) {
|
if (charY - lineHeight / 2 <= boxY + boxH - padPx.bottom) {
|
||||||
ctx.fillText(column[i], colX, charY);
|
ctx.fillText(column[i], colX, charY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -317,31 +351,31 @@ function drawTextInRect(
|
|||||||
const lines = layoutHorizontalTextLines(ctx, String(value), writableW, wordWrap);
|
const lines = layoutHorizontalTextLines(ctx, String(value), writableW, wordWrap);
|
||||||
const totalTextHeight = lines.length * lineHeight;
|
const totalTextHeight = lines.length * lineHeight;
|
||||||
|
|
||||||
let textX = boxX + pad;
|
let textX = boxX + padPx.left;
|
||||||
let textAlign: CanvasTextAlign = 'left';
|
let textAlign: CanvasTextAlign = 'left';
|
||||||
if (elem.textAlign === 'center') {
|
if (elem.textAlign === 'center') {
|
||||||
textX = boxX + boxW / 2;
|
textX = boxX + padPx.left + writableW / 2;
|
||||||
textAlign = 'center';
|
textAlign = 'center';
|
||||||
} else if (elem.textAlign === 'right') {
|
} else if (elem.textAlign === 'right') {
|
||||||
textX = boxX + boxW - pad;
|
textX = boxX + padPx.left + (writableW - 2);
|
||||||
textAlign = 'right';
|
textAlign = 'right';
|
||||||
} else {
|
} else {
|
||||||
textX = boxX + pad + 2;
|
textX = boxX + padPx.left + 2;
|
||||||
}
|
}
|
||||||
ctx.textAlign = textAlign;
|
ctx.textAlign = textAlign;
|
||||||
|
|
||||||
let startY: number;
|
let startY: number;
|
||||||
if (vAlign === 'top') {
|
if (vAlign === 'top') {
|
||||||
startY = boxY + pad + lineHeight / 2;
|
startY = boxY + padPx.top + lineHeight / 2;
|
||||||
} else if (vAlign === 'bottom') {
|
} else if (vAlign === 'bottom') {
|
||||||
startY = boxY + boxH - pad - totalTextHeight + lineHeight / 2;
|
startY = boxY + boxH - padPx.bottom - totalTextHeight + lineHeight / 2;
|
||||||
} else {
|
} else {
|
||||||
startY = boxY + pad + Math.max(0, (writableH - totalTextHeight) / 2) + lineHeight / 2;
|
startY = boxY + padPx.top + Math.max(0, (writableH - totalTextHeight) / 2) + lineHeight / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
const lineY = startY + i * lineHeight;
|
const lineY = startY + i * lineHeight;
|
||||||
if (lineY - lineHeight / 2 <= boxY + boxH - pad) {
|
if (lineY - lineHeight / 2 <= boxY + boxH - padPx.bottom) {
|
||||||
const line = lines[i];
|
const line = lines[i];
|
||||||
ctx.fillText(line, textX, lineY);
|
ctx.fillText(line, textX, lineY);
|
||||||
drawTextLineDecorations(ctx, elem, textX, lineY, line, fontSizePx, textAlign);
|
drawTextLineDecorations(ctx, elem, textX, lineY, line, fontSizePx, textAlign);
|
||||||
@@ -376,19 +410,12 @@ function drawTableGridLines(
|
|||||||
const borderColor = elem.tableBorderColor ?? '#374151';
|
const borderColor = elem.tableBorderColor ?? '#374151';
|
||||||
const rows = getTableRows(elem);
|
const rows = getTableRows(elem);
|
||||||
const cols = getTableCols(elem);
|
const cols = getTableCols(elem);
|
||||||
const rowHeights = getTableRowHeights(elem);
|
const { rowBounds, colBounds } = buildTableGridBoundariesPx(elem, scale, ex, ey);
|
||||||
const colWidths = getTableColWidths(elem);
|
|
||||||
const pad = resolvePaddingMm(elem);
|
|
||||||
|
|
||||||
const x0 = pad.left;
|
const left = colBounds[0];
|
||||||
const y0 = pad.top;
|
const top = rowBounds[0];
|
||||||
const totalW = colWidths.reduce((a, b) => a + b, 0);
|
const width = colBounds[colBounds.length - 1] - left;
|
||||||
const totalH = rowHeights.reduce((a, b) => a + b, 0);
|
const height = rowBounds[rowBounds.length - 1] - top;
|
||||||
|
|
||||||
const left = ex + x0 * scale;
|
|
||||||
const top = ey + y0 * scale;
|
|
||||||
const width = totalW * scale;
|
|
||||||
const height = totalH * scale;
|
|
||||||
|
|
||||||
ctx.strokeStyle = borderColor;
|
ctx.strokeStyle = borderColor;
|
||||||
ctx.lineWidth = borderW;
|
ctx.lineWidth = borderW;
|
||||||
@@ -402,45 +429,31 @@ function drawTableGridLines(
|
|||||||
Math.max(0, height - borderW)
|
Math.max(0, height - borderW)
|
||||||
);
|
);
|
||||||
|
|
||||||
let yAcc = y0;
|
|
||||||
for (let r = 1; r < rows; r++) {
|
for (let r = 1; r < rows; r++) {
|
||||||
yAcc += rowHeights[r - 1];
|
const y = alignStrokeCoord(rowBounds[r]);
|
||||||
const y = alignStrokeCoord(ey + yAcc * scale);
|
|
||||||
let xAcc = x0;
|
|
||||||
for (let c = 0; c < cols; c++) {
|
for (let c = 0; c < cols; c++) {
|
||||||
const w = colWidths[c];
|
|
||||||
const anchorAbove = getCellAnchor(elem, r - 1, c);
|
const anchorAbove = getCellAnchor(elem, r - 1, c);
|
||||||
const anchorBelow = getCellAnchor(elem, r, c);
|
const anchorBelow = getCellAnchor(elem, r, c);
|
||||||
const sameMerge =
|
const sameMerge =
|
||||||
anchorAbove.row === anchorBelow.row && anchorAbove.col === anchorBelow.col;
|
anchorAbove.row === anchorBelow.row && anchorAbove.col === anchorBelow.col;
|
||||||
if (!sameMerge) {
|
if (!sameMerge) {
|
||||||
const x1 = alignStrokeCoord(ex + xAcc * scale);
|
ctx.moveTo(alignStrokeCoord(colBounds[c]), y);
|
||||||
const x2 = alignStrokeCoord(ex + (xAcc + w) * scale);
|
ctx.lineTo(alignStrokeCoord(colBounds[c + 1]), y);
|
||||||
ctx.moveTo(x1, y);
|
|
||||||
ctx.lineTo(x2, y);
|
|
||||||
}
|
}
|
||||||
xAcc += w;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let xAcc = x0;
|
|
||||||
for (let c = 1; c < cols; c++) {
|
for (let c = 1; c < cols; c++) {
|
||||||
xAcc += colWidths[c - 1];
|
const x = alignStrokeCoord(colBounds[c]);
|
||||||
const x = alignStrokeCoord(ex + xAcc * scale);
|
|
||||||
let yAcc2 = y0;
|
|
||||||
for (let r = 0; r < rows; r++) {
|
for (let r = 0; r < rows; r++) {
|
||||||
const h = rowHeights[r];
|
|
||||||
const anchorLeft = getCellAnchor(elem, r, c - 1);
|
const anchorLeft = getCellAnchor(elem, r, c - 1);
|
||||||
const anchorRight = getCellAnchor(elem, r, c);
|
const anchorRight = getCellAnchor(elem, r, c);
|
||||||
const sameMerge =
|
const sameMerge =
|
||||||
anchorLeft.row === anchorRight.row && anchorLeft.col === anchorRight.col;
|
anchorLeft.row === anchorRight.row && anchorLeft.col === anchorRight.col;
|
||||||
if (!sameMerge) {
|
if (!sameMerge) {
|
||||||
const y1 = alignStrokeCoord(ey + yAcc2 * scale);
|
ctx.moveTo(x, alignStrokeCoord(rowBounds[r]));
|
||||||
const y2 = alignStrokeCoord(ey + (yAcc2 + h) * scale);
|
ctx.lineTo(x, alignStrokeCoord(rowBounds[r + 1]));
|
||||||
ctx.moveTo(x, y1);
|
|
||||||
ctx.lineTo(x, y2);
|
|
||||||
}
|
}
|
||||||
yAcc2 += h;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,19 +479,26 @@ async function drawTableElement(
|
|||||||
drawElementFill(ctx, ex, ey, ew, eh, elem, radii);
|
drawElementFill(ctx, ex, ey, ew, eh, elem, radii);
|
||||||
drawElementBorders(ctx, ex, ey, ew, eh, radii, sides, elem, scale);
|
drawElementBorders(ctx, ex, ey, ew, eh, radii, sides, elem, scale);
|
||||||
|
|
||||||
const inner = getPaddedRectPx(ex, ey, ew, eh, elem, scale);
|
const { rowBounds, colBounds } = buildTableGridBoundariesPx(elem, scale, ex, ey);
|
||||||
|
const gridX = colBounds[0];
|
||||||
|
const gridY = rowBounds[0];
|
||||||
|
const gridW = colBounds[colBounds.length - 1] - gridX;
|
||||||
|
const gridH = rowBounds[rowBounds.length - 1] - gridY;
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.rect(inner.x, inner.y, inner.w, inner.h);
|
ctx.rect(gridX, gridY, gridW, gridH);
|
||||||
ctx.clip();
|
ctx.clip();
|
||||||
|
|
||||||
const anchors = iterTableCells(elem);
|
const anchors = iterTableCells(elem);
|
||||||
for (const { row, col } of anchors) {
|
for (const { row, col } of anchors) {
|
||||||
const rect = getCellRectMm(elem, row, col);
|
const { x: cellX, y: cellY, width: cellW, height: cellH } = getCellRectPx(
|
||||||
const cellX = ex + Math.round(rect.x * scale);
|
elem,
|
||||||
const cellY = ey + Math.round(rect.y * scale);
|
row,
|
||||||
const cellW = Math.round(rect.width * scale);
|
col,
|
||||||
const cellH = Math.round(rect.height * scale);
|
scale,
|
||||||
|
ex,
|
||||||
|
ey
|
||||||
|
);
|
||||||
|
|
||||||
const cellData = getCellData(elem, row, col);
|
const cellData = getCellData(elem, row, col);
|
||||||
if (cellData?.backgroundColor && cellData.backgroundColor !== 'transparent') {
|
if (cellData?.backgroundColor && cellData.backgroundColor !== 'transparent') {
|
||||||
@@ -493,7 +513,23 @@ async function drawTableElement(
|
|||||||
const displayText = String(cellValue ?? '').trim();
|
const displayText = String(cellValue ?? '').trim();
|
||||||
if (!displayText) continue;
|
if (!displayText) continue;
|
||||||
|
|
||||||
drawTextInRect(ctx, textElem, cellX, cellY, cellW, cellH, displayText, scale);
|
drawTextContentInBox(
|
||||||
|
ctx,
|
||||||
|
{
|
||||||
|
...textElem,
|
||||||
|
padding: 0,
|
||||||
|
paddingTop: 0,
|
||||||
|
paddingRight: 0,
|
||||||
|
paddingBottom: 0,
|
||||||
|
paddingLeft: 0,
|
||||||
|
},
|
||||||
|
cellX,
|
||||||
|
cellY,
|
||||||
|
cellW,
|
||||||
|
cellH,
|
||||||
|
displayText,
|
||||||
|
scale
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
drawTableGridLines(ctx, elem, ex, ey, scale);
|
drawTableGridLines(ctx, elem, ex, ey, scale);
|
||||||
@@ -561,8 +597,8 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
|
|||||||
|
|
||||||
const scale = scaleOption;
|
const scale = scaleOption;
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.width = Math.max(50, Math.round(widthMm * scale));
|
canvas.width = Math.max(1, Math.round(widthMm * scale));
|
||||||
canvas.height = Math.max(50, Math.round(heightMm * scale));
|
canvas.height = Math.max(1, Math.round(heightMm * scale));
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
if (!ctx) return '';
|
if (!ctx) return '';
|
||||||
@@ -618,133 +654,8 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
|
|||||||
|
|
||||||
drawElementFill(ctx, ex, ey, ew, eh, elem, radii);
|
drawElementFill(ctx, ex, ey, ew, eh, elem, radii);
|
||||||
|
|
||||||
const padPx = (() => {
|
drawTextContentInBox(ctx, elem, ex, ey, ew, eh, String(value), scale);
|
||||||
const pad = resolvePaddingMm(elem);
|
|
||||||
return {
|
|
||||||
top: Math.round(pad.top * scale),
|
|
||||||
right: Math.round(pad.right * scale),
|
|
||||||
bottom: Math.round(pad.bottom * scale),
|
|
||||||
left: Math.round(pad.left * scale),
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
const writableW = Math.max(1, ew - padPx.left - padPx.right);
|
|
||||||
const writableH = Math.max(1, eh - padPx.top - padPx.bottom);
|
|
||||||
|
|
||||||
const fontStyle = elem.fontStyle === 'italic' ? 'italic ' : '';
|
|
||||||
const fontWeight = elem.fontWeight === 'bold' ? 'bold ' : '';
|
|
||||||
const fontSizePx = Math.max(6, Math.max(1, Math.round(elem.fontSize * 0.352777 * scale)));
|
|
||||||
const fontFamily =
|
|
||||||
elem.fontFamily === 'mono'
|
|
||||||
? 'JetBrains Mono, SFMono-Regular, Consolas, Courier New, monospace'
|
|
||||||
: elem.fontFamily === 'serif'
|
|
||||||
? 'Georgia, Times New Roman, serif'
|
|
||||||
: 'Inter, system-ui, -apple-system, Arial, sans-serif';
|
|
||||||
|
|
||||||
ctx.font = `${fontStyle}${fontWeight}${fontSizePx}px ${fontFamily}`.trim();
|
|
||||||
ctx.fillStyle = elem.textColor || '#111827';
|
|
||||||
|
|
||||||
const letterSpacingPx = Math.max(0, (elem.letterSpacing ?? 0) * scale);
|
|
||||||
if (letterSpacingPx > 0 && 'letterSpacing' in ctx) {
|
|
||||||
(ctx as CanvasRenderingContext2D & { letterSpacing: string }).letterSpacing =
|
|
||||||
`${letterSpacingPx}px`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const wordWrap = elem.wordWrap !== false;
|
|
||||||
const lineHeight = fontSizePx * 1.25;
|
|
||||||
const columnWidth = lineHeight;
|
|
||||||
const isVertical = elem.textWritingMode === 'vertical';
|
|
||||||
ctx.textBaseline = 'middle';
|
|
||||||
|
|
||||||
const textScaleX = Math.max(0.1, Math.min(5, elem.textScaleX ?? 1));
|
|
||||||
const textScaleY = Math.max(0.1, Math.min(5, elem.textScaleY ?? 1));
|
|
||||||
const transformOriginX = ex + padPx.left + writableW / 2;
|
|
||||||
const transformOriginY = ey + padPx.top + writableH / 2;
|
|
||||||
|
|
||||||
ctx.save();
|
|
||||||
if (textScaleX !== 1 || textScaleY !== 1) {
|
|
||||||
ctx.translate(transformOriginX, transformOriginY);
|
|
||||||
ctx.scale(textScaleX, textScaleY);
|
|
||||||
ctx.translate(-transformOriginX, -transformOriginY);
|
|
||||||
}
|
|
||||||
|
|
||||||
const vAlign = elem.verticalAlign ?? 'middle';
|
|
||||||
|
|
||||||
if (isVertical) {
|
|
||||||
const columns = layoutVerticalTextColumns(String(value), writableH, lineHeight, wordWrap);
|
|
||||||
const totalColumnsWidth = columns.length * columnWidth;
|
|
||||||
|
|
||||||
let startX: number;
|
|
||||||
if (elem.textAlign === 'center') {
|
|
||||||
startX = ex + padPx.left + Math.max(0, (writableW - totalColumnsWidth) / 2) + columnWidth / 2;
|
|
||||||
} else if (elem.textAlign === 'right') {
|
|
||||||
startX = ex + padPx.left + Math.max(0, writableW - totalColumnsWidth) + columnWidth / 2;
|
|
||||||
} else {
|
|
||||||
startX = ex + padPx.left + columnWidth / 2;
|
|
||||||
}
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
|
|
||||||
for (let col = 0; col < columns.length; col++) {
|
|
||||||
const column = columns[col];
|
|
||||||
const colTextHeight = column.length * lineHeight;
|
|
||||||
let colStartY: number;
|
|
||||||
if (vAlign === 'top') {
|
|
||||||
colStartY = ey + padPx.top + lineHeight / 2;
|
|
||||||
} else if (vAlign === 'bottom') {
|
|
||||||
colStartY = ey + eh - padPx.bottom - colTextHeight + lineHeight / 2;
|
|
||||||
} else {
|
|
||||||
colStartY =
|
|
||||||
ey + padPx.top + Math.max(0, (writableH - colTextHeight) / 2) + lineHeight / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
const colX = startX + col * columnWidth;
|
|
||||||
for (let i = 0; i < column.length; i++) {
|
|
||||||
const charY = colStartY + i * lineHeight;
|
|
||||||
if (charY - lineHeight / 2 <= ey + eh - padPx.bottom) {
|
|
||||||
ctx.fillText(column[i], colX, charY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const lines = layoutHorizontalTextLines(ctx, String(value), writableW, wordWrap);
|
|
||||||
const totalTextHeight = lines.length * lineHeight;
|
|
||||||
|
|
||||||
let textX = ex + padPx.left;
|
|
||||||
let textAlign: CanvasTextAlign = 'left';
|
|
||||||
if (elem.textAlign === 'center') {
|
|
||||||
textX = ex + padPx.left + writableW / 2;
|
|
||||||
textAlign = 'center';
|
|
||||||
} else if (elem.textAlign === 'right') {
|
|
||||||
textX = ex + padPx.left + (writableW - 2);
|
|
||||||
textAlign = 'right';
|
|
||||||
} else {
|
|
||||||
textX = ex + padPx.left + 2;
|
|
||||||
}
|
|
||||||
ctx.textAlign = textAlign;
|
|
||||||
|
|
||||||
let startY: number;
|
|
||||||
if (vAlign === 'top') {
|
|
||||||
startY = ey + padPx.top + lineHeight / 2;
|
|
||||||
} else if (vAlign === 'bottom') {
|
|
||||||
startY = ey + eh - padPx.bottom - totalTextHeight + lineHeight / 2;
|
|
||||||
} else {
|
|
||||||
startY =
|
|
||||||
ey + padPx.top + Math.max(0, (writableH - totalTextHeight) / 2) + lineHeight / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const lineY = startY + i * lineHeight;
|
|
||||||
if (lineY - lineHeight / 2 <= ey + eh - padPx.bottom) {
|
|
||||||
const line = lines[i];
|
|
||||||
ctx.fillText(line, textX, lineY);
|
|
||||||
drawTextLineDecorations(ctx, elem, textX, lineY, line, fontSizePx, textAlign);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.restore();
|
|
||||||
if (letterSpacingPx > 0 && 'letterSpacing' in ctx) {
|
|
||||||
(ctx as CanvasRenderingContext2D & { letterSpacing: string }).letterSpacing = '0px';
|
|
||||||
}
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
drawElementBorders(ctx, ex, ey, ew, eh, radii, sides, elem, scale);
|
drawElementBorders(ctx, ex, ey, ew, eh, radii, sides, elem, scale);
|
||||||
} else if (elem.type === 'barcode') {
|
} else if (elem.type === 'barcode') {
|
||||||
@@ -813,22 +724,20 @@ export async function renderLabelToDataUrl(options: RenderLabelOptions): Promise
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (elem.type === 'image') {
|
} else if (elem.type === 'image') {
|
||||||
|
const primarySrc = value ? String(value).trim() : '';
|
||||||
|
const { img, showPlaceholder } = await resolveElementImage(elem, primarySrc, mode);
|
||||||
|
|
||||||
|
if (!img && !showPlaceholder) {
|
||||||
|
ctx.restore();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
drawElementChrome(ctx, ex, ey, ew, eh, elem, scale);
|
drawElementChrome(ctx, ex, ey, ew, eh, elem, scale);
|
||||||
const inner = getPaddedRectPx(ex, ey, ew, eh, elem, scale);
|
const inner = getPaddedRectPx(ex, ey, ew, eh, elem, scale);
|
||||||
const src = value ? String(value).trim() : '';
|
if (img) {
|
||||||
if (!src) {
|
drawImageWithFit(ctx, img, inner.x, inner.y, inner.w, inner.h, elem.imageFit ?? 'contain');
|
||||||
if (mode === 'design') {
|
|
||||||
drawImagePlaceholder(ctx, inner.x, inner.y, inner.w, inner.h, '图片');
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
try {
|
drawImagePlaceholder(ctx, inner.x, inner.y, inner.w, inner.h, '图片');
|
||||||
const img = await loadImage(src);
|
|
||||||
drawImageWithFit(ctx, img, inner.x, inner.y, inner.w, inner.h, elem.imageFit ?? 'contain');
|
|
||||||
} catch {
|
|
||||||
if (mode === 'design') {
|
|
||||||
drawImagePlaceholder(ctx, inner.x, inner.y, inner.w, inner.h, '加载失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (elem.type === 'table') {
|
} else if (elem.type === 'table') {
|
||||||
await drawTableElement(
|
await drawTableElement(
|
||||||
|
|||||||
@@ -56,18 +56,49 @@ export function getTableColWidths(elem: TemplateElement): number[] {
|
|||||||
return Array.from({ length: cols }, () => even);
|
return Array.from({ length: cols }, () => even);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 内边距变化后保持行高/列宽不变,自动增大元素尺寸 */
|
/** 将轨道尺寸总和调整为 targetTotal(保持相对比例,末项吸收取整误差) */
|
||||||
|
function scaleTracksToTotal(tracks: number[], targetTotal: number, min = 0.5): number[] {
|
||||||
|
if (tracks.length === 0) return [];
|
||||||
|
const safeTotal = Math.max(tracks.length * min, targetTotal);
|
||||||
|
const current = tracks.reduce((a, b) => a + b, 0);
|
||||||
|
if (current <= 0) {
|
||||||
|
return distributeTracks(safeTotal, tracks.length, min);
|
||||||
|
}
|
||||||
|
const scaled = tracks.map((t) => round1(Math.max(min, (t / current) * safeTotal)));
|
||||||
|
const sum = scaled.reduce((a, b) => a + b, 0);
|
||||||
|
const diff = round1(safeTotal - sum);
|
||||||
|
if (scaled.length > 0 && diff !== 0) {
|
||||||
|
scaled[scaled.length - 1] = round1(Math.max(min, scaled[scaled.length - 1] + diff));
|
||||||
|
}
|
||||||
|
return scaled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将 inner 区域均分给 count 条轨道(末项吸收取整误差) */
|
||||||
|
function distributeTracks(total: number, count: number, min = 0.5): number[] {
|
||||||
|
if (count <= 0) return [];
|
||||||
|
const safeTotal = Math.max(count * min, total);
|
||||||
|
const even = round1(safeTotal / count);
|
||||||
|
const tracks = Array.from({ length: count }, () => even);
|
||||||
|
const sum = tracks.reduce((a, b) => a + b, 0);
|
||||||
|
const diff = round1(safeTotal - sum);
|
||||||
|
if (tracks.length > 0 && diff !== 0) {
|
||||||
|
tracks[tracks.length - 1] = round1(Math.max(min, tracks[tracks.length - 1] + diff));
|
||||||
|
}
|
||||||
|
return tracks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 内边距变化后保持元素外框尺寸不变,按比例重算行高/列宽以适配新的内容区 */
|
||||||
export function expandTableForPadding(elem: TemplateElement): TemplateElement {
|
export function expandTableForPadding(elem: TemplateElement): TemplateElement {
|
||||||
if (elem.type !== 'table') return elem;
|
if (elem.type !== 'table') return elem;
|
||||||
const pad = resolvePaddingMm(elem);
|
const pad = resolvePaddingMm(elem);
|
||||||
const rowHeights = getTableRowHeights(elem);
|
const innerW = Math.max(1, elem.width - pad.left - pad.right);
|
||||||
const colWidths = getTableColWidths(elem);
|
const innerH = Math.max(1, elem.height - pad.top - pad.bottom);
|
||||||
return {
|
return {
|
||||||
...elem,
|
...elem,
|
||||||
tableRowHeights: rowHeights,
|
tableRowHeights: scaleTracksToTotal(getTableRowHeights(elem), innerH),
|
||||||
tableColWidths: colWidths,
|
tableColWidths: scaleTracksToTotal(getTableColWidths(elem), innerW),
|
||||||
height: round1(rowHeights.reduce((a, b) => a + b, 0) + pad.top + pad.bottom),
|
width: elem.width,
|
||||||
width: round1(colWidths.reduce((a, b) => a + b, 0) + pad.left + pad.right),
|
height: elem.height,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,6 +214,53 @@ export function getCellRectMm(
|
|||||||
return { x: round1(x), y: round1(y), width: round1(width), height: round1(height) };
|
return { x: round1(x), y: round1(y), width: round1(width), height: round1(height) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 表格行列像素边界(累积取整,避免单元格背景/选区出现缝隙) */
|
||||||
|
export function buildTableGridBoundariesPx(
|
||||||
|
elem: TemplateElement,
|
||||||
|
scale: number,
|
||||||
|
originX = 0,
|
||||||
|
originY = 0
|
||||||
|
): { rowBounds: number[]; colBounds: number[] } {
|
||||||
|
const pad = resolvePaddingMm(elem);
|
||||||
|
const rowHeights = getTableRowHeights(elem);
|
||||||
|
const colWidths = getTableColWidths(elem);
|
||||||
|
|
||||||
|
const rowBounds: number[] = [originY + Math.round(pad.top * scale)];
|
||||||
|
for (const h of rowHeights) {
|
||||||
|
rowBounds.push(rowBounds[rowBounds.length - 1] + Math.round(h * scale));
|
||||||
|
}
|
||||||
|
|
||||||
|
const colBounds: number[] = [originX + Math.round(pad.left * scale)];
|
||||||
|
for (const w of colWidths) {
|
||||||
|
colBounds.push(colBounds[colBounds.length - 1] + Math.round(w * scale));
|
||||||
|
}
|
||||||
|
|
||||||
|
return { rowBounds, colBounds };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单元格像素矩形,与 canvas 绘制及设计器选区 overlay 对齐 */
|
||||||
|
export function getCellRectPx(
|
||||||
|
elem: TemplateElement,
|
||||||
|
row: number,
|
||||||
|
col: number,
|
||||||
|
scale: number,
|
||||||
|
originX = 0,
|
||||||
|
originY = 0
|
||||||
|
): { x: number; y: number; width: number; height: number } {
|
||||||
|
const { rowBounds, colBounds } = buildTableGridBoundariesPx(elem, scale, originX, originY);
|
||||||
|
const anchor = getCellAnchor(elem, row, col);
|
||||||
|
const { rowSpan, colSpan } = getCellSpan(elem, anchor.row, anchor.col);
|
||||||
|
|
||||||
|
const x = colBounds[anchor.col];
|
||||||
|
const y = rowBounds[anchor.row];
|
||||||
|
return {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width: colBounds[anchor.col + colSpan] - x,
|
||||||
|
height: rowBounds[anchor.row + rowSpan] - y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function createDefaultTableElement(id: string, index: number): TemplateElement {
|
export function createDefaultTableElement(id: string, index: number): TemplateElement {
|
||||||
const rows = 3;
|
const rows = 3;
|
||||||
const cols = 3;
|
const cols = 3;
|
||||||
@@ -224,28 +302,6 @@ export function resizeTableGrid(
|
|||||||
): TemplateElement {
|
): TemplateElement {
|
||||||
const rows = Math.max(1, Math.min(50, newRows));
|
const rows = Math.max(1, Math.min(50, newRows));
|
||||||
const cols = Math.max(1, Math.min(50, newCols));
|
const cols = Math.max(1, Math.min(50, newCols));
|
||||||
const oldRows = getTableRows(elem);
|
|
||||||
const oldCols = getTableCols(elem);
|
|
||||||
const { width, height } = getTableInnerSizeMm(elem);
|
|
||||||
|
|
||||||
let rowHeights = getTableRowHeights(elem);
|
|
||||||
let colWidths = getTableColWidths(elem);
|
|
||||||
|
|
||||||
if (rows > oldRows) {
|
|
||||||
const added = rows - oldRows;
|
|
||||||
const avg = rowHeights.reduce((a, b) => a + b, 0) / oldRows;
|
|
||||||
rowHeights = [...rowHeights, ...Array.from({ length: added }, () => round1(avg))];
|
|
||||||
} else if (rows < oldRows) {
|
|
||||||
rowHeights = rowHeights.slice(0, rows);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cols > oldCols) {
|
|
||||||
const added = cols - oldCols;
|
|
||||||
const avg = colWidths.reduce((a, b) => a + b, 0) / oldCols;
|
|
||||||
colWidths = [...colWidths, ...Array.from({ length: added }, () => round1(avg))];
|
|
||||||
} else if (cols < oldCols) {
|
|
||||||
colWidths = colWidths.slice(0, cols);
|
|
||||||
}
|
|
||||||
|
|
||||||
const newCells: Record<string, TableCellData> = {};
|
const newCells: Record<string, TableCellData> = {};
|
||||||
if (elem.tableCells) {
|
if (elem.tableCells) {
|
||||||
@@ -259,15 +315,18 @@ export function resizeTableGrid(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pad = resolvePaddingMm(elem);
|
const pad = resolvePaddingMm(elem);
|
||||||
|
const innerW = Math.max(1, elem.width - pad.left - pad.right);
|
||||||
|
const innerH = Math.max(1, elem.height - pad.top - pad.bottom);
|
||||||
|
|
||||||
const next: TemplateElement = {
|
const next: TemplateElement = {
|
||||||
...elem,
|
...elem,
|
||||||
tableRows: rows,
|
tableRows: rows,
|
||||||
tableCols: cols,
|
tableCols: cols,
|
||||||
tableRowHeights: rowHeights,
|
tableRowHeights: distributeTracks(innerH, rows),
|
||||||
tableColWidths: colWidths,
|
tableColWidths: distributeTracks(innerW, cols),
|
||||||
tableCells: newCells,
|
tableCells: newCells,
|
||||||
width: round1(colWidths.reduce((a, b) => a + b, 0) + pad.left + pad.right),
|
width: elem.width,
|
||||||
height: round1(rowHeights.reduce((a, b) => a + b, 0) + pad.top + pad.bottom),
|
height: elem.height,
|
||||||
};
|
};
|
||||||
return sanitizeTableMerges(next);
|
return sanitizeTableMerges(next);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,6 +133,10 @@ export interface TemplateElement {
|
|||||||
locked?: boolean;
|
locked?: boolean;
|
||||||
/** 图片缩放方式,仅 type=image 时有效 */
|
/** 图片缩放方式,仅 type=image 时有效 */
|
||||||
imageFit?: 'contain' | 'cover' | 'fill';
|
imageFit?: 'contain' | 'cover' | 'fill';
|
||||||
|
/** 默认图片 URL 或 data URI;主图加载失败时使用,仅 type=image 时有效 */
|
||||||
|
defaultImage?: string;
|
||||||
|
/** 输出模式下内容值为空时仍渲染;条码/二维码/图片有效,默认 false */
|
||||||
|
showWhenContentEmpty?: boolean;
|
||||||
/** 文本显示格式,仅 type=text 时有效 */
|
/** 文本显示格式,仅 type=text 时有效 */
|
||||||
textFormat?: TextFormat;
|
textFormat?: TextFormat;
|
||||||
/** 数值/百分比/货币的小数位数,默认 2 */
|
/** 数值/百分比/货币的小数位数,默认 2 */
|
||||||
|
|||||||
12
src/utils.ts
12
src/utils.ts
@@ -524,7 +524,7 @@ export function extractTemplateVariables(template: {
|
|||||||
vars.add(v);
|
vars.add(v);
|
||||||
}
|
}
|
||||||
for (const rule of resolveVisibilityRules(elem)) {
|
for (const rule of resolveVisibilityRules(elem)) {
|
||||||
const name = rule.variable.trim();
|
const name = (rule.variable ?? '').trim();
|
||||||
if (name) vars.add(name);
|
if (name) vars.add(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -539,7 +539,7 @@ export function getUsedTemplateVariables(template: { elements: TemplateElement[]
|
|||||||
vars.add(v);
|
vars.add(v);
|
||||||
}
|
}
|
||||||
for (const rule of resolveVisibilityRules(elem)) {
|
for (const rule of resolveVisibilityRules(elem)) {
|
||||||
const name = rule.variable.trim();
|
const name = (rule.variable ?? '').trim();
|
||||||
if (name) vars.add(name);
|
if (name) vars.add(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -613,7 +613,7 @@ export function resolveVisibilityRules(elem: TemplateElement): VisibilityRule[]
|
|||||||
return elem.visibilityRules
|
return elem.visibilityRules
|
||||||
.map((r) => ({
|
.map((r) => ({
|
||||||
target: r.target ?? 'variable',
|
target: r.target ?? 'variable',
|
||||||
variable: r.variable.trim(),
|
variable: (r.variable ?? '').trim(),
|
||||||
operator: r.operator,
|
operator: r.operator,
|
||||||
value: r.value,
|
value: r.value,
|
||||||
}))
|
}))
|
||||||
@@ -1021,7 +1021,7 @@ export function resolveElementValue(
|
|||||||
return applyTextDisplayAffix(core, elem);
|
return applyTextDisplayAffix(core, elem);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 输出模式下条码/二维码/图片内容为空时不渲染 */
|
/** 输出模式下条码/二维码/图片内容为空时不渲染(除非 showWhenContentEmpty 为 true) */
|
||||||
export function isGraphicContentEmpty(
|
export function isGraphicContentEmpty(
|
||||||
elem: TemplateElement,
|
elem: TemplateElement,
|
||||||
value: string,
|
value: string,
|
||||||
@@ -1029,7 +1029,9 @@ export function isGraphicContentEmpty(
|
|||||||
): boolean {
|
): boolean {
|
||||||
if (mode === 'design') return false;
|
if (mode === 'design') return false;
|
||||||
if (elem.type !== 'barcode' && elem.type !== 'qrcode' && elem.type !== 'image') return false;
|
if (elem.type !== 'barcode' && elem.type !== 'qrcode' && elem.type !== 'image') return false;
|
||||||
return !value || !String(value).trim();
|
const empty = !value || !String(value).trim();
|
||||||
|
if (!empty) return false;
|
||||||
|
return !elem.showWhenContentEmpty;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate columns and rows based on paper dimensions, label dimensions, gaps, and margins
|
// Generate columns and rows based on paper dimensions, label dimensions, gaps, and margins
|
||||||
|
|||||||
Reference in New Issue
Block a user