Files
track/frontend/src/pages/ScanPage.tsx
duxingchen 59c182a743 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: 宏观状态栏
2026-08-05 14:01:36 +08:00

97 lines
3.2 KiB
TypeScript

/** 扫码干活页 — 编排摄像头扫码 + 手动输入 + 查询结果 */
import { useState, useCallback } from "react";
import { QrCode } from "lucide-react";
import { scanProduct } from "../services/productApi";
import type { ProductScanResponse } from "../types/api";
import CameraScanner from "../components/scan/CameraScanner";
import ManualInput from "../components/scan/ManualInput";
import QueryResult from "../components/scan/QueryResult";
export default function ScanPage() {
const [cameraActive, setCameraActive] = useState(false);
const [cameraError, setCameraError] = useState<string | null>(null);
const [serialNumber, setSerialNumber] = useState("");
const [lastScanned, setLastScanned] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [product, setProduct] = useState<ProductScanResponse | null>(null);
/** 通用查询 */
const doQuery = useCallback(async (sn: string) => {
if (sn.length < 8) { setError("序列号至少需要 8 位"); return; }
setSerialNumber(sn);
setLastScanned(sn);
setLoading(true);
setError(null);
setProduct(null);
try {
setProduct(await scanProduct(sn));
} catch (err: unknown) {
const detail =
err && typeof err === "object" && "response" in err
? (err as { response?: { data?: { detail?: string } } }).response?.data?.detail
: null;
setError(detail ?? "网络错误,请检查后端服务");
} finally {
setLoading(false);
}
}, []);
/** 扫码回调:提取纯序列号 */
const handleScan = useCallback(
(decodedText: string) => {
setCameraError(null);
doQuery(decodedText.trim().replace(/[^a-zA-Z0-9]/g, "").slice(0, 16));
},
[doQuery]
);
return (
<div className="flex min-h-full flex-col pb-20 pt-safe">
<div className="flex items-center gap-2 px-4 pt-2">
<QrCode className="h-5 w-5 text-blue-600" />
<h2 className="text-lg font-bold text-gray-800">扫码干活</h2>
</div>
{/* 摄像头扫码 */}
<div className="mt-3 px-4">
<CameraScanner
active={cameraActive}
onToggle={setCameraActive}
onScan={handleScan}
error={cameraError}
onError={setCameraError}
/>
</div>
{/* 手动输入 */}
<div className="mt-3 px-4">
<ManualInput
value={serialNumber}
onChange={setSerialNumber}
onSearch={() => doQuery(serialNumber.trim())}
loading={loading}
lastScanned={lastScanned}
/>
</div>
{/* 宏观状态栏 */}
{product && (
<div className="mt-3 px-4">
<div className="flex items-center gap-2 rounded-lg bg-white px-4 py-2.5 shadow-sm">
<span className="text-xs text-gray-400">宏观状态</span>
<span className={`flex-1 text-sm font-bold ${product.overall_status ? "text-blue-600" : "text-red-500"}`}>
{product.overall_status || "未设定"}
</span>
</div>
</div>
)}
{/* 查询结果 */}
<div className="mt-3 px-4">
<QueryResult loading={loading} error={error} product={product} />
</div>
</div>
);
}