63289723
杨鑫
提交
|
1
2
|
import type { LabelElement } from "../types/labelTemplate";
import { canonicalElementType, NUTRITION_FIXED_ITEMS } from "../types/labelTemplate";
|
20554770
杨鑫
更新bug
|
3
|
import { NUTRITION_FACTS_LAYOUT_ROWS, DEFAULT_NUTRITION_FOOTER_NOTE } from "./nutritionFactsLayout";
|
63289723
杨鑫
提交
|
4
5
6
7
8
9
10
11
12
13
14
|
/** 批量表 / 与 elementId 拼接的字段名分隔(避免与普通 element id 冲突) */
export const NUTRITION_FIELD_COMPOSITE_SEP = "###nut###";
export function nutritionCompositeFieldKey(nutritionElementId: string, subKey: string): string {
return `${nutritionElementId}${NUTRITION_FIELD_COMPOSITE_SEP}${subKey}`;
}
export type NutritionManualFieldSpec = {
subKey: string;
columnLabel: string;
|
20554770
杨鑫
更新bug
|
15
|
inputType?: "text" | "checkbox";
|
63289723
杨鑫
提交
|
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
};
function nutritionExtraRowsFromCfg(cfg: Record<string, unknown>): Array<{
id: string;
name: string;
value: string;
unit: string;
}> {
const raw = cfg.extraNutrients;
if (!Array.isArray(raw)) return [];
return raw.map((item, idx) => {
const row = item as Record<string, unknown>;
return {
id: String(row.id ?? `extra-${idx}`),
name: String(row.name ?? ""),
value: String(row.value ?? ""),
unit: String(row.unit ?? ""),
};
});
}
function fixedLabelForKey(key: string): string {
|
20554770
杨鑫
更新bug
|
38
|
const hit = NUTRITION_FACTS_LAYOUT_ROWS.find((x) => x.key === key) ?? NUTRITION_FIXED_ITEMS.find((x) => x.key === key);
|
63289723
杨鑫
提交
|
39
40
41
|
return hit?.label ?? key;
}
|
63289723
杨鑫
提交
|
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
function fakeNutritionElement(cfg: Record<string, unknown>): LabelElement {
return {
id: "__nutrition_cfg__",
type: "NUTRITION",
x: 0,
y: 0,
width: 1,
height: 1,
rotation: "horizontal",
border: "none",
config: cfg,
} as LabelElement;
}
/**
|
20554770
杨鑫
更新bug
|
57
58
|
* 模板中每个 NUTRITION 元素在「录入 / 批量表」中展开的列。
* 每行营养素固定两列录入:amount(含量含单位,自行填写如 11g)+ %DV。
|
63289723
杨鑫
提交
|
59
60
61
62
|
*/
export function listNutritionManualFieldSpecs(el: LabelElement): NutritionManualFieldSpec[] {
if (canonicalElementType(el.type) !== "NUTRITION") return [];
const cfg = (el.config ?? {}) as Record<string, unknown>;
|
4cb354d4
杨鑫
提交
|
63
|
const specs: NutritionManualFieldSpec[] = [
|
20554770
杨鑫
更新bug
|
64
65
|
{ subKey: "servingsPerContainer", columnLabel: "Servings" },
{ subKey: "servingSize", columnLabel: "Serve size" },
|
4cb354d4
杨鑫
提交
|
66
67
|
{ subKey: "calories", columnLabel: "Calories" },
];
|
63289723
杨鑫
提交
|
68
69
|
const fixedArr = Array.isArray(cfg.fixedNutrients) ? (cfg.fixedNutrients as Record<string, unknown>[]) : [];
|
20554770
杨鑫
更新bug
|
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
|
const labelByKey = new Map<string, string>();
for (const item of NUTRITION_FACTS_LAYOUT_ROWS) {
labelByKey.set(item.key, item.label);
}
for (const row of fixedArr) {
const key = String(row.key ?? "").trim();
if (!key) continue;
if (!labelByKey.has(key)) labelByKey.set(key, String(row.label ?? key));
else if (row.label) labelByKey.set(key, String(row.label));
}
const keysInOrder: string[] = NUTRITION_FACTS_LAYOUT_ROWS.map((r) => r.key);
for (const row of fixedArr) {
const key = String(row.key ?? "").trim();
if (key && !keysInOrder.includes(key)) keysInOrder.push(key);
}
for (const key of keysInOrder) {
const label = labelByKey.get(key) ?? fixedLabelForKey(key);
specs.push({ subKey: key, columnLabel: `${label} (amount)` });
specs.push({ subKey: `${key}Percent`, columnLabel: `${label} (% DV)` });
|
63289723
杨鑫
提交
|
91
92
93
94
95
|
}
for (const ex of nutritionExtraRowsFromCfg(cfg)) {
const id = String(ex.id ?? "").trim();
if (!id) continue;
|
63289723
杨鑫
提交
|
96
|
const name = ex.name.trim() || "Other";
|
20554770
杨鑫
更新bug
|
97
98
|
specs.push({ subKey: `extra:${id}:value`, columnLabel: `${name} (amount)` });
specs.push({ subKey: `extra:${id}:percent`, columnLabel: `${name} (% DV)` });
|
63289723
杨鑫
提交
|
99
|
}
|
20554770
杨鑫
更新bug
|
100
|
|
63289723
杨鑫
提交
|
101
102
103
104
105
106
107
|
return specs;
}
export function listNutritionElements(elements: LabelElement[]): LabelElement[] {
return (elements ?? []).filter((el) => canonicalElementType(el.type) === "NUTRITION");
}
|
4cb354d4
杨鑫
提交
|
108
109
110
|
/** 从模板 config 初始化手动录入 map(模板不含数值,初始均为空) */
export function nutritionManualValuesFromTemplateConfig(_el: LabelElement): Record<string, string> {
const specs = listNutritionManualFieldSpecs(_el);
|
63289723
杨鑫
提交
|
111
|
const out: Record<string, string> = {};
|
63289723
杨鑫
提交
|
112
|
for (const s of specs) {
|
20554770
杨鑫
更新bug
|
113
|
out[s.subKey] = s.inputType === "checkbox" ? "false" : "";
|
63289723
杨鑫
提交
|
114
115
116
117
|
}
return out;
}
|
4cb354d4
杨鑫
提交
|
118
119
120
121
122
|
function pickManual(manual: Record<string, string>, subKey: string): string {
return String(manual[subKey] ?? "").trim();
}
/**
|
20554770
杨鑫
更新bug
|
123
|
* 模板编辑器持久化:仅保留表结构(单位、自定义行名称、页脚文案),清除所有展示数值。
|
4cb354d4
杨鑫
提交
|
124
125
126
127
128
129
130
131
132
133
134
|
*/
export function sanitizeNutritionTemplateConfig(
cfg: Record<string, unknown>,
): Record<string, unknown> {
const out: Record<string, unknown> = { ...cfg };
out.calories = "";
out.servingsPerContainer = "";
out.servingSize = "";
delete out.Calories;
delete out.ServingsPerContainer;
delete out.ServingSize;
|
20554770
杨鑫
更新bug
|
135
136
137
138
139
|
delete out.ingredientsText;
delete out.IngredientsText;
for (const k of Object.keys(out)) {
if (k.endsWith("Percent")) delete out[k];
}
|
4cb354d4
杨鑫
提交
|
140
141
142
143
|
const baseFixed = Array.isArray(out.fixedNutrients)
? (out.fixedNutrients as Record<string, unknown>[])
: [];
|
20554770
杨鑫
更新bug
|
144
|
const fixedArr = NUTRITION_FACTS_LAYOUT_ROWS.map((item) => {
|
4cb354d4
杨鑫
提交
|
145
146
|
const baseRow = baseFixed.find((r) => String(r.key ?? "").trim() === item.key);
const unit = String(baseRow?.unit ?? item.defaultUnit ?? "").trim();
|
20554770
杨鑫
更新bug
|
147
|
const lessThan = Boolean(baseRow?.lessThan ?? out[`${item.key}LessThan`]);
|
4cb354d4
杨鑫
提交
|
148
149
150
151
152
|
return {
key: item.key,
label: String(baseRow?.label ?? item.label),
value: "",
unit,
|
20554770
杨鑫
更新bug
|
153
154
|
dailyValuePercent: "",
lessThan,
|
4cb354d4
杨鑫
提交
|
155
156
157
158
|
};
});
out.fixedNutrients = fixedArr;
|
20554770
杨鑫
更新bug
|
159
|
for (const item of NUTRITION_FACTS_LAYOUT_ROWS) {
|
4cb354d4
杨鑫
提交
|
160
|
out[item.key] = "";
|
20554770
杨鑫
更新bug
|
161
162
|
const row = fixedArr.find((r) => r.key === item.key);
if (row?.unit) out[`${item.key}Unit`] = row.unit;
|
4cb354d4
杨鑫
提交
|
163
|
else delete out[`${item.key}Unit`];
|
20554770
杨鑫
更新bug
|
164
|
out[`${item.key}LessThan`] = Boolean(row?.lessThan);
|
63289723
杨鑫
提交
|
165
|
}
|
4cb354d4
杨鑫
提交
|
166
167
168
169
170
171
172
|
out.extraNutrients = nutritionExtraRowsFromCfg(out).map((ex) => ({
id: ex.id,
name: ex.name,
value: "",
unit: ex.unit,
}));
|
20554770
杨鑫
更新bug
|
173
174
175
176
|
if (!String(out.nutritionFooterNote ?? "").trim()) {
out.nutritionFooterNote = DEFAULT_NUTRITION_FOOTER_NOTE;
}
|
4cb354d4
杨鑫
提交
|
177
178
179
180
181
182
183
184
185
186
187
188
189
190
|
return out;
}
/** 模板编辑器:清除所有 NUTRITION 元素 config 中的展示数值 */
export function sanitizeNutritionElementsForTemplateEditor(
elements: LabelElement[],
): LabelElement[] {
return (elements ?? []).map((el) => {
if (canonicalElementType(el.type) !== "NUTRITION") return el;
return {
...el,
config: sanitizeNutritionTemplateConfig((el.config ?? {}) as Record<string, unknown>),
};
});
|
63289723
杨鑫
提交
|
191
192
193
194
|
}
/**
* 将手动录入合并进 NUTRITION 的 config(供画布预览;与 App 端 apply 逻辑字段一致)。
|
63289723
杨鑫
提交
|
195
196
197
198
199
200
201
202
203
204
|
*/
export function mergeNutritionManualIntoConfig(
baseCfg: Record<string, unknown>,
manual: Record<string, string>,
): Record<string, unknown> {
const cfg: Record<string, unknown> = { ...baseCfg };
const specs = listNutritionManualFieldSpecs(fakeNutritionElement(baseCfg));
const specSubKeys = new Set(specs.map((s) => s.subKey));
if (specSubKeys.has("calories")) {
|
4cb354d4
杨鑫
提交
|
205
|
const cal = pickManual(manual, "calories");
|
63289723
杨鑫
提交
|
206
207
208
209
210
|
if (cal) cfg.calories = cal;
else {
delete cfg.calories;
delete cfg.Calories;
}
|
63289723
杨鑫
提交
|
211
212
213
|
}
if (specSubKeys.has("servingsPerContainer")) {
|
4cb354d4
杨鑫
提交
|
214
|
cfg.servingsPerContainer = pickManual(manual, "servingsPerContainer");
|
63289723
杨鑫
提交
|
215
|
}
|
63289723
杨鑫
提交
|
216
|
if (specSubKeys.has("servingSize")) {
|
4cb354d4
杨鑫
提交
|
217
|
cfg.servingSize = pickManual(manual, "servingSize");
|
63289723
杨鑫
提交
|
218
219
220
221
222
223
|
}
const baseFixed = Array.isArray(baseCfg.fixedNutrients)
? (baseCfg.fixedNutrients as Record<string, unknown>[])
: [];
const fixedArr: Record<string, unknown>[] = [];
|
20554770
杨鑫
更新bug
|
224
|
|
63289723
杨鑫
提交
|
225
|
for (const s of specs) {
|
20554770
杨鑫
更新bug
|
226
227
228
|
if (["calories", "servingsPerContainer", "servingSize"].includes(s.subKey)) {
continue;
}
|
63289723
杨鑫
提交
|
229
|
if (s.subKey.startsWith("extra:")) continue;
|
20554770
杨鑫
更新bug
|
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
|
if (s.subKey.endsWith("Percent")) continue;
const key = s.subKey;
const v = pickManual(manual, key);
const pct = pickManual(manual, `${key}Percent`);
const baseRow = baseFixed.find((r) => String(r.key ?? "").trim() === key);
const lessThan = Boolean(baseRow?.lessThan ?? baseCfg[`${key}LessThan`]);
const label = String(baseRow?.label ?? fixedLabelForKey(key));
fixedArr.push({
key,
label,
value: v,
unit: "",
dailyValuePercent: pct,
lessThan,
});
if (v) cfg[key] = v;
else {
delete cfg[key];
delete cfg[`${key}Unit`];
|
91821909
杨鑫
最新
|
250
|
}
|
20554770
杨鑫
更新bug
|
251
252
|
cfg[`${key}Percent`] = pct;
cfg[`${key}LessThan`] = lessThan;
|
63289723
杨鑫
提交
|
253
254
255
256
257
258
259
260
261
262
263
|
}
cfg.fixedNutrients = fixedArr;
const newExtras: Array<{ id: string; name: string; value: string; unit: string }> = [];
for (const s of specs) {
if (!s.subKey.startsWith("extra:") || !s.subKey.endsWith(":value")) continue;
const id = s.subKey.slice("extra:".length, -":value".length);
const base = nutritionExtraRowsFromCfg(baseCfg).find((r) => r.id === id);
newExtras.push({
id,
name: (base?.name ?? s.columnLabel).trim() || "Other",
|
4cb354d4
杨鑫
提交
|
264
|
value: pickManual(manual, s.subKey),
|
63289723
杨鑫
提交
|
265
266
|
unit: String(base?.unit ?? "").trim(),
});
|
20554770
杨鑫
更新bug
|
267
268
|
cfg[`extra:${id}:percent`] = pickManual(manual, `extra:${id}:percent`);
cfg[`extra:${id}:lessThan`] = Boolean(baseCfg[`extra:${id}:lessThan`]);
|
63289723
杨鑫
提交
|
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
|
}
cfg.extraNutrients = newExtras;
return cfg;
}
export function serializeNutritionManualForDefaults(manual: Record<string, string>): string {
const o: Record<string, string> = {};
for (const [k, v] of Object.entries(manual)) {
const t = String(v ?? "").trim();
if (t !== "") o[k] = t;
}
return JSON.stringify(o);
}
export function parseNutritionManualFromDefaults(raw: string | undefined): Record<string, string> {
const t = String(raw ?? "").trim();
if (!t.startsWith("{")) return {};
try {
const p = JSON.parse(t) as unknown;
if (p == null || typeof p !== "object" || Array.isArray(p)) return {};
const out: Record<string, string> = {};
for (const [k, v] of Object.entries(p as Record<string, unknown>)) {
out[k] = String(v ?? "");
}
return out;
} catch {
return {};
}
}
export function nutritionDefaultValuesJsonForSave(manual: Record<string, string>): string | null {
const json = serializeNutritionManualForDefaults(manual);
return json === "{}" ? null : json;
}
/** 从接口 defaultValues 展开营养成分 JSON 为批量表 composite 列键 */
export function hydrateRowFieldValuesWithNutritionColumns(
defaultValues: Record<string, string>,
elements: LabelElement[],
): Record<string, string> {
const out = { ...defaultValues };
for (const nel of listNutritionElements(elements)) {
const raw = out[nel.id];
if (typeof raw !== "string" || !raw.trim().startsWith("{")) continue;
const parsed = parseNutritionManualFromDefaults(raw);
delete out[nel.id];
const allowed = new Set(listNutritionManualFieldSpecs(nel).map((s) => s.subKey));
for (const [sk, val] of Object.entries(parsed)) {
if (!allowed.has(sk)) continue;
out[nutritionCompositeFieldKey(nel.id, sk)] = val;
}
}
return out;
}
/** 将批量表 composite 列折叠回 defaultValues(元素 id → JSON) */
export function foldNutritionCompositeKeysIntoDefaults(
fieldValues: Record<string, string>,
elements: LabelElement[],
): Record<string, string> {
const out: Record<string, string> = { ...fieldValues };
for (const nel of listNutritionElements(elements)) {
const specs = listNutritionManualFieldSpecs(nel);
const manual: Record<string, string> = {};
for (const s of specs) {
const ck = nutritionCompositeFieldKey(nel.id, s.subKey);
if (Object.prototype.hasOwnProperty.call(fieldValues, ck)) {
manual[s.subKey] = fieldValues[ck] ?? "";
}
}
const prefix = `${nel.id}${NUTRITION_FIELD_COMPOSITE_SEP}`;
for (const k of Object.keys(out)) {
if (k.startsWith(prefix)) delete out[k];
}
const j = nutritionDefaultValuesJsonForSave(manual);
if (j) out[nel.id] = j;
else delete out[nel.id];
}
return out;
}
|