import { useState, useEffect, useRef, useCallback } from "react"; import { Modal, Collapse, Table, Input, Button, Space, Tag, App, Spin, Empty } from "antd"; import { SearchOutlined, PlusOutlined, MinusOutlined } from "@ant-design/icons"; import api from "../../services/api"; import { fetchMaterialGroups, fetchMaterialItems, type MaterialGroup, type MaterialItem } from "../../services/materialApi"; // ============================================================ // 类型 // ============================================================ interface Props { open: boolean; onClose: () => void; onCreated: () => void; } interface SelectedMaterial { material_id: string; material_name: string; spec_model: string; category: string; material_type: string; } // ============================================================ // 主组件 // ============================================================ export default function CreateProductDialog({ open, onClose, onCreated }: Props) { const { message } = App.useApp(); // ---- 搜索 & 分组摘要 ---- const [keyword, setKeyword] = useState(""); const [summary, setSummary] = useState([]); const [summaryLoading, setSummaryLoading] = useState(false); // ---- 手风琴展开 keys ---- const [activeKeys, setActiveKeys] = useState([]); // ---- 缓存 (对标老系统 groupCache / groupLoadingMap) ---- const groupCache = useRef>(new Map()); const groupLoadingMap = useRef>(new Map()); // ---- 选中物料 ---- const [selected, setSelected] = useState(null); // ---- 表单 ---- const [externalSerial, setExternalSerial] = useState(""); const [orderNo, setOrderNo] = useState(""); const [submitting, setSubmitting] = useState(false); const [createdSn, setCreatedSn] = useState(null); // ---- 初始化 ---- useEffect(() => { if (open) { setKeyword(""); setActiveKeys([]); setSelected(null); setExternalSerial(""); setOrderNo(""); setCreatedSn(null); groupCache.current.clear(); groupLoadingMap.current.clear(); loadSummary(); } }, [open]); // ============================================================ // 数据加载 // ============================================================ /** 搜索分组摘要 */ const loadSummary = useCallback(async (kw?: string) => { setSummaryLoading(true); try { const list = await fetchMaterialGroups(kw?.trim() || undefined); setSummary(list); } catch { message.error("加载物料分组失败"); } finally { setSummaryLoading(false); } }, [message]); function handleSearch() { setActiveKeys([]); groupCache.current.clear(); groupLoadingMap.current.clear(); loadSummary(keyword.trim() || undefined); } // 防抖搜索:输入即搜,300ms 无键入后自动触发 useEffect(() => { const timer = setTimeout(() => { handleSearch(); }, 300); return () => clearTimeout(timer); }, [keyword]); /** 懒加载分组内物料 */ async function loadGroupItems(category: string) { // 缓存命中 → 跳过 if (groupCache.current.has(category)) return; // 正在加载 → 跳过 if (groupLoadingMap.current.get(category)) return; groupLoadingMap.current.set(category, true); // 触发重渲染让 Table 显示 loading forceRefresh(); try { const items = await fetchMaterialItems(category, keyword.trim() || undefined); groupCache.current.set(category, items); } catch { message.error(`加载 "${category}" 分组失败`); } finally { groupLoadingMap.current.set(category, false); forceRefresh(); } } /** 强制刷新 — 因为 useRef 不会触发重渲染,借用 state 刷新 */ const [, setTick] = useState(0); function forceRefresh() { setTick((t) => t + 1); } // ============================================================ // 手风琴事件 // ============================================================ function handleCollapseChange(keys: string | string[]) { const newKeys = Array.isArray(keys) ? keys : [keys]; setActiveKeys(newKeys); // 新展开的 panel → 懒加载 const newlyOpened = newKeys.filter((k) => !activeKeys.includes(k)); newlyOpened.forEach((cat) => loadGroupItems(cat)); } function expandAll() { const all = summary.map((g) => g.category); setActiveKeys(all); all.forEach((cat) => loadGroupItems(cat)); } function collapseAll() { setActiveKeys([]); } // ============================================================ // 选择物料 // ============================================================ function handleSelect(item: MaterialItem) { setSelected({ material_id: String(item.id), material_name: item.name, spec_model: item.spec, category: item.category, material_type: item.type, }); } // ============================================================ // 提交创建 // ============================================================ async function handleSubmit() { if (!selected) { message.warning("请选择一个物料"); return; } setSubmitting(true); try { const { data } = await api.post("/products/", { material_id: selected.material_id, material_name: selected.material_name, spec_model: selected.spec_model, category: selected.category, material_type: selected.material_type, external_serial: externalSerial.trim() || null, order_no: orderNo.trim() || null, }); setCreatedSn(data.serial_number); onCreated(); } catch (err: any) { message.error(err?.response?.data?.detail ?? "创建失败"); } finally { setSubmitting(false); } } // ============================================================ // 表格列定义 // ============================================================ const columns = [ { title: "名称", dataIndex: "name", key: "name", ellipsis: true, render: (v: string) => {v}, }, { title: "规格", dataIndex: "spec", key: "spec", ellipsis: true, }, { title: "类型", dataIndex: "type", key: "type", width: 80, render: (v: string) => {v}, }, { title: "单位", dataIndex: "unit", key: "unit", width: 60, }, { title: "操作", key: "action", width: 80, render: (_: unknown, record: MaterialItem) => ( ), }, ]; // ============================================================ // Collapse items 生成 // ============================================================ const collapseItems = summary.map((group) => { const items = groupCache.current.get(group.category); const isLoading = groupLoadingMap.current.get(group.category) === true; return { key: group.category, label: (
{group.category} {group.count}
), children: isLoading ? (
) : items && items.length > 0 ? ( ({ className: selected?.material_id === String(record.id) ? "bg-blue-50" : "", })} /> ) : ( ), }; }); // ============================================================ // 打印标签 // ============================================================ function printCurrentQRCode() { const printArea = document.getElementById("label-print-area"); if (!printArea) return; const printContent = printArea.innerHTML; const styles = Array.from(document.querySelectorAll("style, link[rel=\"stylesheet\"]")) .map(el => el.outerHTML) .join(""); const iframe = document.createElement("iframe"); iframe.style.position = "absolute"; iframe.style.width = "0"; iframe.style.height = "0"; iframe.style.border = "none"; document.body.appendChild(iframe); const doc = iframe.contentWindow!.document; doc.write(` 打印标签${styles}
${printContent}
`); doc.close(); iframe.contentWindow!.onload = () => { iframe.contentWindow!.focus(); iframe.contentWindow!.print(); setTimeout(() => { document.body.removeChild(iframe); }, 1000); }; } // ============================================================ // 渲染 // ============================================================ return ( {createdSn ? ( /* ---- 成功页 ---- */
QR
名: {selected?.material_name ?? ""}
规: {selected?.spec_model ?? ""}
单: {orderNo || "—"}
码: {createdSn}

产品创建成功!

) : ( /* ---- 表单 ---- */
{/* ================================================================ */} {/* 物料选择区域 */} {/* ================================================================ */}
MOM 物料 * {!selected && ( )}
{/* 已选物料 → 折叠手风琴,展示紧凑标签 */} {selected ? (
{selected.material_name}
规格: {selected.spec_model} 分类: {selected.category} 类型: {selected.material_type}
) : ( <> {/* 搜索栏 */}
} value={keyword} onChange={(e) => setKeyword(e.target.value)} onPressEnter={handleSearch} allowClear />
{/* 手风琴 */}
{summaryLoading ? (
) : summary.length === 0 ? ( ) : ( )}
)}
{/* ================================================================ */} {/* 系统唯一 ID(只读) */} {/* ================================================================ */}
{/* 产品序列号(选填) */}
setExternalSerial(e.target.value)} maxLength={64} placeholder="用户自定义序列号" />
{/* 所属订单(选填) */}
setOrderNo(e.target.value)} maxLength={64} placeholder="自由键入订单号" />
{/* 提交 */}
)}
); }