Blame view

美国版/Food Labeling Management App UniApp/src/utils/labelPreview/nutritionDefaultsMerge.ts 8.93 KB
63289723   杨鑫   提交
1
2
  /**
   * 将管理端保存的营养成分默认值 JSON 合并进 NUTRITION 元素 config(与 Web nutritionManualEntry 字段一致)。
20554770   杨鑫   更新bug
3
   * `<` 前缀由模板 config 决定,录入 JSON 不包含 LessThan 字段。
63289723   杨鑫   提交
4
   */
c9bc327f   杨鑫   修改前端
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
  import type { SystemLabelTemplate, SystemTemplateElementBase } from '../print/types/printer'
  import { NUTRITION_FACTS_LAYOUT_ROWS, buildNutritionFactsViewModel, NUTRITION_BODY_FONT_SIZE } from '../nutritionFactsLayout'
  
  /** 与 Web nutritionManualEntry.NUTRITION_FIELD_COMPOSITE_SEP 一致 */
  export const NUTRITION_FIELD_COMPOSITE_SEP = '###nut###'
  
  export function nutritionCompositeFieldKey(nutritionElementId: string, subKey: string): string {
    return `${nutritionElementId}${NUTRITION_FIELD_COMPOSITE_SEP}${subKey}`
  }
  
  function defaultLookupCandidates(el: SystemTemplateElementBase): string[] {
    return [
      String(el.id ?? '').trim(),
      String(el.inputKey ?? '').trim(),
      String(el.elementName ?? '').trim(),
    ].filter(Boolean)
  }
  
  /** 从 templateProductDefaultValues 解析营养成分手动录入(JSON 或 composite 列键) */
  export function collectNutritionManualFromDefaults(
    el: SystemTemplateElementBase,
    defaults: Record<string, string>,
  ): Record<string, string> {
    if (!defaults || !Object.keys(defaults).length) return {}
    const candidates = defaultLookupCandidates(el)
    for (const k of candidates) {
      if (Object.prototype.hasOwnProperty.call(defaults, k)) {
        const raw = String(defaults[k] ?? '').trim()
        if (raw.startsWith('{')) {
          try {
            const p = JSON.parse(raw) as Record<string, string>
            if (p && typeof p === 'object' && !Array.isArray(p)) return p
          } catch {
            /* ignore */
          }
        }
      }
    }
    const lower = Object.entries(defaults).map(([k, v]) => [k.toLowerCase(), v] as const)
    for (const k of candidates) {
      const hit = lower.find(([dk]) => dk === k.toLowerCase())
      if (hit) {
        const raw = String(hit[1] ?? '').trim()
        if (raw.startsWith('{')) {
          try {
            return JSON.parse(raw) as Record<string, string>
          } catch {
            /* ignore */
          }
        }
      }
    }
    const id = String(el.id ?? '').trim()
    if (!id) return {}
    const prefix = `${id}${NUTRITION_FIELD_COMPOSITE_SEP}`
    const manual: Record<string, string> = {}
    for (const [k, v] of Object.entries(defaults)) {
      if (k.startsWith(prefix)) manual[k.slice(prefix.length)] = String(v ?? '')
    }
    return manual
  }
  
  function parseNutritionJsonFromConfigText(cfg: Record<string, unknown>): string | null {
    const text = String(cfg.text ?? cfg.Text ?? '').trim()
    return text.startsWith('{') ? text : null
  }
  
  /** 接口 8.2 会把营养成分 JSON 误写入 config.text;解析进 fixedNutrients / calories 等字段 */
  export function hydrateNutritionElementsInTemplate(
    template: SystemLabelTemplate,
    defaults: Record<string, string> = {},
  ): SystemLabelTemplate {
    const elements = (template.elements || []).map((el) => {
      if (String(el.type || '').toUpperCase() !== 'NUTRITION') return el
      let cfg = { ...(el.config || {}) } as Record<string, unknown>
      const fromText = parseNutritionJsonFromConfigText(cfg)
      if (fromText) {
        cfg = applyNutritionDefaultJsonToConfig(cfg, fromText)
      }
      const manual = collectNutritionManualFromDefaults(el, defaults)
      if (Object.keys(manual).some((k) => String(manual[k] ?? '').trim() !== '')) {
        cfg = applyNutritionDefaultJsonToConfig(cfg, JSON.stringify(manual))
      }
      if (fromText) {
        delete cfg.text
        delete cfg.Text
      }
      return { ...el, config: cfg }
    })
    return { ...template, elements }
  }
  
  /** 按实际行数估算营养表所需高度,避免矮框裁掉 Vitamin D / 页脚等行 */
  export function estimateNutritionPanelHeightPx(cfg: Record<string, unknown>): number {
    const model = buildNutritionFactsViewModel(cfg)
    const bodySize = NUTRITION_BODY_FONT_SIZE
    const titleSize = Math.max(11, Math.min(18, model.titleFontSize))
    const lh = (fs: number) => fs + 4
    let y = 12
    y += titleSize + 6
    y += lh(bodySize) + 3 + lh(bodySize) + 5
    y += bodySize + 8
    for (const row of model.rows) {
      y += lh(bodySize)
      if (row.dividerAfter === 'double') y += 5
      else if (row.dividerAfter === 'thin') y += 3
    }
    y += 24
    return Math.ceil(y)
  }
  
  /**
   * 仅当营养表增高时,把「原底边以下」的元素整体下移相同 delta,保持同行左右布局不变。
   * 禁止按 cursor 重排,避免 Consume By 标签/日期、ALLERGENS 等被拆散叠在一起。
   */
  export function reflowElementsBelowNutritionPanel(
    template: SystemLabelTemplate,
    previousNutritionBottom?: number,
  ): SystemLabelTemplate {
    const elements = template.elements || []
    const nutrition = elements.find((el) => String(el.type || '').toUpperCase() === 'NUTRITION')
    if (!nutrition) return template
  
    const nutY = Number(nutrition.y) || 0
    const nutBottom = nutY + (Number(nutrition.height) || 0)
    const oldBottom = previousNutritionBottom ?? nutBottom
    const delta = nutBottom - oldBottom
    if (delta <= 1) return template
  
    const threshold = oldBottom - 2
    const next = elements.map((el) => {
      if (el.id === nutrition.id) return el
      const y = Number(el.y) || 0
      if (y >= threshold) return { ...el, y: y + delta }
      return el
    })
    return { ...template, elements: next }
  }
  
  /** 仅在内容确实超出模板框时微增高度,并整体下移其下元素(不重排同行) */
  export function expandNutritionElementsToFitContent(template: SystemLabelTemplate): SystemLabelTemplate {
    const elements = [...(template.elements || [])]
    const nutIdx = elements.findIndex((el) => String(el.type || '').toUpperCase() === 'NUTRITION')
    if (nutIdx < 0) return template
  
    const nutrition = elements[nutIdx]
    const nutY = Number(nutrition.y) || 0
    const oldH = Number(nutrition.height) || 0
    const oldBottom = nutY + oldH
    const need = estimateNutritionPanelHeightPx((nutrition.config || {}) as Record<string, unknown>)
    const maxH = Math.max(oldH, need)
    if (maxH <= oldH + 2) return template
  
    elements[nutIdx] = { ...nutrition, height: maxH }
    return reflowElementsBelowNutritionPanel({ ...template, elements }, oldBottom)
  }
20554770   杨鑫   更新bug
161
162
163
164
165
166
167
168
169
170
  
  function fixedLabelForKey(key: string): string {
    const hit = NUTRITION_FACTS_LAYOUT_ROWS.find((x) => x.key === key)
    return hit?.label ?? key
  }
  
  function pickManual(manual: Record<string, string>, subKey: string): string {
    return String(manual[subKey] ?? '').trim()
  }
  
63289723   杨鑫   提交
171
172
173
174
  export function applyNutritionDefaultJsonToConfig(
    baseCfg: Record<string, unknown>,
    jsonStr: string,
  ): Record<string, unknown> {
20554770   杨鑫   更新bug
175
176
177
    const t = String(jsonStr ?? '').trim()
    if (!t.startsWith('{')) return baseCfg
    let manual: Record<string, string> = {}
63289723   杨鑫   提交
178
    try {
20554770   杨鑫   更新bug
179
      manual = JSON.parse(t) as Record<string, string>
63289723   杨鑫   提交
180
    } catch {
20554770   杨鑫   更新bug
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
      return baseCfg
    }
  
    const out: Record<string, unknown> = { ...baseCfg }
    const baseFixed = Array.isArray(baseCfg.fixedNutrients)
      ? (baseCfg.fixedNutrients as Record<string, unknown>[])
      : []
  
    const cal = pickManual(manual, 'calories')
    if (cal) out.calories = cal
    else {
      delete out.calories
      delete out.Calories
    }
    out.servingsPerContainer = pickManual(manual, 'servingsPerContainer')
    out.servingSize = pickManual(manual, 'servingSize')
  
    const keysInOrder = [...NUTRITION_FACTS_LAYOUT_ROWS.map((r) => r.key)]
    for (const row of baseFixed) {
      const key = String(row.key ?? '').trim()
      if (key && !keysInOrder.includes(key)) keysInOrder.push(key)
    }
  
    const fixedArr: Record<string, unknown>[] = []
    for (const key of keysInOrder) {
      const baseRow = baseFixed.find((r) => String(r.key ?? '').trim() === key)
      const layout = NUTRITION_FACTS_LAYOUT_ROWS.find((r) => r.key === key)
      const v = pickManual(manual, key)
      const pct = pickManual(manual, `${key}Percent`)
      const lessThan = Boolean(baseRow?.lessThan ?? baseCfg[`${key}LessThan`])
      const label = String(baseRow?.label ?? layout?.label ?? fixedLabelForKey(key))
      fixedArr.push({
        key,
        label,
        value: v,
        unit: '',
        dailyValuePercent: pct,
        lessThan,
      })
      if (v) out[key] = v
      else delete out[key]
      delete out[`${key}Unit`]
      out[`${key}Percent`] = pct
      out[`${key}LessThan`] = lessThan
63289723   杨鑫   提交
225
    }
20554770   杨鑫   更新bug
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
    out.fixedNutrients = fixedArr
  
    const newExtras: Array<{ id: string; name: string; value: string; unit: string }> = []
    for (const k of Object.keys(manual)) {
      if (!k.startsWith('extra:') || !k.endsWith(':value')) continue
      const id = k.slice('extra:'.length, -':value'.length)
      newExtras.push({
        id,
        name: String(
          (Array.isArray(out.extraNutrients)
            ? (out.extraNutrients as Record<string, unknown>[]).find((row) => String(row.id ?? '') === id)
            : undefined)?.name ?? 'Other',
        ),
        value: pickManual(manual, k),
        unit: '',
      })
      out[`extra:${id}:percent`] = pickManual(manual, `extra:${id}:percent`)
      out[`extra:${id}:lessThan`] = Boolean(baseCfg[`extra:${id}:lessThan`])
63289723   杨鑫   提交
244
    }
20554770   杨鑫   更新bug
245
246
247
248
249
    if (newExtras.length > 0) out.extraNutrients = newExtras
  
    delete out.ingredientsText
    delete out.IngredientsText
    return out
63289723   杨鑫   提交
250
  }