feat: 实现前端管理看板与移动端扫码视图组件
This commit is contained in:
153
frontend/src/pages/admin/AdminProductsPage.tsx
Normal file
153
frontend/src/pages/admin/AdminProductsPage.tsx
Normal file
@ -0,0 +1,153 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Printer, RefreshCw, Loader2, QrCode, Plus } from "lucide-react";
|
||||
import api from "../../services/api";
|
||||
import type { ProductResponse } from "../../types/admin";
|
||||
import CreateProductDialog from "./CreateProductDialog";
|
||||
|
||||
const QR_BASE = "/api/v1/products/qrcode";
|
||||
|
||||
export default function AdminProductsPage() {
|
||||
const [products, setProducts] = useState<ProductResponse[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showCreate, setShowCreate] = useState(false);
|
||||
|
||||
async function loadProducts() {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const { data } = await api.get<ProductResponse[]>("/products/");
|
||||
setProducts(data);
|
||||
} catch {
|
||||
setError("加载产品列表失败,请检查后端服务");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadProducts();
|
||||
}, []);
|
||||
|
||||
/** 打印单个二维码 */
|
||||
function handlePrint(serialNumber: string) {
|
||||
const qrUrl = `${QR_BASE}/${serialNumber}`;
|
||||
const w = window.open("", "_blank", "width=400,height=500");
|
||||
if (!w) return;
|
||||
w.document.write(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>打印标签 - ${serialNumber}</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { display: flex; flex-direction: column; align-items: center; padding: 24px; font-family: monospace; }
|
||||
img { width: 280px; height: 280px; image-rendering: pixelated; }
|
||||
.sn { margin-top: 12px; font-size: 22px; font-weight: bold; letter-spacing: 2px; color: #1f2937; }
|
||||
.hint { margin-top: 8px; font-size: 12px; color: #9ca3af; }
|
||||
@media print { body { padding: 0; } img { width: 260px; height: 260px; } }
|
||||
</style></head>
|
||||
<body>
|
||||
<img src="${qrUrl}" alt="QR-${serialNumber}" />
|
||||
<p class="sn">${serialNumber}</p>
|
||||
<p class="hint">扫描二维码查询生产进度</p>
|
||||
<script>window.onload=()=>window.print()</script>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
w.document.close();
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* 标题栏 */}
|
||||
<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>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<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>
|
||||
<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>
|
||||
</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>
|
||||
)}
|
||||
|
||||
{/* 产品列表 */}
|
||||
{!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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && products.length > 0 && (
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6">
|
||||
{products.map((p) => (
|
||||
<div
|
||||
key={p.id}
|
||||
className="flex flex-col items-center rounded-xl bg-white p-4 shadow-sm transition-shadow hover:shadow-md"
|
||||
>
|
||||
{/* 二维码 */}
|
||||
<img
|
||||
src={`${QR_BASE}/${p.serial_number}`}
|
||||
alt={`QR-${p.serial_number}`}
|
||||
className="h-40 w-40 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>
|
||||
<p className="mt-0.5 text-xs text-gray-400">
|
||||
{p.order_no ?? "—"}
|
||||
</p>
|
||||
|
||||
{/* 打印按钮 */}
|
||||
<button
|
||||
onClick={() => handlePrint(p.serial_number)}
|
||||
className="mt-3 flex w-full items-center justify-center gap-1.5 rounded-lg border border-gray-200 py-2 text-xs font-medium text-gray-600 transition-colors hover:bg-blue-50 hover:text-blue-700 hover:border-blue-200"
|
||||
>
|
||||
<Printer className="h-3.5 w-3.5" />
|
||||
打印标签
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<CreateProductDialog
|
||||
open={showCreate}
|
||||
onClose={() => setShowCreate(false)}
|
||||
onCreated={loadProducts}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user