@7365admin1/layer-common 4.97.1-staging.500 → 4.97.1-staging.502
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/HidAccessPermissions.vue +146 -38
- package/components/HidUserEnrollment.vue +15 -15
- package/components/VehicleQrStickerDialog.vue +88 -43
- package/package.json +1 -1
- package/utils/hid-enrolment-subject.ts +22 -0
- package/utils/hid-permission-assignments.ts +66 -5
- package/utils/vehicle-qr-design.ts +312 -144
- package/utils/vehicle-qr-sticker.ts +119 -34
|
@@ -9,58 +9,118 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
|
+
CUT_LINE_GAP,
|
|
12
13
|
DEFAULT_STICKER_DESIGN,
|
|
13
14
|
QR_MM,
|
|
14
|
-
cardSize,
|
|
15
15
|
isSilhouette,
|
|
16
|
-
|
|
17
|
-
silhouetteLayout,
|
|
16
|
+
stickerGeometry,
|
|
18
17
|
stickerLabel,
|
|
19
18
|
stickerTheme,
|
|
20
|
-
type FrameShape,
|
|
21
19
|
type StickerDesign,
|
|
22
20
|
} from "./vehicle-qr-design.ts";
|
|
23
21
|
|
|
24
22
|
/** The server's most vehicles per request (POST /api/vehicles/qr/stickers). */
|
|
25
23
|
export const STICKER_MAX_PER_REQUEST = 500;
|
|
26
24
|
|
|
27
|
-
/**
|
|
28
|
-
export const
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
/** Modules across the sticker QR: "S365V:" + a 22-character token at ECC Q is always version 3. */
|
|
26
|
+
export const STICKER_QR_MODULES = 29;
|
|
27
|
+
|
|
28
|
+
/** The classic card sheet at Standard size: 4 columns x 4 rows. */
|
|
29
|
+
export const STICKERS_PER_PAGE = 16;
|
|
31
30
|
|
|
32
31
|
/** Printer margin on every side of the A4 sheet, mm. */
|
|
33
32
|
export const SHEET_MARGIN = 5;
|
|
34
33
|
const PAGE_W = 210 - SHEET_MARGIN * 2;
|
|
35
34
|
const PAGE_H = 297 - SHEET_MARGIN * 2;
|
|
35
|
+
/** Strip at the foot of every sheet: the sizes, the print-at-100% reminder and the 10 mm size check. */
|
|
36
|
+
export const INFO_H = 11;
|
|
37
|
+
/** The size-check bar printed on every sheet, mm. */
|
|
38
|
+
export const SIZE_CHECK_MM = 10;
|
|
36
39
|
|
|
37
40
|
/**
|
|
38
41
|
* How stickers sit on the A4 sheet. A cell = the sticker + the installer strip
|
|
39
42
|
* under it; `gap` is the empty cutting gap between cells (no ink in it except
|
|
40
|
-
* the small + cut marks in its middle).
|
|
43
|
+
* the small + cut marks in its middle). As many as fit above the info strip.
|
|
41
44
|
*/
|
|
42
45
|
export type SheetLayout = { cols: number; rows: number; cellW: number; cardH: number; stripH: number; gap: number };
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
const mm = (v: number) => Math.round(v * 100) / 100;
|
|
48
|
+
|
|
49
|
+
export function sheetLayout(design: StickerDesign): SheetLayout {
|
|
50
|
+
const g = stickerGeometry(design, STICKER_QR_MODULES);
|
|
51
|
+
const cellW = mm(g.w * g.scale);
|
|
52
|
+
const cardH = mm(g.h * g.scale);
|
|
53
|
+
const stripH = 8;
|
|
54
|
+
const gap = isSilhouette(design.shape) ? 4 : 3;
|
|
55
|
+
return {
|
|
56
|
+
cols: Math.max(1, Math.floor((PAGE_W + gap) / (cellW + gap))),
|
|
57
|
+
rows: Math.max(1, Math.floor((PAGE_H - INFO_H + gap) / (cardH + stripH + gap))),
|
|
58
|
+
cellW,
|
|
59
|
+
cardH,
|
|
60
|
+
stripH,
|
|
61
|
+
gap,
|
|
62
|
+
};
|
|
49
63
|
}
|
|
50
64
|
|
|
51
65
|
export const perSheet = (l: SheetLayout) => l.cols * l.rows;
|
|
52
66
|
|
|
53
67
|
/** "12 stickers per A4 sheet with 4 mm cutting gaps" */
|
|
54
|
-
export function sheetSummary(
|
|
55
|
-
const l = sheetLayout(
|
|
68
|
+
export function sheetSummary(design: StickerDesign): string {
|
|
69
|
+
const l = sheetLayout(design);
|
|
56
70
|
return `${perSheet(l)} stickers per A4 sheet with ${l.gap} mm cutting gaps`;
|
|
57
71
|
}
|
|
58
72
|
|
|
73
|
+
export type StickerSizeInfo = {
|
|
74
|
+
/** The QR code's data area (what a scanner reads), mm square. */
|
|
75
|
+
qrMm: number;
|
|
76
|
+
/** The shape (shaped code) or the card, mm. */
|
|
77
|
+
shapeW: number;
|
|
78
|
+
shapeH: number;
|
|
79
|
+
/** The piece in your hand after cutting (on the grey line when it is shown), mm. */
|
|
80
|
+
stickerW: number;
|
|
81
|
+
stickerH: number;
|
|
82
|
+
perSheet: number;
|
|
83
|
+
gap: number;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** Every size the person printing needs to know, before printing. */
|
|
87
|
+
export function stickerSizeInfo(design: StickerDesign, n = STICKER_QR_MODULES): StickerSizeInfo {
|
|
88
|
+
const g = stickerGeometry(design, n);
|
|
89
|
+
const l = sheetLayout(design);
|
|
90
|
+
const cut = design.cutLines !== false ? 2 * CUT_LINE_GAP : 0;
|
|
91
|
+
const bare = isSilhouette(design.shape) && design.frame === "none";
|
|
92
|
+
return {
|
|
93
|
+
qrMm: mm(g.dataMm),
|
|
94
|
+
shapeW: mm(g.shapeW),
|
|
95
|
+
shapeH: mm(g.shapeH),
|
|
96
|
+
// a bare shape is cut round the shape and its plate tag; anything else round the card
|
|
97
|
+
stickerW: mm((bare ? g.shapeW : g.w * g.scale) + cut),
|
|
98
|
+
stickerH: mm(g.h * g.scale + cut),
|
|
99
|
+
perSheet: perSheet(l),
|
|
100
|
+
gap: l.gap,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const n1 = (v: number) => String(Math.round(v * 10) / 10);
|
|
105
|
+
|
|
106
|
+
/** The same size lines in the print preview and at the foot of every sheet. */
|
|
107
|
+
export function stickerSizeLines(design: StickerDesign, n = STICKER_QR_MODULES): string[] {
|
|
108
|
+
const i = stickerSizeInfo(design, n);
|
|
109
|
+
const shaped = isSilhouette(design.shape);
|
|
110
|
+
return [
|
|
111
|
+
`QR code: ${n1(i.qrMm)} x ${n1(i.qrMm)} mm`,
|
|
112
|
+
shaped ? `Shape: ${n1(i.shapeW)} x ${n1(i.shapeH)} mm` : `Card: ${n1(i.shapeW)} x ${n1(i.shapeH)} mm`,
|
|
113
|
+
`Sticker after cutting: ${n1(i.stickerW)} x ${n1(i.stickerH)} mm`,
|
|
114
|
+
`${i.perSheet} per A4 sheet, ${i.gap} mm gaps`,
|
|
115
|
+
`Print at 100% ("Actual size") - do not use "Fit to page"`,
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
|
|
59
119
|
/** Top-left of the sticker grid inside the printable area, so it sits in the middle of the sheet. */
|
|
60
120
|
function gridOrigin(l: SheetLayout): { x: number; y: number; w: number; h: number } {
|
|
61
121
|
const w = l.cols * l.cellW + (l.cols - 1) * l.gap;
|
|
62
122
|
const h = l.rows * (l.cardH + l.stripH) + (l.rows - 1) * l.gap;
|
|
63
|
-
return { x: (PAGE_W - w) / 2, y: (PAGE_H - h) / 2, w, h };
|
|
123
|
+
return { x: mm((PAGE_W - w) / 2), y: mm((PAGE_H - INFO_H - h) / 2), w, h };
|
|
64
124
|
}
|
|
65
125
|
|
|
66
126
|
/**
|
|
@@ -241,18 +301,39 @@ html, body { margin: 0; padding: 0; background: #fff; color: #111; font-family:
|
|
|
241
301
|
.hint { font-size: 5.8pt; line-height: 1.2; opacity: 0.85; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
|
|
242
302
|
.brand { display: flex; align-items: center; justify-content: center; gap: 1mm; font-size: 6pt; border-top: 0.2mm solid #ccc; width: 100%; padding-top: 1mm; }
|
|
243
303
|
.mark { width: 3.2mm; height: 3.2mm; border-radius: 0.8mm; color: #fff; font-weight: 800; font-size: 5pt; line-height: 3.2mm; text-align: center; }
|
|
244
|
-
.cut { margin-top: 1.2mm; border-top: 0.3mm dashed #666; padding-top:
|
|
304
|
+
.cut { margin-top: 1.2mm; border-top: 0.3mm dashed #666; padding-top: 0.8mm; font-size: 6.5pt; line-height: 1.2; color: #111; height: ${l.stripH - 1.2}mm; overflow: hidden; }
|
|
245
305
|
.cut.free { margin-top: 1.6mm; border-top: 0; padding-top: 0; height: ${l.stripH - 1.6}mm; }
|
|
246
306
|
.cut .owner { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
247
|
-
.
|
|
307
|
+
.info { position: absolute; left: 0; right: 0; bottom: 0; height: ${INFO_H}mm; border-top: 0.2mm solid #bbb; padding-top: 1mm; font-size: 6.5pt; line-height: 1.3; color: #333; display: flex; gap: 3mm; align-items: flex-start; }
|
|
308
|
+
.info .sizes { flex: 1; }
|
|
309
|
+
.info b { color: #000; }
|
|
310
|
+
.info .check { display: flex; flex-direction: column; align-items: flex-start; white-space: nowrap; }
|
|
311
|
+
.info svg { display: block; margin-top: 0.6mm; }
|
|
248
312
|
@media screen { body { background: #e9e9e9; zoom: 0.75; padding: 4mm 0; } .page { background: #fff; margin-bottom: 4mm; box-shadow: 0 0 2mm rgba(0,0,0,.25); } }
|
|
249
313
|
`;
|
|
250
314
|
}
|
|
251
315
|
|
|
316
|
+
/**
|
|
317
|
+
* The foot of every sheet: the sizes, the print-at-100% reminder and a black
|
|
318
|
+
* bar that must measure exactly 10 mm - if it does not, the printer scaled the page.
|
|
319
|
+
*/
|
|
320
|
+
function sheetInfoHtml(design: StickerDesign, n: number): string {
|
|
321
|
+
const lines = stickerSizeLines(design, n);
|
|
322
|
+
const bar = SIZE_CHECK_MM;
|
|
323
|
+
const ticks = Array.from({ length: bar + 1 }, (_, i) => `<line x1="${i}" y1="${i % 5 ? 1.2 : 0}" x2="${i}" y2="2.4"/>`).join("");
|
|
324
|
+
return (
|
|
325
|
+
`<div class="info"><div class="sizes"><b>Sizes:</b> ${lines.slice(0, 4).map(escapeHtml).join(" · ")}` +
|
|
326
|
+
`<br><b>${escapeHtml(lines[4])}.</b> Installer: cut out each sticker; the strip under it says which car.</div>` +
|
|
327
|
+
`<div class="check"><b>Size check: this bar must measure ${bar} mm</b>` +
|
|
328
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.2 0 ${bar + 0.4} 3.4" width="${bar + 0.4}mm" height="3.4mm">` +
|
|
329
|
+
`<rect x="0" y="2.4" width="${bar}" height="1" fill="#000"/><g stroke="#000" stroke-width="0.15">${ticks}</g></svg></div></div>`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
252
333
|
/** Plate font size (mm) that fits the cut-out shape's plate box. */
|
|
253
|
-
function plateSizeMm(plate: string, boxW: number): number {
|
|
334
|
+
function plateSizeMm(plate: string, boxW: number, max = 4.6): number {
|
|
254
335
|
// bold capitals are about 0.74 em wide, plus 0.3 mm letter spacing each
|
|
255
|
-
return Math.round(Math.min(
|
|
336
|
+
return Math.round(Math.min(max, (boxW / Math.max(plate.length, 1) - 0.3) / 0.74) * 100) / 100;
|
|
256
337
|
}
|
|
257
338
|
|
|
258
339
|
/**
|
|
@@ -263,21 +344,25 @@ function plateSizeMm(plate: string, boxW: number): number {
|
|
|
263
344
|
export function buildStickerDocument(
|
|
264
345
|
items: StickerItem[],
|
|
265
346
|
faceSvgs: string[],
|
|
266
|
-
opts: { design?: StickerDesign; siteName?: string } = {},
|
|
347
|
+
opts: { design?: StickerDesign; siteName?: string; modules?: number } = {},
|
|
267
348
|
): string {
|
|
268
349
|
const design = opts.design ?? DEFAULT_STICKER_DESIGN;
|
|
269
|
-
const
|
|
350
|
+
const n = opts.modules ?? STICKER_QR_MODULES;
|
|
351
|
+
const { ink, paper } = stickerTheme(design.theme);
|
|
270
352
|
const label = stickerLabel(design, opts.siteName);
|
|
271
|
-
const layout = sheetLayout(design
|
|
272
|
-
const
|
|
273
|
-
const
|
|
353
|
+
const layout = sheetLayout(design);
|
|
354
|
+
const g = stickerGeometry(design, n);
|
|
355
|
+
const k = g.scale;
|
|
356
|
+
// Shaped codes: plate (and words) in paper colour on the ink tag at the bottom.
|
|
357
|
+
const tag = g.band ? { x: mm(g.band.x * k), y: mm(g.band.y * k), w: mm(g.band.w * k), h: mm(g.band.h * k) } : null;
|
|
358
|
+
const bare = !!tag && design.frame === "none";
|
|
359
|
+
const wordsTop = mm(((g.qrTop ?? 0) + QR_MM + 0.8) * k);
|
|
274
360
|
const cells = items.map((item, i) => {
|
|
275
361
|
const { place, owner } = installerLine(item);
|
|
276
362
|
const plate = escapeHtml(item.plateNumber);
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
`<div class="plate" style="font-size:${plateSizeMm(text(item.plateNumber), shape.band.w)}mm">${plate}</div>` +
|
|
363
|
+
const words = tag
|
|
364
|
+
? `<div class="words band" style="left:${tag.x}mm;right:auto;top:${tag.y}mm;width:${tag.w}mm;height:${tag.h}mm;color:${paper}">` +
|
|
365
|
+
`<div class="plate" style="font-size:${plateSizeMm(text(item.plateNumber), tag.w, 4.6 * k)}mm">${plate}</div>` +
|
|
281
366
|
(label ? `<div class="hint">${escapeHtml(label)}</div>` : "") +
|
|
282
367
|
`</div>`
|
|
283
368
|
: `<div class="words" style="top:${wordsTop}mm">` +
|
|
@@ -290,12 +375,12 @@ export function buildStickerDocument(
|
|
|
290
375
|
`<div class="face">${faceSvgs[i] ?? ""}</div>` +
|
|
291
376
|
words +
|
|
292
377
|
`</div>` +
|
|
293
|
-
`<div class="cut${
|
|
294
|
-
`<div class="owner">Owner: ${escapeHtml(owner || "-")}</div>`
|
|
295
|
-
`<div class="note">${shape ? "Installer: cut out the sticker, then paste on this car" : "Installer: cut here, then paste on this car"}</div></div></div>`
|
|
378
|
+
`<div class="cut${bare ? " free" : ""}"><div><b>${escapeHtml(place || "-")}</b></div>` +
|
|
379
|
+
`<div class="owner">Owner: ${escapeHtml(owner || "-")}</div></div></div>`
|
|
296
380
|
);
|
|
297
381
|
});
|
|
298
382
|
const marks = cropMarksSvg(layout);
|
|
299
|
-
const
|
|
383
|
+
const info = sheetInfoHtml(design, n);
|
|
384
|
+
const pages = stickerPages(cells, perSheet(layout)).map((p) => `<div class="page">${marks}${p.join("")}${info}</div>`);
|
|
300
385
|
return `<!doctype html><html><head><meta charset="utf-8"><title>Vehicle QR stickers</title><style>${printCss(layout)}</style></head><body>${pages.join("")}</body></html>`;
|
|
301
386
|
}
|