feat(frontend): 管理端页面 — 任务全景树/创建产品/标签打印/认证守卫
App.tsx: - AntApp + AuthProvider + ToastProvider 三层包裹 - /admin/login 独立登录页 - /admin/* 路由认证守卫 AdminLayout: - 未登录→跳转登录; 顶部栏显示用户名+登出 - 侧边栏: 全局概览/产品管理/任务全景 CreateProductDialog (Ant Design 重写): - MOM物料手风琴选择器 (Collapse+Table) - 分组懒加载 + useRef缓存 + 防抖搜索 - HEX ID只读展示 + 序列号/订单号选填 AdminProductsPage: - 打印预览弹窗 (Base64标签预览 + 份数选择) - 打印机设置入口 → AdminPrintConfigPage 扫码组件对齐: - ProductCard: material_name/spec_model/current_location - TaskListCard: 递归 task_tree + 状态配色 + 返工/裂变标签 - QueryResult: task_tree 优先 - ScanPage: 宏观状态栏
This commit is contained in:
@ -1,17 +1,31 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Printer, RefreshCw, Loader2, QrCode, Plus } from "lucide-react";
|
||||
import { Printer, RefreshCw, Loader2, QrCode, Plus, Settings, X, Loader } from "lucide-react";
|
||||
import api from "../../services/api";
|
||||
import type { ProductResponse } from "../../types/admin";
|
||||
import CreateProductDialog from "./CreateProductDialog";
|
||||
import {
|
||||
getLabelPreview,
|
||||
executePrint,
|
||||
type LabelPreviewRequest,
|
||||
} from "../../services/printApi";
|
||||
import { useToast } from "../../components/ui/Toast";
|
||||
|
||||
const QR_BASE = "/api/v1/products/qrcode";
|
||||
|
||||
export default function AdminProductsPage() {
|
||||
const { toast } = useToast();
|
||||
const [products, setProducts] = useState<ProductResponse[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
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);
|
||||
|
||||
async function loadProducts() {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
@ -29,8 +43,54 @@ export default function AdminProductsPage() {
|
||||
loadProducts();
|
||||
}, []);
|
||||
|
||||
/** 打印单个二维码 */
|
||||
function handlePrint(serialNumber: string) {
|
||||
// ---- 打印标签 ----
|
||||
|
||||
async function handleOpenPrint(product: ProductResponse) {
|
||||
setPrintTarget(product);
|
||||
setPreviewUrl(null);
|
||||
setPrintCopies(1);
|
||||
setPrintLoading(true);
|
||||
|
||||
try {
|
||||
const payload: LabelPreviewRequest = {
|
||||
serial_number: product.serial_number,
|
||||
material_name: product.material_name ?? product.material_id ?? "",
|
||||
spec_model: product.spec_model ?? "",
|
||||
order_no: product.order_no ?? "",
|
||||
};
|
||||
const url = await getLabelPreview(payload);
|
||||
setPreviewUrl(url);
|
||||
} catch (err: any) {
|
||||
toast(err?.response?.data?.detail ?? err?.message ?? "生成预览失败", "error");
|
||||
setPrintTarget(null);
|
||||
} finally {
|
||||
setPrintLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleConfirmPrint() {
|
||||
if (!printTarget) return;
|
||||
setPrinting(true);
|
||||
try {
|
||||
const result = await executePrint({
|
||||
serial_number: printTarget.serial_number,
|
||||
material_name: printTarget.material_name ?? printTarget.material_id ?? "",
|
||||
spec_model: printTarget.spec_model ?? "",
|
||||
order_no: printTarget.order_no ?? "",
|
||||
copies: printCopies,
|
||||
});
|
||||
toast(result.message, "success");
|
||||
setPrintTarget(null);
|
||||
} catch (err: any) {
|
||||
toast(err?.response?.data?.detail ?? err?.message ?? "打印失败", "error");
|
||||
} finally {
|
||||
setPrinting(false);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 打印二维码(兼容旧功能) ----
|
||||
|
||||
function handlePrintQr(serialNumber: string) {
|
||||
const qrUrl = `${QR_BASE}/${serialNumber}`;
|
||||
const w = window.open("", "_blank", "width=400,height=500");
|
||||
if (!w) return;
|
||||
@ -68,6 +128,13 @@ export default function AdminProductsPage() {
|
||||
</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="打印机设置"
|
||||
>
|
||||
<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"
|
||||
@ -133,8 +200,8 @@ export default function AdminProductsPage() {
|
||||
|
||||
{/* 打印按钮 */}
|
||||
<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"
|
||||
onClick={() => handleOpenPrint(p)}
|
||||
className="mt-3 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" />
|
||||
打印标签
|
||||
@ -143,11 +210,98 @@ export default function AdminProductsPage() {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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="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>
|
||||
</div>
|
||||
|
||||
{/* 预览图 */}
|
||||
<div className="mb-4 flex justify-center">
|
||||
{printLoading || !previewUrl ? (
|
||||
<div className="flex items-center justify-center rounded-lg bg-gray-50 w-full h-48">
|
||||
<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"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
</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>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user