Commit b04bc3a567c02aa0b11d271be06221577baf5a1c
1 parent
7af95447
打印日志 重新打印
Showing
14 changed files
with
462 additions
and
308 deletions
美国版/Food Labeling Management App UniApp/src/pages/labels/preview.vue
| ... | ... | @@ -215,7 +215,6 @@ import { |
| 215 | 215 | postUsAppLabelPrint, |
| 216 | 216 | US_APP_LABEL_PRINT_PATH, |
| 217 | 217 | } from '../../services/usAppLabeling' |
| 218 | -import { savePrintTemplateSnapshotForTask } from '../../utils/printSnapshotStorage' | |
| 219 | 218 | import { |
| 220 | 219 | applyTemplateProductDefaultValuesToTemplate, |
| 221 | 220 | extractTemplateProductDefaultValuesFromPreviewPayload, |
| ... | ... | @@ -671,38 +670,28 @@ const handlePrint = async () => { |
| 671 | 670 | try { |
| 672 | 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 | 676 | const persistTemplateDoc = JSON.parse( |
| 679 | 677 | JSON.stringify(labelPrintJobPayload.template) |
| 680 | 678 | ) as Record<string, unknown> |
| 679 | + const printInputSnapshotForApi: Record<string, unknown> = { | |
| 680 | + ...printInputJson, | |
| 681 | + ...persistTemplateDoc, | |
| 682 | + } | |
| 681 | 683 | |
| 682 | 684 | printLogRequestBody = buildUsAppLabelPrintRequestBody({ |
| 683 | 685 | locationId: getCurrentStoreId(), |
| 684 | 686 | labelCode: labelCode.value, |
| 685 | 687 | productId: productId.value || undefined, |
| 686 | 688 | printQuantity: printQty.value, |
| 687 | - mergedTemplate: persistTemplateDoc, | |
| 689 | + mergedTemplate: printInputSnapshotForApi, | |
| 688 | 690 | clientRequestId: createPrintClientRequestId(), |
| 689 | 691 | printerMac: bt?.deviceId || undefined, |
| 690 | 692 | }) |
| 691 | 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 | 696 | } catch (syncErr: unknown) { |
| 708 | 697 | if (!isUsAppSessionExpiredError(syncErr)) { | ... | ... |
美国版/Food Labeling Management App UniApp/src/pages/more/print-log.vue
| ... | ... | @@ -125,11 +125,21 @@ |
| 125 | 125 | </scroll-view> |
| 126 | 126 | |
| 127 | 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 | 138 | </view> |
| 129 | 139 | </template> |
| 130 | 140 | |
| 131 | 141 | <script setup lang="ts"> |
| 132 | -import { ref } from 'vue' | |
| 142 | +import { ref, getCurrentInstance, nextTick } from 'vue' | |
| 133 | 143 | import { onShow } from '@dcloudio/uni-app' |
| 134 | 144 | import AppIcon from '../../components/AppIcon.vue' |
| 135 | 145 | import SideMenu from '../../components/SideMenu.vue' |
| ... | ... | @@ -140,11 +150,8 @@ import { |
| 140 | 150 | fetchUsAppPrintLogList, |
| 141 | 151 | postUsAppLabelReprint, |
| 142 | 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 | 155 | import { getBluetoothConnection, getPrinterType } from '../../utils/print/printerConnection' |
| 149 | 156 | import { isUsAppSessionExpiredError } from '../../utils/usAppApiRequest' |
| 150 | 157 | import type { PrintLogItemDto } from '../../types/usAppLabeling' |
| ... | ... | @@ -153,6 +160,20 @@ const statusBarHeight = getStatusBarHeight() |
| 153 | 160 | const isMenuOpen = ref(false) |
| 154 | 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 | 177 | const items = ref<PrintLogItemDto[]>([]) |
| 157 | 178 | const loading = ref(false) |
| 158 | 179 | const loadingMore = ref(false) |
| ... | ... | @@ -250,9 +271,9 @@ const handleReprint = async (row: PrintLogItemDto) => { |
| 250 | 271 | uni.showToast({ title: 'Please connect a printer first', icon: 'none' }) |
| 251 | 272 | return |
| 252 | 273 | } |
| 253 | - uni.showLoading({ title: 'Printing…', mask: true }) | |
| 274 | + uni.showLoading({ title: 'Rendering…', mask: true }) | |
| 254 | 275 | try { |
| 255 | - /** 优先 `renderTemplateJson` 完整模板(与接口 9 一致);无则回退 printDataList */ | |
| 276 | + /** 整页 canvas 光栅:与 Label Preview 页「非 native 快打」同路径,图片/中文/¥ 与屏幕一致 */ | |
| 256 | 277 | await printFromPrintLogRow(row, { |
| 257 | 278 | printQty: 1, |
| 258 | 279 | onProgress: (pct) => { |
| ... | ... | @@ -260,30 +281,24 @@ const handleReprint = async (row: PrintLogItemDto) => { |
| 260 | 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 | 292 | const bt = getBluetoothConnection() |
| 265 | 293 | uni.showLoading({ title: 'Saving…', mask: true }) |
| 266 | 294 | /** 出纸成功后再调接口 11 记重打(与文档「重复打印」落库一致) */ |
| 267 | - const reprintRes = await postUsAppLabelReprint({ | |
| 295 | + await postUsAppLabelReprint({ | |
| 268 | 296 | locationId, |
| 269 | 297 | taskId: row.taskId, |
| 270 | 298 | printQuantity: 1, |
| 271 | 299 | clientRequestId: createClientRequestId(), |
| 272 | 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 | 302 | uni.showToast({ title: 'Done', icon: 'success' }) |
| 288 | 303 | } catch (e: unknown) { |
| 289 | 304 | if (!isUsAppSessionExpiredError(e)) { |
| ... | ... | @@ -607,4 +622,12 @@ const goBack = () => { |
| 607 | 622 | font-size: 24rpx; |
| 608 | 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 | 633 | </style> | ... | ... |
美国版/Food Labeling Management App UniApp/src/services/usAppLabeling.ts
| ... | ... | @@ -130,7 +130,7 @@ export function buildUsAppLabelPrintRequestBody(input: { |
| 130 | 130 | labelCode?: string | null |
| 131 | 131 | productId?: string | null |
| 132 | 132 | printQuantity: number |
| 133 | - /** 与 buildLabelPrintJobPayload().template 同构,写入接口 printInputJson 供重打 */ | |
| 133 | + /** 写入接口 9 `printInputJson`:合并模板快照(可与 `buildPrintInputJson` 结果浅合并),应对齐列表 `renderTemplateJson` */ | |
| 134 | 134 | mergedTemplate: Record<string, unknown> |
| 135 | 135 | clientRequestId?: string | null |
| 136 | 136 | printerMac?: string | null | ... | ... |
美国版/Food Labeling Management App UniApp/src/types/usAppLabeling.ts
| ... | ... | @@ -65,8 +65,8 @@ export interface UsAppLabelPrintInputVo { |
| 65 | 65 | clientRequestId?: string |
| 66 | 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 | 71 | printInputJson?: Record<string, unknown> |
| 72 | 72 | printerId?: string |
| ... | ... | @@ -119,7 +119,11 @@ export interface PrintLogItemDto { |
| 119 | 119 | /** 用于重打:元素列表(接口返回 printDataList) */ |
| 120 | 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 | 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<string, unknown> { |
| 24 | 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 | 96 | const TEXT_PRODUCT_PLACEHOLDERS = new Set(['', '文本', 'text', 'Text', 'TEXT', 'Label', 'label']) |
| 28 | 97 | |
| 29 | 98 | /** |
| ... | ... | @@ -170,19 +239,70 @@ export function applyTemplateProductDefaultValuesToTemplate( |
| 170 | 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 | 287 | * 将接口 8.2 返回的 template(或整段 DTO)规范为 SystemLabelTemplate,供打印适配器与预览画布使用。 |
| 175 | 288 | */ |
| 176 | 289 | export function normalizeLabelTemplateFromPreviewApi(payload: unknown): SystemLabelTemplate | null { |
| 177 | 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 | 297 | const elementsRaw = t.elements ?? t.Elements |
| 181 | 298 | if (!Array.isArray(elementsRaw)) return null |
| 182 | 299 | |
| 183 | 300 | const elements: SystemTemplateElementBase[] = (elementsRaw as unknown[]).map((el, index) => { |
| 184 | 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 | 306 | const type = String(e.type ?? e.elementType ?? e.ElementType ?? 'TEXT_STATIC') |
| 187 | 307 | const vst = e.valueSourceType ?? e.ValueSourceType |
| 188 | 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 | 21 | printNativeFastFromLabelPrintJob, |
| 22 | 22 | printNativeFastTemplate as printNativeFastTemplatePlugin, |
| 23 | 23 | } from '../nativeFastPrinter' |
| 24 | +import { | |
| 25 | + getLabelPrintRasterLayout, | |
| 26 | + renderLabelPreviewCanvasToTempPathForPrint, | |
| 27 | +} from '../../labelPreview/renderLabelPreviewCanvas' | |
| 24 | 28 | import { adaptSystemLabelTemplate } from '../systemTemplateAdapter' |
| 29 | +import { hydrateSystemTemplateImagesForPrint } from '../hydrateTemplateImagesForPrint' | |
| 25 | 30 | import { TEST_PRINT_SYSTEM_TEMPLATE, TEST_PRINT_TEMPLATE_DATA } from '../templates/testPrintTemplate' |
| 26 | 31 | import { describePrinterCandidate, getPrinterDriverByKey, resolvePrinterDriver } from './driverRegistry' |
| 27 | 32 | import type { |
| ... | ... | @@ -491,15 +496,61 @@ export async function printTemplateForCurrentPrinter ( |
| 491 | 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 | 513 | export async function printSystemTemplateForCurrentPrinter ( |
| 495 | 514 | template: SystemLabelTemplate, |
| 496 | 515 | data: LabelTemplateData = {}, |
| 497 | 516 | options: { |
| 498 | 517 | printQty?: number |
| 518 | + canvasRaster?: SystemTemplatePrintCanvasRasterOptions | |
| 499 | 519 | } = {}, |
| 500 | 520 | onProgress?: (percent: number) => void |
| 501 | 521 | ): Promise<PrinterDriver> { |
| 502 | 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 | 554 | const connection = getBluetoothConnection() |
| 504 | 555 | if ( |
| 505 | 556 | driver.protocol === 'tsc' |
| ... | ... | @@ -515,7 +566,7 @@ export async function printSystemTemplateForCurrentPrinter ( |
| 515 | 566 | await printNativeFastTemplatePlugin({ |
| 516 | 567 | deviceId: nativeConnection.deviceId, |
| 517 | 568 | deviceName: nativeConnection.deviceName, |
| 518 | - template, | |
| 569 | + template: templateReady, | |
| 519 | 570 | data, |
| 520 | 571 | dpi: driver.imageDpi || 203, |
| 521 | 572 | printQty: options.printQty || 1, |
| ... | ... | @@ -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 | 580 | dpi: driver.imageDpi || 203, |
| 530 | 581 | printQty: options.printQty || 1, |
| 531 | 582 | disableBitmapText: driver.key === 'gp-d320fx', | ... | ... |
美国版/Food Labeling Management App UniApp/src/utils/print/systemTemplateAdapter.ts
| ... | ... | @@ -314,12 +314,11 @@ function resolveTextX (params: { |
| 314 | 314 | return Math.max(0, left + Math.max(0, boxWidth - textWidth)) |
| 315 | 315 | } |
| 316 | 316 | |
| 317 | -/** TSC 内置西文字体无法显示全角¥时易成「?」;位图失败走此回退时替换为可打字符(与预览位图路径一致时可显示原符号) */ | |
| 317 | +/** 全角人民币符在 TSC 内置字库常成「?」;规范为半角 ¥(U+00A5),与 tscLabelBuilder 单字节编码一致,勿再用字母 Y */ | |
| 318 | 318 | function sanitizeTextForTscBuiltinFont (text: string): string { |
| 319 | 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 | 324 | function buildTscTemplate ( |
| ... | ... | @@ -356,7 +355,15 @@ function buildTscTemplate ( |
| 356 | 355 | const scale = resolveTextScale(getConfigNumber(config, ['fontSize'], 14), dpi) |
| 357 | 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 | 367 | const bitmapPatch = createTextBitmapPatch({ |
| 361 | 368 | element, |
| 362 | 369 | text, | ... | ... |
美国版/Food Labeling Management App UniApp/src/utils/printFromPrintDataList.ts
| 1 | -import { serializeElementForLabelTemplateJson } from './labelPreview/buildLabelPrintPayload' | |
| 2 | 1 | import { |
| 3 | 2 | normalizeLabelTemplateFromPreviewApi, |
| 4 | 3 | parseLabelSizeText, |
| 5 | 4 | sortElementsForPreview, |
| 6 | 5 | } from './labelPreview/normalizePreviewTemplate' |
| 7 | -import { printSystemTemplateForCurrentPrinter } from './print/manager/printerManager' | |
| 6 | +import { | |
| 7 | + printSystemTemplateForCurrentPrinter, | |
| 8 | + type SystemTemplatePrintCanvasRasterOptions, | |
| 9 | +} from './print/manager/printerManager' | |
| 8 | 10 | import type { |
| 9 | 11 | LabelTemplateData, |
| 10 | 12 | SystemLabelTemplate, |
| 11 | 13 | SystemTemplateElementBase, |
| 12 | 14 | } from './print/types/printer' |
| 13 | -import { getPrintTemplateSnapshotForTask } from './printSnapshotStorage' | |
| 14 | 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 | 17 | function nonEmptyDisplay (value: string | null | undefined): string { |
| 53 | 18 | if (value == null || value === '' || value === '无') return '' |
| 54 | 19 | return String(value).trim() |
| ... | ... | @@ -354,8 +319,15 @@ function overlayReprintResolvedFields ( |
| 354 | 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 | 332 | function logReprintJson (label: string, data: unknown): void { |
| 361 | 333 | try { |
| ... | ... | @@ -367,10 +339,7 @@ function logReprintJson (label: string, data: unknown): void { |
| 367 | 339 | |
| 368 | 340 | export async function printFromPrintDataListRow ( |
| 369 | 341 | row: PrintLogItemDto, |
| 370 | - options: { | |
| 371 | - printQty?: number | |
| 372 | - onProgress?: (percent: number) => void | |
| 373 | - } = {} | |
| 342 | + options: PrintFromPrintLogOptions = {} | |
| 374 | 343 | ): Promise<void> { |
| 375 | 344 | const list = |
| 376 | 345 | row.printDataList ?? |
| ... | ... | @@ -413,30 +382,41 @@ export async function printFromPrintDataListRow ( |
| 413 | 382 | |
| 414 | 383 | logReprintJson('bake + sort 后送打印机模板 tmpl', tmpl) |
| 415 | 384 | |
| 416 | - rememberReprintEmittedTemplate(tmpl) | |
| 417 | 385 | await printSystemTemplateForCurrentPrinter( |
| 418 | 386 | tmpl, |
| 419 | 387 | templateData, |
| 420 | - { printQty: options.printQty ?? 1 }, | |
| 388 | + { printQty: options.printQty ?? 1, canvasRaster: options.canvasRaster }, | |
| 421 | 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 | 398 | export function extractPrintTemplateJsonForReprint (raw: string): string | null { |
| 430 | 399 | const s = raw.trim() |
| 431 | 400 | if (!s) return null |
| 432 | 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 | 421 | } catch { |
| 442 | 422 | return null |
| ... | ... | @@ -444,61 +424,70 @@ export function extractPrintTemplateJsonForReprint (raw: string): string | null |
| 444 | 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 | 450 | export async function printFromPrintLogRow ( |
| 454 | 451 | row: PrintLogItemDto, |
| 455 | - options: { | |
| 456 | - printQty?: number | |
| 457 | - onProgress?: (percent: number) => void | |
| 458 | - } = {} | |
| 452 | + options: PrintFromPrintLogOptions = {} | |
| 459 | 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 | 457 | console.log('[Reprint] ========== 重复打印 JSON 调试 ==========') |
| 465 | 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 | 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 | 477 | } else { |
| 485 | - console.log('[Reprint] 无 renderTemplateJson,使用 printDataList') | |
| 478 | + console.log('[Reprint] 无 printInputJson 与 renderTemplateJson,使用 printDataList') | |
| 486 | 479 | } |
| 487 | 480 | |
| 488 | 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 | 487 | export async function printFromMergedTemplateJsonString ( |
| 496 | 488 | mergedTemplateJson: string, |
| 497 | 489 | row: PrintLogItemDto, |
| 498 | - options: { | |
| 499 | - printQty?: number | |
| 500 | - onProgress?: (percent: number) => void | |
| 501 | - } = {} | |
| 490 | + options: PrintFromPrintLogOptions = {} | |
| 502 | 491 | ): Promise<void> { |
| 503 | 492 | console.log('[Reprint] 路径: renderTemplateJson / merged 完整模板') |
| 504 | 493 | logReprintJson('mergedTemplateJson 原始字符串', mergedTemplateJson) |
| ... | ... | @@ -506,6 +495,10 @@ export async function printFromMergedTemplateJsonString ( |
| 506 | 495 | let payload: unknown |
| 507 | 496 | try { |
| 508 | 497 | payload = JSON.parse(mergedTemplateJson) as unknown |
| 498 | + /** 部分网关/序列化会把整段再包一层 JSON 字符串 */ | |
| 499 | + if (typeof payload === 'string') { | |
| 500 | + payload = JSON.parse(payload) as unknown | |
| 501 | + } | |
| 509 | 502 | } catch { |
| 510 | 503 | throw new Error('Invalid merged template JSON') |
| 511 | 504 | } |
| ... | ... | @@ -526,11 +519,10 @@ export async function printFromMergedTemplateJsonString ( |
| 526 | 519 | logReprintJson('bake + sort 后送打印机模板 tmpl', tmpl) |
| 527 | 520 | logReprintJson('templateData(快照重打为空对象)', templateData) |
| 528 | 521 | |
| 529 | - rememberReprintEmittedTemplate(tmpl) | |
| 530 | 522 | await printSystemTemplateForCurrentPrinter( |
| 531 | 523 | tmpl, |
| 532 | 524 | templateData, |
| 533 | - { printQty: options.printQty ?? 1 }, | |
| 525 | + { printQty: options.printQty ?? 1, canvasRaster: options.canvasRaster }, | |
| 534 | 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 | 2 | normalizeLabelTemplateFromPreviewApi, |
| 3 | 3 | sortElementsForPreview, |
| 4 | 4 | } from './labelPreview/normalizePreviewTemplate' |
| 5 | -import { printSystemTemplateForCurrentPrinter } from './print/manager/printerManager' | |
| 5 | +import { | |
| 6 | + printSystemTemplateForCurrentPrinter, | |
| 7 | + type SystemTemplatePrintCanvasRasterOptions, | |
| 8 | +} from './print/manager/printerManager' | |
| 6 | 9 | import type { SystemLabelTemplate } from './print/types/printer' |
| 7 | 10 | |
| 8 | 11 | /** |
| ... | ... | @@ -13,6 +16,7 @@ export async function printMergedTemplateJsonString ( |
| 13 | 16 | options: { |
| 14 | 17 | printQty?: number |
| 15 | 18 | onProgress?: (percent: number) => void |
| 19 | + canvasRaster?: SystemTemplatePrintCanvasRasterOptions | |
| 16 | 20 | } = {} |
| 17 | 21 | ): Promise<void> { |
| 18 | 22 | let raw: unknown |
| ... | ... | @@ -32,7 +36,7 @@ export async function printMergedTemplateJsonString ( |
| 32 | 36 | await printSystemTemplateForCurrentPrinter( |
| 33 | 37 | sorted, |
| 34 | 38 | {}, |
| 35 | - { printQty: options.printQty ?? 1 }, | |
| 39 | + { printQty: options.printQty ?? 1, canvasRaster: options.canvasRaster }, | |
| 36 | 40 | options.onProgress |
| 37 | 41 | ) |
| 38 | 42 | } | ... | ... |
美国版/Food Labeling Management Platform/src/components/labels/LabelTemplateEditor/PropertiesPanel.tsx
| ... | ... | @@ -14,13 +14,10 @@ import { Switch } from '../../ui/switch'; |
| 14 | 14 | import type { |
| 15 | 15 | LabelTemplate, |
| 16 | 16 | LabelElement, |
| 17 | - LabelType, | |
| 18 | 17 | Unit, |
| 19 | 18 | Rotation, |
| 20 | 19 | Border, |
| 21 | - AppliedLocation, | |
| 22 | 20 | } from '../../../types/labelTemplate'; |
| 23 | -import type { LocationDto } from '../../../types/location'; | |
| 24 | 21 | import type { LabelMultipleOptionDto } from '../../../types/labelMultipleOption'; |
| 25 | 22 | import { getLabelMultipleOptions } from '../../../services/labelMultipleOptionService'; |
| 26 | 23 | import { Checkbox } from '../../ui/checkbox'; |
| ... | ... | @@ -32,8 +29,6 @@ interface PropertiesPanelProps { |
| 32 | 29 | onTemplateChange: (patch: Partial<LabelTemplate>) => void; |
| 33 | 30 | onElementChange: (id: string, patch: Partial<LabelElement>) => void; |
| 34 | 31 | onDeleteElement?: (id: string) => void; |
| 35 | - /** 门店列表:appliedLocation=SPECIFIED 时勾选 */ | |
| 36 | - locations?: LocationDto[]; | |
| 37 | 32 | /** 编辑已有模板时禁止修改 Template Code */ |
| 38 | 33 | readOnlyTemplateCode?: boolean; |
| 39 | 34 | } |
| ... | ... | @@ -44,7 +39,6 @@ export function PropertiesPanel({ |
| 44 | 39 | onTemplateChange, |
| 45 | 40 | onElementChange, |
| 46 | 41 | onDeleteElement, |
| 47 | - locations = [], | |
| 48 | 42 | readOnlyTemplateCode = false, |
| 49 | 43 | }: PropertiesPanelProps) { |
| 50 | 44 | if (selectedElement) { |
| ... | ... | @@ -211,75 +205,6 @@ export function PropertiesPanel({ |
| 211 | 205 | className="h-8 text-sm mt-1" |
| 212 | 206 | /> |
| 213 | 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 | 208 | <div className="grid grid-cols-2 gap-2"> |
| 284 | 209 | <div> |
| 285 | 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 | 2 | import { Button } from '../../ui/button'; |
| 3 | 3 | import { ArrowLeft, Save, Download } from 'lucide-react'; |
| 4 | 4 | import { |
| ... | ... | @@ -18,8 +18,6 @@ import { |
| 18 | 18 | resolvedValueSourceTypeForSave, |
| 19 | 19 | valueSourceTypeForLibraryCategory, |
| 20 | 20 | } from '../../../types/labelTemplate'; |
| 21 | -import type { LocationDto } from '../../../types/location'; | |
| 22 | -import { getLocations } from '../../../services/locationService'; | |
| 23 | 21 | import { ElementsPanel } from './ElementsPanel'; |
| 24 | 22 | import { LabelCanvas, LabelPreviewOnly } from './LabelCanvas'; |
| 25 | 23 | import { PropertiesPanel } from './PropertiesPanel'; |
| ... | ... | @@ -52,22 +50,6 @@ export function LabelTemplateEditor({ |
| 52 | 50 | const [selectedId, setSelectedId] = useState<string | null>(null); |
| 53 | 51 | const [scale, setScale] = useState(DEFAULT_SCALE); |
| 54 | 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 | 54 | const selectedElement = template.elements.find((el) => el.id === selectedId) ?? null; |
| 73 | 55 | |
| ... | ... | @@ -325,7 +307,6 @@ export function LabelTemplateEditor({ |
| 325 | 307 | onTemplateChange={handleTemplateChange} |
| 326 | 308 | onElementChange={updateElement} |
| 327 | 309 | onDeleteElement={deleteElement} |
| 328 | - locations={locations} | |
| 329 | 310 | readOnlyTemplateCode={!!templateId} |
| 330 | 311 | /> |
| 331 | 312 | </div> | ... | ... |
美国版/Food Labeling Management Platform/src/components/ui/image-url-upload.tsx
| ... | ... | @@ -10,7 +10,7 @@ export type ImageUrlUploadProps = { |
| 10 | 10 | disabled?: boolean; |
| 11 | 11 | /** 辅助说明,显示在方框下方 */ |
| 12 | 12 | hint?: string; |
| 13 | - /** 空状态主文案 */ | |
| 13 | + /** 空状态主文案(默认无,仅加号;需要时传入如 "Click to upload") */ | |
| 14 | 14 | emptyLabel?: string; |
| 15 | 15 | accept?: string; |
| 16 | 16 | /** 默认 5MB,与平台 picture 上传接口一致 */ |
| ... | ... | @@ -29,7 +29,7 @@ export function ImageUrlUpload({ |
| 29 | 29 | onChange, |
| 30 | 30 | disabled, |
| 31 | 31 | hint, |
| 32 | - emptyLabel = "Click to upload image", | |
| 32 | + emptyLabel = "", | |
| 33 | 33 | accept = "image/jpeg,image/png,image/webp,image/gif", |
| 34 | 34 | maxSizeMb = PICTURE_UPLOAD_MAX_BYTES / (1024 * 1024), |
| 35 | 35 | className, |
| ... | ... | @@ -92,18 +92,30 @@ export function ImageUrlUpload({ |
| 92 | 92 | type="button" |
| 93 | 93 | disabled={busy} |
| 94 | 94 | onClick={openPicker} |
| 95 | + aria-label={emptyLabel || "Upload image"} | |
| 95 | 96 | className={cn( |
| 96 | 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 | 102 | "hover:border-gray-400 hover:bg-gray-100/90 hover:text-gray-500", |
| 99 | 103 | "disabled:pointer-events-none disabled:opacity-50", |
| 100 | 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 | 119 | </button> |
| 108 | 120 | ) : ( |
| 109 | 121 | <div | ... | ... |