import { useEffect, useState, useMemo } from "react"; import { Printer, RefreshCw, Loader2, QrCode, Plus, Settings, X, Package, Hash, Tag, MapPin, Clock, Pencil, Trash2, Save, AlertTriangle, Search, ChevronDown, ChevronRight, Warehouse, Barcode, } from "lucide-react"; import api from "../../services/api"; import type { ProductResponse } from "../../types/admin"; import CreateProductDialog from "./CreateProductDialog"; import { getLabelPreview, executePrint, type LabelPreviewRequest, } from "../../services/printApi"; import { useToast } from "../../components/ui/Toast"; import { getStatusConfig } from "../../constants/task"; const QR_BASE = "/api/v1/products/qrcode"; const STATUS_TABS = [ { key: "", label: "全部" }, { key: "PENDING", label: "待接收" }, { key: "WIP", label: "进行中" }, { key: "COMPLETED", label: "已完成" }, { key: "ARCHIVED", label: "已入库" }, ]; interface ProductGroup { groupKey: string; products: ProductResponse[]; allInWarehouse: boolean; } export default function AdminProductsPage() { const { toast } = useToast(); const [products, setProducts] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [showCreate, setShowCreate] = useState(false); // 搜索 & 筛选 & 分组 const [keyword, setKeyword] = useState(""); const [statusFilter, setStatusFilter] = useState(""); const [groupBy, setGroupBy] = useState<"order" | "device">("order"); const [expandedGroups, setExpandedGroups] = useState>(new Set()); // 打印 const [printTarget, setPrintTarget] = useState(null); const [previewUrl, setPreviewUrl] = useState(null); const [printLoading, setPrintLoading] = useState(false); const [printCopies, setPrintCopies] = useState(1); const [printing, setPrinting] = useState(false); // 编辑 const [editTarget, setEditTarget] = useState(null); const [editOrderNo, setEditOrderNo] = useState(""); const [editExternalSerial, setEditExternalSerial] = useState(""); const [editSaving, setEditSaving] = useState(false); // 删除防呆 const [deleteTarget, setDeleteTarget] = useState(null); const [deleting, setDeleting] = useState(false); const [countdown, setCountdown] = useState(5); const [serialInput, setSerialInput] = useState(""); const canConfirmDelete = countdown <= 0 && serialInput === deleteTarget?.serial_number; async function loadProducts() { setLoading(true); setError(null); try { const { data } = await api.get("/products/", { params: { limit: 1000, ...(keyword.trim() ? { keyword: keyword.trim() } : {}) }, }); setProducts(data); } catch { setError("加载产品列表失败"); } finally { setLoading(false); } } useEffect(() => { loadProducts(); }, []); // eslint-disable-line react-hooks/exhaustive-deps function handleSearch(e?: React.FormEvent) { e?.preventDefault(); setExpandedGroups(new Set()); loadProducts(); } // ---- 分组 + 状态过滤 ---- const productGroups = useMemo(() => { const map = new Map(); for (const p of products) { if (statusFilter) { const s = (p.macro_status || p.status).toUpperCase(); if (s !== statusFilter) continue; } const key = groupBy === "device" ? (p.material_name || p.material_id || "未命名设备") : (p.order_no || "未绑定订单"); if (!map.has(key)) map.set(key, []); map.get(key)!.push(p); } return Array.from(map.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([groupKey, prods]) => ({ groupKey, products: prods, allInWarehouse: prods.every(p => p.current_location_id === "virtual_warehouse"), })); }, [products, statusFilter, groupBy]); function toggleGroup(key: string) { setExpandedGroups(prev => { const n = new Set(prev); if (n.has(key)) n.delete(key); else n.add(key); return n; }); } // ---- 打印 ---- async function handleOpenPrint(product: ProductResponse) { /* unchanged */ setPrintTarget(product); setPreviewUrl(null); setPrintCopies(1); setPrintLoading(true); try { setPreviewUrl(await getLabelPreview({ serial_number: product.serial_number, material_name: product.material_name ?? product.material_id ?? "", spec_model: product.spec_model ?? "", order_no: product.order_no ?? "" })); } catch (err: any) { toast(err?.response?.data?.detail ?? "生成预览失败", "error"); setPrintTarget(null); } finally { setPrintLoading(false); } } async function handleConfirmPrint() { if (!printTarget) return; setPrinting(true); try { const r = await executePrint({ serial_number: printTarget.serial_number, material_name: printTarget.material_name ?? "", spec_model: printTarget.spec_model ?? "", order_no: printTarget.order_no ?? "", copies: printCopies }); toast(r.message, "success"); setPrintTarget(null); } catch (err: any) { toast(err?.response?.data?.detail ?? "打印失败", "error"); } finally { setPrinting(false); } } // ---- 编辑 ---- function openEdit(p: ProductResponse) { setEditTarget(p); setEditOrderNo(p.order_no ?? ""); setEditExternalSerial(p.external_serial ?? ""); } async function handleSaveEdit() { if (!editTarget) return; setEditSaving(true); try { await api.patch(`/products/${editTarget.id}`, { order_no: editOrderNo.trim() || null, external_serial: editExternalSerial.trim() || null }); toast("保存成功", "success"); setEditTarget(null); loadProducts(); } catch (err: any) { toast(err?.response?.data?.detail ?? "保存失败", "error"); } finally { setEditSaving(false); } } // ---- 删除 ---- useEffect(() => { if (!deleteTarget) { setCountdown(5); setSerialInput(""); return; } setCountdown(5); setSerialInput(""); const timer = setInterval(() => setCountdown(c => { if (c <= 1) { clearInterval(timer); return 0; } return c - 1; }), 1000); return () => clearInterval(timer); }, [deleteTarget?.id]); // eslint-disable-line function confirmDelete(p: ProductResponse) { setDeleteTarget(p); } async function handleDelete() { if (!deleteTarget) return; setDeleting(true); try { await api.delete(`/products/${deleteTarget.id}`); toast("已删除", "success"); setDeleteTarget(null); loadProducts(); } catch (err: any) { toast(err?.response?.data?.detail ?? "删除失败", "error"); } finally { setDeleting(false); } } return (
{/* 标题栏 */}

产品管理

查看所有产品身份证并生成二维码

{/* 搜索 + 分组 + 状态过滤(移植自 AdminTasksPage) */}
分组: {(["order", "device"] as const).map(m => ( ))}
setKeyword(e.target.value)} placeholder="搜索产品身份证、订单号、规格型号..." className="w-full rounded-lg border border-gray-200 py-2.5 pl-9 pr-3 text-sm focus:border-blue-400 focus:outline-none focus:ring-2 focus:ring-blue-100" />
{STATUS_TABS.map(tab => ( ))}
{error &&
{error}
} {loading &&
} {/* 空态 */} {!loading && products.length === 0 && !error && (

暂无产品数据

)} {/* 🚀 分组折叠面板 */} {!loading && productGroups.length > 0 && (
{productGroups.map(group => { const isOpen = expandedGroups.has(group.groupKey); return (
{/* 产品卡片网格 */} {isOpen && (
{group.products.map(p => (
{`QR-${p.serial_number}`}

{p.serial_number}

))}
)}
); })}
)} setShowCreate(false)} onCreated={loadProducts} /> {/* 打印预览弹窗 */} {printTarget && (
!printing && setPrintTarget(null)} />

标签打印预览

{printLoading || !previewUrl ?
: 预览}

{printTarget.serial_number}

打印份数
{printCopies}
)} {/* 编辑弹窗 */} {editTarget && (
!editSaving && setEditTarget(null)} />

编辑产品

产品ID: {editTarget.serial_number}

setEditOrderNo(e.target.value)} className="w-full rounded-lg border border-gray-200 px-3 py-2.5 text-sm focus:border-blue-400 focus:outline-none" />
setEditExternalSerial(e.target.value)} className="w-full rounded-lg border border-gray-200 px-3 py-2.5 text-sm focus:border-blue-400 focus:outline-none" />
)} {/* 删除确认弹窗 */} {deleteTarget && (
!deleting && setDeleteTarget(null)} />

危险操作

删除产品及其全部关联数据

将永久删除 {deleteTarget.serial_number} 及其所有任务、记录和二维码。

setSerialInput(e.target.value)} placeholder={deleteTarget.serial_number} maxLength={16} className="w-full rounded-lg border border-gray-200 px-3 py-2 font-mono text-sm focus:border-red-400 focus:outline-none" disabled={deleting} />
)}
); } function InfoRow({ icon: Icon, label, value }: { icon: React.ComponentType<{ className?: string }>; label: string; value: string }) { return
{label}{value}
; }