diff --git a/frontend/src/pages/admin/AdminProductsPage.tsx b/frontend/src/pages/admin/AdminProductsPage.tsx index ee7705d..8f91b0b 100644 --- a/frontend/src/pages/admin/AdminProductsPage.tsx +++ b/frontend/src/pages/admin/AdminProductsPage.tsx @@ -1,32 +1,32 @@ import { useEffect, useState, useMemo } from "react"; +import { useNavigate } from "react-router-dom"; import { Printer, RefreshCw, Loader2, QrCode, Plus, Settings, X, - Package, Hash, Tag, MapPin, Clock, Pencil, Trash2, Save, AlertTriangle, + Package, Hash, Tag, MapPin, Clock, Timer, CalendarDays, + 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, } 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 navigate = useNavigate(); const [products, setProducts] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -34,7 +34,7 @@ export default function AdminProductsPage() { // 搜索 & 筛选 & 分组 const [keyword, setKeyword] = useState(""); - const [statusFilter, setStatusFilter] = useState(""); + const [statusFilters, setStatusFilters] = useState>(new Set()); const [groupBy, setGroupBy] = useState<"order" | "device">("device"); const [expandedGroups, setExpandedGroups] = useState>(new Set()); @@ -73,13 +73,13 @@ export default function AdminProductsPage() { function handleSearch(e?: React.FormEvent) { e?.preventDefault(); loadProducts(); } - // ---- 分组 + 状态过滤 ---- + // ---- 分组 + 状态过滤(多选)---- const productGroups = useMemo(() => { const map = new Map(); for (const p of products) { - if (statusFilter) { + if (statusFilters.size > 0) { const s = (p.macro_status || p.status).toUpperCase(); - if (s !== statusFilter) continue; + if (!statusFilters.has(s)) continue; } const key = groupBy === "device" ? (p.material_name || p.material_id || "未命名设备") @@ -91,7 +91,15 @@ export default function AdminProductsPage() { groupKey, products: prods, allInWarehouse: prods.every(p => p.current_location_id === "virtual_warehouse"), })); - }, [products, statusFilter, groupBy]); + }, [products, statusFilters, groupBy]); + + function toggleStatusFilter(key: string) { + setStatusFilters(prev => { + const next = new Set(prev); + if (next.has(key)) next.delete(key); else next.add(key); + return next; + }); + } // 🔧 默认全部展开:productGroups 变化时自动展开所有面板 useEffect(() => { @@ -161,12 +169,19 @@ export default function AdminProductsPage() {
- {STATUS_TABS.map(tab => ( - - ))} + + {STATUS_TABS.map(tab => { + const active = statusFilters.has(tab.key); + return ( + + ); + })}
@@ -210,10 +225,10 @@ export default function AdminProductsPage() {
{group.products.map(p => ( -
+
navigate(`/admin/tasks?sn=${p.serial_number}`)} className="group relative flex cursor-pointer flex-col rounded-xl bg-white shadow-sm ring-1 ring-gray-100 transition-shadow hover:shadow-md">
- - + +
{`QR-${p.serial_number}`} @@ -225,10 +240,12 @@ export default function AdminProductsPage() { + +
-
@@ -294,3 +311,17 @@ export default function AdminProductsPage() { function InfoRow({ icon: Icon, label, value }: { icon: React.ComponentType<{ className?: string }>; label: string; value: string }) { return
{label}{value}
; } + +/** 滞留时长格式化:小时 → 天/小时/分钟 */ +function formatDuration(hours: number | null): string { + if (hours == null) return "—"; + if (hours >= 24) return `${Math.round(hours / 24)}天`; + if (hours >= 1) return `${Math.round(hours)}小时`; + return `${Math.max(1, Math.round(hours * 60))}分钟`; +} + +/** 生产总天数:从产品创建(created_at)到现在,向上取整,最少 1 天 */ +function formatProductionDays(createdAt: string): string { + const days = Math.max(1, Math.ceil((Date.now() - new Date(createdAt).getTime()) / 86400000)); + return `${days} 天`; +}