/** 任务全景 Dashboard — 按订单聚合 + 关键词搜索 + 状态筛选 */ import { useState, useEffect, useMemo, useCallback } from "react"; import { useSearchParams } from "react-router-dom"; import { Search, Loader2, Package, ChevronDown, ChevronRight, Warehouse, GitBranch, X, ArrowUp, ArrowDown, ArrowUpDown, Filter, Columns, } from "lucide-react"; import { Tooltip, Popover, Checkbox, Input } from "antd"; 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 { Modal, ReceiveConfirmModal, RejectModal, TransferModal } from "../../components/TaskTree/TaskTreeViewer"; import type { ProductResponse } from "../../types/admin"; import type { ProductScanResponse } from "../../types/api"; import { useToast } from "../../components/ui/Toast"; import { getStatusConfig, lifecycleBadge, ALL_OVERALL_OPTIONS } from "../../constants/task"; import { useAuth } from "../../contexts/AuthContext"; const STATUS_TABS = [ { key: "PENDING", label: "待接收" }, { key: "WIP", label: "进行中" }, { key: "COMPLETED", label: "已完成" }, { key: "ARCHIVED", label: "已入库" }, { key: "OUTBOUND", label: "已出库" }, ]; type SortOrder = "asc" | "desc"; interface ColumnDef { key: string; label: string; colSpan: number; sortable?: boolean; sortValue?: (p: ProductResponse) => string | number; filterType?: "text" | "enum"; getFilterValue?: (p: ProductResponse) => string; enumOptions?: { value: string; label: string }[]; render: (p: ProductResponse) => React.ReactNode; } interface OrderGroup { orderNo: string; products: ProductResponse[]; allInWarehouse: boolean; allCompleted: boolean; } /** 滞留时长格式化:小时 → 天/小时/分钟 */ 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))}分钟`; } export default function AdminTasksPage() { const { toast } = useToast(); const { user: currentUser } = useAuth(); const [searchParams] = useSearchParams(); // 搜索 & 筛选 const [keyword, setKeyword] = useState(""); const [statusFilters, setStatusFilters] = useState>(new Set()); const [groupBy, setGroupBy] = useState<"order" | "device">("device"); const [products, setProducts] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // 手风琴展开状态 const [expandedOrders, setExpandedOrders] = useState>(new Set()); // 🔧 排他展开:同时只展开一个产品的流转树 const [activeTreeProductId, setActiveTreeProductId] = useState(null); const [taskTrees, setTaskTrees] = useState>({}); const [treeLoading, setTreeLoading] = useState>({}); // 弹窗 const [modalTarget, setModalTarget] = useState(null); const [submitting, setSubmitting] = useState(false); const [qrSerial, setQrSerial] = useState(null); // 🔧 QR弹窗 // ---- 列配置(10列)---- const columns: ColumnDef[] = [ { key: "serial_number", label: "产品身份证", colSpan: 2, sortable: true, sortValue: (p) => p.serial_number, filterType: "text", getFilterValue: (p) => p.serial_number, render: (p) => (
setQrSerial(p.serial_number)} title="点击查看二维码">{p.serial_number}
), }, { key: "external_serial", label: "序列号", colSpan: 1, sortable: true, sortValue: (p) => p.external_serial || "", filterType: "text", getFilterValue: (p) => p.external_serial || "", render: (p) =>
{p.external_serial || "—"}
, }, { key: "spec", label: "规格型号", colSpan: 2, filterType: "text", getFilterValue: (p) => p.spec_model || p.material_name || p.material_id || "", render: (p) =>
{p.spec_model || p.material_name || p.material_id || "—"}
, }, { key: "overall_status", label: "当前工序", colSpan: 1, filterType: "enum", getFilterValue: (p) => p.overall_status || "—", // 🔧 筛选用两阶段并集:售后设备的「发货测试 / 售后维修」也要能被筛出来 enumOptions: ALL_OVERALL_OPTIONS.map((v) => ({ value: v, label: v })), render: (p) => { const lb = lifecycleBadge(p.overall_status, p.lifecycle_phase); return ( {p.overall_status || "—"} {lb && ( {lb.label} )} ); }, }, { key: "status", label: "产品状态", colSpan: 1, filterType: "enum", getFilterValue: (p) => (p.macro_status || p.status).toUpperCase(), enumOptions: [ { value: "PENDING", label: "待接收" }, { value: "WIP", label: "进行中" }, { value: "COMPLETED", label: "已完成" }, { value: "ARCHIVED", label: "已入库" }, { value: "OUTBOUND", label: "已出库" }, ], render: (p) => { const statusCfg = getStatusConfig(p.macro_status || p.status); return {statusCfg.label}; }, }, { key: "location", label: "当前位置", colSpan: 1, filterType: "enum", getFilterValue: (p) => p.current_location_id === "virtual_warehouse" ? "仓库" : (p.current_location_name || p.current_location_id || "—"), render: (p) => (
{p.current_location_id === "virtual_warehouse" ? 🏭 仓库 : (p.current_location_name || p.current_location_id || "—")}
), }, { key: "active_duration", label: "当前人滞留", colSpan: 1, sortable: true, sortValue: (p) => p.active_duration_hours ?? -1, render: (p) => { const h = p.active_duration_hours; if (h == null) return —; const cls = h >= 24 ? "bg-red-50 text-red-600" : h >= 1 ? "bg-orange-50 text-orange-600" : "bg-emerald-50 text-emerald-600"; return {formatDuration(h)}; }, }, { key: "latest", label: "最新动态", colSpan: 2, sortable: true, sortValue: (p) => p.latest_record_time ? new Date(p.latest_record_time).getTime() : 0, filterType: "text", getFilterValue: (p) => { const t = p.latest_record_time ? new Date(p.latest_record_time).toLocaleString("zh-CN") : ""; return `${p.latest_record_assignee_name || ""} ${p.latest_record_content || ""} ${t}`; }, render: (p) => ( p.latest_record_time ? (
{new Date(p.latest_record_time).toLocaleString("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" })}
{p.latest_record_has_images && 📷} {p.latest_record_assignee_name && {p.latest_record_assignee_name}: } {p.latest_record_content || (p.latest_record_has_images ? "图片记录" : "—")}
) : — ), }, { key: "created_at", label: "创建时间", colSpan: 2, sortable: true, sortValue: (p) => new Date(p.created_at).getTime(), filterType: "text", getFilterValue: (p) => new Date(p.created_at).toLocaleDateString("zh-CN"), render: (p) =>
{new Date(p.created_at).toLocaleDateString("zh-CN")}
, }, { key: "actions", label: "操作", colSpan: 2, render: (p) => { const isTreeLoading = treeLoading[p.serial_number]; return ( ); }, }, ]; // ---- 列顺序 / 行排序 / 列筛选状态 ---- const [columnOrder, setColumnOrder] = useState(() => columns.map((c) => c.key)); const [sort, setSort] = useState<{ key: string; order: SortOrder } | null>(null); const [textFilters, setTextFilters] = useState>({}); const [enumFilters, setEnumFilters] = useState>>({}); const [dragCol, setDragCol] = useState(null); const [dragOverCol, setDragOverCol] = useState(null); // 🔧 列显隐:默认全部显示,勾选取消的列从表格中去掉(操作列始终显示) const [hiddenColumns, setHiddenColumns] = useState>(new Set()); // 从看板跳转: ?sn=xxx → 自动搜索 + 自动展开流转树 const autoSn = searchParams.get("sn") || ""; // ---- 加载产品列表(始终拉全量,不做服务端状态过滤) ---- 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); return data; } catch { setError("加载产品列表失败,请检查后端服务"); return []; } finally { setLoading(false); } } useEffect(() => { if (autoSn) { setKeyword(autoSn); loadProducts(autoSn).then((data) => { // 产品加载完成后自动打开流转树弹窗 const found = data.find((p: ProductResponse) => p.serial_number === autoSn); if (found) openTreeModal(found.serial_number); }); } else { loadProducts(keyword); } }, []); // eslint-disable-line react-hooks/exhaustive-deps function handleSearch(e?: React.FormEvent) { e?.preventDefault(); setExpandedOrders(new Set()); setTaskTrees({}); setActiveTreeProductId(null); loadProducts(keyword); } // 🚀 二维码弹窗打开时锁定背景滚动(防止滚动穿透) useEffect(() => { if (!qrSerial) return; const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = prev; }; }, [qrSerial]); // ---- 按订单/设备分组 + 状态过滤 + 列筛选 + 组内排序 ---- const orderGroups = useMemo(() => { const map = new Map(); for (const p of products) { if (statusFilters.size > 0) { const currentStatus = (p.macro_status || p.status).toUpperCase(); if (!statusFilters.has(currentStatus)) continue; } // 列筛选(所有列) let skip = false; for (const col of columns) { if (col.filterType === "text") { const kw = (textFilters[col.key] || "").trim().toLowerCase(); if (kw && !(col.getFilterValue!(p) || "").toLowerCase().includes(kw)) { skip = true; break; } } else if (col.filterType === "enum") { const set = enumFilters[col.key]; if (set && set.size > 0 && !set.has(col.getFilterValue!(p))) { skip = true; break; } } } if (skip) 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]) => { // 组内排序(升/降) let sorted = prods; if (sort) { const col = columns.find((c) => c.key === sort!.key); if (col?.sortValue) { sorted = [...prods].sort((a, b) => { const va = col.sortValue!(a); const vb = col.sortValue!(b); const cmp = (typeof va === "number" && typeof vb === "number") ? va - vb : String(va ?? "").localeCompare(String(vb ?? "")); return sort!.order === "asc" ? cmp : -cmp; }); } } return { orderNo, products: sorted, // 已入库 = MOM 已扫码实收 (macro_status==ARCHIVED),而非仅"在仓库位置" allInWarehouse: prods.every( (p) => (p.macro_status || p.status).toUpperCase() === "ARCHIVED" ), allCompleted: prods.every( (p) => (p.macro_status || p.status).toUpperCase() === "COMPLETED" ), }; }); }, [products, statusFilters, groupBy, textFilters, enumFilters, sort]); // eslint-disable-line react-hooks/exhaustive-deps function toggleStatusFilter(key: string) { setStatusFilters((prev) => { const next = new Set(prev); if (next.has(key)) next.delete(key); else next.add(key); return next; }); } // ---- 列头排序:无 → 升序 → 降序 → 无 ---- function toggleSort(key: string) { setSort((prev) => { if (!prev || prev.key !== key) return { key, order: "asc" }; if (prev.order === "asc") return { key, order: "desc" }; return null; }); } // ---- 列显隐:勾选=显示,取消=隐藏(操作列始终显示) ---- function toggleColumnVisible(key: string) { setHiddenColumns((prev) => { const next = new Set(prev); if (next.has(key)) next.delete(key); else next.add(key); return next; }); } // ---- 枚举列多选筛选 ---- function toggleEnumFilter(key: string, value: string) { setEnumFilters((prev) => { const cur = prev[key] || new Set(); const next = new Set(cur); if (next.has(value)) next.delete(value); else next.add(value); const copy = { ...prev }; if (next.size === 0) delete copy[key]; else copy[key] = next; return copy; }); } // ---- 列头拖拽换序 ---- function onDragStart(e: React.DragEvent, key: string) { setDragCol(key); e.dataTransfer.effectAllowed = "move"; } function onDragOver(e: React.DragEvent, key: string) { e.preventDefault(); e.dataTransfer.dropEffect = "move"; setDragOverCol(key); } function onDrop(e: React.DragEvent, targetKey: string) { e.preventDefault(); if (!dragCol || dragCol === targetKey) { setDragCol(null); setDragOverCol(null); return; } setColumnOrder((prev) => { const next = [...prev]; const from = next.indexOf(dragCol); const to = next.indexOf(targetKey); if (from < 0 || to < 0) return prev; next.splice(from, 1); next.splice(to, 0, dragCol); return next; }); setDragCol(null); setDragOverCol(null); } // 🔧 默认全部展开 useEffect(() => { setExpandedOrders(new Set(orderGroups.map(g => g.orderNo))); }, [orderGroups]); // ---- 手风琴切换 ---- function toggleOrder(orderNo: string) { setExpandedOrders((prev) => { const next = new Set(prev); if (next.has(orderNo)) next.delete(orderNo); else next.add(orderNo); return next; }); } // ---- 流转树宽屏弹窗(懒加载,缓存到 taskTrees) ---- function openTreeModal(serialNumber: string) { setActiveTreeProductId(serialNumber); if (!taskTrees[serialNumber]) { setTreeLoading((s) => ({ ...s, [serialNumber]: true })); scanProduct(serialNumber) .then((result) => setTaskTrees((s) => ({ ...s, [serialNumber]: result }))) .catch((err: any) => toast(err?.response?.data?.detail ?? err?.message ?? "加载任务树失败", "error")) .finally(() => setTreeLoading((s) => ({ ...s, [serialNumber]: false }))); } } function closeTreeModal() { setActiveTreeProductId(null); } // ---- 任务操作 ---- 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; // 按当前列顺序渲染的列 const orderedColumns = columnOrder .map((key) => columns.find((c) => c.key === key)) .filter((c): c is ColumnDef => !!c); // 🔧 实际显示的列(排除被隐藏的)+ 动态总跨度 const visibleColumns = orderedColumns.filter((c) => !hiddenColumns.has(c.key)); const visibleSpan = visibleColumns.reduce((s, c) => s + c.colSpan, 0); // 列设置面板内容(列显隐) function renderColumnPanel() { const toggleable = columns.filter((c) => c.key !== "actions"); return (
{toggleable.map((col) => ( ))}
操作列固定
); } // 列筛选面板内容 function renderFilterPanel(col: ColumnDef) { if (col.filterType === "text") { return (
setTextFilters((prev) => ({ ...prev, [col.key]: e.target.value }))} allowClear />
); } // enum:优先固定选项,否则从当前数据动态去重 const options = col.enumOptions || Array.from(new Set(products.map((p) => col.getFilterValue!(p)))).filter(Boolean).map((v) => ({ value: v, label: v })); const selected = enumFilters[col.key] || new Set(); return (
{options.length === 0 ? (
无可用选项
) : options.map((opt) => ( ))}
); } 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-8 text-sm focus:border-blue-400 focus:outline-none focus:ring-2 focus:ring-blue-100" /> {keyword && ( )}
{/* 状态筛选 Tabs(多选) */}
{STATUS_TABS.map((tab) => { const active = statusFilters.has(tab.key); return ( ); })}
{/* ---- 错误 ---- */} {error && (
{error}
)} {/* ---- 加载中 ---- */} {loading && (
)} {/* ---- 空态 ---- */} {!loading && products.length === 0 && !error && (

暂无产品数据

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

)} {/* ---- 订单手风琴列表 ---- */} {!loading && orderGroups.length > 0 && (
{orderGroups.map((group) => { const isOpen = expandedOrders.has(group.orderNo); return (
{/* 订单头部 */} {/* 订单展开内容 */} {isOpen && (
{/* 表头(可排序 / 可筛选 / 可拖拽换列 / 可显隐) */}
{visibleColumns.map((col) => { const sorted = sort?.key === col.key; const hasFilter = col.filterType === "text" ? !!(textFilters[col.key] || "").trim() : !!enumFilters[col.key]?.size; return (
onDragStart(e, col.key)} onDragOver={(e) => onDragOver(e, col.key)} onDrop={(e) => onDrop(e, col.key)} onDragEnd={() => { setDragCol(null); setDragOverCol(null); }} className={`flex select-none items-center gap-1 rounded ${dragOverCol === col.key ? "bg-blue-100 ring-2 ring-blue-300" : ""}`} style={{ gridColumn: `span ${col.colSpan} / span ${col.colSpan}` }} > toggleSort(col.key) : undefined} className={`flex items-center gap-0.5 ${col.sortable ? "cursor-pointer hover:text-blue-600" : ""}`} > {col.label} {col.sortable && ( sorted ? (sort!.order === "asc" ? : ) : )} {col.filterType && ( )}
); })}
{/* 产品行 */} {group.products.map((p) => { return (
{visibleColumns.map((col) => (
{col.render(p)}
))}
); })}
)}
); })}
)} {/* 🔧 二维码弹窗 */} {qrSerial && (
setQrSerial(null)}>
e.stopPropagation()}>

产品二维码

{`QR-${qrSerial}`}

{qrSerial}

)} {/* 🔧 流转树宽屏弹窗 */} {activeTreeProductId && ( {treeLoading[activeTreeProductId] ? (
) : taskTrees[activeTreeProductId]?.task_tree?.length ? ( ) : (

暂无流转记录

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

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