Commit 9d21893049b40f11372c0a879399537b164c131b

Authored by 杨鑫
1 parent b11f42ef

修改

Showing 19 changed files with 562 additions and 87 deletions
打印机安卓基座/native-fast-printer/android-src/src/com/foodlabel/nativeprinter/template/NativeTemplateCommandBuilder.java
@@ -367,6 +367,9 @@ public final class NativeTemplateCommandBuilder { @@ -367,6 +367,9 @@ public final class NativeTemplateCommandBuilder {
367 } 367 }
368 368
369 private static boolean shouldRasterizeText(String text, String type, JSONObject config) { 369 private static boolean shouldRasterizeText(String text, String type, JSONObject config) {
  370 + if (getBoolean(config, "invertColors", false) || getBoolean(config, "InvertColors", false)) {
  371 + return true;
  372 + }
370 if (getBoolean(config, "forceRasterText", false)) { 373 if (getBoolean(config, "forceRasterText", false)) {
371 return true; 374 return true;
372 } 375 }
@@ -431,11 +434,13 @@ public final class NativeTemplateCommandBuilder { @@ -431,11 +434,13 @@ public final class NativeTemplateCommandBuilder {
431 text = text.replaceAll("\\s{2,}", " ").trim(); 434 text = text.replaceAll("\\s{2,}", " ").trim();
432 } 435 }
433 int contentWidth = Math.max(8, pxToDots(getDouble(element, "width", 0), dpi)); 436 int contentWidth = Math.max(8, pxToDots(getDouble(element, "width", 0), dpi));
  437 + int elementHeightDots = Math.max(16, pxToDots(getDouble(element, "height", 0), dpi));
  438 + boolean inverted = getBoolean(config, "invertColors", false) || getBoolean(config, "InvertColors", false);
434 Paint paint = new Paint(); 439 Paint paint = new Paint();
435 paint.setAntiAlias(true); 440 paint.setAntiAlias(true);
436 paint.setDither(true); 441 paint.setDither(true);
437 paint.setSubpixelText(true); 442 paint.setSubpixelText(true);
438 - paint.setColor(Color.BLACK); 443 + paint.setColor(inverted ? Color.WHITE : Color.BLACK);
439 int fontSizeDots = Math.max(14, pxToDots(getDouble(config, "fontSize", 14), dpi)); 444 int fontSizeDots = Math.max(14, pxToDots(getDouble(config, "fontSize", 14), dpi));
440 paint.setTextSize(fontSizeDots); 445 paint.setTextSize(fontSizeDots);
441 /** 不再对 TEXT_PRICE 强制加粗:fakeBold + 粗体会糊边、measureText 偏窄,右对齐时左侧易出现杂点 */ 446 /** 不再对 TEXT_PRICE 强制加粗:fakeBold + 粗体会糊边、measureText 偏窄,右对齐时左侧易出现杂点 */
@@ -467,10 +472,15 @@ public final class NativeTemplateCommandBuilder { @@ -467,10 +472,15 @@ public final class NativeTemplateCommandBuilder {
467 int horizontalPadding = TEXT_PADDING_DOTS * 2; 472 int horizontalPadding = TEXT_PADDING_DOTS * 2;
468 int verticalPadding = TEXT_PADDING_DOTS * 2; 473 int verticalPadding = TEXT_PADDING_DOTS * 2;
469 int width = ensureMultipleOf8(Math.max(contentWidth + horizontalPadding * 2, (int) Math.ceil(maxLineWidth) + horizontalPadding * 2 + 4)); 474 int width = ensureMultipleOf8(Math.max(contentWidth + horizontalPadding * 2, (int) Math.ceil(maxLineWidth) + horizontalPadding * 2 + 4));
470 - int height = Math.max(16, Math.max(pxToDots(getDouble(element, "height", 0), dpi) + verticalPadding * 2, totalHeight + verticalPadding * 2 + 4)); 475 + int height = Math.max(16, Math.max(elementHeightDots + verticalPadding * 2, totalHeight + verticalPadding * 2 + 4));
471 Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 476 Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
472 Canvas canvas = new Canvas(bitmap); 477 Canvas canvas = new Canvas(bitmap);
473 - canvas.drawColor(Color.WHITE); 478 + canvas.drawColor(inverted ? Color.BLACK : Color.WHITE);
  479 + if (inverted && elementHeightDots > 0 && contentWidth > 0) {
  480 + Paint fill = new Paint();
  481 + fill.setColor(Color.BLACK);
  482 + canvas.drawRect(0, 0, width, height, fill);
  483 + }
474 484
475 int topOffset = "TEXT_PRICE".equals(type) 485 int topOffset = "TEXT_PRICE".equals(type)
476 ? Math.max(verticalPadding, (height - totalHeight) / 2) 486 ? Math.max(verticalPadding, (height - totalHeight) / 2)
美国版/Food Labeling Management App UniApp/src/pages/labels/labels.vue
@@ -952,7 +952,7 @@ const goBluetoothPage = () => { @@ -952,7 +952,7 @@ const goBluetoothPage = () => {
952 952
953 .cat-icon-text--wrap, 953 .cat-icon-text--wrap,
954 .cat-icon-text--on-color { 954 .cat-icon-text--on-color {
955 - white-space: normal; 955 + white-space: pre-wrap;
956 word-break: break-word; 956 word-break: break-word;
957 overflow: hidden; 957 overflow: hidden;
958 } 958 }
@@ -1085,7 +1085,7 @@ const goBluetoothPage = () => { @@ -1085,7 +1085,7 @@ const goBluetoothPage = () => {
1085 padding: 0; 1085 padding: 0;
1086 max-width: 100%; 1086 max-width: 100%;
1087 max-height: 100%; 1087 max-height: 100%;
1088 - white-space: normal; 1088 + white-space: pre-wrap;
1089 word-break: break-word; 1089 word-break: break-word;
1090 overflow: hidden; 1090 overflow: hidden;
1091 } 1091 }
@@ -1238,14 +1238,14 @@ const goBluetoothPage = () => { @@ -1238,14 +1238,14 @@ const goBluetoothPage = () => {
1238 color: #111827; 1238 color: #111827;
1239 line-height: 1.25; 1239 line-height: 1.25;
1240 text-align: center; 1240 text-align: center;
1241 - white-space: normal; 1241 + white-space: pre-wrap;
1242 word-break: break-word; 1242 word-break: break-word;
1243 overflow: hidden; 1243 overflow: hidden;
1244 box-sizing: border-box; 1244 box-sizing: border-box;
1245 } 1245 }
1246 1246
1247 .food-thumb-text--wrap { 1247 .food-thumb-text--wrap {
1248 - white-space: normal; 1248 + white-space: pre-wrap;
1249 word-break: break-word; 1249 word-break: break-word;
1250 } 1250 }
1251 1251
美国版/Food Labeling Management App UniApp/src/pages/labels/preview.vue
@@ -299,6 +299,7 @@ import { @@ -299,6 +299,7 @@ import {
299 templateIncludesEmployeeElement, 299 templateIncludesEmployeeElement,
300 } from '../../utils/labelPreview/employeeElement' 300 } from '../../utils/labelPreview/employeeElement'
301 import { applyLiveDateTimeFieldsToTemplate } from '../../utils/labelPreview/printInputOffset' 301 import { applyLiveDateTimeFieldsToTemplate } from '../../utils/labelPreview/printInputOffset'
  302 +import { readInvertColors } from '../../utils/invertColorsConfig'
302 import { 303 import {
303 getLabelPrintRasterLayout, 304 getLabelPrintRasterLayout,
304 getPreviewCanvasCssSize, 305 getPreviewCanvasCssSize,
@@ -672,6 +673,9 @@ function applyNativeTemplateStyleScale( @@ -672,6 +673,9 @@ function applyNativeTemplateStyleScale(
672 // 时间/日期类右对齐文本在部分机型原生 TEXT 宽度估算有误差,强制位图可避免截断。 673 // 时间/日期类右对齐文本在部分机型原生 TEXT 宽度估算有误差,强制位图可避免截断。
673 cfg.forceRasterText = true 674 cfg.forceRasterText = true
674 } 675 }
  676 + if (readInvertColors(cfg)) {
  677 + cfg.forceRasterText = true
  678 + }
675 } 679 }
676 if (type === 'BARCODE') { 680 if (type === 'BARCODE') {
677 const raw = String( 681 const raw = String(
@@ -824,6 +828,8 @@ const systemTemplate = ref<SystemLabelTemplate | null>(null) @@ -824,6 +828,8 @@ const systemTemplate = ref<SystemLabelTemplate | null>(null)
824 const basePreviewTemplate = ref<SystemLabelTemplate | null>(null) 828 const basePreviewTemplate = ref<SystemLabelTemplate | null>(null)
825 /** 接口返回的 templateProductDefaults,每次合并/重绘按当前时刻重算日期时间 */ 829 /** 接口返回的 templateProductDefaults,每次合并/重绘按当前时刻重算日期时间 */
826 const previewProductDefaults = ref<Record<string, string>>({}) 830 const previewProductDefaults = ref<Record<string, string>>({})
  831 +/** 最近一次 8.2 原始响应,便于 defaults 解析失败时重试 */
  832 +const lastPreviewRawPayload = ref<unknown>(null)
827 const printOptionSelections = ref<Record<string, string[]>>({}) 833 const printOptionSelections = ref<Record<string, string[]>>({})
828 const dictLabelsByElementId = ref<Record<string, string>>({}) 834 const dictLabelsByElementId = ref<Record<string, string>>({})
829 const dictValuesByElementId = ref<Record<string, string[]>>({}) 835 const dictValuesByElementId = ref<Record<string, string[]>>({})
@@ -1166,7 +1172,14 @@ function computeMergedPreviewTemplate(at?: Date): SystemLabelTemplate | null { @@ -1166,7 +1172,14 @@ function computeMergedPreviewTemplate(at?: Date): SystemLabelTemplate | null {
1166 let base = basePreviewTemplate.value 1172 let base = basePreviewTemplate.value
1167 if (!base) return null 1173 if (!base) return null
1168 const baseTime = at ?? new Date() 1174 const baseTime = at ?? new Date()
1169 - const defaults = previewProductDefaults.value 1175 + let defaults = previewProductDefaults.value
  1176 + if (Object.keys(defaults).length === 0 && lastPreviewRawPayload.value != null) {
  1177 + const recovered = extractTemplateProductDefaultValuesFromPreviewPayload(lastPreviewRawPayload.value)
  1178 + if (Object.keys(recovered).length > 0) {
  1179 + defaults = recovered
  1180 + previewProductDefaults.value = recovered
  1181 + }
  1182 + }
1170 if (Object.keys(defaults).length > 0) { 1183 if (Object.keys(defaults).length > 0) {
1171 base = applyTemplateProductDefaultValuesToTemplate(base, defaults, baseTime) 1184 base = applyTemplateProductDefaultValuesToTemplate(base, defaults, baseTime)
1172 } 1185 }
@@ -1332,6 +1345,7 @@ async function loadPreview() { @@ -1332,6 +1345,7 @@ async function loadPreview() {
1332 systemTemplate.value = null 1345 systemTemplate.value = null
1333 basePreviewTemplate.value = null 1346 basePreviewTemplate.value = null
1334 previewProductDefaults.value = {} 1347 previewProductDefaults.value = {}
  1348 + lastPreviewRawPayload.value = null
1335 printOptionSelections.value = {} 1349 printOptionSelections.value = {}
1336 printFreeFieldValues.value = {} 1350 printFreeFieldValues.value = {}
1337 dictLabelsByElementId.value = {} 1351 dictLabelsByElementId.value = {}
@@ -1344,6 +1358,7 @@ async function loadPreview() { @@ -1344,6 +1358,7 @@ async function loadPreview() {
1344 productId: productId.value || undefined, 1358 productId: productId.value || undefined,
1345 baseTime: new Date().toISOString(), 1359 baseTime: new Date().toISOString(),
1346 }) 1360 })
  1361 + lastPreviewRawPayload.value = raw
1347 const root = raw as Record<string, unknown> 1362 const root = raw as Record<string, unknown>
1348 const nested = root.data ?? root.Data 1363 const nested = root.data ?? root.Data
1349 const inner = 1364 const inner =
@@ -1410,19 +1425,13 @@ async function loadPreview() { @@ -1410,19 +1425,13 @@ async function loadPreview() {
1410 } 1425 }
1411 const productDefaults = extractTemplateProductDefaultValuesFromPreviewPayload(raw) 1426 const productDefaults = extractTemplateProductDefaultValuesFromPreviewPayload(raw)
1412 previewProductDefaults.value = productDefaults 1427 previewProductDefaults.value = productDefaults
1413 - let tmplWithDefaults =  
1414 - Object.keys(productDefaults).length > 0  
1415 - ? applyTemplateProductDefaultValuesToTemplate(tmplRaw, productDefaults)  
1416 - : tmplRaw 1428 + let tmplBase = tmplRaw
1417 const productCodeValue = extractProductCodeValueFromPreviewPayload(raw) 1429 const productCodeValue = extractProductCodeValueFromPreviewPayload(raw)
1418 if (productCodeValue) { 1430 if (productCodeValue) {
1419 - tmplWithDefaults = applyProductCodeValueToTemplateScanElements(  
1420 - tmplWithDefaults,  
1421 - productCodeValue,  
1422 - ) 1431 + tmplBase = applyProductCodeValueToTemplateScanElements(tmplBase, productCodeValue)
1423 } 1432 }
1424 - /** 画布像素仅按接口 template 的 width / height / unit 换算(与 renderLabelPreviewCanvas.toCanvasPx 一致),不用 labelSizeText 覆盖以免单位被误判 */  
1425 - const base = overlayProductNameOnPreviewTemplate(tmplWithDefaults, displayProductName.value) 1433 + /** 仅存原始模板 + 默认值;日期/时间在 computeMergedPreviewTemplate 内按同一 baseTime 重算一次 */
  1434 + const base = overlayProductNameOnPreviewTemplate(tmplBase, displayProductName.value)
1426 basePreviewTemplate.value = base 1435 basePreviewTemplate.value = base
1427 printOptionSelections.value = readSelectionsFromTemplate(base) 1436 printOptionSelections.value = readSelectionsFromTemplate(base)
1428 printFreeFieldValues.value = ensureFreeFieldKeys(base, readFreeFieldValuesFromTemplate(base)) 1437 printFreeFieldValues.value = ensureFreeFieldKeys(base, readFreeFieldValuesFromTemplate(base))
@@ -1626,7 +1635,7 @@ const handlePrint = async () =&gt; { @@ -1626,7 +1635,7 @@ const handlePrint = async () =&gt; {
1626 * Virtual BT 不做 xScale/安全区收窄(易导致营养表错位、条码丢失);光栅仅作回退。 1635 * Virtual BT 不做 xScale/安全区收窄(易导致营养表错位、条码丢失);光栅仅作回退。
1627 * 普通蓝牙(BLE 或 classic+JS socket):canPrintCurrentLabelViaNativeFastJob 为 false,走下方光栅/直发 TSC。 1636 * 普通蓝牙(BLE 或 classic+JS socket):canPrintCurrentLabelViaNativeFastJob 为 false,走下方光栅/直发 TSC。
1628 */ 1637 */
1629 - const tmplForNativeJob = normalizeTemplateForNativeFastJob(tmpl, printInputJson as any) 1638 + const tmplForNativeJob = normalizeTemplateForNativeFastJob(tmpl, printInputJson as any, printBaseTime)
1630 const currentDriver = getCurrentPrinterDriver() 1639 const currentDriver = getCurrentPrinterDriver()
1631 const currentConn = getBluetoothConnection() 1640 const currentConn = getBluetoothConnection()
1632 const isD320faxClassicNative = 1641 const isD320faxClassicNative =
美国版/Food Labeling Management App UniApp/src/services/usAppLabeling.ts
@@ -128,13 +128,12 @@ export async function fetchUsAppLabelingTree(input: { @@ -128,13 +128,12 @@ export async function fetchUsAppLabelingTree(input: {
128 }) 128 })
129 } 129 }
130 130
  131 +/** 离线缓存键:仅 location + labelCode + productId(不含 baseTime,避免同步预拉与预览页 key 不一致) */
131 export function buildLabelPreviewCacheKey(body: UsAppLabelPreviewInputVo): string { 132 export function buildLabelPreviewCacheKey(body: UsAppLabelPreviewInputVo): string {
132 const loc = String(body.locationId || '').trim() 133 const loc = String(body.locationId || '').trim()
133 const code = String(body.labelCode || '').trim() 134 const code = String(body.labelCode || '').trim()
134 const pid = String(body.productId || '').trim() 135 const pid = String(body.productId || '').trim()
135 - const bt = String(body.baseTime || '').trim()  
136 - const minuteBucket = bt ? bt.slice(0, 16) : ''  
137 - return `preview:${loc}:${code}:${pid}:${minuteBucket}` 136 + return `preview:${loc}:${code}:${pid}`
138 } 137 }
139 138
140 /** 接口 8.2(在线写入 SQLite,离线读缓存) */ 139 /** 接口 8.2(在线写入 SQLite,离线读缓存) */
美国版/Food Labeling Management App UniApp/src/utils/labelPreview/normalizePreviewTemplate.ts
1 import type { SystemLabelTemplate, SystemTemplateElementBase } from '../print/types/printer' 1 import type { SystemLabelTemplate, SystemTemplateElementBase } from '../print/types/printer'
2 import { 2 import {
3 applyLiveDateTimeFieldsToTemplate, 3 applyLiveDateTimeFieldsToTemplate,
  4 + isUniAppDateTimeOffsetField,
4 resolveTemplateDefaultValueForElement, 5 resolveTemplateDefaultValueForElement,
  6 + tryParsePrintInputOffsetStored,
5 } from './printInputOffset' 7 } from './printInputOffset'
6 import { applyNutritionDefaultJsonToConfig } from './nutritionDefaultsMerge' 8 import { applyNutritionDefaultJsonToConfig } from './nutritionDefaultsMerge'
7 9
@@ -168,6 +170,41 @@ export function applyLabelSizeTextToTemplate( @@ -168,6 +170,41 @@ export function applyLabelSizeTextToTemplate(
168 } 170 }
169 171
170 /** 172 /**
  173 + * 将接口/缓存中的默认值统一为字符串(兼容 object、双重 JSON 编码、PascalCase unit/value)。
  174 + */
  175 +export function coerceTemplateProductDefaultValueToString(v: unknown): string {
  176 + if (v == null) return ''
  177 + if (typeof v === 'string') {
  178 + const t = v.trim()
  179 + if (!t) return ''
  180 + if (t.startsWith('{') || t.startsWith('[')) return t
  181 + if (t.startsWith('"') && t.endsWith('"')) {
  182 + try {
  183 + const inner = JSON.parse(t) as unknown
  184 + if (typeof inner === 'string') return inner.trim()
  185 + } catch {
  186 + /* ignore */
  187 + }
  188 + }
  189 + return t
  190 + }
  191 + if (typeof v === 'number' || typeof v === 'boolean') return String(v)
  192 + if (typeof v === 'object' && !Array.isArray(v)) {
  193 + const rec = v as Record<string, unknown>
  194 + const unit = rec.unit ?? rec.Unit
  195 + const value = rec.value ?? rec.Value
  196 + if (unit != null && value != null) {
  197 + return JSON.stringify({ unit: String(unit), value: String(value) })
  198 + }
  199 + }
  200 + try {
  201 + return JSON.stringify(v)
  202 + } catch {
  203 + return String(v)
  204 + }
  205 +}
  206 +
  207 +/**
171 * 从预览接口响应中取出 `templateProductDefaultValues`(elementId → 字符串,兼容 PascalCase / 嵌套 data)。 208 * 从预览接口响应中取出 `templateProductDefaultValues`(elementId → 字符串,兼容 PascalCase / 嵌套 data)。
172 */ 209 */
173 export function extractTemplateProductDefaultValuesFromPreviewPayload(payload: unknown): Record<string, string> { 210 export function extractTemplateProductDefaultValuesFromPreviewPayload(payload: unknown): Record<string, string> {
@@ -180,10 +217,7 @@ export function extractTemplateProductDefaultValuesFromPreviewPayload(payload: u @@ -180,10 +217,7 @@ export function extractTemplateProductDefaultValuesFromPreviewPayload(payload: u
180 for (const [k, v] of Object.entries(raw as Record<string, unknown>)) { 217 for (const [k, v] of Object.entries(raw as Record<string, unknown>)) {
181 const key = String(k).trim() 218 const key = String(k).trim()
182 if (!key) continue 219 if (!key) continue
183 - if (v == null) out[key] = ''  
184 - else if (typeof v === 'string') out[key] = v  
185 - else if (typeof v === 'number' || typeof v === 'boolean') out[key] = String(v)  
186 - else out[key] = JSON.stringify(v) 220 + out[key] = coerceTemplateProductDefaultValueToString(v)
187 } 221 }
188 return Object.keys(out).length ? out : null 222 return Object.keys(out).length ? out : null
189 } 223 }
@@ -236,9 +270,32 @@ export function extractTemplateProductDefaultValuesFromPreviewPayload(payload: u @@ -236,9 +270,32 @@ export function extractTemplateProductDefaultValuesFromPreviewPayload(payload: u
236 inner ? tryLayer(inner) : null, 270 inner ? tryLayer(inner) : null,
237 tryLayer(r), 271 tryLayer(r),
238 tryLayer(payload), 272 tryLayer(payload),
  273 + tryExtractFromTemplateProductDefaultsArray(r),
  274 + inner ? tryExtractFromTemplateProductDefaultsArray(inner) : null,
239 ) 275 )
240 } 276 }
241 277
  278 +/** Web 模板 DTO 的 templateProductDefaults 数组 → 扁平 elementId → value */
  279 +function tryExtractFromTemplateProductDefaultsArray(layer: unknown): Record<string, string> | null {
  280 + if (layer == null || typeof layer !== 'object' || Array.isArray(layer)) return null
  281 + const L = layer as Record<string, unknown>
  282 + const raw = L.templateProductDefaults ?? L.TemplateProductDefaults
  283 + if (!Array.isArray(raw)) return null
  284 + const out: Record<string, string> = {}
  285 + for (const row of raw) {
  286 + if (row == null || typeof row !== 'object' || Array.isArray(row)) continue
  287 + const rec = row as Record<string, unknown>
  288 + const dv = rec.defaultValues ?? rec.DefaultValues
  289 + if (dv == null || typeof dv !== 'object' || Array.isArray(dv)) continue
  290 + for (const [k, v] of Object.entries(dv as Record<string, unknown>)) {
  291 + const key = String(k).trim()
  292 + if (!key) continue
  293 + out[key] = coerceTemplateProductDefaultValueToString(v)
  294 + }
  295 + }
  296 + return Object.keys(out).length ? out : null
  297 +}
  298 +
242 function isTemplateSectionScanElement(el: SystemTemplateElementBase): boolean { 299 function isTemplateSectionScanElement(el: SystemTemplateElementBase): boolean {
243 const cfg = el.config || {} 300 const cfg = el.config || {}
244 const typeAdd = String( 301 const typeAdd = String(
@@ -313,6 +370,30 @@ export function applyProductCodeValueToTemplateScanElements( @@ -313,6 +370,30 @@ export function applyProductCodeValueToTemplateScanElements(
313 } 370 }
314 371
315 /** 372 /**
  373 + * 从 templateProductDefaultValues 按 id / inputKey / elementName 匹配(大小写不敏感兜底)。
  374 + */
  375 +export function lookupTemplateProductDefaultValue(
  376 + el: SystemTemplateElementBase,
  377 + defaults: Record<string, string>,
  378 +): string | undefined {
  379 + const candidates = [
  380 + String(el.id ?? '').trim(),
  381 + String(el.inputKey ?? '').trim(),
  382 + String(el.elementName ?? '').trim(),
  383 + ].filter(Boolean)
  384 + for (const k of candidates) {
  385 + if (Object.prototype.hasOwnProperty.call(defaults, k)) return defaults[k]
  386 + }
  387 + const lowerEntries = Object.entries(defaults).map(([k, v]) => [k.toLowerCase(), v] as const)
  388 + for (const k of candidates) {
  389 + const lk = k.toLowerCase()
  390 + const hit = lowerEntries.find(([dk]) => dk === lk)
  391 + if (hit) return hit[1]
  392 + }
  393 + return undefined
  394 +}
  395 +
  396 +/**
316 * 将平台录入的默认值合并进模板元素 config,供画布预览(键与 elements[].id 一致)。 397 * 将平台录入的默认值合并进模板元素 config,供画布预览(键与 elements[].id 一致)。
317 */ 398 */
318 export function applyTemplateProductDefaultValuesToTemplate( 399 export function applyTemplateProductDefaultValuesToTemplate(
@@ -323,9 +404,7 @@ export function applyTemplateProductDefaultValuesToTemplate( @@ -323,9 +404,7 @@ export function applyTemplateProductDefaultValuesToTemplate(
323 const keys = Object.keys(defaults) 404 const keys = Object.keys(defaults)
324 if (!keys.length) return template 405 if (!keys.length) return template
325 const elements = (template.elements || []).map((el) => { 406 const elements = (template.elements || []).map((el) => {
326 - const byName = (el.elementName ?? '').trim()  
327 - const v =  
328 - defaults[el.id] ?? (byName ? defaults[byName] : undefined) 407 + const v = lookupTemplateProductDefaultValue(el, defaults)
329 if (v === undefined) return el 408 if (v === undefined) return el
330 const type = String(el.type || '').toUpperCase() 409 const type = String(el.type || '').toUpperCase()
331 const cfg = { ...(el.config || {}) } as Record<string, any> 410 const cfg = { ...(el.config || {}) } as Record<string, any>
@@ -359,9 +438,15 @@ export function applyTemplateProductDefaultValuesToTemplate( @@ -359,9 +438,15 @@ export function applyTemplateProductDefaultValuesToTemplate(
359 } 438 }
360 439
361 if (type === 'DATE' || type === 'TIME' || type === 'DURATION') { 440 if (type === 'DATE' || type === 'TIME' || type === 'DURATION') {
362 - const text = resolveTemplateDefaultValueForElement(el, v, base)  
363 - cfg.text = text  
364 - cfg.Text = text 441 + /** 偏移类字段保留 JSON(如 {"unit":"Days","value":"2"}),供 applyLiveDateTimeFieldsToTemplate 按出纸时刻重算 */
  442 + if (isUniAppDateTimeOffsetField(el) && tryParsePrintInputOffsetStored(v)) {
  443 + cfg.text = v
  444 + cfg.Text = v
  445 + } else {
  446 + const text = resolveTemplateDefaultValueForElement(el, v, base)
  447 + cfg.text = text
  448 + cfg.Text = text
  449 + }
365 return { ...el, config: cfg } 450 return { ...el, config: cfg }
366 } 451 }
367 452
美国版/Food Labeling Management App UniApp/src/utils/labelPreview/printInputOffset.ts
@@ -224,13 +224,46 @@ export function resolveElementDateTimeDisplay ( @@ -224,13 +224,46 @@ export function resolveElementDateTimeDisplay (
224 type === 'TIME' 224 type === 'TIME'
225 ) { 225 ) {
226 const off = readOffsetFromElementConfig(el) 226 const off = readOffsetFromElementConfig(el)
227 - return resolveFromOffsetAmount(el, off?.amount ?? 0, off?.unit ?? 'Days', base) 227 + if (off) return resolveFromOffsetAmount(el, off.amount, off.unit, base)
  228 + /** 默认值已展开为字面日期时仍保留,避免丢失 durationdate 等 +N Days */
  229 + if (
  230 + rawText &&
  231 + !tryParsePrintInputOffsetStored(rawText) &&
  232 + isLikelyResolvedDateTimeLiteral(rawText)
  233 + ) {
  234 + return applyElementPrefix(cfg, rawText)
  235 + }
  236 + return resolveFromOffsetAmount(el, 0, 'Days', base)
228 } 237 }
229 return null 238 return null
230 } 239 }
231 240
  241 + /** FIXED 的 durationdate/durationtime 等:config.text 可能已是平台录入的偏移 JSON */
  242 + if (vst === 'FIXED' && isUniAppDateTimeOffsetField(el)) {
  243 + const off = readOffsetFromElementConfig(el)
  244 + if (off && (off.amount !== 0 || rawText)) {
  245 + return resolveFromOffsetAmount(el, off.amount, off.unit, base)
  246 + }
  247 + if (
  248 + rawText &&
  249 + !tryParsePrintInputOffsetStored(rawText) &&
  250 + isLikelyResolvedDateTimeLiteral(rawText)
  251 + ) {
  252 + return applyElementPrefix(cfg, rawText)
  253 + }
  254 + return resolveFromOffsetAmount(el, 0, 'Days', base)
  255 + }
  256 +
232 const off = readOffsetFromElementConfig(el) 257 const off = readOffsetFromElementConfig(el)
233 if (off) return resolveFromOffsetAmount(el, off.amount, off.unit, base) 258 if (off) return resolveFromOffsetAmount(el, off.amount, off.unit, base)
  259 + if (
  260 + rawText &&
  261 + !tryParsePrintInputOffsetStored(rawText) &&
  262 + isLikelyResolvedDateTimeLiteral(rawText) &&
  263 + isUniAppDateTimeOffsetField(el)
  264 + ) {
  265 + return applyElementPrefix(cfg, rawText)
  266 + }
234 return resolveFromOffsetAmount(el, 0, 'Days', base) 267 return resolveFromOffsetAmount(el, 0, 'Days', base)
235 } 268 }
236 269
美国版/Food Labeling Management App UniApp/src/utils/labelPreview/renderLabelPreviewCanvas.ts
1 import type { RawImageDataSource, SystemLabelTemplate, SystemTemplateElementBase } from '../print/types/printer' 1 import type { RawImageDataSource, SystemLabelTemplate, SystemTemplateElementBase } from '../print/types/printer'
2 import { resolveMediaUrlForApp, storedValueLooksLikeImagePath } from '../resolveMediaUrl' 2 import { resolveMediaUrlForApp, storedValueLooksLikeImagePath } from '../resolveMediaUrl'
3 import { sortElementsForPreview, normalizeTemplatePrintOrientation } from './normalizePreviewTemplate' 3 import { sortElementsForPreview, normalizeTemplatePrintOrientation } from './normalizePreviewTemplate'
4 -import { resolveElementDateTimeDisplay } from './printInputOffset' 4 +import { resolveElementDateTimeDisplay, isLikelyResolvedDateTimeLiteral } from './printInputOffset'
5 import { getLoggedInEmployeeDisplayName, isEmployeeTemplateElement } from './employeeElement' 5 import { getLoggedInEmployeeDisplayName, isEmployeeTemplateElement } from './employeeElement'
6 import QRCode from 'qrcode' 6 import QRCode from 'qrcode'
7 import { readInvertColors } from '../invertColorsConfig' 7 import { readInvertColors } from '../invertColorsConfig'
@@ -134,6 +134,11 @@ function previewTextForElement(element: SystemTemplateElementBase, baseTime: Dat @@ -134,6 +134,11 @@ function previewTextForElement(element: SystemTemplateElementBase, baseTime: Dat
134 return applyConfigPrefix(config, getLoggedInEmployeeDisplayName()) 134 return applyConfigPrefix(config, getLoggedInEmployeeDisplayName())
135 } 135 }
136 if (type === 'DATE' || type === 'TIME' || type === 'DURATION') { 136 if (type === 'DATE' || type === 'TIME' || type === 'DURATION') {
  137 + /** applyLiveDateTimeFieldsToTemplate 已写入 config.text 时直接展示,避免二次 resolve 把 +N Days 重置为当天 */
  138 + const baked = cfgStr(config, ['text', 'Text'], '').trim()
  139 + if (baked && isLikelyResolvedDateTimeLiteral(baked)) {
  140 + return applyConfigPrefix(config, baked)
  141 + }
137 const live = resolveElementDateTimeDisplay(element, baseTime) 142 const live = resolveElementDateTimeDisplay(element, baseTime)
138 if (live != null && live.trim()) return live 143 if (live != null && live.trim()) return live
139 } 144 }
@@ -661,10 +666,15 @@ function runLabelPreviewCanvasDraw( @@ -661,10 +666,15 @@ function runLabelPreviewCanvasDraw(
661 else if (align === 'right') tx = x + w - pad 666 else if (align === 'right') tx = x + w - pad
662 ctx.setTextAlign(align === 'center' ? 'center' : align === 'right' ? 'right' : 'left') 667 ctx.setTextAlign(align === 'center' ? 'center' : align === 'right' ? 'right' : 'left')
663 const lineHeight = fontSize + Math.max(2, Math.round(fontSize * 0.15)) 668 const lineHeight = fontSize + Math.max(2, Math.round(fontSize * 0.15))
664 - const maxChars = maxCharsPerLine(innerW, fontSize)  
665 - const lines = wrapTextToWidth(text, maxChars)  
666 - const maxLines =  
667 - innerH >= fontSize ? Math.max(1, Math.floor(innerH / lineHeight)) : lines.length 669 + const isDateTimeType = type === 'DATE' || type === 'TIME' || type === 'DURATION'
  670 + const lines = isDateTimeType
  671 + ? [String(text).replace(/\s+/g, ' ').trim()]
  672 + : wrapTextToWidth(text, maxCharsPerLine(innerW, fontSize))
  673 + const maxLines = isDateTimeType
  674 + ? 1
  675 + : innerH >= fontSize
  676 + ? Math.max(1, Math.floor(innerH / lineHeight))
  677 + : lines.length
668 const startY = y + pad + fontSize 678 const startY = y + pad + fontSize
669 lines.slice(0, maxLines).forEach((ln, li) => { 679 lines.slice(0, maxLines).forEach((ln, li) => {
670 ctx.fillText(ln, tx, startY + li * lineHeight) 680 ctx.fillText(ln, tx, startY + li * lineHeight)
美国版/Food Labeling Management App UniApp/src/utils/print/nativeTemplateElementSupport.ts
@@ -10,6 +10,7 @@ import type { @@ -10,6 +10,7 @@ import type {
10 import { formatBarcodeValueForTsc, normalizeBarcodeType } from '../barcodeFormat' 10 import { formatBarcodeValueForTsc, normalizeBarcodeType } from '../barcodeFormat'
11 import { applyTemplateData } from './templateRenderer' 11 import { applyTemplateData } from './templateRenderer'
12 import { resolveElementDateTimeDisplay } from '../labelPreview/printInputOffset' 12 import { resolveElementDateTimeDisplay } from '../labelPreview/printInputOffset'
  13 +import { readInvertColors } from '../invertColorsConfig'
13 14
14 function isElementHandledByNativeFastPrinter (el: SystemTemplateElementBase): boolean { 15 function isElementHandledByNativeFastPrinter (el: SystemTemplateElementBase): boolean {
15 const type = String(el.type || '').toUpperCase() 16 const type = String(el.type || '').toUpperCase()
@@ -95,9 +96,10 @@ function prepareBarcodeElementForNativePrint (el: SystemTemplateElementBase): Sy @@ -95,9 +96,10 @@ function prepareBarcodeElementForNativePrint (el: SystemTemplateElementBase): Sy
95 */ 96 */
96 export function normalizeTemplateForNativeFastJob ( 97 export function normalizeTemplateForNativeFastJob (
97 template: SystemLabelTemplate, 98 template: SystemLabelTemplate,
98 - data: LabelTemplateData 99 + data: LabelTemplateData,
  100 + baseTime: Date = new Date(),
99 ): SystemLabelTemplate { 101 ): SystemLabelTemplate {
100 - const now = new Date() 102 + const now = baseTime
101 103
102 const extras: SystemTemplateElementBase[] = [] 104 const extras: SystemTemplateElementBase[] = []
103 const elements = (template.elements || []).map((el) => { 105 const elements = (template.elements || []).map((el) => {
@@ -125,10 +127,14 @@ export function normalizeTemplateForNativeFastJob ( @@ -125,10 +127,14 @@ export function normalizeTemplateForNativeFastJob (
125 if (v && u && !v.endsWith(u)) text = `${v}${u}` 127 if (v && u && !v.endsWith(u)) text = `${v}${u}`
126 else text = v || u 128 else text = v || u
127 } 129 }
  130 + const nextConfig: Record<string, unknown> = { ...config, text, nativeSourceType: type }
  131 + if (readInvertColors(config)) {
  132 + nextConfig.forceRasterText = true
  133 + }
128 return { 134 return {
129 ...el, 135 ...el,
130 type: 'TEXT_STATIC' as typeof el.type, 136 type: 'TEXT_STATIC' as typeof el.type,
131 - config: { ...config, text, nativeSourceType: type }, 137 + config: nextConfig,
132 } 138 }
133 } 139 }
134 if (type === 'NUTRITION') { 140 if (type === 'NUTRITION') {
@@ -164,7 +170,14 @@ export function normalizeTemplateForNativeFastJob ( @@ -164,7 +170,14 @@ export function normalizeTemplateForNativeFastJob (
164 } 170 }
165 return el 171 return el
166 }) 172 })
167 - const merged = resolveNativeLayoutCollisions([...elements, ...extras]) 173 + const withInvertRaster = elements.map((el) => {
  174 + const cfg = { ...(el.config || {}) } as Record<string, unknown>
  175 + if (readInvertColors(cfg)) {
  176 + cfg.forceRasterText = true
  177 + }
  178 + return { ...el, config: cfg }
  179 + })
  180 + const merged = resolveNativeLayoutCollisions([...withInvertRaster, ...extras])
168 return { ...template, elements: merged } 181 return { ...template, elements: merged }
169 } 182 }
170 183
美国版/Food Labeling Management App UniApp/src/utils/sqliteSync.ts
@@ -207,6 +207,28 @@ export async function hasOfflineCache(module: string, name: string): Promise&lt;boo @@ -207,6 +207,28 @@ export async function hasOfflineCache(module: string, name: string): Promise&lt;boo
207 return rows.length > 0 207 return rows.length > 0
208 } 208 }
209 209
  210 +/** 离线读缓存:匹配以 prefix 开头的 cache_name(兼容旧版 preview 键带 baseTime 后缀) */
  211 +async function findOfflineCacheKeyByPrefix(module: string, namePrefix: string): Promise<string | null> {
  212 + if (!namePrefix) return null
  213 + if (!isAppSqliteAvailable()) {
  214 + const map = getStorageMap<{ module: string; name: string }>(FALLBACK_CACHE_KEY)
  215 + for (const row of Object.values(map)) {
  216 + const m = String((row as { module?: string }).module || '')
  217 + const n = String((row as { name?: string }).name || '')
  218 + if (m === module && n.startsWith(namePrefix)) return n
  219 + }
  220 + return null
  221 + }
  222 + await initOfflineSqlite()
  223 + const rows = await sqliteSelect(
  224 + `SELECT cache_name FROM ${CACHE_TABLE}
  225 + WHERE module_name='${esc(module)}' AND cache_name LIKE '${esc(namePrefix)}%'
  226 + ORDER BY updated_at DESC LIMIT 1`
  227 + )
  228 + const hit = rows?.[0]?.cache_name
  229 + return hit != null && String(hit).trim() ? String(hit) : null
  230 +}
  231 +
210 export async function getOfflineCache<T = unknown>(module: string, name: string): Promise<T | null> { 232 export async function getOfflineCache<T = unknown>(module: string, name: string): Promise<T | null> {
211 const key = cachePk(module, name) 233 const key = cachePk(module, name)
212 if (!isAppSqliteAvailable()) { 234 if (!isAppSqliteAvailable()) {
@@ -399,6 +421,14 @@ export async function fetchWithOfflineCache&lt;T&gt;( @@ -399,6 +421,14 @@ export async function fetchWithOfflineCache&lt;T&gt;(
399 if (await hasOfflineCache(module, name)) { 421 if (await hasOfflineCache(module, name)) {
400 return (await getOfflineCache<T>(module, name)) as T 422 return (await getOfflineCache<T>(module, name)) as T
401 } 423 }
  424 + /** 兼容旧版 preview 缓存键(含 baseTime 分钟桶) */
  425 + if (name.startsWith('preview:')) {
  426 + const legacyPrefix = `${name}:`
  427 + const legacyHit = await findOfflineCacheKeyByPrefix(module, legacyPrefix)
  428 + if (legacyHit) {
  429 + return (await getOfflineCache<T>(module, legacyHit)) as T
  430 + }
  431 + }
402 throw new Error('No offline cache available') 432 throw new Error('No offline cache available')
403 } 433 }
404 434
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs
@@ -463,6 +463,11 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -463,6 +463,11 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
463 } 463 }
464 } 464 }
465 465
  466 + if (templateProductDefaultValues is { Count: > 0 })
  467 + {
  468 + ApplyTemplateProductDefaultValuesToPreviewElements(template.Elements, templateProductDefaultValues);
  469 + }
  470 +
466 return new UsAppLabelPreviewDto 471 return new UsAppLabelPreviewDto
467 { 472 {
468 LabelId = labelRow.Id, 473 LabelId = labelRow.Id,
@@ -1177,6 +1182,177 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -1177,6 +1182,177 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
1177 return Math.Round((current - previous) * 100m / previous, 2); 1182 return Math.Round((current - previous) * 100m / previous, 2);
1178 } 1183 }
1179 1184
  1185 + /// <summary>
  1186 + /// 将 fl_label_template_product_default 默认值写入预览模板元素 config(日期偏移保留 JSON,由客户端按 BaseTime 展开)。
  1187 + /// </summary>
  1188 + private static void ApplyTemplateProductDefaultValuesToPreviewElements(
  1189 + IList<LabelTemplateElementDto>? elements,
  1190 + Dictionary<string, object?> defaults)
  1191 + {
  1192 + if (elements == null || elements.Count == 0 || defaults == null || defaults.Count == 0)
  1193 + {
  1194 + return;
  1195 + }
  1196 +
  1197 + foreach (var el in elements)
  1198 + {
  1199 + var stored = ResolveTemplateProductDefaultValueForElement(el, defaults);
  1200 + if (string.IsNullOrWhiteSpace(stored))
  1201 + {
  1202 + continue;
  1203 + }
  1204 +
  1205 + var cfg = ParsePreviewElementConfig(el.ConfigJson);
  1206 + var type = (el.ElementType ?? string.Empty).Trim().ToUpperInvariant();
  1207 +
  1208 + switch (type)
  1209 + {
  1210 + case "DATE":
  1211 + case "TIME":
  1212 + case "DURATION":
  1213 + cfg["text"] = stored;
  1214 + cfg["Text"] = stored;
  1215 + break;
  1216 + case "IMAGE":
  1217 + case "LOGO":
  1218 + cfg["src"] = stored;
  1219 + cfg["Src"] = stored;
  1220 + cfg["url"] = stored;
  1221 + cfg["Url"] = stored;
  1222 + break;
  1223 + case "BARCODE":
  1224 + case "QRCODE":
  1225 + cfg["data"] = stored;
  1226 + cfg["Data"] = stored;
  1227 + break;
  1228 + default:
  1229 + cfg["text"] = stored;
  1230 + cfg["Text"] = stored;
  1231 + break;
  1232 + }
  1233 +
  1234 + el.ConfigJson = cfg;
  1235 + }
  1236 + }
  1237 +
  1238 + private static string? ResolveTemplateProductDefaultValueForElement(
  1239 + LabelTemplateElementDto el,
  1240 + Dictionary<string, object?> defaults)
  1241 + {
  1242 + var candidates = new[]
  1243 + {
  1244 + el.Id?.Trim(),
  1245 + el.InputKey?.Trim(),
  1246 + el.ElementName?.Trim(),
  1247 + }.Where(x => !string.IsNullOrWhiteSpace(x)).ToList();
  1248 +
  1249 + foreach (var key in candidates)
  1250 + {
  1251 + if (defaults.TryGetValue(key!, out var direct))
  1252 + {
  1253 + return TemplateProductDefaultValueToStoredString(direct);
  1254 + }
  1255 + }
  1256 +
  1257 + foreach (var kv in defaults)
  1258 + {
  1259 + foreach (var key in candidates)
  1260 + {
  1261 + if (string.Equals(kv.Key, key, StringComparison.OrdinalIgnoreCase))
  1262 + {
  1263 + return TemplateProductDefaultValueToStoredString(kv.Value);
  1264 + }
  1265 + }
  1266 + }
  1267 +
  1268 + return null;
  1269 + }
  1270 +
  1271 + private static string? TemplateProductDefaultValueToStoredString(object? value)
  1272 + {
  1273 + if (value is null)
  1274 + {
  1275 + return null;
  1276 + }
  1277 +
  1278 + if (value is string s)
  1279 + {
  1280 + return s;
  1281 + }
  1282 +
  1283 + if (value is JsonElement je)
  1284 + {
  1285 + if (je.ValueKind == JsonValueKind.String)
  1286 + {
  1287 + return je.GetString();
  1288 + }
  1289 +
  1290 + if (je.ValueKind is JsonValueKind.Object or JsonValueKind.Array)
  1291 + {
  1292 + return je.GetRawText();
  1293 + }
  1294 +
  1295 + return je.ToString();
  1296 + }
  1297 +
  1298 + if (value is bool or int or long or decimal or double or float)
  1299 + {
  1300 + return Convert.ToString(value, CultureInfo.InvariantCulture);
  1301 + }
  1302 +
  1303 + return JsonSerializer.Serialize(value);
  1304 + }
  1305 +
  1306 + private static Dictionary<string, object?> ParsePreviewElementConfig(object? configJson)
  1307 + {
  1308 + if (configJson is null)
  1309 + {
  1310 + return new Dictionary<string, object?>();
  1311 + }
  1312 +
  1313 + if (configJson is Dictionary<string, object?> dict)
  1314 + {
  1315 + return new Dictionary<string, object?>(dict);
  1316 + }
  1317 +
  1318 + if (configJson is JsonElement je && je.ValueKind == JsonValueKind.Object)
  1319 + {
  1320 + try
  1321 + {
  1322 + return JsonSerializer.Deserialize<Dictionary<string, object?>>(je.GetRawText())
  1323 + ?? new Dictionary<string, object?>();
  1324 + }
  1325 + catch
  1326 + {
  1327 + return new Dictionary<string, object?>();
  1328 + }
  1329 + }
  1330 +
  1331 + if (configJson is string raw && !string.IsNullOrWhiteSpace(raw))
  1332 + {
  1333 + try
  1334 + {
  1335 + return JsonSerializer.Deserialize<Dictionary<string, object?>>(raw)
  1336 + ?? new Dictionary<string, object?>();
  1337 + }
  1338 + catch
  1339 + {
  1340 + return new Dictionary<string, object?>();
  1341 + }
  1342 + }
  1343 +
  1344 + try
  1345 + {
  1346 + var json = JsonSerializer.Serialize(configJson);
  1347 + return JsonSerializer.Deserialize<Dictionary<string, object?>>(json)
  1348 + ?? new Dictionary<string, object?>();
  1349 + }
  1350 + catch
  1351 + {
  1352 + return new Dictionary<string, object?>();
  1353 + }
  1354 + }
  1355 +
1180 private async Task<string?> ResolveTemplateProductDefaultValuesJsonAsync( 1356 private async Task<string?> ResolveTemplateProductDefaultValuesJsonAsync(
1181 string templateId, 1357 string templateId,
1182 string? productId, 1358 string? productId,
美国版/Food Labeling Management Platform/.env.local
1 -VITE_API_BASE_URL=http://192.168.31.88:19001  
2 -# VITE_API_BASE_URL=http://flus-test.3ffoodsafety.com 1 +# VITE_API_BASE_URL=http://192.168.31.88:19001
  2 + VITE_API_BASE_URL=http://flus-test.3ffoodsafety.com
美国版/Food Labeling Management Platform/src/components/labels/LabelCategoriesView.tsx
@@ -8,6 +8,7 @@ import { @@ -8,6 +8,7 @@ import {
8 TableRow, 8 TableRow,
9 } from "../ui/table"; 9 } from "../ui/table";
10 import { Input } from "../ui/input"; 10 import { Input } from "../ui/input";
  11 +import { Textarea } from "../ui/textarea";
11 import { Button } from "../ui/button"; 12 import { Button } from "../ui/button";
12 import { 13 import {
13 Select, 14 Select,
@@ -1095,14 +1096,14 @@ function CreateLabelCategoryDialog({ @@ -1095,14 +1096,14 @@ function CreateLabelCategoryDialog({
1095 {apSel.text && !apSel.image ? ( 1096 {apSel.text && !apSel.image ? (
1096 <div className="space-y-2"> 1097 <div className="space-y-2">
1097 <Label>Display Text</Label> 1098 <Label>Display Text</Label>
1098 - <Input  
1099 - className="h-10" 1099 + <Textarea
  1100 + className="min-h-[4.5rem] resize-y"
1100 placeholder="Category Name" 1101 placeholder="Category Name"
1101 value={displayTextForPhoto} 1102 value={displayTextForPhoto}
1102 onChange={(e) => setDisplayTextForPhoto(e.target.value)} 1103 onChange={(e) => setDisplayTextForPhoto(e.target.value)}
1103 /> 1104 />
1104 <div className="text-xs text-gray-500"> 1105 <div className="text-xs text-gray-500">
1105 - Saved to <span className="font-mono">photo</span> as this text. 1106 + Press Enter for a new line. Multiple spaces collapse to one; use Enter to break lines.
1106 </div> 1107 </div>
1107 </div> 1108 </div>
1108 ) : null} 1109 ) : null}
@@ -1566,14 +1567,14 @@ function EditLabelCategoryDialog({ @@ -1566,14 +1567,14 @@ function EditLabelCategoryDialog({
1566 {apSel.text && !apSel.image ? ( 1567 {apSel.text && !apSel.image ? (
1567 <div className="space-y-2"> 1568 <div className="space-y-2">
1568 <Label>Display Text</Label> 1569 <Label>Display Text</Label>
1569 - <Input  
1570 - className="h-10" 1570 + <Textarea
  1571 + className="min-h-[4.5rem] resize-y"
1571 placeholder="Category Name" 1572 placeholder="Category Name"
1572 value={displayTextForPhoto} 1573 value={displayTextForPhoto}
1573 onChange={(e) => setDisplayTextForPhoto(e.target.value)} 1574 onChange={(e) => setDisplayTextForPhoto(e.target.value)}
1574 /> 1575 />
1575 <div className="text-xs text-gray-500"> 1576 <div className="text-xs text-gray-500">
1576 - Saved to <span className="font-mono">photo</span> as this text. 1577 + Press Enter for a new line. Multiple spaces collapse to one; use Enter to break lines.
1577 </div> 1578 </div>
1578 </div> 1579 </div>
1579 ) : null} 1580 ) : null}
美国版/Food Labeling Management Platform/src/components/labels/LabelsList.tsx
@@ -110,6 +110,7 @@ import { @@ -110,6 +110,7 @@ import {
110 regionNamesToGroupIds, 110 regionNamesToGroupIds,
111 regionOptionsForPartner, 111 regionOptionsForPartner,
112 scopePartnerValidationMessage, 112 scopePartnerValidationMessage,
  113 + buildSpecifiedLocationPayload,
113 } from "../../lib/categoryScopeForm"; 114 } from "../../lib/categoryScopeForm";
114 115
115 function toDisplay(v: string | null | undefined): string { 116 function toDisplay(v: string | null | undefined): string {
@@ -308,7 +309,12 @@ function labelDtoToUpdateForm(d: LabelDto): LabelUpdateInput { @@ -308,7 +309,12 @@ function labelDtoToUpdateForm(d: LabelDto): LabelUpdateInput {
308 return { 309 return {
309 labelName: d.labelName ?? "", 310 labelName: d.labelName ?? "",
310 templateCode: d.templateCode ?? "", 311 templateCode: d.templateCode ?? "",
311 - locationId: d.locationId ?? "", 312 + locationIds: (() => {
  313 + const fromList = (d.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean);
  314 + if (fromList.length) return [...new Set(fromList)];
  315 + const single = (d.locationId ?? "").trim();
  316 + return single ? [single] : [];
  317 + })(),
312 labelCategoryId: d.labelCategoryId ?? "", 318 labelCategoryId: d.labelCategoryId ?? "",
313 labelTypeId: d.labelTypeId ?? "", 319 labelTypeId: d.labelTypeId ?? "",
314 /** 编辑表单仅支持单商品:多商品时取第一个 */ 320 /** 编辑表单仅支持单商品:多商品时取第一个 */
@@ -1762,7 +1768,7 @@ function CreateLabelDialog({ @@ -1762,7 +1768,7 @@ function CreateLabelDialog({
1762 const [form, setForm] = useState<LabelCreateInput>({ 1768 const [form, setForm] = useState<LabelCreateInput>({
1763 labelName: "", 1769 labelName: "",
1764 templateCode: "", 1770 templateCode: "",
1765 - locationId: "", 1771 + locationIds: [],
1766 labelCategoryId: "", 1772 labelCategoryId: "",
1767 labelTypeId: "", 1773 labelTypeId: "",
1768 productIds: [], 1774 productIds: [],
@@ -1774,7 +1780,7 @@ function CreateLabelDialog({ @@ -1774,7 +1780,7 @@ function CreateLabelDialog({
1774 setForm({ 1780 setForm({
1775 labelName: "", 1781 labelName: "",
1776 templateCode: "", 1782 templateCode: "",
1777 - locationId: "", 1783 + locationIds: [],
1778 labelCategoryId: "", 1784 labelCategoryId: "",
1779 labelTypeId: "", 1785 labelTypeId: "",
1780 productIds: [], 1786 productIds: [],
@@ -1840,7 +1846,7 @@ function CreateLabelDialog({ @@ -1840,7 +1846,7 @@ function CreateLabelDialog({
1840 setForm((p) => ({ 1846 setForm((p) => ({
1841 ...p, 1847 ...p,
1842 templateCode: "", 1848 templateCode: "",
1843 - locationId: "", 1849 + locationIds: [],
1844 labelCategoryId: "", 1850 labelCategoryId: "",
1845 labelTypeId: "", 1851 labelTypeId: "",
1846 productIds: [], 1852 productIds: [],
@@ -1858,13 +1864,23 @@ function CreateLabelDialog({ @@ -1858,13 +1864,23 @@ function CreateLabelDialog({
1858 setForm((p) => ({ 1864 setForm((p) => ({
1859 ...p, 1865 ...p,
1860 templateCode: "", 1866 templateCode: "",
1861 - locationId: "", 1867 + locationIds: [],
1862 labelCategoryId: "", 1868 labelCategoryId: "",
1863 labelTypeId: "", 1869 labelTypeId: "",
1864 productIds: [], 1870 productIds: [],
1865 })); 1871 }));
1866 }, [open, scopePartnerId]); 1872 }, [open, scopePartnerId]);
1867 1873
  1874 + useEffect(() => {
  1875 + if (!regionScopeReady) return;
  1876 + const allowed = new Set(locationsInScope.map((l) => l.id));
  1877 + setForm((p) => {
  1878 + const next = (p.locationIds ?? []).filter((id) => allowed.has(id));
  1879 + if (next.length === (p.locationIds ?? []).length) return p;
  1880 + return { ...p, locationIds: next };
  1881 + });
  1882 + }, [locationsInScope, regionScopeReady]);
  1883 +
1868 const companySelectOptionsForCreate = useMemo( 1884 const companySelectOptionsForCreate = useMemo(
1869 () => 1885 () =>
1870 [...partners] 1886 [...partners]
@@ -2035,9 +2051,21 @@ function CreateLabelDialog({ @@ -2035,9 +2051,21 @@ function CreateLabelDialog({
2035 }); 2051 });
2036 return; 2052 return;
2037 } 2053 }
2038 - if (!form.labelName.trim() || !form.templateCode.trim() || !form.locationId.trim() || !form.labelCategoryId.trim() || !form.labelTypeId.trim()) { 2054 + const locPayload = buildSpecifiedLocationPayload(
  2055 + locations,
  2056 + partners,
  2057 + groups,
  2058 + effectivePartnerId,
  2059 + selectedRegionNames,
  2060 + form.locationIds,
  2061 + );
  2062 + if (!locPayload.ok) {
  2063 + toast.error("Validation failed", { description: locPayload.message });
  2064 + return;
  2065 + }
  2066 + if (!form.labelName.trim() || !form.templateCode.trim() || !form.labelCategoryId.trim() || !form.labelTypeId.trim()) {
2039 toast.error("Validation failed", { 2067 toast.error("Validation failed", {
2040 - description: "Fill all required fields and select template, location, category, and type.", 2068 + description: "Fill all required fields and select template, location(s), category, and type.",
2041 }); 2069 });
2042 return; 2070 return;
2043 } 2071 }
@@ -2118,6 +2146,7 @@ function CreateLabelDialog({ @@ -2118,6 +2146,7 @@ function CreateLabelDialog({
2118 try { 2146 try {
2119 await createLabel({ 2147 await createLabel({
2120 ...form, 2148 ...form,
  2149 + locationIds: locPayload.locationIds,
2121 appliedRegionType: regionPayload.appliedRegionType, 2150 appliedRegionType: regionPayload.appliedRegionType,
2122 regionIds: regionPayload.regionIds, 2151 regionIds: regionPayload.regionIds,
2123 groupIds: regionPayload.regionIds, 2152 groupIds: regionPayload.regionIds,
@@ -2415,15 +2444,21 @@ function CreateLabelDialog({ @@ -2415,15 +2444,21 @@ function CreateLabelDialog({
2415 </div> 2444 </div>
2416 <div className="space-y-2"> 2445 <div className="space-y-2">
2417 <Label>Location *</Label> 2446 <Label>Location *</Label>
2418 - <SearchableSelect  
2419 - value={form.locationId}  
2420 - onValueChange={(v) => setForm((p) => ({ ...p, locationId: v }))} 2447 + <SearchableMultiSelect
  2448 + values={form.locationIds}
  2449 + onValuesChange={(ids) => setForm((p) => ({ ...p, locationIds: ids }))}
2421 options={locationOptions} 2450 options={locationOptions}
2422 - placeholder={regionScopeReady ? "Select location" : scopeGateHint} 2451 + placeholder={regionScopeReady ? "Select location(s)…" : scopeGateHint}
2423 searchPlaceholder="Search location…" 2452 searchPlaceholder="Search location…"
  2453 + selectAllRowLabel="Select All"
2424 emptyText={regionScopeReady ? "No locations in this region." : scopeGateEmpty} 2454 emptyText={regionScopeReady ? "No locations in this region." : scopeGateEmpty}
2425 disabled={scopeBootstrapLoading || !regionScopeReady || scopedOptionsLoading} 2455 disabled={scopeBootstrapLoading || !regionScopeReady || scopedOptionsLoading}
2426 /> 2456 />
  2457 + <p className="text-xs text-gray-500">
  2458 + {regionScopeReady
  2459 + ? "Select All applies this label to every location in the selected region(s)."
  2460 + : scopeGateEmpty}
  2461 + </p>
2427 </div> 2462 </div>
2428 <div className="space-y-2"> 2463 <div className="space-y-2">
2429 <Label>Label Category *</Label> 2464 <Label>Label Category *</Label>
@@ -2672,7 +2707,7 @@ function EditLabelDialog({ @@ -2672,7 +2707,7 @@ function EditLabelDialog({
2672 const [form, setForm] = useState<LabelUpdateInput>({ 2707 const [form, setForm] = useState<LabelUpdateInput>({
2673 labelName: "", 2708 labelName: "",
2674 templateCode: "", 2709 templateCode: "",
2675 - locationId: "", 2710 + locationIds: [],
2676 labelCategoryId: "", 2711 labelCategoryId: "",
2677 labelTypeId: "", 2712 labelTypeId: "",
2678 productIds: [], 2713 productIds: [],
@@ -2988,9 +3023,21 @@ function EditLabelDialog({ @@ -2988,9 +3023,21 @@ function EditLabelDialog({
2988 }); 3023 });
2989 return; 3024 return;
2990 } 3025 }
2991 - if (!form.labelName.trim() || !form.templateCode.trim() || !form.locationId.trim() || !form.labelCategoryId.trim() || !form.labelTypeId.trim()) { 3026 + const locPayload = buildSpecifiedLocationPayload(
  3027 + locations,
  3028 + partners,
  3029 + groups,
  3030 + effectivePartnerId,
  3031 + selectedRegionNames,
  3032 + form.locationIds,
  3033 + );
  3034 + if (!locPayload.ok) {
  3035 + toast.error("Validation failed", { description: locPayload.message });
  3036 + return;
  3037 + }
  3038 + if (!form.labelName.trim() || !form.templateCode.trim() || !form.labelCategoryId.trim() || !form.labelTypeId.trim()) {
2992 toast.error("Validation failed", { 3039 toast.error("Validation failed", {
2993 - description: "Fill all required fields and select template, location, category, and type.", 3040 + description: "Fill all required fields and select template, location(s), category, and type.",
2994 }); 3041 });
2995 return; 3042 return;
2996 } 3043 }
@@ -3071,6 +3118,7 @@ function EditLabelDialog({ @@ -3071,6 +3118,7 @@ function EditLabelDialog({
3071 try { 3118 try {
3072 await updateLabel(label.id, { 3119 await updateLabel(label.id, {
3073 ...form, 3120 ...form,
  3121 + locationIds: locPayload.locationIds,
3074 appliedRegionType: regionPayload.appliedRegionType, 3122 appliedRegionType: regionPayload.appliedRegionType,
3075 regionIds: regionPayload.regionIds, 3123 regionIds: regionPayload.regionIds,
3076 groupIds: regionPayload.regionIds, 3124 groupIds: regionPayload.regionIds,
@@ -3116,12 +3164,20 @@ function EditLabelDialog({ @@ -3116,12 +3164,20 @@ function EditLabelDialog({
3116 value: loc.id, 3164 value: loc.id,
3117 label: toDisplay(loc.locationName ?? loc.locationCode ?? loc.id), 3165 label: toDisplay(loc.locationName ?? loc.locationCode ?? loc.id),
3118 })); 3166 }));
3119 - const id = form.locationId;  
3120 - if (id && !base.some((o) => o.value === id)) {  
3121 - return [{ value: id, label: `${id} (current)` }, ...base];  
3122 - }  
3123 - return base;  
3124 - }, [editScopeReady, locationsInEditScope, locations, form.locationId]); 3167 + const missing = (form.locationIds ?? []).filter((id) => id && !base.some((o) => o.value === id));
  3168 + const extras = missing.map((id) => ({ value: id, label: `${id} (current)` }));
  3169 + return extras.length ? [...extras, ...base] : base;
  3170 + }, [editScopeReady, locationsInEditScope, locations, form.locationIds]);
  3171 +
  3172 + useEffect(() => {
  3173 + if (!editScopeReady) return;
  3174 + const allowed = new Set(locationsInEditScope.map((l) => l.id));
  3175 + setForm((p) => {
  3176 + const next = (p.locationIds ?? []).filter((id) => allowed.has(id));
  3177 + if (next.length === (p.locationIds ?? []).length) return p;
  3178 + return { ...p, locationIds: next };
  3179 + });
  3180 + }, [locationsInEditScope, editScopeReady]);
3125 3181
3126 const editCategoryOptions = useMemo(() => { 3182 const editCategoryOptions = useMemo(() => {
3127 const base = labelCategoriesScopedEdit.map((c) => ({ 3183 const base = labelCategoriesScopedEdit.map((c) => ({
@@ -3351,17 +3407,23 @@ function EditLabelDialog({ @@ -3351,17 +3407,23 @@ function EditLabelDialog({
3351 </div> 3407 </div>
3352 <div className="space-y-2"> 3408 <div className="space-y-2">
3353 <Label>Location *</Label> 3409 <Label>Location *</Label>
3354 - <SearchableSelect  
3355 - value={form.locationId}  
3356 - onValueChange={(v) => setForm((p) => ({ ...p, locationId: v }))} 3410 + <SearchableMultiSelect
  3411 + values={form.locationIds}
  3412 + onValuesChange={(ids) => setForm((p) => ({ ...p, locationIds: ids }))}
3357 options={editLocationOptions} 3413 options={editLocationOptions}
3358 - placeholder={editScopeReady ? "Select location" : "Select applicable region(s) first"} 3414 + placeholder={editScopeReady ? "Select location(s)…" : "Select applicable region(s) first"}
3359 searchPlaceholder="Search location…" 3415 searchPlaceholder="Search location…"
  3416 + selectAllRowLabel="Select All"
3360 emptyText={ 3417 emptyText={
3361 editScopeReady ? "No locations in selected regions." : "Select applicable region(s) first." 3418 editScopeReady ? "No locations in selected regions." : "Select applicable region(s) first."
3362 } 3419 }
3363 disabled={refLoading || detailLoading || !editScopeReady || editScopedLoading} 3420 disabled={refLoading || detailLoading || !editScopeReady || editScopedLoading}
3364 /> 3421 />
  3422 + <p className="text-xs text-gray-500">
  3423 + {editScopeReady
  3424 + ? "Select All applies this label to every location in the selected region(s)."
  3425 + : "Select applicable region(s) first."}
  3426 + </p>
3365 </div> 3427 </div>
3366 </div> 3428 </div>
3367 3429
美国版/Food Labeling Management Platform/src/components/products/ProductsView.tsx
@@ -13,6 +13,7 @@ import { @@ -13,6 +13,7 @@ import {
13 import { Button } from "../ui/button"; 13 import { Button } from "../ui/button";
14 import { Checkbox } from "../ui/checkbox"; 14 import { Checkbox } from "../ui/checkbox";
15 import { Input } from "../ui/input"; 15 import { Input } from "../ui/input";
  16 +import { Textarea } from "../ui/textarea";
16 import { 17 import {
17 Table, 18 Table,
18 TableBody, 19 TableBody,
@@ -1917,14 +1918,14 @@ function ProductFormDialog({ @@ -1917,14 +1918,14 @@ function ProductFormDialog({
1917 {apSel.text && !apSel.image ? ( 1918 {apSel.text && !apSel.image ? (
1918 <div className="space-y-2"> 1919 <div className="space-y-2">
1919 <Label>Display Text</Label> 1920 <Label>Display Text</Label>
1920 - <Input  
1921 - className="h-10" 1921 + <Textarea
  1922 + className="min-h-[4.5rem] resize-y"
1922 value={displayText} 1923 value={displayText}
1923 onChange={(e) => setDisplayText(e.target.value)} 1924 onChange={(e) => setDisplayText(e.target.value)}
1924 placeholder="Product Name" 1925 placeholder="Product Name"
1925 /> 1926 />
1926 <div className="text-xs text-gray-500"> 1927 <div className="text-xs text-gray-500">
1927 - Saved to <span className="font-mono">photo</span> as this text. 1928 + Press Enter for a new line. Multiple spaces collapse to one; use Enter to break lines.
1928 </div> 1929 </div>
1929 </div> 1930 </div>
1930 ) : null} 1931 ) : null}
@@ -2480,14 +2481,14 @@ function ProductCategoryFormDialog({ @@ -2480,14 +2481,14 @@ function ProductCategoryFormDialog({
2480 {apSel.text && !apSel.image ? ( 2481 {apSel.text && !apSel.image ? (
2481 <div className="space-y-2"> 2482 <div className="space-y-2">
2482 <Label>Display Text</Label> 2483 <Label>Display Text</Label>
2483 - <Input  
2484 - className="h-10" 2484 + <Textarea
  2485 + className="min-h-[4.5rem] resize-y"
2485 value={displayText} 2486 value={displayText}
2486 onChange={(e) => setDisplayText(e.target.value)} 2487 onChange={(e) => setDisplayText(e.target.value)}
2487 placeholder="Category Name" 2488 placeholder="Category Name"
2488 /> 2489 />
2489 <div className="text-xs text-gray-500"> 2490 <div className="text-xs text-gray-500">
2490 - Saved to <span className="font-mono">photo</span> as this text. 2491 + Press Enter for a new line. Multiple spaces collapse to one; use Enter to break lines.
2491 </div> 2492 </div>
2492 </div> 2493 </div>
2493 ) : null} 2494 ) : null}
美国版/Food Labeling Management Platform/src/components/ui/category-button-visual-thumb.tsx
@@ -16,7 +16,7 @@ const TEXT_WRAP_STYLE: CSSProperties = { @@ -16,7 +16,7 @@ const TEXT_WRAP_STYLE: CSSProperties = {
16 width: "100%", 16 width: "100%",
17 maxWidth: "100%", 17 maxWidth: "100%",
18 minWidth: 0, 18 minWidth: 0,
19 - whiteSpace: "normal", 19 + whiteSpace: "pre-wrap",
20 wordBreak: "break-word", 20 wordBreak: "break-word",
21 overflow: "hidden", 21 overflow: "hidden",
22 textAlign: "center", 22 textAlign: "center",
美国版/Food Labeling Management Platform/src/lib/categoryButtonAppearance.ts
@@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
2 2
3 export type AppearanceToken = "TEXT" | "COLOR" | "IMAGE"; 3 export type AppearanceToken = "TEXT" | "COLOR" | "IMAGE";
4 4
5 -/** Display Text 输入:仅 trim,无字数上限 */ 5 +/** Display Text 输入:仅 trim 首尾空白,保留中间换行(Enter) */
6 export function normalizeDisplayText(s: string | null | undefined): string { 6 export function normalizeDisplayText(s: string | null | undefined): string {
7 return String(s ?? "").trim(); 7 return String(s ?? "").trim();
8 } 8 }
美国版/Food Labeling Management Platform/src/lib/labelFormDatePreview.ts
@@ -369,13 +369,29 @@ export function resolveElementDateTimeDisplay( @@ -369,13 +369,29 @@ export function resolveElementDateTimeDisplay(
369 type === "TIME" 369 type === "TIME"
370 ) { 370 ) {
371 const off = readOffsetFromElementConfig(el); 371 const off = readOffsetFromElementConfig(el);
372 - return resolveFromOffsetAmount(el, off?.amount ?? 0, off?.unit ?? "Days", base); 372 + if (off) return resolveFromOffsetAmount(el, off.amount, off.unit, base);
  373 + if (
  374 + rawText &&
  375 + !tryParsePrintInputOffsetStored(rawText) &&
  376 + isLikelyResolvedDateTimeLiteral(rawText)
  377 + ) {
  378 + return applyElementPrefix(cfg, rawText);
  379 + }
  380 + return resolveFromOffsetAmount(el, 0, "Days", base);
373 } 381 }
374 return null; 382 return null;
375 } 383 }
376 384
377 const off = readOffsetFromElementConfig(el); 385 const off = readOffsetFromElementConfig(el);
378 if (off) return resolveFromOffsetAmount(el, off.amount, off.unit, base); 386 if (off) return resolveFromOffsetAmount(el, off.amount, off.unit, base);
  387 + if (
  388 + rawText &&
  389 + !tryParsePrintInputOffsetStored(rawText) &&
  390 + isLikelyResolvedDateTimeLiteral(rawText) &&
  391 + isDateTimeLiveResolveField(el)
  392 + ) {
  393 + return applyElementPrefix(cfg, rawText);
  394 + }
379 return resolveFromOffsetAmount(el, 0, "Days", base); 395 return resolveFromOffsetAmount(el, 0, "Days", base);
380 } 396 }
381 397
美国版/Food Labeling Management Platform/src/services/labelService.ts
@@ -37,9 +37,23 @@ export function normalizeLabelDto(raw: unknown): LabelDto { @@ -37,9 +37,23 @@ export function normalizeLabelDto(raw: unknown): LabelDto {
37 ? "SPECIFIED" 37 ? "SPECIFIED"
38 : null; 38 : null;
39 const regionRaw = r.region ?? r.Region; 39 const regionRaw = r.region ?? r.Region;
  40 + const locationIds = parseIdList(r.locationIds ?? r.LocationIds) ?? [];
  41 + const locationIdRaw = r.locationId ?? r.LocationId;
  42 + const locationId =
  43 + locationIdRaw != null && String(locationIdRaw).trim()
  44 + ? String(locationIdRaw).trim()
  45 + : locationIds[0] ?? null;
  46 + const mergedLocationIds = [
  47 + ...new Set([
  48 + ...(locationId ? [locationId] : []),
  49 + ...locationIds,
  50 + ]),
  51 + ];
40 return { 52 return {
41 ...(r as object), 53 ...(r as object),
42 id: String(r.id ?? r.Id ?? r.labelCode ?? r.LabelCode ?? ""), 54 id: String(r.id ?? r.Id ?? r.labelCode ?? r.LabelCode ?? ""),
  55 + locationId,
  56 + locationIds: mergedLocationIds.length ? mergedLocationIds : locationIds,
43 regionIds: mergedRegionIds.length ? mergedRegionIds : regionIds, 57 regionIds: mergedRegionIds.length ? mergedRegionIds : regionIds,
44 groupIds: mergedRegionIds.length ? mergedRegionIds : groupIds, 58 groupIds: mergedRegionIds.length ? mergedRegionIds : groupIds,
45 appliedRegionType: appliedRegionType as LabelDto["appliedRegionType"], 59 appliedRegionType: appliedRegionType as LabelDto["appliedRegionType"],
@@ -47,6 +61,18 @@ export function normalizeLabelDto(raw: unknown): LabelDto { @@ -47,6 +61,18 @@ export function normalizeLabelDto(raw: unknown): LabelDto {
47 } as LabelDto; 61 } as LabelDto;
48 } 62 }
49 63
  64 +function locationBodyFromInput(input: LabelCreateInput | LabelUpdateInput): Record<string, unknown> {
  65 + const locationIds = [
  66 + ...new Set((input.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean)),
  67 + ];
  68 + const body: Record<string, unknown> = {};
  69 + if (locationIds.length) {
  70 + body.locationIds = locationIds;
  71 + body.locationId = locationIds[0];
  72 + }
  73 + return body;
  74 +}
  75 +
50 function scopeBodyFromInput(input: LabelCreateInput | LabelUpdateInput): Record<string, unknown> { 76 function scopeBodyFromInput(input: LabelCreateInput | LabelUpdateInput): Record<string, unknown> {
51 const regionIds = [ 77 const regionIds = [
52 ...new Set( 78 ...new Set(
@@ -118,7 +144,7 @@ export async function createLabel(input: LabelCreateInput): Promise&lt;LabelDto&gt; { @@ -118,7 +144,7 @@ export async function createLabel(input: LabelCreateInput): Promise&lt;LabelDto&gt; {
118 labelCode: String(input.labelCode ?? "").trim() || null, 144 labelCode: String(input.labelCode ?? "").trim() || null,
119 labelName: input.labelName, 145 labelName: input.labelName,
120 templateCode: input.templateCode, 146 templateCode: input.templateCode,
121 - locationId: input.locationId, 147 + ...locationBodyFromInput(input),
122 labelCategoryId: input.labelCategoryId, 148 labelCategoryId: input.labelCategoryId,
123 labelTypeId: input.labelTypeId, 149 labelTypeId: input.labelTypeId,
124 productIds: input.productIds, 150 productIds: input.productIds,
@@ -137,7 +163,7 @@ export async function updateLabel(labelCode: string, input: LabelUpdateInput): P @@ -137,7 +163,7 @@ export async function updateLabel(labelCode: string, input: LabelUpdateInput): P
137 body: { 163 body: {
138 labelName: input.labelName, 164 labelName: input.labelName,
139 templateCode: input.templateCode, 165 templateCode: input.templateCode,
140 - locationId: input.locationId, 166 + ...locationBodyFromInput(input),
141 labelCategoryId: input.labelCategoryId, 167 labelCategoryId: input.labelCategoryId,
142 labelTypeId: input.labelTypeId, 168 labelTypeId: input.labelTypeId,
143 productIds: input.productIds, 169 productIds: input.productIds,
美国版/Food Labeling Management Platform/src/types/label.ts
@@ -4,6 +4,8 @@ export type LabelDto = { @@ -4,6 +4,8 @@ export type LabelDto = {
4 labelName?: string | null; 4 labelName?: string | null;
5 templateCode?: string | null; 5 templateCode?: string | null;
6 locationId?: string | null; 6 locationId?: string | null;
  7 + /** 详情/编辑:适用门店 Id 列表(与后端 LocationIds 对齐) */
  8 + locationIds?: string[] | null;
7 labelCategoryId?: string | null; 9 labelCategoryId?: string | null;
8 labelTypeId?: string | null; 10 labelTypeId?: string | null;
9 productIds?: string[] | null; 11 productIds?: string[] | null;
@@ -61,7 +63,8 @@ export type LabelCreateInput = { @@ -61,7 +63,8 @@ export type LabelCreateInput = {
61 labelCode?: string | null; 63 labelCode?: string | null;
62 labelName: string; 64 labelName: string;
63 templateCode: string; 65 templateCode: string;
64 - locationId: string; 66 + /** 适用门店(至少 1 个;Select All 时为当前 Region 下全部门店 Id) */
  67 + locationIds: string[];
65 labelCategoryId: string; 68 labelCategoryId: string;
66 labelTypeId: string; 69 labelTypeId: string;
67 productIds: string[]; // 至少 1 个 70 productIds: string[]; // 至少 1 个
@@ -75,7 +78,8 @@ export type LabelCreateInput = { @@ -75,7 +78,8 @@ export type LabelCreateInput = {
75 export type LabelUpdateInput = { 78 export type LabelUpdateInput = {
76 labelName: string; 79 labelName: string;
77 templateCode: string; 80 templateCode: string;
78 - locationId: string; 81 + /** 适用门店(至少 1 个) */
  82 + locationIds: string[];
79 labelCategoryId: string; 83 labelCategoryId: string;
80 labelTypeId: string; 84 labelTypeId: string;
81 productIds: string[]; // 至少 1 个 85 productIds: string[]; // 至少 1 个