diff --git a/furniture-3d-system/src/app/intake/barcode-print/BarcodePrintCertificateEditDialog.test.tsx b/furniture-3d-system/src/app/intake/barcode-print/BarcodePrintCertificateEditDialog.test.tsx index 8c82874b..262f8def 100644 --- a/furniture-3d-system/src/app/intake/barcode-print/BarcodePrintCertificateEditDialog.test.tsx +++ b/furniture-3d-system/src/app/intake/barcode-print/BarcodePrintCertificateEditDialog.test.tsx @@ -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( , @@ -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( + , + ) + + 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'), + ) }) }) diff --git a/furniture-3d-system/src/app/intake/barcode-print/BarcodePrintCertificateEditDialog.tsx b/furniture-3d-system/src/app/intake/barcode-print/BarcodePrintCertificateEditDialog.tsx index 82278446..af560b89 100644 --- a/furniture-3d-system/src/app/intake/barcode-print/BarcodePrintCertificateEditDialog.tsx +++ b/furniture-3d-system/src/app/intake/barcode-print/BarcodePrintCertificateEditDialog.tsx @@ -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", }} > + (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 ( +
+ {isCurrentQrCode && renderedQrCode.dataUrl ? ( + // eslint-disable-next-line @next/next/no-img-element -- QR data URLs are generated locally. + + ) : null} +
+ ); +} diff --git a/furniture-3d-system/src/app/intake/barcode-print/barcodePrintCertificate.ts b/furniture-3d-system/src/app/intake/barcode-print/barcodePrintCertificate.ts index 9204ba15..5f88c33a 100644 --- a/furniture-3d-system/src/app/intake/barcode-print/barcodePrintCertificate.ts +++ b/furniture-3d-system/src/app/intake/barcode-print/barcodePrintCertificate.ts @@ -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((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 { 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); diff --git a/furniture-3d-system/src/app/intake/barcode-print/barcodePrintQrCode.test.ts b/furniture-3d-system/src/app/intake/barcode-print/barcodePrintQrCode.test.ts new file mode 100644 index 00000000..88cd4d6b --- /dev/null +++ b/furniture-3d-system/src/app/intake/barcode-print/barcodePrintQrCode.test.ts @@ -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", + ); + }); +}); diff --git a/furniture-3d-system/src/app/intake/barcode-print/barcodePrintQrCode.ts b/furniture-3d-system/src/app/intake/barcode-print/barcodePrintQrCode.ts new file mode 100644 index 00000000..8073063e --- /dev/null +++ b/furniture-3d-system/src/app/intake/barcode-print/barcodePrintQrCode.ts @@ -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((resolve, reject) => { + QRCode.toDataURL( + value, + { + width: BARCODE_QR_CODE_SIZE, + margin: 0, + }, + (error, url) => { + if (error) { + reject(error); + return; + } + + resolve(url); + }, + ); + }); +}