ソースを参照

perf: 优化物品明细表大批量回显性能

- product-list: 库存/供应商/包装规格三个请求改为并行,选择后先关弹框再回显
- product-list: getSupplierObj 补齐 categoryIds 与 supplier.id 去重,修复重复 key 报错
- inventoryTable: 变更回调重构为 buildProductRow,只重算受影响行,避免整表深拷贝
- inventoryTable: 批量回显改为分批追加(appendBatchSize,默认 20),逐帧让出主线程
- inventoryTable: 自绘 loading 遮罩,全部批次渲染完成后才关闭(pro-table 内置 loading 会被 reload 重置)
- inventoryTable: 分批追加期间禁用会改动数据源的操作,滚动与列设置保持可用
- inventoryTable: 附件组件启用 lazy-render,避免每行挂载 4 套弹框与嵌套表格
- addDoc: 新增 lazyRender 参数(默认关闭),弹框内容懒渲染
- setProduct: 修复无包装规格/仅一条包装规格时读取 conversionUnit 报错
- store/dict: requestDict 支持 dedupe 参数合并并发请求,默认不开启
xieyong 1 週間 前
親
コミット
458b1a1918

+ 493 - 290
src/BIZComponents/inventoryTable.vue

@@ -1,11 +1,15 @@
 <template>
   <el-form ref="form" :model="form" :rules="rules">
+    <!-- 自己渲染 loading 遮罩:ele-pro-table 内部的 loading 会在每次 reload 时被重置 -->
+    <div class="inventory-table-wrap">
     <ele-pro-table
       ref="table"
       :needPage="false"
+      row-key="key"
       :columns="columns"
       :datasource="form.datasource"
       class="time-form"
+      :class="{ 'time-form-busy': tableBusy }"
       height="300"
       full-height="calc(100vh - 76px)"
       @columns-change="handleColumnChange"
@@ -24,6 +28,7 @@
               type="primary"
               icon="el-icon-plus"
               class="ele-btn-icon"
+              :disabled="tableBusy"
               @click="handParent('', -1)"
             >
               新增
@@ -34,6 +39,7 @@
               icon="el-icon-plus"
               class="ele-btn-icon"
               v-if="isGoods"
+              :disabled="tableBusy"
               @click="handGoods(-1)"
             >
               选择商品
@@ -44,6 +50,7 @@
               icon="el-icon-plus"
               class="ele-btn-icon"
               v-if="isTemporary"
+              :disabled="tableBusy"
               @click="handlAdd"
             >
               新增临时产品
@@ -54,23 +61,24 @@
               class="ele-btn-icon"
               @click="save"
               v-if="isDrawer"
+              :disabled="tableBusy"
             >
               保存
             </el-button>
             <setAllValue
-              :disabled="!selection.length"
+              :disabled="tableBusy || !selection.length"
               title="客户期望交期"
               @success="setAllValueChange"
               valueKey="customerExpectDeliveryDeadline"
             />
             <setAllValue
-              :disabled="!selection.length"
+              :disabled="tableBusy || !selection.length"
               title="生产交付交期"
               valueKey="produceDeliveryDeadline"
               @success="setAllValueChange"
             />
             <setAllValue
-              :disabled="!selection.length"
+              :disabled="tableBusy || !selection.length"
               inputType="number"
               title="税率"
               valueKey="taxRate"
@@ -81,6 +89,9 @@
             <span class="amount" v-if="showSummary"
               >数量合计:{{ allQuantity }}</span
             >
+            <span class="amount" v-if="batchLoading"
+              >正在加载:{{ loadedCount }}/{{ totalCount }}</span
+            >
             <span class="amount">总计:{{ allPrice }}元</span>
 
             <el-form-item
@@ -367,14 +378,17 @@
         <el-form-item
           :prop="'datasource.' + scope.$index + '.technicalDrawings'"
         >
-          <fileMain v-model="scope.row.technicalDrawings"></fileMain>
+          <fileMain
+            v-model="scope.row.technicalDrawings"
+            lazy-render
+          ></fileMain>
         </el-form-item>
       </template>
       <template v-slot:customerReqFiles="scope">
         <el-form-item
           :prop="'datasource.' + scope.$index + '.customerReqFiles'"
         >
-          <fileMain v-model="scope.row.customerReqFiles"></fileMain>
+          <fileMain v-model="scope.row.customerReqFiles" lazy-render></fileMain>
         </el-form-item>
       </template>
       <template v-slot:industryArtFiles="scope">
@@ -386,12 +400,12 @@
             trigger: 'change'
           }"
         >
-          <fileMain v-model="scope.row.industryArtFiles"></fileMain>
+          <fileMain v-model="scope.row.industryArtFiles" lazy-render></fileMain>
         </el-form-item>
       </template>
       <template v-slot:otherFiles="scope">
         <el-form-item :prop="'datasource.' + scope.$index + '. otherFiles'">
-          <fileMain v-model="scope.row.otherFiles"></fileMain>
+          <fileMain v-model="scope.row.otherFiles" lazy-render></fileMain>
         </el-form-item>
       </template>
       <template v-slot:headerProduceDeliveryDeadline="{ column }">
@@ -953,16 +967,34 @@
         <el-popconfirm
           class="ele-action"
           title="确定要删除吗?"
+          :disabled="tableBusy"
           @confirm="remove(scope.$index)"
         >
           <template v-slot:reference>
-            <el-link type="danger" :underline="false" icon="el-icon-delete">
+            <el-link
+              type="danger"
+              :underline="false"
+              :disabled="tableBusy"
+              icon="el-icon-delete"
+            >
               删除
             </el-link>
           </template>
         </el-popconfirm>
       </template>
     </ele-pro-table>
+      <!-- 遮罩放在表格之后,保证盖在内容上方;v-if 直接绑定 tableLoading,全部追加完成后才卸载 -->
+      <div v-if="tableLoading" class="table-loading-mask">
+        <i class="el-icon-loading"></i>
+        <span class="table-loading-text">
+          {{
+            batchLoading
+              ? `正在加载 ${loadedCount}/${totalCount}`
+              : '正在加载,请稍候'
+          }}
+        </span>
+      </div>
+    </div>
     <product-list
       ref="productListRef"
       classType="1"
@@ -1214,6 +1246,11 @@
       showStockCount: {
         type: Boolean,
         default: false
+      },
+      // 一次追加的行数较多时,按批渲染,避免一次渲染整表长时间阻塞主线程
+      appendBatchSize: {
+        type: Number,
+        default: 20
       }
     },
     data() {
@@ -1243,6 +1280,11 @@
       return {
         levelList,
         taskinstanceDialogFlag: false,
+        tableLoading: false,
+        // 分批追加进度
+        batchLoading: false,
+        loadedCount: 0,
+        totalCount: 0,
         allPrice: 0,
         allQuantity: 0,
         numberReg,
@@ -1267,6 +1309,10 @@
       };
     },
     computed: {
+      // 批量追加进行中:锁住会改动数据源的操作,避免行下标错位
+      tableBusy() {
+        return this.batchLoading;
+      },
       canHandl() {
         return this.form.datasource.length;
       },
@@ -1278,7 +1324,11 @@
         const cacheWidths = this.getStorage(this.cacheKeyUrl + 'Widths') || {};
         const applyWidth = (col) => {
           if (col.prop && cacheWidths[col.prop]) {
-            return { ...col, width: cacheWidths[col.prop], minWidth: undefined };
+            return {
+              ...col,
+              width: cacheWidths[col.prop],
+              minWidth: undefined
+            };
           }
           return col;
         };
@@ -1290,7 +1340,9 @@
             type: 'selection',
             columnKey: 'selection',
             align: 'center',
-            fixed: 'left'
+            fixed: 'left',
+            // 分批追加会多次替换数据源,靠 rowKey 保住勾选状态
+            reserveSelection: true
           },
           {
             width: 65,
@@ -2094,7 +2146,9 @@
             productName: item.productName
           };
           group.rows.push(index);
-          group.requiredQuantity += Number(item.totalCount || item.saleCount || 0);
+          group.requiredQuantity += Number(
+            item.totalCount || item.saleCount || 0
+          );
           inventoryGroups.set(identity, group);
         });
         const groups = Array.from(inventoryGroups.values());
@@ -2123,12 +2177,11 @@
               availableCountBase
             );
           });
-          if (
-            validateQuantity &&
-            group.requiredQuantity > availableCountBase
-          ) {
+          if (validateQuantity && group.requiredQuantity > availableCountBase) {
             insufficient.push(
-              `${group.productCode}-${group.productName || ''}需求${group.requiredQuantity},可用${availableCountBase}`
+              `${group.productCode}-${group.productName || ''}需求${
+                group.requiredQuantity
+              },可用${availableCountBase}`
             );
           }
         });
@@ -2266,7 +2319,6 @@
       },
 
       changeAll() {
-        console.log('changeAll', this.form.datasource);
         this.allPrice = getAllPrice(this.form.datasource) || 0;
         this.allQuantity =
           getAllQuantity(this.form.datasource, this.countObj) || 0;
@@ -2364,6 +2416,7 @@
       },
       //批量设置税率
       setAllTaxRateChange({ key, value }) {
+        if (this.tableBusy) return;
         let indexS = this.selection.map((item) => item.key - 1);
         indexS.forEach((i) => {
           this.$set(this.form.datasource[i], key, value);
@@ -2371,6 +2424,7 @@
         });
       },
       setAllValueChange({ key, value }) {
+        if (this.tableBusy) return;
         let indexS = this.selection.map((item) => item.key - 1);
         indexS.forEach((i) => {
           this.$set(this.form.datasource[i], key, value);
@@ -2525,6 +2579,7 @@
 
       //选择产品
       handParent(row, index) {
+        if (this.tableBusy) return;
         let item = {
           id: row?.productCode
         };
@@ -2539,6 +2594,7 @@
         }
       },
       handGoods(index) {
+        if (this.tableBusy) return;
         this.$refs.commodityPriceListDialogRef.open(index);
       },
       //选择技术人回调
@@ -2552,295 +2608,283 @@
         };
         this.$refs.headRef.open(item, index);
       },
-      //选择产品回调
-      changeParent(obj, idx) {
-        obj.forEach((item, index) => {
-          let i = idx == -1 ? index : idx;
-          let row = JSON.parse(JSON.stringify(this.defaultForm));
-          row.key = this.form.datasource.length + 1;
-          let parasm = idx == -1 ? row : this.form.datasource[i];
-
-          this.$set(parasm, 'productId', item.id);
-          this.$set(parasm, 'categoryName', item.name);
-          this.$set(parasm, 'productCategoryId', item.categoryLevelId);
-          this.$set(parasm, 'productBrand', item.brandNum);
-          this.$set(parasm, 'productCategoryName', item.categoryLevelPath);
-          this.$set(parasm, 'productCode', item.code);
-          this.$set(parasm, 'productName', item.name);
-          this.$set(parasm, 'modelType', item.modelType);
-          if (this.showStockCount) {
-            this.$set(parasm, 'stockCountBase', this.getStockCount(item));
-          }
-          this.$set(
-            parasm,
-            'availableCountBase',
-            this.showStockCount
-              ? this.getAvailableStockCount(item)
-              : item.availableCountBase
-          );
-          this.$set(parasm, 'measuringUnit', item.measuringUnit);
-          this.$set(parasm, 'specification', item.specification);
-          this.$set(parasm, 'weightUnit', item.weightUnit);
-          this.$set(parasm, 'singleWeight', item.netWeight);
-          this.$set(parasm, 'pricingWay', 1);
-          this.$set(parasm, 'goodsLevel', item.goodsLevel);
-          this.$set(parasm, 'guaranteePeriod', item.warrantyPeriod);
-          this.$set(
-            parasm,
-            'customerExpectDeliveryDeadline',
-            dayjs(new Date()).format('YYYY-MM-DD')
-          );
-          // console.log('colorKey~~', item.colorKey);
-          this.$set(parasm, 'modelKey', item.modelKey?.split(',')?.[0] || '');
-          this.$set(parasm, 'colorKey', item.colorKey?.split(',')?.[0] || '');
-          this.$set(
-            parasm,
-            'technologyRouteName',
-            item.technologyRouteName || ''
-          );
-          this.$set(parasm, 'technologyRouteId', item.technologyRouteId || '');
-          if (this.defTaxRate) {
-            this.$set(
-              parasm,
-              'taxRate',
-              parasm.taxRate !== undefined && parasm.taxRate !== null
-                ? parasm.taxRate
-                : this.isContractBook
-                ? this.defTaxRate
-                : this.isTaxRate == 1
-                ? this.defTaxRate
-                : undefined
-            );
-          }
-          this.$set(
-            parasm,
-            'guaranteePeriodUnitCode',
-            item.warrantyPeriodUnit ? item.warrantyPeriodUnit + '' : ''
-          );
-          if (item.warrantyPeriod && item.warrantyPeriodUnit) {
-            this.$set(
-              parasm,
-              'guaranteePeriodDeadline',
-              this.setDay(
-                item.warrantyPeriod,
-                this.guaranteePeriodUnit(item.warrantyPeriodUnit),
-                {}
-              )
-            );
-          }
-
-          this.$set(
-            parasm,
-            'packageDispositionList',
-            item.packageDispositionList
-          );
-          if (item.packageDispositionList?.length) {
-            this.$set(
-              parasm,
-              this.countObj.unitIdKey,
-              item.packageDispositionList[0].id
-            );
-            this.$set(
-              parasm,
-              this.countObj.unitKey,
-              item.packageDispositionList[0].conversionUnit
-            );
-          }
-
-          this.$set(parasm, 'arrivalWay', item.arrivalWay || 1);
-
-          this.$set(parasm, 'imgCode', item.imgCode);
-          this.$set(parasm, 'produceType', item.componentAttribute);
-          if (this.isOrderNo) {
-            this.$set(parasm, 'orderNo', this.orderOption[0]?.orderNo);
-            this.$set(parasm, 'orderId', this.orderOption[0]?.orderId);
-          }
-          if (this.isGoods && item.goodsId) {
-            this.$set(parasm, 'goodsId', item.goodsId);
-            this.$set(parasm, 'goodsPriceId', item.goodsPriceId);
-            this.$set(parasm, 'goodsPriceType', item.goodsPriceType);
-            this.$set(parasm, 'singlePrice', item.singlePrice);
-            this.$set(parasm, 'notaxSinglePrice', item.notaxSinglePrice);
-            this.$set(parasm, 'taxRate', item.taxRate);
-            this.$set(parasm, 'discountSinglePrice', item.singlePrice);
-            this.$set(parasm, 'totalCount', '');
-            this.$set(parasm, 'discountRatio', item.discountRatio);
-            this.$set(parasm, 'quoteWay', item.quoteWay || 1);
-          } else {
-            this.$set(parasm, 'singlePrice', parasm.singlePrice || 0);
-            this.$set(parasm, 'discountRatio', parasm.discountRatio || 100);
-            this.$set(parasm, 'quoteWay', parasm.quoteWay || 1);
-          }
-
-          // 初始化生产加工特有字段及未税小记
-          // 当quoteType为2时,数量默认设置为1
-          if (this.quoteType === 2) {
-            this.processSetValue(parasm);
-          }
-
-          if (this.isSupplier) {
-            this.$set(
-              parasm,
-              'entrustedEnterpriseIdList',
-              item.entrustedEnterpriseIdList
-            );
-            this.$set(
-              parasm,
-              'entrustedEnterpriseId',
-              item.entrustedEnterpriseId
-            );
-          }
-          this.$set(parasm, 'approvalNumber', item.extField?.approvalNumber);
-          this.$set(
-            parasm,
-            'packingSpecification',
-            item.extField.packingSpecification
-          );
-          this.$set(parasm, 'customerMark', this.customerMark);
-          if (item.purchaseOrigins?.length > 0) {
-            item.purchaseOrigins = item.purchaseOrigins.map((val) => val + '');
-          }
-          this.$set(parasm, 'provenance', item.purchaseOrigins || []);
-
-          if (idx == -1) {
-            this.form.datasource.push(row);
-          }
+      buildProductRow(item, oldRow, key) {
+        const row = oldRow ? copyObj(oldRow) : copyObj(this.defaultForm);
+        const warrantyPeriodUnit = item.warrantyPeriodUnit
+          ? item.warrantyPeriodUnit + ''
+          : '';
+
+        Object.assign(row, {
+          key,
+          productId: item.id,
+          categoryName: item.name,
+          productCategoryId: item.categoryLevelId,
+          productBrand: item.brandNum,
+          productCategoryName: item.categoryLevelPath,
+          productCode: item.code,
+          productName: item.name,
+          modelType: item.modelType,
+          availableCountBase: this.showStockCount
+            ? this.getAvailableStockCount(item)
+            : item.availableCountBase,
+          measuringUnit: item.measuringUnit,
+          specification: item.specification,
+          weightUnit: item.weightUnit,
+          singleWeight: item.netWeight,
+          pricingWay: 1,
+          goodsLevel: item.goodsLevel,
+          guaranteePeriod: item.warrantyPeriod,
+          customerExpectDeliveryDeadline: dayjs(new Date()).format(
+            'YYYY-MM-DD'
+          ),
+          modelKey: item.modelKey?.split(',')?.[0] || '',
+          colorKey: item.colorKey?.split(',')?.[0] || '',
+          technologyRouteName: item.technologyRouteName || '',
+          technologyRouteId: item.technologyRouteId || '',
+          guaranteePeriodUnitCode: warrantyPeriodUnit,
+          packageDispositionList: item.packageDispositionList,
+          arrivalWay: item.arrivalWay || 1,
+          imgCode: item.imgCode,
+          produceType: item.componentAttribute,
+          approvalNumber: item.extField?.approvalNumber,
+          packingSpecification: item.extField?.packingSpecification,
+          customerMark: this.customerMark,
+          provenance: item.purchaseOrigins?.map((val) => val + '') || []
         });
 
-        console.log('changeParent~~~', this.form.datasource);
-        this.changeCount();
-      },
-      async replaceTable(obj, idx) {
-        console.log('replaceTable', obj, idx);
-        // 先按物品ID去重并并行查询商品信息,避免逐行串行请求,物品较多时明显加快回显
-        const goodsMap = await this.loadGoodsByCategoryIds(obj);
-        for (let index = 0; index < obj.length; index++) {
-          const item = obj[index];
-          let i = idx == -1 ? index : idx;
-          let row = JSON.parse(JSON.stringify(this.defaultForm));
-          row.key = this.form.datasource.length + 1;
-          let parasm = idx == -1 ? row : this.form.datasource[i];
-          this.$set(parasm, 'customerMark', this.customerMark);
-          this.$set(parasm, 'saleCount', item.saleCount);
-          this.$set(parasm, 'productId', item.productId);
-          this.$set(parasm, 'categoryName', item.productName);
-          this.$set(parasm, 'productCategoryId', item.categoryLevelId);
-          this.$set(parasm, 'productBrand', item.productBrand);
-          this.$set(parasm, 'productCategoryName', item.categoryLevelPath);
-          this.$set(parasm, 'productCode', item.productCode);
-          this.$set(parasm, 'productName', item.productName);
-          this.$set(parasm, 'modelType', item.categoryModel);
-          this.$set(parasm, 'warehouseId', item.warehouseId);
-          this.$set(parasm, 'warehouseName', item.warehouseName);
-          if (this.showStockCount) {
-            this.$set(parasm, 'stockCountBase', this.getStockCount(item));
-          }
-          this.$set(
-            parasm,
-            'availableCountBase',
-            this.showStockCount
-              ? this.getAvailableStockCount(item)
-              : item.measureQuantity
-          );
-          this.$set(parasm, 'measuringUnit', item.measuringUnit);
-          this.$set(parasm, 'specification', item.specification);
-          this.$set(parasm, 'weightUnit', item.weightUnit);
-          this.$set(parasm, 'singleWeight', item.weight);
-          this.$set(parasm, 'pricingWay', 1);
-          this.$set(parasm, 'arrivalWay', item.arrivalWay || 1);
-          this.$set(parasm, 'batchNo', item.batchNo);
-          this.$set(parasm, 'systemBatchNo', item.systemBatchNo);
-          this.$set(parasm, 'imgCode', '');
-          this.$set(parasm, 'produceType', item.componentAttribute);
-          this.$set(parasm, 'reelSpecification', item.reelSpecification);
-          this.$set(
-            parasm,
-            'taxRate',
-            item.taxRate !== undefined && item.taxRate !== null
-              ? item.taxRate
+        if (this.showStockCount) {
+          row.stockCountBase = this.getStockCount(item);
+        }
+        if (this.defTaxRate) {
+          row.taxRate =
+            row.taxRate !== undefined && row.taxRate !== null
+              ? row.taxRate
               : this.isContractBook
               ? this.defTaxRate
               : this.isTaxRate == 1
               ? this.defTaxRate
-              : undefined
+              : undefined;
+        }
+        if (item.warrantyPeriod && item.warrantyPeriodUnit) {
+          row.guaranteePeriodDeadline = this.setDay(
+            item.warrantyPeriod,
+            this.guaranteePeriodUnit(item.warrantyPeriodUnit),
+            {}
           );
-          let goodsData = goodsMap.get(item.productId) || [];
-          goodsData = goodsData.filter((item) => item.approvalStatus == 2);
-          if (goodsData?.length) {
-            let goodsPriceList = goodsData[0]?.goodsPriceList[0];
-            this.$set(parasm, 'goodsId', goodsData[0].id);
-            this.$set(parasm, 'goodsPriceId', goodsPriceList.id);
-            this.$set(parasm, 'goodsPriceType', goodsPriceList.priceType);
-            this.$set(parasm, 'singlePrice', goodsPriceList.unitPrice || 0);
-            this.$set(
-              parasm,
-              'notaxSinglePrice',
-              goodsPriceList.excludeTaxPrice
-            );
-            this.$set(parasm, 'taxRate', goodsPriceList.taxRate);
-            this.$set(parasm, 'discountSinglePrice', goodsPriceList.unitPrice);
-            this.$set(parasm, 'totalCount', '');
-          } else {
-            this.$set(parasm, 'singlePrice', 0);
-          }
-
-          this.$set(parasm, 'modelKey', item.modelKey?.split(',')?.[0] || '');
-
-          this.$set(parasm, 'colorKey', item.colorKey?.split(',')?.[0] || '');
+        }
+        if (item.packageDispositionList?.length) {
+          row[this.countObj.unitIdKey] = item.packageDispositionList[0].id;
+          row[this.countObj.unitKey] =
+            item.packageDispositionList[0]?.conversionUnit || '';
+        }
+        if (this.isOrderNo) {
+          row.orderNo = this.orderOption[0]?.orderNo;
+          row.orderId = this.orderOption[0]?.orderId;
+        }
+        if (this.isGoods && item.goodsId) {
+          Object.assign(row, {
+            goodsId: item.goodsId,
+            goodsPriceId: item.goodsPriceId,
+            goodsPriceType: item.goodsPriceType,
+            singlePrice: item.singlePrice,
+            notaxSinglePrice: item.notaxSinglePrice,
+            taxRate: item.taxRate,
+            discountSinglePrice: item.singlePrice,
+            totalCount: '',
+            discountRatio: item.discountRatio,
+            quoteWay: item.quoteWay || 1
+          });
+        } else {
+          row.singlePrice = row.singlePrice || 0;
+          row.discountRatio = row.discountRatio || 100;
+          row.quoteWay = row.quoteWay || 1;
+        }
+        if (this.quoteType === 2) {
+          this.processSetValue(row);
+        }
+        if (this.isSupplier) {
+          row.entrustedEnterpriseIdList = item.entrustedEnterpriseIdList;
+          row.entrustedEnterpriseId = item.entrustedEnterpriseId;
+        }
 
-          if (this.isSupplier) {
-            this.$set(
-              parasm,
-              'entrustedEnterpriseIdList',
-              item.entrustedEnterpriseIdList
-            );
-            this.$set(
-              parasm,
-              'entrustedEnterpriseId',
-              item.entrustedEnterpriseId
+        return row;
+      },
+      /**
+       * 重算表格。dirtyIndexes 只指定本次真正变动的行下标,
+       * 其余行直接复用原对象引用,省掉深拷贝与重复观测的成本。
+       * 不传 dirtyIndexes 时退化为全量重算(数量/税率等手工输入场景)。
+       */
+      recalculateTableOnce(dirtyIndexes) {
+        const dirty = Array.isArray(dirtyIndexes)
+          ? new Set(dirtyIndexes)
+          : null;
+        this.form.datasource = this.form.datasource.map((item, index) => {
+          if (dirty && !dirty.has(index)) {
+            return item;
+          }
+          const row = changeCount(item, this.countObj, false);
+          if (row.pricingWay == 3) {
+            row.totalPrice = formatPrice(
+              (row.increaseTotalWeight || 0) * row.singlePrice
             );
           }
-          this.$set(parasm, 'approvalNumber', item.extField?.approvalNumber);
-          this.$set(
-            parasm,
-            'packingSpecification',
-            item.extField.packingSpecification
+          row.notaxSinglePrice =
+            row.singlePrice && row.taxRate
+              ? formatPrice(row.singlePrice / (1 + row.taxRate / 100))
+              : '';
+          return row;
+        });
+        this.changeAll();
+      },
+      /**
+       * 大批量回显时先让 loading 遮罩绘制出来,再执行会阻塞主线程的计算。
+       * pro-table 内部在同一个 tick 里把 tableLoading 置 true 又置 false,
+       * 遮罩来不及显示,所以这里必须跨帧控制。
+       */
+      runAfterPaint(task) {
+        this._loadingStart = Date.now();
+        this.tableLoading = true;
+        this.$nextTick(() => {
+          requestAnimationFrame(task);
+        });
+      },
+      // 保证遮罩最短展示时间,避免一闪而过
+      endTableLoading(minDuration = 200) {
+        const start = this._loadingStart || Date.now();
+        const rest = minDuration - (Date.now() - start);
+        if (rest > 0) {
+          setTimeout(() => {
+            this.tableLoading = false;
+          }, rest);
+        } else {
+          this.tableLoading = false;
+        }
+      },
+      //选择产品回调
+      changeParent(obj, idx) {
+        if (idx == -1) {
+          // 新增:先构造好所有行,再分批追加并渲染
+          const startIndex = this.form.datasource.length;
+          const maxKey = this.form.datasource.reduce(
+            (max, item) => Math.max(max, Number(item.key) || 0),
+            0
           );
-
-          this.$set(
-            parasm,
-            'packageDispositionList',
-            item.packageDispositionList
+          const rows = obj.map((item, index) =>
+            this.buildProductRow(item, null, maxKey + index + 1)
           );
-          if (item.packageDispositionList?.length) {
-            this.$set(
-              parasm,
-              this.countObj.unitIdKey,
-              item.packageDispositionList[0].id
-            );
-            this.$set(
-              parasm,
-              this.countObj.unitKey,
-              item.packageDispositionList[0].conversionUnit
-            );
+          this.runAfterPaint(() => this.appendRowsInBatches(rows, startIndex));
+          return;
+        }
+        // 编辑已有行:单行替换
+        this.runAfterPaint(() => {
+          try {
+            this.updateProductRow(obj[0], idx);
+          } finally {
+            this.endTableLoading();
           }
-
-          this.$set(parasm, 'customerMark', this.customerMark);
-          if (item.purchaseOrigins?.length > 0) {
-            item.purchaseOrigins = item.purchaseOrigins.map((val) => val + '');
+        });
+      },
+      updateProductRow(item, idx) {
+        this.$set(
+          this.form.datasource,
+          idx,
+          this.buildProductRow(
+            item,
+            this.form.datasource[idx],
+            this.form.datasource[idx]?.key || idx + 1
+          )
+        );
+        this.recalculateTableOnce([idx]);
+      },
+      /**
+       * 分批追加:一次选中上百个物品时,一次性塞进表格会让主线程长时间阻塞,
+       * 这里按 appendBatchSize 分批写入并重算,每批之间让出一帧,
+       * 浏览器可以正常绘制 loading 遮罩和已经渲染出来的行。
+       */
+      appendRowsInBatches(
+        rows,
+        startIndex = this.form.datasource.length,
+        onDone
+      ) {
+        if (!rows.length) {
+          if (onDone) onDone();
+          this.endTableLoading();
+          return;
+        }
+        const batchSize = Math.max(1, this.appendBatchSize || 20);
+        const total = rows.length;
+        this.batchLoading = true;
+        this.loadedCount = 0;
+        this.totalCount = total;
+        let done = 0;
+
+        const step = () => {
+          const batch = rows.slice(done, done + batchSize);
+          if (batch.length) {
+            const from = startIndex + done;
+            this.form.datasource = this.form.datasource.concat(batch);
+            // 只重算本批新增的行,其余行复用原对象
+            this.recalculateTableOnce(batch.map((row, i) => from + i));
+            done += batch.length;
+            this.loadedCount = done;
           }
-          this.$set(parasm, 'provenance', item.purchaseOrigins || []);
-
-          // 初始化生产加工特有字段及未税小记
-          // 当quoteType为2时,数量默认设置为1
-          if (this.quoteType === 2) {
-            this.processSetValue(parasm);
+          if (done < total) {
+            requestAnimationFrame(step);
+            return;
           }
-          if (idx == -1) {
-            this.form.datasource.push(row);
+          // 全部批次都写完并渲染完,才收尾关闭 loading
+          this.batchLoading = false;
+          if (onDone) onDone();
+          this.endTableLoading();
+        };
+
+        requestAnimationFrame(step);
+      },
+      async replaceTable(obj, idx) {
+        // 先按物品ID去重并并行查询商品信息,避免逐行串行请求,物品较多时明显加快回显
+        const goodsMap = await this.loadGoodsByCategoryIds(obj);
+        this.runAfterPaint(() => {
+          try {
+            if (idx == -1) {
+              // 新增:先把行构造在普通对象上(不触发任何渲染),最后统一分批入表
+              const startIndex = this.form.datasource.length;
+              const maxKey = this.form.datasource.reduce(
+                (max, item) => Math.max(max, Number(item.key) || 0),
+                0
+              );
+              const pendingRows = [];
+              for (let index = 0; index < obj.length; index++) {
+                pendingRows.push(
+                  this.buildStockLedgerRow(
+                    obj[index],
+                    goodsMap,
+                    maxKey + index + 1
+                  )
+                );
+              }
+              this.appendRowsInBatches(
+                pendingRows,
+                startIndex,
+                this.afterReplaceTable
+              );
+              return;
+            }
+            // 编辑已有行:就地更新
+            this.buildStockLedgerRow(
+              obj[0],
+              goodsMap,
+              this.form.datasource[idx]?.key || idx + 1,
+              this.form.datasource[idx]
+            );
+            this.afterReplaceTable();
+            this.endTableLoading();
+          } catch (error) {
+            this.endTableLoading();
+            throw error;
           }
-        }
-        // console.log(this.form.datasource);
+        });
+      },
+      afterReplaceTable() {
         // 计量数量为后端非空字段:只补齐留空的行,避免覆盖已按计量单位换算好的数据
         this.form.datasource.forEach((item) => {
           const count = item[this.countObj.countKey];
@@ -2859,6 +2903,122 @@
           this.changeCount();
         }
       },
+      /**
+       * 构造库存台账选择回显的一行。
+       * 不传 targetRow 时返回普通对象(尚未进入响应式数组,赋值零成本)。
+       */
+      buildStockLedgerRow(item, goodsMap, key, targetRow) {
+        let row = targetRow || JSON.parse(JSON.stringify(this.defaultForm));
+        row.key = key;
+        let parasm = row;
+        this.$set(parasm, 'customerMark', this.customerMark);
+        this.$set(parasm, 'saleCount', item.saleCount);
+        this.$set(parasm, 'productId', item.productId);
+        this.$set(parasm, 'categoryName', item.productName);
+        this.$set(parasm, 'productCategoryId', item.categoryLevelId);
+        this.$set(parasm, 'productBrand', item.productBrand);
+        this.$set(parasm, 'productCategoryName', item.categoryLevelPath);
+        this.$set(parasm, 'productCode', item.productCode);
+        this.$set(parasm, 'productName', item.productName);
+        this.$set(parasm, 'modelType', item.categoryModel);
+        this.$set(parasm, 'warehouseId', item.warehouseId);
+        this.$set(parasm, 'warehouseName', item.warehouseName);
+        if (this.showStockCount) {
+          this.$set(parasm, 'stockCountBase', this.getStockCount(item));
+        }
+        this.$set(
+          parasm,
+          'availableCountBase',
+          this.showStockCount
+            ? this.getAvailableStockCount(item)
+            : item.measureQuantity
+        );
+        this.$set(parasm, 'measuringUnit', item.measuringUnit);
+        this.$set(parasm, 'specification', item.specification);
+        this.$set(parasm, 'weightUnit', item.weightUnit);
+        this.$set(parasm, 'singleWeight', item.weight);
+        this.$set(parasm, 'pricingWay', 1);
+        this.$set(parasm, 'arrivalWay', item.arrivalWay || 1);
+        this.$set(parasm, 'batchNo', item.batchNo);
+        this.$set(parasm, 'systemBatchNo', item.systemBatchNo);
+        this.$set(parasm, 'imgCode', '');
+        this.$set(parasm, 'produceType', item.componentAttribute);
+        this.$set(parasm, 'reelSpecification', item.reelSpecification);
+        this.$set(
+          parasm,
+          'taxRate',
+          item.taxRate !== undefined && item.taxRate !== null
+            ? item.taxRate
+            : this.isContractBook
+            ? this.defTaxRate
+            : this.isTaxRate == 1
+            ? this.defTaxRate
+            : undefined
+        );
+        let goodsData = goodsMap.get(item.productId) || [];
+        goodsData = goodsData.filter((goods) => goods.approvalStatus == 2);
+        if (goodsData?.length) {
+          let goodsPriceList = goodsData[0]?.goodsPriceList[0];
+          this.$set(parasm, 'goodsId', goodsData[0].id);
+          this.$set(parasm, 'goodsPriceId', goodsPriceList.id);
+          this.$set(parasm, 'goodsPriceType', goodsPriceList.priceType);
+          this.$set(parasm, 'singlePrice', goodsPriceList.unitPrice || 0);
+          this.$set(
+            parasm,
+            'notaxSinglePrice',
+            goodsPriceList.excludeTaxPrice
+          );
+          this.$set(parasm, 'taxRate', goodsPriceList.taxRate);
+          this.$set(parasm, 'discountSinglePrice', goodsPriceList.unitPrice);
+          this.$set(parasm, 'totalCount', '');
+        } else {
+          this.$set(parasm, 'singlePrice', 0);
+        }
+
+        this.$set(parasm, 'modelKey', item.modelKey?.split(',')?.[0] || '');
+
+        this.$set(parasm, 'colorKey', item.colorKey?.split(',')?.[0] || '');
+
+        if (this.isSupplier) {
+          this.$set(
+            parasm,
+            'entrustedEnterpriseIdList',
+            item.entrustedEnterpriseIdList
+          );
+          this.$set(parasm, 'entrustedEnterpriseId', item.entrustedEnterpriseId);
+        }
+
+        this.$set(
+          parasm,
+          'packageDispositionList',
+          item.packageDispositionList
+        );
+        if (item.packageDispositionList?.length) {
+          this.$set(
+            parasm,
+            this.countObj.unitIdKey,
+            item.packageDispositionList[0].id
+          );
+          this.$set(
+            parasm,
+            this.countObj.unitKey,
+            item.packageDispositionList[0].conversionUnit
+          );
+        }
+
+        if (item.purchaseOrigins?.length > 0) {
+          item.purchaseOrigins = item.purchaseOrigins.map((val) => val + '');
+        }
+        this.$set(parasm, 'provenance', item.purchaseOrigins || []);
+
+        // 初始化生产加工特有字段及未税小计
+        // 当quoteType为2时,数量默认设置为1
+        if (this.quoteType === 2) {
+          this.processSetValue(parasm);
+        }
+
+        return parasm;
+      },
       /**
        * 批量查询物品对应的商品信息。
        * 按物品ID去重后并行请求,替代逐行 await 的串行查询,
@@ -2880,6 +3040,7 @@
         return goodsMap;
       },
       remove(index) {
+        if (this.tableBusy) return;
         this.form.datasource.splice(index, 1);
         this.setSort();
         this.changeAll();
@@ -2893,6 +3054,7 @@
       },
       // 添加临时产品
       handlAdd() {
+        if (this.tableBusy) return;
         let item = JSON.parse(JSON.stringify(this.defaultForm));
         item.key = this.form.datasource.length + 1;
         item.customerMark = this.customerMark;
@@ -3022,6 +3184,47 @@
     background-color: #ffffff;
   }
 
+  .inventory-table-wrap {
+    position: relative;
+  }
+
+  .table-loading-mask {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    z-index: 10;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    background-color: rgba(255, 255, 255, 0.75);
+  }
+
+  .table-loading-mask .el-icon-loading {
+    font-size: 30px;
+    color: #409eff;
+  }
+
+  .table-loading-text {
+    margin-top: 10px;
+    font-size: 13px;
+    color: #909399;
+  }
+
+  /* 批量追加期间禁止单元格交互:只关掉 .cell 的指针事件,
+     滚轮事件仍会落在滚动容器上,所以表格照常可以滚动 */
+  ::v-deep .time-form-busy .el-table__body-wrapper .cell,
+  ::v-deep .time-form-busy .el-table__fixed-body-wrapper .cell {
+    pointer-events: none;
+  }
+
+  ::v-deep .time-form-busy .el-table__body-wrapper,
+  ::v-deep .time-form-busy .el-table__fixed-body-wrapper {
+    cursor: progress;
+  }
+
   .pricebox {
     display: flex;
     justify-content: flex-start;

+ 101 - 44
src/BIZComponents/product-list.vue

@@ -185,7 +185,13 @@
     </el-card>
 
     <div slot="footer">
-      <el-button type="primary" size="small" @click="selected">选择</el-button>
+      <el-button
+        type="primary"
+        size="small"
+        :loading="selecting"
+        @click="selected"
+        >选择</el-button
+      >
       <el-button size="small" @click="handleClose">关闭</el-button>
     </div>
     <c-bom-list
@@ -287,6 +293,7 @@
     data() {
       return {
         visible: false,
+        selecting: false,
         currentIndex: null,
         radio: null,
         cBomListDialogFlag: null,
@@ -665,13 +672,32 @@
       },
       async getSupplierObj(productList, queryName) {
         try {
-          let categoryIds = productList
-            .filter((item) => item[queryName])
-            .map((item) => item[queryName]);
+          const categoryIds = [
+            ...new Set(
+              productList
+                .filter((item) => item[queryName])
+                .map((item) => item[queryName])
+            )
+          ];
           if (categoryIds.length > 0) {
-            return await contactQueryByCategoryIdsAPI({
+            const supplierObj = await contactQueryByCategoryIdsAPI({
               categoryIds
             });
+            // 同一物品的供应商列表去重,避免重复 id 渲染成多个 el-option
+            Object.keys(supplierObj || {}).forEach((categoryId) => {
+              const supplierIds = new Set();
+              supplierObj[categoryId] = (supplierObj[categoryId] || []).filter(
+                (supplier) => {
+                  const supplierId = String(supplier.id);
+                  if (supplierIds.has(supplierId)) {
+                    return false;
+                  }
+                  supplierIds.add(supplierId);
+                  return true;
+                }
+              );
+            });
+            return supplierObj;
           } else {
             return Promise.resolve({});
           }
@@ -704,50 +730,81 @@
         let codeList = list.map((item) => item.code);
         let idList = list.map((item) => item.id);
 
-        //获取仓库库存
-        if (this.isGetInventoryTotal) {
-          let inventoryTotalList = await getInventoryTotalAPI(codeList);
-          list.forEach((item) => {
-            let find =
-              inventoryTotalList.find((key) => key.code == item.code) || {};
-            if (this.showStockCount) {
-              item.stockCountBase = find.stockCountBase;
-              item.lockQuantity = find.lockQuantity;
-            }
-            item.availableCountBase = find.availableCountBase;
+        this.selecting = true;
+        try {
+          // 库存、供应商、包装规格三个请求互不依赖,并行发起,避免串行等待多次往返
+          const [inventoryTotalList, supplierList, packingSpecification] =
+            await Promise.all([
+              this.isGetInventoryTotal
+                ? getInventoryTotalAPI(codeList)
+                : Promise.resolve([]),
+              this.isSupplier
+                ? this.getSupplierObj(list, 'id')
+                : Promise.resolve({}),
+              getCategoryPackageDisposition({ categoryIds: idList })
+            ]);
+
+          //获取仓库库存
+          if (this.isGetInventoryTotal) {
+            list.forEach((item) => {
+              let find =
+                (inventoryTotalList || []).find(
+                  (key) => key.code == item.code
+                ) || {};
+              if (this.showStockCount) {
+                item.stockCountBase = find.stockCountBase;
+                item.lockQuantity = find.lockQuantity;
+              }
+              item.availableCountBase = find.availableCountBase;
+            });
+          }
+          //获取供应商
+          if (this.isSupplier) {
+            list.forEach((item) => {
+              item['supplierList'] = supplierList[item.id];
+              item['entrustedEnterpriseIdList'] = supplierList[item.id];
+              if (supplierList[item.id]?.length) {
+                item['entrustedEnterpriseId'] = supplierList[item.id][0].id;
+                item['supplierCode'] = supplierList[item.id][0].code;
+                item['supplierId'] = supplierList[item.id][0].id;
+                item['supplierName'] = supplierList[item.id][0].name;
+              }
+            });
+          }
+          // 获取包装规格
+          const packageMap = (packingSpecification || []).reduce(
+            (map, item) => {
+              if (!item?.conversionUnit) return map;
+              const mapList = map[item.categoryId] || [];
+              mapList.push(item);
+              map[item.categoryId] = mapList;
+              return map;
+            },
+            {}
+          );
+          Object.keys(packageMap).forEach((key) => {
+            packageMap[key].sort((a, b) => a.sort - b.sort);
           });
-        }
-        //获取供应商
-        if (this.isSupplier) {
-          let supplierList = await this.getSupplierObj(list, 'id');
           list.forEach((item) => {
-            item['supplierList'] = supplierList[item.id];
-            item['entrustedEnterpriseIdList'] = supplierList[item.id];
-            if (supplierList[item.id]?.length) {
-              item['entrustedEnterpriseId'] = supplierList[item.id][0].id;
-              item['supplierCode'] = supplierList[item.id][0].code;
-              item['supplierId'] = supplierList[item.id][0].id;
-              item['supplierName'] = supplierList[item.id][0].name;
+            item['packageDispositionList'] = packageMap[item.id] || [];
+            if (item.level) {
+              item['goodsLevel'] = levelList.find(
+                (val) => val.label == item.level
+              )?.value;
             }
           });
-        }
-        // 获取包装规格
-        let packingSpecification = await getCategoryPackageDisposition({
-          categoryIds: idList
-        });
-        list.forEach((item) => {
-          item['packageDispositionList'] = packingSpecification
-            .filter((ite) => item.id == ite.categoryId && ite.conversionUnit)
-            .sort((a, b) => a.sort - b.sort);
-          if (item.level) {
-            item['goodsLevel'] = levelList.find(
-              (val) => val.label == item.level
-            )?.value;
-          }
-        });
 
-        this.$emit('changeParent', list, this.currentIndex);
-        this.handleClose();
+          // 先关弹框,再等浏览器把关闭动画画出来,最后才做回显计算
+          const currentIndex = this.currentIndex;
+          this.handleClose();
+          requestAnimationFrame(() => {
+            requestAnimationFrame(() => {
+              this.$emit('changeParent', list, currentIndex);
+            });
+          });
+        } finally {
+          this.selecting = false;
+        }
       },
       // 仓库查询
       doSearch() {

+ 17 - 13
src/BIZComponents/setProduct.js

@@ -13,23 +13,27 @@ export function changeCount(row, countObj, noDiscountSingle, weightType) {
   let total = data[countObj.countKey] || 0;
 
   let _endIndex = 0; //计算单重需要
-  if (data.packageDispositionList) {
-    let endIndex = data.packageDispositionList.findIndex(
+  const packList = data.packageDispositionList;
+  // 注意:空数组 [] 也是真值,必须用 length 判断,否则下面 find 不到会取到 undefined
+  if (packList?.length) {
+    let endIndex = packList.findIndex(
       (ite) => data[countObj.unitIdKey] == ite.id
     );
+    // 当前单位在包装规格里找不到(物品没有配置包装、单位已失效等)时退回第一个包装
+    if (endIndex === -1) {
+      endIndex = 0;
+    }
     _endIndex = endIndex;
-    let packageData = data.packageDispositionList.find(
-      (ite) => data[countObj.unitIdKey] == ite.id
-    );
-    data[countObj.unitKey] = packageData.conversionUnit;
-    if (data.weightUnit == data.measuringUnit && data.singleWeight) {
-      data.packageDispositionList[1].packageCell = data.singleWeight;
+    data[countObj.unitKey] = packList[endIndex].conversionUnit;
+    if (
+      data.weightUnit == data.measuringUnit &&
+      data.singleWeight &&
+      packList[1]
+    ) {
+      packList[1].packageCell = data.singleWeight;
     }
-    for (; 0 < endIndex; endIndex--) {
-      total = Vue.prototype.$math.format(
-        data.packageDispositionList[endIndex].packageCell * total,
-        14
-      );
+    for (let i = endIndex; i > 0; i--) {
+      total = Vue.prototype.$math.format(packList[i].packageCell * total, 14);
     }
   }
   data['totalCount'] = total;

+ 10 - 1
src/components/Dict/DictSelection.vue

@@ -51,6 +51,11 @@
       isOne: {
         type: Boolean,
         default: true
+      },
+      // 合并同一字典的并发请求,用于表格里同列多行同时挂载的场景
+      dedupe: {
+        type: Boolean,
+        default: false
       }
     },
     data() {
@@ -84,7 +89,11 @@
     },
     created() {
       if (this.dictName && this.isOne) {
-        this.requestDict(this.dictName);
+        this.requestDict(
+          this.dedupe
+            ? { enumName: this.dictName, dedupe: true }
+            : this.dictName
+        );
       }
     },
     methods: {

+ 6 - 0
src/components/addDoc/index.vue

@@ -24,6 +24,7 @@
       @success="getFiles"
       ref="fileUploadRef"
       :disabled="disabled"
+      :lazy-render="lazyRender"
     ></file-upload>
   </div>
 </template>
@@ -56,6 +57,11 @@
       disabled: {
         type: Boolean,
         default: false
+      },
+      // 透传给内部弹框:关闭状态下不渲染内部表格(表格里大量挂载时开启)
+      lazyRender: {
+        type: Boolean,
+        default: false
       }
     },
     data() {

+ 6 - 0
src/components/addDoc/main.vue

@@ -2,6 +2,7 @@
   <ele-modal
     width="60%"
     :visible.sync="showEditFlag"
+    :destroy-on-close="lazyRender"
     :close-on-click-modal="false"
     custom-class="ele-dialog-form"
     append-to-body
@@ -96,6 +97,11 @@
       disabled: {
         type: Boolean,
         default: false
+      },
+      // 弹框内容懒渲染:关闭状态下不渲染内部表格,用于表格里大量挂载本组件的场景
+      lazyRender: {
+        type: Boolean,
+        default: false
       }
     },
     components: { doc_template, fileEdit, browse },

+ 41 - 19
src/store/modules/dict.js

@@ -14,6 +14,9 @@ import { getByCode } from '@/api/system/dictionary-data';
 
 const state = {};
 
+// dedupe 模式下同一字典的并发请求合并使用,避免表格多实例重复打接口
+const inflightDicts = {};
+
 const mutations = {
   //根据字典code 添加字典
   ADD_DICT: (state, { code, dict }) => {
@@ -23,11 +26,16 @@ const mutations = {
 
 const actions = {
   // 根据字典enumName请求字典 已获取的不做重复请求
-  async requestDict({ commit, state }, enumName) {
- 
+  // payload 支持两种形式:'字典名' 或 { enumName: '字典名', dedupe: true }
+  // dedupe 为 true 时,合并同一字典的并发请求(表格里同一个字典有 N 个实例时只发一次)
+  async requestDict({ commit, state }, payload) {
+    const { enumName, dedupe } =
+      typeof payload === 'string' ? { enumName: payload } : payload || {};
+
     const code = dictEnum[enumName];
 
     if (state[code]?.length) return state[code];
+    if (dedupe && inflightDicts[code]) return inflightDicts[code];
     let res;
     // if (otherDictConfig[dictEnum[enumName]]) {
     //   const config = otherDictConfig[dictEnum[enumName]];
@@ -47,25 +55,39 @@ const actions = {
     //     return res.list;
     //   }
     // } else {
-    res = await getByCode(code);
-    if (res?.code == 0) {
-      const isNumber = numberList.includes(code);
-      commit('ADD_DICT', {
-        code,
-        dict: res.data.map((item) => {
-          const arr = Object.entries(item)[0] || [];
+    const request = (async () => {
+      res = await getByCode(code);
+      if (res?.code == 0) {
+        const isNumber = numberList.includes(code);
+        commit('ADD_DICT', {
+          code,
+          dict: res.data.map((item) => {
+            const arr = Object.entries(item)[0] || [];
 
-          return {
-            dictCode: isNumber ? Number(arr[0]) : arr[0],
-            dictValue: arr[1]
-          };
-        })
-      });
-      return res.data;
-    }
-    // }
+            return {
+              dictCode: isNumber ? Number(arr[0]) : arr[0],
+              dictValue: arr[1]
+            };
+          })
+        });
+        return res.data;
+      }
+      // }
 
-    return [];
+      return [];
+    })();
+
+    if (dedupe) {
+      inflightDicts[code] = request;
+      request
+        .catch(() => {})
+        .finally(() => {
+          if (inflightDicts[code] === request) {
+            delete inflightDicts[code];
+          }
+        });
+    }
+    return request;
   },
   // 更新字典
   async reloadRequestDict({ commit }, enumName) {