feat(barcode-print): add size template and keep package codes on one line

This commit is contained in:
WuYongJie 2026-09-11 17:08:02 +08:00
parent 02ad38908e
commit 1b3f210e0f
6 changed files with 571 additions and 61 deletions

View File

@ -49,12 +49,22 @@ describe("splitBarcodeParts", () => {
});
describe("second barcode template", () => {
it("uses the legacy fixed layout with whole-page scaling", async () => {
beforeEach(() => {
jest.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue({
scale: jest.fn(), fillText: jest.fn(), measureText: (text: string) => ({ width: text.length * 10 }),
} as never);
jest.spyOn(HTMLCanvasElement.prototype, "toDataURL").mockReturnValue("data:image/png;base64,dGVzdA==");
});
afterEach(() => jest.restoreAllMocks());
it("prints ordinary segments as one image while retaining whole-page scaling", async () => {
const lodop = {
PRINT_INIT: jest.fn(),
SET_PRINT_MODE: jest.fn(),
NEWPAGE: jest.fn(),
ADD_PRINT_BARCODE: jest.fn(),
ADD_PRINT_IMAGE: jest.fn(),
ADD_PRINT_TEXT: jest.fn(),
ADD_PRINT_RECT: jest.fn(),
SET_PRINT_STYLEA: jest.fn(),
@ -82,24 +92,10 @@ describe("second barcode template", () => {
}
expect(lodop.SET_PRINT_MODE).toHaveBeenCalledWith("PRINT_PAGE_PERCENT", "Full-Page");
expect(lodop.ADD_PRINT_TEXT).toHaveBeenCalledWith(203, 191, 150, 25, "CJ");
expect(lodop.ADD_PRINT_TEXT).toHaveBeenCalledWith(186, 274, 230, 52, "026601");
expect(lodop.ADD_PRINT_TEXT).toHaveBeenCalledWith(203, 474, 141, 25, "N1280V01");
const prefixCallIndex = lodop.ADD_PRINT_TEXT.mock.calls.findIndex(
(call) => call[4] === "CJ",
);
const prefixCallOrder = lodop.ADD_PRINT_TEXT.mock.invocationCallOrder[prefixCallIndex];
const prefixAlignmentCallIndex = lodop.SET_PRINT_STYLEA.mock.calls.findIndex(
(call, index) =>
lodop.SET_PRINT_STYLEA.mock.invocationCallOrder[index] > prefixCallOrder &&
call[1] === "Alignment",
);
expect(lodop.SET_PRINT_STYLEA.mock.calls[prefixAlignmentCallIndex]).toEqual([
0,
"Alignment",
2,
]);
expect(lodop.ADD_PRINT_IMAGE).toHaveBeenCalledWith(186, 211, 387, 52,
'<img border="0" src="data:image/png;base64,dGVzdA==" />');
expect(lodop.SET_PRINT_STYLEA).toHaveBeenCalledWith(0, "Stretch", 2);
expect(lodop.ADD_PRINT_TEXT.mock.calls.some(call => ["CJ", "026601", "N1280V01"].includes(call[4]))).toBe(false);
});
it("prints modular suffixes on one row with the final five digits in the small style", async () => {

View File

@ -3,6 +3,8 @@
import { pinyin } from "pinyin-pro";
import type { BarcodePrintRow } from "./barcodePrintRows";
import { splitGujiaBarcodeParts } from "./barcodePrintGujia";
import { BARCODE_PRINT_THIRD_TEMPLATE } from "./barcodePrintThirdTemplate";
import { printSplitBarcodeText } from "./barcodePrintSplitCode";
import {
SOFIA_CERTIFICATE_FIELD_FONT_SIZE,
SOFIA_CERTIFICATE_FIELD_LEFT,
@ -12,7 +14,7 @@ import {
type LodopValue = string | number;
type Lodop = {
export type Lodop = {
PRINT_INIT(title: string): void;
SET_PRINT_PAGESIZE?(orientation: number, width: string, height: string, name: string): void;
NEWPAGE(): void;
@ -59,10 +61,11 @@ declare global {
}
}
type BarcodeLodopRecord = {
export type BarcodeLodopRecord = {
barcode: string;
factoryNumber: string;
skuName1: string;
size: string;
productCategory: string;
hangtagMaterial: string;
executionStandard: string;
@ -85,7 +88,7 @@ type CertificateLodopRecord = {
supplierName: string;
};
type BarcodePrintTemplate = {
export type BarcodePrintTemplate = {
title: string;
useFullPagePercent?: boolean;
printFunc: (
@ -243,6 +246,7 @@ function buildLodopRecord(row: BarcodePrintRow): BarcodeLodopRecord {
barcode,
factoryNumber: trimText(row.factoryModel),
skuName1: `${buildColorCode(row.color)}${size}`,
size,
productCategory: readLeafCategory(row.productCategory),
hangtagMaterial: trimText(row.tagMaterial || row.material),
executionStandard: trimText(row.executeStandard),
@ -350,16 +354,6 @@ function printCommonLayout(
barcodeTextHeight?: number;
barcodeFontSize?: number;
productionBatchLeft?: number;
splitCodePrefixLeft?: number;
splitCodePrefixWidth?: number;
splitCodeCenterTop?: number;
splitCodeCenterLeft?: number;
splitCodeCenterWidth?: number;
splitCodeCenterHeight?: number;
splitCodeCenterFontSize?: number;
splitCodeSuffixLeft?: number;
splitCodeSuffixWidth?: number;
splitCodeSuffixFontSize?: number;
},
) {
lodop.PRINT_INIT("条码打印");
@ -380,35 +374,7 @@ function printCommonLayout(
setFont(lodop, 15);
if (options?.splitCode) {
const isModular13DigitSplit =
Boolean(record.catcode) && /^\d{8}$/.test(record.bar2) && /^\d{5}$/.test(record.bar3);
const splitCodePrefixLeft = options?.splitCodePrefixLeft ?? (isModular13DigitSplit ? 207 : 191);
const splitCodePrefixWidth = options?.splitCodePrefixWidth ?? (isModular13DigitSplit ? 120 : 150);
const splitCodePrefixAlignment = isModular13DigitSplit ? 1 : 2;
const splitCodeCenterTop = options?.splitCodeCenterTop ?? 186;
const splitCodeCenterLeft = options?.splitCodeCenterLeft ?? (isModular13DigitSplit ? 290 : 274);
const splitCodeCenterWidth = options?.splitCodeCenterWidth ?? 230;
const splitCodeCenterHeight = options?.splitCodeCenterHeight ?? 52;
const splitCodeCenterFontSize = options?.splitCodeCenterFontSize ?? 28;
const splitCodeSuffixLeft = options?.splitCodeSuffixLeft ?? (isModular13DigitSplit ? 482 : 474);
const splitCodeSuffixWidth = options?.splitCodeSuffixWidth ?? 141;
const splitCodeSuffixFontSize = options?.splitCodeSuffixFontSize ?? 15;
lodop.ADD_PRINT_TEXT(203, splitCodePrefixLeft, splitCodePrefixWidth, 25, record.catcode);
setFont(lodop, 15);
lodop.SET_PRINT_STYLEA(0, "Alignment", splitCodePrefixAlignment);
lodop.ADD_PRINT_TEXT(
splitCodeCenterTop,
splitCodeCenterLeft,
splitCodeCenterWidth,
splitCodeCenterHeight,
record.bar2,
);
setFont(lodop, splitCodeCenterFontSize, true);
lodop.SET_PRINT_STYLEA(0, "Alignment", 2);
lodop.ADD_PRINT_TEXT(203, splitCodeSuffixLeft, splitCodeSuffixWidth, 25, record.bar3);
setFont(lodop, splitCodeSuffixFontSize);
lodop.SET_PRINT_STYLEA(0, "Alignment", 2);
printSplitBarcodeText(record, lodop);
} else {
// Keep the full barcode on a single row and shrink text when needed.
const barcodeTextHeight = options?.barcodeTextHeight ?? 25;
@ -446,6 +412,9 @@ const BARCODE_PRINT_TEMPLATES: BarcodePrintTemplate[] = [
});
},
},
{
...BARCODE_PRINT_THIRD_TEMPLATE,
},
{
title: "外贸条码打印",
printFunc(record, lodop) {

View File

@ -0,0 +1,233 @@
import { __barcodePrintTestUtils, printBarcodeRow, type Lodop } from "./barcodePrintLodop";
import type { BarcodePrintRow } from "./barcodePrintRows";
import { printSplitBarcodeText } from "./barcodePrintSplitCode";
type CanvasText = { text: string; font: string; x: number; y: number; alignment: string; baseline: string };
type PrintedText = { top: number; left: number; width: number; height: number; value: string };
const PNG_DATA_URL = "data:image/png;base64,c2luZ2xlLWxpbmU=";
const TEMPLATES = ["条码打印_第二套模板", "条码打印-第三套模板(尺寸信息)"];
const COLUMNS = [{ left: 0, width: 114 }, { left: 120, width: 160 }, { left: 286, width: 101 }];
function fontSizePt(font: string) {
return Number(font.match(/([\d.]+)px/)?.[1]) * 72 / 96;
}
function measureWidth(text: string, font: string) {
const emWidth = Array.from(text).reduce((sum, char) => sum + (/[MW]/.test(char) ? 0.95 : 0.62), 0);
return emWidth * fontSizePt(font) * 96 / 72 * (font.startsWith("bold") ? 1.05 : 1);
}
function createCanvasRecorder() {
const canvases: HTMLCanvasElement[] = [];
const draws: CanvasText[] = [];
const measurements: Array<{ text: string; font: string }> = [];
const context = {
font: "", textAlign: "start", textBaseline: "alphabetic", fillStyle: "#000000",
scale: jest.fn(), setTransform: jest.fn(), clearRect: jest.fn(), fillRect: jest.fn(),
measureText(text: string) {
measurements.push({ text, font: this.font });
const width = measureWidth(text, this.font);
return {
width,
actualBoundingBoxLeft: this.textAlign === "center" ? width / 2 : 0,
actualBoundingBoxRight: this.textAlign === "center" ? width / 2 : width,
};
},
fillText(text: string, x: number, y: number) {
draws.push({ text, font: this.font, x, y, alignment: this.textAlign, baseline: this.textBaseline });
},
};
const getContext = jest.spyOn(HTMLCanvasElement.prototype, "getContext").mockImplementation(function (
this: HTMLCanvasElement,
) {
canvases.push(this);
return context as never;
});
const toDataURL = jest.spyOn(HTMLCanvasElement.prototype, "toDataURL").mockReturnValue(PNG_DATA_URL);
return { canvases, draws, measurements, context, getContext, toDataURL };
}
function createLodopRecorder() {
const texts: PrintedText[] = [];
const calls: Array<{ method: string; args: unknown[] }> = [];
const record = (method: string) => jest.fn((...args: unknown[]) => { calls.push({ method, args }); });
const lodop: Lodop = {
PRINT_INIT: record("PRINT_INIT"), SET_PRINT_MODE: record("SET_PRINT_MODE"), NEWPAGE: record("NEWPAGE"),
ADD_PRINT_BARCODE: record("ADD_PRINT_BARCODE"), ADD_PRINT_RECT: record("ADD_PRINT_RECT"),
ADD_PRINT_IMAGE: record("ADD_PRINT_IMAGE"), PREVIEW: record("PREVIEW"),
ADD_PRINT_TEXT: jest.fn((top, left, width, height, value) => {
texts.push({ top: Number(top), left: Number(left), width: Number(width), height: Number(height), value });
calls.push({ method: "ADD_PRINT_TEXT", args: [top, left, width, height, value] });
}),
SET_PRINT_STYLEA: record("SET_PRINT_STYLEA"),
};
return { texts, calls, lodop };
}
function createRow(barcode: string): BarcodePrintRow {
return {
barcode, supplierName: "", size: "120/60/75", color: "", productCategory: "电脑桌",
factoryModel: "ZWJ0249N1407", tagMaterial: "实木", executeStandard: "GB/T 3324",
} as BarcodePrintRow;
}
describe("single-line barcode image", () => {
let canvas: ReturnType<typeof createCanvasRecorder>;
let savedLodop: Pick<Window, "getLodop" | "getCLodop" | "LODOP">;
beforeEach(() => {
savedLodop = { getLodop: window.getLodop, getCLodop: window.getCLodop, LODOP: window.LODOP };
canvas = createCanvasRecorder();
});
afterEach(() => {
Object.assign(window, savedLodop);
jest.restoreAllMocks();
});
describe.each(TEMPLATES)("%s", (templateTitle) => {
it.each([
"GBGJ-ZWJ024903AN1407V01", "GBGJ-ZWJ024903BN1407V01",
"GBGJ-ZWJ024901AN1407V01", "GBGJ-ZWJ024901BN1407V01", "GBGJ-ZWJ024901CN1407V01",
"GBSFY-LRSF009001N1280V01", "CJ026601N1280V01", "DNZ040717P396V01",
"GBGJ-ZWJ024903BN140712345V01",
])("renders %s as one image without handing segments to LODOP text layout", async (barcode) => {
const { lodop, texts, calls } = createLodopRecorder();
window.getLodop = () => lodop;
window.getCLodop = () => lodop;
await printBarcodeRow(createRow(barcode), { templateTitle });
const parts = __barcodePrintTestUtils.splitBarcodeParts(barcode);
expect(canvas.draws.map(({ text }) => text)).toEqual([parts.catcode, parts.bar2, parts.bar3]);
expect(canvas.draws.map(({ text }) => text).join("")).toBe(barcode);
expect(canvas.draws.map(({ y, baseline }) => [y, baseline])).toEqual([
[38, "alphabetic"], [38, "alphabetic"], [38, "alphabetic"],
]);
expect(canvas.getContext).toHaveBeenCalledTimes(1);
expect(canvas.canvases[0].width).toBe(3096);
expect(canvas.canvases[0].height).toBe(416);
expect(canvas.context.scale).toHaveBeenCalledWith(8, 8);
expect(canvas.context.fillRect).not.toHaveBeenCalled();
expect(canvas.toDataURL).toHaveBeenCalledWith("image/png");
for (const [index, draw] of canvas.draws.entries()) {
const fontPt = fontSizePt(draw.font);
const width = measureWidth(draw.text, draw.font);
const column = COLUMNS[index];
const left = draw.alignment === "center" ? draw.x - width / 2 : draw.x;
expect(draw.font).toContain("微软雅黑");
expect(fontPt).toBeGreaterThanOrEqual(8);
expect(fontPt).toBeLessThanOrEqual(index === 1 ? 28 : 15);
expect(fontPt * 2).toBeCloseTo(Math.round(fontPt * 2), 8);
expect(width).toBeLessThanOrEqual(column.width - 8);
expect(left).toBeGreaterThanOrEqual(column.left);
expect(left + width).toBeLessThanOrEqual(column.left + column.width);
expect(canvas.measurements).toContainEqual({ text: draw.text, font: draw.font });
}
expect(canvas.draws[1].font).toMatch(/^bold /);
expect(lodop.ADD_PRINT_IMAGE).toHaveBeenCalledTimes(1);
expect(lodop.ADD_PRINT_IMAGE).toHaveBeenCalledWith(186, 211, 387, 52, expect.stringContaining(PNG_DATA_URL));
const imageIndex = calls.findIndex(({ method }) => method === "ADD_PRINT_IMAGE");
expect(calls[imageIndex + 1]).toEqual({ method: "SET_PRINT_STYLEA", args: [0, "Stretch", 2] });
expect(texts.some(({ value }) => [parts.catcode, parts.bar2, parts.bar3].includes(value))).toBe(false);
expect(lodop.ADD_PRINT_BARCODE).toHaveBeenCalledWith(15, 22, "100.01mm", "45.01mm", "QRCode", barcode);
expect(lodop.SET_PRINT_MODE).toHaveBeenCalledWith("PRINT_PAGE_PERCENT", "Full-Page");
expect(lodop.PREVIEW).toHaveBeenCalledTimes(1);
expect(texts).toContainEqual({ top: 26, left: 181, width: 304, height: 25, value: "型号:ZWJ0249N1407" });
expect(texts).toContainEqual({ top: 203, left: 14, width: 195, height: 25, value: "GB/T 3324" });
if (templateTitle.includes("第三套")) {
expect(texts).toContainEqual({ top: 145, left: 429, width: 166, height: 25, value: "120.60.75" });
expect(lodop.ADD_PRINT_RECT).toHaveBeenCalledWith(136, 426, 1, 40, 0, 1);
}
});
it.each([
`${"W".repeat(100)}024903BN1407V01`,
`CJ${"0".repeat(100)}BN1407V01`,
`CJ024903BN${"1".repeat(100)}V01`,
])("refuses unreadably long %s before preview rather than wrapping or truncating it", async (barcode) => {
const { lodop } = createLodopRecorder();
window.getLodop = () => lodop;
window.getCLodop = () => lodop;
await expect(printBarcodeRow(createRow(barcode), { templateTitle }))
.rejects.toThrow(/单行|过长|无法/);
expect(lodop.ADD_PRINT_IMAGE).not.toHaveBeenCalled();
expect(lodop.PREVIEW).not.toHaveBeenCalled();
});
it("refuses missing canvas support rather than returning to wrapping text", async () => {
canvas.getContext.mockReturnValue(null);
const { lodop } = createLodopRecorder();
window.getLodop = () => lodop;
window.getCLodop = () => lodop;
await expect(printBarcodeRow(createRow("GBGJ-ZWJ024903BN1407V01"), { templateTitle }))
.rejects.toThrow();
expect(lodop.ADD_PRINT_IMAGE).not.toHaveBeenCalled();
expect(lodop.PREVIEW).not.toHaveBeenCalled();
});
it("refuses invalid image data before preview", async () => {
canvas.toDataURL.mockReturnValue("data:,");
const { lodop } = createLodopRecorder();
window.getLodop = () => lodop;
window.getCLodop = () => lodop;
await expect(printBarcodeRow(createRow("GBGJ-ZWJ024903BN1407V01"), { templateTitle }))
.rejects.toThrow(/图像|单行/);
expect(lodop.ADD_PRINT_IMAGE).not.toHaveBeenCalled();
expect(lodop.PREVIEW).not.toHaveBeenCalled();
});
});
it("keeps the maximum sizes for short segments", () => {
const { lodop } = createLodopRecorder();
printSplitBarcodeText({ catcode: "CJ", bar2: "026601", bar3: "N851V01" }, lodop);
canvas.draws.forEach(({ font }, index) => {
expect(fontSizePt(font)).toBeCloseTo(index === 1 ? 28 : 15, 8);
});
});
it("fits a long prefix at the readable 8pt lower limit", () => {
const { lodop } = createLodopRecorder();
printSplitBarcodeText({ catcode: "W".repeat(10), bar2: "026601", bar3: "N851V01" }, lodop);
expect(fontSizePt(canvas.draws[0].font)).toBe(8);
expect(canvas.draws[0].text).toBe("W".repeat(10));
});
it("keeps glyph overhang inside each column as well as the advance width", () => {
const measure = canvas.context.measureText.bind(canvas.context);
jest.spyOn(canvas.context, "measureText").mockImplementation((text) => {
const metrics = measure(text);
return { ...metrics, actualBoundingBoxLeft: 5, actualBoundingBoxRight: metrics.width + 3 };
});
const { lodop } = createLodopRecorder();
printSplitBarcodeText({ catcode: "GBGJ-ZWJ", bar2: "024903B", bar3: "N1407V01" }, lodop);
canvas.draws.forEach((draw, index) => {
const column = COLUMNS[index];
expect(draw.x - 5).toBeGreaterThanOrEqual(column.left + 4);
expect(draw.x + measureWidth(draw.text, draw.font) + 3).toBeLessThanOrEqual(column.left + column.width - 4);
});
});
it("preserves every modular 8+5 print instruction without requiring canvas", () => {
canvas.getContext.mockReturnValue(null);
const { lodop, calls } = createLodopRecorder();
printSplitBarcodeText({ catcode: "MKHDNZ", bar2: "04092606", bar3: "00047" }, lodop);
expect(calls).toEqual([
{ method: "ADD_PRINT_TEXT", args: [203, 207, 120, 25, "MKHDNZ"] },
{ method: "SET_PRINT_STYLEA", args: [0, "FontName", "微软雅黑"] },
{ method: "SET_PRINT_STYLEA", args: [0, "FontSize", 15] },
{ method: "SET_PRINT_STYLEA", args: [0, "Alignment", 1] },
{ method: "ADD_PRINT_TEXT", args: [186, 290, 230, 52, "04092606"] },
{ method: "SET_PRINT_STYLEA", args: [0, "FontName", "微软雅黑"] },
{ method: "SET_PRINT_STYLEA", args: [0, "FontSize", 28] },
{ method: "SET_PRINT_STYLEA", args: [0, "Bold", 1] },
{ method: "SET_PRINT_STYLEA", args: [0, "Alignment", 2] },
{ method: "ADD_PRINT_TEXT", args: [203, 482, 141, 25, "00047"] },
{ method: "SET_PRINT_STYLEA", args: [0, "FontName", "微软雅黑"] },
{ method: "SET_PRINT_STYLEA", args: [0, "FontSize", 15] },
{ method: "SET_PRINT_STYLEA", args: [0, "Alignment", 2] },
]);
expect(canvas.getContext).not.toHaveBeenCalled();
expect(canvas.toDataURL).not.toHaveBeenCalled();
});
});

View File

@ -0,0 +1,77 @@
import type { BarcodeLodopRecord, Lodop } from "./barcodePrintLodop";
type BarcodeParts = Pick<BarcodeLodopRecord, "catcode" | "bar2" | "bar3">;
const PRINT_FONT = "微软雅黑";
const PX_PER_PT = 96 / 72;
const TEXT_INSET = 8;
const MIN_FONT_PT = 8;
const IMAGE_SCALE = 8;
const IMAGE_WIDTH = 387;
const IMAGE_HEIGHT = 52;
function fitText(context: CanvasRenderingContext2D, text: string, width: number, maximumPt: number, bold: boolean) {
for (let size = maximumPt; size >= MIN_FONT_PT; size -= 0.5) {
context.font = `${bold ? "bold" : "normal"} ${size * PX_PER_PT}px "${PRINT_FONT}", sans-serif`;
const measurement = context.measureText(text);
const left = Math.max(0, measurement.actualBoundingBoxLeft || 0);
const right = Math.max(measurement.width, measurement.actualBoundingBoxRight || 0);
if (left + right <= width - TEXT_INSET) {
return { left, right };
}
}
throw new Error("条码文字过长,无法清晰地单行打印,请检查条码内容。");
}
function buildSingleLineImage(record: BarcodeParts) {
const canvas = document.createElement("canvas");
canvas.width = IMAGE_WIDTH * IMAGE_SCALE;
canvas.height = IMAGE_HEIGHT * IMAGE_SCALE;
const context = canvas.getContext("2d");
if (!context) throw new Error("无法生成单行条码图像,请刷新页面后重试。");
// Measure and draw the same glyphs; LODOP must not reflow the three text segments.
context.scale(IMAGE_SCALE, IMAGE_SCALE);
context.fillStyle = "#000000";
context.textAlign = "left";
context.textBaseline = "alphabetic";
const segments = [
{ text: record.catcode, left: 0, width: 114, size: 15, bold: false },
{ text: record.bar2, left: 120, width: 160, size: 28, bold: true },
{ text: record.bar3, left: 286, width: 101, size: 15, bold: false },
];
for (const segment of segments) {
const bounds = fitText(context, segment.text, segment.width, segment.size, segment.bold);
const x = segment.left + (segment.width - bounds.left - bounds.right) / 2 + bounds.left;
context.fillText(segment.text, x, 38);
}
const image = canvas.toDataURL("image/png");
if (!image.startsWith("data:image/png;base64,")) {
throw new Error("无法生成单行条码图像,请刷新页面后重试。");
}
return image;
}
export function printSplitBarcodeText(record: BarcodeParts, lodop: Lodop) {
const modular = Boolean(record.catcode) && /^\d{8}$/.test(record.bar2) && /^\d{5}$/.test(record.bar3);
if (!modular) {
const image = buildSingleLineImage(record);
lodop.ADD_PRINT_IMAGE(186, 211, IMAGE_WIDTH, IMAGE_HEIGHT, `<img border="0" src="${image}" />`);
lodop.SET_PRINT_STYLEA(0, "Stretch", 2);
return;
}
// Preserve the separately calibrated modular layout and its original instructions.
const segments = [
{ text: record.catcode, top: 203, left: 207, width: 120, height: 25, size: 15, bold: false, alignment: 1 },
{ text: record.bar2, top: 186, left: 290, width: 230, height: 52, size: 28, bold: true, alignment: 2 },
{ text: record.bar3, top: 203, left: 482, width: 141, height: 25, size: 15, bold: false, alignment: 2 },
];
for (const segment of segments) {
lodop.ADD_PRINT_TEXT(segment.top, segment.left, segment.width, segment.height, segment.text);
lodop.SET_PRINT_STYLEA(0, "FontName", PRINT_FONT);
lodop.SET_PRINT_STYLEA(0, "FontSize", segment.size);
if (segment.bold) lodop.SET_PRINT_STYLEA(0, "Bold", 1);
lodop.SET_PRINT_STYLEA(0, "Alignment", segment.alignment);
}
}

View File

@ -0,0 +1,185 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { BarcodePrintTemplateDialog } from "./BarcodePrintTemplateDialog";
import {
BARCODE_PRINT_TEMPLATE_OPTIONS,
getDefaultBarcodePrintTemplateTitle,
printBarcodeRow,
type Lodop,
} from "./barcodePrintLodop";
import type { BarcodePrintRow } from "./barcodePrintRows";
const SECOND_TITLE = "条码打印_第二套模板";
const THIRD_TITLE = "条码打印-第三套模板(尺寸信息)";
function createRow(barcode: string): BarcodePrintRow {
return {
barcode,
factoryModel: "ZWJ0276K01N1392",
productCategory: "家具>电脑桌",
color: "胡桃色",
size: "140/76/74.5",
tagMaterial: "人造板+铁架",
material: "人造板+铁架",
executeStandard: "T/CZFA 0002-2025",
supplierName: "测试供应商",
} as BarcodePrintRow;
}
function createRecorder() {
const calls: Array<{ method: string; args: unknown[] }> = [];
const record = (method: string) => (...args: unknown[]) => {
calls.push({ method, args });
};
const lodop: Lodop = {
PRINT_INIT: record("PRINT_INIT"),
SET_PRINT_PAGESIZE: record("SET_PRINT_PAGESIZE"),
SET_PRINT_MODE: record("SET_PRINT_MODE"),
NEWPAGE: record("NEWPAGE"),
ADD_PRINT_BARCODE: record("ADD_PRINT_BARCODE"),
ADD_PRINT_TEXT: record("ADD_PRINT_TEXT"),
ADD_PRINT_RECT: record("ADD_PRINT_RECT"),
ADD_PRINT_IMAGE: record("ADD_PRINT_IMAGE"),
SET_PRINT_STYLEA: record("SET_PRINT_STYLEA"),
PREVIEW: record("PREVIEW"),
};
return { calls, lodop };
}
describe("third barcode template with size column", () => {
beforeEach(() => {
jest.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue({
scale: jest.fn(), fillText: jest.fn(), measureText: (text: string) => ({ width: text.length * 5 }),
} as never);
jest.spyOn(HTMLCanvasElement.prototype, "toDataURL").mockReturnValue("data:image/png;base64,dGVzdA==");
});
afterEach(() => jest.restoreAllMocks());
it("adds one selectable template after the second without changing old options", () => {
const titles = BARCODE_PRINT_TEMPLATE_OPTIONS.map(({ title }) => title);
expect(titles).toEqual([
"条码打印_第一套编码(数字开头)&一次性采买条码",
SECOND_TITLE,
THIRD_TITLE,
"外贸条码打印",
"顾家",
]);
expect(new Set(titles).size).toBe(titles.length);
expect(getDefaultBarcodePrintTemplateTitle(createRow("CJ026601N1280V01"))).toBe(SECOND_TITLE);
expect(getDefaultBarcodePrintTemplateTitle(createRow("1234567890123"))).toBe(titles[0]);
});
it.each([
"CJ026601N1280V01",
"DNZ040717P396V01",
"GBSFY-LRSF009001N1280V01",
"GBGJ-ZWJ024903AN1407V01",
"GBGJ-ZWJ024903BN1407V01",
"MKHDNZ0409260600047",
"1234567890123",
])("changes only the material row from template two for %s", async (barcode) => {
jest.useFakeTimers().setSystemTime(new Date("2026-09-11T06:00:00.000Z"));
const originalGetLodop = window.getLodop;
const originalGetCLodop = window.getCLodop;
const originalLodop = window.LODOP;
const second = createRecorder();
const third = createRecorder();
try {
window.getLodop = () => second.lodop;
window.getCLodop = () => second.lodop;
expect(await printBarcodeRow(createRow(barcode), { templateTitle: SECOND_TITLE })).toBe(SECOND_TITLE);
window.getLodop = () => third.lodop;
window.getCLodop = () => third.lodop;
expect(await printBarcodeRow(createRow(barcode), { templateTitle: THIRD_TITLE })).toBe(THIRD_TITLE);
const expectedCalls = second.calls.map((call) => ({ ...call, args: [...call.args] }));
const materialIndex = expectedCalls.findIndex(
(call) => call.method === "ADD_PRINT_TEXT" && call.args[0] === 145,
);
expect(expectedCalls[materialIndex]).toEqual({
method: "ADD_PRINT_TEXT", args: [145, 181, 407, 25, "材质:人造板+铁架"],
});
expectedCalls[materialIndex].args[2] = 242;
expectedCalls.splice(materialIndex + 3, 0,
{ method: "ADD_PRINT_TEXT", args: [145, 429, 166, 25, "140.76.74.5"] },
{ method: "SET_PRINT_STYLEA", args: [0, "FontName", "微软雅黑"] },
{ method: "SET_PRINT_STYLEA", args: [0, "FontSize", 15] },
);
expectedCalls.splice(-1, 0, {
method: "ADD_PRINT_RECT", args: [136, 426, 1, 40, 0, 1],
});
expect(third.calls).toEqual(expectedCalls);
expect(third.calls).toContainEqual({
method: "SET_PRINT_MODE", args: ["PRINT_PAGE_PERCENT", "Full-Page"],
});
expect(third.calls.at(-1)).toEqual({ method: "PREVIEW", args: [] });
} finally {
window.getLodop = originalGetLodop;
window.getCLodop = originalGetCLodop;
window.LODOP = originalLodop;
jest.useRealTimers();
}
});
it.each([
["120/60/75", "120.60.75"],
["140/76/74.5", "140.76.74.5"],
["51/31.5/116", "51.31.5.116"],
["120.60.75", "120.60.75"],
[" 120/60/75 ", "120.60.75"],
["", ""],
[null, ""],
[undefined, ""],
])("prints size %s without a label or changing source data", async (size, expected) => {
const originalGetLodop = window.getLodop;
const originalGetCLodop = window.getCLodop;
const originalLodop = window.LODOP;
const { calls, lodop } = createRecorder();
const row = {
...createRow("HJ006701N717V01"),
size: size as string,
lengthCm: "999",
widthCm: "888",
heightCm: "777",
};
const originalRow = { ...row };
try {
window.getLodop = () => lodop;
window.getCLodop = () => lodop;
await printBarcodeRow(row, { templateTitle: THIRD_TITLE });
const sizeCalls = calls.filter(
(call) => call.method === "ADD_PRINT_TEXT" && call.args[0] === 145 && call.args[1] === 429,
);
expect(sizeCalls).toEqual([
{ method: "ADD_PRINT_TEXT", args: [145, 429, 166, 25, expected] },
]);
expect(row).toEqual(originalRow);
} finally {
window.getLodop = originalGetLodop;
window.getCLodop = originalGetCLodop;
window.LODOP = originalLodop;
}
});
it("submits the third template using the actual template options", async () => {
const actor = userEvent.setup();
const onSubmit = jest.fn();
render(
<BarcodePrintTemplateDialog
isOpen mode="barcode"
templates={BARCODE_PRINT_TEMPLATE_OPTIONS}
defaultTemplateTitle={SECOND_TITLE}
rowSummary="MKHDNZ0409260600047"
onClose={jest.fn()} onSubmit={onSubmit}
/>,
);
const select = screen.getByLabelText("打印模板");
expect(select).toHaveValue(SECOND_TITLE);
expect(screen.getByRole("option", { name: THIRD_TITLE })).not.toBeDisabled();
await actor.selectOptions(select, THIRD_TITLE);
await actor.click(screen.getByRole("button", { name: "确定" }));
expect(onSubmit).toHaveBeenCalledWith({
templateTitle: THIRD_TITLE, batchNumber: "", editMode: undefined,
});
});
});

View File

@ -0,0 +1,50 @@
import type {
BarcodeLodopRecord,
BarcodePrintTemplate,
Lodop,
} from "./barcodePrintLodop";
import { printSplitBarcodeText } from "./barcodePrintSplitCode";
function setFont(lodop: Lodop, size: number, bold = false) {
lodop.SET_PRINT_STYLEA(0, "FontName", "微软雅黑");
lodop.SET_PRINT_STYLEA(0, "FontSize", size);
if (bold) lodop.SET_PRINT_STYLEA(0, "Bold", 1);
}
// Independent layout so size-specific changes do not affect template two.
function printThirdTemplate(record: BarcodeLodopRecord, lodop: Lodop) {
lodop.PRINT_INIT("条码打印");
lodop.ADD_PRINT_BARCODE(15, 22, "100.01mm", "45.01mm", "QRCode", record.barcode);
lodop.SET_PRINT_STYLEA(0, "ShowBarText", 0);
lodop.ADD_PRINT_TEXT(26, 181, 304, 25, `型号:${record.factoryNumber}`);
setFont(lodop, 12);
lodop.ADD_PRINT_TEXT(59, 181, 415, 75, `编码:${record.skuName1}`);
setFont(lodop, 15);
lodop.SET_PRINT_STYLEA(0, "TextNeatRow", 1);
lodop.ADD_PRINT_TEXT(26, 429, 198, 25, `类目:${record.productCategory}`);
setFont(lodop, 15);
lodop.ADD_PRINT_TEXT(145, 181, 242, 25, `材质:${record.hangtagMaterial}`);
setFont(lodop, 15);
lodop.ADD_PRINT_TEXT(145, 429, 166, 25, record.size);
setFont(lodop, 15);
lodop.ADD_PRINT_TEXT(179, 26, 160, 25, "执行标准");
setFont(lodop, 15);
lodop.ADD_PRINT_TEXT(203, 14, 195, 25, record.executionStandard);
setFont(lodop, 15);
printSplitBarcodeText(record, lodop);
lodop.ADD_PRINT_TEXT(182, 458, 172, 25, `生产批号:${record.printDate}`);
lodop.SET_PRINT_STYLEA(0, "FontName", "微软雅黑");
lodop.ADD_PRINT_RECT(20, 178, 420, 157, 0, 1);
lodop.ADD_PRINT_RECT(56, 178, 420, 1, 0, 1);
lodop.ADD_PRINT_RECT(135, 178, 420, 1, 0, 1);
lodop.ADD_PRINT_RECT(21, 426, 1, 34, 0, 1);
lodop.ADD_PRINT_RECT(136, 426, 1, 40, 0, 1);
}
export const BARCODE_PRINT_THIRD_TEMPLATE: BarcodePrintTemplate = {
title: "条码打印-第三套模板(尺寸信息)",
useFullPagePercent: true,
printFunc: printThirdTemplate,
};