feat(barcode-print): add size template and keep package codes on one line
This commit is contained in:
parent
6f49206964
commit
222f3e6a97
@ -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 () => {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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,
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user