import React from "react"; import { Input } from "../ui/input"; import { Label } from "../ui/label"; import type { LabelElement } from "../../types/labelTemplate"; import { NUTRITION_FACTS_LAYOUT_ROWS } from "../../lib/nutritionFactsLayout"; import { listNutritionManualFieldSpecs } from "../../lib/nutritionManualEntry"; function pick(values: Record, key: string): string { return String(values[key] ?? ""); } type NutrientRowDef = { key: string; label: string; indent?: boolean; }; function nutrientRowsForElement(el: LabelElement): NutrientRowDef[] { const cfg = (el.config ?? {}) as Record; const fixed = Array.isArray(cfg.fixedNutrients) ? (cfg.fixedNutrients as Record[]) : []; const layoutByKey = new Map(NUTRITION_FACTS_LAYOUT_ROWS.map((r) => [r.key, r])); const rows: NutrientRowDef[] = NUTRITION_FACTS_LAYOUT_ROWS.map((r) => ({ key: r.key, label: r.label, indent: r.indent, })); for (const row of fixed) { const key = String(row.key ?? "").trim(); if (!key || layoutByKey.has(key)) { if (key && layoutByKey.has(key) && row.label) { const idx = rows.findIndex((x) => x.key === key); if (idx >= 0) rows[idx] = { ...rows[idx], label: String(row.label) }; } continue; } rows.push({ key, label: String(row.label ?? key), indent: false, }); } return rows; } function extraNutrientIds(el: LabelElement): Array<{ id: string; name: string }> { const specs = listNutritionManualFieldSpecs(el); const out: Array<{ id: string; name: 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); if (out.some((x) => x.id === id)) continue; out.push({ id, name: s.columnLabel.replace(/ \(amount\)$/, "") }); } return out; } /** 分组录入:顶部 Servings / Serve size / Calories + 每行「含量 + %DV」(< 前缀在模板编辑器配置) */ export function NutritionManualEntryForm({ element, values, onFieldChange, className, }: { element: LabelElement; values: Record; onFieldChange: (subKey: string, next: string) => void; className?: string; }) { const nutrientRows = nutrientRowsForElement(element); const extras = extraNutrientIds(element); return (
Top metrics
onFieldChange("servingsPerContainer", v)} placeholder="e.g. 6" /> onFieldChange("servingSize", v)} placeholder="e.g. 500g" />
onFieldChange("calories", v)} placeholder="e.g. 368" />
Nutrients
{nutrientRows.map((row) => ( onFieldChange(row.key, v)} onPercent={(v) => onFieldChange(`${row.key}Percent`, v)} /> ))} {extras.map((ex) => ( onFieldChange(`extra:${ex.id}:value`, v)} onPercent={(v) => onFieldChange(`extra:${ex.id}:percent`, v)} /> ))}
); } /** 从 element config 读取当前预览/录入值(模板编辑器画布同步用) */ export function nutritionValuesFromElementConfig(cfg: Record): Record { const out: Record = { servingsPerContainer: String(cfg.servingsPerContainer ?? cfg.ServingsPerContainer ?? ""), servingSize: String(cfg.servingSize ?? cfg.ServingSize ?? ""), calories: String(cfg.calories ?? cfg.Calories ?? ""), }; const fixed = Array.isArray(cfg.fixedNutrients) ? (cfg.fixedNutrients as Record[]) : []; for (const row of fixed) { const key = String(row.key ?? "").trim(); if (!key) continue; out[key] = String(row.value ?? cfg[key] ?? ""); out[`${key}Percent`] = String(row.dailyValuePercent ?? row.percent ?? cfg[`${key}Percent`] ?? ""); } for (const item of NUTRITION_FACTS_LAYOUT_ROWS) { if (out[item.key] === undefined) out[item.key] = String(cfg[item.key] ?? ""); if (out[`${item.key}Percent`] === undefined) { out[`${item.key}Percent`] = String(cfg[`${item.key}Percent`] ?? ""); } } const extras = Array.isArray(cfg.extraNutrients) ? cfg.extraNutrients : []; extras.forEach((item, idx) => { const row = item as Record; const id = String(row.id ?? `extra-${idx}`); out[`extra:${id}:value`] = String(row.value ?? ""); out[`extra:${id}:percent`] = String(cfg[`extra:${id}:percent`] ?? ""); }); return out; } /** 将分组录入写回 element config(模板编辑器预览;保存模板时仍会清空数值) */ export function applyNutritionValuesToElementConfig( baseCfg: Record, values: Record, ): Record { const cfg: Record = { ...baseCfg }; cfg.servingsPerContainer = pick(values, "servingsPerContainer"); cfg.servingSize = pick(values, "servingSize"); cfg.calories = pick(values, "calories"); const baseFixed = Array.isArray(baseCfg.fixedNutrients) ? (baseCfg.fixedNutrients as Record[]) : []; const keys = baseFixed.length > 0 ? baseFixed.map((r) => String(r.key ?? "").trim()).filter(Boolean) : NUTRITION_FACTS_LAYOUT_ROWS.map((r) => r.key); const fixedArr = keys.map((key) => { const baseRow = baseFixed.find((r) => String(r.key ?? "").trim() === key); const layout = NUTRITION_FACTS_LAYOUT_ROWS.find((r) => r.key === key); const unit = String(baseRow?.unit ?? layout?.defaultUnit ?? ""); const label = String(baseRow?.label ?? layout?.label ?? key); const value = pick(values, key); const dailyValuePercent = pick(values, `${key}Percent`); const lessThan = Boolean(baseRow?.lessThan ?? baseCfg[`${key}LessThan`]); if (value) cfg[key] = value; else delete cfg[key]; delete cfg[`${key}Unit`]; cfg[`${key}Percent`] = dailyValuePercent; cfg[`${key}LessThan`] = lessThan; return { key, label, value, unit, dailyValuePercent, lessThan }; }); cfg.fixedNutrients = fixedArr; const extras = Array.isArray(baseCfg.extraNutrients) ? [...(baseCfg.extraNutrients as object[])] : []; for (const ex of extras) { const row = ex as Record; const id = String(row.id ?? ""); if (!id) continue; row.value = pick(values, `extra:${id}:value`); cfg[`extra:${id}:percent`] = pick(values, `extra:${id}:percent`); cfg[`extra:${id}:lessThan`] = Boolean(baseCfg[`extra:${id}:lessThan`]); } cfg.extraNutrients = extras; return cfg; } function FieldPair({ label, value, onChange, placeholder, }: { label: string; value: string; onChange: (v: string) => void; placeholder?: string; }) { return (
onChange(e.target.value)} placeholder={placeholder} className="h-8 text-sm" />
); } function NutrientInputRow({ label, indent, amount, percent, onAmount, onPercent, }: { label: string; indent?: boolean; amount: string; percent: string; onAmount: (v: string) => void; onPercent: (v: string) => void; }) { return (
{label} onAmount(e.target.value)} placeholder="e.g. 11g" className="h-8 text-sm text-right" /> onPercent(e.target.value)} placeholder="0%" className="h-8 text-sm text-right" />
); } export type { NutritionManualFieldSpec } from "../../lib/nutritionManualEntry";