From 4854eb626ebf2d8452e0ef6a548fa34aad50d404 Mon Sep 17 00:00:00 2001 From: duxingchen Date: Mon, 10 Aug 2026 10:37:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8F=E8=A7=82=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E5=8F=AA=E5=90=8C=E6=AD=A5=E4=B8=BB=E7=BA=BF+=E5=88=A0?= =?UTF-8?q?=E9=99=A45s=E5=80=92=E8=AE=A1=E6=97=B6+=E5=BA=8F=E5=88=97?= =?UTF-8?q?=E5=8F=B7=E7=A1=AE=E8=AE=A4+=E5=88=86=E7=BB=84=E5=88=87?= =?UTF-8?q?=E6=8D=A2(=E8=AE=A2=E5=8D=95/=E8=AE=BE=E5=A4=87)+=E5=BA=8F?= =?UTF-8?q?=E5=88=97=E5=8F=B7=E5=88=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/services/task_service.py | 2 +- .../src/pages/admin/AdminProductsPage.tsx | 52 +++++++++++++++---- frontend/src/pages/admin/AdminTasksPage.tsx | 26 ++++++++-- 3 files changed, 63 insertions(+), 17 deletions(-) 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 || "—"}