spring
12 小时以前 b937241a2c20f62f45b31b232b6cebdec03d41d7
src/views/salesManagement/salesLedger/index.vue
@@ -376,8 +376,25 @@
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="批号:" prop="batchNo">
              <el-select v-model="productForm.batchNo" placeholder="请选择" clearable filterable>
              <el-select v-model="productForm.batchNo"
                          placeholder="请选择"
                          clearable
                          filterable
                          @change="handleBatchNoChange">
                <el-option v-for="item in batchNoOptions" :key="item.value" :label="item.label" :value="item.value" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="供应商:" prop="customer">
              <el-select v-model="productForm.customer"
                          placeholder="请选择"
                          clearable
                          filterable
                          :disabled="!supplierOptions.length">
                <el-option v-for="item in supplierOptions" :key="item.value" :label="item.label" :value="item.value" />
              </el-select>
            </el-form-item>
          </el-col>
@@ -697,11 +714,11 @@
  delProduct,
  delLedgerFile, getProductInventory, saleOutboundExport,
} from "@/api/salesManagement/salesLedger.js";
import { modelList, productTreeList } from "@/api/basicData/product.js";
import { getStockInventoryAll } from "@/api/inventoryManagement/stockInventory.js";
import useFormData from "@/hooks/useFormData.js";
import dayjs from "dayjs";
import { getCurrentDate } from "@/utils/index.js";
import {getProductOrderBatchNoOptions} from "@/api/productionManagement/productionOrder.js";
// 由 /stockInventory/getStockInventoryAll 驱动“批号/供应商”联动
import {safeTrainingExport} from "@/api/safeProduction/safetyTrainingAssessment.js";
const userStore = useUserStore();
@@ -714,6 +731,7 @@
const customerOption = ref([]);
const productOptions = ref([]);
const modelOptions = ref([]);
const supplierOptions = ref([]);
const tableLoading = ref(false);
const page = reactive({
   current: 1,
@@ -762,6 +780,7 @@
const productFormData = reactive({
   productForm: {
      productCategory: "",
      customer: "",
      specificationModel: "",
    uidNo: "",
      unit: "",
@@ -777,6 +796,7 @@
      productCategory: [{ required: true, message: "请选择", trigger: "change" }],
      productModelId: [{ required: true, message: "请选择", trigger: "change" }],
    batchNo: [{ required: true, message: "请选择", trigger: "change" }],
      customer: [{ required: true, message: "请选择", trigger: "change" }],
      specificationModel: [
         { required: true, message: "请选择", trigger: "change" },
      ],
@@ -945,64 +965,202 @@
         tableLoading.value = false;
      });
};
// 获取产品大类tree数据
const getProductOptions = () => {
   // 返回 Promise,便于在编辑产品时等待加载完成
   return productTreeList().then((res) => {
      productOptions.value = convertIdToValue(res);
      return productOptions.value;
let stockInventoryAllTree = [];
let batchNodeByBatchNo = new Map();
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.model ?? node.label ?? "");
      return String(node.productName ?? node.label ?? "");
   };
   const normalized = (list) =>
      (list || []).map((n) => {
         const value = normalizeNodeValue(n);
         const label = n.label ?? n.productName ?? n.model ?? n.batchNo ?? n.customer ?? "";
         return {
            ...n,
            value,
            label,
            children: normalized(n.children),
         };
   });
   return normalized(nodes);
};
// 仅展示最多 3 个层级:第 1 层(product) -> 第 2 层(model) -> 第 3 层(batch),更深的节点不展示
const filterStockInventoryFirst3Levels = (nodes = []) => {
   const MAX_LEVEL = 3;
   const cloneAndFilterByLevel = (list = [], level = 1) => {
      return (list || [])
         .map((n) => {
            // 后续层级里如果还有 customer,直接剔除
            if (n.nodeType === "customer") return null;
            // 到达展示深度后,不再向下挂子节点
            if (level >= MAX_LEVEL) {
               return { ...n, children: [] };
            }
            // 特例:batch 节点本身也不再展示 children(保持与接口节点语义一致)
            if (n.nodeType === "batch") {
               return { ...n, children: [] };
            }
            return { ...n, children: cloneAndFilterByLevel(n.children, level + 1) };
         })
         .filter(Boolean);
   };
   return cloneAndFilterByLevel(nodes, 1);
};
const findNodeObjByValue = (nodes = [], value) => {
   for (let i = 0; i < (nodes || []).length; i++) {
      const node = nodes[i];
      if (String(node?.value) === String(value)) return node;
      const children = node?.children || [];
      if (children.length) {
         const found = findNodeObjByValue(children, value);
         if (found) return found;
      }
   }
   return null;
};
// 获取库存树(用于产品大类/规格型号联动)
const getProductOptions = async () => {
   // 返回 Promise,便于在编辑产品时等待加载完成
   const res = await getStockInventoryAll();
   const data = res?.data || [];
   stockInventoryAllTree = normalizeStockInventoryTree(data);
   productOptions.value = filterStockInventoryFirst3Levels(stockInventoryAllTree);
   return productOptions.value;
};
const formattedNumber = (row, column, cellValue) => {
   return parseFloat(cellValue).toFixed(2);
};
// 获取tree子数据
// 获取tree子数据(先选产品,再选规格型号)
const getModels = (value) => {
   productForm.value.productCategory = findNodeById(productOptions.value, value);
   modelList({ id: value }).then((res) => {
      modelOptions.value = res;
   });
   const node = findNodeObjByValue(stockInventoryAllTree, value);
   if (!node) return;
   if (node.nodeType !== "product") 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 ?? "",
      }));
   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 = "";
   modelOptions.value = modelOptions.value || [];
   batchNoOptions.value = [];
   supplierOptions.value = [];
   batchNodeByBatchNo = new Map();
};
// 规格型号选择后:回显 UID,并生成“批号下拉”
const getProductModel = (value) => {
   const index = modelOptions.value.findIndex((item) => item.id === value);
   if (index !== -1) {
      productForm.value.specificationModel = modelOptions.value[index].model;
      productForm.value.unit = modelOptions.value[index].unit;
    productForm.value.uidNo = modelOptions.value[index].uidNo || "";
   } else {
      productForm.value.specificationModel = null;
      productForm.value.unit = null;
      productForm.value.uidNo = null;
   const modelNode = findNodeObjByValue(stockInventoryAllTree, value);
   if (!modelNode || modelNode.nodeType !== "model") return;
   const prevBatchNo = productForm.value.batchNo;
   const prevCustomer = productForm.value.customer;
   productForm.value.specificationModel = modelNode.model ?? modelNode.label ?? "";
   // 有些接口/树数据里可能不包含 unit,这种情况下不要覆盖编辑时已回显的值
   const nextUnit = modelNode.unit ?? "";
   if (nextUnit !== null && nextUnit !== undefined && String(nextUnit).trim() !== "") {
      productForm.value.unit = nextUnit;
   }
};
const findNodeById = (nodes, productId) => {
   for (let i = 0; i < nodes.length; i++) {
      if (nodes[i].value === productId) {
         return nodes[i].label; // 找到节点,返回该节点
      }
      if (nodes[i].children && nodes[i].children.length > 0) {
         const foundNode = findNodeById(nodes[i].children, productId);
         if (foundNode) {
            return foundNode; // 在子节点中找到,返回该节点
         }
      }
   }
   return null; // 没有找到节点,返回null
};
function convertIdToValue(data) {
   return data.map((item) => {
      const { id, children, ...rest } = item;
      const newItem = {
         ...rest,
         value: id, // 将 id 改为 value
      };
      if (children && children.length > 0) {
         newItem.children = convertIdToValue(children);
   // 有些接口/树数据里可能不包含 uidNo,这种情况下不要覆盖编辑时已回显的值
   const nextUidNo = modelNode.uidNo ?? modelNode.identifierCode ?? "";
   if (nextUidNo !== null && nextUidNo !== undefined && String(nextUidNo).trim() !== "") {
      productForm.value.uidNo = nextUidNo;
      }
      
      return newItem;
   });
   const batchNodes = (modelNode.children || []).filter((b) => b.nodeType === "batch");
   batchNodeByBatchNo = new Map(
      batchNodes.map((b) => {
         const key = String(b.batchNo ?? b.value ?? b.label ?? "").trim();
         return [key, b];
      })
   );
   batchNoOptions.value = batchNodes.map((b) => ({
      label: String(b.batchNo ?? b.label ?? "").trim(),
      value: String(b.batchNo ?? b.value ?? b.label ?? "").trim(),
   }));
   // 批号不再属于新规格时,清空
   const batchValues = new Set(batchNoOptions.value.map((x) => x.value));
   if (!prevBatchNo || !batchValues.has(prevBatchNo)) {
      productForm.value.batchNo = "";
}
   // 需要供应商:批号回显后再生成
   productForm.value.customer = "";
   supplierOptions.value = [];
   if (productForm.value.batchNo) {
      handleBatchNoChange(productForm.value.batchNo, prevCustomer);
   }
};
const handleBatchNoChange = (batchNo, prevCustomer) => {
   const safeBatchNo = String(batchNo ?? "").trim();
   if (!safeBatchNo || !batchNodeByBatchNo.size) {
      productForm.value.customer = "";
      supplierOptions.value = [];
      return;
   }
   const batchNode = batchNodeByBatchNo.get(String(safeBatchNo));
   if (!batchNode) {
      productForm.value.customer = "";
      supplierOptions.value = [];
      return;
   }
   // UID码可能来源于 batch 节点(不同接口字段名不一致时尽量兜底)
   const nextUidNo = batchNode.uidNo ?? batchNode.identifierCode ?? batchNode.uid ?? "";
   if (nextUidNo !== null && nextUidNo !== undefined && String(nextUidNo).trim() !== "") {
      productForm.value.uidNo = nextUidNo;
   }
   const customers = (batchNode.children || [])
      .filter((c) => c.nodeType === "customer")
      .map((c) => c.customer ?? c.label ?? "")
      .filter(Boolean);
   const uniq = Array.from(new Set(customers));
   supplierOptions.value = uniq.map((s) => ({ label: s, value: s }));
   // 编辑场景尽量回显;新增场景不回显
   if (prevCustomer && uniq.includes(prevCustomer)) {
      productForm.value.customer = prevCustomer;
   } else {
      productForm.value.customer = "";
   }
};
// 根据名称反查产品大类 id,便于仅存名称时的反显
function findNodeIdByLabel(nodes, label) {
   if (!label) return null;
@@ -1276,10 +1434,6 @@
};
const batchNoOptions = ref([]);
const fetchBatchNoOptions = async () => {
   const res = await getProductOrderBatchNoOptions();
   batchNoOptions.value = res.data;
};
// 关闭弹框
const closeDia = () => {
   proxy.resetForm("formRef");
@@ -1303,25 +1457,44 @@
      productIndex.value = index;
      // 编辑时根据产品大类名称反查 tree 节点 id,并加载规格型号列表
      try {
         const options = productOptions.value && productOptions.value.length > 0
            ? productOptions.value
            : await getProductOptions();
         const categoryId = findNodeIdByLabel(options, productForm.value.productCategory);
         if (categoryId) {
            const models = await modelList({ id: categoryId });
            modelOptions.value = models || [];
            // 根据当前规格型号名称反查并设置 productModelId,便于下拉框显示已选值
            const currentModel = (modelOptions.value || []).find(
               (m) => m.model === productForm.value.specificationModel
            );
         if (!productOptions.value || productOptions.value.length === 0) {
            await getProductOptions();
         }
         // 回显:根据“产品大类”反查产品节点
         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 ?? "",
               }));
            modelOptions.value = models;
            // 根据当前规格型号回显
            const targetSpec = String(productForm.value.specificationModel ?? "").trim();
            const currentModel =
               (models || []).find((m) => String(m.model ?? "").trim() === targetSpec) ||
               (models || []).find((m) => String(m.model ?? "").trim().includes(targetSpec)) ||
               (models || []).find((m) => targetSpec.includes(String(m.model ?? "").trim()));
            if (currentModel) {
               productForm.value.customer = productForm.value.customer || row.customer || row.supplierName || "";
               productForm.value.productModelId = currentModel.id;
               getProductModel(currentModel.id);
            }
         }
      } catch (e) {
         // 加载失败时保持可编辑,不中断弹窗
         console.error("加载产品规格型号失败", e);
      }
      // 最终兜底:如果中途被重置清空,至少回显行数据里的 UID
      productForm.value.uidNo = row.uidNo ?? productForm.value.uidNo ?? "";
      // 最终兜底:同样保证单位不会因树数据缺失而被覆盖为空
      productForm.value.unit = row.unit ?? productForm.value.unit ?? "";
   } else {
      getProductOptions()
   }
@@ -2251,7 +2424,6 @@
onMounted(() => {
   getList();
  fetchBatchNoOptions();
   userListNoPage().then(res => {
      userList.value = res.data;
   })