/** 任务全景 Dashboard — 按订单聚合 + 关键词搜索 + 状态筛选 */ import { useState, useEffect, useMemo, useCallback } from "react"; import { Search, Loader2, Package, ChevronDown, ChevronRight, Warehouse, GitBranch, X, } from "lucide-react"; import api from "../../services/api"; import { scanProduct } from "../../services/productApi"; import { receiveTask, rejectTask, transferTask, } from "../../services/taskApi"; import { TaskFlowView } from "../../components/TaskTree/TaskFlowView"; import type { ModalTarget } from "../../components/TaskTree/TaskTreeViewer"; import { ReceiveConfirmModal, RejectModal, TransferModal } from "../../components/TaskTree/TaskTreeViewer"; import type { ProductResponse } from "../../types/admin"; import type { ProductScanResponse, TaskResponse } from "../../types/api"; import { useToast } from "../../components/ui/Toast"; import { getStatusConfig } from "../../constants/task"; import { useAuth } from "../../contexts/AuthContext"; const STATUS_TABS = [ { key: "", label: "全部" }, { key: "PENDING", label: "待接收" }, { key: "WIP", label: "进行中" }, { key: "COMPLETED", label: "已完成" }, { key: "ARCHIVED", label: "已入库" }, ]; interface OrderGroup { orderNo: string; products: ProductResponse[]; allInWarehouse: boolean; } export default function AdminTasksPage() { const { toast } = useToast(); const { user: currentUser } = useAuth(); // 搜索 & 筛选 const [keyword, setKeyword] = useState(""); const [statusFilter, setStatusFilter] = useState(""); const [groupBy, setGroupBy] = useState<"order" | "device">("order"); // 🔧 分组维度 const [products, setProducts] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // 手风琴展开状态 const [expandedOrders, setExpandedOrders] = useState>(new Set()); // 任务树展开 const [expandedProducts, setExpandedProducts] = useState>(new Set()); const [taskTrees, setTaskTrees] = useState>({}); const [treeLoading, setTreeLoading] = useState>({}); // 弹窗 const [modalTarget, setModalTarget] = useState(null); const [submitting, setSubmitting] = useState(false); // ---- 加载产品列表(始终拉全量,不做服务端状态过滤) ---- async function loadProducts(kw: string) { setLoading(true); setError(null); try { const params: Record = { limit: 1000 }; if (kw.trim()) params.keyword = kw.trim(); const { data } = await api.get("/products/", { params }); setProducts(data); } catch { setError("加载产品列表失败,请检查后端服务"); } finally { setLoading(false); } } useEffect(() => { loadProducts(keyword); }, []); // eslint-disable-line react-hooks/exhaustive-deps function handleSearch(e?: React.FormEvent) { e?.preventDefault(); setExpandedOrders(new Set()); setExpandedProducts(new Set()); setTaskTrees({}); loadProducts(keyword); } // ---- 按订单/设备分组 + 本地状态过滤 ---- const orderGroups = useMemo(() => { const map = new Map(); for (const p of products) { if (statusFilter) { const currentStatus = (p.macro_status || p.status).toUpperCase(); if (currentStatus !== 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(([orderNo, prods]) => ({ orderNo, products: prods, allInWarehouse: prods.every( (p) => p.current_location_id === "virtual_warehouse" ), })); }, [products, statusFilter, groupBy]); // ---- 手风琴切换 ---- function toggleOrder(orderNo: string) { setExpandedOrders((prev) => { const next = new Set(prev); if (next.has(orderNo)) next.delete(orderNo); else next.add(orderNo); return next; }); } // ---- 任务树展开/收起 ---- async function toggleProductTree(serialNumber: string) { setExpandedProducts((prev) => { const next = new Set(prev); if (next.has(serialNumber)) { next.delete(serialNumber); return next; } // 加载任务树 next.add(serialNumber); if (!taskTrees[serialNumber]) { setTreeLoading((s) => ({ ...s, [serialNumber]: true })); scanProduct(serialNumber) .then((result) => { setTaskTrees((s) => ({ ...s, [serialNumber]: result })); }) .catch((err: any) => { const msg = err?.response?.data?.detail ?? err?.message ?? "加载任务树失败"; toast(msg, "error"); setExpandedProducts((prev2) => { const n2 = new Set(prev2); n2.delete(serialNumber); return n2; }); }) .finally(() => { setTreeLoading((s) => ({ ...s, [serialNumber]: false })); }); } return next; }); } // ---- 任务操作 ---- const refreshProductTree = useCallback(async (serialNumber: string) => { try { const result = await scanProduct(serialNumber); setTaskTrees((s) => ({ ...s, [serialNumber]: result })); } catch { /* 静默 */ } }, []); async function handleReceive() { if (!modalTarget) return; setSubmitting(true); try { await receiveTask(modalTarget.task.id); toast("任务已接收", "success"); const sn = modalTarget.task.product_sn || ""; setModalTarget(null); if (sn) await refreshProductTree(sn); await loadProducts(keyword); } catch (err: any) { toast(err?.response?.data?.detail ?? err?.message ?? "接收失败", "error"); } finally { setSubmitting(false); } } async function handleReject(reason: string) { if (!modalTarget) return; setSubmitting(true); try { await rejectTask(modalTarget.task.id, reason); toast("任务已驳回", "success"); const sn = modalTarget.task.product_sn || ""; setModalTarget(null); if (sn) await refreshProductTree(sn); await loadProducts(keyword); } catch (err: any) { toast(err?.response?.data?.detail ?? err?.message ?? "驳回失败", "error"); } finally { setSubmitting(false); } } async function handleTransfer( nextTaskName: string, assignees: string[], note: string, ) { if (!modalTarget) return; setSubmitting(true); try { const result = await transferTask( modalTarget.task.id, assignees, nextTaskName, note || undefined, ); toast(result.message, "success"); const sn = modalTarget.task.product_sn || ""; setModalTarget(null); if (sn) await refreshProductTree(sn); await loadProducts(keyword); } catch (err: any) { toast(err?.response?.data?.detail ?? err?.message ?? "转交失败", "error"); } finally { setSubmitting(false); } } // ---- 渲染 ---- const modalTask = modalTarget?.task ?? null; return (
{/* ---- 标题 ---- */}

任务全景 Dashboard

按订单聚合查看产品流转状态,支持多维搜索与状态筛选

{/* ---- 搜索 + 筛选 ---- */}
分组: {(["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" />
{/* 状态筛选 Tabs */}
{STATUS_TABS.map((tab) => ( ))}
{/* ---- 错误 ---- */} {error && (
{error}
)} {/* ---- 加载中 ---- */} {loading && (
)} {/* ---- 空态 ---- */} {!loading && products.length === 0 && !error && (

暂无产品数据

创建产品并绑定订单后,在此查看流转状态

)} {/* ---- 订单手风琴列表 ---- */} {!loading && orderGroups.length > 0 && (
{orderGroups.map((group) => { const isOpen = expandedOrders.has(group.orderNo); return (
{/* 订单头部 */} {/* 订单展开内容 */} {isOpen && (
{/* 表头 */}
产品身份证
序列号
规格型号
宏观状态
任务状态
当前位置
创建时间
操作
{/* 产品行 */} {group.products.map((p) => { const productExpanded = expandedProducts.has(p.serial_number); const isTreeLoading = treeLoading[p.serial_number]; const tree = taskTrees[p.serial_number]; // 综合状态:优先流转树状态,兜底产品状态 // 单一数据源:后端预计算 macro_status,兜底产品 status const currentStatus = p.macro_status || p.status; const statusCfg = getStatusConfig(currentStatus); return (
{p.serial_number}
{p.external_serial || "—"}
{p.spec_model || p.material_name || p.material_id || "—"}
{p.overall_status || "—"}
{statusCfg.label}
{p.current_location_id === "virtual_warehouse" ? ( 🏭 仓库 ) : ( p.current_location_name || p.current_location_id || "—" )}
{new Date(p.created_at).toLocaleDateString("zh-CN")}
{/* 展开的流转树 — 卡片堆叠视图 */} {productExpanded && tree && (

流转卡片 — {p.serial_number}

{tree.task_tree && tree.task_tree.length > 0 ? ( ) : (

暂无流转记录

产品刚创建,尚未分配生产任务

)}
)} {productExpanded && isTreeLoading && (

加载流转树...

)}
); })}
)}
); })}
)} {/* ---- 弹窗 ---- */} setModalTarget(null)} onConfirm={handleReceive} /> setModalTarget(null)} onSubmit={handleReject} /> setModalTarget(null)} onSubmit={handleTransfer} />
); }