feat: 宏观状态只同步主线+删除5s倒计时+序列号确认+分组切换(订单/设备)+序列号列

This commit is contained in:
2026-08-10 10:37:56 +08:00
parent ad9fb8d37e
commit 4854eb626e
3 changed files with 63 additions and 17 deletions

View File

@ -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>
{/* 步骤25秒倒计时 + 确认按钮 */}
<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>