feat: 实现前端管理看板与移动端扫码视图组件
This commit is contained in:
189
frontend/src/pages/admin/CreateProductDialog.tsx
Normal file
189
frontend/src/pages/admin/CreateProductDialog.tsx
Normal file
@ -0,0 +1,189 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { X, Loader2, QrCode } from "lucide-react";
|
||||
import api from "../../services/api";
|
||||
import { listOrders, type OrderOption } from "../../services/orderApi";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onCreated: () => void;
|
||||
}
|
||||
|
||||
/** 生成 16 位随机 hex 序列号 */
|
||||
function genSerial(): string {
|
||||
const chars = "0123456789ABCDEF";
|
||||
let s = "";
|
||||
for (let i = 0; i < 16; i++) {
|
||||
s += chars[Math.floor(Math.random() * 16)];
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
export default function CreateProductDialog({ open, onClose, onCreated }: Props) {
|
||||
const [orders, setOrders] = useState<OrderOption[]>([]);
|
||||
const [serialNumber, setSerialNumber] = useState(genSerial());
|
||||
const [orderId, setOrderId] = useState("");
|
||||
const [materialId, setMaterialId] = useState("");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [createdSn, setCreatedSn] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setSerialNumber(genSerial());
|
||||
setError(null);
|
||||
setCreatedSn(null);
|
||||
listOrders()
|
||||
.then(setOrders)
|
||||
.catch(() => setError("加载订单列表失败"));
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (!orderId) { setError("请选择订单"); return; }
|
||||
if (serialNumber.length !== 16) { setError("序列号必须为16位"); return; }
|
||||
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
try {
|
||||
await api.post("/products/", {
|
||||
serial_number: serialNumber,
|
||||
order_id: orderId,
|
||||
material_id: materialId || null,
|
||||
});
|
||||
setCreatedSn(serialNumber);
|
||||
onCreated();
|
||||
} catch (err: unknown) {
|
||||
const detail =
|
||||
err && typeof err === "object" && "response" in err
|
||||
? (err as { response?: { data?: { detail?: unknown } } }).response?.data?.detail
|
||||
: null;
|
||||
setError(
|
||||
typeof detail === "string"
|
||||
? detail
|
||||
: JSON.stringify(detail) || "创建失败"
|
||||
);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
|
||||
<div className="w-full max-w-md rounded-xl bg-white shadow-xl">
|
||||
{/* 标题 */}
|
||||
<div className="flex items-center justify-between border-b border-gray-100 px-6 py-4">
|
||||
<h3 className="text-lg font-semibold text-gray-800">创建产品</h3>
|
||||
<button onClick={onClose} className="rounded-md p-1 text-gray-400 hover:bg-gray-100">
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 创建成功 — 展示二维码 */}
|
||||
{createdSn ? (
|
||||
<div className="flex flex-col items-center px-6 py-8">
|
||||
<img
|
||||
src={`/api/v1/products/qrcode/${createdSn}`}
|
||||
alt={`QR-${createdSn}`}
|
||||
className="h-52 w-52 rounded-lg border"
|
||||
/>
|
||||
<p className="mt-4 font-mono text-lg font-bold tracking-wider text-gray-800">
|
||||
{createdSn}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-green-600">产品创建成功!</p>
|
||||
<div className="mt-6 flex w-full gap-3">
|
||||
<button
|
||||
onClick={() => window.open(`/api/v1/products/qrcode/${createdSn}`, "_blank")}
|
||||
className="flex-1 rounded-lg border border-gray-200 py-2.5 text-sm font-medium text-gray-600 hover:bg-gray-50"
|
||||
>
|
||||
打开二维码
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setCreatedSn(null);
|
||||
setSerialNumber(genSerial());
|
||||
}}
|
||||
className="flex-1 rounded-lg bg-blue-600 py-2.5 text-sm font-medium text-white hover:bg-blue-700"
|
||||
>
|
||||
继续创建
|
||||
</button>
|
||||
</div>
|
||||
<button onClick={onClose} className="mt-3 w-full rounded-lg py-2 text-sm text-gray-400 hover:text-gray-600">
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
/* 表单 */
|
||||
<form onSubmit={handleSubmit} className="space-y-4 px-6 py-5">
|
||||
{/* 序列号 */}
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">产品序列号 (16位)</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
value={serialNumber}
|
||||
onChange={(e) => setSerialNumber(e.target.value.toUpperCase())}
|
||||
maxLength={16}
|
||||
className="flex-1 rounded-lg border border-gray-200 px-3 py-2 font-mono text-sm focus:border-blue-500 focus:outline-none"
|
||||
placeholder="16位HEX序列号"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSerialNumber(genSerial())}
|
||||
className="rounded-lg border border-gray-200 px-3 py-2 text-xs text-gray-500 hover:bg-gray-50"
|
||||
>
|
||||
随机
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 订单 */}
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">所属订单</label>
|
||||
<select
|
||||
value={orderId}
|
||||
onChange={(e) => setOrderId(e.target.value)}
|
||||
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none"
|
||||
required
|
||||
>
|
||||
<option value="">请选择订单</option>
|
||||
{orders.map((o) => (
|
||||
<option key={o.id} value={o.id}>{o.order_no}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 物料 ID */}
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">
|
||||
物料 ID <span className="text-gray-400">(可选)</span>
|
||||
</label>
|
||||
<input
|
||||
value={materialId}
|
||||
onChange={(e) => setMaterialId(e.target.value)}
|
||||
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none"
|
||||
placeholder="关联老系统物料"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">{error}</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
className="flex w-full items-center justify-center gap-2 rounded-lg bg-blue-600 py-2.5 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-60"
|
||||
>
|
||||
{submitting ? <Loader2 className="h-4 w-4 animate-spin" /> : <QrCode className="h-4 w-4" />}
|
||||
创建并生成二维码
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user