merge: fix barcode certificate QR codes
This commit is contained in:
commit
20fee3b7ba
@ -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'),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@ -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="请填写产品名称"
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user