fix(barcode-print): recalibrate Gujia card and disable 2026 template

This commit is contained in:
WuYongJie 2026-09-09 17:54:04 +08:00
parent 1c0beffcb6
commit 0271da1b69
6 changed files with 128 additions and 14 deletions

View File

@ -1,8 +1,35 @@
import { render, screen } from '@testing-library/react'
import { fireEvent, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { BarcodePrintTemplateDialog } from './BarcodePrintTemplateDialog'
describe('BarcodePrintTemplateDialog', () => {
it('marks a disabled certificate unavailable, skips a stale default and prevents submission', async () => {
const actor = userEvent.setup()
const onSubmit = jest.fn()
const disabledTitle = '顾家合格证卡片2026新版(11cm*13cm)'
const legacyTitle = '顾家合格证卡片(11cm*13cm)'
render(
<BarcodePrintTemplateDialog
isOpen mode="certificate"
templates={[{ title: disabledTitle, disabled: true }, { title: legacyTitle }]}
defaultTemplateTitle={disabledTitle}
rowSummary="顾家合格证"
onClose={jest.fn()} onSubmit={onSubmit}
/>,
)
const select = screen.getByLabelText('打印模板')
expect(screen.getByRole('option', { name: `${disabledTitle}(暂时不可用)` })).toBeDisabled()
expect(select).toHaveValue(legacyTitle)
await actor.selectOptions(select, disabledTitle)
expect(select).toHaveValue(legacyTitle)
fireEvent.change(select, { target: { value: disabledTitle } })
await actor.click(screen.getByRole('button', { name: '确定' }))
expect(onSubmit).not.toHaveBeenCalled()
await actor.selectOptions(select, legacyTitle)
await actor.click(screen.getByRole('button', { name: '确定' }))
expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({ templateTitle: legacyTitle }))
})
it('submits the selected barcode template', async () => {
const actor = userEvent.setup()
const onSubmit = jest.fn()

View File

@ -6,6 +6,7 @@ import type { CertificateSpecialEditMode } from "./barcodePrintCertificate";
type PrintTemplateOption = {
title: string;
disabled?: boolean;
editMode?: CertificateSpecialEditMode;
};
@ -36,9 +37,9 @@ export function BarcodePrintTemplateDialog({
}) {
const resolvedDefaultTemplateTitle = useMemo(
() =>
templates.some((template) => template.title === defaultTemplateTitle)
templates.some((template) => template.title === defaultTemplateTitle && !template.disabled)
? defaultTemplateTitle ?? ""
: (templates[0]?.title ?? ""),
: (templates.find((template) => !template.disabled)?.title ?? ""),
[defaultTemplateTitle, templates],
);
const [templateTitle, setTemplateTitle] = useState(resolvedDefaultTemplateTitle);
@ -66,10 +67,14 @@ export function BarcodePrintTemplateDialog({
if (!isOpen) return null;
function submit() {
if (!templateTitle) {
if (!selectedTemplate) {
setErrorMessage("请选择打印模板。");
return;
}
if (selectedTemplate.disabled) {
setErrorMessage("该模板暂时不可用。");
return;
}
onSubmit({
templateTitle,
@ -125,8 +130,13 @@ export function BarcodePrintTemplateDialog({
>
<option value="">请选择打印模板,目前暂不支持模板配置</option>
{templates.map((template) => (
<option key={template.title} value={template.title}>
{template.title}
<option
key={template.title}
value={template.title}
disabled={template.disabled}
className={template.disabled ? "text-[var(--obs-shell-subtle)]" : undefined}
>
{template.title}{template.disabled ? "(暂时不可用)" : ""}
</option>
))}
</select>
@ -155,6 +165,7 @@ export function BarcodePrintTemplateDialog({
<button
type="button"
onClick={submit}
disabled={selectedTemplate?.disabled}
className="inline-flex h-8 items-center rounded-[6px] border border-[var(--obs-shell-action-border)] bg-[var(--obs-shell-action-bg)] px-4 text-[12px] font-apple-semibold text-[var(--obs-shell-action-text)] transition hover:bg-[var(--obs-shell-action-hover-bg)]"
>
确定

View File

@ -1,10 +1,57 @@
import { __barcodePrintCertificateTestUtils } from "./barcodePrintCertificate";
import {
__barcodePrintCertificateTestUtils,
CERTIFICATE_PRINT_TEMPLATE_OPTIONS,
createCertificatePrintPreview,
printCertificatePreview,
} from "./barcodePrintCertificate";
import type { BarcodePrintRow } from "./barcodePrintRows";
import { applyGujiaCertificate2026Content } from "./barcodePrintGujiaCertificate2026";
import { SOFIA_CARTON_BARCODE_QR_LAYOUT } from "./barcodePrintSofiaCartonLayout";
import {
SOFIA_CERTIFICATE_FIELD_LINE_TOPS,
SOFIA_CERTIFICATE_FIELD_TOPS,
} from "./barcodePrintSofiaCertificateLayout";
describe("Gujia certificate availability and calibration", () => {
const disabledTitle = "顾家合格证卡片2026新版(11cm*13cm)";
const record = {
barcode: "GBGJ-TEST", productCategory: "电脑桌", color: "经典北美黑胡桃色+黑架",
procode: "DS5147BG", brandProcode: "DS5147BG-C4", size: "120*60*75",
productSpec: "120*60*75", hangtagMaterial: "人造板+铁架",
qualifiedCertMainMaterial: "", executionStandard: "T/CZFA 0002-2025",
};
it("disables only the new card and rejects generation and previously generated previews", async () => {
expect(CERTIFICATE_PRINT_TEMPLATE_OPTIONS.filter((option) => option.disabled).map((option) => option.title))
.toEqual([disabledTitle]);
await expect(createCertificatePrintPreview({} as BarcodePrintRow, disabledTitle))
.rejects.toThrow("暂时不可用");
await expect(printCertificatePreview({ title: disabledTitle, pageSize: "110mm 130mm", imageDataUrl: "data:image/png;base64,old" }))
.rejects.toThrow("暂时不可用");
});
it("applies one calibrated offset to the legacy card while retaining all original printed values", () => {
const card = __barcodePrintCertificateTestUtils.renderGujiaCertificate(record, "", {}, "background");
const values = Array.from(card.querySelectorAll<HTMLElement>(".item"));
expect(values.map((value) => value.style.top)).toEqual(Array.from({ length: 8 }, (_, index) => `${488 + index * 118.5}px`));
expect(values.map((value) => value.textContent).slice(0, 7)).toEqual([
"电脑桌-经典北美黑胡桃色+黑架", "DS5147BG-C4", "120*60*75", "人造板+铁架",
"顾家家居股份有限公司", "B", "合格",
]);
expect(values.every((value) => value.style.left === "474px" && value.style.fontSize === "42px")).toBe(true);
});
it("retains the disabled new card content and coordinates for future use", () => {
const base = __barcodePrintCertificateTestUtils.renderGujiaCertificate(record, "", {}, "background", -72);
const card = applyGujiaCertificate2026Content(base, record.executionStandard);
const values = Array.from(card.querySelectorAll<HTMLElement>(".item"));
expect(values.map((value) => value.style.top)).toEqual(Array.from({ length: 9 }, (_, index) => `${436 + index * 118.5}px`));
expect(values[4].textContent).toBe("欧伯娜家居有限公司");
expect(values[6].textContent).toBe("合格品");
expect(values[8].textContent).toBe(record.executionStandard);
});
});
describe("Sofia certificate field positioning", () => {
afterEach(() => {
jest.useRealTimers();

View File

@ -4,6 +4,8 @@ import html2canvas from "html2canvas";
import { applyGujiaCertificate2026Content } from "./barcodePrintGujiaCertificate2026";
import {
GUJIA_CERTIFICATE_CANVAS,
GUJIA_FIELD_PRINT_VERTICAL_OFFSET,
GUJIA_LEGACY_FIELD_PRINT_VERTICAL_OFFSET,
getGujiaCertificateFieldStyle,
} from "./barcodePrintGujiaCertificateLayout";
import {
@ -27,6 +29,7 @@ import type { BarcodePrintRow } from "./barcodePrintRows";
type CertificateTemplate = {
title: string;
disabled?: boolean;
pageSize: string;
background: string;
editMode?: CertificateSpecialEditMode;
@ -544,6 +547,7 @@ function renderGujiaCertificate(
_qrCodeBase64: string,
_options: CertificatePrintOptions,
backgroundDataUrl: string,
verticalOffset = GUJIA_LEGACY_FIELD_PRINT_VERTICAL_OFFSET,
) {
const container = createCertificateMain(
"certificate_main_gj",
@ -565,7 +569,7 @@ function renderGujiaCertificate(
container,
`item_${index + 1} item`,
text,
getGujiaCertificateFieldStyle(index),
getGujiaCertificateFieldStyle(index, verticalOffset),
);
});
@ -646,6 +650,7 @@ function renderSofiaCarton(
}
export const __barcodePrintCertificateTestUtils = {
renderGujiaCertificate,
getLandscapeProductNameFontSize,
renderCommonWithMaker,
renderCommonWithoutMaker,
@ -745,18 +750,22 @@ const CERTIFICATE_TEMPLATES: CertificateTemplate[] = [
},
{
title: GUJIA_CERTIFICATE_2026_TITLE,
disabled: true,
pageSize: "110mm 130mm",
background: "hgz_gj.jpg",
render: (record, qrCodeBase64, options, backgroundDataUrl) =>
applyGujiaCertificate2026Content(
renderGujiaCertificate(record, qrCodeBase64, options, backgroundDataUrl),
renderGujiaCertificate(
record, qrCodeBase64, options, backgroundDataUrl,
GUJIA_FIELD_PRINT_VERTICAL_OFFSET,
),
record.executionStandard,
),
},
];
export const CERTIFICATE_PRINT_TEMPLATE_OPTIONS = CERTIFICATE_TEMPLATES.map(
({ editMode, title }) => ({ editMode, title }),
({ editMode, title, disabled }) => ({ editMode, title, disabled }),
);
function findCertificateTemplate(title: string) {
@ -764,6 +773,9 @@ function findCertificateTemplate(title: string) {
if (!template) {
throw new Error("请选择有效的合格证模板。");
}
if (template.disabled) {
throw new Error("该合格证模板暂时不可用。");
}
return template;
}
@ -890,6 +902,7 @@ export async function createCertificatePrintPreview(
}
export async function printCertificatePreview(preview: CertificatePrintPreview) {
findCertificateTemplate(preview.title);
const printJS = (await import("print-js")).default;
printJS({

View File

@ -1,6 +1,7 @@
import {
GUJIA_CERTIFICATE_CANVAS,
GUJIA_FIELD_PRINT_VERTICAL_OFFSET,
GUJIA_LEGACY_FIELD_PRINT_VERTICAL_OFFSET,
getGujiaCertificateFieldStyle,
} from "./barcodePrintGujiaCertificateLayout";
@ -19,7 +20,7 @@ describe("Gujia certificate layout", () => {
}
});
it("moves all dynamic fields up 6mm while preserving the 118.5px row spacing", () => {
it("preserves the disabled 2026 template coordinates", () => {
expect(GUJIA_FIELD_PRINT_VERTICAL_OFFSET).toBe(-72);
expect(getGujiaCertificateFieldStyle(0).top).toBe("436px");
expect(getGujiaCertificateFieldStyle(1).top).toBe("554.5px");
@ -27,6 +28,17 @@ describe("Gujia certificate layout", () => {
expect(getGujiaCertificateFieldStyle(7).top).toBe("1265.5px");
});
it("calibrates the legacy card from original coordinates without changing field spacing or style", () => {
for (let index = 0; index < 8; index += 1) {
const original = getGujiaCertificateFieldStyle(index, 0);
const calibrated = getGujiaCertificateFieldStyle(index, GUJIA_LEGACY_FIELD_PRINT_VERTICAL_OFFSET);
expect(calibrated).toEqual({
...original,
top: `${Number.parseFloat(original.top!) - 20}px`,
});
}
});
it("keeps the original Gujia canvas dimensions", () => {
expect(GUJIA_CERTIFICATE_CANVAS).toEqual({ width: 1323, height: 1559 });
});

View File

@ -4,15 +4,19 @@ export const GUJIA_CERTIFICATE_CANVAS = {
} as const;
const GUJIA_FIELD_LEFT = 474;
// 实物卡片校准:动态字段相对红色底图整体上移 6mm(1323px / 110mm)。
// 保留已停用2026新版的现有坐标,旧版校准单独传入下方偏移量。
export const GUJIA_FIELD_PRINT_VERTICAL_OFFSET = -72;
const GUJIA_FIELD_TOP = 508 + GUJIA_FIELD_PRINT_VERTICAL_OFFSET;
// 三张实物照片按红线行距归一化:目标位移约为旧版-72px实测位移的28%。
// 从图1原始坐标上移20px,待同一打印机试打确认;这不是物理毫米换算。
export const GUJIA_LEGACY_FIELD_PRINT_VERTICAL_OFFSET = -20;
const GUJIA_FIELD_TOP = 508;
const GUJIA_FIELD_ROW_GAP = 118.5;
export function getGujiaCertificateFieldStyle(
index: number,
verticalOffset = GUJIA_FIELD_PRINT_VERTICAL_OFFSET,
): Partial<CSSStyleDeclaration> {
const top = GUJIA_FIELD_TOP + index * GUJIA_FIELD_ROW_GAP;
const top = GUJIA_FIELD_TOP + verticalOffset + index * GUJIA_FIELD_ROW_GAP;
return {
position: "absolute",