From 2925eb8e0b3daa00902e9ec420496c13b56e422f Mon Sep 17 00:00:00 2001 From: duxingchen Date: Mon, 7 Sep 2026 13:43:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BA=A7=E5=93=81=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E9=A1=B5=E6=96=B0=E5=A2=9E=E7=8A=B6=E6=80=81=E6=94=B6=E5=8F=A3?= =?UTF-8?q?=E6=8C=89=E9=92=AE,=E5=80=92=E8=AE=A1=E6=97=B6+=E8=BE=93?= =?UTF-8?q?=E5=85=A5=E4=BA=A7=E5=93=81=E8=BA=AB=E4=BB=BD=E8=AF=81=E5=8F=8C?= =?UTF-8?q?=E9=87=8D=E7=A1=AE=E8=AE=A4=E9=98=B2=E8=AF=AF=E8=A7=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/admin/AdminProductsPage.tsx | 93 ++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/admin/AdminProductsPage.tsx b/frontend/src/pages/admin/AdminProductsPage.tsx index 3e61ed6..0c8e39a 100644 --- a/frontend/src/pages/admin/AdminProductsPage.tsx +++ b/frontend/src/pages/admin/AdminProductsPage.tsx @@ -12,6 +12,7 @@ import { getLabelPreview, executePrint, } from "../../services/printApi"; import { useToast } from "../../components/ui/Toast"; +import { useAuth } from "../../contexts/AuthContext"; import { getStatusConfig } from "../../constants/task"; const QR_BASE = "/api/v1/products/qrcode"; @@ -28,6 +29,8 @@ interface ProductGroup { groupKey: string; products: ProductResponse[]; allInWar export default function AdminProductsPage() { const { toast } = useToast(); + const { user: authUser } = useAuth(); + const isAdmin = authUser?.role === "SUPER_ADMIN" || authUser?.role === "SUPERVISOR"; const [products, setProducts] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -46,6 +49,15 @@ export default function AdminProductsPage() { const [printCopies, setPrintCopies] = useState(1); const [printing, setPrinting] = useState(false); + // 收口(入库/出库,仅管理员,需倒计时+输入身份证确认) + const [finalizeTarget, setFinalizeTarget] = useState(null); + const [finalizeStatus, setFinalizeStatus] = useState<"已入库" | "已出库">("已入库"); + const [finalizeNote, setFinalizeNote] = useState(""); + const [finalizeSerialInput, setFinalizeSerialInput] = useState(""); + const [finalizeCountdown, setFinalizeCountdown] = useState(5); + const [finalizing, setFinalizing] = useState(false); + const canConfirmFinalize = !!finalizeTarget && finalizeCountdown <= 0 && finalizeSerialInput === finalizeTarget.serial_number; + // 编辑 const [editTarget, setEditTarget] = useState(null); const [editOrderNo, setEditOrderNo] = useState(""); @@ -131,6 +143,34 @@ export default function AdminProductsPage() { function confirmDelete(p: ProductResponse) { setDeleteTarget(p); } async function handleDelete() { if (!deleteTarget) return; setDeleting(true); try { await api.delete(`/products/${deleteTarget.id}`); toast("已删除", "success"); setDeleteTarget(null); loadProducts(); } catch (err: any) { toast(err?.response?.data?.detail ?? "删除失败", "error"); } finally { setDeleting(false); } } + // ---- 状态收口(入库 / 出库,管理员)---- + // 🛡️ 双重确认倒计时 + 必须输入产品身份证,防误触 + useEffect(() => { + if (!finalizeTarget) { setFinalizeCountdown(5); setFinalizeSerialInput(""); return; } + setFinalizeCountdown(5); setFinalizeSerialInput(""); + const timer = setInterval(() => setFinalizeCountdown(c => { if (c <= 1) { clearInterval(timer); return 0; } return c - 1; }), 1000); + return () => clearInterval(timer); + }, [finalizeTarget?.id]); // eslint-disable-line react-hooks/exhaustive-deps + + function openFinalize(p: ProductResponse) { setFinalizeTarget(p); setFinalizeStatus("已入库"); setFinalizeNote(""); setFinalizeSerialInput(""); setFinalizeCountdown(5); } + async function handleFinalize() { + if (!canConfirmFinalize) return; + setFinalizing(true); + try { + await api.post(`/products/scan/${finalizeTarget.serial_number}/finalize`, { + status: finalizeStatus, + note: finalizeNote.trim() || undefined, + }); + toast(`已收口为「${finalizeStatus}」`, "success"); + setFinalizeTarget(null); + loadProducts(); + } catch (err: any) { + toast(err?.response?.data?.detail ?? "操作失败", "error"); + } finally { + setFinalizing(false); + } + } + return (
{/* 标题栏 */} @@ -262,7 +302,12 @@ export default function AdminProductsPage() {
-
+
+ {isAdmin && ( + + )} @@ -310,6 +355,52 @@ export default function AdminProductsPage() {
)} + {/* 收口弹窗(入库/出库,仅管理员) */} + {finalizeTarget && ( +
+
!finalizing && setFinalizeTarget(null)} /> +
+

产品状态收口

+

{finalizeTarget.serial_number}

+

当前:{getStatusConfig(finalizeTarget.macro_status || finalizeTarget.status).label || "—"} | 位置:{finalizeTarget.current_location_name || finalizeTarget.current_location_id || "—"}

+
+ {(["已入库", "已出库"] as const).map(s => ( + + ))} +
+

⚠ 收口为最终状态:若该产品仍有进行中/待接收任务,将一并结束为完工。

+
setFinalizeNote(e.target.value)} placeholder="例如:纠错补录" className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:border-indigo-400 focus:outline-none" disabled={finalizing} />
+
+ + setFinalizeSerialInput(e.target.value)} + placeholder={finalizeTarget.serial_number} + maxLength={16} + disabled={finalizing} + className="w-full rounded-lg border border-gray-200 px-3 py-2 font-mono text-sm focus:border-red-400 focus:outline-none" + /> +

0 ? "text-amber-600" : "text-red-600"}`}> + {finalizeCountdown > 0 + ? `⏳ ${finalizeCountdown}s 确认等待中:请核对目标状态与产品身份证` + : finalizeSerialInput === finalizeTarget.serial_number + ? "✅ 已核验,可确认收口" + : `⚠ 请完整输入:${finalizeTarget.serial_number}`} +

+
+
+ + +
+
+
+ )} + {/* 删除确认弹窗 */} {deleteTarget && (