diff --git a/backend/app/services/task_service.py b/backend/app/services/task_service.py index efec879..3aa6c0f 100644 --- a/backend/app/services/task_service.py +++ b/backend/app/services/task_service.py @@ -298,7 +298,7 @@ async def create_task(db: AsyncSession, data: TaskCreate) -> TaskResponse: product_result = await db.execute(select(Product).where(Product.id == data.product_id)) product = product_result.scalar_one_or_none() if product: - if data.task_name: + if data.task_name and (not data.parent_task_id or data.task_type in ("TRANSFER", "RECOVERY")): product.overall_status = "在库" if "virtual_warehouse" in data.task_name else data.task_name # 派发给人 → 产品离开仓库 if data.assignee_id and data.assignee_id != VIRTUAL_WAREHOUSE: diff --git a/frontend/src/pages/admin/AdminProductsPage.tsx b/frontend/src/pages/admin/AdminProductsPage.tsx index 6b675f6..a8175b5 100644 --- a/frontend/src/pages/admin/AdminProductsPage.tsx +++ b/frontend/src/pages/admin/AdminProductsPage.tsx @@ -35,9 +35,12 @@ export default function AdminProductsPage() { 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); @@ -118,6 +121,14 @@ export default function AdminProductsPage() { } // ---- 删除 ---- + // 🔧 倒计时:打开删除弹窗时启动 + 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 + function confirmDelete(product: ProductResponse) { setDeleteTarget(product); } @@ -277,6 +288,7 @@ export default function AdminProductsPage() { )} {/* 删除确认弹窗 */} + {/* 🔧 删除确认 — 5秒倒计时 + 序列号二次确认 */} {deleteTarget && (
!deleting && setDeleteTarget(null)} /> @@ -286,21 +298,39 @@ export default function AdminProductsPage() {
-

确认删除

-

此操作不可恢复

+

危险操作

+

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

-

- 确定要删除产品 {deleteTarget.serial_number} 吗? +

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

-

将同时删除该产品关联的所有任务和二维码。

-
- - +
diff --git a/frontend/src/pages/admin/AdminTasksPage.tsx b/frontend/src/pages/admin/AdminTasksPage.tsx index ce8ee8e..f1fef2d 100644 --- a/frontend/src/pages/admin/AdminTasksPage.tsx +++ b/frontend/src/pages/admin/AdminTasksPage.tsx @@ -39,6 +39,7 @@ export default function AdminTasksPage() { // 搜索 & 筛选 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); @@ -81,7 +82,7 @@ export default function AdminTasksPage() { loadProducts(keyword); } - // ---- 按订单分组 + 本地状态过滤(单一数据源: macro_status || status) ---- + // ---- 按订单/设备分组 + 本地状态过滤 ---- const orderGroups = useMemo(() => { const map = new Map(); for (const p of products) { @@ -89,7 +90,9 @@ export default function AdminTasksPage() { const currentStatus = (p.macro_status || p.status).toUpperCase(); if (currentStatus !== statusFilter) continue; } - const key = p.order_no || "未绑定订单"; + 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); } @@ -102,7 +105,7 @@ export default function AdminTasksPage() { (p) => p.current_location_id === "virtual_warehouse" ), })); - }, [products, statusFilter]); + }, [products, statusFilter, groupBy]); // ---- 手风琴切换 ---- function toggleOrder(orderNo: string) { @@ -228,6 +231,15 @@ export default function AdminTasksPage() { {/* ---- 搜索 + 筛选 ---- */}
+
+ 分组: + {(["order", "device"] as const).map(m => ( + + ))} +
@@ -339,8 +351,9 @@ export default function AdminTasksPage() { {isOpen && (
{/* 表头 */} -
+
产品身份证
+
序列号
规格型号
宏观状态
任务状态
@@ -361,10 +374,13 @@ export default function AdminTasksPage() { return (
-
+
{p.serial_number}
+
+ {p.external_serial || "—"} +
{p.spec_model || p.material_name || p.material_id || "—"}