fix(barcode-print): sync certificate QR codes

This commit is contained in:
WuYongJie 2026-08-11 12:31:22 +08:00
parent 83a4d456b5
commit 5c547dd25f
6 changed files with 199 additions and 38 deletions

View File

@ -1,8 +1,27 @@
import { render, screen } from '@testing-library/react'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { BarcodePrintCertificateEditDialog } from './BarcodePrintCertificateEditDialog'
import { getSofiaCartonInputStyle } from './barcodePrintSofiaCartonLayout'
jest.mock('./barcodePrintQrCode', () => ({
createBarcodeQrCodeDataUrl: jest.fn(async (barcode: string) =>
`data:image/png;barcode=${encodeURIComponent(barcode)}`,
),
}))
const COMMON_FORM = {
productName: '餐椅-奶油白',
barcode: 'WPH-SZ015713N1170V01',
size: '100*70*40',
hangtagMaterial: '科技布',
maker: '常州朴美家居有限公司',
executionStandard: 'GB/T 3324',
level: '合格',
auther: 'B',
address: '常州经济开发区横林镇红赵路62号',
phone: '19941684327',
}
describe('BarcodePrintCertificateEditDialog', () => {
it('shows Sofia carton editable fields before printing', async () => {
const actor = userEvent.setup()
@ -59,24 +78,13 @@ describe('BarcodePrintCertificateEditDialog', () => {
})
})
it('keeps maker address and phone hidden for common template without supplier', () => {
it('keeps maker address and phone hidden for common template without supplier', async () => {
render(
<BarcodePrintCertificateEditDialog
isOpen
editMode="commonWithoutMaker"
templateTitle="通用横版贴纸-不带供应商(10cm*7cm)"
initialCommonForm={{
productName: '餐椅-奶油白',
barcode: 'WPH-SZ015713N1170V01',
size: '100*70*40',
hangtagMaterial: '科技布',
maker: '常州朴美家居有限公司',
executionStandard: 'GB/T 3324',
level: '合格',
auther: 'B',
address: '常州经济开发区横林镇红赵路62号',
phone: '19941684327',
}}
initialCommonForm={COMMON_FORM}
onClose={jest.fn()}
onPrint={jest.fn()}
/>,
@ -87,5 +95,41 @@ describe('BarcodePrintCertificateEditDialog', () => {
expect(screen.queryByLabelText('制造商')).not.toBeInTheDocument()
expect(screen.queryByLabelText('地址')).not.toBeInTheDocument()
expect(screen.queryByLabelText('联系电话')).not.toBeInTheDocument()
await waitFor(() =>
expect(screen.getByRole('img', { name: '条码二维码' })).toHaveAttribute(
'data-barcode',
COMMON_FORM.barcode,
),
)
})
it.each([
['commonWithMaker', '通用横版贴纸-带供应商(10cm*7cm)'],
['commonWithoutMaker', '通用横版贴纸-不带供应商(10cm*7cm)'],
] as const)('keeps the %s QR code synced with the editable barcode', async (editMode, templateTitle) => {
const actor = userEvent.setup()
render(
<BarcodePrintCertificateEditDialog
isOpen
editMode={editMode}
templateTitle={templateTitle}
initialCommonForm={COMMON_FORM}
onClose={jest.fn()}
onPrint={jest.fn()}
/>,
)
const qrCode = screen.getByRole('img', { name: '条码二维码' })
await waitFor(() =>
expect(qrCode).toHaveAttribute('data-barcode', COMMON_FORM.barcode),
)
await actor.clear(screen.getByLabelText('条码'))
await actor.type(screen.getByLabelText('条码'), 'GBSFY-SJ037640N1410V01')
await waitFor(() =>
expect(qrCode).toHaveAttribute('data-barcode', 'GBSFY-SJ037640N1410V01'),
)
})
})

View File

@ -2,6 +2,7 @@
import { Printer, Upload, X } from "lucide-react";
import { useEffect, useRef, useState, type CSSProperties } from "react";
import { BarcodePrintQrCodePreview } from "./BarcodePrintQrCodePreview";
import type {
CertificatePrintOptions,
CertificateSpecialEditMode,
@ -294,6 +295,7 @@ export function BarcodePrintCertificateEditDialog({
backgroundSize: "cover",
}}
>
<BarcodePrintQrCodePreview barcode={commonForm.barcode} />
<input
aria-label="产品名称"
placeholder="请填写产品名称"

View File

@ -0,0 +1,75 @@
"use client";
import { useEffect, useState } from "react";
import { createBarcodeQrCodeDataUrl } from "./barcodePrintQrCode";
type BarcodePrintQrCodePreviewProps = {
barcode: string;
};
type RenderedQrCode = {
barcode: string;
dataUrl: string;
};
const EMPTY_QR_CODE: RenderedQrCode = {
barcode: "",
dataUrl: "",
};
export function BarcodePrintQrCodePreview({
barcode,
}: BarcodePrintQrCodePreviewProps) {
const normalizedBarcode = barcode.trim();
const [renderedQrCode, setRenderedQrCode] =
useState<RenderedQrCode>(EMPTY_QR_CODE);
useEffect(() => {
if (!normalizedBarcode) return;
let active = true;
void createBarcodeQrCodeDataUrl(normalizedBarcode)
.then((dataUrl) => {
if (active) {
setRenderedQrCode({ barcode: normalizedBarcode, dataUrl });
}
})
.catch(() => {
if (active) {
setRenderedQrCode(EMPTY_QR_CODE);
}
});
return () => {
active = false;
};
}, [normalizedBarcode]);
const isCurrentQrCode = renderedQrCode.barcode === normalizedBarcode;
return (
<div
role="img"
aria-label="条码二维码"
data-barcode={isCurrentQrCode ? normalizedBarcode : ""}
style={{
position: "absolute",
left: 460,
top: 22,
zIndex: 2,
width: 91,
height: 91,
background: "#ffffff",
}}
>
{isCurrentQrCode && renderedQrCode.dataUrl ? (
// eslint-disable-next-line @next/next/no-img-element -- QR data URLs are generated locally.
<img
src={renderedQrCode.dataUrl}
alt=""
style={{ display: "block", width: "100%", height: "100%" }}
/>
) : null}
</div>
);
}

View File

@ -1,11 +1,14 @@
"use client";
import html2canvas from "html2canvas";
import QRCode from "qrcode";
import {
GUJIA_CERTIFICATE_CANVAS,
getGujiaCertificateFieldStyle,
} from "./barcodePrintGujiaCertificateLayout";
import {
createBarcodeQrCodeDataUrl,
resolveCertificateQrCodeValue,
} from "./barcodePrintQrCode";
import {
getSofiaCartonTitleStyle,
getSofiaCartonValueStyle,
@ -635,28 +638,6 @@ function findCertificateTemplate(title: string) {
return template;
}
async function createQrCodeBase64(barcode: string) {
if (!barcode) return "";
return new Promise<string>((resolve, reject) => {
QRCode.toDataURL(
barcode,
{
width: 182,
margin: 0,
},
(error, url) => {
if (error) {
reject(error);
return;
}
resolve(url);
},
);
});
}
function appendHiddenRenderRoot(element: HTMLElement) {
const root = document.createElement("div");
Object.assign(root.style, {
@ -749,7 +730,11 @@ export async function createCertificatePrintPreview(
): Promise<CertificatePrintPreview> {
const template = findCertificateTemplate(templateTitle);
const record = buildCertificateRecord(row);
const qrCodeBase64 = await createQrCodeBase64(record.barcode);
const qrCodeValue = resolveCertificateQrCodeValue(
record.barcode,
options.commonForm?.barcode,
);
const qrCodeBase64 = await createBarcodeQrCodeDataUrl(qrCodeValue);
const backgroundUrl = assetUrl(`${TEMPLATE_BASE}/${template.background}`);
const backgroundDataUrl = await imageUrlToDataUrl(backgroundUrl);
const element = template.render(record, qrCodeBase64, options, backgroundDataUrl);

View File

@ -0,0 +1,22 @@
import { resolveCertificateQrCodeValue } from "./barcodePrintQrCode";
describe("resolveCertificateQrCodeValue", () => {
it("uses the edited common-template barcode", () => {
expect(
resolveCertificateQrCodeValue(
"GBGJ-DNZ036501N08V01",
" GBSFY-SJ037640N1410V01 ",
),
).toBe("GBSFY-SJ037640N1410V01");
});
it("keeps an intentionally cleared edited barcode blank", () => {
expect(resolveCertificateQrCodeValue("GBGJ-DNZ036501N08V01", "")).toBe("");
});
it("falls back to the row barcode when there is no editable form", () => {
expect(resolveCertificateQrCodeValue(" GBGJ-DNZ036501N08V01 ")).toBe(
"GBGJ-DNZ036501N08V01",
);
});
});

View File

@ -0,0 +1,33 @@
import QRCode from "qrcode";
const BARCODE_QR_CODE_SIZE = 182;
export function resolveCertificateQrCodeValue(
originalBarcode: string,
editedBarcode?: string,
) {
return (editedBarcode ?? originalBarcode).trim();
}
export async function createBarcodeQrCodeDataUrl(barcode: string) {
const value = barcode.trim();
if (!value) return "";
return new Promise<string>((resolve, reject) => {
QRCode.toDataURL(
value,
{
width: BARCODE_QR_CODE_SIZE,
margin: 0,
},
(error, url) => {
if (error) {
reject(error);
return;
}
resolve(url);
},
);
});
}