import { useEffect, useState } from "react"; import { api, ApiError } from "../api"; import { Brand, Category, FIELD_LABELS, ProductRow } from "../types"; import { Search, AlertCircle, Plus } from "lucide-react"; const STATUS_LABEL: Record = { active: "在用", merged: "已合并", deprecated: "已停用", }; function QualityBadge({ score }: { score: number }) { const pct = Math.round(score * 100); const color = score >= 0.8 ? "bg-emerald-100 text-emerald-700" : score >= 0.5 ? "bg-amber-100 text-amber-700" : "bg-red-100 text-red-700"; return ( {pct} ); } export default function ProductList({ onOpen, }: { onOpen: (id: string) => void; }) { const [q, setQ] = useState(""); const [input, setInput] = useState(""); const [page, setPage] = useState(1); const [size] = useState(20); const [rows, setRows] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const [creating, setCreating] = useState(false); const [selected, setSelected] = useState>(new Set()); const [categories, setCategories] = useState([]); const [bulkStatus, setBulkStatus] = useState(""); const [bulkCategory, setBulkCategory] = useState(""); const [bulkBusy, setBulkBusy] = useState(false); function reload() { setLoading(true); setError(""); api .listProducts(q, page, size) .then((r) => { setRows(r.items); setTotal(r.total); }) .catch((e) => setError(e.message)) .finally(() => setLoading(false)); } useEffect(() => { setSelected(new Set()); reload(); }, [q, page, size]); useEffect(() => { api.listCategories().then((r) => setCategories(r.items)).catch(() => {}); }, []); function toggle(id: string) { setSelected((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); } function toggleAll() { setSelected((prev) => prev.size === rows.length ? new Set() : new Set(rows.map((r) => r.id)), ); } async function applyBulkStatus() { if (!bulkStatus || selected.size === 0) return; setBulkBusy(true); setError(""); try { await api.bulkProducts({ ids: [...selected], action: "status", status: bulkStatus, }); setSelected(new Set()); setBulkStatus(""); reload(); } catch (e) { setError(e instanceof ApiError ? e.message : "批量操作失败"); } finally { setBulkBusy(false); } } async function applyBulkCategory() { if (selected.size === 0) return; setBulkBusy(true); setError(""); try { await api.bulkProducts({ ids: [...selected], action: "category", category_id: bulkCategory || null, }); setSelected(new Set()); setBulkCategory(""); reload(); } catch (e) { setError(e instanceof ApiError ? e.message : "批量操作失败"); } finally { setBulkBusy(false); } } const pages = Math.max(1, Math.ceil(total / size)); return (

商品档案 共 {total} 条

{ e.preventDefault(); setPage(1); setQ(input.trim()); }} className="flex items-center gap-2" >
setInput(e.target.value)} placeholder="按名称 / 条码搜索" className="w-64 rounded border border-gray-300 py-2 pl-8 pr-3 text-sm focus:border-emerald-500 focus:outline-none" />
{creating && ( setCreating(false)} onCreated={(id) => { setCreating(false); onOpen(id); }} /> )} {error && (
{error}
)} {selected.size > 0 && (
已选 {selected.size} 项
改状态
改分类
)}
{loading ? ( ) : rows.length === 0 ? ( ) : ( rows.map((r) => ( onOpen(r.id)} className={`cursor-pointer hover:bg-emerald-50/50 ${ selected.has(r.id) ? "bg-emerald-50/60" : "" }`} > )) )}
0 && selected.size === rows.length} onChange={toggleAll} aria-label="全选" /> 名称 品牌 条码 品类 状态 质量分 缺失字段
加载中…
暂无数据
e.stopPropagation()} > toggle(r.id)} aria-label="选择" /> {r.name} {r.brand || "—"} {r.gtin || "—"} {r.category_path || "—"} {STATUS_LABEL[r.status] || r.status} {r.missing.length === 0 ? ( 完整 ) : ( {r.missing .map((f) => FIELD_LABELS[f] || f) .join("、")} )}
{page} / {pages}
); } function CreateProductModal({ onClose, onCreated, }: { onClose: () => void; onCreated: (id: string) => void; }) { const [name, setName] = useState(""); const [gtin, setGtin] = useState(""); const [brand, setBrand] = useState(""); const [categoryId, setCategoryId] = useState(""); const [brands, setBrands] = useState([]); const [categories, setCategories] = useState([]); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); useEffect(() => { api.listBrands().then((r) => setBrands(r.items)).catch(() => {}); api.listCategories().then((r) => setCategories(r.items)).catch(() => {}); }, []); async function submit() { if (!name.trim()) { setError("名称不能为空"); return; } setBusy(true); setError(""); try { const created = await api.createProduct({ name: name.trim(), gtin: gtin.trim() || null, brand_name: brand.trim() || null, category_id: categoryId || null, status: "active", }); onCreated(created.id); } catch (e) { setError(e instanceof ApiError ? e.message : "新建失败"); } finally { setBusy(false); } } return (

新建商品

{error && (
{error}
)}

创建后将进入详情页,可继续补全营养、图片、多条码等信息。

); }