import type { SystemLabelTemplate, SystemTemplateElementBase } from '../print/types/printer' import { resolveTemplateDefaultValueForElement } from './printInputOffset' import { applyNutritionDefaultJsonToConfig } from './nutritionDefaultsMerge' function asRecord(v: unknown): Record { if (v != null && typeof v === 'object' && !Array.isArray(v)) return v as Record return {} } function normalizeConfig(raw: unknown): Record { if (raw == null) return {} if (typeof raw === 'string') { const t = raw.trim() if (!t) return {} try { const parsed = JSON.parse(t) as unknown if (parsed != null && typeof parsed === 'object' && !Array.isArray(parsed)) { return { ...(parsed as Record) } } } catch { return {} } return {} } const o = asRecord(raw) return { ...o } } /** * 落库/列表返回的 element 可能把 text、src、fontSize 等摊在根上,仅 `config` 为空或不全; * 合并进 config 后打印与预览才能读到样式与图片地址。 */ function mergeFlatElementFieldsIntoConfig( e: Record, cfg: Record, ): Record { const out = { ...cfg } const keys = [ 'text', 'Text', 'prefix', 'Prefix', 'suffix', 'Suffix', 'fontSize', 'FontSize', 'textAlign', 'TextAlign', 'fontFamily', 'fontWeight', 'color', 'Color', 'src', 'Src', 'url', 'Url', 'data', 'Data', 'value', 'Value', 'unit', 'Unit', 'format', 'Format', 'decimal', 'Decimal', 'inputType', 'InputType', 'offsetDays', 'OffsetDays', 'multipleOptionId', 'MultipleOptionId', 'multipleOptionName', 'MultipleOptionName', 'selectedOptionValues', 'SelectedOptionValues', 'errorLevel', 'scaleMode', 'showText', 'placeholder', 'Placeholder', ] as const for (const k of keys) { const existing = out[k] const has = existing !== undefined && existing !== null && !(typeof existing === 'string' && String(existing).trim() === '') if (has) continue const v = e[k] if (v !== undefined && v !== null && !(typeof v === 'string' && String(v).trim() === '')) { out[k] = v as unknown } } return out } const TEXT_PRODUCT_PLACEHOLDERS = new Set(['', '文本', 'text', 'Text', 'TEXT', 'Label', 'label']) /** * Web 端设计器里 TEXT_PRODUCT 若为 FIXED 且占位「文本」,预览页用当前商品名覆盖(与列表传入的 productName 一致)。 */ export function overlayProductNameOnPreviewTemplate( template: SystemLabelTemplate, productName: string | undefined ): SystemLabelTemplate { const name = (productName ?? '').trim() if (!name) return template const elements = (template.elements || []).map((el) => { const type = String(el.type || '').toUpperCase() if (type !== 'TEXT_PRODUCT') return el const raw = String((el.config as any)?.text ?? (el.config as any)?.Text ?? '').trim() if (raw && !TEXT_PRODUCT_PLACEHOLDERS.has(raw)) return el return { ...el, config: { ...(el.config || {}), text: name }, } }) return { ...template, elements } } /** * 解析接口 `labelSizeText`(如 `2"x2"`、`6.00x4.00cm`、`2.00*2.00"`)。 * 无单位后缀时默认 **inch**(与历史 `2"x2"` 一致)。 */ export function parseLabelSizeText( raw: string | null | undefined ): { width: number; height: number; unit: 'inch' | 'mm' | 'cm' | 'px' } | null { if (raw == null) return null let s = String(raw).trim() if (!s) return null s = s.replace(/["'\u201C\u201D\u2018\u2019\u2032\u2033]/g, '').replace(/\s+/g, '').toLowerCase() const m = s.match(/^(\d+(?:\.\d+)?)[*×x](\d+(?:\.\d+)?)(mm|cm|inch|in|px)?$/i) if (!m) return null const w = Number(m[1]) const h = Number(m[2]) if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) return null const suf = (m[3] || '').toLowerCase() let unit: 'inch' | 'mm' | 'cm' | 'px' = 'inch' if (suf === 'mm') unit = 'mm' else if (suf === 'cm') unit = 'cm' else if (suf === 'px') unit = 'px' else if (suf === 'in' || suf === 'inch') unit = 'inch' return { width: w, height: h, unit } } /** * 用 `labelSizeText` 覆盖模板物理宽高(解析失败则保持原模板)。 * 注意:App 预览画布应以接口 **template.width / height / unit** 为准(见 preview 页),勿与本函数同时叠加强制覆盖。 */ export function applyLabelSizeTextToTemplate( template: SystemLabelTemplate, labelSizeText: string | null | undefined ): SystemLabelTemplate { const parsed = parseLabelSizeText(labelSizeText) if (!parsed) return template return { ...template, unit: parsed.unit, width: parsed.width, height: parsed.height, } } /** * 从预览接口响应中取出 `templateProductDefaultValues`(elementId → 字符串,兼容 PascalCase / 嵌套 data)。 */ export function extractTemplateProductDefaultValuesFromPreviewPayload(payload: unknown): Record { const tryLayer = (layer: unknown): Record | null => { if (layer == null || typeof layer !== 'object' || Array.isArray(layer)) return null const L = layer as Record const raw = L.templateProductDefaultValues ?? L.TemplateProductDefaultValues if (raw == null || typeof raw !== 'object' || Array.isArray(raw)) return null const out: Record = {} 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) } return Object.keys(out).length ? out : null } const unwrapTemplateNode = (raw: unknown): Record | null => { if (raw == null) return null if (typeof raw === 'string') { const s = raw.trim() if (!s) return null try { const p = JSON.parse(s) as unknown if (p != null && typeof p === 'object' && !Array.isArray(p)) return p as Record } catch { return null } return null } if (typeof raw === 'object' && !Array.isArray(raw)) return raw as Record return null } /** 多来源合并:后者覆盖前者,便于「嵌套在 template 内」与「与 template 平级」同时存在时取全。 */ const mergeMaps = (...maps: (Record | null | undefined)[]): Record => { const out: Record = {} for (const m of maps) { if (!m) continue for (const [k, v] of Object.entries(m)) { const key = String(k).trim() if (!key) continue out[key] = v } } return out } if (payload == null || typeof payload !== 'object') return {} const r = payload as Record const nested = r.data ?? r.Data const inner = nested != null && typeof nested === 'object' && !Array.isArray(nested) ? (nested as Record) : null const innerTpl = inner ? unwrapTemplateNode(inner.template ?? inner.Template) : null const rootTpl = unwrapTemplateNode(r.template ?? r.Template) return mergeMaps( innerTpl ? tryLayer(innerTpl) : null, rootTpl ? tryLayer(rootTpl) : null, inner ? tryLayer(inner) : null, tryLayer(r), tryLayer(payload), ) } function isTemplateSectionScanElement(el: SystemTemplateElementBase): boolean { const cfg = el.config || {} const typeAdd = String( (el as { typeAdd?: string }).typeAdd ?? cfg.typeAdd ?? cfg.TypeAdd ?? el.type ?? '', ) .trim() .toLowerCase() if (!typeAdd.startsWith('template_')) return false const t = String(el.type || '').toUpperCase() return t === 'BARCODE' || t === 'QRCODE' } /** 预览/打印接口响应中的产品 codeValue(兼容 PascalCase / data 嵌套) */ export function extractProductCodeValueFromPreviewPayload(payload: unknown): string { const readLayer = (layer: Record | null | undefined): string => { if (!layer) return '' const cv = layer.codeValue ?? layer.CodeValue ?? layer.productCodeValue ?? layer.ProductCodeValue if (cv != null && String(cv).trim()) return String(cv).trim() const prod = layer.product ?? layer.Product if (prod != null && typeof prod === 'object' && !Array.isArray(prod)) { const p = prod as Record const nested = p.codeValue ?? p.CodeValue if (nested != null && String(nested).trim()) return String(nested).trim() } return '' } if (payload == null || typeof payload !== 'object') return '' const r = payload as Record const nested = r.data ?? r.Data const inner = nested != null && typeof nested === 'object' && !Array.isArray(nested) ? (nested as Record) : null return ( readLayer(inner) || readLayer(r) || '' ) } /** 将产品 codeValue 写入 Template 分组下的 BARCODE / QRCODE(覆盖模板内静态 data) */ export function applyProductCodeValueToTemplateScanElements( template: SystemLabelTemplate, codeValue: string | null | undefined, ): SystemLabelTemplate { const cv = String(codeValue ?? '').trim() if (!cv) return template const elements = (template.elements || []).map((el) => { if (!isTemplateSectionScanElement(el)) return el const cfg = { ...(el.config || {}) } as Record cfg.data = cv cfg.Data = cv if (String(el.type || '').toUpperCase() === 'BARCODE') { cfg.barcodeData = cv cfg.BarcodeData = cv cfg.value = cv cfg.Value = cv cfg.content = cv cfg.Content = cv } return { ...el, config: cfg } }) return { ...template, elements } } /** * 将平台录入的默认值合并进模板元素 config,供画布预览(键与 elements[].id 一致)。 */ export function applyTemplateProductDefaultValuesToTemplate( template: SystemLabelTemplate, defaults: Record ): SystemLabelTemplate { 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) if (v === undefined) return el const type = String(el.type || '').toUpperCase() const cfg = { ...(el.config || {}) } as Record if (type === 'IMAGE' || type === 'LOGO') { cfg.src = v cfg.url = v cfg.Src = v cfg.Url = v return { ...el, config: cfg } } if (type === 'BARCODE' || type === 'QRCODE') { cfg.data = v cfg.Data = v if (type === 'BARCODE') { cfg.barcodeData = v cfg.BarcodeData = v cfg.value = v cfg.Value = v cfg.content = v cfg.Content = v } return { ...el, config: cfg } } if (type === 'WEIGHT') { cfg.value = v cfg.Value = v cfg.text = v cfg.Text = v return { ...el, config: cfg } } if (type === 'DATE' || type === 'TIME' || type === 'DURATION') { const text = resolveTemplateDefaultValueForElement(el, v, new Date()) cfg.text = text cfg.Text = text return { ...el, config: cfg } } if (type === 'NUTRITION') { const s = String(v).trim() if (s.startsWith('{')) { const merged = applyNutritionDefaultJsonToConfig(cfg, s) return { ...el, config: merged } } cfg.text = s cfg.Text = s return { ...el, config: cfg } } cfg.text = v cfg.Text = v return { ...el, config: cfg } }) return { ...template, elements } } function elementArrayLength (o: Record): number { const a = o.elements ?? o.Elements return Array.isArray(a) ? a.length : 0 } /** * 列表 `renderTemplateJson` / 接口 9 落库可能是:① 根上直接 elements;② 包在 `printInputJson`;③ 包在 `template`(对象或 JSON 字符串); * ④ 根上既有 `template: {}` 又有 `elements` 时,**不能**误用空 template 丢掉根级 elements(会导致重打无字、无图)。 */ function pickTemplateRootRecord (payload: Record): Record { const unwrapTemplateKey = (raw: unknown): Record | null => { if (raw == null) return null if (typeof raw === 'string') { const s = raw.trim() if (!s) return null try { const p = JSON.parse(s) as unknown if (p != null && typeof p === 'object' && !Array.isArray(p)) return p as Record } catch { return null } return null } if (typeof raw === 'object' && !Array.isArray(raw)) return raw as Record return null } if (elementArrayLength(payload) > 0) return payload const pi = payload.printInputJson ?? payload.PrintInputJson if (pi != null && typeof pi === 'object' && !Array.isArray(pi)) { const p = pi as Record if (elementArrayLength(p) > 0) return p } const nested = unwrapTemplateKey(payload.template ?? payload.Template) ?? asRecord(payload.template ?? payload.Template) if (elementArrayLength(nested) > 0) return nested if (pi != null && typeof pi === 'object' && !Array.isArray(pi)) return pi as Record if (Object.keys(nested).length > 0) return nested return payload } /** * 将接口 8.2 返回的 template(或整段 DTO)规范为 SystemLabelTemplate,供打印适配器与预览画布使用。 */ export function normalizeLabelTemplateFromPreviewApi(payload: unknown): SystemLabelTemplate | null { if (payload == null || typeof payload !== 'object') return null let root = payload as Record const wrapped = root.data ?? root.Data if (wrapped != null && typeof wrapped === 'object' && !Array.isArray(wrapped)) { root = wrapped as Record } const t = pickTemplateRootRecord(root) const elementsRaw = t.elements ?? t.Elements if (!Array.isArray(elementsRaw)) return null const elements: SystemTemplateElementBase[] = (elementsRaw as unknown[]).map((el, index) => { const e = asRecord(el) let cfg = normalizeConfig( e.config ?? e.Config ?? e.ConfigJson ?? e.configJson ?? e.ConfigString, ) cfg = mergeFlatElementFieldsIntoConfig(e, cfg) const type = String(e.type ?? e.elementType ?? e.ElementType ?? 'TEXT_STATIC') const vst = e.valueSourceType ?? e.ValueSourceType const ik = e.inputKey ?? e.InputKey const en = e.elementName ?? e.ElementName return { id: String(e.id ?? e.Id ?? `el-${index}`), type, x: Number(e.x ?? e.posX ?? e.PosX ?? 0), y: Number(e.y ?? e.posY ?? e.PosY ?? 0), width: Number(e.width ?? e.Width ?? 0), height: Number(e.height ?? e.Height ?? 0), rotation: String(e.rotation ?? e.Rotation ?? 'horizontal') as 'horizontal' | 'vertical', border: String(e.border ?? e.BorderType ?? e.borderType ?? 'none'), config: cfg as Record, zIndex: Number(e.zIndex ?? e.ZIndex ?? 0), orderNum: Number(e.orderNum ?? e.OrderNum ?? index), valueSourceType: vst != null ? String(vst) : undefined, inputKey: ik != null && String(ik).trim() !== '' ? String(ik).trim() : undefined, elementName: en != null && String(en).trim() !== '' ? String(en).trim() : undefined, } as SystemTemplateElementBase & { zIndex: number; orderNum: number } }) const unitRaw = String(t.unit ?? t.Unit ?? 'inch').toLowerCase() const unit = (unitRaw === 'mm' || unitRaw === 'cm' || unitRaw === 'px' ? unitRaw : 'inch') as | 'inch' | 'mm' | 'cm' | 'px' return { id: String(t.id ?? t.Id ?? 'preview'), name: String(t.name ?? t.Name ?? 'Label'), labelType: String(t.labelType ?? t.LabelType ?? ''), unit, width: Number(t.width ?? t.Width ?? 2), height: Number(t.height ?? t.Height ?? 2), appliedLocation: String(t.appliedLocation ?? t.AppliedLocation ?? 'ALL'), showRuler: !!(t.showRuler ?? t.ShowRuler), showGrid: !!(t.showGrid ?? t.ShowGrid), elements, } } export function sortElementsForPreview( elements: SystemTemplateElementBase[] ): SystemTemplateElementBase[] { return [...elements].sort((a, b) => { const za = Number((a as any).zIndex ?? 0) const zb = Number((b as any).zIndex ?? 0) if (za !== zb) return za - zb const oa = Number((a as any).orderNum ?? 0) const ob = Number((b as any).orderNum ?? 0) return oa - ob }) }