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:
2026-08-05 14:01:36 +08:00
parent d04085fc38
commit 59c182a743
9 changed files with 919 additions and 220 deletions

View File

@ -0,0 +1,162 @@
import { useEffect, useState } from "react";
import { Settings, Save, Loader2, Wifi, WifiOff } from "lucide-react";
import {
getPrinterConfig,
updatePrinterConfig,
type PrinterConfig,
} from "../../services/printApi";
import { useToast } from "../../components/ui/Toast";
export default function AdminPrintConfigPage() {
const { toast } = useToast();
const [config, setConfig] = useState<PrinterConfig | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
// 表单字段
const [ip, setIp] = useState("");
const [port, setPort] = useState(9100);
const [enabled, setEnabled] = useState(true);
useEffect(() => {
loadConfig();
}, []);
async function loadConfig() {
setLoading(true);
try {
const cfg = await getPrinterConfig();
setConfig(cfg);
const lp = cfg.label_printer;
setIp(lp.ip);
setPort(lp.port);
setEnabled(lp.enabled ?? true);
} catch (err: any) {
toast(err?.response?.data?.detail ?? "加载配置失败", "error");
} finally {
setLoading(false);
}
}
async function handleSave(e: React.FormEvent) {
e.preventDefault();
if (!ip.trim()) {
toast("请输入打印机 IP 地址", "error");
return;
}
setSaving(true);
try {
const result = await updatePrinterConfig(ip.trim(), port, enabled);
toast(result.message, "success");
} catch (err: any) {
toast(err?.response?.data?.detail ?? "保存失败", "error");
} finally {
setSaving(false);
}
}
if (loading) {
return (
<div className="flex items-center justify-center py-20">
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
</div>
);
}
return (
<div>
<div className="mb-6">
<h2 className="text-xl font-bold text-gray-800">打印机设置</h2>
<p className="mt-1 text-sm text-gray-500">
配置标签打印机(热敏打标机)的 IP 地址和端口,协议: TSPL
</p>
</div>
<div className="max-w-md rounded-xl bg-white p-6 shadow-sm">
<form onSubmit={handleSave} className="space-y-4">
{/* IP 地址 */}
<div>
<label className="mb-1 block text-sm font-medium text-gray-700">
打印机 IP 地址
</label>
<input
type="text"
value={ip}
onChange={(e) => setIp(e.target.value)}
placeholder="如 192.168.9.221"
className="w-full rounded-lg border border-gray-200 px-3 py-2.5 text-sm font-mono 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
type="number"
value={port}
onChange={(e) => setPort(Number(e.target.value))}
min={1}
max={65535}
className="w-full rounded-lg border border-gray-200 px-3 py-2.5 text-sm font-mono focus:border-blue-400 focus:outline-none focus:ring-2 focus:ring-blue-100"
/>
<p className="mt-0.5 text-xs text-gray-400">
热敏打印机通常使用 9100 端口(Raw Socket)
</p>
</div>
{/* 启用状态 */}
<label className="flex items-center gap-3 cursor-pointer select-none">
<input
type="checkbox"
checked={enabled}
onChange={(e) => setEnabled(e.target.checked)}
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<div className="flex items-center gap-1.5">
{enabled ? (
<Wifi className="h-4 w-4 text-green-500" />
) : (
<WifiOff className="h-4 w-4 text-gray-400" />
)}
<span className="text-sm text-gray-700">
{enabled ? "已启用" : "已禁用"}
</span>
</div>
</label>
{/* 保存 */}
<button
type="submit"
disabled={saving}
className="flex w-full items-center justify-center gap-2 rounded-lg bg-blue-600 py-2.5 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:opacity-50"
>
{saving ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Save className="h-4 w-4" />
)}
保存设置
</button>
</form>
{/* 当前状态 */}
{config?.label_printer && (
<div className="mt-4 rounded-lg bg-gray-50 px-3 py-2.5 text-xs text-gray-500">
<p>
当前配置: {config.label_printer.ip}:{config.label_printer.port}
{" · "}
{config.label_printer.enabled ? (
<span className="text-green-600 font-medium">已启用</span>
) : (
<span className="text-gray-400">已禁用</span>
)}
</p>
</div>
)}
</div>
</div>
);
}