优化预览与导出排版
This commit is contained in:
67
src/components/CommitInput.tsx
Normal file
67
src/components/CommitInput.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
|
||||
export interface CommitInputProps
|
||||
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value' | 'type'> {
|
||||
value: string | number;
|
||||
onCommit: (val: string) => void;
|
||||
/** number 类型在编辑时用文本框,提交时由外部校验 min/max */
|
||||
type?: 'text' | 'number';
|
||||
}
|
||||
|
||||
/**
|
||||
* 失焦/回车提交型输入框:编辑过程中不应用 HTML min/max 限制,便于清空后重输。
|
||||
*/
|
||||
export const CommitInput: React.FC<CommitInputProps> = ({
|
||||
value,
|
||||
onCommit,
|
||||
type = 'text',
|
||||
min: _min,
|
||||
max: _max,
|
||||
step: _step,
|
||||
onFocus,
|
||||
onBlur,
|
||||
onKeyDown,
|
||||
...props
|
||||
}) => {
|
||||
const [localVal, setLocalVal] = useState<string>(String(value));
|
||||
const focusedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!focusedRef.current) {
|
||||
setLocalVal(String(value));
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||
focusedRef.current = false;
|
||||
onBlur?.(e);
|
||||
if (localVal !== String(value)) {
|
||||
onCommit(localVal);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
onKeyDown?.(e);
|
||||
};
|
||||
|
||||
const inputType = type === 'number' ? 'text' : type;
|
||||
|
||||
return (
|
||||
<input
|
||||
{...props}
|
||||
type={inputType}
|
||||
inputMode={type === 'number' ? 'decimal' : props.inputMode}
|
||||
value={localVal}
|
||||
onChange={(e) => setLocalVal(e.target.value)}
|
||||
onFocus={(e) => {
|
||||
focusedRef.current = true;
|
||||
onFocus?.(e);
|
||||
}}
|
||||
onBlur={handleBlur}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import { Database, Layout, Settings } from 'lucide-react';
|
||||
import { ImportData, PaperConfig, LabelTemplate, RecordRow, ExportRangeConfig, CutLineConfig } from '../types';
|
||||
import { DataImporter } from './DataImporter';
|
||||
import { VariableMappingPanel } from './VariableMappingPanel';
|
||||
import { PaperSettingsPanel } from './PreviewGrid';
|
||||
import { PaperSettingsPanel, PageInput } from './PreviewGrid';
|
||||
import { CollapsiblePanelSection } from './CollapsiblePanelSection';
|
||||
import { DataRangeFields } from './DataRangeFields';
|
||||
|
||||
@@ -236,19 +236,19 @@ export const ExportSidebar: React.FC<ExportSidebarProps> = ({
|
||||
</div>
|
||||
<div>
|
||||
<label className="ps-field-label">线粗细 (mm)</label>
|
||||
<input
|
||||
<PageInput
|
||||
type="number"
|
||||
min="0.02"
|
||||
max="2"
|
||||
step="0.02"
|
||||
value={cutLine.width}
|
||||
onChange={(e) =>
|
||||
onCommit={(val) =>
|
||||
onChangeCutLine({
|
||||
...cutLine,
|
||||
width: Math.max(0.02, Math.min(2, Number(e.target.value) || 0.1)),
|
||||
width: Math.max(0.02, Math.min(2, Number(val) || 0.1)),
|
||||
})
|
||||
}
|
||||
className="ps-field font-mono text-[11px]"
|
||||
inputClassName="ps-field font-mono text-[11px]"
|
||||
/>
|
||||
<div
|
||||
className="mt-2 h-8 rounded border border-[#3a3a3a] bg-[#1e1e1e] flex items-center justify-center"
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
PaperConfig,
|
||||
RecordRow,
|
||||
} from '../types';
|
||||
import { PaperSettingsPanel } from './PreviewGrid';
|
||||
import { PaperSettingsPanel, PageInput } from './PreviewGrid';
|
||||
|
||||
interface MobileExportPropsPanelProps {
|
||||
template: LabelTemplate;
|
||||
@@ -115,19 +115,19 @@ export const MobileExportPropsPanel: React.FC<MobileExportPropsPanelProps> = ({
|
||||
</div>
|
||||
<div>
|
||||
<label className="mobile-field-label">线粗细 (mm)</label>
|
||||
<input
|
||||
<PageInput
|
||||
type="number"
|
||||
min="0.02"
|
||||
max="2"
|
||||
step="0.02"
|
||||
value={cutLine.width}
|
||||
onChange={(e) =>
|
||||
onCommit={(val) =>
|
||||
onChangeCutLine({
|
||||
...cutLine,
|
||||
width: Math.max(0.02, Math.min(2, Number(e.target.value) || 0.1)),
|
||||
width: Math.max(0.02, Math.min(2, Number(val) || 0.1)),
|
||||
})
|
||||
}
|
||||
className="mobile-field font-mono"
|
||||
inputClassName="mobile-field font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -4,9 +4,12 @@ import {
|
||||
DEFAULT_EXPORT_RANGE,
|
||||
DEFAULT_CUT_LINE_CONFIG,
|
||||
buildPrintablePages,
|
||||
collectGridCutLinePositions,
|
||||
getCutLineChannel,
|
||||
type PrintableLabel,
|
||||
} from '../utils';
|
||||
import { CanvasLabelImage } from './CanvasLabelImage';
|
||||
import { CommitInput } from './CommitInput';
|
||||
import { FileDown, RefreshCcw } from 'lucide-react';
|
||||
|
||||
interface PageInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> {
|
||||
@@ -15,36 +18,14 @@ interface PageInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement
|
||||
inputClassName?: string;
|
||||
}
|
||||
|
||||
export const PageInput: React.FC<PageInputProps> = ({ value, onCommit, inputClassName = '', ...props }) => {
|
||||
const [localVal, setLocalVal] = useState<string>(String(value));
|
||||
|
||||
useEffect(() => {
|
||||
setLocalVal(String(value));
|
||||
}, [value]);
|
||||
|
||||
const handleBlur = () => {
|
||||
if (localVal !== String(value)) {
|
||||
onCommit(localVal);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<input
|
||||
{...props}
|
||||
value={localVal}
|
||||
onChange={(e) => setLocalVal(e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={inputClassName}
|
||||
/>
|
||||
export const PageInput: React.FC<PageInputProps> = ({
|
||||
value,
|
||||
onCommit,
|
||||
inputClassName = '',
|
||||
...props
|
||||
}) => (
|
||||
<CommitInput value={value} onCommit={onCommit} className={inputClassName} {...props} />
|
||||
);
|
||||
};
|
||||
|
||||
interface LayoutStats {
|
||||
gridCols: number;
|
||||
@@ -176,7 +157,7 @@ export const PaperSettingsPanel: React.FC<PaperSettingsPanelProps> = ({
|
||||
|
||||
const applyPaper = (next: PaperConfig) => {
|
||||
setDraftPaper(next);
|
||||
if (isMobile) onChangePaper(next);
|
||||
onChangePaper(next);
|
||||
};
|
||||
|
||||
const handlePaperTypeChange = (type: PaperType) => {
|
||||
@@ -213,7 +194,7 @@ export const PaperSettingsPanel: React.FC<PaperSettingsPanelProps> = ({
|
||||
)}
|
||||
{isPs && (
|
||||
<p className="text-[10px] text-[#666] leading-relaxed">
|
||||
修改后点击面板外区域或切换焦点,预览将自动更新。
|
||||
排版参数修改后将自动更新预览。
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -465,6 +446,31 @@ export const LayoutPreview: React.FC<LayoutPreviewProps> = ({
|
||||
const currentPaperH = paper.orientation === 'portrait' ? paper.height : paper.width;
|
||||
const colGapPx = Math.max(0, paper.columnGap * previewScale);
|
||||
const rowGapPx = Math.max(0, paper.rowGap * previewScale);
|
||||
const labelWPx = template.width * previewScale;
|
||||
const labelHPx = template.height * previewScale;
|
||||
const cutStrokeWidth = Math.max(0.5, cutLine.width * previewScale);
|
||||
const cutHalfStroke = cutStrokeWidth / 2;
|
||||
const cutChannel = getCutLineChannel(paper.columnGap, paper.rowGap, cutLine);
|
||||
const cutChannelXPx = cutChannel.x * previewScale;
|
||||
const cutChannelYPx = cutChannel.y * previewScale;
|
||||
const cutDash =
|
||||
cutLine.style === 'dashed'
|
||||
? `${Math.max(2, cutStrokeWidth * 4)} ${Math.max(2, cutStrokeWidth * 4)}`
|
||||
: undefined;
|
||||
const pageCutLines = useMemo(
|
||||
() =>
|
||||
collectGridCutLinePositions(
|
||||
0,
|
||||
0,
|
||||
labelWPx,
|
||||
labelHPx,
|
||||
gridInfo.cols,
|
||||
gridInfo.rows,
|
||||
colGapPx,
|
||||
rowGapPx
|
||||
),
|
||||
[labelWPx, labelHPx, gridInfo.cols, gridInfo.rows, colGapPx, rowGapPx]
|
||||
);
|
||||
const visiblePages = showAllPages ? printablePages : printablePages.slice(0, MAX_PREVIEW_PAGES);
|
||||
const hasMorePages = printablePages.length > MAX_PREVIEW_PAGES;
|
||||
|
||||
@@ -587,15 +593,58 @@ export const LayoutPreview: React.FC<LayoutPreviewProps> = ({
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="grid bg-white"
|
||||
className="grid bg-white relative"
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${gridInfo.cols}, ${template.width * previewScale}px)`,
|
||||
gridAutoRows: `${template.height * previewScale}px`,
|
||||
gridTemplateColumns: `repeat(${gridInfo.cols}, ${labelWPx}px)`,
|
||||
gridAutoRows: `${labelHPx}px`,
|
||||
columnGap: `${colGapPx}px`,
|
||||
rowGap: `${rowGapPx}px`,
|
||||
width: 'max-content',
|
||||
}}
|
||||
>
|
||||
{cutLine.enabled && (
|
||||
<svg
|
||||
className="absolute pointer-events-none z-20"
|
||||
style={{
|
||||
left: -cutHalfStroke,
|
||||
top: -cutHalfStroke,
|
||||
width: pageCutLines.gridW + cutStrokeWidth,
|
||||
height: pageCutLines.gridH + cutStrokeWidth,
|
||||
overflow: 'visible',
|
||||
}}
|
||||
viewBox={`${-cutHalfStroke} ${-cutHalfStroke} ${pageCutLines.gridW + cutStrokeWidth} ${pageCutLines.gridH + cutStrokeWidth}`}
|
||||
aria-hidden
|
||||
>
|
||||
{pageCutLines.ys.map((y) => (
|
||||
<line
|
||||
key={`h-${y}`}
|
||||
x1={0}
|
||||
y1={y}
|
||||
x2={pageCutLines.gridW}
|
||||
y2={y}
|
||||
stroke={cutLine.color}
|
||||
strokeWidth={cutStrokeWidth}
|
||||
strokeDasharray={cutDash}
|
||||
strokeLinecap="butt"
|
||||
shapeRendering="crispEdges"
|
||||
/>
|
||||
))}
|
||||
{pageCutLines.xs.map((x) => (
|
||||
<line
|
||||
key={`v-${x}`}
|
||||
x1={x}
|
||||
y1={0}
|
||||
x2={x}
|
||||
y2={pageCutLines.gridH}
|
||||
stroke={cutLine.color}
|
||||
strokeWidth={cutStrokeWidth}
|
||||
strokeDasharray={cutDash}
|
||||
strokeLinecap="butt"
|
||||
shapeRendering="crispEdges"
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
)}
|
||||
{pageRows.map((item, cellIdx) => {
|
||||
if (!item) {
|
||||
return (
|
||||
@@ -616,13 +665,16 @@ export const LayoutPreview: React.FC<LayoutPreviewProps> = ({
|
||||
<div
|
||||
key={cellIdx}
|
||||
onClick={() => onSelectRowIndex(item.sourceIndex)}
|
||||
className={`bg-white text-center relative overflow-hidden flex flex-col justify-center cursor-pointer box-border ${isActive ? 'ring-2 ring-inset ring-[#31a8ff] z-10' : ''
|
||||
className={`bg-white text-center relative flex flex-col justify-center cursor-pointer box-border ${isActive ? 'ring-2 ring-inset ring-[#31a8ff] z-10' : ''
|
||||
}`}
|
||||
style={{
|
||||
width: `${template.width * previewScale}px`,
|
||||
height: `${template.height * previewScale}px`,
|
||||
width: `${labelWPx}px`,
|
||||
height: `${labelHPx}px`,
|
||||
padding: `${cutChannelYPx}px ${cutChannelXPx}px`,
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
>
|
||||
<div className="w-full h-full overflow-hidden relative">
|
||||
<CanvasLabelImage
|
||||
widthMm={template.width}
|
||||
heightMm={template.height}
|
||||
@@ -632,14 +684,7 @@ export const LayoutPreview: React.FC<LayoutPreviewProps> = ({
|
||||
showBorder={false}
|
||||
mode="output"
|
||||
/>
|
||||
{cutLine.enabled && (
|
||||
<div
|
||||
className="absolute inset-0 pointer-events-none box-border"
|
||||
style={{
|
||||
border: `${Math.max(1, cutLine.width * previewScale)}px ${cutLine.style} ${cutLine.color}`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from '../elementAlign';
|
||||
import type { ElementSide } from '../types';
|
||||
import { CollapsiblePanelSection } from './CollapsiblePanelSection';
|
||||
import { CommitInput } from './CommitInput';
|
||||
import {
|
||||
Sliders,
|
||||
LayoutGrid,
|
||||
@@ -90,40 +91,7 @@ const AlignToolButton: React.FC<AlignToolButtonProps> = ({ icon, label, title, o
|
||||
</button>
|
||||
);
|
||||
|
||||
interface PropInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'> {
|
||||
value: string | number;
|
||||
onCommit: (val: string) => void;
|
||||
}
|
||||
|
||||
const PropInput: React.FC<PropInputProps> = ({ value, onCommit, ...props }) => {
|
||||
const [localVal, setLocalVal] = useState<string>(String(value));
|
||||
|
||||
useEffect(() => {
|
||||
setLocalVal(String(value));
|
||||
}, [value]);
|
||||
|
||||
const handleBlur = () => {
|
||||
if (localVal !== String(value)) {
|
||||
onCommit(localVal);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<input
|
||||
{...props}
|
||||
value={localVal}
|
||||
onChange={(e) => setLocalVal(e.target.value)}
|
||||
onBlur={handleBlur}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const PropInput = CommitInput;
|
||||
|
||||
interface PropTextareaProps extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'onChange'> {
|
||||
value: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { jsPDF } from 'jspdf';
|
||||
import type { PaperConfig, LabelTemplate, CutLineConfig } from './types';
|
||||
import type { PrintableLabel } from './utils';
|
||||
import { collectGridCutLinePositions, getCutLineChannel, type PrintableLabel } from './utils';
|
||||
|
||||
export interface PdfExportProgress {
|
||||
done: number;
|
||||
@@ -48,24 +48,61 @@ function parseColorToRgb(color: string): [number, number, number] {
|
||||
return [204, 204, 204];
|
||||
}
|
||||
|
||||
function drawCutLineRect(
|
||||
pdf: jsPDF,
|
||||
x: number,
|
||||
y: number,
|
||||
w: number,
|
||||
h: number,
|
||||
cutLine: CutLineConfig
|
||||
) {
|
||||
function setupCutLinePen(pdf: jsPDF, cutLine: CutLineConfig) {
|
||||
const [r, g, b] = parseColorToRgb(cutLine.color);
|
||||
pdf.setDrawColor(r, g, b);
|
||||
pdf.setLineWidth(Math.max(0.02, cutLine.width));
|
||||
pdf.setLineWidth(cutLine.width);
|
||||
if (cutLine.style === 'dashed') {
|
||||
const dash = Math.max(0.5, cutLine.width * 4);
|
||||
pdf.setLineDashPattern([dash, dash], 0);
|
||||
} else {
|
||||
pdf.setLineDashPattern([], 0);
|
||||
}
|
||||
pdf.rect(x, y, w, h, 'S');
|
||||
}
|
||||
|
||||
function drawPageGridCutLines(
|
||||
pdf: jsPDF,
|
||||
paper: PaperConfig,
|
||||
labelW: number,
|
||||
labelH: number,
|
||||
gridCols: number,
|
||||
gridRows: number,
|
||||
cutLine: CutLineConfig
|
||||
) {
|
||||
const originX = paper.marginLeft;
|
||||
const originY = paper.marginTop;
|
||||
const lineW = Math.max(0.02, cutLine.width);
|
||||
const { xs, ys, gridW, gridH } = collectGridCutLinePositions(
|
||||
originX,
|
||||
originY,
|
||||
labelW,
|
||||
labelH,
|
||||
gridCols,
|
||||
gridRows,
|
||||
paper.columnGap,
|
||||
paper.rowGap
|
||||
);
|
||||
|
||||
const [r, g, b] = parseColorToRgb(cutLine.color);
|
||||
pdf.setFillColor(r, g, b);
|
||||
|
||||
if (cutLine.style === 'solid') {
|
||||
for (const y of ys) {
|
||||
pdf.rect(originX, y - lineW / 2, gridW, lineW, 'F');
|
||||
}
|
||||
for (const x of xs) {
|
||||
pdf.rect(x - lineW / 2, originY, lineW, gridH, 'F');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setupCutLinePen(pdf, cutLine);
|
||||
for (const y of ys) {
|
||||
pdf.line(originX, y, originX + gridW, y);
|
||||
}
|
||||
for (const x of xs) {
|
||||
pdf.line(x, originY, x, originY + gridH);
|
||||
}
|
||||
pdf.setLineDashPattern([], 0);
|
||||
}
|
||||
|
||||
@@ -115,6 +152,8 @@ export async function exportLabelsPdfAsync(options: AsyncExportLabelsPdfOptions)
|
||||
}
|
||||
|
||||
const pageRows = printablePages[pIdx];
|
||||
const gridRows = Math.ceil(pageRows.length / gridCols);
|
||||
const cutChannel = getCutLineChannel(paper.columnGap, paper.rowGap, cutLine);
|
||||
report(pIdx + 1, 'render');
|
||||
|
||||
const pageJobs: { cellIdx: number; label: PrintableLabel }[] = [];
|
||||
@@ -136,14 +175,12 @@ export async function exportLabelsPdfAsync(options: AsyncExportLabelsPdfOptions)
|
||||
for (const { cellIdx, dataUrl } of rendered) {
|
||||
const col = cellIdx % gridCols;
|
||||
const row = Math.floor(cellIdx / gridCols);
|
||||
const x = paper.marginLeft + col * (labelW + paper.columnGap);
|
||||
const y = paper.marginTop + row * (labelH + paper.rowGap);
|
||||
const x = paper.marginLeft + col * (labelW + paper.columnGap) + cutChannel.x;
|
||||
const y = paper.marginTop + row * (labelH + paper.rowGap) + cutChannel.y;
|
||||
const drawW = labelW - 2 * cutChannel.x;
|
||||
const drawH = labelH - 2 * cutChannel.y;
|
||||
|
||||
pdf.addImage(dataUrl, imageFormat, x, y, labelW, labelH, undefined, 'FAST');
|
||||
|
||||
if (cutLine.enabled) {
|
||||
drawCutLineRect(pdf, x, y, labelW, labelH, cutLine);
|
||||
}
|
||||
pdf.addImage(dataUrl, imageFormat, x, y, drawW, drawH, undefined, 'FAST');
|
||||
|
||||
done++;
|
||||
}
|
||||
@@ -156,6 +193,10 @@ export async function exportLabelsPdfAsync(options: AsyncExportLabelsPdfOptions)
|
||||
}
|
||||
}
|
||||
|
||||
if (cutLine.enabled && pageRows.length > 0) {
|
||||
drawPageGridCutLines(pdf, paper, labelW, labelH, gridCols, gridRows, cutLine);
|
||||
}
|
||||
|
||||
report(pIdx + 1, 'render');
|
||||
await yieldToMain();
|
||||
}
|
||||
|
||||
54
src/utils.ts
54
src/utils.ts
@@ -151,6 +151,60 @@ export const DEFAULT_CUT_LINE_CONFIG: CutLineConfig = {
|
||||
color: '#cccccc',
|
||||
};
|
||||
|
||||
export interface GridCutLinePositions {
|
||||
xs: number[];
|
||||
ys: number[];
|
||||
gridW: number;
|
||||
gridH: number;
|
||||
}
|
||||
|
||||
/** 收集网格裁切线坐标(去重),每条线只绘制一次,避免转角/外缘叠线变粗 */
|
||||
export function collectGridCutLinePositions(
|
||||
originX: number,
|
||||
originY: number,
|
||||
labelW: number,
|
||||
labelH: number,
|
||||
gridCols: number,
|
||||
gridRows: number,
|
||||
columnGap: number,
|
||||
rowGap: number
|
||||
): GridCutLinePositions {
|
||||
const gridW = gridCols * labelW + Math.max(0, gridCols - 1) * columnGap;
|
||||
const gridH = gridRows * labelH + Math.max(0, gridRows - 1) * rowGap;
|
||||
const xs = new Set<number>();
|
||||
const ys = new Set<number>();
|
||||
|
||||
for (let c = 0; c < gridCols; c++) {
|
||||
xs.add(originX + c * (labelW + columnGap));
|
||||
xs.add(originX + c * (labelW + columnGap) + labelW);
|
||||
}
|
||||
for (let r = 0; r < gridRows; r++) {
|
||||
ys.add(originY + r * (labelH + rowGap));
|
||||
ys.add(originY + r * (labelH + rowGap) + labelH);
|
||||
}
|
||||
|
||||
return {
|
||||
xs: [...xs].sort((a, b) => a - b),
|
||||
ys: [...ys].sort((a, b) => a - b),
|
||||
gridW,
|
||||
gridH,
|
||||
};
|
||||
}
|
||||
|
||||
/** 间距为 0 且启用裁切线时,为裁切线预留半线宽通道,避免与标签图重叠导致内外粗细不一 */
|
||||
export function getCutLineChannel(
|
||||
columnGap: number,
|
||||
rowGap: number,
|
||||
cutLine: { enabled: boolean; width: number }
|
||||
): { x: number; y: number } {
|
||||
if (!cutLine.enabled) return { x: 0, y: 0 };
|
||||
const half = cutLine.width / 2;
|
||||
return {
|
||||
x: columnGap === 0 ? half : 0,
|
||||
y: rowGap === 0 ? half : 0,
|
||||
};
|
||||
}
|
||||
|
||||
export interface PrintableLabel {
|
||||
row: RecordRow;
|
||||
/** 原始数据行索引(0-based),用于变量解析 */
|
||||
|
||||
Reference in New Issue
Block a user