feat: 产品管理 — 当前位置显示真实姓名 + 编辑弹窗 + 删除确认
This commit is contained in:
@ -1,5 +1,8 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Printer, RefreshCw, Loader2, QrCode, Plus, Settings, X, Package, Hash, Tag, MapPin, Clock } from "lucide-react";
|
||||
import {
|
||||
Printer, RefreshCw, Loader2, QrCode, Plus, Settings, X,
|
||||
Package, Hash, Tag, MapPin, Clock, Pencil, Trash2, Save, AlertTriangle,
|
||||
} from "lucide-react";
|
||||
import api from "../../services/api";
|
||||
import type { ProductResponse } from "../../types/admin";
|
||||
import CreateProductDialog from "./CreateProductDialog";
|
||||
@ -9,7 +12,6 @@ import {
|
||||
type LabelPreviewRequest,
|
||||
} from "../../services/printApi";
|
||||
import { useToast } from "../../components/ui/Toast";
|
||||
import { getStatusConfig } from "../../constants/task";
|
||||
|
||||
const QR_BASE = "/api/v1/products/qrcode";
|
||||
|
||||
@ -20,13 +22,23 @@ export default function AdminProductsPage() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showCreate, setShowCreate] = useState(false);
|
||||
|
||||
// 打印弹窗状态
|
||||
// 打印弹窗
|
||||
const [printTarget, setPrintTarget] = useState<ProductResponse | null>(null);
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||
const [printLoading, setPrintLoading] = useState(false);
|
||||
const [printCopies, setPrintCopies] = useState(1);
|
||||
const [printing, setPrinting] = useState(false);
|
||||
|
||||
// 编辑弹窗
|
||||
const [editTarget, setEditTarget] = useState<ProductResponse | null>(null);
|
||||
const [editOrderNo, setEditOrderNo] = useState("");
|
||||
const [editExternalSerial, setEditExternalSerial] = useState("");
|
||||
const [editSaving, setEditSaving] = useState(false);
|
||||
|
||||
// 删除确认
|
||||
const [deleteTarget, setDeleteTarget] = useState<ProductResponse | null>(null);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
async function loadProducts() {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
@ -40,18 +52,14 @@ export default function AdminProductsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadProducts();
|
||||
}, []);
|
||||
|
||||
// ---- 打印标签 ----
|
||||
useEffect(() => { loadProducts(); }, []);
|
||||
|
||||
// ---- 打印 ----
|
||||
async function handleOpenPrint(product: ProductResponse) {
|
||||
setPrintTarget(product);
|
||||
setPreviewUrl(null);
|
||||
setPrintCopies(1);
|
||||
setPrintLoading(true);
|
||||
|
||||
try {
|
||||
const payload: LabelPreviewRequest = {
|
||||
serial_number: product.serial_number,
|
||||
@ -59,8 +67,7 @@ export default function AdminProductsPage() {
|
||||
spec_model: product.spec_model ?? "",
|
||||
order_no: product.order_no ?? "",
|
||||
};
|
||||
const url = await getLabelPreview(payload);
|
||||
setPreviewUrl(url);
|
||||
setPreviewUrl(await getLabelPreview(payload));
|
||||
} catch (err: any) {
|
||||
toast(err?.response?.data?.detail ?? err?.message ?? "生成预览失败", "error");
|
||||
setPrintTarget(null);
|
||||
@ -84,9 +91,48 @@ export default function AdminProductsPage() {
|
||||
setPrintTarget(null);
|
||||
} catch (err: any) {
|
||||
toast(err?.response?.data?.detail ?? err?.message ?? "打印失败", "error");
|
||||
} finally {
|
||||
setPrinting(false);
|
||||
}
|
||||
} finally { setPrinting(false); }
|
||||
}
|
||||
|
||||
// ---- 编辑 ----
|
||||
function openEdit(product: ProductResponse) {
|
||||
setEditTarget(product);
|
||||
setEditOrderNo(product.order_no ?? "");
|
||||
setEditExternalSerial(product.external_serial ?? "");
|
||||
}
|
||||
|
||||
async function handleSaveEdit() {
|
||||
if (!editTarget) return;
|
||||
setEditSaving(true);
|
||||
try {
|
||||
await api.patch(`/products/${editTarget.id}`, {
|
||||
order_no: editOrderNo.trim() || null,
|
||||
external_serial: editExternalSerial.trim() || null,
|
||||
});
|
||||
toast("保存成功", "success");
|
||||
setEditTarget(null);
|
||||
loadProducts();
|
||||
} catch (err: any) {
|
||||
toast(err?.response?.data?.detail ?? "保存失败", "error");
|
||||
} finally { setEditSaving(false); }
|
||||
}
|
||||
|
||||
// ---- 删除 ----
|
||||
function confirmDelete(product: ProductResponse) {
|
||||
setDeleteTarget(product);
|
||||
}
|
||||
|
||||
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); }
|
||||
}
|
||||
|
||||
return (
|
||||
@ -95,203 +141,166 @@ export default function AdminProductsPage() {
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-gray-800">产品管理</h2>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
查看所有产品身份证并生成二维码用于打印标签
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-gray-500">查看所有产品身份证并生成二维码用于打印标签</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<a
|
||||
href="/admin/print-config"
|
||||
className="flex items-center gap-1.5 rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-500 transition-colors hover:bg-gray-50"
|
||||
title="打印机设置"
|
||||
>
|
||||
<a href="/admin/print-config" className="flex items-center gap-1.5 rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-gray-500 hover:bg-gray-50" title="打印机设置">
|
||||
<Settings className="h-4 w-4" />
|
||||
</a>
|
||||
<button
|
||||
onClick={() => setShowCreate(true)}
|
||||
className="flex items-center gap-1.5 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
创建产品
|
||||
<button onClick={() => setShowCreate(true)} className="flex items-center gap-1.5 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700">
|
||||
<Plus className="h-4 w-4" />创建产品
|
||||
</button>
|
||||
<button
|
||||
onClick={loadProducts}
|
||||
className="flex items-center gap-1.5 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm text-gray-600 transition-colors hover:bg-gray-50"
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${loading ? "animate-spin" : ""}`} />
|
||||
刷新
|
||||
<button onClick={loadProducts} className="flex items-center gap-1.5 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm text-gray-600 hover:bg-gray-50">
|
||||
<RefreshCw className={`h-4 w-4 ${loading ? "animate-spin" : ""}`} />刷新
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 错误 */}
|
||||
{error && (
|
||||
<div className="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 加载中 */}
|
||||
{loading && (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 空状态 */}
|
||||
{error && <div className="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error}</div>}
|
||||
{loading && <div className="flex items-center justify-center py-20"><Loader2 className="h-8 w-8 animate-spin text-blue-500" /></div>}
|
||||
{!loading && products.length === 0 && !error && (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-gray-400">
|
||||
<QrCode className="mb-3 h-12 w-12" />
|
||||
<p>暂无产品数据</p>
|
||||
<p className="mt-1 text-sm">创建产品后将在此显示二维码</p>
|
||||
<QrCode className="mb-3 h-12 w-12" /><p>暂无产品数据</p><p className="mt-1 text-sm">创建产品后将在此显示二维码</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 🚀 响应式网格 — 稳定不拉伸 */}
|
||||
{/* 产品卡片网格 */}
|
||||
{!loading && products.length > 0 && (
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{products.map((p) => {
|
||||
const statusCfg = getStatusConfig(p.status);
|
||||
return (
|
||||
<div
|
||||
key={p.id}
|
||||
className="flex flex-col rounded-xl bg-white shadow-sm transition-shadow hover:shadow-md"
|
||||
>
|
||||
{/* 顶部:二维码 + 产品ID */}
|
||||
<div className="flex flex-col items-center px-4 pt-5 pb-3">
|
||||
<img
|
||||
src={`${QR_BASE}/${p.serial_number}`}
|
||||
alt={`QR-${p.serial_number}`}
|
||||
className="h-32 w-32 rounded-lg border border-gray-100"
|
||||
loading="lazy"
|
||||
/>
|
||||
<p className="mt-3 font-mono text-sm font-bold tracking-wider text-gray-800">
|
||||
{p.serial_number}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 主体:关键信息键值对 */}
|
||||
<div className="flex-1 space-y-2 border-t border-gray-50 px-4 py-3">
|
||||
<InfoRow icon={Package} label="物料名称" value={p.material_name || p.material_id || "—"} />
|
||||
<InfoRow icon={Hash} label="规格型号" value={p.spec_model || "—"} />
|
||||
<InfoRow icon={Tag} label="订单编号" value={p.order_no || "—"} />
|
||||
<InfoRow
|
||||
icon={MapPin}
|
||||
label="当前位置"
|
||||
value={
|
||||
p.current_location_id === "virtual_warehouse"
|
||||
? "🏭 仓库"
|
||||
: p.current_location_id || "—"
|
||||
}
|
||||
/>
|
||||
<InfoRow
|
||||
icon={Clock}
|
||||
label="创建时间"
|
||||
value={new Date(p.created_at).toLocaleDateString("zh-CN")}
|
||||
/>
|
||||
{p.overall_status && (
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span className="shrink-0 text-gray-400">宏观状态</span>
|
||||
<span className="rounded-full bg-blue-50 px-2 py-0.5 text-[11px] font-medium text-blue-700">
|
||||
{p.overall_status}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 底部:打印按钮 */}
|
||||
<div className="border-t border-gray-50 px-4 py-3">
|
||||
<button
|
||||
onClick={() => handleOpenPrint(p)}
|
||||
className="flex w-full items-center justify-center gap-1.5 rounded-lg border border-blue-200 bg-blue-50 py-2 text-xs font-medium text-blue-700 transition-colors hover:bg-blue-100 hover:border-blue-300"
|
||||
>
|
||||
<Printer className="h-3.5 w-3.5" />
|
||||
打印标签
|
||||
</button>
|
||||
</div>
|
||||
{products.map((p) => (
|
||||
<div key={p.id} className="group relative flex flex-col rounded-xl bg-white shadow-sm transition-shadow hover:shadow-md">
|
||||
{/* 操作图标 — hover 显示 */}
|
||||
<div className="absolute top-2 right-2 flex gap-1 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<button onClick={() => openEdit(p)} className="rounded-lg bg-white p-1.5 text-gray-400 shadow-sm hover:bg-blue-50 hover:text-blue-600" title="编辑">
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => confirmDelete(p)} className="rounded-lg bg-white p-1.5 text-gray-400 shadow-sm hover:bg-red-50 hover:text-red-500" title="删除">
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 二维码 + ID */}
|
||||
<div className="flex flex-col items-center px-4 pt-5 pb-3">
|
||||
<img src={`${QR_BASE}/${p.serial_number}`} alt={`QR-${p.serial_number}`} className="h-32 w-32 rounded-lg border border-gray-100" loading="lazy" />
|
||||
<p className="mt-3 font-mono text-sm font-bold tracking-wider text-gray-800">{p.serial_number}</p>
|
||||
</div>
|
||||
|
||||
{/* 信息区 */}
|
||||
<div className="flex-1 space-y-2 border-t border-gray-50 px-4 py-3">
|
||||
<InfoRow icon={Package} label="物料名称" value={p.material_name || p.material_id || "—"} />
|
||||
<InfoRow icon={Hash} label="规格型号" value={p.spec_model || "—"} />
|
||||
<InfoRow icon={Tag} label="订单编号" value={p.order_no || "—"} />
|
||||
<InfoRow icon={MapPin} label="当前位置" value={p.current_location_name || p.current_location_id || "—"} />
|
||||
<InfoRow icon={Clock} label="创建时间" value={new Date(p.created_at).toLocaleDateString("zh-CN")} />
|
||||
</div>
|
||||
|
||||
{/* 打印 */}
|
||||
<div className="border-t border-gray-50 px-4 py-3">
|
||||
<button onClick={() => handleOpenPrint(p)} className="flex w-full items-center justify-center gap-1.5 rounded-lg border border-blue-200 bg-blue-50 py-2 text-xs font-medium text-blue-700 hover:bg-blue-100 hover:border-blue-300">
|
||||
<Printer className="h-3.5 w-3.5" />打印标签
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CreateProductDialog
|
||||
open={showCreate}
|
||||
onClose={() => setShowCreate(false)}
|
||||
onCreated={loadProducts}
|
||||
/>
|
||||
<CreateProductDialog open={showCreate} onClose={() => setShowCreate(false)} onCreated={loadProducts} />
|
||||
|
||||
{/* 打印预览弹窗 */}
|
||||
{printTarget && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div
|
||||
className="absolute inset-0 bg-black/40 backdrop-blur-sm"
|
||||
onClick={() => !printing && setPrintTarget(null)}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" onClick={() => !printing && setPrintTarget(null)} />
|
||||
<div className="relative z-10 mx-4 w-full max-w-sm rounded-xl bg-white p-6 shadow-2xl">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h3 className="text-base font-bold text-gray-800">标签打印预览</h3>
|
||||
<button
|
||||
onClick={() => setPrintTarget(null)}
|
||||
disabled={printing}
|
||||
className="rounded-lg p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600 disabled:opacity-50"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
<button onClick={() => setPrintTarget(null)} disabled={printing} className="rounded-lg p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600 disabled:opacity-50"><X className="h-4 w-4" /></button>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 flex justify-center">
|
||||
{printLoading || !previewUrl ? (
|
||||
<div className="flex h-48 w-full items-center justify-center rounded-lg bg-gray-50">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
|
||||
</div>
|
||||
<div className="flex h-48 w-full items-center justify-center rounded-lg bg-gray-50"><Loader2 className="h-8 w-8 animate-spin text-blue-500" /></div>
|
||||
) : (
|
||||
<img
|
||||
src={previewUrl}
|
||||
alt="标签预览"
|
||||
className="max-h-64 rounded-lg border border-gray-200"
|
||||
/>
|
||||
<img src={previewUrl} alt="标签预览" className="max-h-64 rounded-lg border border-gray-200" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="mb-4 text-center font-mono text-sm font-bold tracking-wider text-gray-700">
|
||||
{printTarget.serial_number}
|
||||
</p>
|
||||
|
||||
<p className="mb-4 text-center font-mono text-sm font-bold tracking-wider text-gray-700">{printTarget.serial_number}</p>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<span className="text-sm text-gray-600">打印份数</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setPrintCopies((c) => Math.max(1, c - 1))}
|
||||
className="rounded border border-gray-200 px-2.5 py-1 text-sm hover:bg-gray-50"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<button onClick={() => setPrintCopies((c) => Math.max(1, c - 1))} className="rounded border border-gray-200 px-2.5 py-1 text-sm hover:bg-gray-50">−</button>
|
||||
<span className="w-8 text-center text-sm font-semibold">{printCopies}</span>
|
||||
<button
|
||||
onClick={() => setPrintCopies((c) => Math.min(100, c + 1))}
|
||||
className="rounded border border-gray-200 px-2.5 py-1 text-sm hover:bg-gray-50"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<button onClick={() => setPrintCopies((c) => Math.min(100, c + 1))} className="rounded border border-gray-200 px-2.5 py-1 text-sm hover:bg-gray-50">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<button onClick={() => setPrintTarget(null)} disabled={printing} 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={handleConfirmPrint} disabled={printing || printLoading} className="flex items-center gap-1.5 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50">
|
||||
{printing && <Loader2 className="h-3.5 w-3.5 animate-spin" />}确认打印
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 编辑弹窗 */}
|
||||
{editTarget && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" onClick={() => !editSaving && setEditTarget(null)} />
|
||||
<div className="relative z-10 mx-4 w-full max-w-sm rounded-xl bg-white p-6 shadow-2xl">
|
||||
<div className="mb-5 flex items-center justify-between">
|
||||
<h3 className="text-base font-bold text-gray-800">编辑产品</h3>
|
||||
<button onClick={() => setEditTarget(null)} disabled={editSaving} className="rounded-lg p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600 disabled:opacity-50"><X className="h-4 w-4" /></button>
|
||||
</div>
|
||||
|
||||
<p className="mb-4 font-mono text-sm text-gray-500">产品ID: {editTarget.serial_number}</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">订单编号</label>
|
||||
<input value={editOrderNo} onChange={(e) => setEditOrderNo(e.target.value)} placeholder="请输入订单编号" className="w-full rounded-lg border border-gray-200 px-3 py-2.5 text-sm focus:border-blue-400 focus:outline-none focus:ring-2 focus:ring-blue-100" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">产品序列号</label>
|
||||
<input value={editExternalSerial} onChange={(e) => setEditExternalSerial(e.target.value)} placeholder="请输入产品序列号" className="w-full rounded-lg border border-gray-200 px-3 py-2.5 text-sm focus:border-blue-400 focus:outline-none focus:ring-2 focus:ring-blue-100" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
onClick={() => setPrintTarget(null)}
|
||||
disabled={printing}
|
||||
className="rounded-lg border border-gray-200 px-4 py-2 text-sm text-gray-600 hover:bg-gray-50 disabled:opacity-50"
|
||||
>
|
||||
取消
|
||||
<div className="mt-6 flex justify-end gap-2">
|
||||
<button onClick={() => setEditTarget(null)} disabled={editSaving} 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={handleSaveEdit} disabled={editSaving} className="flex items-center gap-1.5 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50">
|
||||
{editSaving && <Loader2 className="h-3.5 w-3.5 animate-spin" />}
|
||||
<Save className="h-3.5 w-3.5" />保存
|
||||
</button>
|
||||
<button
|
||||
onClick={handleConfirmPrint}
|
||||
disabled={printing || printLoading}
|
||||
className="flex items-center gap-1.5 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{printing && <Loader2 className="h-3.5 w-3.5 animate-spin" />}
|
||||
确认打印
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 删除确认弹窗 */}
|
||||
{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)} />
|
||||
<div className="relative z-10 mx-4 w-full max-w-sm rounded-xl bg-white p-6 shadow-2xl">
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-red-100">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mb-2 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" />确认删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -301,16 +310,7 @@ export default function AdminProductsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
/** 信息行:图标 + 标签 + 值 */
|
||||
function InfoRow({
|
||||
icon: Icon,
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
label: string;
|
||||
value: string;
|
||||
}) {
|
||||
function InfoRow({ icon: Icon, label, value }: { icon: React.ComponentType<{ className?: string }>; label: string; value: string }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<Icon className="h-3 w-3 shrink-0 text-gray-400" />
|
||||
|
||||
@ -13,6 +13,7 @@ export interface ProductResponse {
|
||||
material_type: string | null;
|
||||
parent_product_id: string | null;
|
||||
current_location_id: string | null;
|
||||
current_location_name: string | null;
|
||||
overall_status: string | null;
|
||||
status: string;
|
||||
created_at: string;
|
||||
|
||||
Reference in New Issue
Block a user