From 2eed7db82d39e4709dae4234a1dd63a6ca1c9af7 Mon Sep 17 00:00:00 2001 From: duxingchen Date: Mon, 10 Aug 2026 11:07:53 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BA=A7=E5=93=81=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E9=A1=B5=E5=85=A8=E9=9D=A2=E5=8D=87=E7=BA=A7=E2=80=94?= =?UTF-8?q?=E5=88=86=E7=BB=84=E6=8A=98=E5=8F=A0=E9=9D=A2=E6=9D=BF+?= =?UTF-8?q?=E6=90=9C=E7=B4=A2/=E7=8A=B6=E6=80=81=E8=BF=87=E6=BB=A4+?= =?UTF-8?q?=E5=BA=8F=E5=88=97=E5=8F=B7=E5=88=97+=E4=B8=8E=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E5=85=A8=E6=99=AF=E4=BD=93=E9=AA=8C=E4=B8=80=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/admin/AdminProductsPage.tsx | 404 ++++++++---------- 1 file changed, 168 insertions(+), 236 deletions(-) diff --git a/frontend/src/pages/admin/AdminProductsPage.tsx b/frontend/src/pages/admin/AdminProductsPage.tsx index a8175b5..286db3b 100644 --- a/frontend/src/pages/admin/AdminProductsPage.tsx +++ b/frontend/src/pages/admin/AdminProductsPage.tsx @@ -1,20 +1,30 @@ -import { useEffect, useState } from "react"; +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, + 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([]); @@ -22,20 +32,26 @@ export default function AdminProductsPage() { 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); @@ -43,176 +59,174 @@ export default function AdminProductsPage() { const canConfirmDelete = countdown <= 0 && serialInput === deleteTarget?.serial_number; async function loadProducts() { - setLoading(true); - setError(null); + setLoading(true); setError(null); try { - const { data } = await api.get("/products/"); + const { data } = await api.get("/products/", { + params: { limit: 1000, ...(keyword.trim() ? { keyword: keyword.trim() } : {}) }, + }); setProducts(data); - } catch { - setError("加载产品列表失败,请检查后端服务"); - } finally { - setLoading(false); - } + } catch { setError("加载产品列表失败"); } + finally { setLoading(false); } } - useEffect(() => { loadProducts(); }, []); + 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) { - setPrintTarget(product); - setPreviewUrl(null); - setPrintCopies(1); - setPrintLoading(true); - try { - const payload: LabelPreviewRequest = { - serial_number: product.serial_number, - material_name: product.material_name ?? product.material_id ?? "", - spec_model: product.spec_model ?? "", - order_no: product.order_no ?? "", - }; - setPreviewUrl(await getLabelPreview(payload)); - } catch (err: any) { - toast(err?.response?.data?.detail ?? err?.message ?? "生成预览失败", "error"); - setPrintTarget(null); - } finally { - setPrintLoading(false); - } - } - - async function handleConfirmPrint() { - if (!printTarget) return; - setPrinting(true); - try { - const result = await executePrint({ - serial_number: printTarget.serial_number, - material_name: printTarget.material_name ?? printTarget.material_id ?? "", - spec_model: printTarget.spec_model ?? "", - order_no: printTarget.order_no ?? "", - copies: printCopies, - }); - toast(result.message, "success"); - setPrintTarget(null); - } catch (err: any) { - toast(err?.response?.data?.detail ?? err?.message ?? "打印失败", "error"); - } finally { setPrinting(false); } - } + 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(product: ProductResponse) { - setEditTarget(product); - setEditOrderNo(product.order_no ?? ""); - setEditExternalSerial(product.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); } - } + 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 react-hooks/exhaustive-deps + }, [deleteTarget?.id]); // eslint-disable-line - function confirmDelete(product: ProductResponse) { - setDeleteTarget(product); - } - - 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); } - } + 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 && products.length > 0 && ( -
- {products.map((p) => ( -
- {/* 操作图标 — hover 显示 */} -
- - -
- {/* 二维码 + ID */} -
- {`QR-${p.serial_number}`} -

{p.serial_number}

+ {/* 产品卡片网格 */} + {isOpen && ( +
+
+ {group.products.map(p => ( +
+
+ + +
+
+ {`QR-${p.serial_number}`} +

{p.serial_number}

+
+
+ + + + + + +
+
+ +
+
+ ))} +
+
+ )}
- - {/* 信息区 */} -
- - - - - -
- - {/* 打印 */} -
- -
-
- ))} + ); + })}
)} @@ -223,32 +237,11 @@ export default function AdminProductsPage() {
!printing && setPrintTarget(null)} />
-
-

标签打印预览

- -
-
- {printLoading || !previewUrl ? ( -
- ) : ( - 标签预览 - )} -
+

标签打印预览

+
{printLoading || !previewUrl ?
: 预览}

{printTarget.serial_number}

-
- 打印份数 -
- - {printCopies} - -
-
-
- - -
+
打印份数
{printCopies}
+
)} @@ -258,81 +251,26 @@ export default function AdminProductsPage() {
!editSaving && setEditTarget(null)} />
-
-

编辑产品

- -
- +

编辑产品

产品ID: {editTarget.serial_number}

-
-
- - setEditOrderNo(e.target.value)} placeholder="请输入订单编号" className="w-full rounded-lg border border-gray-200 px-3 py-2.5 text-sm focus:border-blue-400 focus:outline-none focus:ring-2 focus:ring-blue-100" /> -
-
- - setEditExternalSerial(e.target.value)} placeholder="请输入产品序列号" className="w-full rounded-lg border border-gray-200 px-3 py-2.5 text-sm focus:border-blue-400 focus:outline-none focus:ring-2 focus:ring-blue-100" /> -
-
- -
- - +
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" />
+
)} {/* 删除确认弹窗 */} - {/* 🔧 删除确认 — 5秒倒计时 + 序列号二次确认 */} {deleteTarget && (
!deleting && setDeleteTarget(null)} />
-
-
- -
-
-

危险操作

-

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

-
-
- -

- 将永久删除 {deleteTarget.serial_number} 及其所有任务、记录和二维码。此操作不可恢复。 -

- - {/* 步骤1:输入序列号确认 */} -
- - 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 focus:ring-2 focus:ring-red-100" - disabled={deleting} - /> -
- - {/* 步骤2:5秒倒计时 + 确认按钮 */} -
- - -
+

危险操作

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

+

将永久删除 {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} />
+
)} @@ -341,11 +279,5 @@ export default function AdminProductsPage() { } function InfoRow({ icon: Icon, label, value }: { icon: React.ComponentType<{ className?: string }>; label: string; value: string }) { - return ( -
- - {label} - {value} -
- ); + return
{label}{value}
; }