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,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) => { | @@ -250,9 +271,9 @@ const handleReprint = async (row: PrintLogItemDto) => { | ||
| 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) => { | @@ -260,30 +281,24 @@ const handleReprint = async (row: PrintLogItemDto) => { | ||
| 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 = () => { | @@ -607,4 +622,12 @@ const goBack = () => { | ||
| 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<string, unknown> { | @@ -24,6 +24,75 @@ function normalizeConfig(raw: unknown): Record<string, unknown> { | ||
| 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 '../../ui/switch'; | @@ -14,13 +14,10 @@ import { Switch } from '../../ui/switch'; | ||
| 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 |