fix(barcode-print): recalibrate Gujia card and disable 2026 template
This commit is contained in:
parent
1c0beffcb6
commit
0271da1b69
@ -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()
|
||||
|
||||
@ -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)]"
|
||||
>
|
||||
确定
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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 });
|
||||
});
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user