Commit b04bc3a567c02aa0b11d271be06221577baf5a1c

Authored by 杨鑫
1 parent 7af95447

打印日志 重新打印

美国版/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) =&gt; {
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) =&gt; {
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 = () =&gt; {
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&lt;string, unknown&gt; {
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 &#39;../../ui/switch&#39;;
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
... ...