feat: 宏观状态只同步主线+删除5s倒计时+序列号确认+分组切换(订单/设备)+序列号列
This commit is contained in:
@ -35,9 +35,12 @@ export default function AdminProductsPage() {
|
||||
const [editExternalSerial, setEditExternalSerial] = useState("");
|
||||
const [editSaving, setEditSaving] = useState(false);
|
||||
|
||||
// 删除确认
|
||||
// 删除确认 + 防呆
|
||||
const [deleteTarget, setDeleteTarget] = useState<ProductResponse | null>(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 && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" onClick={() => !deleting && setDeleteTarget(null)} />
|
||||
@ -286,21 +298,39 @@ export default function AdminProductsPage() {
|
||||
<AlertTriangle className="h-5 w-5 text-red-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-base font-bold text-gray-800">确认删除</h3>
|
||||
<p className="text-xs text-gray-500">此操作不可恢复</p>
|
||||
<h3 className="text-base font-bold text-gray-800">危险操作</h3>
|
||||
<p className="text-xs text-gray-500">删除产品及其全部关联数据</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mb-2 text-sm text-gray-600">
|
||||
确定要删除产品 <span className="font-mono font-bold">{deleteTarget.serial_number}</span> 吗?
|
||||
<p className="mb-4 text-sm text-gray-600">
|
||||
将永久删除 <span className="font-mono font-bold">{deleteTarget.serial_number}</span> 及其所有任务、记录和二维码。此操作不可恢复。
|
||||
</p>
|
||||
<p className="mb-6 text-xs text-red-500">将同时删除该产品关联的所有任务和二维码。</p>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<button onClick={() => setDeleteTarget(null)} disabled={deleting} className="rounded-lg border border-gray-200 px-4 py-2 text-sm text-gray-600 hover:bg-gray-50 disabled:opacity-50">取消</button>
|
||||
<button onClick={handleDelete} disabled={deleting} className="flex items-center gap-1.5 rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 disabled:opacity-50">
|
||||
{deleting && <Loader2 className="h-3.5 w-3.5 animate-spin" />}
|
||||
<Trash2 className="h-3.5 w-3.5" />确认删除
|
||||
{/* 步骤1:输入序列号确认 */}
|
||||
<div className="mb-4">
|
||||
<label className="mb-1 block text-xs font-medium text-gray-600">
|
||||
请输入产品身份证以确认删除
|
||||
</label>
|
||||
<input
|
||||
value={serialInput}
|
||||
onChange={(e) => setSerialInput(e.target.value)}
|
||||
placeholder={deleteTarget.serial_number}
|
||||
maxLength={16}
|
||||
className="w-full rounded-lg border border-gray-200 px-3 py-2 font-mono text-sm focus:border-red-400 focus:outline-none focus:ring-2 focus:ring-red-100"
|
||||
disabled={deleting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 步骤2:5秒倒计时 + 确认按钮 */}
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<button onClick={() => setDeleteTarget(null)} disabled={deleting}
|
||||
className="rounded-lg border border-gray-200 px-4 py-2 text-sm text-gray-600 hover:bg-gray-50 disabled:opacity-50">取消</button>
|
||||
<button onClick={handleDelete} disabled={!canConfirmDelete || deleting}
|
||||
className="flex items-center gap-1.5 rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white transition-all hover:bg-red-700 disabled:cursor-not-allowed disabled:opacity-50">
|
||||
{deleting ? <><Loader2 className="h-3.5 w-3.5 animate-spin" />删除中...</>
|
||||
: canConfirmDelete ? <><Trash2 className="h-3.5 w-3.5" />确认删除</>
|
||||
: <>确认删除 ({countdown}s)</>}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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<ProductResponse[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@ -81,7 +82,7 @@ export default function AdminTasksPage() {
|
||||
loadProducts(keyword);
|
||||
}
|
||||
|
||||
// ---- 按订单分组 + 本地状态过滤(单一数据源: macro_status || status) ----
|
||||
// ---- 按订单/设备分组 + 本地状态过滤 ----
|
||||
const orderGroups = useMemo<OrderGroup[]>(() => {
|
||||
const map = new Map<string, ProductResponse[]>();
|
||||
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() {
|
||||
|
||||
{/* ---- 搜索 + 筛选 ---- */}
|
||||
<div className="mb-6 rounded-xl bg-white p-4 shadow-sm">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<span className="text-xs text-gray-400">分组:</span>
|
||||
{(["order", "device"] as const).map(m => (
|
||||
<button key={m} onClick={() => setGroupBy(m)}
|
||||
className={`rounded-full px-3 py-1 text-xs font-medium transition-colors ${groupBy === m ? "bg-blue-600 text-white" : "bg-gray-100 text-gray-500 hover:bg-gray-200"}`}>
|
||||
{m === "order" ? "按订单" : "按设备"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<form onSubmit={handleSearch} className="flex items-center gap-2">
|
||||
<div className="relative flex-1 max-w-xl">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
||||
@ -339,8 +351,9 @@ export default function AdminTasksPage() {
|
||||
{isOpen && (
|
||||
<div className="border-t border-gray-100">
|
||||
{/* 表头 */}
|
||||
<div className="grid grid-cols-12 gap-2 bg-gray-50 px-5 py-2 text-xs font-medium text-gray-500">
|
||||
<div className="grid gap-2" style="grid-template-columns: 2fr 1fr 2fr 1fr 1fr 2fr 2fr 2fr" bg-gray-50 px-5 py-2 text-xs font-medium text-gray-500">
|
||||
<div className="col-span-2">产品身份证</div>
|
||||
<div className="col-span-1">序列号</div>
|
||||
<div className="col-span-2">规格型号</div>
|
||||
<div className="col-span-1">宏观状态</div>
|
||||
<div className="col-span-1">任务状态</div>
|
||||
@ -361,10 +374,13 @@ export default function AdminTasksPage() {
|
||||
|
||||
return (
|
||||
<div key={p.id}>
|
||||
<div className="grid grid-cols-12 gap-2 border-t border-gray-50 px-5 py-3 items-center text-sm hover:bg-gray-50/50">
|
||||
<div className="grid gap-2" style="grid-template-columns: 2fr 1fr 2fr 1fr 1fr 2fr 2fr 2fr" border-t border-gray-50 px-5 py-3 items-center text-sm hover:bg-gray-50/50">
|
||||
<div className="col-span-2 font-mono text-xs font-semibold text-gray-800 tracking-wider">
|
||||
{p.serial_number}
|
||||
</div>
|
||||
<div className="col-span-1 font-mono text-xs text-gray-600 truncate">
|
||||
{p.external_serial || "—"}
|
||||
</div>
|
||||
<div className="col-span-2 text-xs text-gray-500 truncate">
|
||||
{p.spec_model || p.material_name || p.material_id || "—"}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user