taro-bluetooth-print 2.9.0 → 2.9.2
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/CHANGELOG.md +16 -1
- package/README.md +53 -4
- package/dist/index.cjs.js +1 -1
- package/dist/index.es.js +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/types/core/di/Container.d.ts +84 -0
- package/dist/types/core/di/Tokens.d.ts +29 -0
- package/dist/types/core/di/index.d.ts +3 -0
- package/dist/types/core/event/EventBus.d.ts +66 -0
- package/dist/types/core/event/index.d.ts +2 -0
- package/dist/types/core/index.d.ts +5 -4
- package/dist/types/core/plugin/PluginManager.d.ts +64 -0
- package/dist/types/core/plugin/index.d.ts +2 -0
- package/dist/types/device/MultiPrinterManager.d.ts +2 -0
- package/dist/types/factory/di-factory.d.ts +52 -0
- package/dist/types/index.d.ts +5 -1
- package/dist/types/providers/ServiceProvider.d.ts +56 -0
- package/dist/types/providers/index.d.ts +2 -0
- package/dist/types/template/TemplateEngine.d.ts +24 -68
- package/dist/types/template/engines/TemplateRenderer.d.ts +71 -0
- package/dist/types/template/parsers/TemplateParser.d.ts +23 -0
- package/dist/types/utils/index.d.ts +8 -0
- package/dist/types/utils/logger.d.ts +4 -3
- package/dist/types/utils/outputLimiter.d.ts +87 -0
- package/dist/types/utils/validation.d.ts +11 -309
- package/dist/types/utils/validators/array.d.ts +19 -0
- package/dist/types/utils/validators/buffer.d.ts +18 -0
- package/dist/types/utils/validators/chain.d.ts +31 -0
- package/dist/types/utils/validators/common.d.ts +22 -0
- package/dist/types/utils/validators/number.d.ts +20 -0
- package/dist/types/utils/validators/object.d.ts +24 -0
- package/dist/types/utils/validators/printer.d.ts +40 -0
- package/dist/types/utils/validators/types.d.ts +125 -0
- package/dist/types/utils/validators/uuid.d.ts +23 -0
- package/package.json +1 -1
- package/src/core/BluetoothPrinter.ts +2 -1
- package/src/core/di/Container.ts +332 -0
- package/src/core/di/Tokens.ts +45 -0
- package/src/core/di/index.ts +3 -0
- package/src/core/event/EventBus.ts +251 -0
- package/src/core/event/index.ts +2 -0
- package/src/core/index.ts +10 -4
- package/src/core/plugin/PluginManager.ts +161 -0
- package/src/core/plugin/index.ts +2 -0
- package/src/device/MultiPrinterManager.ts +15 -6
- package/src/factory/di-factory.ts +61 -0
- package/src/index.ts +50 -1
- package/src/providers/ServiceProvider.ts +213 -0
- package/src/providers/index.ts +2 -0
- package/src/template/TemplateEngine.ts +27 -792
- package/src/template/engines/TemplateRenderer.ts +762 -0
- package/src/template/parsers/TemplateParser.ts +94 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/logger.ts +17 -4
- package/src/utils/outputLimiter.ts +227 -0
- package/src/utils/validation.ts +21 -1138
- package/src/utils/validators/array.ts +95 -0
- package/src/utils/validators/buffer.ts +81 -0
- package/src/utils/validators/chain.ts +181 -0
- package/src/utils/validators/common.ts +216 -0
- package/src/utils/validators/number.ts +101 -0
- package/src/utils/validators/object.ts +63 -0
- package/src/utils/validators/printer.ts +294 -0
- package/src/utils/validators/types.ts +105 -0
- package/src/utils/validators/uuid.ts +49 -0
|
@@ -16,9 +16,8 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { Logger } from '@/utils/logger';
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import { EscPos } from '@/drivers/EscPos';
|
|
19
|
+
import { TemplateParser } from './parsers/TemplateParser';
|
|
20
|
+
import { TemplateRenderer } from './engines/TemplateRenderer';
|
|
22
21
|
|
|
23
22
|
/**
|
|
24
23
|
* Template type
|
|
@@ -90,7 +89,7 @@ export interface LabelData {
|
|
|
90
89
|
name: string;
|
|
91
90
|
price: number;
|
|
92
91
|
barcode?: string;
|
|
93
|
-
barcodeFormat?: BarcodeFormat;
|
|
92
|
+
barcodeFormat?: import('@/barcode').BarcodeFormat;
|
|
94
93
|
spec?: string;
|
|
95
94
|
productionDate?: string;
|
|
96
95
|
expiryDate?: string;
|
|
@@ -196,9 +195,9 @@ export interface TableColumn {
|
|
|
196
195
|
/** Width of column in characters */
|
|
197
196
|
width: number;
|
|
198
197
|
/** Text alignment for header */
|
|
199
|
-
headerAlign?: TextAlign;
|
|
198
|
+
headerAlign?: import('@/formatter').TextAlign;
|
|
200
199
|
/** Text alignment for cells */
|
|
201
|
-
cellAlign?: TextAlign;
|
|
200
|
+
cellAlign?: import('@/formatter').TextAlign;
|
|
202
201
|
}
|
|
203
202
|
|
|
204
203
|
/**
|
|
@@ -228,11 +227,22 @@ export interface TableElement {
|
|
|
228
227
|
* Template element types
|
|
229
228
|
*/
|
|
230
229
|
export type TemplateElement =
|
|
231
|
-
| {
|
|
230
|
+
| {
|
|
231
|
+
type: 'text';
|
|
232
|
+
content: string;
|
|
233
|
+
align?: import('@/formatter').TextAlign;
|
|
234
|
+
size?: number;
|
|
235
|
+
bold?: boolean;
|
|
236
|
+
}
|
|
232
237
|
| { type: 'line'; char?: string; length?: number }
|
|
233
238
|
| { type: 'image'; data: Uint8Array; width: number; height: number }
|
|
234
239
|
| { type: 'qrcode'; content: string; size?: number }
|
|
235
|
-
| {
|
|
240
|
+
| {
|
|
241
|
+
type: 'barcode';
|
|
242
|
+
content: string;
|
|
243
|
+
format: import('@/barcode').BarcodeFormat;
|
|
244
|
+
height?: number;
|
|
245
|
+
}
|
|
236
246
|
| { type: 'feed'; lines: number }
|
|
237
247
|
| { type: 'variable'; name: string; format?: string }
|
|
238
248
|
| LoopElement
|
|
@@ -273,314 +283,43 @@ export interface ITemplateEngine {
|
|
|
273
283
|
validate(template: TemplateDefinition, data: Record<string, unknown>): ValidationResult;
|
|
274
284
|
}
|
|
275
285
|
|
|
276
|
-
/**
|
|
277
|
-
* Border style character sets
|
|
278
|
-
*/
|
|
279
|
-
const BORDER_CHARS: Record<
|
|
280
|
-
BorderStyle,
|
|
281
|
-
{
|
|
282
|
-
topLeft: string;
|
|
283
|
-
topRight: string;
|
|
284
|
-
bottomLeft: string;
|
|
285
|
-
bottomRight: string;
|
|
286
|
-
top: string;
|
|
287
|
-
bottom: string;
|
|
288
|
-
left: string;
|
|
289
|
-
right: string;
|
|
290
|
-
cross: string;
|
|
291
|
-
}
|
|
292
|
-
> = {
|
|
293
|
-
single: {
|
|
294
|
-
topLeft: '+',
|
|
295
|
-
topRight: '+',
|
|
296
|
-
bottomLeft: '+',
|
|
297
|
-
bottomRight: '+',
|
|
298
|
-
top: '-',
|
|
299
|
-
bottom: '-',
|
|
300
|
-
left: '|',
|
|
301
|
-
right: '|',
|
|
302
|
-
cross: '+',
|
|
303
|
-
},
|
|
304
|
-
double: {
|
|
305
|
-
topLeft: '╔',
|
|
306
|
-
topRight: '╗',
|
|
307
|
-
bottomLeft: '╚',
|
|
308
|
-
bottomRight: '╝',
|
|
309
|
-
top: '═',
|
|
310
|
-
bottom: '═',
|
|
311
|
-
left: '║',
|
|
312
|
-
right: '║',
|
|
313
|
-
cross: '╬',
|
|
314
|
-
},
|
|
315
|
-
thick: {
|
|
316
|
-
topLeft: '┏',
|
|
317
|
-
topRight: '┓',
|
|
318
|
-
bottomLeft: '┗',
|
|
319
|
-
bottomRight: '┛',
|
|
320
|
-
top: '━',
|
|
321
|
-
bottom: '━',
|
|
322
|
-
left: '┃',
|
|
323
|
-
right: '┃',
|
|
324
|
-
cross: '╋',
|
|
325
|
-
},
|
|
326
|
-
rounded: {
|
|
327
|
-
topLeft: '╭',
|
|
328
|
-
topRight: '╮',
|
|
329
|
-
bottomLeft: '╰',
|
|
330
|
-
bottomRight: '╯',
|
|
331
|
-
top: '─',
|
|
332
|
-
bottom: '─',
|
|
333
|
-
left: '│',
|
|
334
|
-
right: '│',
|
|
335
|
-
cross: '┼',
|
|
336
|
-
},
|
|
337
|
-
dashed: {
|
|
338
|
-
topLeft: '+',
|
|
339
|
-
topRight: '+',
|
|
340
|
-
bottomLeft: '+',
|
|
341
|
-
bottomRight: '+',
|
|
342
|
-
top: '-',
|
|
343
|
-
bottom: '-',
|
|
344
|
-
left: ':',
|
|
345
|
-
right: ':',
|
|
346
|
-
cross: '+',
|
|
347
|
-
},
|
|
348
|
-
none: {
|
|
349
|
-
topLeft: ' ',
|
|
350
|
-
topRight: ' ',
|
|
351
|
-
bottomLeft: ' ',
|
|
352
|
-
bottomRight: ' ',
|
|
353
|
-
top: ' ',
|
|
354
|
-
bottom: ' ',
|
|
355
|
-
left: ' ',
|
|
356
|
-
right: ' ',
|
|
357
|
-
cross: ' ',
|
|
358
|
-
},
|
|
359
|
-
};
|
|
360
|
-
|
|
361
286
|
/**
|
|
362
287
|
* Template Engine class
|
|
363
|
-
*
|
|
288
|
+
* Facade for template parsing and rendering
|
|
364
289
|
*/
|
|
365
290
|
export class TemplateEngine implements ITemplateEngine {
|
|
366
291
|
private readonly logger = Logger.scope('TemplateEngine');
|
|
367
|
-
private readonly
|
|
368
|
-
private readonly
|
|
369
|
-
private readonly driver: EscPos;
|
|
292
|
+
private readonly parser: TemplateParser;
|
|
293
|
+
private readonly renderer: TemplateRenderer;
|
|
370
294
|
private readonly templates: Map<string, TemplateDefinition> = new Map();
|
|
371
|
-
private readonly paperWidth: number;
|
|
372
295
|
|
|
373
296
|
/**
|
|
374
297
|
* Creates a new TemplateEngine instance
|
|
375
298
|
*/
|
|
376
299
|
constructor(paperWidth = 48) {
|
|
377
|
-
this.
|
|
378
|
-
this.
|
|
379
|
-
this.barcodeGenerator = new BarcodeGenerator();
|
|
380
|
-
this.driver = new EscPos();
|
|
300
|
+
this.parser = new TemplateParser();
|
|
301
|
+
this.renderer = new TemplateRenderer(paperWidth);
|
|
381
302
|
}
|
|
382
303
|
|
|
383
304
|
/**
|
|
384
305
|
* Render a receipt template
|
|
385
306
|
*/
|
|
386
307
|
renderReceipt(data: ReceiptData): Uint8Array {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
// Initialize printer
|
|
390
|
-
commands.push(...this.driver.init());
|
|
391
|
-
|
|
392
|
-
// Store header
|
|
393
|
-
commands.push(...this.formatter.align(TextAlign.CENTER));
|
|
394
|
-
commands.push(...this.formatter.setSize(2, 2));
|
|
395
|
-
commands.push(...this.formatter.setBold(true));
|
|
396
|
-
commands.push(...this.driver.text(data.store.name));
|
|
397
|
-
commands.push(...this.driver.feed(1));
|
|
398
|
-
commands.push(...this.formatter.resetStyle());
|
|
399
|
-
|
|
400
|
-
// Store address and phone
|
|
401
|
-
if (data.store.address) {
|
|
402
|
-
commands.push(...this.formatter.align(TextAlign.CENTER));
|
|
403
|
-
commands.push(...this.driver.text(data.store.address));
|
|
404
|
-
commands.push(...this.driver.feed(1));
|
|
405
|
-
}
|
|
406
|
-
if (data.store.phone) {
|
|
407
|
-
commands.push(...this.formatter.align(TextAlign.CENTER));
|
|
408
|
-
commands.push(...this.driver.text(`电话: ${data.store.phone}`));
|
|
409
|
-
commands.push(...this.driver.feed(1));
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// Separator line
|
|
413
|
-
commands.push(...this.renderLine());
|
|
414
|
-
|
|
415
|
-
// Order info
|
|
416
|
-
if (data.order) {
|
|
417
|
-
commands.push(...this.formatter.align(TextAlign.LEFT));
|
|
418
|
-
commands.push(...this.driver.text(`订单号: ${data.order.id}`));
|
|
419
|
-
commands.push(...this.driver.feed(1));
|
|
420
|
-
commands.push(...this.driver.text(`日期: ${data.order.date}`));
|
|
421
|
-
commands.push(...this.driver.feed(1));
|
|
422
|
-
if (data.order.cashier) {
|
|
423
|
-
commands.push(...this.driver.text(`收银员: ${data.order.cashier}`));
|
|
424
|
-
commands.push(...this.driver.feed(1));
|
|
425
|
-
}
|
|
426
|
-
commands.push(...this.renderLine());
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// Items header
|
|
430
|
-
commands.push(...this.formatter.align(TextAlign.LEFT));
|
|
431
|
-
commands.push(...this.formatter.setBold(true));
|
|
432
|
-
commands.push(...this.driver.text(this.formatItemLine('商品', '数量', '金额')));
|
|
433
|
-
commands.push(...this.driver.feed(1));
|
|
434
|
-
commands.push(...this.formatter.setBold(false));
|
|
435
|
-
commands.push(...this.renderLine('-'));
|
|
436
|
-
|
|
437
|
-
// Items
|
|
438
|
-
for (const item of data.items) {
|
|
439
|
-
const amount = item.quantity * item.price - (item.discount ?? 0);
|
|
440
|
-
commands.push(...this.driver.text(item.name));
|
|
441
|
-
commands.push(...this.driver.feed(1));
|
|
442
|
-
commands.push(
|
|
443
|
-
...this.driver.text(this.formatItemLine('', `x${item.quantity}`, `¥${amount.toFixed(2)}`))
|
|
444
|
-
);
|
|
445
|
-
commands.push(...this.driver.feed(1));
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
commands.push(...this.renderLine());
|
|
449
|
-
|
|
450
|
-
// Payment summary
|
|
451
|
-
commands.push(...this.formatter.align(TextAlign.RIGHT));
|
|
452
|
-
if (data.payment.subtotal !== data.payment.total) {
|
|
453
|
-
commands.push(...this.driver.text(`小计: ¥${data.payment.subtotal.toFixed(2)}`));
|
|
454
|
-
commands.push(...this.driver.feed(1));
|
|
455
|
-
}
|
|
456
|
-
if (data.payment.tax) {
|
|
457
|
-
commands.push(...this.driver.text(`税额: ¥${data.payment.tax.toFixed(2)}`));
|
|
458
|
-
commands.push(...this.driver.feed(1));
|
|
459
|
-
}
|
|
460
|
-
if (data.payment.discount) {
|
|
461
|
-
commands.push(...this.driver.text(`优惠: -¥${data.payment.discount.toFixed(2)}`));
|
|
462
|
-
commands.push(...this.driver.feed(1));
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
commands.push(...this.formatter.setBold(true));
|
|
466
|
-
commands.push(...this.formatter.setSize(1, 2));
|
|
467
|
-
commands.push(...this.driver.text(`合计: ¥${data.payment.total.toFixed(2)}`));
|
|
468
|
-
commands.push(...this.driver.feed(1));
|
|
469
|
-
commands.push(...this.formatter.resetStyle());
|
|
470
|
-
|
|
471
|
-
commands.push(...this.formatter.align(TextAlign.RIGHT));
|
|
472
|
-
commands.push(...this.driver.text(`支付方式: ${data.payment.method}`));
|
|
473
|
-
commands.push(...this.driver.feed(1));
|
|
474
|
-
|
|
475
|
-
if (data.payment.received !== undefined) {
|
|
476
|
-
commands.push(...this.driver.text(`实收: ¥${data.payment.received.toFixed(2)}`));
|
|
477
|
-
commands.push(...this.driver.feed(1));
|
|
478
|
-
}
|
|
479
|
-
if (data.payment.change !== undefined) {
|
|
480
|
-
commands.push(...this.driver.text(`找零: ¥${data.payment.change.toFixed(2)}`));
|
|
481
|
-
commands.push(...this.driver.feed(1));
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
commands.push(...this.renderLine());
|
|
485
|
-
|
|
486
|
-
// QR Code
|
|
487
|
-
if (data.qrCode) {
|
|
488
|
-
commands.push(...this.formatter.align(TextAlign.CENTER));
|
|
489
|
-
commands.push(...this.driver.qr(data.qrCode, { size: 6 }));
|
|
490
|
-
commands.push(...this.driver.feed(1));
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// Footer
|
|
494
|
-
if (data.footer) {
|
|
495
|
-
commands.push(...this.formatter.align(TextAlign.CENTER));
|
|
496
|
-
commands.push(...this.driver.text(data.footer));
|
|
497
|
-
commands.push(...this.driver.feed(1));
|
|
498
|
-
} else {
|
|
499
|
-
commands.push(...this.formatter.align(TextAlign.CENTER));
|
|
500
|
-
commands.push(...this.driver.text('谢谢惠顾,欢迎再次光临!'));
|
|
501
|
-
commands.push(...this.driver.feed(1));
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
commands.push(...this.driver.feed(3));
|
|
505
|
-
commands.push(...this.driver.cut());
|
|
506
|
-
|
|
507
|
-
return this.combineCommands(commands);
|
|
308
|
+
return this.renderer.renderReceipt(data);
|
|
508
309
|
}
|
|
509
310
|
|
|
510
311
|
/**
|
|
511
312
|
* Render a label template
|
|
512
313
|
*/
|
|
513
314
|
renderLabel(data: LabelData): Uint8Array {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
// Initialize printer
|
|
517
|
-
commands.push(...this.driver.init());
|
|
518
|
-
|
|
519
|
-
// Product name
|
|
520
|
-
commands.push(...this.formatter.align(TextAlign.CENTER));
|
|
521
|
-
commands.push(...this.formatter.setBold(true));
|
|
522
|
-
commands.push(...this.driver.text(data.name));
|
|
523
|
-
commands.push(...this.driver.feed(1));
|
|
524
|
-
commands.push(...this.formatter.setBold(false));
|
|
525
|
-
|
|
526
|
-
// Spec
|
|
527
|
-
if (data.spec) {
|
|
528
|
-
commands.push(...this.driver.text(data.spec));
|
|
529
|
-
commands.push(...this.driver.feed(1));
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// Price
|
|
533
|
-
commands.push(...this.formatter.setSize(2, 2));
|
|
534
|
-
commands.push(...this.driver.text(`¥${data.price.toFixed(2)}`));
|
|
535
|
-
commands.push(...this.driver.feed(1));
|
|
536
|
-
commands.push(...this.formatter.resetStyle());
|
|
537
|
-
|
|
538
|
-
// Dates
|
|
539
|
-
commands.push(...this.formatter.align(TextAlign.LEFT));
|
|
540
|
-
if (data.productionDate) {
|
|
541
|
-
commands.push(...this.driver.text(`生产日期: ${data.productionDate}`));
|
|
542
|
-
commands.push(...this.driver.feed(1));
|
|
543
|
-
}
|
|
544
|
-
if (data.expiryDate) {
|
|
545
|
-
commands.push(...this.driver.text(`保质期至: ${data.expiryDate}`));
|
|
546
|
-
commands.push(...this.driver.feed(1));
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
// Barcode
|
|
550
|
-
if (data.barcode) {
|
|
551
|
-
commands.push(...this.formatter.align(TextAlign.CENTER));
|
|
552
|
-
const barcodeCommands = this.barcodeGenerator.generate(data.barcode, {
|
|
553
|
-
format: data.barcodeFormat ?? BarcodeFormat.CODE128,
|
|
554
|
-
height: 60,
|
|
555
|
-
showText: true,
|
|
556
|
-
});
|
|
557
|
-
commands.push(...barcodeCommands);
|
|
558
|
-
commands.push(...this.driver.feed(1));
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
commands.push(...this.driver.feed(2));
|
|
562
|
-
commands.push(...this.driver.cut());
|
|
563
|
-
|
|
564
|
-
return this.combineCommands(commands);
|
|
315
|
+
return this.renderer.renderLabel(data);
|
|
565
316
|
}
|
|
566
317
|
|
|
567
318
|
/**
|
|
568
319
|
* Render a custom template
|
|
569
320
|
*/
|
|
570
321
|
render(template: TemplateDefinition, data: Record<string, unknown>): Uint8Array {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
// Initialize printer
|
|
574
|
-
commands.push(...this.driver.init());
|
|
575
|
-
|
|
576
|
-
for (const element of template.elements) {
|
|
577
|
-
commands.push(...this.renderElement(element, data));
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
commands.push(...this.driver.feed(2));
|
|
581
|
-
commands.push(...this.driver.cut());
|
|
582
|
-
|
|
583
|
-
return this.combineCommands(commands);
|
|
322
|
+
return this.renderer.render(template, data);
|
|
584
323
|
}
|
|
585
324
|
|
|
586
325
|
/**
|
|
@@ -602,511 +341,7 @@ export class TemplateEngine implements ITemplateEngine {
|
|
|
602
341
|
* Validate template data
|
|
603
342
|
*/
|
|
604
343
|
validate(template: TemplateDefinition, data: Record<string, unknown>): ValidationResult {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
for (const element of template.elements) {
|
|
608
|
-
if (element.type === 'variable') {
|
|
609
|
-
const value = this.getNestedValue(data, element.name);
|
|
610
|
-
if (value === undefined) {
|
|
611
|
-
errors.push({
|
|
612
|
-
field: element.name,
|
|
613
|
-
message: `Missing required variable: ${element.name}`,
|
|
614
|
-
code: 'MISSING_VARIABLE',
|
|
615
|
-
});
|
|
616
|
-
}
|
|
617
|
-
} else if (element.type === 'loop') {
|
|
618
|
-
const items = this.getNestedValue(data, element.items);
|
|
619
|
-
if (!Array.isArray(items)) {
|
|
620
|
-
errors.push({
|
|
621
|
-
field: element.items,
|
|
622
|
-
message: `Loop variable '${element.items}' must be an array`,
|
|
623
|
-
code: 'INVALID_LOOP_VARIABLE',
|
|
624
|
-
});
|
|
625
|
-
}
|
|
626
|
-
} else if (element.type === 'condition') {
|
|
627
|
-
// Condition validation is optional - conditions can reference missing data
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
return { valid: errors.length === 0, errors };
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
/**
|
|
635
|
-
* Render a single template element
|
|
636
|
-
*/
|
|
637
|
-
private renderElement(element: TemplateElement, data: Record<string, unknown>): Uint8Array[] {
|
|
638
|
-
switch (element.type) {
|
|
639
|
-
case 'loop':
|
|
640
|
-
return this.renderLoop(element, data);
|
|
641
|
-
case 'condition':
|
|
642
|
-
return this.renderCondition(element, data);
|
|
643
|
-
case 'border':
|
|
644
|
-
return this.renderBorder(element);
|
|
645
|
-
case 'table':
|
|
646
|
-
return this.renderTable(element, data);
|
|
647
|
-
default:
|
|
648
|
-
return this.renderStandardElement(element, data);
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
/**
|
|
653
|
-
* Render a loop element
|
|
654
|
-
*/
|
|
655
|
-
private renderLoop(loop: LoopElement, data: Record<string, unknown>): Uint8Array[] {
|
|
656
|
-
const commands: Uint8Array[] = [];
|
|
657
|
-
const items = this.getNestedValue(data, loop.items);
|
|
658
|
-
|
|
659
|
-
if (!Array || !Array.isArray(items)) {
|
|
660
|
-
this.logger.warn(`Loop variable '${loop.items}' is not an array`);
|
|
661
|
-
return commands;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
for (let i = 0; i < items.length; i++) {
|
|
665
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
|
|
666
|
-
const itemData: any = items[i];
|
|
667
|
-
|
|
668
|
-
// Create iteration context with item and optionally index
|
|
669
|
-
const context: Record<string, unknown> = {
|
|
670
|
-
...data,
|
|
671
|
-
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
672
|
-
[loop.itemVar]: itemData,
|
|
673
|
-
/* eslint-enable @typescript-eslint/no-unsafe-assignment */
|
|
674
|
-
};
|
|
675
|
-
|
|
676
|
-
if (loop.indexVar) {
|
|
677
|
-
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
678
|
-
context[loop.indexVar] = i;
|
|
679
|
-
/* eslint-enable @typescript-eslint/no-unsafe-assignment */
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
// Render each element in the loop
|
|
683
|
-
for (const childElement of loop.elements) {
|
|
684
|
-
commands.push(...this.renderElement(childElement, context));
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
// Add separator between items (but not after the last)
|
|
688
|
-
if (loop.separator && i < items.length - 1) {
|
|
689
|
-
commands.push(...this.driver.text(loop.separator));
|
|
690
|
-
commands.push(...this.driver.feed(1));
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
return commands;
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
/**
|
|
698
|
-
* Render a condition element
|
|
699
|
-
*/
|
|
700
|
-
private renderCondition(
|
|
701
|
-
condition: ConditionElement,
|
|
702
|
-
data: Record<string, unknown>
|
|
703
|
-
): Uint8Array[] {
|
|
704
|
-
const commands: Uint8Array[] = [];
|
|
705
|
-
const value = this.getNestedValue(data, condition.variable);
|
|
706
|
-
const result = this.evaluateCondition(value, condition.operator, condition.value);
|
|
707
|
-
|
|
708
|
-
const elementsToRender = result ? condition.then : (condition.else ?? []);
|
|
709
|
-
|
|
710
|
-
for (const childElement of elementsToRender) {
|
|
711
|
-
commands.push(...this.renderElement(childElement, data));
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
return commands;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
/**
|
|
718
|
-
* Evaluate a condition
|
|
719
|
-
*/
|
|
720
|
-
private evaluateCondition(
|
|
721
|
-
value: unknown,
|
|
722
|
-
operator: ConditionElement['operator'],
|
|
723
|
-
compareValue?: unknown
|
|
724
|
-
): boolean {
|
|
725
|
-
switch (operator) {
|
|
726
|
-
case 'exists':
|
|
727
|
-
return value !== undefined && value !== null;
|
|
728
|
-
case 'not_exists':
|
|
729
|
-
return value === undefined || value === null;
|
|
730
|
-
case 'equals':
|
|
731
|
-
return value === compareValue;
|
|
732
|
-
case 'not_equals':
|
|
733
|
-
return value !== compareValue;
|
|
734
|
-
case 'gt':
|
|
735
|
-
return (
|
|
736
|
-
typeof value === 'number' && typeof compareValue === 'number' && value > compareValue
|
|
737
|
-
);
|
|
738
|
-
case 'gte':
|
|
739
|
-
return (
|
|
740
|
-
typeof value === 'number' && typeof compareValue === 'number' && value >= compareValue
|
|
741
|
-
);
|
|
742
|
-
case 'lt':
|
|
743
|
-
return (
|
|
744
|
-
typeof value === 'number' && typeof compareValue === 'number' && value < compareValue
|
|
745
|
-
);
|
|
746
|
-
case 'lte':
|
|
747
|
-
return (
|
|
748
|
-
typeof value === 'number' && typeof compareValue === 'number' && value <= compareValue
|
|
749
|
-
);
|
|
750
|
-
case 'truthy':
|
|
751
|
-
return Boolean(value);
|
|
752
|
-
case 'falsy':
|
|
753
|
-
return !value;
|
|
754
|
-
default:
|
|
755
|
-
return false;
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
/**
|
|
760
|
-
* Render a border element
|
|
761
|
-
*/
|
|
762
|
-
private renderBorder(border: BorderElement): Uint8Array[] {
|
|
763
|
-
const commands: Uint8Array[] = [];
|
|
764
|
-
const style = BORDER_CHARS[border.style ?? 'single'];
|
|
765
|
-
const width = this.paperWidth;
|
|
766
|
-
const padding = border.padding ?? 0;
|
|
767
|
-
|
|
768
|
-
// Build custom or default characters
|
|
769
|
-
const tl = border.topLeft ?? style.topLeft;
|
|
770
|
-
const tr = border.topRight ?? style.topRight;
|
|
771
|
-
const bl = border.bottomLeft ?? style.bottomLeft;
|
|
772
|
-
const br = border.bottomRight ?? style.bottomRight;
|
|
773
|
-
const t = border.top ?? style.top;
|
|
774
|
-
const b = border.bottom ?? style.bottom;
|
|
775
|
-
const l = border.left ?? style.left;
|
|
776
|
-
const r = border.right ?? style.right;
|
|
777
|
-
|
|
778
|
-
// Top border
|
|
779
|
-
if (border.drawTop !== false) {
|
|
780
|
-
const topLine = tl + t.repeat(width - 2) + tr;
|
|
781
|
-
commands.push(...this.driver.text(topLine));
|
|
782
|
-
commands.push(...this.driver.feed(1));
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
// Middle lines with optional fill
|
|
786
|
-
if (border.filled) {
|
|
787
|
-
const innerWidth = width - 2 - padding * 2;
|
|
788
|
-
const fillChar = ' ';
|
|
789
|
-
for (let i = 0; i < padding; i++) {
|
|
790
|
-
const fillLine = l + fillChar.repeat(width - 2) + r;
|
|
791
|
-
commands.push(...this.driver.text(fillLine));
|
|
792
|
-
commands.push(...this.driver.feed(1));
|
|
793
|
-
}
|
|
794
|
-
const contentLine =
|
|
795
|
-
l + fillChar.repeat(padding) + fillChar.repeat(innerWidth) + fillChar.repeat(padding) + r;
|
|
796
|
-
commands.push(...this.driver.text(contentLine));
|
|
797
|
-
commands.push(...this.driver.feed(1));
|
|
798
|
-
for (let i = 0; i < padding; i++) {
|
|
799
|
-
const fillLine = l + fillChar.repeat(width - 2) + r;
|
|
800
|
-
commands.push(...this.driver.text(fillLine));
|
|
801
|
-
commands.push(...this.driver.feed(1));
|
|
802
|
-
}
|
|
803
|
-
} else if (border.drawLeft || border.drawRight) {
|
|
804
|
-
const middleLine =
|
|
805
|
-
(border.drawLeft !== false ? l : ' ') +
|
|
806
|
-
' '.repeat(width - 2) +
|
|
807
|
-
(border.drawRight !== false ? r : ' ');
|
|
808
|
-
commands.push(...this.driver.text(middleLine));
|
|
809
|
-
commands.push(...this.driver.feed(1));
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
// Bottom border
|
|
813
|
-
if (border.drawBottom !== false) {
|
|
814
|
-
const bottomLine = bl + b.repeat(width - 2) + br;
|
|
815
|
-
commands.push(...this.driver.text(bottomLine));
|
|
816
|
-
commands.push(...this.driver.feed(1));
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
return commands;
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
/**
|
|
823
|
-
* Render a table element
|
|
824
|
-
*/
|
|
825
|
-
private renderTable(table: TableElement, data: Record<string, unknown>): Uint8Array[] {
|
|
826
|
-
const commands: Uint8Array[] = [];
|
|
827
|
-
const rows = this.getNestedValue(data, table.rowsVar);
|
|
828
|
-
|
|
829
|
-
if (!Array.isArray(rows)) {
|
|
830
|
-
this.logger.warn(`Table rows variable '${table.rowsVar}' is not an array`);
|
|
831
|
-
return commands;
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
const style = BORDER_CHARS[table.borderStyle ?? 'single'];
|
|
835
|
-
|
|
836
|
-
// Draw top border
|
|
837
|
-
if (table.showHeader) {
|
|
838
|
-
const topLine =
|
|
839
|
-
style.topLeft +
|
|
840
|
-
table.columns
|
|
841
|
-
.map(col => {
|
|
842
|
-
const width = col.width + 1;
|
|
843
|
-
return style.top.repeat(width);
|
|
844
|
-
})
|
|
845
|
-
.join('') +
|
|
846
|
-
style.topRight;
|
|
847
|
-
commands.push(...this.driver.text(topLine));
|
|
848
|
-
commands.push(...this.driver.feed(1));
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
// Draw header row
|
|
852
|
-
if (table.showHeader) {
|
|
853
|
-
let headerContent = style.left + ' ';
|
|
854
|
-
table.columns.forEach((col, i) => {
|
|
855
|
-
const cellText = col.header.substring(0, col.width);
|
|
856
|
-
const aligned = this.alignText(cellText, col.width, col.headerAlign ?? TextAlign.LEFT);
|
|
857
|
-
headerContent +=
|
|
858
|
-
aligned + (i < table.columns.length - 1 ? style.cross + ' ' : ' ' + style.right);
|
|
859
|
-
});
|
|
860
|
-
commands.push(...this.driver.text(headerContent));
|
|
861
|
-
commands.push(...this.driver.feed(1));
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
// Draw separator after header
|
|
865
|
-
if (table.showHeader) {
|
|
866
|
-
const sepLine =
|
|
867
|
-
style.cross +
|
|
868
|
-
table.columns
|
|
869
|
-
.map(col => {
|
|
870
|
-
return style.bottom.repeat(col.width + 1);
|
|
871
|
-
})
|
|
872
|
-
.join('') +
|
|
873
|
-
style.cross;
|
|
874
|
-
commands.push(...this.driver.text(sepLine));
|
|
875
|
-
commands.push(...this.driver.feed(1));
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
// Draw data rows
|
|
879
|
-
rows.forEach((rowData, rowIndex) => {
|
|
880
|
-
const row = rowData as TableRowData;
|
|
881
|
-
let rowContent = style.left + ' ';
|
|
882
|
-
|
|
883
|
-
table.columns.forEach((col, colIndex) => {
|
|
884
|
-
const cellValue = row[col.header] ?? '';
|
|
885
|
-
const cellText = String(cellValue).substring(0, col.width);
|
|
886
|
-
const aligned = this.alignText(cellText, col.width, col.cellAlign ?? TextAlign.LEFT);
|
|
887
|
-
rowContent +=
|
|
888
|
-
aligned + (colIndex < table.columns.length - 1 ? style.cross + ' ' : ' ' + style.right);
|
|
889
|
-
});
|
|
890
|
-
|
|
891
|
-
commands.push(...this.driver.text(rowContent));
|
|
892
|
-
commands.push(...this.driver.feed(1));
|
|
893
|
-
|
|
894
|
-
// Draw row separator
|
|
895
|
-
if (rowIndex < rows.length - 1) {
|
|
896
|
-
const sepLine =
|
|
897
|
-
style.cross +
|
|
898
|
-
table.columns
|
|
899
|
-
.map(col => {
|
|
900
|
-
return style.bottom.repeat(col.width + 1);
|
|
901
|
-
})
|
|
902
|
-
.join('') +
|
|
903
|
-
style.cross;
|
|
904
|
-
commands.push(...this.driver.text(sepLine));
|
|
905
|
-
commands.push(...this.driver.feed(1));
|
|
906
|
-
}
|
|
907
|
-
});
|
|
908
|
-
|
|
909
|
-
// Draw bottom border
|
|
910
|
-
const bottomLine =
|
|
911
|
-
style.bottomLeft +
|
|
912
|
-
table.columns
|
|
913
|
-
.map(col => {
|
|
914
|
-
return style.bottom.repeat(col.width + 1);
|
|
915
|
-
})
|
|
916
|
-
.join('') +
|
|
917
|
-
style.bottomRight;
|
|
918
|
-
commands.push(...this.driver.text(bottomLine));
|
|
919
|
-
commands.push(...this.driver.feed(1));
|
|
920
|
-
|
|
921
|
-
return commands;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
/**
|
|
925
|
-
* Align text within a specified width
|
|
926
|
-
*/
|
|
927
|
-
private alignText(text: string, width: number, align: TextAlign): string {
|
|
928
|
-
const padded = text.padEnd(width).substring(0, width);
|
|
929
|
-
switch (align) {
|
|
930
|
-
case TextAlign.CENTER: {
|
|
931
|
-
const leftPad = Math.floor((width - padded.length) / 2);
|
|
932
|
-
return padded.padStart(leftPad + padded.length).padEnd(width);
|
|
933
|
-
}
|
|
934
|
-
case TextAlign.RIGHT:
|
|
935
|
-
return text.padStart(width);
|
|
936
|
-
default:
|
|
937
|
-
return padded;
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
/**
|
|
942
|
-
* Render standard elements (text, line, image, qrcode, barcode, feed, variable)
|
|
943
|
-
*/
|
|
944
|
-
private renderStandardElement(
|
|
945
|
-
element: Exclude<
|
|
946
|
-
TemplateElement,
|
|
947
|
-
LoopElement | ConditionElement | BorderElement | TableElement
|
|
948
|
-
>,
|
|
949
|
-
data: Record<string, unknown>
|
|
950
|
-
): Uint8Array[] {
|
|
951
|
-
const commands: Uint8Array[] = [];
|
|
952
|
-
|
|
953
|
-
switch (element.type) {
|
|
954
|
-
case 'text': {
|
|
955
|
-
const content = this.substituteVariables(element.content, data);
|
|
956
|
-
if (element.align) {
|
|
957
|
-
commands.push(...this.formatter.align(element.align));
|
|
958
|
-
}
|
|
959
|
-
if (element.size) {
|
|
960
|
-
commands.push(...this.formatter.setSize(element.size, element.size));
|
|
961
|
-
}
|
|
962
|
-
if (element.bold) {
|
|
963
|
-
commands.push(...this.formatter.setBold(true));
|
|
964
|
-
}
|
|
965
|
-
commands.push(...this.driver.text(content));
|
|
966
|
-
commands.push(...this.driver.feed(1));
|
|
967
|
-
if (element.bold) {
|
|
968
|
-
commands.push(...this.formatter.setBold(false));
|
|
969
|
-
}
|
|
970
|
-
if (element.size) {
|
|
971
|
-
commands.push(...this.formatter.setSize(1, 1));
|
|
972
|
-
}
|
|
973
|
-
break;
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
case 'line':
|
|
977
|
-
commands.push(...this.renderLine(element.char, element.length));
|
|
978
|
-
break;
|
|
979
|
-
|
|
980
|
-
case 'image':
|
|
981
|
-
commands.push(...this.driver.image(element.data, element.width, element.height));
|
|
982
|
-
commands.push(...this.driver.feed(1));
|
|
983
|
-
break;
|
|
984
|
-
|
|
985
|
-
case 'qrcode': {
|
|
986
|
-
const qrContent = this.substituteVariables(element.content, data);
|
|
987
|
-
commands.push(...this.formatter.align(TextAlign.CENTER));
|
|
988
|
-
commands.push(...this.driver.qr(qrContent, { size: element.size ?? 6 }));
|
|
989
|
-
commands.push(...this.driver.feed(1));
|
|
990
|
-
break;
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
case 'barcode': {
|
|
994
|
-
const barcodeContent = this.substituteVariables(element.content, data);
|
|
995
|
-
commands.push(...this.formatter.align(TextAlign.CENTER));
|
|
996
|
-
const barcodeCommands = this.barcodeGenerator.generate(barcodeContent, {
|
|
997
|
-
format: element.format,
|
|
998
|
-
height: element.height ?? 60,
|
|
999
|
-
showText: true,
|
|
1000
|
-
});
|
|
1001
|
-
commands.push(...barcodeCommands);
|
|
1002
|
-
commands.push(...this.driver.feed(1));
|
|
1003
|
-
break;
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
case 'feed':
|
|
1007
|
-
commands.push(...this.driver.feed(element.lines));
|
|
1008
|
-
break;
|
|
1009
|
-
|
|
1010
|
-
case 'variable': {
|
|
1011
|
-
const value = this.getNestedValue(data, element.name);
|
|
1012
|
-
if (value !== undefined) {
|
|
1013
|
-
const formatted = this.formatValue(value, element.format);
|
|
1014
|
-
commands.push(...this.driver.text(formatted));
|
|
1015
|
-
commands.push(...this.driver.feed(1));
|
|
1016
|
-
}
|
|
1017
|
-
break;
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
return commands;
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
/**
|
|
1025
|
-
* Render a separator line
|
|
1026
|
-
*/
|
|
1027
|
-
private renderLine(char?: string, length?: number): Uint8Array[] {
|
|
1028
|
-
const lineChar = char ?? '-';
|
|
1029
|
-
const lineLength = length ?? this.paperWidth;
|
|
1030
|
-
const line = lineChar.repeat(lineLength);
|
|
1031
|
-
return [...this.driver.text(line), ...this.driver.feed(1)];
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
/**
|
|
1035
|
-
* Format an item line with columns
|
|
1036
|
-
*/
|
|
1037
|
-
private formatItemLine(name: string, qty: string, amount: string): string {
|
|
1038
|
-
const nameWidth = this.paperWidth - 16;
|
|
1039
|
-
const qtyWidth = 8;
|
|
1040
|
-
const amountWidth = 8;
|
|
1041
|
-
|
|
1042
|
-
const paddedName = name.padEnd(nameWidth).substring(0, nameWidth);
|
|
1043
|
-
const paddedQty = qty.padStart(qtyWidth);
|
|
1044
|
-
const paddedAmount = amount.padStart(amountWidth);
|
|
1045
|
-
|
|
1046
|
-
return `${paddedName}${paddedQty}${paddedAmount}`;
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
/**
|
|
1050
|
-
* Substitute variables in a string
|
|
1051
|
-
*/
|
|
1052
|
-
private substituteVariables(template: string, data: Record<string, unknown>): string {
|
|
1053
|
-
return template.replace(/\{\{(\w+(?:\.\w+)*)\}\}/g, (_, key: string) => {
|
|
1054
|
-
const value = this.getNestedValue(data, key);
|
|
1055
|
-
if (value === undefined) return '';
|
|
1056
|
-
if (typeof value === 'object') return JSON.stringify(value);
|
|
1057
|
-
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
|
1058
|
-
return String(value);
|
|
1059
|
-
});
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
/**
|
|
1063
|
-
* Get nested value from object
|
|
1064
|
-
*/
|
|
1065
|
-
private getNestedValue(obj: Record<string, unknown>, path: string): unknown {
|
|
1066
|
-
const keys = path.split('.');
|
|
1067
|
-
let current: unknown = obj;
|
|
1068
|
-
|
|
1069
|
-
for (const key of keys) {
|
|
1070
|
-
if (current === null || current === undefined) {
|
|
1071
|
-
return undefined;
|
|
1072
|
-
}
|
|
1073
|
-
if (typeof current === 'object') {
|
|
1074
|
-
current = (current as Record<string, unknown>)[key];
|
|
1075
|
-
} else {
|
|
1076
|
-
return undefined;
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
return current;
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
/**
|
|
1084
|
-
* Format a value with optional format string
|
|
1085
|
-
*/
|
|
1086
|
-
private formatValue(value: unknown, format?: string): string {
|
|
1087
|
-
if (format === 'currency' && typeof value === 'number') {
|
|
1088
|
-
return `¥${value.toFixed(2)}`;
|
|
1089
|
-
}
|
|
1090
|
-
if (format === 'date' && value instanceof Date) {
|
|
1091
|
-
return value.toLocaleDateString('zh-CN');
|
|
1092
|
-
}
|
|
1093
|
-
return String(value);
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
/**
|
|
1097
|
-
* Combine multiple command arrays into one
|
|
1098
|
-
*/
|
|
1099
|
-
private combineCommands(commands: Uint8Array[]): Uint8Array {
|
|
1100
|
-
const totalLength = commands.reduce((acc, cmd) => acc + cmd.length, 0);
|
|
1101
|
-
const result = new Uint8Array(totalLength);
|
|
1102
|
-
let offset = 0;
|
|
1103
|
-
|
|
1104
|
-
for (const cmd of commands) {
|
|
1105
|
-
result.set(cmd, offset);
|
|
1106
|
-
offset += cmd.length;
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
return result;
|
|
344
|
+
return this.parser.validate(template, data);
|
|
1110
345
|
}
|
|
1111
346
|
}
|
|
1112
347
|
|