yuan
6 天以前 4a811fb2cd4ee4e1cbfe284bfd1fe3a7d16204ce
src/views/salesManagement/salesLedger/index.vue
@@ -33,7 +33,7 @@
          <el-button type="primary" plain @click="handleImport">导入</el-button>
          <el-button @click="handleOut">导出</el-button>
          <el-button type="danger" plain @click="handleDelete">删除</el-button>
          <el-button type="primary" plain @click="handlePrint">打印</el-button>
          <!-- <el-button type="primary" plain @click="handlePrint">打印</el-button> -->
        </div>
      </div>
      <el-table :data="tableData" border v-loading="tableLoading" @selection-change="handleSelectionChange"
@@ -112,8 +112,8 @@
        <el-table-column label="销售合同号" prop="salesContractNo" width="180" show-overflow-tooltip />
        <el-table-column label="客户名称" prop="customerName" width="300" show-overflow-tooltip />
        <el-table-column label="业务员" prop="salesman" width="100" show-overflow-tooltip />
        <el-table-column label="项目名称" prop="projectName" width="180" show-overflow-tooltip />
        <el-table-column label="付款方式" prop="paymentMethod" show-overflow-tooltip />
        <!-- <el-table-column label="项目名称" prop="projectName" width="180" show-overflow-tooltip />
        <el-table-column label="付款方式" prop="paymentMethod" show-overflow-tooltip /> -->
        <el-table-column label="合同金额(元)" prop="contractAmount" width="220" show-overflow-tooltip
          :formatter="formattedNumber" />
        <el-table-column label="录入人" prop="entryPersonName" width="100" show-overflow-tooltip />
@@ -173,24 +173,12 @@
            </el-form-item>
          </el-col>
               <el-col :span="12">
                  <el-form-item label="项目名称:" prop="projectName">
                     <el-input v-model="form.projectName" placeholder="请输入" clearable :disabled="operationType === 'view'" />
                  </el-form-item>
               </el-col>
        </el-row>
        <el-row :gutter="30">
               <el-col :span="12">
                  <el-form-item label="签订日期:" prop="executionDate">
                     <el-date-picker style="width: 100%" v-model="form.executionDate" value-format="YYYY-MM-DD"
                                             format="YYYY-MM-DD" type="date" placeholder="请选择" clearable :disabled="operationType === 'view'" />
                  </el-form-item>
               </el-col>
               <el-col :span="12">
                  <el-form-item label="付款方式">
                     <el-input v-model="form.paymentMethod" placeholder="请输入" clearable :disabled="operationType === 'view'" />
                  </el-form-item>
               </el-col>
            </el-row>
        </el-row>
            <el-row :gutter="30">
               <el-col :span="12">
                  <el-form-item label="录入人:" prop="entryPerson">
@@ -311,7 +299,7 @@
            <el-table-column prop="customer" label="客户名称" min-width="220" show-overflow-tooltip />
            <el-table-column prop="salesperson" label="业务员" width="120" show-overflow-tooltip />
            <el-table-column prop="quotationDate" label="报价日期" width="140" />
            <el-table-column prop="status" label="审批状态" width="120" align="center" />
            <!-- <el-table-column prop="status" label="审批状态" width="120" align="center" /> -->
            <el-table-column prop="totalAmount" label="报价金额(元)" width="160" align="right">
               <template #default="scope">
                  {{ Number(scope.row.totalAmount ?? 0).toFixed(2) }}
@@ -376,8 +364,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>
@@ -620,51 +625,6 @@
                  </el-form-item>
               </el-col>
            </el-row>
        <!-- 审批人选择(仿协同审批里的审批人节点选择) -->
        <el-row>
          <el-col :span="24">
            <el-form-item>
              <template #label>
                <span>审批人选择:</span>
                <el-button type="primary" @click="addApproverNode" style="margin-left: 8px;">新增节点</el-button>
              </template>
              <div style="display: flex; align-items: flex-end; flex-wrap: wrap;">
                <div
                  v-for="(node, index) in approverNodes"
                  :key="node.id"
                  style="margin-right: 20px; text-align: center; margin-bottom: 10px;"
                >
                  <div>
                    <span>审批人</span>
                    →
                  </div>
                  <el-select
                    v-model="node.userId"
                    placeholder="选择人员"
                    filterable
                    style="width: 140px; margin-bottom: 8px;"
                  >
                    <el-option
                      v-for="user in userList"
                      :key="user.userId"
                      :label="user.nickName"
                      :value="user.userId"
                    />
                  </el-select>
                  <div>
                    <el-button
                      type="danger"
                      size="small"
                      @click="removeApproverNode(index)"
                      v-if="approverNodes.length > 1"
                    >删除</el-button>
                  </div>
                </div>
              </div>
            </el-form-item>
          </el-col>
        </el-row>
         </el-form>
         <template #footer>
            <div class="dialog-footer">
@@ -699,11 +659,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();
@@ -716,6 +676,7 @@
const customerOption = ref([]);
const productOptions = ref([]);
const modelOptions = ref([]);
const supplierOptions = ref([]);
const tableLoading = ref(false);
const page = reactive({
   current: 1,
@@ -764,6 +725,7 @@
const productFormData = reactive({
   productForm: {
      productCategory: "",
      customer: "",
      specificationModel: "",
    uidNo: "",
      unit: "",
@@ -779,6 +741,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" },
      ],
@@ -947,64 +910,203 @@
         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.productModelId ?? 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.productModelId = modelNode.value;
   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;
@@ -1198,8 +1300,9 @@
      return {
         // 台账字段
         productCategory: p.product || p.productName || "",
         productModelId: p.productModelId || "",
         specificationModel: p.specification || "",
      uidNo: p.uidNo || "",
         uidNo: p.uidNo || "",
         unit: p.unit || "",
         quantity: quantity,
         taxRate: taxRate,
@@ -1278,10 +1381,6 @@
};
const batchNoOptions = ref([]);
const fetchBatchNoOptions = async () => {
   const res = await getProductOrderBatchNoOptions();
   batchNoOptions.value = res.data;
};
// 关闭弹框
const closeDia = () => {
   proxy.resetForm("formRef");
@@ -1305,25 +1404,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()
   }
@@ -2167,13 +2285,6 @@
const submitDelivery = () => {
  proxy.$refs["deliveryFormRef"].validate((valid) => {
    if (valid) {
      // 审批人必填校验(所有节点都要选人)
      const hasEmptyApprover = approverNodes.value.some(node => !node.userId);
      if (hasEmptyApprover) {
        proxy.$modal.msgError("请为所有审批节点选择审批人!");
        return;
      }
      const approveUserIds = approverNodes.value.map(node => node.userId).join(",");
      // 保存当前展开的行ID,以便发货后重新加载子表格数据
      const currentExpandedKeys = [...expandedRowKeys.value];
      const salesLedgerId = currentDeliveryRow.value.salesLedgerId;
@@ -2181,7 +2292,6 @@
        salesLedgerId: salesLedgerId,
        salesLedgerProductId: currentDeliveryRow.value.id,
        type: deliveryForm.value.type,
            approveUserIds,
      })
        .then(() => {
          proxy.$modal.msgSuccess("发货成功");
@@ -2255,7 +2365,6 @@
onMounted(() => {
   getList();
  fetchBatchNoOptions();
   userListNoPage().then(res => {
      userList.value = res.data;
   })