From 9d21893049b40f11372c0a879399537b164c131b Mon Sep 17 00:00:00 2001 From: jokerxue <2509699647@qq.com> Date: Tue, 23 Jun 2026 14:40:46 +0800 Subject: [PATCH] 修改 --- 打印机安卓基座/native-fast-printer/android-src/src/com/foodlabel/nativeprinter/template/NativeTemplateCommandBuilder.java | 16 +++++++++++++--- 美国版/Food Labeling Management App UniApp/src/pages/labels/labels.vue | 8 ++++---- 美国版/Food Labeling Management App UniApp/src/pages/labels/preview.vue | 33 +++++++++++++++++++++------------ 美国版/Food Labeling Management App UniApp/src/services/usAppLabeling.ts | 5 ++--- 美国版/Food Labeling Management App UniApp/src/utils/labelPreview/normalizePreviewTemplate.ts | 105 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------- 美国版/Food Labeling Management App UniApp/src/utils/labelPreview/printInputOffset.ts | 35 ++++++++++++++++++++++++++++++++++- 美国版/Food Labeling Management App UniApp/src/utils/labelPreview/renderLabelPreviewCanvas.ts | 20 +++++++++++++++----- 美国版/Food Labeling Management App UniApp/src/utils/print/nativeTemplateElementSupport.ts | 21 +++++++++++++++++---- 美国版/Food Labeling Management App UniApp/src/utils/sqliteSync.ts | 30 ++++++++++++++++++++++++++++++ 美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs | 176 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 美国版/Food Labeling Management Platform/.env.local | 4 ++-- 美国版/Food Labeling Management Platform/src/components/labels/LabelCategoriesView.tsx | 13 +++++++------ 美国版/Food Labeling Management Platform/src/components/labels/LabelsList.tsx | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------ 美国版/Food Labeling Management Platform/src/components/products/ProductsView.tsx | 13 +++++++------ 美国版/Food Labeling Management Platform/src/components/ui/category-button-visual-thumb.tsx | 2 +- 美国版/Food Labeling Management Platform/src/lib/categoryButtonAppearance.ts | 2 +- 美国版/Food Labeling Management Platform/src/lib/labelFormDatePreview.ts | 18 +++++++++++++++++- 美国版/Food Labeling Management Platform/src/services/labelService.ts | 30 ++++++++++++++++++++++++++++-- 美国版/Food Labeling Management Platform/src/types/label.ts | 8 ++++++-- 19 files changed, 562 insertions(+), 87 deletions(-) diff --git a/打印机安卓基座/native-fast-printer/android-src/src/com/foodlabel/nativeprinter/template/NativeTemplateCommandBuilder.java b/打印机安卓基座/native-fast-printer/android-src/src/com/foodlabel/nativeprinter/template/NativeTemplateCommandBuilder.java index f01881e..ef44462 100644 --- a/打印机安卓基座/native-fast-printer/android-src/src/com/foodlabel/nativeprinter/template/NativeTemplateCommandBuilder.java +++ b/打印机安卓基座/native-fast-printer/android-src/src/com/foodlabel/nativeprinter/template/NativeTemplateCommandBuilder.java @@ -367,6 +367,9 @@ public final class NativeTemplateCommandBuilder { } private static boolean shouldRasterizeText(String text, String type, JSONObject config) { + if (getBoolean(config, "invertColors", false) || getBoolean(config, "InvertColors", false)) { + return true; + } if (getBoolean(config, "forceRasterText", false)) { return true; } @@ -431,11 +434,13 @@ public final class NativeTemplateCommandBuilder { text = text.replaceAll("\\s{2,}", " ").trim(); } int contentWidth = Math.max(8, pxToDots(getDouble(element, "width", 0), dpi)); + int elementHeightDots = Math.max(16, pxToDots(getDouble(element, "height", 0), dpi)); + boolean inverted = getBoolean(config, "invertColors", false) || getBoolean(config, "InvertColors", false); Paint paint = new Paint(); paint.setAntiAlias(true); paint.setDither(true); paint.setSubpixelText(true); - paint.setColor(Color.BLACK); + paint.setColor(inverted ? Color.WHITE : Color.BLACK); int fontSizeDots = Math.max(14, pxToDots(getDouble(config, "fontSize", 14), dpi)); paint.setTextSize(fontSizeDots); /** 不再对 TEXT_PRICE 强制加粗:fakeBold + 粗体会糊边、measureText 偏窄,右对齐时左侧易出现杂点 */ @@ -467,10 +472,15 @@ public final class NativeTemplateCommandBuilder { int horizontalPadding = TEXT_PADDING_DOTS * 2; int verticalPadding = TEXT_PADDING_DOTS * 2; int width = ensureMultipleOf8(Math.max(contentWidth + horizontalPadding * 2, (int) Math.ceil(maxLineWidth) + horizontalPadding * 2 + 4)); - int height = Math.max(16, Math.max(pxToDots(getDouble(element, "height", 0), dpi) + verticalPadding * 2, totalHeight + verticalPadding * 2 + 4)); + int height = Math.max(16, Math.max(elementHeightDots + verticalPadding * 2, totalHeight + verticalPadding * 2 + 4)); Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); - canvas.drawColor(Color.WHITE); + canvas.drawColor(inverted ? Color.BLACK : Color.WHITE); + if (inverted && elementHeightDots > 0 && contentWidth > 0) { + Paint fill = new Paint(); + fill.setColor(Color.BLACK); + canvas.drawRect(0, 0, width, height, fill); + } int topOffset = "TEXT_PRICE".equals(type) ? Math.max(verticalPadding, (height - totalHeight) / 2) diff --git a/美国版/Food Labeling Management App UniApp/src/pages/labels/labels.vue b/美国版/Food Labeling Management App UniApp/src/pages/labels/labels.vue index 4fbc4db..d33fbd5 100644 --- a/美国版/Food Labeling Management App UniApp/src/pages/labels/labels.vue +++ b/美国版/Food Labeling Management App UniApp/src/pages/labels/labels.vue @@ -952,7 +952,7 @@ const goBluetoothPage = () => { .cat-icon-text--wrap, .cat-icon-text--on-color { - white-space: normal; + white-space: pre-wrap; word-break: break-word; overflow: hidden; } @@ -1085,7 +1085,7 @@ const goBluetoothPage = () => { padding: 0; max-width: 100%; max-height: 100%; - white-space: normal; + white-space: pre-wrap; word-break: break-word; overflow: hidden; } @@ -1238,14 +1238,14 @@ const goBluetoothPage = () => { color: #111827; line-height: 1.25; text-align: center; - white-space: normal; + white-space: pre-wrap; word-break: break-word; overflow: hidden; box-sizing: border-box; } .food-thumb-text--wrap { - white-space: normal; + white-space: pre-wrap; word-break: break-word; } diff --git a/美国版/Food Labeling Management App UniApp/src/pages/labels/preview.vue b/美国版/Food Labeling Management App UniApp/src/pages/labels/preview.vue index 63603a7..d50274f 100644 --- a/美国版/Food Labeling Management App UniApp/src/pages/labels/preview.vue +++ b/美国版/Food Labeling Management App UniApp/src/pages/labels/preview.vue @@ -299,6 +299,7 @@ import { templateIncludesEmployeeElement, } from '../../utils/labelPreview/employeeElement' import { applyLiveDateTimeFieldsToTemplate } from '../../utils/labelPreview/printInputOffset' +import { readInvertColors } from '../../utils/invertColorsConfig' import { getLabelPrintRasterLayout, getPreviewCanvasCssSize, @@ -672,6 +673,9 @@ function applyNativeTemplateStyleScale( // 时间/日期类右对齐文本在部分机型原生 TEXT 宽度估算有误差,强制位图可避免截断。 cfg.forceRasterText = true } + if (readInvertColors(cfg)) { + cfg.forceRasterText = true + } } if (type === 'BARCODE') { const raw = String( @@ -824,6 +828,8 @@ const systemTemplate = ref(null) const basePreviewTemplate = ref(null) /** 接口返回的 templateProductDefaults,每次合并/重绘按当前时刻重算日期时间 */ const previewProductDefaults = ref>({}) +/** 最近一次 8.2 原始响应,便于 defaults 解析失败时重试 */ +const lastPreviewRawPayload = ref(null) const printOptionSelections = ref>({}) const dictLabelsByElementId = ref>({}) const dictValuesByElementId = ref>({}) @@ -1166,7 +1172,14 @@ function computeMergedPreviewTemplate(at?: Date): SystemLabelTemplate | null { let base = basePreviewTemplate.value if (!base) return null const baseTime = at ?? new Date() - const defaults = previewProductDefaults.value + let defaults = previewProductDefaults.value + if (Object.keys(defaults).length === 0 && lastPreviewRawPayload.value != null) { + const recovered = extractTemplateProductDefaultValuesFromPreviewPayload(lastPreviewRawPayload.value) + if (Object.keys(recovered).length > 0) { + defaults = recovered + previewProductDefaults.value = recovered + } + } if (Object.keys(defaults).length > 0) { base = applyTemplateProductDefaultValuesToTemplate(base, defaults, baseTime) } @@ -1332,6 +1345,7 @@ async function loadPreview() { systemTemplate.value = null basePreviewTemplate.value = null previewProductDefaults.value = {} + lastPreviewRawPayload.value = null printOptionSelections.value = {} printFreeFieldValues.value = {} dictLabelsByElementId.value = {} @@ -1344,6 +1358,7 @@ async function loadPreview() { productId: productId.value || undefined, baseTime: new Date().toISOString(), }) + lastPreviewRawPayload.value = raw const root = raw as Record const nested = root.data ?? root.Data const inner = @@ -1410,19 +1425,13 @@ async function loadPreview() { } const productDefaults = extractTemplateProductDefaultValuesFromPreviewPayload(raw) previewProductDefaults.value = productDefaults - let tmplWithDefaults = - Object.keys(productDefaults).length > 0 - ? applyTemplateProductDefaultValuesToTemplate(tmplRaw, productDefaults) - : tmplRaw + let tmplBase = tmplRaw const productCodeValue = extractProductCodeValueFromPreviewPayload(raw) if (productCodeValue) { - tmplWithDefaults = applyProductCodeValueToTemplateScanElements( - tmplWithDefaults, - productCodeValue, - ) + tmplBase = applyProductCodeValueToTemplateScanElements(tmplBase, productCodeValue) } - /** 画布像素仅按接口 template 的 width / height / unit 换算(与 renderLabelPreviewCanvas.toCanvasPx 一致),不用 labelSizeText 覆盖以免单位被误判 */ - const base = overlayProductNameOnPreviewTemplate(tmplWithDefaults, displayProductName.value) + /** 仅存原始模板 + 默认值;日期/时间在 computeMergedPreviewTemplate 内按同一 baseTime 重算一次 */ + const base = overlayProductNameOnPreviewTemplate(tmplBase, displayProductName.value) basePreviewTemplate.value = base printOptionSelections.value = readSelectionsFromTemplate(base) printFreeFieldValues.value = ensureFreeFieldKeys(base, readFreeFieldValuesFromTemplate(base)) @@ -1626,7 +1635,7 @@ const handlePrint = async () => { * Virtual BT 不做 xScale/安全区收窄(易导致营养表错位、条码丢失);光栅仅作回退。 * 普通蓝牙(BLE 或 classic+JS socket):canPrintCurrentLabelViaNativeFastJob 为 false,走下方光栅/直发 TSC。 */ - const tmplForNativeJob = normalizeTemplateForNativeFastJob(tmpl, printInputJson as any) + const tmplForNativeJob = normalizeTemplateForNativeFastJob(tmpl, printInputJson as any, printBaseTime) const currentDriver = getCurrentPrinterDriver() const currentConn = getBluetoothConnection() const isD320faxClassicNative = diff --git a/美国版/Food Labeling Management App UniApp/src/services/usAppLabeling.ts b/美国版/Food Labeling Management App UniApp/src/services/usAppLabeling.ts index c892e73..e709648 100644 --- a/美国版/Food Labeling Management App UniApp/src/services/usAppLabeling.ts +++ b/美国版/Food Labeling Management App UniApp/src/services/usAppLabeling.ts @@ -128,13 +128,12 @@ export async function fetchUsAppLabelingTree(input: { }) } +/** 离线缓存键:仅 location + labelCode + productId(不含 baseTime,避免同步预拉与预览页 key 不一致) */ export function buildLabelPreviewCacheKey(body: UsAppLabelPreviewInputVo): string { const loc = String(body.locationId || '').trim() const code = String(body.labelCode || '').trim() const pid = String(body.productId || '').trim() - const bt = String(body.baseTime || '').trim() - const minuteBucket = bt ? bt.slice(0, 16) : '' - return `preview:${loc}:${code}:${pid}:${minuteBucket}` + return `preview:${loc}:${code}:${pid}` } /** 接口 8.2(在线写入 SQLite,离线读缓存) */ diff --git a/美国版/Food Labeling Management App UniApp/src/utils/labelPreview/normalizePreviewTemplate.ts b/美国版/Food Labeling Management App UniApp/src/utils/labelPreview/normalizePreviewTemplate.ts index 1040090..c6d60e1 100644 --- a/美国版/Food Labeling Management App UniApp/src/utils/labelPreview/normalizePreviewTemplate.ts +++ b/美国版/Food Labeling Management App UniApp/src/utils/labelPreview/normalizePreviewTemplate.ts @@ -1,7 +1,9 @@ import type { SystemLabelTemplate, SystemTemplateElementBase } from '../print/types/printer' import { applyLiveDateTimeFieldsToTemplate, + isUniAppDateTimeOffsetField, resolveTemplateDefaultValueForElement, + tryParsePrintInputOffsetStored, } from './printInputOffset' import { applyNutritionDefaultJsonToConfig } from './nutritionDefaultsMerge' @@ -168,6 +170,41 @@ export function applyLabelSizeTextToTemplate( } /** + * 将接口/缓存中的默认值统一为字符串(兼容 object、双重 JSON 编码、PascalCase unit/value)。 + */ +export function coerceTemplateProductDefaultValueToString(v: unknown): string { + if (v == null) return '' + if (typeof v === 'string') { + const t = v.trim() + if (!t) return '' + if (t.startsWith('{') || t.startsWith('[')) return t + if (t.startsWith('"') && t.endsWith('"')) { + try { + const inner = JSON.parse(t) as unknown + if (typeof inner === 'string') return inner.trim() + } catch { + /* ignore */ + } + } + return t + } + if (typeof v === 'number' || typeof v === 'boolean') return String(v) + if (typeof v === 'object' && !Array.isArray(v)) { + const rec = v as Record + const unit = rec.unit ?? rec.Unit + const value = rec.value ?? rec.Value + if (unit != null && value != null) { + return JSON.stringify({ unit: String(unit), value: String(value) }) + } + } + try { + return JSON.stringify(v) + } catch { + return String(v) + } +} + +/** * 从预览接口响应中取出 `templateProductDefaultValues`(elementId → 字符串,兼容 PascalCase / 嵌套 data)。 */ export function extractTemplateProductDefaultValuesFromPreviewPayload(payload: unknown): Record { @@ -180,10 +217,7 @@ export function extractTemplateProductDefaultValuesFromPreviewPayload(payload: u for (const [k, v] of Object.entries(raw as Record)) { const key = String(k).trim() if (!key) continue - if (v == null) out[key] = '' - else if (typeof v === 'string') out[key] = v - else if (typeof v === 'number' || typeof v === 'boolean') out[key] = String(v) - else out[key] = JSON.stringify(v) + out[key] = coerceTemplateProductDefaultValueToString(v) } return Object.keys(out).length ? out : null } @@ -236,9 +270,32 @@ export function extractTemplateProductDefaultValuesFromPreviewPayload(payload: u inner ? tryLayer(inner) : null, tryLayer(r), tryLayer(payload), + tryExtractFromTemplateProductDefaultsArray(r), + inner ? tryExtractFromTemplateProductDefaultsArray(inner) : null, ) } +/** Web 模板 DTO 的 templateProductDefaults 数组 → 扁平 elementId → value */ +function tryExtractFromTemplateProductDefaultsArray(layer: unknown): Record | null { + if (layer == null || typeof layer !== 'object' || Array.isArray(layer)) return null + const L = layer as Record + const raw = L.templateProductDefaults ?? L.TemplateProductDefaults + if (!Array.isArray(raw)) return null + const out: Record = {} + for (const row of raw) { + if (row == null || typeof row !== 'object' || Array.isArray(row)) continue + const rec = row as Record + const dv = rec.defaultValues ?? rec.DefaultValues + if (dv == null || typeof dv !== 'object' || Array.isArray(dv)) continue + for (const [k, v] of Object.entries(dv as Record)) { + const key = String(k).trim() + if (!key) continue + out[key] = coerceTemplateProductDefaultValueToString(v) + } + } + return Object.keys(out).length ? out : null +} + function isTemplateSectionScanElement(el: SystemTemplateElementBase): boolean { const cfg = el.config || {} const typeAdd = String( @@ -313,6 +370,30 @@ export function applyProductCodeValueToTemplateScanElements( } /** + * 从 templateProductDefaultValues 按 id / inputKey / elementName 匹配(大小写不敏感兜底)。 + */ +export function lookupTemplateProductDefaultValue( + el: SystemTemplateElementBase, + defaults: Record, +): string | undefined { + const candidates = [ + String(el.id ?? '').trim(), + String(el.inputKey ?? '').trim(), + String(el.elementName ?? '').trim(), + ].filter(Boolean) + for (const k of candidates) { + if (Object.prototype.hasOwnProperty.call(defaults, k)) return defaults[k] + } + const lowerEntries = Object.entries(defaults).map(([k, v]) => [k.toLowerCase(), v] as const) + for (const k of candidates) { + const lk = k.toLowerCase() + const hit = lowerEntries.find(([dk]) => dk === lk) + if (hit) return hit[1] + } + return undefined +} + +/** * 将平台录入的默认值合并进模板元素 config,供画布预览(键与 elements[].id 一致)。 */ export function applyTemplateProductDefaultValuesToTemplate( @@ -323,9 +404,7 @@ export function applyTemplateProductDefaultValuesToTemplate( const keys = Object.keys(defaults) if (!keys.length) return template const elements = (template.elements || []).map((el) => { - const byName = (el.elementName ?? '').trim() - const v = - defaults[el.id] ?? (byName ? defaults[byName] : undefined) + const v = lookupTemplateProductDefaultValue(el, defaults) if (v === undefined) return el const type = String(el.type || '').toUpperCase() const cfg = { ...(el.config || {}) } as Record @@ -359,9 +438,15 @@ export function applyTemplateProductDefaultValuesToTemplate( } if (type === 'DATE' || type === 'TIME' || type === 'DURATION') { - const text = resolveTemplateDefaultValueForElement(el, v, base) - cfg.text = text - cfg.Text = text + /** 偏移类字段保留 JSON(如 {"unit":"Days","value":"2"}),供 applyLiveDateTimeFieldsToTemplate 按出纸时刻重算 */ + if (isUniAppDateTimeOffsetField(el) && tryParsePrintInputOffsetStored(v)) { + cfg.text = v + cfg.Text = v + } else { + const text = resolveTemplateDefaultValueForElement(el, v, base) + cfg.text = text + cfg.Text = text + } return { ...el, config: cfg } } diff --git a/美国版/Food Labeling Management App UniApp/src/utils/labelPreview/printInputOffset.ts b/美国版/Food Labeling Management App UniApp/src/utils/labelPreview/printInputOffset.ts index 2d9c8a1..5ed46cd 100644 --- a/美国版/Food Labeling Management App UniApp/src/utils/labelPreview/printInputOffset.ts +++ b/美国版/Food Labeling Management App UniApp/src/utils/labelPreview/printInputOffset.ts @@ -224,13 +224,46 @@ export function resolveElementDateTimeDisplay ( type === 'TIME' ) { const off = readOffsetFromElementConfig(el) - return resolveFromOffsetAmount(el, off?.amount ?? 0, off?.unit ?? 'Days', base) + if (off) return resolveFromOffsetAmount(el, off.amount, off.unit, base) + /** 默认值已展开为字面日期时仍保留,避免丢失 durationdate 等 +N Days */ + if ( + rawText && + !tryParsePrintInputOffsetStored(rawText) && + isLikelyResolvedDateTimeLiteral(rawText) + ) { + return applyElementPrefix(cfg, rawText) + } + return resolveFromOffsetAmount(el, 0, 'Days', base) } return null } + /** FIXED 的 durationdate/durationtime 等:config.text 可能已是平台录入的偏移 JSON */ + if (vst === 'FIXED' && isUniAppDateTimeOffsetField(el)) { + const off = readOffsetFromElementConfig(el) + if (off && (off.amount !== 0 || rawText)) { + return resolveFromOffsetAmount(el, off.amount, off.unit, base) + } + if ( + rawText && + !tryParsePrintInputOffsetStored(rawText) && + isLikelyResolvedDateTimeLiteral(rawText) + ) { + return applyElementPrefix(cfg, rawText) + } + return resolveFromOffsetAmount(el, 0, 'Days', base) + } + const off = readOffsetFromElementConfig(el) if (off) return resolveFromOffsetAmount(el, off.amount, off.unit, base) + if ( + rawText && + !tryParsePrintInputOffsetStored(rawText) && + isLikelyResolvedDateTimeLiteral(rawText) && + isUniAppDateTimeOffsetField(el) + ) { + return applyElementPrefix(cfg, rawText) + } return resolveFromOffsetAmount(el, 0, 'Days', base) } diff --git a/美国版/Food Labeling Management App UniApp/src/utils/labelPreview/renderLabelPreviewCanvas.ts b/美国版/Food Labeling Management App UniApp/src/utils/labelPreview/renderLabelPreviewCanvas.ts index c29ebe5..4fdf1a7 100644 --- a/美国版/Food Labeling Management App UniApp/src/utils/labelPreview/renderLabelPreviewCanvas.ts +++ b/美国版/Food Labeling Management App UniApp/src/utils/labelPreview/renderLabelPreviewCanvas.ts @@ -1,7 +1,7 @@ import type { RawImageDataSource, SystemLabelTemplate, SystemTemplateElementBase } from '../print/types/printer' import { resolveMediaUrlForApp, storedValueLooksLikeImagePath } from '../resolveMediaUrl' import { sortElementsForPreview, normalizeTemplatePrintOrientation } from './normalizePreviewTemplate' -import { resolveElementDateTimeDisplay } from './printInputOffset' +import { resolveElementDateTimeDisplay, isLikelyResolvedDateTimeLiteral } from './printInputOffset' import { getLoggedInEmployeeDisplayName, isEmployeeTemplateElement } from './employeeElement' import QRCode from 'qrcode' import { readInvertColors } from '../invertColorsConfig' @@ -134,6 +134,11 @@ function previewTextForElement(element: SystemTemplateElementBase, baseTime: Dat return applyConfigPrefix(config, getLoggedInEmployeeDisplayName()) } if (type === 'DATE' || type === 'TIME' || type === 'DURATION') { + /** applyLiveDateTimeFieldsToTemplate 已写入 config.text 时直接展示,避免二次 resolve 把 +N Days 重置为当天 */ + const baked = cfgStr(config, ['text', 'Text'], '').trim() + if (baked && isLikelyResolvedDateTimeLiteral(baked)) { + return applyConfigPrefix(config, baked) + } const live = resolveElementDateTimeDisplay(element, baseTime) if (live != null && live.trim()) return live } @@ -661,10 +666,15 @@ function runLabelPreviewCanvasDraw( else if (align === 'right') tx = x + w - pad ctx.setTextAlign(align === 'center' ? 'center' : align === 'right' ? 'right' : 'left') const lineHeight = fontSize + Math.max(2, Math.round(fontSize * 0.15)) - const maxChars = maxCharsPerLine(innerW, fontSize) - const lines = wrapTextToWidth(text, maxChars) - const maxLines = - innerH >= fontSize ? Math.max(1, Math.floor(innerH / lineHeight)) : lines.length + const isDateTimeType = type === 'DATE' || type === 'TIME' || type === 'DURATION' + const lines = isDateTimeType + ? [String(text).replace(/\s+/g, ' ').trim()] + : wrapTextToWidth(text, maxCharsPerLine(innerW, fontSize)) + const maxLines = isDateTimeType + ? 1 + : innerH >= fontSize + ? Math.max(1, Math.floor(innerH / lineHeight)) + : lines.length const startY = y + pad + fontSize lines.slice(0, maxLines).forEach((ln, li) => { ctx.fillText(ln, tx, startY + li * lineHeight) diff --git a/美国版/Food Labeling Management App UniApp/src/utils/print/nativeTemplateElementSupport.ts b/美国版/Food Labeling Management App UniApp/src/utils/print/nativeTemplateElementSupport.ts index fe8818d..9db3d8d 100644 --- a/美国版/Food Labeling Management App UniApp/src/utils/print/nativeTemplateElementSupport.ts +++ b/美国版/Food Labeling Management App UniApp/src/utils/print/nativeTemplateElementSupport.ts @@ -10,6 +10,7 @@ import type { import { formatBarcodeValueForTsc, normalizeBarcodeType } from '../barcodeFormat' import { applyTemplateData } from './templateRenderer' import { resolveElementDateTimeDisplay } from '../labelPreview/printInputOffset' +import { readInvertColors } from '../invertColorsConfig' function isElementHandledByNativeFastPrinter (el: SystemTemplateElementBase): boolean { const type = String(el.type || '').toUpperCase() @@ -95,9 +96,10 @@ function prepareBarcodeElementForNativePrint (el: SystemTemplateElementBase): Sy */ export function normalizeTemplateForNativeFastJob ( template: SystemLabelTemplate, - data: LabelTemplateData + data: LabelTemplateData, + baseTime: Date = new Date(), ): SystemLabelTemplate { - const now = new Date() + const now = baseTime const extras: SystemTemplateElementBase[] = [] const elements = (template.elements || []).map((el) => { @@ -125,10 +127,14 @@ export function normalizeTemplateForNativeFastJob ( if (v && u && !v.endsWith(u)) text = `${v}${u}` else text = v || u } + const nextConfig: Record = { ...config, text, nativeSourceType: type } + if (readInvertColors(config)) { + nextConfig.forceRasterText = true + } return { ...el, type: 'TEXT_STATIC' as typeof el.type, - config: { ...config, text, nativeSourceType: type }, + config: nextConfig, } } if (type === 'NUTRITION') { @@ -164,7 +170,14 @@ export function normalizeTemplateForNativeFastJob ( } return el }) - const merged = resolveNativeLayoutCollisions([...elements, ...extras]) + const withInvertRaster = elements.map((el) => { + const cfg = { ...(el.config || {}) } as Record + if (readInvertColors(cfg)) { + cfg.forceRasterText = true + } + return { ...el, config: cfg } + }) + const merged = resolveNativeLayoutCollisions([...withInvertRaster, ...extras]) return { ...template, elements: merged } } diff --git a/美国版/Food Labeling Management App UniApp/src/utils/sqliteSync.ts b/美国版/Food Labeling Management App UniApp/src/utils/sqliteSync.ts index bf05160..4fc9a63 100644 --- a/美国版/Food Labeling Management App UniApp/src/utils/sqliteSync.ts +++ b/美国版/Food Labeling Management App UniApp/src/utils/sqliteSync.ts @@ -207,6 +207,28 @@ export async function hasOfflineCache(module: string, name: string): Promise 0 } +/** 离线读缓存:匹配以 prefix 开头的 cache_name(兼容旧版 preview 键带 baseTime 后缀) */ +async function findOfflineCacheKeyByPrefix(module: string, namePrefix: string): Promise { + if (!namePrefix) return null + if (!isAppSqliteAvailable()) { + const map = getStorageMap<{ module: string; name: string }>(FALLBACK_CACHE_KEY) + for (const row of Object.values(map)) { + const m = String((row as { module?: string }).module || '') + const n = String((row as { name?: string }).name || '') + if (m === module && n.startsWith(namePrefix)) return n + } + return null + } + await initOfflineSqlite() + const rows = await sqliteSelect( + `SELECT cache_name FROM ${CACHE_TABLE} + WHERE module_name='${esc(module)}' AND cache_name LIKE '${esc(namePrefix)}%' + ORDER BY updated_at DESC LIMIT 1` + ) + const hit = rows?.[0]?.cache_name + return hit != null && String(hit).trim() ? String(hit) : null +} + export async function getOfflineCache(module: string, name: string): Promise { const key = cachePk(module, name) if (!isAppSqliteAvailable()) { @@ -399,6 +421,14 @@ export async function fetchWithOfflineCache( if (await hasOfflineCache(module, name)) { return (await getOfflineCache(module, name)) as T } + /** 兼容旧版 preview 缓存键(含 baseTime 分钟桶) */ + if (name.startsWith('preview:')) { + const legacyPrefix = `${name}:` + const legacyHit = await findOfflineCacheKeyByPrefix(module, legacyPrefix) + if (legacyHit) { + return (await getOfflineCache(module, legacyHit)) as T + } + } throw new Error('No offline cache available') } diff --git a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs b/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs index be42a9e..8708a2a 100644 --- a/美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs +++ b/美国版/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 } } + if (templateProductDefaultValues is { Count: > 0 }) + { + ApplyTemplateProductDefaultValuesToPreviewElements(template.Elements, templateProductDefaultValues); + } + return new UsAppLabelPreviewDto { LabelId = labelRow.Id, @@ -1177,6 +1182,177 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ return Math.Round((current - previous) * 100m / previous, 2); } + /// + /// 将 fl_label_template_product_default 默认值写入预览模板元素 config(日期偏移保留 JSON,由客户端按 BaseTime 展开)。 + /// + private static void ApplyTemplateProductDefaultValuesToPreviewElements( + IList? elements, + Dictionary defaults) + { + if (elements == null || elements.Count == 0 || defaults == null || defaults.Count == 0) + { + return; + } + + foreach (var el in elements) + { + var stored = ResolveTemplateProductDefaultValueForElement(el, defaults); + if (string.IsNullOrWhiteSpace(stored)) + { + continue; + } + + var cfg = ParsePreviewElementConfig(el.ConfigJson); + var type = (el.ElementType ?? string.Empty).Trim().ToUpperInvariant(); + + switch (type) + { + case "DATE": + case "TIME": + case "DURATION": + cfg["text"] = stored; + cfg["Text"] = stored; + break; + case "IMAGE": + case "LOGO": + cfg["src"] = stored; + cfg["Src"] = stored; + cfg["url"] = stored; + cfg["Url"] = stored; + break; + case "BARCODE": + case "QRCODE": + cfg["data"] = stored; + cfg["Data"] = stored; + break; + default: + cfg["text"] = stored; + cfg["Text"] = stored; + break; + } + + el.ConfigJson = cfg; + } + } + + private static string? ResolveTemplateProductDefaultValueForElement( + LabelTemplateElementDto el, + Dictionary defaults) + { + var candidates = new[] + { + el.Id?.Trim(), + el.InputKey?.Trim(), + el.ElementName?.Trim(), + }.Where(x => !string.IsNullOrWhiteSpace(x)).ToList(); + + foreach (var key in candidates) + { + if (defaults.TryGetValue(key!, out var direct)) + { + return TemplateProductDefaultValueToStoredString(direct); + } + } + + foreach (var kv in defaults) + { + foreach (var key in candidates) + { + if (string.Equals(kv.Key, key, StringComparison.OrdinalIgnoreCase)) + { + return TemplateProductDefaultValueToStoredString(kv.Value); + } + } + } + + return null; + } + + private static string? TemplateProductDefaultValueToStoredString(object? value) + { + if (value is null) + { + return null; + } + + if (value is string s) + { + return s; + } + + if (value is JsonElement je) + { + if (je.ValueKind == JsonValueKind.String) + { + return je.GetString(); + } + + if (je.ValueKind is JsonValueKind.Object or JsonValueKind.Array) + { + return je.GetRawText(); + } + + return je.ToString(); + } + + if (value is bool or int or long or decimal or double or float) + { + return Convert.ToString(value, CultureInfo.InvariantCulture); + } + + return JsonSerializer.Serialize(value); + } + + private static Dictionary ParsePreviewElementConfig(object? configJson) + { + if (configJson is null) + { + return new Dictionary(); + } + + if (configJson is Dictionary dict) + { + return new Dictionary(dict); + } + + if (configJson is JsonElement je && je.ValueKind == JsonValueKind.Object) + { + try + { + return JsonSerializer.Deserialize>(je.GetRawText()) + ?? new Dictionary(); + } + catch + { + return new Dictionary(); + } + } + + if (configJson is string raw && !string.IsNullOrWhiteSpace(raw)) + { + try + { + return JsonSerializer.Deserialize>(raw) + ?? new Dictionary(); + } + catch + { + return new Dictionary(); + } + } + + try + { + var json = JsonSerializer.Serialize(configJson); + return JsonSerializer.Deserialize>(json) + ?? new Dictionary(); + } + catch + { + return new Dictionary(); + } + } + private async Task ResolveTemplateProductDefaultValuesJsonAsync( string templateId, string? productId, diff --git a/美国版/Food Labeling Management Platform/.env.local b/美国版/Food Labeling Management Platform/.env.local index 1f60dea..15535e6 100644 --- a/美国版/Food Labeling Management Platform/.env.local +++ b/美国版/Food Labeling Management Platform/.env.local @@ -1,2 +1,2 @@ -VITE_API_BASE_URL=http://192.168.31.88:19001 -# VITE_API_BASE_URL=http://flus-test.3ffoodsafety.com +# VITE_API_BASE_URL=http://192.168.31.88:19001 + VITE_API_BASE_URL=http://flus-test.3ffoodsafety.com diff --git a/美国版/Food Labeling Management Platform/src/components/labels/LabelCategoriesView.tsx b/美国版/Food Labeling Management Platform/src/components/labels/LabelCategoriesView.tsx index d71d1a0..b7b56b3 100755 --- a/美国版/Food Labeling Management Platform/src/components/labels/LabelCategoriesView.tsx +++ b/美国版/Food Labeling Management Platform/src/components/labels/LabelCategoriesView.tsx @@ -8,6 +8,7 @@ import { TableRow, } from "../ui/table"; import { Input } from "../ui/input"; +import { Textarea } from "../ui/textarea"; import { Button } from "../ui/button"; import { Select, @@ -1095,14 +1096,14 @@ function CreateLabelCategoryDialog({ {apSel.text && !apSel.image ? (
- setDisplayTextForPhoto(e.target.value)} />
- Saved to photo as this text. + Press Enter for a new line. Multiple spaces collapse to one; use Enter to break lines.
) : null} @@ -1566,14 +1567,14 @@ function EditLabelCategoryDialog({ {apSel.text && !apSel.image ? (
- setDisplayTextForPhoto(e.target.value)} />
- Saved to photo as this text. + Press Enter for a new line. Multiple spaces collapse to one; use Enter to break lines.
) : null} diff --git a/美国版/Food Labeling Management Platform/src/components/labels/LabelsList.tsx b/美国版/Food Labeling Management Platform/src/components/labels/LabelsList.tsx index 29f29a6..e873a8a 100755 --- a/美国版/Food Labeling Management Platform/src/components/labels/LabelsList.tsx +++ b/美国版/Food Labeling Management Platform/src/components/labels/LabelsList.tsx @@ -110,6 +110,7 @@ import { regionNamesToGroupIds, regionOptionsForPartner, scopePartnerValidationMessage, + buildSpecifiedLocationPayload, } from "../../lib/categoryScopeForm"; function toDisplay(v: string | null | undefined): string { @@ -308,7 +309,12 @@ function labelDtoToUpdateForm(d: LabelDto): LabelUpdateInput { return { labelName: d.labelName ?? "", templateCode: d.templateCode ?? "", - locationId: d.locationId ?? "", + locationIds: (() => { + const fromList = (d.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean); + if (fromList.length) return [...new Set(fromList)]; + const single = (d.locationId ?? "").trim(); + return single ? [single] : []; + })(), labelCategoryId: d.labelCategoryId ?? "", labelTypeId: d.labelTypeId ?? "", /** 编辑表单仅支持单商品:多商品时取第一个 */ @@ -1762,7 +1768,7 @@ function CreateLabelDialog({ const [form, setForm] = useState({ labelName: "", templateCode: "", - locationId: "", + locationIds: [], labelCategoryId: "", labelTypeId: "", productIds: [], @@ -1774,7 +1780,7 @@ function CreateLabelDialog({ setForm({ labelName: "", templateCode: "", - locationId: "", + locationIds: [], labelCategoryId: "", labelTypeId: "", productIds: [], @@ -1840,7 +1846,7 @@ function CreateLabelDialog({ setForm((p) => ({ ...p, templateCode: "", - locationId: "", + locationIds: [], labelCategoryId: "", labelTypeId: "", productIds: [], @@ -1858,13 +1864,23 @@ function CreateLabelDialog({ setForm((p) => ({ ...p, templateCode: "", - locationId: "", + locationIds: [], labelCategoryId: "", labelTypeId: "", productIds: [], })); }, [open, scopePartnerId]); + useEffect(() => { + if (!regionScopeReady) return; + const allowed = new Set(locationsInScope.map((l) => l.id)); + setForm((p) => { + const next = (p.locationIds ?? []).filter((id) => allowed.has(id)); + if (next.length === (p.locationIds ?? []).length) return p; + return { ...p, locationIds: next }; + }); + }, [locationsInScope, regionScopeReady]); + const companySelectOptionsForCreate = useMemo( () => [...partners] @@ -2035,9 +2051,21 @@ function CreateLabelDialog({ }); return; } - if (!form.labelName.trim() || !form.templateCode.trim() || !form.locationId.trim() || !form.labelCategoryId.trim() || !form.labelTypeId.trim()) { + const locPayload = buildSpecifiedLocationPayload( + locations, + partners, + groups, + effectivePartnerId, + selectedRegionNames, + form.locationIds, + ); + if (!locPayload.ok) { + toast.error("Validation failed", { description: locPayload.message }); + return; + } + if (!form.labelName.trim() || !form.templateCode.trim() || !form.labelCategoryId.trim() || !form.labelTypeId.trim()) { toast.error("Validation failed", { - description: "Fill all required fields and select template, location, category, and type.", + description: "Fill all required fields and select template, location(s), category, and type.", }); return; } @@ -2118,6 +2146,7 @@ function CreateLabelDialog({ try { await createLabel({ ...form, + locationIds: locPayload.locationIds, appliedRegionType: regionPayload.appliedRegionType, regionIds: regionPayload.regionIds, groupIds: regionPayload.regionIds, @@ -2415,15 +2444,21 @@ function CreateLabelDialog({
- setForm((p) => ({ ...p, locationId: v }))} + setForm((p) => ({ ...p, locationIds: ids }))} options={locationOptions} - placeholder={regionScopeReady ? "Select location" : scopeGateHint} + placeholder={regionScopeReady ? "Select location(s)…" : scopeGateHint} searchPlaceholder="Search location…" + selectAllRowLabel="Select All" emptyText={regionScopeReady ? "No locations in this region." : scopeGateEmpty} disabled={scopeBootstrapLoading || !regionScopeReady || scopedOptionsLoading} /> +

+ {regionScopeReady + ? "Select All applies this label to every location in the selected region(s)." + : scopeGateEmpty} +

@@ -2672,7 +2707,7 @@ function EditLabelDialog({ const [form, setForm] = useState({ labelName: "", templateCode: "", - locationId: "", + locationIds: [], labelCategoryId: "", labelTypeId: "", productIds: [], @@ -2988,9 +3023,21 @@ function EditLabelDialog({ }); return; } - if (!form.labelName.trim() || !form.templateCode.trim() || !form.locationId.trim() || !form.labelCategoryId.trim() || !form.labelTypeId.trim()) { + const locPayload = buildSpecifiedLocationPayload( + locations, + partners, + groups, + effectivePartnerId, + selectedRegionNames, + form.locationIds, + ); + if (!locPayload.ok) { + toast.error("Validation failed", { description: locPayload.message }); + return; + } + if (!form.labelName.trim() || !form.templateCode.trim() || !form.labelCategoryId.trim() || !form.labelTypeId.trim()) { toast.error("Validation failed", { - description: "Fill all required fields and select template, location, category, and type.", + description: "Fill all required fields and select template, location(s), category, and type.", }); return; } @@ -3071,6 +3118,7 @@ function EditLabelDialog({ try { await updateLabel(label.id, { ...form, + locationIds: locPayload.locationIds, appliedRegionType: regionPayload.appliedRegionType, regionIds: regionPayload.regionIds, groupIds: regionPayload.regionIds, @@ -3116,12 +3164,20 @@ function EditLabelDialog({ value: loc.id, label: toDisplay(loc.locationName ?? loc.locationCode ?? loc.id), })); - const id = form.locationId; - if (id && !base.some((o) => o.value === id)) { - return [{ value: id, label: `${id} (current)` }, ...base]; - } - return base; - }, [editScopeReady, locationsInEditScope, locations, form.locationId]); + const missing = (form.locationIds ?? []).filter((id) => id && !base.some((o) => o.value === id)); + const extras = missing.map((id) => ({ value: id, label: `${id} (current)` })); + return extras.length ? [...extras, ...base] : base; + }, [editScopeReady, locationsInEditScope, locations, form.locationIds]); + + useEffect(() => { + if (!editScopeReady) return; + const allowed = new Set(locationsInEditScope.map((l) => l.id)); + setForm((p) => { + const next = (p.locationIds ?? []).filter((id) => allowed.has(id)); + if (next.length === (p.locationIds ?? []).length) return p; + return { ...p, locationIds: next }; + }); + }, [locationsInEditScope, editScopeReady]); const editCategoryOptions = useMemo(() => { const base = labelCategoriesScopedEdit.map((c) => ({ @@ -3351,17 +3407,23 @@ function EditLabelDialog({
- setForm((p) => ({ ...p, locationId: v }))} + setForm((p) => ({ ...p, locationIds: ids }))} options={editLocationOptions} - placeholder={editScopeReady ? "Select location" : "Select applicable region(s) first"} + placeholder={editScopeReady ? "Select location(s)…" : "Select applicable region(s) first"} searchPlaceholder="Search location…" + selectAllRowLabel="Select All" emptyText={ editScopeReady ? "No locations in selected regions." : "Select applicable region(s) first." } disabled={refLoading || detailLoading || !editScopeReady || editScopedLoading} /> +

+ {editScopeReady + ? "Select All applies this label to every location in the selected region(s)." + : "Select applicable region(s) first."} +

diff --git a/美国版/Food Labeling Management Platform/src/components/products/ProductsView.tsx b/美国版/Food Labeling Management Platform/src/components/products/ProductsView.tsx index f7c2ba8..f46f32f 100755 --- a/美国版/Food Labeling Management Platform/src/components/products/ProductsView.tsx +++ b/美国版/Food Labeling Management Platform/src/components/products/ProductsView.tsx @@ -13,6 +13,7 @@ import { import { Button } from "../ui/button"; import { Checkbox } from "../ui/checkbox"; import { Input } from "../ui/input"; +import { Textarea } from "../ui/textarea"; import { Table, TableBody, @@ -1917,14 +1918,14 @@ function ProductFormDialog({ {apSel.text && !apSel.image ? (
- setDisplayText(e.target.value)} placeholder="Product Name" />
- Saved to photo as this text. + Press Enter for a new line. Multiple spaces collapse to one; use Enter to break lines.
) : null} @@ -2480,14 +2481,14 @@ function ProductCategoryFormDialog({ {apSel.text && !apSel.image ? (
- setDisplayText(e.target.value)} placeholder="Category Name" />
- Saved to photo as this text. + Press Enter for a new line. Multiple spaces collapse to one; use Enter to break lines.
) : null} diff --git a/美国版/Food Labeling Management Platform/src/components/ui/category-button-visual-thumb.tsx b/美国版/Food Labeling Management Platform/src/components/ui/category-button-visual-thumb.tsx index ffb33f9..30a765e 100644 --- a/美国版/Food Labeling Management Platform/src/components/ui/category-button-visual-thumb.tsx +++ b/美国版/Food Labeling Management Platform/src/components/ui/category-button-visual-thumb.tsx @@ -16,7 +16,7 @@ const TEXT_WRAP_STYLE: CSSProperties = { width: "100%", maxWidth: "100%", minWidth: 0, - whiteSpace: "normal", + whiteSpace: "pre-wrap", wordBreak: "break-word", overflow: "hidden", textAlign: "center", diff --git a/美国版/Food Labeling Management Platform/src/lib/categoryButtonAppearance.ts b/美国版/Food Labeling Management Platform/src/lib/categoryButtonAppearance.ts index 4e16f56..a61c3cc 100644 --- a/美国版/Food Labeling Management Platform/src/lib/categoryButtonAppearance.ts +++ b/美国版/Food Labeling Management Platform/src/lib/categoryButtonAppearance.ts @@ -2,7 +2,7 @@ export type AppearanceToken = "TEXT" | "COLOR" | "IMAGE"; -/** Display Text 输入:仅 trim,无字数上限 */ +/** Display Text 输入:仅 trim 首尾空白,保留中间换行(Enter) */ export function normalizeDisplayText(s: string | null | undefined): string { return String(s ?? "").trim(); } diff --git a/美国版/Food Labeling Management Platform/src/lib/labelFormDatePreview.ts b/美国版/Food Labeling Management Platform/src/lib/labelFormDatePreview.ts index 3150963..012da9a 100644 --- a/美国版/Food Labeling Management Platform/src/lib/labelFormDatePreview.ts +++ b/美国版/Food Labeling Management Platform/src/lib/labelFormDatePreview.ts @@ -369,13 +369,29 @@ export function resolveElementDateTimeDisplay( type === "TIME" ) { const off = readOffsetFromElementConfig(el); - return resolveFromOffsetAmount(el, off?.amount ?? 0, off?.unit ?? "Days", base); + if (off) return resolveFromOffsetAmount(el, off.amount, off.unit, base); + if ( + rawText && + !tryParsePrintInputOffsetStored(rawText) && + isLikelyResolvedDateTimeLiteral(rawText) + ) { + return applyElementPrefix(cfg, rawText); + } + return resolveFromOffsetAmount(el, 0, "Days", base); } return null; } const off = readOffsetFromElementConfig(el); if (off) return resolveFromOffsetAmount(el, off.amount, off.unit, base); + if ( + rawText && + !tryParsePrintInputOffsetStored(rawText) && + isLikelyResolvedDateTimeLiteral(rawText) && + isDateTimeLiveResolveField(el) + ) { + return applyElementPrefix(cfg, rawText); + } return resolveFromOffsetAmount(el, 0, "Days", base); } diff --git a/美国版/Food Labeling Management Platform/src/services/labelService.ts b/美国版/Food Labeling Management Platform/src/services/labelService.ts index 63e0357..bc0b094 100644 --- a/美国版/Food Labeling Management Platform/src/services/labelService.ts +++ b/美国版/Food Labeling Management Platform/src/services/labelService.ts @@ -37,9 +37,23 @@ export function normalizeLabelDto(raw: unknown): LabelDto { ? "SPECIFIED" : null; const regionRaw = r.region ?? r.Region; + const locationIds = parseIdList(r.locationIds ?? r.LocationIds) ?? []; + const locationIdRaw = r.locationId ?? r.LocationId; + const locationId = + locationIdRaw != null && String(locationIdRaw).trim() + ? String(locationIdRaw).trim() + : locationIds[0] ?? null; + const mergedLocationIds = [ + ...new Set([ + ...(locationId ? [locationId] : []), + ...locationIds, + ]), + ]; return { ...(r as object), id: String(r.id ?? r.Id ?? r.labelCode ?? r.LabelCode ?? ""), + locationId, + locationIds: mergedLocationIds.length ? mergedLocationIds : locationIds, regionIds: mergedRegionIds.length ? mergedRegionIds : regionIds, groupIds: mergedRegionIds.length ? mergedRegionIds : groupIds, appliedRegionType: appliedRegionType as LabelDto["appliedRegionType"], @@ -47,6 +61,18 @@ export function normalizeLabelDto(raw: unknown): LabelDto { } as LabelDto; } +function locationBodyFromInput(input: LabelCreateInput | LabelUpdateInput): Record { + const locationIds = [ + ...new Set((input.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean)), + ]; + const body: Record = {}; + if (locationIds.length) { + body.locationIds = locationIds; + body.locationId = locationIds[0]; + } + return body; +} + function scopeBodyFromInput(input: LabelCreateInput | LabelUpdateInput): Record { const regionIds = [ ...new Set( @@ -118,7 +144,7 @@ export async function createLabel(input: LabelCreateInput): Promise { labelCode: String(input.labelCode ?? "").trim() || null, labelName: input.labelName, templateCode: input.templateCode, - locationId: input.locationId, + ...locationBodyFromInput(input), labelCategoryId: input.labelCategoryId, labelTypeId: input.labelTypeId, productIds: input.productIds, @@ -137,7 +163,7 @@ export async function updateLabel(labelCode: string, input: LabelUpdateInput): P body: { labelName: input.labelName, templateCode: input.templateCode, - locationId: input.locationId, + ...locationBodyFromInput(input), labelCategoryId: input.labelCategoryId, labelTypeId: input.labelTypeId, productIds: input.productIds, diff --git a/美国版/Food Labeling Management Platform/src/types/label.ts b/美国版/Food Labeling Management Platform/src/types/label.ts index 1590a1e..4176626 100644 --- a/美国版/Food Labeling Management Platform/src/types/label.ts +++ b/美国版/Food Labeling Management Platform/src/types/label.ts @@ -4,6 +4,8 @@ export type LabelDto = { labelName?: string | null; templateCode?: string | null; locationId?: string | null; + /** 详情/编辑:适用门店 Id 列表(与后端 LocationIds 对齐) */ + locationIds?: string[] | null; labelCategoryId?: string | null; labelTypeId?: string | null; productIds?: string[] | null; @@ -61,7 +63,8 @@ export type LabelCreateInput = { labelCode?: string | null; labelName: string; templateCode: string; - locationId: string; + /** 适用门店(至少 1 个;Select All 时为当前 Region 下全部门店 Id) */ + locationIds: string[]; labelCategoryId: string; labelTypeId: string; productIds: string[]; // 至少 1 个 @@ -75,7 +78,8 @@ export type LabelCreateInput = { export type LabelUpdateInput = { labelName: string; templateCode: string; - locationId: string; + /** 适用门店(至少 1 个) */ + locationIds: string[]; labelCategoryId: string; labelTypeId: string; productIds: string[]; // 至少 1 个 -- libgit2 0.21.4