Blame view

泰额版/Food Labeling Management App UniApp/src/utils/printFromPrintDataList.ts 23.2 KB
59e51671   “wangming”   1
1
2
3
4
5
6
  import {
    normalizeLabelTemplateFromPreviewApi,
    parseLabelSizeText,
    sortElementsForPreview,
  } from './labelPreview/normalizePreviewTemplate'
  import {
5a192b24   杨鑫   最新同步
7
8
9
10
11
12
    isCorruptedDateDisplayFormat,
    isWeekdayBarElement,
    parseStoredPrintInputOffset,
    resolveWeekdayDisplayForElement,
  } from './labelPreview/printInputOffset'
  import {
59e51671   “wangming”   1
13
    canPrintCurrentLabelViaNativeFastJob,
5a192b24   杨鑫   最新同步
14
    getCurrentPrinterSummary,
59e51671   “wangming”   1
15
16
17
18
19
20
21
22
23
    printLabelPrintJobPayloadForCurrentPrinter,
    printSystemTemplateForCurrentPrinter,
    type SystemTemplatePrintCanvasRasterOptions,
  } from './print/manager/printerManager'
  import {
    buildLabelPrintJobPayload,
    setLastLabelPrintJobPayload,
  } from './labelPreview/buildLabelPrintPayload'
  import { getCurrentStoreId } from './stores'
5a192b24   杨鑫   最新同步
24
25
  import { ensureNativeClassicTransportIfPossible, getBluetoothConnection, preferBuiltinPrinterOnAioDevice } from './print/printerConnection'
  import { isVirtualBtPrinterDeviceName } from './print/bluetoothPrinterAllowlist'
59e51671   “wangming”   1
26
27
28
29
30
31
  import {
    hydrateSystemTemplateImagesForPrint,
    resetHydrateImageDebugRecords,
  } from './print/hydrateTemplateImagesForPrint'
  import {
    normalizeTemplateForNativeFastJob,
5a192b24   杨鑫   最新同步
32
    templateHasDateTimeElements,
59e51671   “wangming”   1
33
    templateHasUnsupportedNativeFastElements,
5a192b24   杨鑫   最新同步
34
    templateRequiresCanvasStyleFidelity,
59e51671   “wangming”   1
35
  } from './print/nativeTemplateElementSupport'
540ac0e3   杨鑫   前端修改bug
36
37
38
39
  import {
    ensureTemplateHeightCoversElements,
    isTemplateWithinNativeFastPrintBounds,
  } from './print/templatePhysicalMm'
59e51671   “wangming”   1
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
  import type {
    LabelTemplateData,
    SystemLabelTemplate,
    SystemTemplateElementBase,
  } from './print/types/printer'
  import type { PrintLogDataItemDto, PrintLogItemDto } from '../types/usAppLabeling'
  
  function nonEmptyDisplay (value: string | null | undefined): string {
    if (value == null || value === '' || value === '无') return ''
    return String(value).trim()
  }
  
  /**
   * 接口 9/11 落库的快照里 elements[].config 已是最终展示值;重打时 data 不能再带 productName,
   * 否则 TEXT_PRODUCT 会优先解析为 productName(与预览里用 config.text 的「freeze」等冲突,出现两行 sandwich)。
   */
  function labelTemplateDataForSnapshotReprint (): LabelTemplateData {
    return {}
  }
  
  function formatPriceLineForBake (config: Record<string, any>, rawText: string): string {
    const prefix = String(config.prefix ?? config.Prefix ?? '')
    const suffix = String(config.suffix ?? config.Suffix ?? '')
    const decRaw = config.decimal ?? config.Decimal
    const decimal = typeof decRaw === 'number' ? decRaw : Number(decRaw)
    const numericValue = Number(rawText)
    const value =
      !Number.isNaN(numericValue) && Number.isFinite(numericValue) && Number(decimal) >= 0
        ? numericValue.toFixed(Number(decimal))
        : rawText
    return `${prefix}${value}${suffix}`
  }
  
5a192b24   杨鑫   最新同步
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
  function parseReprintBaseTime (row: PrintLogItemDto): Date {
    const raw = String(row.printedAt ?? '').trim()
    if (raw) {
      const d = new Date(raw)
      if (!Number.isNaN(d.getTime())) return d
    }
    return new Date()
  }
  
  /** 重打快照里已算好的展示文案(优先 __previewFormatted,其次 config.text) */
  function readReprintSnapshotLine (cfg: Record<string, any>): string {
    for (const key of ['__previewFormatted', '__PreviewFormatted', 'text', 'Text'] as const) {
      const raw = String(cfg[key] ?? '').trim()
      if (!raw) continue
      if (parseStoredPrintInputOffset(raw)) continue
      if (isCorruptedDateDisplayFormat(raw)) continue
      return raw
    }
    return ''
  }
  
59e51671   “wangming”   1
94
95
96
97
  /**
   * 原生快打插件对 TEXT_PRODUCT / TEXT_PRICE / DATE 等仍会按类型做绑定;重打时 dataJson 为空,
   * 必须把接口/落库快照里已填好的展示值焙成 TEXT_STATIC,否则会出现第二行商品名、价格 0、日期成 format 等问题。
   */
5a192b24   杨鑫   最新同步
98
99
100
101
  export function bakeReprintTemplateSnapshot (
    tmpl: SystemLabelTemplate,
    reprintBaseTime: Date = new Date(),
  ): SystemLabelTemplate {
59e51671   “wangming”   1
102
103
104
105
106
107
108
109
110
    const elements = (tmpl.elements || []).map((el) => {
      const vst = String(el.valueSourceType || '').toUpperCase()
      const type = String(el.type || '').toUpperCase()
      const cfg = { ...(el.config || {}) } as Record<string, any>
  
      const toStatic = (line: string): SystemTemplateElementBase => ({
        ...el,
        type: 'TEXT_STATIC',
        valueSourceType: 'FIXED',
5a192b24   杨鑫   最新同步
111
112
113
114
115
116
117
        config: {
          ...cfg,
          text: line,
          Text: line,
          __previewFormatted: line,
          __PreviewFormatted: line,
        },
59e51671   “wangming”   1
118
119
      })
  
5a192b24   杨鑫   最新同步
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
      /** 黑条星期:快照有 FRIDAY 等则冻结;仅有偏移 JSON 时按原打印时刻 + 偏移还原 */
      if (isWeekdayBarElement({ ...el, config: cfg })) {
        let line = readReprintSnapshotLine(cfg)
        if (!line) {
          try {
            const live = resolveWeekdayDisplayForElement({ ...el, config: cfg }, reprintBaseTime)
            if (live && !isCorruptedDateDisplayFormat(live)) line = live
          } catch {
            /* ignore */
          }
        }
        if (line) return toStatic(line)
      }
  
      /** 快照里 DATE/TIME/DURATION 的 config.text 已是当时出纸值;重打禁止再按 now 重算 */
      if (type === 'DATE' || type === 'TIME' || type === 'DURATION') {
        const line = readReprintSnapshotLine(cfg)
        if (line) return toStatic(line)
      }
  
      if (type === 'WEIGHT') {
        const line =
          readReprintSnapshotLine(cfg)
          || (() => {
            const v = String(cfg.value ?? cfg.Value ?? '').trim()
            const u = String(cfg.unit ?? cfg.Unit ?? '').trim()
            if (v && u) return v.endsWith(u) ? v : `${v}${u}`
            return v || u
          })()
        if (line) return toStatic(line)
      }
  
59e51671   “wangming”   1
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
      if (vst === 'FIXED') {
        if (type === 'TEXT_PRODUCT' || type === 'TEXT_CATEGORY' || type === 'TEXT_LABEL_ID') {
          const t = String(cfg.text ?? cfg.Text ?? '').trim()
          if (t) return toStatic(t)
        }
        if (type === 'TEXT_PRICE') {
          const raw = String(cfg.text ?? cfg.Text ?? '').trim()
          if (raw) {
            const line = formatPriceLineForBake(cfg, raw)
            return {
              ...el,
              type: 'TEXT_STATIC',
              valueSourceType: 'FIXED',
              config: { ...cfg, text: line, Text: line, prefix: '', Prefix: '', suffix: '', Suffix: '' },
            }
          }
        }
      }
  
      if (vst === 'PRINT_INPUT') {
59e51671   “wangming”   1
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
        if (type === 'WEIGHT') {
          const textVal = String(cfg.text ?? cfg.Text ?? '').trim()
          const v = String(cfg.value ?? cfg.Value ?? '').trim()
          const u = String(cfg.unit ?? cfg.Unit ?? '').trim()
          const line =
            textVal || (v && u ? (v.endsWith(u) ? v : `${v}${u}`) : v || u)
          if (line) return toStatic(line)
        }
        if (type === 'TEXT_STATIC') {
          const inputType = String(cfg.inputType ?? cfg.InputType ?? '').toLowerCase()
          if (inputType === 'number' || inputType === 'text') {
            const t = String(cfg.text ?? cfg.Text ?? '').trim()
            if (t) return toStatic(t)
          }
          if (
            inputType === 'options' ||
            cfg.multipleOptionId ||
            cfg.MultipleOptionId
          ) {
            const rawText = String(cfg.text ?? cfg.Text ?? '').trim()
            const sel = cfg.selectedOptionValues ?? cfg.SelectedOptionValues
            const joined =
              Array.isArray(sel) && sel.length ? sel.map((x: unknown) => String(x)).join(', ') : ''
            let line = rawText
            if (!line && joined) {
              const dictLabel = String(
                cfg.multipleOptionName ?? cfg.MultipleOptionName ?? 'Options',
              ).trim()
              const prefix = String(cfg.prefix ?? cfg.Prefix ?? '').trim()
              line = prefix ? `${prefix}${joined}` : `${dictLabel}: ${joined}`
            }
            if (line) return toStatic(line)
          }
        }
      }
  
      return el
    })
    return { ...tmpl, elements }
  }
  
  /**
   * 接口可能返回两类 renderConfigJson:
   * 1)与设计器 elements[] 单项同构(含 type、x、y、config)
   * 2)仅画布 config 快照(只有 text、fontSize 等),无 type、无坐标 —— 若直接当 element 传入,
   *    normalizeLabelTemplateFromPreviewApi 只读 e.config,会得到空 config,打印全空。
   */
  function inferElementTypeFromPrintSnapshotConfig (cfg: Record<string, unknown>): string {
5a192b24   杨鑫   最新同步
220
221
    const inputType = String(cfg.inputType ?? cfg.InputType ?? '').toLowerCase()
    if (inputType === 'number' || inputType === 'text') return 'TEXT_STATIC'
59e51671   “wangming”   1
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
    const hasUnit = cfg.unit != null || cfg.Unit != null
    const hasValue = cfg.value != null || cfg.Value != null
    if (hasUnit && hasValue) return 'WEIGHT'
  
    if (
      typeof cfg.decimal === 'number' &&
      ('prefix' in cfg || 'suffix' in cfg)
    ) {
      return 'TEXT_PRICE'
    }
  
    if (cfg.format != null && inputType === 'datetime') return 'DATE'
  
    return 'TEXT_STATIC'
  }
  
  function applyRenderValueToSnapshotConfig (
    cfg: Record<string, unknown>,
    elementType: string,
    renderValue: string | null | undefined
  ): void {
    if (renderValue === undefined || renderValue === null) return
    const t = elementType.toUpperCase()
    const s = String(renderValue)
    if (t === 'WEIGHT') {
      cfg.value = s
      cfg.Value = s
      return
    }
    if (t === 'DATE' || t === 'TIME') {
      cfg.text = s
      cfg.Text = s
      return
    }
    cfg.text = s
    cfg.Text = s
  }
  
  function pageWidthPxFromPrintLogRow (row: PrintLogItemDto): number {
    const size = parseLabelSizeText(row.labelSizeText ?? null)
    const w = size?.width ?? 2
    return Math.max(96, Math.round(w * 96))
  }
  
  /**
   * 将 printDataList 单项转为 normalize 可用的 element;扁平 config 会包进 config 并补全坐标(纵向堆叠)。
   */
  function elementFromPrintDataItem (
    item: PrintLogDataItemDto,
    index: number,
    pageWidthPx: number
  ): Record<string, unknown> {
    const raw =
      item.renderConfigJson ?? (item as unknown as { RenderConfigJson?: unknown }).RenderConfigJson
    if (raw == null) {
      throw new Error('Missing renderConfigJson')
    }
  
    let obj: Record<string, unknown>
    if (typeof raw === 'string') {
      try {
        obj = JSON.parse(raw) as Record<string, unknown>
      } catch {
        throw new Error('Invalid element JSON')
      }
    } else if (typeof raw === 'object' && !Array.isArray(raw)) {
      obj = { ...(raw as Record<string, unknown>) }
    } else {
      throw new Error('Invalid renderConfigJson')
    }
  
    const hasElementEnvelope =
      typeof obj.type === 'string' ||
      typeof obj.Type === 'string' ||
      typeof obj.elementType === 'string' ||
      'x' in obj ||
      'posX' in obj ||
      'config' in obj ||
      'ConfigJson' in obj ||
      'configJson' in obj
  
    if (hasElementEnvelope) {
      const base = { ...obj }
      if (item.elementId) {
        base.id = item.elementId
        base.Id = item.elementId
      }
5a192b24   杨鑫   最新同步
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
      const cfgRaw = base.config ?? base.Config ?? base.ConfigJson ?? base.configJson
      let cfg: Record<string, unknown>
      if (cfgRaw != null && typeof cfgRaw === 'object' && !Array.isArray(cfgRaw)) {
        cfg = { ...(cfgRaw as Record<string, unknown>) }
      } else if (typeof cfgRaw === 'string' && cfgRaw.trim()) {
        try {
          cfg = JSON.parse(cfgRaw) as Record<string, unknown>
        } catch {
          cfg = {}
        }
      } else {
        cfg = {}
      }
      const elType = String(
        base.type ?? base.Type ?? base.elementType ?? base.ElementType ?? inferElementTypeFromPrintSnapshotConfig(cfg),
      )
      applyRenderValueToSnapshotConfig(cfg, elType, item.renderValue)
      base.config = cfg
      base.Config = cfg
59e51671   “wangming”   1
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
      return base
    }
  
    const cfg = { ...obj }
    const inferredType = inferElementTypeFromPrintSnapshotConfig(cfg)
    applyRenderValueToSnapshotConfig(cfg, inferredType, item.renderValue)
  
    const lineHeight = 40
    const pad = 8
    return {
      id: item.elementId ?? `el-${index}`,
      type: inferredType,
      x: pad,
      y: pad + index * lineHeight,
      width: Math.max(40, pageWidthPx - pad * 2),
      height: lineHeight,
      rotation: 'horizontal',
      border: 'none',
      config: cfg,
      zIndex: index,
      orderNum: index,
    }
  }
  
  /**
   * 重打快照:勿调用 overlayProductNameOnPreviewTemplate。
   * 列表 printDataList 里 TEXT_PRODUCT 若丢字端,text 变空串会触发「占位」逻辑被整行替成 productName,出现两行 sandwich。
   */
  function overlayReprintResolvedFields (
    tmpl: SystemLabelTemplate,
    row: PrintLogItemDto,
    data: LabelTemplateData
  ): SystemLabelTemplate {
    const t = tmpl
    const productName = nonEmptyDisplay(row.productName)
    /** 勿含空串:接口拆包丢字端时 text 为空,若把 '' 当占位并整行换成 productName,会与第一行商品名重复成两个 sandwich */
    const placeholders = new Set([
      '文本',
      'text',
      'Text',
      'TEXT',
      '名称',
      'name',
      'Name',
      'label',
      'Label',
    ])
  
    const elements = (t.elements || []).map((el) => {
      const type = String(el.type || '').toUpperCase()
      const cfg = { ...(el.config || {}) } as Record<string, unknown>
      const inputType = String(cfg.inputType ?? '').toLowerCase()
  
      if (type === 'TEXT_STATIC' || type === 'TEXT_PRODUCT') {
        if (cfg.multipleOptionId || inputType === 'options') {
          const rawText = String(cfg.text ?? cfg.Text ?? '').trim()
          const sel = cfg.selectedOptionValues
          const joined =
            Array.isArray(sel) && sel.length ? sel.map((x) => String(x)).join(', ') : ''
          /** 快照已存整行「Allergens: x, y」时勿改成仅选项值,否则缺行且缺前缀 */
          if (rawText && (rawText.includes(joined) || (joined && rawText.length > joined.length))) {
            return el
          }
          if (!rawText && joined) {
            const dictLabel = String(cfg.multipleOptionName ?? cfg.MultipleOptionName ?? 'Options').trim()
            const prefix = String(cfg.prefix ?? cfg.Prefix ?? '').trim()
            const line = prefix ? `${prefix}${joined}` : `${dictLabel}: ${joined}`
            return { ...el, config: { ...cfg, text: line } }
          }
          return el
        }
      }
  
      if ((type === 'TEXT_STATIC' || type === 'TEXT_PRODUCT') && productName) {
        const raw = String(cfg.text ?? cfg.Text ?? '').trim()
        const vstEl = String(el.valueSourceType || '').toUpperCase()
        /** FIXED 的 TEXT_PRODUCT 多为标签类型/说明,占位「名称」等不应被商品名替换(避免出现两行 sandwich) */
        if (type === 'TEXT_PRODUCT' && vstEl === 'FIXED') {
          return el
        }
        if (placeholders.has(raw)) {
          return {
            ...el,
            type: 'TEXT_PRODUCT',
            config: { ...cfg, text: productName },
          }
        }
      }
  
      if (type === 'DATE' && !String(cfg.text ?? cfg.Text ?? '').trim() && data.date) {
        return { ...el, config: { ...cfg, text: String(data.date) } }
      }
      if (type === 'TIME' && !String(cfg.text ?? cfg.Text ?? '').trim() && data.time) {
        return { ...el, config: { ...cfg, text: String(data.time) } }
      }
  
      return el
    })
  
    return { ...t, elements }
  }
  
  /** 打印日志重打:可选整页 canvas 光栅,与预览页「非 native 快打」分支一致 */
  export type PrintFromPrintLogOptions = {
    printQty?: number
    onProgress?: (percent: number) => void
    canvasRaster?: SystemTemplatePrintCanvasRasterOptions
  }
  
  /**
   * 与 Label Preview 一致:一体机(经典蓝牙 + native-plugin 基座)且模板可走原生时走 printLabelPrintJob + 本地图片 hydration;
   * 普通蓝牙仍走 canvas 光栅或直发 TSC。
   */
  async function printReprintTemplateWithPreviewStrategy (
    tmpl: SystemLabelTemplate,
    row: PrintLogItemDto,
    options: PrintFromPrintLogOptions,
  ): Promise<void> {
    await ensureNativeClassicTransportIfPossible()
5a192b24   杨鑫   最新同步
447
    preferBuiltinPrinterOnAioDevice()
59e51671   “wangming”   1
448
449
    const templateData = labelTemplateDataForSnapshotReprint()
    const printInputJson: Record<string, unknown> = {}
540ac0e3   杨鑫   前端修改bug
450
451
    const tmplSized = ensureTemplateHeightCoversElements(tmpl)
    const tmplForNative = normalizeTemplateForNativeFastJob(tmplSized, printInputJson as any)
5a192b24   杨鑫   最新同步
452
453
454
455
456
457
    const summary = getCurrentPrinterSummary()
    const conn = getBluetoothConnection()
    const mustUseCanvasRaster =
      summary.type === 'builtin'
      || templateRequiresCanvasStyleFidelity(tmplSized)
      || templateHasDateTimeElements(tmplSized)
59e51671   “wangming”   1
458
    const useNative =
5a192b24   杨鑫   最新同步
459
460
      !mustUseCanvasRaster
      && canPrintCurrentLabelViaNativeFastJob()
540ac0e3   杨鑫   前端修改bug
461
      && isTemplateWithinNativeFastPrintBounds(tmplSized)
59e51671   “wangming”   1
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
      && !templateHasUnsupportedNativeFastElements(tmplForNative)
  
    const printQty = options.printQty ?? 1
  
    if (useNative) {
      resetHydrateImageDebugRecords()
      const hydrated = await hydrateSystemTemplateImagesForPrint(tmplForNative)
      const payload = buildLabelPrintJobPayload(hydrated, printInputJson, {
        labelCode: row.labelCode,
        productId: row.productId ?? undefined,
        printQuantity: printQty,
        locationId: getCurrentStoreId() || undefined,
      })
      setLastLabelPrintJobPayload(payload)
      await printLabelPrintJobPayloadForCurrentPrinter(
        payload,
        { printQty },
        options.onProgress,
      )
      return
    }
  
    await printSystemTemplateForCurrentPrinter(
540ac0e3   杨鑫   前端修改bug
485
      tmplSized,
59e51671   “wangming”   1
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
      templateData,
      { printQty, canvasRaster: options.canvasRaster },
      options.onProgress,
    )
  }
  
  /**
   * 使用接口 10 返回的 `printDataList` 组装模板并走当前打印机。
   */
  function logReprintJson (label: string, data: unknown): void {
    try {
      console.log(`[Reprint] ${label}`, typeof data === 'string' ? data : JSON.stringify(data, null, 2))
    } catch {
      console.log(`[Reprint] ${label}`, data)
    }
  }
  
  export async function printFromPrintDataListRow (
    row: PrintLogItemDto,
    options: PrintFromPrintLogOptions = {}
  ): Promise<void> {
    const list =
      row.printDataList ??
      (row as unknown as { PrintDataList?: PrintLogDataItemDto[] }).PrintDataList ??
      []
    if (!Array.isArray(list) || list.length === 0) {
      throw new Error('No printDataList in record')
    }
  
    const pageW = pageWidthPxFromPrintLogRow(row)
    const elements = list.map((item, index) => elementFromPrintDataItem(item, index, pageW))
    const size = parseLabelSizeText(row.labelSizeText ?? null)
    const payload: Record<string, unknown> = {
      id: row.labelId || row.labelCode || 'reprint',
      name: 'Reprint',
      unit: size?.unit ?? 'inch',
      width: size?.width ?? 2,
      height: size?.height ?? 2,
      appliedLocation: 'ALL',
      elements,
    }
  
    console.log('[Reprint] 路径: printDataList 组装')
    logReprintJson('printDataList 原始', list)
    logReprintJson('组装的 template payload(normalize 前)', payload)
  
    let tmpl = normalizeLabelTemplateFromPreviewApi(payload)
    if (!tmpl) {
      throw new Error('Cannot build template from printDataList')
    }
  
    const templateData = labelTemplateDataForSnapshotReprint()
5a192b24   杨鑫   最新同步
538
    const reprintBase = parseReprintBaseTime(row)
59e51671   “wangming”   1
539
    tmpl = overlayReprintResolvedFields(tmpl, row, templateData)
5a192b24   杨鑫   最新同步
540
    tmpl = bakeReprintTemplateSnapshot(tmpl, reprintBase)
59e51671   “wangming”   1
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
  
    tmpl = {
      ...tmpl,
      elements: sortElementsForPreview(tmpl.elements || []),
    } as SystemLabelTemplate
  
    logReprintJson('bake + sort 后送打印机模板 tmpl', tmpl)
  
    await printReprintTemplateWithPreviewStrategy(tmpl, row, options)
  }
  
  /**
   * 从列表快照字符串(`printInputJson` / `renderTemplateJson`)解析出与接口 9 同构、可 `normalize` 的模板 JSON 字符串。
   *
   * 若根上同时有嵌套 `printInputJson`(小对象)和根级 `elements`(整模板),优先保留含 `elements` 的那份。
   */
  export function extractPrintTemplateJsonForReprint (raw: string): string | null {
    const s = raw.trim()
    if (!s) return null
    try {
      let doc: unknown = JSON.parse(s)
      if (typeof doc === 'string') {
        doc = JSON.parse(doc)
      }
      if (doc == null || typeof doc !== 'object' || Array.isArray(doc)) return null
      const d = doc as Record<string, unknown>
  
      if (Array.isArray(d.elements) || Array.isArray(d.Elements)) {
        return JSON.stringify(d)
      }
  
      const pi = d.printInputJson ?? d.PrintInputJson
      if (pi != null && typeof pi === 'object' && !Array.isArray(pi)) {
        const p = pi as Record<string, unknown>
        if (Array.isArray(p.elements) || Array.isArray(p.Elements)) {
          return JSON.stringify(p)
        }
        return JSON.stringify(p)
      }
    } catch {
      return null
    }
    return null
  }
  
  /** 列表项里 JSON 快照字段:可能是 string(含转义)或已解析的 object */
  function snapshotJsonFieldToString (
    row: PrintLogItemDto,
    camel: 'printInputJson' | 'renderTemplateJson',
    pascal: 'PrintInputJson' | 'RenderTemplateJson',
  ): string | null {
    const rec = row as unknown as Record<string, unknown>
    const r = rec[camel] ?? rec[pascal]
    if (r == null) return null
    if (typeof r === 'string') {
      const t = r.trim()
      return t || null
    }
    try {
      return JSON.stringify(r)
    } catch {
      return null
    }
  }
  
  /**
   * 打印日志重打入口:**优先 `printInputJson`**(与接口 9 落库快照一致),其次 `renderTemplateJson`,最后 `printDataList`。
   */
  export async function printFromPrintLogRow (
    row: PrintLogItemDto,
    options: PrintFromPrintLogOptions = {}
  ): Promise<void> {
    const fromPrintInput = snapshotJsonFieldToString(row, 'printInputJson', 'PrintInputJson')
    const fromRender = snapshotJsonFieldToString(row, 'renderTemplateJson', 'RenderTemplateJson')
  
    console.log('[Reprint] ========== 重复打印 JSON 调试 ==========')
    console.log('[Reprint] taskId', row.taskId, 'labelCode', row.labelCode, 'productName', row.productName)
    logReprintJson('printInputJson(优先,接口 10)', fromPrintInput ?? '(无)')
    logReprintJson('renderTemplateJson(回退)', fromRender ?? '(无)')
  
    const tryExtract = (raw: string | null): string | null => {
      if (!raw) return null
      return extractPrintTemplateJsonForReprint(raw)
    }
  
    const extracted = tryExtract(fromPrintInput) ?? tryExtract(fromRender)
  
    if (extracted) {
      logReprintJson('extract 后用于打印的模板 JSON 字符串', extracted)
      await printFromMergedTemplateJsonString(extracted, row, options)
      return
    }
  
    if (fromPrintInput || fromRender) {
      console.warn('[Reprint] 有 printInputJson/renderTemplateJson 但 extract 失败,回退 printDataList')
    } else {
      console.log('[Reprint] 无 printInputJson 与 renderTemplateJson,使用 printDataList')
    }
  
    await printFromPrintDataListRow(row, options)
  }
  
  /**
   * 将已解析的快照 JSON 字符串走 normalize → overlay → bake 后送机(列表优先来自 `printInputJson`)。
   */
  export async function printFromMergedTemplateJsonString (
    mergedTemplateJson: string,
    row: PrintLogItemDto,
    options: PrintFromPrintLogOptions = {}
  ): Promise<void> {
    console.log('[Reprint] 路径: renderTemplateJson / merged 完整模板')
    logReprintJson('mergedTemplateJson 原始字符串', mergedTemplateJson)
  
    let payload: unknown
    try {
      payload = JSON.parse(mergedTemplateJson) as unknown
      /** 部分网关/序列化会把整段再包一层 JSON 字符串 */
      if (typeof payload === 'string') {
        payload = JSON.parse(payload) as unknown
      }
    } catch {
      throw new Error('Invalid merged template JSON')
    }
    logReprintJson('JSON.parse 后的 payload', payload)
  
    let tmpl = normalizeLabelTemplateFromPreviewApi(payload)
    if (!tmpl) {
      throw new Error('Cannot parse merged template')
    }
    const templateData = labelTemplateDataForSnapshotReprint()
5a192b24   杨鑫   最新同步
671
    const reprintBase = parseReprintBaseTime(row)
59e51671   “wangming”   1
672
    tmpl = overlayReprintResolvedFields(tmpl, row, templateData)
5a192b24   杨鑫   最新同步
673
    tmpl = bakeReprintTemplateSnapshot(tmpl, reprintBase)
59e51671   “wangming”   1
674
675
676
677
678
679
680
681
682
683
    tmpl = {
      ...tmpl,
      elements: sortElementsForPreview(tmpl.elements || []),
    } as SystemLabelTemplate
  
    logReprintJson('bake + sort 后送打印机模板 tmpl', tmpl)
    logReprintJson('templateData(快照重打为空对象)', templateData)
  
    await printReprintTemplateWithPreviewStrategy(tmpl, row, options)
  }