export interface LabelLayoutConfig {
|
columns: number;
|
enabled: boolean;
|
horizontalGap: number;
|
itemsPerPage: number;
|
labelHeight: number;
|
labelWidth: number;
|
marginBottom: number;
|
marginLeft: number;
|
marginRight: number;
|
marginTop: number;
|
paperHeight: number;
|
paperWidth: number;
|
startOffset: number;
|
verticalGap: number;
|
}
|
|
export interface LabelSize {
|
height: number;
|
width: number;
|
}
|
|
export interface LabelLayoutResult {
|
html: string;
|
pageCount: number;
|
}
|
|
export const defaultLabelLayoutConfig: LabelLayoutConfig = {
|
columns: 1,
|
enabled: false,
|
horizontalGap: 0,
|
itemsPerPage: 1,
|
labelHeight: 0,
|
labelWidth: 0,
|
marginBottom: 0,
|
marginLeft: 0,
|
marginRight: 0,
|
marginTop: 0,
|
paperHeight: 210,
|
paperWidth: 297,
|
startOffset: 0,
|
verticalGap: 0,
|
};
|
|
const POSITIVE_INTEGER_KEYS = ['columns', 'itemsPerPage'] as const;
|
const NON_NEGATIVE_NUMBER_KEYS = ['horizontalGap', 'marginBottom', 'marginLeft', 'marginRight', 'marginTop', 'startOffset', 'verticalGap'] as const;
|
|
function toFiniteNumber(value: unknown, fallback: number) {
|
if (value === null || value === undefined || value === '') return fallback;
|
const number = Number(value);
|
return Number.isFinite(number) ? number : fallback;
|
}
|
|
export function normalizeLabelLayoutConfig(config?: Partial<LabelLayoutConfig>): LabelLayoutConfig {
|
const normalized = { ...defaultLabelLayoutConfig, ...config };
|
for (const key of POSITIVE_INTEGER_KEYS) {
|
normalized[key] = Math.trunc(toFiniteNumber(normalized[key], defaultLabelLayoutConfig[key]));
|
}
|
for (const key of NON_NEGATIVE_NUMBER_KEYS) {
|
normalized[key] = toFiniteNumber(normalized[key], defaultLabelLayoutConfig[key]);
|
}
|
normalized.paperHeight = toFiniteNumber(normalized.paperHeight, defaultLabelLayoutConfig.paperHeight);
|
normalized.paperWidth = toFiniteNumber(normalized.paperWidth, defaultLabelLayoutConfig.paperWidth);
|
normalized.labelHeight = toFiniteNumber(normalized.labelHeight, defaultLabelLayoutConfig.labelHeight);
|
normalized.labelWidth = toFiniteNumber(normalized.labelWidth, defaultLabelLayoutConfig.labelWidth);
|
normalized.startOffset = Math.trunc(normalized.startOffset);
|
normalized.enabled = normalized.enabled === true;
|
return normalized;
|
}
|
|
export function getLabelSize(printTemplate: any, config?: Partial<LabelLayoutConfig>): LabelSize {
|
const panels = printTemplate?.panels;
|
if (!Array.isArray(panels) || panels.length !== 1) throw new Error('标签拼版仅支持单面板打印模板');
|
|
const width = Number(config?.labelWidth) > 0 ? Number(config?.labelWidth) : Number(panels[0]?.width);
|
const height = Number(config?.labelHeight) > 0 ? Number(config?.labelHeight) : Number(panels[0]?.height);
|
if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(height) || height <= 0) {
|
throw new Error('无法读取标签模板尺寸,请检查打印模板纸张设置');
|
}
|
return { height, width };
|
}
|
|
export function validateLabelLayout(config: LabelLayoutConfig, labelSize: LabelSize, labelCount = config.itemsPerPage) {
|
if (!Number.isInteger(config.itemsPerPage) || config.itemsPerPage < 1) return '每页标签数必须为正整数';
|
if (!Number.isInteger(config.columns) || config.columns < 1) return '每行标签数必须为正整数';
|
if (config.columns > config.itemsPerPage) return '每行标签数不能大于每页标签数';
|
if (!Number.isInteger(config.startOffset) || config.startOffset < 0) return '起始留空数必须为非负整数';
|
if (config.startOffset >= config.itemsPerPage) return '起始留空数必须小于每页标签数';
|
if (config.paperWidth <= 0 || config.paperHeight <= 0) return '纸张宽高必须大于 0';
|
|
const spacingValues = [config.marginTop, config.marginRight, config.marginBottom, config.marginLeft, config.horizontalGap, config.verticalGap];
|
if (spacingValues.some((value) => !Number.isFinite(value) || value < 0)) return '页边距和标签间距不能小于 0';
|
|
const usedWidth = config.columns * labelSize.width + (config.columns - 1) * config.horizontalGap + config.marginLeft + config.marginRight;
|
if (usedWidth > config.paperWidth + 0.001) return '标签列宽、水平间距和左右边距之和超过纸张宽度';
|
|
const maximumSlots = Math.min(config.itemsPerPage, Math.max(1, labelCount)) + config.startOffset;
|
const rows = Math.ceil(maximumSlots / config.columns);
|
const usedHeight = rows * labelSize.height + (rows - 1) * config.verticalGap + config.marginTop + config.marginBottom;
|
if (usedHeight > config.paperHeight + 0.001) return '标签行高、垂直间距和上下边距之和超过纸张高度';
|
return '';
|
}
|
|
function appendRenderedHtml(container: HTMLElement, renderedHtml: unknown) {
|
if (typeof renderedHtml === 'string') {
|
const document = new DOMParser().parseFromString(renderedHtml, 'text/html');
|
for (const child of document.body.childNodes) container.append(child.cloneNode(true));
|
return;
|
}
|
if (renderedHtml instanceof Node) {
|
container.append(renderedHtml.cloneNode(true));
|
return;
|
}
|
|
const nodes = (renderedHtml as any)?.toArray?.() || (renderedHtml as any)?.get?.();
|
if (Array.isArray(nodes)) {
|
for (const node of nodes) {
|
if (node instanceof Node) container.append(node.cloneNode(true));
|
}
|
}
|
}
|
|
function createSlot(document: Document, paper?: Element) {
|
const slot = document.createElement('div');
|
slot.className = paper ? 'jnpf-label-slot' : 'jnpf-label-slot is-empty';
|
if (paper) slot.append(paper.cloneNode(true));
|
return slot;
|
}
|
|
export function buildLabelLayoutHtml(renderedHtml: unknown, printTemplate: any, inputConfig?: Partial<LabelLayoutConfig>): LabelLayoutResult {
|
if (typeof document === 'undefined' || typeof DOMParser === 'undefined') throw new Error('当前环境不支持标签拼版');
|
|
const config = normalizeLabelLayoutConfig(inputConfig);
|
const source = document.createElement('div');
|
appendRenderedHtml(source, renderedHtml);
|
const papers = [...source.querySelectorAll('.hiprint-printPaper')];
|
if (!papers.length) throw new Error('打印模板未生成有效标签内容');
|
|
const labelSize = getLabelSize(printTemplate, config);
|
const error = validateLabelLayout(config, labelSize, papers.length);
|
if (error) throw new Error(error);
|
|
const root = document.createElement('div');
|
root.className = 'jnpf-label-layout';
|
const pageCount = Math.ceil(papers.length / config.itemsPerPage);
|
|
for (let pageIndex = 0; pageIndex < pageCount; pageIndex++) {
|
const sheet = document.createElement('section');
|
sheet.className = 'jnpf-label-sheet';
|
sheet.style.width = `${config.paperWidth}mm`;
|
sheet.style.height = `${config.paperHeight}mm`;
|
sheet.style.padding = `${config.marginTop}mm ${config.marginRight}mm ${config.marginBottom}mm ${config.marginLeft}mm`;
|
sheet.style.gridTemplateColumns = `repeat(${config.columns}, ${labelSize.width}mm)`;
|
sheet.style.gridAutoRows = `${labelSize.height}mm`;
|
sheet.style.columnGap = `${config.horizontalGap}mm`;
|
sheet.style.rowGap = `${config.verticalGap}mm`;
|
|
if (pageIndex === 0) {
|
for (let index = 0; index < config.startOffset; index++) sheet.append(createSlot(document));
|
}
|
const pagePapers = papers.slice(pageIndex * config.itemsPerPage, (pageIndex + 1) * config.itemsPerPage);
|
for (const paper of pagePapers) sheet.append(createSlot(document, paper));
|
root.append(sheet);
|
}
|
|
const style = document.createElement('style');
|
style.textContent = `
|
@page { size: ${config.paperWidth}mm ${config.paperHeight}mm; margin: 0; }
|
.jnpf-label-layout { margin: 0; padding: 0; }
|
.jnpf-label-sheet {
|
box-sizing: border-box;
|
display: grid;
|
align-content: start;
|
justify-content: start;
|
margin: 10px auto;
|
overflow: hidden;
|
background: #fff;
|
break-after: page;
|
page-break-after: always;
|
}
|
.jnpf-label-sheet:last-child { break-after: auto; page-break-after: auto; }
|
.jnpf-label-slot { box-sizing: border-box; overflow: hidden; }
|
.jnpf-label-slot > .hiprint-printPaper {
|
margin: 0 !important;
|
break-after: auto !important;
|
page-break-after: auto !important;
|
}
|
.jnpf-label-slot.is-empty { visibility: hidden; }
|
@media print {
|
.jnpf-label-sheet { margin: 0; }
|
}
|
`;
|
root.prepend(style);
|
return { html: root.outerHTML, pageCount };
|
}
|