import { useEffect, useMemo, useState } from "react"; import { CheckCircle2, PlusCircle, Trash2 } from "lucide-react"; import { api } from "../api"; import type { Category, KindField, SubmissionImage, SubmissionInput } from "../types"; const NUTRI_FIELDS: { key: string; label: string }[] = [ { key: "energy_kcal", label: "能量 (kcal)" }, { key: "energy_kj", label: "能量 (kJ)" }, { key: "fat", label: "脂肪 (g)" }, { key: "saturated_fat", label: "饱和脂肪 (g)" }, { key: "carbohydrates", label: "碳水 (g)" }, { key: "sugars", label: "糖 (g)" }, { key: "proteins", label: "蛋白质 (g)" }, { key: "salt", label: "盐 (g)" }, ]; function field(v: string): string | null { const t = v.trim(); return t === "" ? null : t; } export default function Contribute({ onDone }: { onDone: () => void }) { const [categories, setCategories] = useState([]); const [done, setDone] = useState(false); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(""); const [name, setName] = useState(""); const [gtin, setGtin] = useState(""); const [brand, setBrand] = useState(""); const [categoryID, setCategoryID] = useState(""); const [netValue, setNetValue] = useState(""); const [netUnit, setNetUnit] = useState(""); const [country, setCountry] = useState(""); const [ingredients, setIngredients] = useState(""); const [basis, setBasis] = useState(""); const [nutri, setNutri] = useState>({}); const [kindFields, setKindFields] = useState([]); const [attrs, setAttrs] = useState>({}); const [images, setImages] = useState([]); const [imageURL, setImageURL] = useState(""); const [submitter, setSubmitter] = useState(""); const [contact, setContact] = useState(""); const [note, setNote] = useState(""); useEffect(() => { api.categories().then((r) => setCategories(r.items)).catch(() => undefined); }, []); // Derive the archive kind from the selected category. No category => generic. const kind = useMemo(() => { const c = categories.find((x) => x.id === categoryID); return c?.archive_kind || "generic"; }, [categories, categoryID]); // Fetch the kind-specific field template for non-food kinds. useEffect(() => { setAttrs({}); if (kind === "food" || kind === "generic") { setKindFields([]); return; } let alive = true; api .kindFields(kind) .then((r) => { if (alive) setKindFields(r.items); }) .catch(() => { if (alive) setKindFields([]); }); return () => { alive = false; }; }, [kind]); async function submit(e: React.FormEvent) { e.preventDefault(); if (name.trim() === "") { setError("请填写商品名称"); return; } setSubmitting(true); setError(""); const nutriments: Record = {}; if (kind === "food") { for (const [k, v] of Object.entries(nutri)) { const n = parseFloat(v); if (!Number.isNaN(n)) nutriments[k] = n; } } const attributes: Record = {}; if (kind !== "food" && kind !== "generic") { for (const f of kindFields) { const raw = (attrs[f.field_key] || "").trim(); if (raw === "") continue; if (f.field_type === "number") { const n = parseFloat(raw); if (!Number.isNaN(n)) attributes[f.field_key] = n; } else if (f.field_type === "list") { const parts = raw .split(/[\n,,、]/) .map((s) => s.trim()) .filter((s) => s !== ""); if (parts.length) attributes[f.field_key] = parts; } else { attributes[f.field_key] = raw; } } } const input: SubmissionInput = { name: name.trim(), gtin: field(gtin), brand_name: field(brand), category_id: categoryID || null, net_content_value: field(netValue) ? parseFloat(netValue) : null, net_content_unit: field(netUnit), country_of_origin: field(country), ingredients_text: kind === "food" ? field(ingredients) : null, nutriments: Object.keys(nutriments).length ? nutriments : null, attributes: Object.keys(attributes).length ? attributes : null, nutrition_basis: kind === "food" && basis ? basis : null, images: images.length ? images : undefined, submitter_name: field(submitter), submitter_contact: field(contact), note: field(note), }; try { await api.submit(input); setDone(true); } catch (err) { setError(err instanceof Error ? err.message : "提交失败"); } finally { setSubmitting(false); } } if (done) { return (

已提交,等待审核

感谢你的贡献!资料将由管理员人工审核,通过后会收录进公共商品库。

); } const input = "input"; const label = "block text-xs font-medium text-gray-500 mb-1"; // Group the kind fields by their group label, preserving template order. const groups: { label: string; fields: KindField[] }[] = []; for (const f of kindFields) { let g = groups.find((x) => x.label === f.group_label); if (!g) { g = { label: f.group_label, fields: [] }; groups.push(g); } g.fields.push(f); } const KIND_LABEL: Record = { drug: "药品", electronics: "数码 3C", food: "食品", generic: "通用", }; function renderField(f: KindField) { const val = attrs[f.field_key] || ""; const set = (v: string) => setAttrs((p) => ({ ...p, [f.field_key]: v })); const lbl = f.unit ? `${f.label_zh} (${f.unit})` : f.label_zh; return (
{f.field_type === "select" ? ( ) : f.field_type === "textarea" ? (