| | |
| | | |
| | | const normalizeStockInventoryTree = (nodes = []) => { |
| | | const normalizeNodeValue = (node) => { |
| | | // 后端有时会出现 id=null 的层级,这里给一个可用的 key |
| | | if (node?.id !== null && node?.id !== undefined) return String(node.id); |
| | | if (node?.nodeType === "batch") return String(node.batchNo ?? node.label ?? ""); |
| | | if (node?.nodeType === "customer") return String(node.customer ?? node.label ?? ""); |
| | | if (node?.nodeType === "model") return String(node.productModelId ?? node.model ?? node.label ?? ""); |
| | | return String(node.productName ?? node.label ?? ""); |
| | | // 必须用类型前缀保证全局唯一:否则会出现「产品 id=325」与「规格 productModelId=325」都规范化成 "325", |
| | | // findNodeObjByValue 先命中规格节点,getModels 误判为选中了 model 从而不加载规格列表(Ⅰ型2号(GQ) 即此例)。 |
| | | if (node?.nodeType === "batch") return `batch:${String(node.batchNo ?? node.label ?? "").trim()}`; |
| | | if (node?.nodeType === "customer") return `customer:${String(node.customer ?? node.label ?? "").trim()}`; |
| | | if (node?.nodeType === "model") return `model:${String(node.productModelId ?? node.model ?? node.label ?? "").trim()}`; |
| | | if (node?.id !== null && node?.id !== undefined) return `product:${String(node.id)}`; |
| | | return `product:${String(node.productName ?? node.label ?? "").trim()}`; |
| | | }; |
| | | |
| | | const normalized = (list) => |
| | |
| | | return null; |
| | | }; |
| | | |
| | | // 规格层节点:正常有 nodeType===model;部分接口漏写 nodeType 时靠 productModelId 识别 |
| | | const isInventoryModelNode = (n) => { |
| | | if (!n) return false; |
| | | if (n.nodeType === "model") return true; |
| | | if (n.nodeType === "batch" || n.nodeType === "customer") return false; |
| | | return n.productModelId !== null && n.productModelId !== undefined; |
| | | }; |
| | | |
| | | // 规格下拉、表单里用的是数值 productModelId,需在整棵树中按 productModelId 查找(不能依赖可能与产品 id 撞车的纯数字 value) |
| | | const findInventoryModelByProductModelId = (nodes = [], productModelId) => { |
| | | const target = String(productModelId ?? "").trim(); |
| | | if (!target) return null; |
| | | for (const node of nodes || []) { |
| | | if (isInventoryModelNode(node) && String(node.productModelId ?? "").trim() === target) { |
| | | return node; |
| | | } |
| | | const found = findInventoryModelByProductModelId(node.children || [], productModelId); |
| | | if (found) return found; |
| | | } |
| | | return null; |
| | | }; |
| | | |
| | | // 库存树为「产品 → 子产品 → 规格型号 → 批次…」,选中大类或中间产品节点时需收集整棵子树里的规格节点 |
| | | const collectDescendantModelNodes = (root) => { |
| | | if (!root) return []; |
| | | const models = []; |
| | | const seen = new Set(); |
| | | const walk = (n) => { |
| | | if (!n) return; |
| | | if (isInventoryModelNode(n)) { |
| | | const key = String(n.productModelId ?? n.value ?? ""); |
| | | if (key && !seen.has(key)) { |
| | | seen.add(key); |
| | | models.push(n); |
| | | } |
| | | return; |
| | | } |
| | | for (const c of n.children || []) walk(c); |
| | | }; |
| | | walk(root); |
| | | return models; |
| | | }; |
| | | |
| | | // 获取库存树(用于产品大类/规格型号联动) |
| | | const getProductOptions = async () => { |
| | | // 返回 Promise,便于在编辑产品时等待加载完成 |
| | |
| | | }; |
| | | // 获取tree子数据(先选产品,再选规格型号) |
| | | const getModels = (value) => { |
| | | if (value === null || value === undefined || value === "") { |
| | | modelOptions.value = []; |
| | | productForm.value.productModelId = null; |
| | | productForm.value.specificationModel = ""; |
| | | productForm.value.uidNo = ""; |
| | | productForm.value.unit = ""; |
| | | productForm.value.batchNo = ""; |
| | | productForm.value.customer = ""; |
| | | productForm.value.taxInclusiveUnitPrice = ""; |
| | | productForm.value.taxInclusiveTotalPrice = ""; |
| | | productForm.value.taxExclusiveTotalPrice = ""; |
| | | batchNoOptions.value = []; |
| | | supplierOptions.value = []; |
| | | batchNodeByBatchNo = new Map(); |
| | | return; |
| | | } |
| | | |
| | | const node = findNodeObjByValue(stockInventoryAllTree, value); |
| | | if (!node) return; |
| | | if (node.nodeType !== "product") return; |
| | | // 树里可选的是产品/子目录;仅排除真正的叶语义节点(部分数据漏写 nodeType:product 时不能卡死) |
| | | if (node.nodeType === "model" || node.nodeType === "batch" || node.nodeType === "customer") return; |
| | | |
| | | // 选择产品后,重置下游字段 |
| | | productForm.value.productCategory = node.label; |
| | | modelOptions.value = (node.children || []) |
| | | .filter((c) => c.nodeType === "model") |
| | | .map((m) => ({ |
| | | id: m.value, |
| | | model: m.model ?? m.label ?? "", |
| | | unit: m.unit ?? "", |
| | | uidNo: m.uidNo ?? m.identifierCode ?? "", |
| | | })); |
| | | const modelNodes = collectDescendantModelNodes(node); |
| | | modelOptions.value = modelNodes.map((m) => ({ |
| | | id: |
| | | m.productModelId !== null && m.productModelId !== undefined |
| | | ? m.productModelId |
| | | : Number(String(m.value).replace(/^model:/, "")) || m.value, |
| | | model: m.model ?? m.label ?? "", |
| | | unit: m.unit ?? "", |
| | | uidNo: m.uidNo ?? m.identifierCode ?? "", |
| | | })); |
| | | |
| | | productForm.value.productModelId = null; |
| | | productForm.value.specificationModel = ""; |
| | |
| | | |
| | | // 规格型号选择后:回显 UID,并生成“批号下拉” |
| | | const getProductModel = (value) => { |
| | | const modelNode = findNodeObjByValue(stockInventoryAllTree, value); |
| | | if (!modelNode || modelNode.nodeType !== "model") return; |
| | | if (value === null || value === undefined || value === "") return; |
| | | let modelNode = findNodeObjByValue(stockInventoryAllTree, value); |
| | | if (!modelNode || !isInventoryModelNode(modelNode)) { |
| | | modelNode = findInventoryModelByProductModelId(stockInventoryAllTree, value); |
| | | } |
| | | if (!modelNode || !isInventoryModelNode(modelNode)) return; |
| | | |
| | | const prevBatchNo = productForm.value.batchNo; |
| | | const prevCustomer = productForm.value.customer; |
| | | |
| | | productForm.value.productModelId = modelNode.value; |
| | | productForm.value.productModelId = |
| | | modelNode.productModelId !== null && modelNode.productModelId !== undefined |
| | | ? modelNode.productModelId |
| | | : Number(String(modelNode.value).replace(/^model:/, "")) || null; |
| | | productForm.value.specificationModel = modelNode.model ?? modelNode.label ?? ""; |
| | | // 有些接口/树数据里可能不包含 unit,这种情况下不要覆盖编辑时已回显的值 |
| | | const nextUnit = modelNode.unit ?? ""; |
| | |
| | | const categoryKey = findNodeIdByLabel(productOptions.value, productForm.value.productCategory); |
| | | if (categoryKey) { |
| | | const categoryNode = findNodeObjByValue(stockInventoryAllTree, categoryKey); |
| | | const models = (categoryNode?.children || []) |
| | | .filter((n) => n.nodeType === "model") |
| | | .map((m) => ({ |
| | | id: m.value, |
| | | model: m.model ?? m.label ?? "", |
| | | unit: m.unit ?? "", |
| | | uidNo: m.uidNo ?? m.identifierCode ?? "", |
| | | })); |
| | | const modelNodes = collectDescendantModelNodes(categoryNode); |
| | | const models = modelNodes.map((m) => ({ |
| | | id: |
| | | m.productModelId !== null && m.productModelId !== undefined |
| | | ? m.productModelId |
| | | : Number(String(m.value).replace(/^model:/, "")) || m.value, |
| | | model: m.model ?? m.label ?? "", |
| | | unit: m.unit ?? "", |
| | | uidNo: m.uidNo ?? m.identifierCode ?? "", |
| | | })); |
| | | modelOptions.value = models; |
| | | |
| | | // 根据当前规格型号回显 |