spring
12 小时以前 b937241a2c20f62f45b31b232b6cebdec03d41d7
fix: 销售批号
已修改3个文件
328 ■■■■ 文件已修改
src/api/inventoryManagement/stockInventory.js 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementLedger/index.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/index.vue 304 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockInventory.js
@@ -65,4 +65,13 @@
        url: "/stockInventory/getMaterials",
        method: "get",
    });
}
}
// 获取库存树(产品->规格/型号->批号->供应商)
export const getStockInventoryAll = (params = {}) => {
    return request({
        url: "/stockInventory/getStockInventoryAll",
        method: "get",
        params,
    });
};
src/views/procurementManagement/procurementLedger/index.vue
@@ -410,6 +410,7 @@
                           prop="unit"
                           width="70" />
          <el-table-column label="UID码" prop="uidNo" />
          <el-table-column label="批次号" prop="batchNo" />
          <el-table-column label="数量"
                           prop="quantity"
                           width="70" />
@@ -580,6 +581,17 @@
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="批次号:"
                          prop="batchNo">
              <el-input v-model="productForm.batchNo"
                        placeholder="请输入"
                        disabled />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="单位:"
@@ -964,6 +976,7 @@
      productCategory: "",
      productModelId: "",
      uidNo: "",
        batchNo: "",
      specificationModel: "",
      unit: "",
      quantity: "",
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 = () => {
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,便于在编辑产品时等待加载完成
    return productTreeList().then((res) => {
        productOptions.value = convertIdToValue(res);
        return productOptions.value;
    });
    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 || "";
    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;
    }
    // 有些接口/树数据里可能不包含 uidNo,这种情况下不要覆盖编辑时已回显的值
    const nextUidNo = modelNode.uidNo ?? modelNode.identifierCode ?? "";
    if (nextUidNo !== null && nextUidNo !== undefined && String(nextUidNo).trim() !== "") {
        productForm.value.uidNo = nextUidNo;
    }
    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.specificationModel = null;
        productForm.value.unit = null;
        productForm.value.uidNo = null;
        productForm.value.customer = "";
    }
};
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);
        }
        return newItem;
    });
}
// 根据名称反查产品大类 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;
    })