Commit b04bc3a567c02aa0b11d271be06221577baf5a1c

Authored by 杨鑫
1 parent 7af95447

打印日志 重新打印

美国版/Food Labeling Management App UniApp/src/pages/labels/preview.vue
@@ -215,7 +215,6 @@ import { @@ -215,7 +215,6 @@ import {
215 postUsAppLabelPrint, 215 postUsAppLabelPrint,
216 US_APP_LABEL_PRINT_PATH, 216 US_APP_LABEL_PRINT_PATH,
217 } from '../../services/usAppLabeling' 217 } from '../../services/usAppLabeling'
218 -import { savePrintTemplateSnapshotForTask } from '../../utils/printSnapshotStorage'  
219 import { 218 import {
220 applyTemplateProductDefaultValuesToTemplate, 219 applyTemplateProductDefaultValuesToTemplate,
221 extractTemplateProductDefaultValuesFromPreviewPayload, 220 extractTemplateProductDefaultValuesFromPreviewPayload,
@@ -671,38 +670,28 @@ const handlePrint = async () => { @@ -671,38 +670,28 @@ const handlePrint = async () => {
671 try { 670 try {
672 const bt = getBluetoothConnection() 671 const bt = getBluetoothConnection()
673 /** 672 /**
674 - * 接口 9 落库必须与本次出纸使用的合并模板完全一致(与 labelPrintJobPayload.template 同源),  
675 - * 避免另起 buildPrintPersistTemplateSnapshot(base) 与 computeMergedPreviewTemplate() 细微偏差,  
676 - * 导致库内缺用户输入的价签/过敏原/数字/日期,重打与预览不一致。 673 + * 接口 9 的 `printInputJson`:整份合并模板快照(与出纸 labelPrintJobPayload.template 同源)+
  674 + * buildPrintInputJson 的键值(PRINT_INPUT / 多选等),后端应原样落库并在接口 10 `renderTemplateJson` 返回供重打。
677 */ 675 */
678 const persistTemplateDoc = JSON.parse( 676 const persistTemplateDoc = JSON.parse(
679 JSON.stringify(labelPrintJobPayload.template) 677 JSON.stringify(labelPrintJobPayload.template)
680 ) as Record<string, unknown> 678 ) as Record<string, unknown>
  679 + const printInputSnapshotForApi: Record<string, unknown> = {
  680 + ...printInputJson,
  681 + ...persistTemplateDoc,
  682 + }
681 683
682 printLogRequestBody = buildUsAppLabelPrintRequestBody({ 684 printLogRequestBody = buildUsAppLabelPrintRequestBody({
683 locationId: getCurrentStoreId(), 685 locationId: getCurrentStoreId(),
684 labelCode: labelCode.value, 686 labelCode: labelCode.value,
685 productId: productId.value || undefined, 687 productId: productId.value || undefined,
686 printQuantity: printQty.value, 688 printQuantity: printQty.value,
687 - mergedTemplate: persistTemplateDoc, 689 + mergedTemplate: printInputSnapshotForApi,
688 clientRequestId: createPrintClientRequestId(), 690 clientRequestId: createPrintClientRequestId(),
689 printerMac: bt?.deviceId || undefined, 691 printerMac: bt?.deviceId || undefined,
690 }) 692 })
691 if (printLogRequestBody) { 693 if (printLogRequestBody) {
692 - const printRes = await postUsAppLabelPrint(printLogRequestBody)  
693 - /** 本机快照:列表接口 renderTemplateJson 常为设计器占位,重打需与当次出纸合并模板一致 */  
694 - try {  
695 - const tid = String(  
696 - (printRes as { taskId?: string })?.taskId  
697 - ?? (printRes as { TaskId?: string })?.TaskId  
698 - ?? '',  
699 - ).trim()  
700 - if (tid) {  
701 - savePrintTemplateSnapshotForTask(tid, JSON.stringify(persistTemplateDoc))  
702 - }  
703 - } catch {  
704 - /* 忽略快照写入失败 */  
705 - } 694 + await postUsAppLabelPrint(printLogRequestBody)
706 } 695 }
707 } catch (syncErr: unknown) { 696 } catch (syncErr: unknown) {
708 if (!isUsAppSessionExpiredError(syncErr)) { 697 if (!isUsAppSessionExpiredError(syncErr)) {
美国版/Food Labeling Management App UniApp/src/pages/more/print-log.vue
@@ -125,11 +125,21 @@ @@ -125,11 +125,21 @@
125 </scroll-view> 125 </scroll-view>
126 126
127 <SideMenu v-model="isMenuOpen" /> 127 <SideMenu v-model="isMenuOpen" />
  128 +
  129 + <!-- 重打整页光栅与预览页一致:须绑定 width/height 像素,否则 canvasToTempFilePath 易空白 -->
  130 + <canvas
  131 + canvas-id="reprintLabelCanvas"
  132 + id="reprintLabelCanvas"
  133 + class="hidden-canvas"
  134 + :style="{ width: reprintCanvasW + 'px', height: reprintCanvasH + 'px' }"
  135 + :width="reprintCanvasW"
  136 + :height="reprintCanvasH"
  137 + />
128 </view> 138 </view>
129 </template> 139 </template>
130 140
131 <script setup lang="ts"> 141 <script setup lang="ts">
132 -import { ref } from 'vue' 142 +import { ref, getCurrentInstance, nextTick } from 'vue'
133 import { onShow } from '@dcloudio/uni-app' 143 import { onShow } from '@dcloudio/uni-app'
134 import AppIcon from '../../components/AppIcon.vue' 144 import AppIcon from '../../components/AppIcon.vue'
135 import SideMenu from '../../components/SideMenu.vue' 145 import SideMenu from '../../components/SideMenu.vue'
@@ -140,11 +150,8 @@ import { @@ -140,11 +150,8 @@ import {
140 fetchUsAppPrintLogList, 150 fetchUsAppPrintLogList,
141 postUsAppLabelReprint, 151 postUsAppLabelReprint,
142 } from '../../services/usAppLabeling' 152 } from '../../services/usAppLabeling'
143 -import {  
144 - consumeReprintEmittedTemplateJsonForPersist,  
145 - printFromPrintLogRow,  
146 -} from '../../utils/printFromPrintDataList'  
147 -import { savePrintTemplateSnapshotForTask } from '../../utils/printSnapshotStorage' 153 +import { printFromPrintLogRow } from '../../utils/printFromPrintDataList'
  154 +import type { SystemTemplatePrintCanvasRasterOptions } from '../../utils/print/manager/printerManager'
148 import { getBluetoothConnection, getPrinterType } from '../../utils/print/printerConnection' 155 import { getBluetoothConnection, getPrinterType } from '../../utils/print/printerConnection'
149 import { isUsAppSessionExpiredError } from '../../utils/usAppApiRequest' 156 import { isUsAppSessionExpiredError } from '../../utils/usAppApiRequest'
150 import type { PrintLogItemDto } from '../../types/usAppLabeling' 157 import type { PrintLogItemDto } from '../../types/usAppLabeling'
@@ -153,6 +160,20 @@ const statusBarHeight = getStatusBarHeight() @@ -153,6 +160,20 @@ const statusBarHeight = getStatusBarHeight()
153 const isMenuOpen = ref(false) 160 const isMenuOpen = ref(false)
154 const viewMode = ref<'card' | 'list'>('card') 161 const viewMode = ref<'card' | 'list'>('card')
155 162
  163 +const reprintCanvasW = ref(400)
  164 +const reprintCanvasH = ref(400)
  165 +
  166 +/** 须在 setup 顶层取实例,异步回调里 getCurrentInstance() 为 null */
  167 +const reprintCanvasComponentProxy = getCurrentInstance()?.proxy
  168 +
  169 +const applyReprintCanvasLayout: NonNullable<SystemTemplatePrintCanvasRasterOptions['applyLayout']> = async (
  170 + layout,
  171 +) => {
  172 + reprintCanvasW.value = layout.outW
  173 + reprintCanvasH.value = layout.outH
  174 + await nextTick()
  175 +}
  176 +
156 const items = ref<PrintLogItemDto[]>([]) 177 const items = ref<PrintLogItemDto[]>([])
157 const loading = ref(false) 178 const loading = ref(false)
158 const loadingMore = ref(false) 179 const loadingMore = ref(false)
@@ -250,9 +271,9 @@ const handleReprint = async (row: PrintLogItemDto) =&gt; { @@ -250,9 +271,9 @@ const handleReprint = async (row: PrintLogItemDto) =&gt; {
250 uni.showToast({ title: 'Please connect a printer first', icon: 'none' }) 271 uni.showToast({ title: 'Please connect a printer first', icon: 'none' })
251 return 272 return
252 } 273 }
253 - uni.showLoading({ title: 'Printing…', mask: true }) 274 + uni.showLoading({ title: 'Rendering…', mask: true })
254 try { 275 try {
255 - /** 优先 `renderTemplateJson` 完整模板(与接口 9 一致);无则回退 printDataList */ 276 + /** 整页 canvas 光栅:与 Label Preview 页「非 native 快打」同路径,图片/中文/¥ 与屏幕一致 */
256 await printFromPrintLogRow(row, { 277 await printFromPrintLogRow(row, {
257 printQty: 1, 278 printQty: 1,
258 onProgress: (pct) => { 279 onProgress: (pct) => {
@@ -260,30 +281,24 @@ const handleReprint = async (row: PrintLogItemDto) =&gt; { @@ -260,30 +281,24 @@ const handleReprint = async (row: PrintLogItemDto) =&gt; {
260 uni.showLoading({ title: `Printing ${pct}%`, mask: true }) 281 uni.showLoading({ title: `Printing ${pct}%`, mask: true })
261 } 282 }
262 }, 283 },
  284 + canvasRaster: reprintCanvasComponentProxy
  285 + ? {
  286 + canvasId: 'reprintLabelCanvas',
  287 + componentInstance: reprintCanvasComponentProxy,
  288 + applyLayout: applyReprintCanvasLayout,
  289 + }
  290 + : undefined,
263 }) 291 })
264 const bt = getBluetoothConnection() 292 const bt = getBluetoothConnection()
265 uni.showLoading({ title: 'Saving…', mask: true }) 293 uni.showLoading({ title: 'Saving…', mask: true })
266 /** 出纸成功后再调接口 11 记重打(与文档「重复打印」落库一致) */ 294 /** 出纸成功后再调接口 11 记重打(与文档「重复打印」落库一致) */
267 - const reprintRes = await postUsAppLabelReprint({ 295 + await postUsAppLabelReprint({
268 locationId, 296 locationId,
269 taskId: row.taskId, 297 taskId: row.taskId,
270 printQuantity: 1, 298 printQuantity: 1,
271 clientRequestId: createClientRequestId(), 299 clientRequestId: createClientRequestId(),
272 printerMac: bt?.deviceId || undefined, 300 printerMac: bt?.deviceId || undefined,
273 }) 301 })
274 - try {  
275 - const nextTid = String(  
276 - (reprintRes as { taskId?: string })?.taskId  
277 - ?? (reprintRes as { TaskId?: string })?.TaskId  
278 - ?? '',  
279 - ).trim()  
280 - const emitted = consumeReprintEmittedTemplateJsonForPersist()  
281 - if (nextTid && emitted) {  
282 - savePrintTemplateSnapshotForTask(nextTid, emitted)  
283 - }  
284 - } catch {  
285 - /* 忽略 */  
286 - }  
287 uni.showToast({ title: 'Done', icon: 'success' }) 302 uni.showToast({ title: 'Done', icon: 'success' })
288 } catch (e: unknown) { 303 } catch (e: unknown) {
289 if (!isUsAppSessionExpiredError(e)) { 304 if (!isUsAppSessionExpiredError(e)) {
@@ -607,4 +622,12 @@ const goBack = () =&gt; { @@ -607,4 +622,12 @@ const goBack = () =&gt; {
607 font-size: 24rpx; 622 font-size: 24rpx;
608 color: #6b7280; 623 color: #6b7280;
609 } 624 }
  625 +
  626 +.hidden-canvas {
  627 + position: fixed;
  628 + left: -9999px;
  629 + top: 0;
  630 + opacity: 0;
  631 + pointer-events: none;
  632 +}
610 </style> 633 </style>
美国版/Food Labeling Management App UniApp/src/services/usAppLabeling.ts
@@ -130,7 +130,7 @@ export function buildUsAppLabelPrintRequestBody(input: { @@ -130,7 +130,7 @@ export function buildUsAppLabelPrintRequestBody(input: {
130 labelCode?: string | null 130 labelCode?: string | null
131 productId?: string | null 131 productId?: string | null
132 printQuantity: number 132 printQuantity: number
133 - /** 与 buildLabelPrintJobPayload().template 同构,写入接口 printInputJson 供重打 */ 133 + /** 写入接口 9 `printInputJson`:合并模板快照(可与 `buildPrintInputJson` 结果浅合并),应对齐列表 `renderTemplateJson` */
134 mergedTemplate: Record<string, unknown> 134 mergedTemplate: Record<string, unknown>
135 clientRequestId?: string | null 135 clientRequestId?: string | null
136 printerMac?: string | null 136 printerMac?: string | null
美国版/Food Labeling Management App UniApp/src/types/usAppLabeling.ts
@@ -65,8 +65,8 @@ export interface UsAppLabelPrintInputVo { @@ -65,8 +65,8 @@ export interface UsAppLabelPrintInputVo {
65 clientRequestId?: string 65 clientRequestId?: string
66 baseTime?: string 66 baseTime?: string
67 /** 67 /**
68 - * 对齐《标签模块接口对接说明(10)》:存**可再次打印**的合并模板(与 label-template-*.json 同构,含 elements[].config)。  
69 - * 与 `buildLabelPrintJobPayload().template` 一致;服务端写入落库字段并用于重打。 68 + * 预览出纸落库:合并后的整份模板快照(id/name/unit/width/height/elements[]…,与 `buildLabelPrintJobPayload().template` 同源),
  69 + * 并可叠 `buildPrintInputJson` 的 PRINT_INPUT 键值;后端应写入任务表并在列表 `renderTemplateJson` 原样返回供重打。
70 */ 70 */
71 printInputJson?: Record<string, unknown> 71 printInputJson?: Record<string, unknown>
72 printerId?: string 72 printerId?: string
@@ -119,7 +119,11 @@ export interface PrintLogItemDto { @@ -119,7 +119,11 @@ export interface PrintLogItemDto {
119 /** 用于重打:元素列表(接口返回 printDataList) */ 119 /** 用于重打:元素列表(接口返回 printDataList) */
120 printDataList?: PrintLogDataItemDto[] | null 120 printDataList?: PrintLogDataItemDto[] | null
121 /** 121 /**
122 - * 列表接口若返回与接口 9 同构的完整模板 JSON 字符串(或含 `printInputJson` 的保存体),重打应优先用此字段以保留坐标与样式相关 config。 122 + * 接口 10:与接口 9 落库一致的打印快照(常为 JSON 字符串,含 elements/config),**重打优先**。
  123 + */
  124 + printInputJson?: string | Record<string, unknown> | null
  125 + /**
  126 + * 列表备用:渲染模板 JSON 字符串;无 `printInputJson` 时使用。
123 */ 127 */
124 renderTemplateJson?: string | null 128 renderTemplateJson?: string | null
125 } 129 }
美国版/Food Labeling Management App UniApp/src/utils/labelPreview/normalizePreviewTemplate.ts
@@ -24,6 +24,75 @@ function normalizeConfig(raw: unknown): Record&lt;string, unknown&gt; { @@ -24,6 +24,75 @@ function normalizeConfig(raw: unknown): Record&lt;string, unknown&gt; {
24 return { ...o } 24 return { ...o }
25 } 25 }
26 26
  27 +/**
  28 + * 落库/列表返回的 element 可能把 text、src、fontSize 等摊在根上,仅 `config` 为空或不全;
  29 + * 合并进 config 后打印与预览才能读到样式与图片地址。
  30 + */
  31 +function mergeFlatElementFieldsIntoConfig(
  32 + e: Record<string, unknown>,
  33 + cfg: Record<string, unknown>,
  34 +): Record<string, unknown> {
  35 + const out = { ...cfg }
  36 + const keys = [
  37 + 'text',
  38 + 'Text',
  39 + 'prefix',
  40 + 'Prefix',
  41 + 'suffix',
  42 + 'Suffix',
  43 + 'fontSize',
  44 + 'FontSize',
  45 + 'textAlign',
  46 + 'TextAlign',
  47 + 'fontFamily',
  48 + 'fontWeight',
  49 + 'color',
  50 + 'Color',
  51 + 'src',
  52 + 'Src',
  53 + 'url',
  54 + 'Url',
  55 + 'data',
  56 + 'Data',
  57 + 'value',
  58 + 'Value',
  59 + 'unit',
  60 + 'Unit',
  61 + 'format',
  62 + 'Format',
  63 + 'decimal',
  64 + 'Decimal',
  65 + 'inputType',
  66 + 'InputType',
  67 + 'offsetDays',
  68 + 'OffsetDays',
  69 + 'multipleOptionId',
  70 + 'MultipleOptionId',
  71 + 'multipleOptionName',
  72 + 'MultipleOptionName',
  73 + 'selectedOptionValues',
  74 + 'SelectedOptionValues',
  75 + 'errorLevel',
  76 + 'scaleMode',
  77 + 'showText',
  78 + 'placeholder',
  79 + 'Placeholder',
  80 + ] as const
  81 + for (const k of keys) {
  82 + const existing = out[k]
  83 + const has =
  84 + existing !== undefined &&
  85 + existing !== null &&
  86 + !(typeof existing === 'string' && String(existing).trim() === '')
  87 + if (has) continue
  88 + const v = e[k]
  89 + if (v !== undefined && v !== null && !(typeof v === 'string' && String(v).trim() === '')) {
  90 + out[k] = v as unknown
  91 + }
  92 + }
  93 + return out
  94 +}
  95 +
27 const TEXT_PRODUCT_PLACEHOLDERS = new Set(['', '文本', 'text', 'Text', 'TEXT', 'Label', 'label']) 96 const TEXT_PRODUCT_PLACEHOLDERS = new Set(['', '文本', 'text', 'Text', 'TEXT', 'Label', 'label'])
28 97
29 /** 98 /**
@@ -170,19 +239,70 @@ export function applyTemplateProductDefaultValuesToTemplate( @@ -170,19 +239,70 @@ export function applyTemplateProductDefaultValuesToTemplate(
170 return { ...template, elements } 239 return { ...template, elements }
171 } 240 }
172 241
  242 +function elementArrayLength (o: Record<string, unknown>): number {
  243 + const a = o.elements ?? o.Elements
  244 + return Array.isArray(a) ? a.length : 0
  245 +}
  246 +
  247 +/**
  248 + * 列表 `renderTemplateJson` / 接口 9 落库可能是:① 根上直接 elements;② 包在 `printInputJson`;③ 包在 `template`(对象或 JSON 字符串);
  249 + * ④ 根上既有 `template: {}` 又有 `elements` 时,**不能**误用空 template 丢掉根级 elements(会导致重打无字、无图)。
  250 + */
  251 +function pickTemplateRootRecord (payload: Record<string, unknown>): Record<string, unknown> {
  252 + const unwrapTemplateKey = (raw: unknown): Record<string, unknown> | null => {
  253 + if (raw == null) return null
  254 + if (typeof raw === 'string') {
  255 + const s = raw.trim()
  256 + if (!s) return null
  257 + try {
  258 + const p = JSON.parse(s) as unknown
  259 + if (p != null && typeof p === 'object' && !Array.isArray(p)) return p as Record<string, unknown>
  260 + } catch {
  261 + return null
  262 + }
  263 + return null
  264 + }
  265 + if (typeof raw === 'object' && !Array.isArray(raw)) return raw as Record<string, unknown>
  266 + return null
  267 + }
  268 +
  269 + if (elementArrayLength(payload) > 0) return payload
  270 +
  271 + const pi = payload.printInputJson ?? payload.PrintInputJson
  272 + if (pi != null && typeof pi === 'object' && !Array.isArray(pi)) {
  273 + const p = pi as Record<string, unknown>
  274 + if (elementArrayLength(p) > 0) return p
  275 + }
  276 +
  277 + const nested =
  278 + unwrapTemplateKey(payload.template ?? payload.Template) ?? asRecord(payload.template ?? payload.Template)
  279 + if (elementArrayLength(nested) > 0) return nested
  280 +
  281 + if (pi != null && typeof pi === 'object' && !Array.isArray(pi)) return pi as Record<string, unknown>
  282 + if (Object.keys(nested).length > 0) return nested
  283 + return payload
  284 +}
  285 +
173 /** 286 /**
174 * 将接口 8.2 返回的 template(或整段 DTO)规范为 SystemLabelTemplate,供打印适配器与预览画布使用。 287 * 将接口 8.2 返回的 template(或整段 DTO)规范为 SystemLabelTemplate,供打印适配器与预览画布使用。
175 */ 288 */
176 export function normalizeLabelTemplateFromPreviewApi(payload: unknown): SystemLabelTemplate | null { 289 export function normalizeLabelTemplateFromPreviewApi(payload: unknown): SystemLabelTemplate | null {
177 if (payload == null || typeof payload !== 'object') return null 290 if (payload == null || typeof payload !== 'object') return null
178 - const root = payload as Record<string, unknown>  
179 - const t = asRecord(root.template ?? root.Template ?? root) 291 + let root = payload as Record<string, unknown>
  292 + const wrapped = root.data ?? root.Data
  293 + if (wrapped != null && typeof wrapped === 'object' && !Array.isArray(wrapped)) {
  294 + root = wrapped as Record<string, unknown>
  295 + }
  296 + const t = pickTemplateRootRecord(root)
180 const elementsRaw = t.elements ?? t.Elements 297 const elementsRaw = t.elements ?? t.Elements
181 if (!Array.isArray(elementsRaw)) return null 298 if (!Array.isArray(elementsRaw)) return null
182 299
183 const elements: SystemTemplateElementBase[] = (elementsRaw as unknown[]).map((el, index) => { 300 const elements: SystemTemplateElementBase[] = (elementsRaw as unknown[]).map((el, index) => {
184 const e = asRecord(el) 301 const e = asRecord(el)
185 - const cfg = normalizeConfig(e.config ?? e.ConfigJson ?? e.configJson) 302 + let cfg = normalizeConfig(
  303 + e.config ?? e.Config ?? e.ConfigJson ?? e.configJson ?? e.ConfigString,
  304 + )
  305 + cfg = mergeFlatElementFieldsIntoConfig(e, cfg)
186 const type = String(e.type ?? e.elementType ?? e.ElementType ?? 'TEXT_STATIC') 306 const type = String(e.type ?? e.elementType ?? e.ElementType ?? 'TEXT_STATIC')
187 const vst = e.valueSourceType ?? e.ValueSourceType 307 const vst = e.valueSourceType ?? e.ValueSourceType
188 const ik = e.inputKey ?? e.InputKey 308 const ik = e.inputKey ?? e.InputKey
美国版/Food Labeling Management App UniApp/src/utils/print/hydrateTemplateImagesForPrint.ts 0 → 100644
  1 +/**
  2 + * TSC/Android 位图打印:`BitmapFactory.decodeFile` 无法直接读 http(s) 或「仅相对路径」;
  3 + * 打印前把需网络的图片拉到本地临时路径,重打/预览落库里的 /picture/ URL 才能出图。
  4 + */
  5 +import { resolveMediaUrlForApp, storedValueLooksLikeImagePath } from '../resolveMediaUrl'
  6 +import type { SystemLabelTemplate, SystemTemplateElementBase } from './types/printer'
  7 +
  8 +/** 与 usAppApiRequest 一致:静态图 /picture/ 常需登录态,无头下载会 401 → 解码失败、纸面空白 */
  9 +function downloadAuthHeaders (): Record<string, string> {
  10 + const h: Record<string, string> = {}
  11 + try {
  12 + const token = uni.getStorageSync('access_token')
  13 + if (token) h.Authorization = `Bearer ${token}`
  14 + } catch (_) {}
  15 + return h
  16 +}
  17 +
  18 +function cfgStr(config: Record<string, unknown>, keys: string[]): string {
  19 + for (const k of keys) {
  20 + const v = config[k]
  21 + if (v != null && String(v).trim() !== '') return String(v).trim()
  22 + }
  23 + return ''
  24 +}
  25 +
  26 +/** 需先下载再 decodeFile 的地址(非 data:、非已是本地 file) */
  27 +function needsDownloadForDecode (raw: string): boolean {
  28 + const s = String(raw || '').trim()
  29 + if (!s) return false
  30 + if (s.startsWith('data:')) return false
  31 + if (s.startsWith('file://')) return false
  32 + if (/^https?:\/\//i.test(s)) return true
  33 + if (s.startsWith('/picture/') || s.startsWith('/static/')) return true
  34 + return false
  35 +}
  36 +
  37 +function downloadUrlToTempFile (url: string): Promise<string | null> {
  38 + return new Promise((resolve) => {
  39 + if (!url) {
  40 + resolve(null)
  41 + return
  42 + }
  43 + try {
  44 + uni.downloadFile({
  45 + url,
  46 + header: downloadAuthHeaders(),
  47 + success: (res) => {
  48 + if (res.statusCode === 200 && res.tempFilePath) resolve(res.tempFilePath)
  49 + else resolve(null)
  50 + },
  51 + fail: () => resolve(null),
  52 + })
  53 + } catch {
  54 + resolve(null)
  55 + }
  56 + })
  57 +}
  58 +
  59 +async function resolveToLocalPathIfNeeded (raw: string): Promise<string | null> {
  60 + const trimmed = String(raw || '').trim()
  61 + if (!trimmed) return null
  62 + if (!needsDownloadForDecode(trimmed)) return null
  63 + const url = resolveMediaUrlForApp(trimmed)
  64 + if (!url) return null
  65 + return downloadUrlToTempFile(url)
  66 +}
  67 +
  68 +async function hydrateElement (el: SystemTemplateElementBase): Promise<SystemTemplateElementBase> {
  69 + const type = String(el.type || '').toUpperCase()
  70 + const cfg = { ...(el.config || {}) } as Record<string, unknown>
  71 +
  72 + if (type === 'IMAGE' || type === 'LOGO') {
  73 + const raw = cfgStr(cfg, ['src', 'url', 'Src', 'Url'])
  74 + const local = await resolveToLocalPathIfNeeded(raw)
  75 + if (!local) return el
  76 + return {
  77 + ...el,
  78 + config: { ...cfg, src: local, url: local, Src: local, Url: local },
  79 + }
  80 + }
  81 +
  82 + if (type === 'QRCODE') {
  83 + const raw = cfgStr(cfg, ['data', 'Data'])
  84 + if (!raw || !storedValueLooksLikeImagePath(raw)) return el
  85 + const local = await resolveToLocalPathIfNeeded(raw)
  86 + if (!local) return el
  87 + return {
  88 + ...el,
  89 + config: { ...cfg, data: local, Data: local, src: local, url: local },
  90 + }
  91 + }
  92 +
  93 + return el
  94 +}
  95 +
  96 +/**
  97 + * 返回新模板对象;无元素或无需下载时可能与原引用相同(未改元素时仍返回浅拷贝以统一调用方)。
  98 + */
  99 +export async function hydrateSystemTemplateImagesForPrint (
  100 + tmpl: SystemLabelTemplate
  101 +): Promise<SystemLabelTemplate> {
  102 + const elements = tmpl.elements || []
  103 + if (elements.length === 0) return tmpl
  104 +
  105 + const next = await Promise.all(elements.map((el) => hydrateElement(el)))
  106 + return { ...tmpl, elements: next }
  107 +}
美国版/Food Labeling Management App UniApp/src/utils/print/manager/printerManager.ts
@@ -21,7 +21,12 @@ import { @@ -21,7 +21,12 @@ import {
21 printNativeFastFromLabelPrintJob, 21 printNativeFastFromLabelPrintJob,
22 printNativeFastTemplate as printNativeFastTemplatePlugin, 22 printNativeFastTemplate as printNativeFastTemplatePlugin,
23 } from '../nativeFastPrinter' 23 } from '../nativeFastPrinter'
  24 +import {
  25 + getLabelPrintRasterLayout,
  26 + renderLabelPreviewCanvasToTempPathForPrint,
  27 +} from '../../labelPreview/renderLabelPreviewCanvas'
24 import { adaptSystemLabelTemplate } from '../systemTemplateAdapter' 28 import { adaptSystemLabelTemplate } from '../systemTemplateAdapter'
  29 +import { hydrateSystemTemplateImagesForPrint } from '../hydrateTemplateImagesForPrint'
25 import { TEST_PRINT_SYSTEM_TEMPLATE, TEST_PRINT_TEMPLATE_DATA } from '../templates/testPrintTemplate' 30 import { TEST_PRINT_SYSTEM_TEMPLATE, TEST_PRINT_TEMPLATE_DATA } from '../templates/testPrintTemplate'
26 import { describePrinterCandidate, getPrinterDriverByKey, resolvePrinterDriver } from './driverRegistry' 31 import { describePrinterCandidate, getPrinterDriverByKey, resolvePrinterDriver } from './driverRegistry'
27 import type { 32 import type {
@@ -491,15 +496,61 @@ export async function printTemplateForCurrentPrinter ( @@ -491,15 +496,61 @@ export async function printTemplateForCurrentPrinter (
491 return driver 496 return driver
492 } 497 }
493 498
  499 +/** 与预览页「整页光栅」分支一致:用同一套 canvas 绘制再下发位图(/picture/、中文、¥ 与屏幕一致) */
  500 +export type SystemTemplatePrintCanvasRasterOptions = {
  501 + canvasId: string
  502 + componentInstance: any
  503 + /** 绘制前把隐藏 canvas 的 width/height(像素)设为 layout.outW/outH,并 await nextTick */
  504 + applyLayout?: (layout: {
  505 + cw: number
  506 + ch: number
  507 + outW: number
  508 + outH: number
  509 + scale: number
  510 + }) => void | Promise<void>
  511 +}
  512 +
494 export async function printSystemTemplateForCurrentPrinter ( 513 export async function printSystemTemplateForCurrentPrinter (
495 template: SystemLabelTemplate, 514 template: SystemLabelTemplate,
496 data: LabelTemplateData = {}, 515 data: LabelTemplateData = {},
497 options: { 516 options: {
498 printQty?: number 517 printQty?: number
  518 + canvasRaster?: SystemTemplatePrintCanvasRasterOptions
499 } = {}, 519 } = {},
500 onProgress?: (percent: number) => void 520 onProgress?: (percent: number) => void
501 ): Promise<PrinterDriver> { 521 ): Promise<PrinterDriver> {
502 const driver = getCurrentPrinterDriver() 522 const driver = getCurrentPrinterDriver()
  523 + const canvasRaster = options.canvasRaster
  524 +
  525 + if (canvasRaster) {
  526 + const maxDots =
  527 + driver.imageMaxWidthDots || (driver.protocol === 'esc' ? 384 : 576)
  528 + const layout = getLabelPrintRasterLayout(template, maxDots, driver.imageDpi || 203)
  529 + if (canvasRaster.applyLayout) {
  530 + await canvasRaster.applyLayout(layout)
  531 + }
  532 + await new Promise<void>((r) => setTimeout(r, 50))
  533 + const tmpPath = await renderLabelPreviewCanvasToTempPathForPrint(
  534 + canvasRaster.canvasId,
  535 + canvasRaster.componentInstance,
  536 + template,
  537 + layout,
  538 + )
  539 + await printImageForCurrentPrinter(
  540 + tmpPath,
  541 + {
  542 + printQty: options.printQty || 1,
  543 + clearTopRasterRows: 1,
  544 + targetWidthDots: layout.outW,
  545 + targetHeightDots: layout.outH,
  546 + },
  547 + onProgress,
  548 + )
  549 + return driver
  550 + }
  551 +
  552 + const templateReady = await hydrateSystemTemplateImagesForPrint(template)
  553 +
503 const connection = getBluetoothConnection() 554 const connection = getBluetoothConnection()
504 if ( 555 if (
505 driver.protocol === 'tsc' 556 driver.protocol === 'tsc'
@@ -515,7 +566,7 @@ export async function printSystemTemplateForCurrentPrinter ( @@ -515,7 +566,7 @@ export async function printSystemTemplateForCurrentPrinter (
515 await printNativeFastTemplatePlugin({ 566 await printNativeFastTemplatePlugin({
516 deviceId: nativeConnection.deviceId, 567 deviceId: nativeConnection.deviceId,
517 deviceName: nativeConnection.deviceName, 568 deviceName: nativeConnection.deviceName,
518 - template, 569 + template: templateReady,
519 data, 570 data,
520 dpi: driver.imageDpi || 203, 571 dpi: driver.imageDpi || 203,
521 printQty: options.printQty || 1, 572 printQty: options.printQty || 1,
@@ -525,7 +576,7 @@ export async function printSystemTemplateForCurrentPrinter ( @@ -525,7 +576,7 @@ export async function printSystemTemplateForCurrentPrinter (
525 } 576 }
526 } 577 }
527 578
528 - const structuredTemplate = adaptSystemLabelTemplate(template, data, { 579 + const structuredTemplate = adaptSystemLabelTemplate(templateReady, data, {
529 dpi: driver.imageDpi || 203, 580 dpi: driver.imageDpi || 203,
530 printQty: options.printQty || 1, 581 printQty: options.printQty || 1,
531 disableBitmapText: driver.key === 'gp-d320fx', 582 disableBitmapText: driver.key === 'gp-d320fx',
美国版/Food Labeling Management App UniApp/src/utils/print/systemTemplateAdapter.ts
@@ -314,12 +314,11 @@ function resolveTextX (params: { @@ -314,12 +314,11 @@ function resolveTextX (params: {
314 return Math.max(0, left + Math.max(0, boxWidth - textWidth)) 314 return Math.max(0, left + Math.max(0, boxWidth - textWidth))
315 } 315 }
316 316
317 -/** TSC 内置西文字体无法显示全角¥时易成「?」;位图失败走此回退时替换为可打字符(与预览位图路径一致时可显示原符号) */ 317 +/** 全角人民币符在 TSC 内置字库常成「?」;规范为半角 ¥(U+00A5),与 tscLabelBuilder 单字节编码一致,勿再用字母 Y */
318 function sanitizeTextForTscBuiltinFont (text: string): string { 318 function sanitizeTextForTscBuiltinFont (text: string): string {
319 return String(text || '') 319 return String(text || '')
320 - .replace(/\uFFE5/g, 'Y')  
321 - .replace(/\u00A5/g, 'Y')  
322 - .replace(/¥/g, 'Y') 320 + .replace(/\uFFE5/g, '\u00A5')
  321 + .replace(/¥/g, '\u00A5')
323 } 322 }
324 323
325 function buildTscTemplate ( 324 function buildTscTemplate (
@@ -356,7 +355,15 @@ function buildTscTemplate ( @@ -356,7 +355,15 @@ function buildTscTemplate (
356 const scale = resolveTextScale(getConfigNumber(config, ['fontSize'], 14), dpi) 355 const scale = resolveTextScale(getConfigNumber(config, ['fontSize'], 14), dpi)
357 const align = resolveElementAlign(element, pageWidth) 356 const align = resolveElementAlign(element, pageWidth)
358 357
359 - if (!options.disableBitmapText && shouldRasterizeTextElement(text, type)) { 358 + /**
  359 + * gp-d320fx 等机型默认 disableBitmapText(走 TSC 文本);但内置字库把 ¥(0xA5) 打成字母 Y。
  360 + * 含货币符号时仍尝试 Android 位图文本,成功则纸面与预览一致。
  361 + */
  362 + const currencyGlyph = /[\u00A5\uFFE5€£¥]/.test(text)
  363 + const tryTextBitmap =
  364 + shouldRasterizeTextElement(text, type) &&
  365 + (!options.disableBitmapText || currencyGlyph)
  366 + if (tryTextBitmap) {
360 const bitmapPatch = createTextBitmapPatch({ 367 const bitmapPatch = createTextBitmapPatch({
361 element, 368 element,
362 text, 369 text,
美国版/Food Labeling Management App UniApp/src/utils/printFromPrintDataList.ts
1 -import { serializeElementForLabelTemplateJson } from './labelPreview/buildLabelPrintPayload'  
2 import { 1 import {
3 normalizeLabelTemplateFromPreviewApi, 2 normalizeLabelTemplateFromPreviewApi,
4 parseLabelSizeText, 3 parseLabelSizeText,
5 sortElementsForPreview, 4 sortElementsForPreview,
6 } from './labelPreview/normalizePreviewTemplate' 5 } from './labelPreview/normalizePreviewTemplate'
7 -import { printSystemTemplateForCurrentPrinter } from './print/manager/printerManager' 6 +import {
  7 + printSystemTemplateForCurrentPrinter,
  8 + type SystemTemplatePrintCanvasRasterOptions,
  9 +} from './print/manager/printerManager'
8 import type { 10 import type {
9 LabelTemplateData, 11 LabelTemplateData,
10 SystemLabelTemplate, 12 SystemLabelTemplate,
11 SystemTemplateElementBase, 13 SystemTemplateElementBase,
12 } from './print/types/printer' 14 } from './print/types/printer'
13 -import { getPrintTemplateSnapshotForTask } from './printSnapshotStorage'  
14 import type { PrintLogDataItemDto, PrintLogItemDto } from '../types/usAppLabeling' 15 import type { PrintLogDataItemDto, PrintLogItemDto } from '../types/usAppLabeling'
15 16
16 -/** 本次重打实际送机的模板(焙平后),供接口 11 返回新 taskId 时写入本机快照,支持连续重打 */  
17 -let reprintEmittedTemplateJsonForPersist: string | null = null  
18 -  
19 -export function consumeReprintEmittedTemplateJsonForPersist (): string | null {  
20 - const s = reprintEmittedTemplateJsonForPersist  
21 - reprintEmittedTemplateJsonForPersist = null  
22 - return s  
23 -}  
24 -  
25 -function rememberReprintEmittedTemplate (tmpl: SystemLabelTemplate) {  
26 - try {  
27 - reprintEmittedTemplateJsonForPersist = persistableTemplateJsonFromSystem(tmpl)  
28 - } catch {  
29 - reprintEmittedTemplateJsonForPersist = null  
30 - }  
31 -}  
32 -  
33 -/** 与 preview 落库、接口 printInputJson 根结构一致,便于本机存储与解析 */  
34 -function persistableTemplateJsonFromSystem (tmpl: SystemLabelTemplate): string {  
35 - const doc: Record<string, unknown> = {  
36 - id: String(tmpl.id ?? ''),  
37 - name: String(tmpl.name ?? 'Label'),  
38 - labelType: tmpl.labelType ?? '',  
39 - unit: String(tmpl.unit ?? 'inch'),  
40 - width: Number(tmpl.width) || 0,  
41 - height: Number(tmpl.height) || 0,  
42 - appliedLocation: tmpl.appliedLocation ?? 'ALL',  
43 - showRuler: tmpl.showRuler !== false,  
44 - showGrid: tmpl.showGrid !== false,  
45 - elements: (tmpl.elements || []).map((el) =>  
46 - serializeElementForLabelTemplateJson(el as SystemTemplateElementBase),  
47 - ),  
48 - }  
49 - return JSON.stringify(doc)  
50 -}  
51 -  
52 function nonEmptyDisplay (value: string | null | undefined): string { 17 function nonEmptyDisplay (value: string | null | undefined): string {
53 if (value == null || value === '' || value === '无') return '' 18 if (value == null || value === '' || value === '无') return ''
54 return String(value).trim() 19 return String(value).trim()
@@ -354,8 +319,15 @@ function overlayReprintResolvedFields ( @@ -354,8 +319,15 @@ function overlayReprintResolvedFields (
354 return { ...t, elements } 319 return { ...t, elements }
355 } 320 }
356 321
  322 +/** 打印日志重打:可选整页 canvas 光栅,与预览页「非 native 快打」分支一致 */
  323 +export type PrintFromPrintLogOptions = {
  324 + printQty?: number
  325 + onProgress?: (percent: number) => void
  326 + canvasRaster?: SystemTemplatePrintCanvasRasterOptions
  327 +}
  328 +
357 /** 329 /**
358 - * 使用接口 10 返回的 `printDataList` 组装模板并走当前打印机(与预览同路径) 330 + * 使用接口 10 返回的 `printDataList` 组装模板并走当前打印机
359 */ 331 */
360 function logReprintJson (label: string, data: unknown): void { 332 function logReprintJson (label: string, data: unknown): void {
361 try { 333 try {
@@ -367,10 +339,7 @@ function logReprintJson (label: string, data: unknown): void { @@ -367,10 +339,7 @@ function logReprintJson (label: string, data: unknown): void {
367 339
368 export async function printFromPrintDataListRow ( 340 export async function printFromPrintDataListRow (
369 row: PrintLogItemDto, 341 row: PrintLogItemDto,
370 - options: {  
371 - printQty?: number  
372 - onProgress?: (percent: number) => void  
373 - } = {} 342 + options: PrintFromPrintLogOptions = {}
374 ): Promise<void> { 343 ): Promise<void> {
375 const list = 344 const list =
376 row.printDataList ?? 345 row.printDataList ??
@@ -413,30 +382,41 @@ export async function printFromPrintDataListRow ( @@ -413,30 +382,41 @@ export async function printFromPrintDataListRow (
413 382
414 logReprintJson('bake + sort 后送打印机模板 tmpl', tmpl) 383 logReprintJson('bake + sort 后送打印机模板 tmpl', tmpl)
415 384
416 - rememberReprintEmittedTemplate(tmpl)  
417 await printSystemTemplateForCurrentPrinter( 385 await printSystemTemplateForCurrentPrinter(
418 tmpl, 386 tmpl,
419 templateData, 387 templateData,
420 - { printQty: options.printQty ?? 1 }, 388 + { printQty: options.printQty ?? 1, canvasRaster: options.canvasRaster },
421 options.onProgress 389 options.onProgress
422 ) 390 )
423 } 391 }
424 392
425 /** 393 /**
426 - * 从列表接口 `renderTemplateJson` 或本地保存的整段请求体中取出与 `printInputJson` 同构的对象 JSON 字符串。  
427 - * 支持:`{ "printInputJson": { "elements": [...] } }` 或直接 `{ "elements": [...] }`。 394 + * 从列表快照字符串(`printInputJson` / `renderTemplateJson`)解析出与接口 9 同构、可 `normalize` 的模板 JSON 字符串。
  395 + *
  396 + * 若根上同时有嵌套 `printInputJson`(小对象)和根级 `elements`(整模板),优先保留含 `elements` 的那份。
428 */ 397 */
429 export function extractPrintTemplateJsonForReprint (raw: string): string | null { 398 export function extractPrintTemplateJsonForReprint (raw: string): string | null {
430 const s = raw.trim() 399 const s = raw.trim()
431 if (!s) return null 400 if (!s) return null
432 try { 401 try {
433 - const doc = JSON.parse(s) as Record<string, unknown>  
434 - const pi = doc.printInputJson ?? doc.PrintInputJson  
435 - if (pi != null && typeof pi === 'object' && !Array.isArray(pi)) {  
436 - return JSON.stringify(pi) 402 + let doc: unknown = JSON.parse(s)
  403 + if (typeof doc === 'string') {
  404 + doc = JSON.parse(doc)
437 } 405 }
438 - if (Array.isArray(doc.elements) || Array.isArray(doc.Elements)) {  
439 - return JSON.stringify(doc) 406 + if (doc == null || typeof doc !== 'object' || Array.isArray(doc)) return null
  407 + const d = doc as Record<string, unknown>
  408 +
  409 + if (Array.isArray(d.elements) || Array.isArray(d.Elements)) {
  410 + return JSON.stringify(d)
  411 + }
  412 +
  413 + const pi = d.printInputJson ?? d.PrintInputJson
  414 + if (pi != null && typeof pi === 'object' && !Array.isArray(pi)) {
  415 + const p = pi as Record<string, unknown>
  416 + if (Array.isArray(p.elements) || Array.isArray(p.Elements)) {
  417 + return JSON.stringify(p)
  418 + }
  419 + return JSON.stringify(p)
440 } 420 }
441 } catch { 421 } catch {
442 return null 422 return null
@@ -444,61 +424,70 @@ export function extractPrintTemplateJsonForReprint (raw: string): string | null @@ -444,61 +424,70 @@ export function extractPrintTemplateJsonForReprint (raw: string): string | null
444 return null 424 return null
445 } 425 }
446 426
  427 +/** 列表项里 JSON 快照字段:可能是 string(含转义)或已解析的 object */
  428 +function snapshotJsonFieldToString (
  429 + row: PrintLogItemDto,
  430 + camel: 'printInputJson' | 'renderTemplateJson',
  431 + pascal: 'PrintInputJson' | 'RenderTemplateJson',
  432 +): string | null {
  433 + const rec = row as unknown as Record<string, unknown>
  434 + const r = rec[camel] ?? rec[pascal]
  435 + if (r == null) return null
  436 + if (typeof r === 'string') {
  437 + const t = r.trim()
  438 + return t || null
  439 + }
  440 + try {
  441 + return JSON.stringify(r)
  442 + } catch {
  443 + return null
  444 + }
  445 +}
  446 +
447 /** 447 /**
448 - * 打印日志重打入口:  
449 - * 1)本机按 taskId 存的合并快照(预览出纸成功时写入)——与接口 10 的 renderTemplateJson 解耦;  
450 - * 2)否则 `renderTemplateJson`(常为设计器占位,易错);  
451 - * 3)再否则 `printDataList`。 448 + * 打印日志重打入口:**优先 `printInputJson`**(与接口 9 落库快照一致),其次 `renderTemplateJson`,最后 `printDataList`。
452 */ 449 */
453 export async function printFromPrintLogRow ( 450 export async function printFromPrintLogRow (
454 row: PrintLogItemDto, 451 row: PrintLogItemDto,
455 - options: {  
456 - printQty?: number  
457 - onProgress?: (percent: number) => void  
458 - } = {} 452 + options: PrintFromPrintLogOptions = {}
459 ): Promise<void> { 453 ): Promise<void> {
460 - const r =  
461 - row.renderTemplateJson ??  
462 - (row as unknown as { RenderTemplateJson?: string | null }).RenderTemplateJson 454 + const fromPrintInput = snapshotJsonFieldToString(row, 'printInputJson', 'PrintInputJson')
  455 + const fromRender = snapshotJsonFieldToString(row, 'renderTemplateJson', 'RenderTemplateJson')
463 456
464 console.log('[Reprint] ========== 重复打印 JSON 调试 ==========') 457 console.log('[Reprint] ========== 重复打印 JSON 调试 ==========')
465 console.log('[Reprint] taskId', row.taskId, 'labelCode', row.labelCode, 'productName', row.productName) 458 console.log('[Reprint] taskId', row.taskId, 'labelCode', row.labelCode, 'productName', row.productName)
466 - logReprintJson('renderTemplateJson(列表字段,可为空)', r ?? '(无)') 459 + logReprintJson('printInputJson(优先,接口 10)', fromPrintInput ?? '(无)')
  460 + logReprintJson('renderTemplateJson(回退)', fromRender ?? '(无)')
  461 +
  462 + const tryExtract = (raw: string | null): string | null => {
  463 + if (!raw) return null
  464 + return extractPrintTemplateJsonForReprint(raw)
  465 + }
  466 +
  467 + const extracted = tryExtract(fromPrintInput) ?? tryExtract(fromRender)
467 468
468 - const localSnap = getPrintTemplateSnapshotForTask(row.taskId)  
469 - if (localSnap) {  
470 - console.log('[Reprint] 优先使用本机存储的合并快照(与当次预览出纸一致),taskId=', row.taskId)  
471 - logReprintJson('本机快照 JSON', localSnap)  
472 - await printFromMergedTemplateJsonString(localSnap, row, options) 469 + if (extracted) {
  470 + logReprintJson('extract 后用于打印的模板 JSON 字符串', extracted)
  471 + await printFromMergedTemplateJsonString(extracted, row, options)
473 return 472 return
474 } 473 }
475 474
476 - if (typeof r === 'string' && r.trim()) {  
477 - const extracted = extractPrintTemplateJsonForReprint(r)  
478 - if (extracted) {  
479 - logReprintJson('extract 后用于打印的模板 JSON 字符串', extracted)  
480 - await printFromMergedTemplateJsonString(extracted, row, options)  
481 - return  
482 - }  
483 - console.warn('[Reprint] renderTemplateJson 存在但 extractPrintTemplateJsonForReprint 失败,回退 printDataList') 475 + if (fromPrintInput || fromRender) {
  476 + console.warn('[Reprint] 有 printInputJson/renderTemplateJson 但 extract 失败,回退 printDataList')
484 } else { 477 } else {
485 - console.log('[Reprint] 无 renderTemplateJson,使用 printDataList') 478 + console.log('[Reprint] 无 printInputJson 与 renderTemplateJson,使用 printDataList')
486 } 479 }
487 480
488 await printFromPrintDataListRow(row, options) 481 await printFromPrintDataListRow(row, options)
489 } 482 }
490 483
491 /** 484 /**
492 - * 接口 11 返回的 `mergedTemplateJson`(与落库 PrintInputJson/RenderDataJson 同源),用于重打编排。  
493 - * 勿使用列表接口里的 `renderTemplateJson`/仅拆出来的 printDataList 代替完整模板,否则易缺坐标或缺用户输入快照。 485 + * 将已解析的快照 JSON 字符串走 normalize → overlay → bake 后送机(列表优先来自 `printInputJson`)。
494 */ 486 */
495 export async function printFromMergedTemplateJsonString ( 487 export async function printFromMergedTemplateJsonString (
496 mergedTemplateJson: string, 488 mergedTemplateJson: string,
497 row: PrintLogItemDto, 489 row: PrintLogItemDto,
498 - options: {  
499 - printQty?: number  
500 - onProgress?: (percent: number) => void  
501 - } = {} 490 + options: PrintFromPrintLogOptions = {}
502 ): Promise<void> { 491 ): Promise<void> {
503 console.log('[Reprint] 路径: renderTemplateJson / merged 完整模板') 492 console.log('[Reprint] 路径: renderTemplateJson / merged 完整模板')
504 logReprintJson('mergedTemplateJson 原始字符串', mergedTemplateJson) 493 logReprintJson('mergedTemplateJson 原始字符串', mergedTemplateJson)
@@ -506,6 +495,10 @@ export async function printFromMergedTemplateJsonString ( @@ -506,6 +495,10 @@ export async function printFromMergedTemplateJsonString (
506 let payload: unknown 495 let payload: unknown
507 try { 496 try {
508 payload = JSON.parse(mergedTemplateJson) as unknown 497 payload = JSON.parse(mergedTemplateJson) as unknown
  498 + /** 部分网关/序列化会把整段再包一层 JSON 字符串 */
  499 + if (typeof payload === 'string') {
  500 + payload = JSON.parse(payload) as unknown
  501 + }
509 } catch { 502 } catch {
510 throw new Error('Invalid merged template JSON') 503 throw new Error('Invalid merged template JSON')
511 } 504 }
@@ -526,11 +519,10 @@ export async function printFromMergedTemplateJsonString ( @@ -526,11 +519,10 @@ export async function printFromMergedTemplateJsonString (
526 logReprintJson('bake + sort 后送打印机模板 tmpl', tmpl) 519 logReprintJson('bake + sort 后送打印机模板 tmpl', tmpl)
527 logReprintJson('templateData(快照重打为空对象)', templateData) 520 logReprintJson('templateData(快照重打为空对象)', templateData)
528 521
529 - rememberReprintEmittedTemplate(tmpl)  
530 await printSystemTemplateForCurrentPrinter( 522 await printSystemTemplateForCurrentPrinter(
531 tmpl, 523 tmpl,
532 templateData, 524 templateData,
533 - { printQty: options.printQty ?? 1 }, 525 + { printQty: options.printQty ?? 1, canvasRaster: options.canvasRaster },
534 options.onProgress 526 options.onProgress
535 ) 527 )
536 } 528 }
美国版/Food Labeling Management App UniApp/src/utils/printSnapshotStorage.ts deleted
1 -/**  
2 - * 接口 10 返回的 renderTemplateJson 常为设计器占位(文本/名称/0.00),与当次出纸合并模板不一致。  
3 - * 本机按 taskId 存「与出纸同源」的合并模板 JSON,重打优先读取(仅前端,不改后端)。  
4 - */  
5 -const STORAGE_KEY = 'us_app_print_template_snapshot_v1'  
6 -const MAX_ENTRIES = 400  
7 -  
8 -type SnapshotStore = {  
9 - order: string[]  
10 - map: Record<string, string>  
11 -}  
12 -  
13 -function readStore (): SnapshotStore {  
14 - try {  
15 - const raw = uni.getStorageSync(STORAGE_KEY)  
16 - if (raw == null || raw === '') return { order: [], map: {} }  
17 - const p = typeof raw === 'string' ? JSON.parse(raw) : raw  
18 - if (!p || typeof p !== 'object') return { order: [], map: {} }  
19 - const order = Array.isArray(p.order) ? p.order.map((x: unknown) => String(x)) : []  
20 - const map = typeof p.map === 'object' && p.map && !Array.isArray(p.map) ? (p.map as Record<string, string>) : {}  
21 - return { order, map }  
22 - } catch {  
23 - return { order: [], map: {} }  
24 - }  
25 -}  
26 -  
27 -function writeStore (s: SnapshotStore) {  
28 - try {  
29 - uni.setStorageSync(STORAGE_KEY, JSON.stringify(s))  
30 - } catch {  
31 - /* 存储满或不可用则跳过 */  
32 - }  
33 -}  
34 -  
35 -function trimStore (s: SnapshotStore): SnapshotStore {  
36 - let { order, map } = s  
37 - while (order.length > MAX_ENTRIES) {  
38 - const k = order.shift()  
39 - if (k && map[k] != null) delete map[k]  
40 - }  
41 - return { order, map }  
42 -}  
43 -  
44 -/** 保存与本次出纸一致的合并模板 JSON(与 preview 落库 printInputJson 同构根对象) */  
45 -export function savePrintTemplateSnapshotForTask (taskId: string, mergedTemplateJson: string): void {  
46 - const id = String(taskId || '').trim()  
47 - if (!id || !mergedTemplateJson.trim()) return  
48 - const s = readStore()  
49 - const nextMap = { ...s.map, [id]: mergedTemplateJson }  
50 - let order = s.order.filter((k) => k !== id)  
51 - order.push(id)  
52 - writeStore(trimStore({ order, map: nextMap }))  
53 -}  
54 -  
55 -export function getPrintTemplateSnapshotForTask (taskId: string): string | null {  
56 - const id = String(taskId || '').trim()  
57 - if (!id) return null  
58 - const s = readStore()  
59 - const v = s.map[id]  
60 - return typeof v === 'string' && v.trim() ? v : null  
61 -}  
美国版/Food Labeling Management App UniApp/src/utils/reprintFromMergedTemplate.ts
@@ -2,7 +2,10 @@ import { @@ -2,7 +2,10 @@ import {
2 normalizeLabelTemplateFromPreviewApi, 2 normalizeLabelTemplateFromPreviewApi,
3 sortElementsForPreview, 3 sortElementsForPreview,
4 } from './labelPreview/normalizePreviewTemplate' 4 } from './labelPreview/normalizePreviewTemplate'
5 -import { printSystemTemplateForCurrentPrinter } from './print/manager/printerManager' 5 +import {
  6 + printSystemTemplateForCurrentPrinter,
  7 + type SystemTemplatePrintCanvasRasterOptions,
  8 +} from './print/manager/printerManager'
6 import type { SystemLabelTemplate } from './print/types/printer' 9 import type { SystemLabelTemplate } from './print/types/printer'
7 10
8 /** 11 /**
@@ -13,6 +16,7 @@ export async function printMergedTemplateJsonString ( @@ -13,6 +16,7 @@ export async function printMergedTemplateJsonString (
13 options: { 16 options: {
14 printQty?: number 17 printQty?: number
15 onProgress?: (percent: number) => void 18 onProgress?: (percent: number) => void
  19 + canvasRaster?: SystemTemplatePrintCanvasRasterOptions
16 } = {} 20 } = {}
17 ): Promise<void> { 21 ): Promise<void> {
18 let raw: unknown 22 let raw: unknown
@@ -32,7 +36,7 @@ export async function printMergedTemplateJsonString ( @@ -32,7 +36,7 @@ export async function printMergedTemplateJsonString (
32 await printSystemTemplateForCurrentPrinter( 36 await printSystemTemplateForCurrentPrinter(
33 sorted, 37 sorted,
34 {}, 38 {},
35 - { printQty: options.printQty ?? 1 }, 39 + { printQty: options.printQty ?? 1, canvasRaster: options.canvasRaster },
36 options.onProgress 40 options.onProgress
37 ) 41 )
38 } 42 }
美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/PropertiesPanel.tsx
@@ -14,13 +14,10 @@ import { Switch } from &#39;../../ui/switch&#39;; @@ -14,13 +14,10 @@ import { Switch } from &#39;../../ui/switch&#39;;
14 import type { 14 import type {
15 LabelTemplate, 15 LabelTemplate,
16 LabelElement, 16 LabelElement,
17 - LabelType,  
18 Unit, 17 Unit,
19 Rotation, 18 Rotation,
20 Border, 19 Border,
21 - AppliedLocation,  
22 } from '../../../types/labelTemplate'; 20 } from '../../../types/labelTemplate';
23 -import type { LocationDto } from '../../../types/location';  
24 import type { LabelMultipleOptionDto } from '../../../types/labelMultipleOption'; 21 import type { LabelMultipleOptionDto } from '../../../types/labelMultipleOption';
25 import { getLabelMultipleOptions } from '../../../services/labelMultipleOptionService'; 22 import { getLabelMultipleOptions } from '../../../services/labelMultipleOptionService';
26 import { Checkbox } from '../../ui/checkbox'; 23 import { Checkbox } from '../../ui/checkbox';
@@ -32,8 +29,6 @@ interface PropertiesPanelProps { @@ -32,8 +29,6 @@ interface PropertiesPanelProps {
32 onTemplateChange: (patch: Partial<LabelTemplate>) => void; 29 onTemplateChange: (patch: Partial<LabelTemplate>) => void;
33 onElementChange: (id: string, patch: Partial<LabelElement>) => void; 30 onElementChange: (id: string, patch: Partial<LabelElement>) => void;
34 onDeleteElement?: (id: string) => void; 31 onDeleteElement?: (id: string) => void;
35 - /** 门店列表:appliedLocation=SPECIFIED 时勾选 */  
36 - locations?: LocationDto[];  
37 /** 编辑已有模板时禁止修改 Template Code */ 32 /** 编辑已有模板时禁止修改 Template Code */
38 readOnlyTemplateCode?: boolean; 33 readOnlyTemplateCode?: boolean;
39 } 34 }
@@ -44,7 +39,6 @@ export function PropertiesPanel({ @@ -44,7 +39,6 @@ export function PropertiesPanel({
44 onTemplateChange, 39 onTemplateChange,
45 onElementChange, 40 onElementChange,
46 onDeleteElement, 41 onDeleteElement,
47 - locations = [],  
48 readOnlyTemplateCode = false, 42 readOnlyTemplateCode = false,
49 }: PropertiesPanelProps) { 43 }: PropertiesPanelProps) {
50 if (selectedElement) { 44 if (selectedElement) {
@@ -211,75 +205,6 @@ export function PropertiesPanel({ @@ -211,75 +205,6 @@ export function PropertiesPanel({
211 className="h-8 text-sm mt-1" 205 className="h-8 text-sm mt-1"
212 /> 206 />
213 </div> 207 </div>
214 - <div>  
215 - <Label className="text-xs">Label Type</Label>  
216 - <Select  
217 - value={template.labelType}  
218 - onValueChange={(v: LabelType) => onTemplateChange({ labelType: v })}  
219 - >  
220 - <SelectTrigger className="h-8 text-sm mt-1">  
221 - <SelectValue />  
222 - </SelectTrigger>  
223 - <SelectContent>  
224 - <SelectItem value="PRICE">PRICE</SelectItem>  
225 - <SelectItem value="NUTRITION">NUTRITION</SelectItem>  
226 - <SelectItem value="SHIPPING">SHIPPING</SelectItem>  
227 - </SelectContent>  
228 - </Select>  
229 - </div>  
230 - <div>  
231 - <Label className="text-xs">Applied Location</Label>  
232 - <Select  
233 - value={template.appliedLocation}  
234 - onValueChange={(v: AppliedLocation) => {  
235 - if (v === "ALL") {  
236 - onTemplateChange({ appliedLocation: v, appliedLocationIds: [] });  
237 - } else {  
238 - onTemplateChange({ appliedLocation: v });  
239 - }  
240 - }}  
241 - >  
242 - <SelectTrigger className="h-8 text-sm mt-1">  
243 - <SelectValue />  
244 - </SelectTrigger>  
245 - <SelectContent>  
246 - <SelectItem value="ALL">All locations</SelectItem>  
247 - <SelectItem value="SPECIFIED">Specified locations</SelectItem>  
248 - </SelectContent>  
249 - </Select>  
250 - </div>  
251 - {template.appliedLocation === "SPECIFIED" && (  
252 - <div className="rounded-md border border-gray-200 p-2 max-h-40 overflow-y-auto space-y-2">  
253 - <Label className="text-xs text-gray-600">Select locations</Label>  
254 - {locations.length === 0 ? (  
255 - <p className="text-xs text-gray-500">No locations loaded.</p>  
256 - ) : (  
257 - locations.map((loc) => {  
258 - const checked = (template.appliedLocationIds ?? []).includes(loc.id);  
259 - return (  
260 - <label  
261 - key={loc.id}  
262 - className="flex items-center gap-2 text-xs cursor-pointer"  
263 - >  
264 - <Checkbox  
265 - checked={checked}  
266 - onCheckedChange={(v) => {  
267 - const on = v === true;  
268 - const cur = new Set(template.appliedLocationIds ?? []);  
269 - if (on) cur.add(loc.id);  
270 - else cur.delete(loc.id);  
271 - onTemplateChange({ appliedLocationIds: Array.from(cur) });  
272 - }}  
273 - />  
274 - <span className="truncate">  
275 - {(loc.locationName ?? loc.locationCode ?? loc.id).trim() || loc.id}  
276 - </span>  
277 - </label>  
278 - );  
279 - })  
280 - )}  
281 - </div>  
282 - )}  
283 <div className="grid grid-cols-2 gap-2"> 208 <div className="grid grid-cols-2 gap-2">
284 <div> 209 <div>
285 <Label className="text-xs">Width</Label> 210 <Label className="text-xs">Width</Label>
美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/index.tsx
1 -import React, { useCallback, useEffect, useState } from 'react'; 1 +import React, { useCallback, useState } from 'react';
2 import { Button } from '../../ui/button'; 2 import { Button } from '../../ui/button';
3 import { ArrowLeft, Save, Download } from 'lucide-react'; 3 import { ArrowLeft, Save, Download } from 'lucide-react';
4 import { 4 import {
@@ -18,8 +18,6 @@ import { @@ -18,8 +18,6 @@ import {
18 resolvedValueSourceTypeForSave, 18 resolvedValueSourceTypeForSave,
19 valueSourceTypeForLibraryCategory, 19 valueSourceTypeForLibraryCategory,
20 } from '../../../types/labelTemplate'; 20 } from '../../../types/labelTemplate';
21 -import type { LocationDto } from '../../../types/location';  
22 -import { getLocations } from '../../../services/locationService';  
23 import { ElementsPanel } from './ElementsPanel'; 21 import { ElementsPanel } from './ElementsPanel';
24 import { LabelCanvas, LabelPreviewOnly } from './LabelCanvas'; 22 import { LabelCanvas, LabelPreviewOnly } from './LabelCanvas';
25 import { PropertiesPanel } from './PropertiesPanel'; 23 import { PropertiesPanel } from './PropertiesPanel';
@@ -52,22 +50,6 @@ export function LabelTemplateEditor({ @@ -52,22 +50,6 @@ export function LabelTemplateEditor({
52 const [selectedId, setSelectedId] = useState<string | null>(null); 50 const [selectedId, setSelectedId] = useState<string | null>(null);
53 const [scale, setScale] = useState(DEFAULT_SCALE); 51 const [scale, setScale] = useState(DEFAULT_SCALE);
54 const [previewOpen, setPreviewOpen] = useState(false); 52 const [previewOpen, setPreviewOpen] = useState(false);
55 - const [locations, setLocations] = useState<LocationDto[]>([]);  
56 -  
57 - useEffect(() => {  
58 - let cancelled = false;  
59 - (async () => {  
60 - try {  
61 - const res = await getLocations({ skipCount: 1, maxResultCount: 500 });  
62 - if (!cancelled) setLocations(res.items ?? []);  
63 - } catch {  
64 - if (!cancelled) setLocations([]);  
65 - }  
66 - })();  
67 - return () => {  
68 - cancelled = true;  
69 - };  
70 - }, []);  
71 53
72 const selectedElement = template.elements.find((el) => el.id === selectedId) ?? null; 54 const selectedElement = template.elements.find((el) => el.id === selectedId) ?? null;
73 55
@@ -325,7 +307,6 @@ export function LabelTemplateEditor({ @@ -325,7 +307,6 @@ export function LabelTemplateEditor({
325 onTemplateChange={handleTemplateChange} 307 onTemplateChange={handleTemplateChange}
326 onElementChange={updateElement} 308 onElementChange={updateElement}
327 onDeleteElement={deleteElement} 309 onDeleteElement={deleteElement}
328 - locations={locations}  
329 readOnlyTemplateCode={!!templateId} 310 readOnlyTemplateCode={!!templateId}
330 /> 311 />
331 </div> 312 </div>
美国版/Food Labeling Management Platform/src/components/ui/image-url-upload.tsx
@@ -10,7 +10,7 @@ export type ImageUrlUploadProps = { @@ -10,7 +10,7 @@ export type ImageUrlUploadProps = {
10 disabled?: boolean; 10 disabled?: boolean;
11 /** 辅助说明,显示在方框下方 */ 11 /** 辅助说明,显示在方框下方 */
12 hint?: string; 12 hint?: string;
13 - /** 空状态主文案 */ 13 + /** 空状态主文案(默认无,仅加号;需要时传入如 "Click to upload") */
14 emptyLabel?: string; 14 emptyLabel?: string;
15 accept?: string; 15 accept?: string;
16 /** 默认 5MB,与平台 picture 上传接口一致 */ 16 /** 默认 5MB,与平台 picture 上传接口一致 */
@@ -29,7 +29,7 @@ export function ImageUrlUpload({ @@ -29,7 +29,7 @@ export function ImageUrlUpload({
29 onChange, 29 onChange,
30 disabled, 30 disabled,
31 hint, 31 hint,
32 - emptyLabel = "Click to upload image", 32 + emptyLabel = "",
33 accept = "image/jpeg,image/png,image/webp,image/gif", 33 accept = "image/jpeg,image/png,image/webp,image/gif",
34 maxSizeMb = PICTURE_UPLOAD_MAX_BYTES / (1024 * 1024), 34 maxSizeMb = PICTURE_UPLOAD_MAX_BYTES / (1024 * 1024),
35 className, 35 className,
@@ -92,18 +92,30 @@ export function ImageUrlUpload({ @@ -92,18 +92,30 @@ export function ImageUrlUpload({
92 type="button" 92 type="button"
93 disabled={busy} 93 disabled={busy}
94 onClick={openPicker} 94 onClick={openPicker}
  95 + aria-label={emptyLabel || "Upload image"}
95 className={cn( 96 className={cn(
96 boxBase, 97 boxBase,
97 - "flex flex-col items-center justify-center gap-3 border-2 border-dashed border-gray-300 bg-gray-50/80 text-gray-400", 98 + "flex border-2 border-dashed border-gray-300 bg-gray-50/80 text-gray-400",
  99 + emptyLabel && !uploading
  100 + ? "flex-col items-center justify-center gap-2"
  101 + : "items-center justify-center",
98 "hover:border-gray-400 hover:bg-gray-100/90 hover:text-gray-500", 102 "hover:border-gray-400 hover:bg-gray-100/90 hover:text-gray-500",
99 "disabled:pointer-events-none disabled:opacity-50", 103 "disabled:pointer-events-none disabled:opacity-50",
100 boxClassName, 104 boxClassName,
101 )} 105 )}
102 > 106 >
103 - <Plus className="h-10 w-10 shrink-0 stroke-[1.25]" aria-hidden />  
104 - <span className="px-3 text-center text-sm font-normal leading-tight">  
105 - {uploading ? "Uploading…" : emptyLabel}  
106 - </span> 107 + {uploading ? (
  108 + <span className="px-3 text-center text-sm font-normal text-gray-500">Uploading…</span>
  109 + ) : (
  110 + <>
  111 + <Plus className="h-10 w-10 shrink-0 stroke-[1.25]" aria-hidden />
  112 + {emptyLabel ? (
  113 + <span className="px-3 text-center text-sm font-normal leading-tight text-gray-400">
  114 + {emptyLabel}
  115 + </span>
  116 + ) : null}
  117 + </>
  118 + )}
107 </button> 119 </button>
108 ) : ( 120 ) : (
109 <div 121 <div