Explorar el Código

Merge remote-tracking branch 'origin/dev' into dev

lucw hace 4 días
padre
commit
923ba73727

+ 1 - 0
.gitignore

@@ -23,3 +23,4 @@ yarn-error.log*
 *.sw?
 vue.config.js
 package-lock.json
+.history

+ 6 - 5
package.json

@@ -16,7 +16,11 @@
     "@amap/amap-jsapi-loader": "^1.0.1",
     "@ant-design/colors": "^6.0.0",
     "@babel/core": "^7.18.13",
+    "@riophae/vue-treeselect": "0.4.0",
     "axios": "^0.27.2",
+    "bpmn-js": "8.9.0",
+    "bpmn-js-properties-panel": "0.46.0",
+    "bpmn-js-token-simulation": "0.10.0",
     "core-js": "^3.25.0",
     "countup.js": "^2.3.2",
     "cropperjs": "^1.5.12",
@@ -40,17 +44,14 @@
     "vue-i18n": "^8.27.2",
     "vue-printjs": "^1.0.0",
     "vue-router": "^3.6.4",
+    "vue-virtual-scroller": "^1.1.2",
     "vue2-ace-editor": "0.0.15",
     "vuedraggable": "^2.24.3",
     "vuex": "^3.6.2",
     "vuex-persistedstate": "^4.1.0",
     "xgplayer-vue": "^1.1.5",
     "xlsx": "^0.18.5",
-    "bpmn-js": "8.9.0",
-    "bpmn-js-properties-panel": "0.46.0",
-    "bpmn-js-token-simulation": "0.10.0",
-    "xml-js": "1.6.11",
-    "@riophae/vue-treeselect": "0.4.0"
+    "xml-js": "1.6.11"
   },
   "devDependencies": {
     "@vue/cli-plugin-babel": "^5.0.8",

+ 184 - 28
src/views/inspectionWork/components/inspectionProjectReport.vue

@@ -63,7 +63,7 @@
           @input="formSampleNoQualifiedNumberChange"
           placeholder="请输入"
           type="number"
-          :disabled="type == 'detail'"
+          :disabled="type == 'detail' || qms_defaultDisposalType"
         >
           <template slot="append">{{ form.measureUnit }}</template>
         </el-input>
@@ -162,7 +162,7 @@
         <el-input
           v-model="row.lossNumber"
           placeholder="请输入"
-          :disabled="type == 'detail'"
+          :disabled="type == 'detail' || qms_defaultDisposalType"
           type="number"
           @input="inputValue('lossNumber', row, $index)"
           @click.native="curretNum = row.lossNumber"
@@ -174,7 +174,7 @@
         <el-input
           v-model="row.lossNumberUnqualified"
           placeholder="请输入"
-          :disabled="type == 'detail'"
+          :disabled="type == 'detail' || qms_defaultDisposalType"
           type="number"
           @input="inputValue('lossNumberUnqualified', row, $index)"
           @click.native="curretNum = row.lossNumberUnqualified"
@@ -212,6 +212,7 @@
           placeholder="请选择"
           style="width: 100%"
           :disabled="type == 'detail'"
+          @change="(val) => handleQualityResultsChange(val, row, $index)"
         >
           <el-option
             v-for="item in qualityResultsList"
@@ -313,6 +314,7 @@
         rowIndex: [],
         curretNum: 0,
         qms_qualityResultContent: 1,
+        qms_defaultDisposalType: false,
         workColumns: [
           {
             minWidth: 150,
@@ -335,13 +337,13 @@
             label: '检测工单名称',
             showOverflowTooltip: true
           },
-          {
-            minWidth: 150,
-            prop: 'qualityWorkOrderCode',
-            align: 'center',
-            label: '来源单号',
-            showOverflowTooltip: true
-          },
+          // {
+          //   minWidth: 150,
+          //   prop: 'qualityWorkOrderCode',
+          //   align: 'center',
+          //   label: '来源单号',
+          //   showOverflowTooltip: true
+          // },
           {
             minWidth: 100,
             prop: 'qualityType',
@@ -422,14 +424,6 @@
             label: '样品数',
             showOverflowTooltip: true
           },
-          {
-            minWidth: 180,
-            prop: 'sampleNoQualifiedNumber',
-            slot: 'sampleNoQualifiedNumber',
-            align: 'center',
-            label: '样品不合格数',
-            showOverflowTooltip: true
-          },
           {
             minWidth: 140,
             prop: 'sampleQualifiedNumber',
@@ -437,24 +431,31 @@
             slot: 'sampleQualifiedNumber',
             align: 'center',
             formatter: (row) => {
-              if (row.sampleQualifiedNumber) {
-                return (
-                  row.sampleQualifiedNumber + (this.form.measureUnit || '')
-                );
-              }
+              let value = row.sampleQualifiedNumber || 0;
+              if(Number(value) < 0) value = 0;
+              return (value) + (this.form.measureUnit || '');
             },
             label: '样品合格数',
             showOverflowTooltip: true
           },
+          {
+            minWidth: 180,
+            prop: 'sampleNoQualifiedNumber',
+            headerSlot: 'isRequired',
+            slot: 'sampleNoQualifiedNumber',
+            align: 'center',
+            label: '样品不合格数',
+            showOverflowTooltip: true
+          },
           {
             minWidth: 140,
             prop: 'lossNumber',
             slot: 'lossNumber',
             align: 'center',
             formatter: (row) => {
-              if (row.lossNumber) {
-                return row.lossNumber + (this.form.measureUnit || '');
-              }
+              let value = row.lossNumber || 0;
+              if(Number(value) < 0) value = 0;
+              return (value) + (this.form.measureUnit || '');
             },
             label: '损耗数(合格品)',
             showOverflowTooltip: true
@@ -761,6 +762,11 @@
     },
 
     created() {
+      parameterGetByCode({
+        code: 'default_disposal_type'
+      }).then((res) => {
+        this.qms_defaultDisposalType = res?.value == 1 ? true : false;
+      });
       this.requestDict('质检计划类型');
       this.requestDict('取样类型');
       this.requestDict('质检标准类型');
@@ -811,6 +817,22 @@
         // this.initData();
       },
 
+      //当 qms_defaultDisposalType 为 true 时,损耗数自动同步合格/不合格数
+      syncLossNumber(row, index) {
+        if (!this.qms_defaultDisposalType) return;
+        this.$set(
+          this.tableData[index],
+          'lossNumber',
+          Number(row.qualifiedQuantity) || 0
+        );
+        this.$set(
+          this.tableData[index],
+          'lossNumberUnqualified',
+          Number(row.noQualifiedQuantity) || 0
+        );
+        this.initData();
+      },
+
       async open({ workData, row, list }, type, pageName) {
         this.tableData = [];
         this.pageName = pageName;
@@ -870,6 +892,13 @@
           // }
         }
 
+        //当 qms_defaultDisposalType 为 true 时,初始化时同步损耗数
+        if (this.qms_defaultDisposalType) {
+          this.tableData.forEach((item, index) => {
+            this.syncLossNumber(item, index);
+          });
+        }
+
         this.visible = true;
       },
       handleOneKeyQualified() {
@@ -896,13 +925,40 @@
             ]);
           });
         }
-        this.form.sampleQualifiedNumber = this.form.sampleQuantity;
-        this.form.sampleNoQualifiedNumber = 0;
+        // 样品数变化后,重新计算合格数/不合格数
+        if (this.qms_defaultDisposalType) {
+          // qms_defaultDisposalType为true时,sampleNoQualifiedNumber为下方noQualifiedQuantity累加值,最大为sampleQuantity数
+          const sumNoQualified = this.tableData.reduce((sum, item) => {
+            return sum + (Number(item.noQualifiedQuantity) || 0);
+          }, 0);
+          this.form.sampleNoQualifiedNumber = Math.min(
+            sumNoQualified,
+            this.form.sampleQuantity
+          );
+        } else {
+          this.form.sampleNoQualifiedNumber = this.tableData.length
+            ? this.tableData.reduce(
+                (max, item) =>
+                  Math.max(max, Number(item.noQualifiedQuantity) || 0),
+                0
+              )
+            : 0;
+        }
+        this.form.sampleQualifiedNumber =
+          this.form.sampleQuantity - this.form.sampleNoQualifiedNumber;
       },
       formSampleNoQualifiedNumberChange() {
         if (+this.form.sampleNoQualifiedNumber < 0) {
           this.form.sampleNoQualifiedNumber = 0;
         }
+
+        const sampleNoQualifiedNumber = Number(this.form.sampleNoQualifiedNumber) || 0;
+        const lossNumberUnqualified = Number(this.form.lossNumberUnqualified) || 0;
+        if(lossNumberUnqualified && sampleNoQualifiedNumber && sampleNoQualifiedNumber < lossNumberUnqualified) {
+          this.$message.error("样品不合格数不能小于损耗数(不合格数)")
+          this.$set(this.form, 'sampleNoQualifiedNumber', lossNumberUnqualified);
+        }
+
         if (+this.form.sampleNoQualifiedNumber > this.form.sampleQuantity) {
           this.form.sampleNoQualifiedNumber = this.form.sampleQuantity;
         }
@@ -964,6 +1020,8 @@
           if (type == 'noQualifiedQuantity' && item[type] > 0) {
             this.$set(this.tableData[index], 'qualityResults', 2);
           }
+          //当 qms_defaultDisposalType 为 true 时,损耗数自动同步合格/不合格数
+          this.syncLossNumber(item, index);
         }
         //损耗数
         if (type == 'lossNumber' || type == 'lossNumberUnqualified') {
@@ -976,6 +1034,104 @@
         ) {
           this.setNumber(item, index, type, '留样', 'retainedSampleQuantity');
         }
+      },
+      handleQualityResultsChange(val, row, index) {
+        //切换到合格:把不合格数移到合格数
+        if (val == 1) {
+          if (
+            row.qualifiedQuantity == 0 &&
+            row.noQualifiedQuantity &&
+            row.noQualifiedQuantity != 0
+          ) {
+            this.$set(row, 'qualifiedQuantity', row.noQualifiedQuantity);
+            this.$set(row, 'noQualifiedQuantity', 0);
+            this.isQualifiedQuantity(row, index, 'qualifiedQuantity');
+          }
+        }
+        //切换到不合格:把合格数移到不合格数
+        if (val == 2) {
+          if (
+            row.noQualifiedQuantity == 0 &&
+            row.qualifiedQuantity &&
+            row.qualifiedQuantity != 0
+          ) {
+            this.$set(row, 'noQualifiedQuantity', row.qualifiedQuantity);
+            this.$set(row, 'qualifiedQuantity', 0);
+            this.isQualifiedQuantity(row, index, 'noQualifiedQuantity');
+          }
+        }
+        //当 qms_defaultDisposalType 为 true 时,损耗数自动同步合格/不合格数
+        this.syncLossNumber(row, index);
+      }
+    },
+    watch: {
+      tableData: {
+        deep: true,
+        handler(newVal) {
+          if (!newVal || !newVal.length) {
+            this.form.sampleNoQualifiedNumber = 0;
+            this.form.sampleQualifiedNumber = this.form.sampleQuantity || 0;
+            return;
+          }
+          if (this.qms_defaultDisposalType) {
+            // qms_defaultDisposalType为true时,sampleNoQualifiedNumber为下方noQualifiedQuantity累加值,最大为sampleQuantity数
+            const sumNoQualified = newVal.reduce((sum, item) => {
+              return sum + (Number(item.noQualifiedQuantity) || 0);
+            }, 0);
+            const sampleQuantity = Number(this.form.sampleQuantity) || 0;
+            this.$set(
+              this.form,
+              'sampleNoQualifiedNumber',
+              Math.min(sumNoQualified, sampleQuantity)
+            );
+            const qualifiedNumber = sampleQuantity - (Number(this.form.sampleNoQualifiedNumber) || 0);
+            if (qualifiedNumber < 0) {
+              this.$message.warning('样品合格数不能小于0');
+              this.$set(this.form, 'sampleQualifiedNumber', 0);
+            } else {
+              this.$set(this.form, 'sampleQualifiedNumber', qualifiedNumber);
+            }
+          } else {
+            const maxNoQualified = newVal.reduce((max, item) => {
+              const val = Number(item.noQualifiedQuantity) || 0;
+              return val > max ? val : max;
+            }, 0);
+            this.$set(this.form, 'sampleNoQualifiedNumber', maxNoQualified);
+            const qualifiedNumber =
+              (Number(this.form.sampleQuantity) || 0) - maxNoQualified;
+            if (qualifiedNumber < 0) {
+              this.$message.warning('样品合格数不能小于0');
+              this.$set(this.form, 'sampleQualifiedNumber', 0);
+            } else {
+              this.$set(this.form, 'sampleQualifiedNumber', qualifiedNumber);
+            }
+          }
+
+          // 损耗数逻辑:qms_defaultDisposalType为true时获取下方lossNumberUnqualified累加值,
+          // 如果累加值大于sampleQuantity数,取最大sampleQuantity数,lossNumberUnqualified = 总数量 - lossNumber
+          if(this.qms_defaultDisposalType) {
+            let maxLossNumberUnqualified = newVal.reduce((max, item) => {
+              const val = Number(item.lossNumberUnqualified) || 0;
+              return val + max;
+            }, 0);
+            const lossNumber = Number(this.form.lossNumber) || 0;
+            const sampleQuantity = Number(this.form.sampleQuantity) || 0;
+            const maxLossNumber = sampleQuantity - maxLossNumberUnqualified;
+            if(maxLossNumberUnqualified > sampleQuantity) {
+              maxLossNumberUnqualified = sampleQuantity;
+            }
+            this.$set(this.form, 'lossNumberUnqualified', maxLossNumberUnqualified);
+            if(lossNumber > maxLossNumber) {
+              this.$set(this.form, 'lossNumber', maxLossNumber);
+            }
+          } else {
+             const lossNumberUnqualified = Number(this.form.lossNumberUnqualified) || 0;
+             const sampleNoQualifiedNumber = Number(this.form.sampleNoQualifiedNumber) || 0;
+             if(lossNumberUnqualified > sampleNoQualifiedNumber) {
+               this.$set(this.form, 'sampleNoQualifiedNumber', lossNumberUnqualified);
+             }
+          }
+        }
       }
     }
   };

+ 642 - 249
src/views/inspectionWork/components/sampleListDialog.vue

@@ -9,231 +9,199 @@
     append-to-body
     width="80%"
     :maxable="true"
+    @update:fullscreen="handleFullscreenChange"
   >
-    <el-table :data="tableData" :default-expand-all="true">
+    <el-table
+      ref="mainTable"
+      :data="tableData"
+      :expand-row-keys="expandedKeys"
+      :row-class-name="rowClassName"
+      :row-key="row => row.__rowKey"
+      :height="tableHeight"
+      @scroll="handleScroll"
+      @expand-change="handleExpandChange"
+    >
       <el-table-column type="expand">
         <template slot-scope="props">
-          <div style="padding: 10px">
-            <el-table :data="props.row.qualitySampleTemplateList" border>
-              <el-table-column
-                label="检测方案编码"
-                prop="qualitySchemeTemplateCode"
-                align="center"
-                show-overflow-tooltip
-              >
-                <template slot-scope="scope">
-                  <span :class="scope.row.qualityResults === 2 ? 'warn' : ''">
-                    {{ scope.row.qualitySchemeTemplateCode }}
-                  </span>
-                </template>
-              </el-table-column>
-              <el-table-column
-                label="检测方案名称"
-                prop="qualitySchemeTemplateName"
-                align="center"
-                show-overflow-tooltip
-              >
-                <template slot-scope="scope">
-                  <span :class="scope.row.qualityResults === 2 ? 'warn' : ''">
-                    {{ scope.row.qualitySchemeTemplateName }}
-                  </span>
-                </template>
-              </el-table-column>
-              <el-table-column
-                label="检测类型"
-                prop="categoryLevelClassName"
-                align="center"
-                show-overflow-tooltip
-              >
-                <template slot-scope="scope">
-                  <span :class="scope.row.qualityResults === 2 ? 'warn' : ''">
-                    {{ scope.row.categoryLevelClassName }}
-                  </span>
-                </template>
-              </el-table-column>
-              <el-table-column
-                label="检测项编码"
-                prop="inspectionCode"
-                align="center"
-                show-overflow-tooltip
-              >
-                <template slot-scope="scope">
-                  <span :class="scope.row.qualityResults === 2 ? 'warn' : ''">
-                    {{ scope.row.inspectionCode }}
-                  </span>
-                </template>
-              </el-table-column>
-              <el-table-column
-                label="检测项名称"
-                prop="inspectionName"
-                align="center"
-                show-overflow-tooltip
-              >
-                <template slot-scope="scope">
-                  <span :class="scope.row.qualityResults === 2 ? 'warn' : ''">
-                    {{ scope.row.inspectionName }}
-                  </span>
-                </template>
-              </el-table-column>
-              <el-table-column
-                label="工艺参数"
-                prop="defaultValue"
-                align="center"
-                show-overflow-tooltip
-                min-width="220px"
-              >
-                <template slot-scope="scope">
-                  <!-- <span :class="scope.row.qualityResults === 2 ? 'warn' : ''">
-                    {{ scope.row.defaultValue }}
-                  </span> -->
+          <div class="virtual-expand-wrapper">
+            <!-- 表头 -->
+            <div class="virtual-table-header">
+              <div class="cell">检测方案编码</div>
+              <div class="cell">检测方案名称</div>
+              <div class="cell">检测类型</div>
+              <div class="cell">检测项编码</div>
+              <div class="cell">检测项名称</div>
+              <div class="cell wide">工艺参数</div>
+              <div class="cell">检测工具</div>
+              <div class="cell input-col">检测内容</div>
+              <div class="cell">检测结果</div>
+            </div>
+            <!-- 虚拟滚动表体 -->
+            <RecycleScroller
+              class="virtual-scroller"
+              :items="props.row.qualitySampleTemplateList || []"
+              :item-size="64"
+              key-field="__vsKey"
+              :buffer="200"
+              :style="{
+                height:
+                  Math.min(
+                    (props.row.qualitySampleTemplateList || []).length * 64,
+                    400
+                  ) + 'px'
+              }"
+              v-if="(props.row.qualitySampleTemplateList || []).length"
+            >
+              <template #default="{ item }">
+                <div
+                  class="virtual-row"
+                  :class="{ warn: item.qualityResults === 2 }"
+                >
                   <div
-                    style="display: flex; justify-content: center"
-                    :class="scope.row.qualityResults === 2 ? 'warn' : ''"
+                    class="cell"
+                    :title="item.qualitySchemeTemplateCode"
                   >
-                    <span v-if="scope.row.textType != 8">{{
-                      scope.row.symbol
-                    }}</span>
-                    <span v-if="scope.row.textType == 3">
-                      {{ scope.row.minValue }}-{{ scope.row.maxValue }}
-                    </span>
-                    <span v-else-if="scope.row.textType == 8">
-                      标准值:{{ scope.row.defaultValue
-                      }}{{ scope.row.unitName || '' }} 下限:{{
-                        scope.row.minValue || 0
-                      }}{{ scope.row.unitName || '' }} 上限:{{
-                        scope.row.maxValue || 0
-                      }}{{ scope.row.unitName || '' }}
-                    </span>
-                    <span v-else>
-                      <span>{{ scope.row.defaultValue }}</span>
-                    </span>
-                    <span v-if="scope.row.textType != 8">
-                      {{ scope.row.unitName }}
-                    </span>
+                    {{ item.qualitySchemeTemplateCode }}
                   </div>
-                </template>
-              </el-table-column>
-              <el-table-column label="检测工具" prop="" align="center">
-                <template slot-scope="scope">
-                  <toolButtom
-                    v-model="scope.row.useTools"
-                    :type="
-                      !pageName && props.row.correlationId ? 'detail' : type
-                    "
-                    :sList="scope.row.toolList"
-                  />
-                </template>
-              </el-table-column>
-
-              <!-- @blur="(e) => inputResultContent(e, scope.row)" -->
-              <el-table-column
-                label="检测内容"
-                prop="qualityResultContent"
-                align="center"
-                fixed="right"
-                width="360"
-              >
-                <template slot-scope="scope">
-                  <el-input
-                    :class="scope.row.qualityResults === 2 ? 'warn' : ''"
-                    v-model="scope.row.qualityResultContent"
-                    placeholder="请输入内容"
-                    :disabled="
-                      type == 'detail' || (!pageName && props.row.correlationId)
-                    "
-                    @input="
-                      clientEnvironmentId == 10
-                        ? newHandleInput(
-                            props.row.qualitySampleTemplateList,
-                            props.$index
-                          )
-                        : handleInput(
-                            scope.row,
-                            scope.row.qualityResultContent,
-                            tableData
-                          )
-                    "
+                  <div
+                    class="cell"
+                    :title="item.qualitySchemeTemplateName"
                   >
-                    <template slot="append" v-if="scope.row.unitName">
-                      {{ scope.row.unitName }}
-                    </template>
-                  </el-input>
-                </template>
-              </el-table-column>
-              <el-table-column
-                label="检测结果"
-                prop="qualityResults"
-                align="center"
-                fixed="right"
-              >
-                <template slot-scope="scope">
-                  <el-select
-                    v-if="
-                      !['炉水电导率', '给水电导率'].includes(
-                        scope.row.inspectionName
-                      )
-                    "
-                    v-model="scope.row.qualityResults"
-                    placeholder="请选择"
-                    style="width: 100%"
-                    @change="qualityResultsListChange(tableData)"
-                    :disabled="
-                      type == 'detail' || (!pageName && props.row.correlationId)
-                    "
+                    {{ item.qualitySchemeTemplateName }}
+                  </div>
+                  <div
+                    class="cell"
+                    :title="item.categoryLevelClassName"
                   >
-                    <el-option
-                      v-for="item in qualityResultsList"
-                      :key="item.value"
-                      :label="item.label"
-                      :value="item.value"
+                    {{ item.categoryLevelClassName }}
+                  </div>
+                  <div class="cell" :title="item.inspectionCode">
+                    {{ item.inspectionCode }}
+                  </div>
+                  <div class="cell" :title="item.inspectionName">
+                    {{ item.inspectionName }}
+                  </div>
+                  <div class="cell wide">
+                    <div
+                      style="
+                        display: flex;
+                        justify-content: center;
+                        align-items: center;
+                      "
+                      :class="item.qualityResults === 2 ? 'warn' : ''"
+                    >
+                      <span v-if="item.textType != 8">{{ item.symbol }}</span>
+                      <span v-if="item.textType == 3">
+                        {{ item.minValue }}-{{ item.maxValue }}
+                      </span>
+                      <span v-else-if="item.textType == 8">
+                        标准值:{{ item.defaultValue
+                        }}{{ item.unitName || '' }} 下限:{{
+                          item.minValue || 0
+                        }}{{ item.unitName || '' }} 上限:{{
+                          item.maxValue || 0
+                        }}{{ item.unitName || '' }}
+                      </span>
+                      <span v-else>
+                        <span>{{ item.defaultValue }}</span>
+                      </span>
+                      <span v-if="item.textType != 8">
+                        {{ item.unitName }}
+                      </span>
+                    </div>
+                  </div>
+                  <div class="cell">
+                    <toolButtom
+                      v-model="item.useTools"
+                      :type="
+                        !pageName && props.row.correlationId ? 'detail' : type
+                      "
+                      :sList="item.toolList"
+                    />
+                  </div>
+                  <div class="cell input-col">
+                    <el-input
+                      :class="item.qualityResults === 2 ? 'warn' : ''"
+                      v-model="item.qualityResultContent"
+                      placeholder="请输入内容"
+                      :disabled="
+                        type == 'detail' ||
+                        (!pageName && props.row.correlationId)
+                      "
+                      @input="
+                        clientEnvironmentId == 10
+                          ? newHandleInput(
+                              props.row.qualitySampleTemplateList,
+                              props.$index
+                            )
+                          : handleInput(
+                              item,
+                              item.qualityResultContent,
+                              tableData
+                            )
+                      "
+                    >
+                      <template slot="append" v-if="item.unitName">
+                        {{ item.unitName }}
+                      </template>
+                    </el-input>
+                  </div>
+                  <div class="cell">
+                    <el-select
+                      v-if="
+                        !['炉水电导率', '给水电导率'].includes(
+                          item.inspectionName
+                        )
+                      "
+                      v-model="item.qualityResults"
+                      placeholder="请选择"
+                      style="width: 100%"
+                      @change="qualityResultsListChange(tableData)"
+                      :disabled="
+                        type == 'detail' ||
+                        (!pageName && props.row.correlationId)
+                      "
                     >
-                    </el-option>
-                  </el-select>
-                </template>
-              </el-table-column>
-            </el-table>
+                      <el-option
+                        v-for="opt in qualityResultsList"
+                        :key="opt.value"
+                        :label="opt.label"
+                        :value="opt.value"
+                      >
+                      </el-option>
+                    </el-select>
+                  </div>
+                </div>
+              </template>
+            </RecycleScroller>
+            <div
+              v-else
+              class="empty-tpl"
+              style="
+                padding: 20px;
+                text-align: center;
+                color: #909399;
+                border: 1px solid #ebeef5;
+                border-top: none;
+              "
+            >
+              暂无检测项
+            </div>
           </div>
         </template>
       </el-table-column>
-      <template v-for="column in tableColumns">
-        <el-table-column
-          :label="column.label"
-          :prop="column.prop"
-          :fixed="column.fixed"
-          :show-overflow-tooltip="true"
-          :width="column.width"
-          :align="column.align"
-          :formatter="column.formatter"
-        >
-          <!-- <template slot-scope="scope" v-if="column.slot=='qualityResults'">
-            <el-select
-              v-model="scope.row.qualityResults"
-              placeholder="请选择"
-              style="width: 100%"
-              :disabled="type == 'detail'"
-            >
-              <el-option
-                v-for="item in qualityResultsList"
-                :key="item.value"
-                :label="item.label"
-                :value="item.value"
-              >
-              </el-option>
-            </el-select>
-          </template> -->
-        </el-table-column>
-      </template>
-      <!-- <el-table-column label="操作" align="center" width="80">
-        <template slot-scope="scope">
-          <el-link
-            :underline="false"
-            type="primary"
-            @click="handleDispose(scope.$index, scope.row, 'dispose')"
-          >
-            处置
-          </el-link>
-        </template>
-      </el-table-column> -->
+      <el-table-column
+        v-for="column in tableColumns"
+        :key="column.prop"
+        :label="column.label"
+        :prop="column.prop"
+        :fixed="column.fixed"
+        :show-overflow-tooltip="true"
+        :width="column.width"
+        :align="column.align"
+        :formatter="column.formatter"
+      >
+      </el-table-column>
     </el-table>
     <template v-slot:footer v-if="type == 'report'">
       <el-button @click="handleClose">取消</el-button>
@@ -244,10 +212,13 @@
 
 <script>
   import toolButtom from './toolButtom.vue';
+  import { RecycleScroller } from 'vue-virtual-scroller';
+  import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
   import { handleInput, qualityResultsListChange } from './unit.js';
   export default {
     components: {
-      toolButtom
+      toolButtom,
+      RecycleScroller
     },
     data() {
       return {
@@ -322,11 +293,6 @@
               }
             }
           }
-          // { label: '货区', prop: 'areaName', align: 'center', },
-          // { label: '货架', prop: 'goodsShelfName', align: 'center', },
-          // { label: '货位', prop: 'goodsAllocationName', align: 'center', },
-          // { label: '生产日期', prop: 'productionDate', align: 'center', },
-          // { label: '采购日期', prop: 'purchaseDate', align: 'center', }
         ],
         qualityResultsList: [
           {
@@ -342,56 +308,386 @@
             label: '让步接收'
           }
         ],
-        clientEnvironmentId:'',
-        sList: [] //检测工具筛选
+        clientEnvironmentId: '',
+        sList: [], //检测工具筛选
+        // ==== 滚动懒加载相关 ====
+        sourceMap: new Map(), // sampleCode -> 原始样品(包含完整 qualitySampleTemplateList)
+        expandedKeys: [], // 当前展开的样品 sampleCode 列表
+        loadedKeys: [], // 持久预加载的样品(卸载时跳过,永驻内存)
+        scrollRAF: null, // 滚动节流
+        scrollBound: false, // 滚动监听是否已绑定
+        resizeObserver: null, // 监听 dialog body 大小变化
+        isFullscreen: false, // 当前是否处于全屏
+        DEFAULT_TABLE_HEIGHT: 560, // 默认(非全屏)表格最大高度
+        MAX_TABLE_HEIGHT: 560, // 硬上限:非全屏和全屏都受此限制
+        _fullscreenTableHeight: 560, // 全屏时由 recomputeTableHeight 重算(dialog body 高度)
+        PRELOAD_COUNT: 3 // 预加载样品数(打开弹框时立即加载,滚动时永驻内存不卸载)
       };
     },
-    computed: {},
+    computed: {
+      // 表格高度:
+      //   - 非全屏:取"内容自然高度"和默认上限的较小者
+      //   - 全屏:取 dialog body 高度(recomputeTableHeight 重算)
+      //   - 两种情况都受 MAX_TABLE_HEIGHT 硬上限约束(避免 dialog body 很高时表格过大)
+      tableHeight() {
+        const cap = Math.min(
+          this.isFullscreen
+            ? this._fullscreenTableHeight
+            : this.DEFAULT_TABLE_HEIGHT,
+          this.MAX_TABLE_HEIGHT
+        );
+        // 最小 200,避免空数据/异常情况下表格太矮
+        return Math.max(200, Math.min(cap, this.naturalContentHeight));
+      },
+      // 估算 tableData 实际占的高度(el-table 的 :height 需要精确数字)
+      // 注意:内层 RecycleScroller 现在是动态高度(按模板数自适应,封顶 400),
+      // 这里要保持一致,否则估算会比实际大、表格会撑到 cap 而不是收缩
+      naturalContentHeight() {
+        const TABLE_HEADER = 44;       // el-table 表头
+        const MAIN_ROW = 48;            // 主行
+        const EXPAND_HEADER = 40;       // virtual-table-header(包含 padding)
+        const EXPAND_WRAPPER_PAD = 20;  // virtual-expand-wrapper 的上下 padding
+        const EXPAND_EMPTY = 56;        // "暂无检测项" 占位(padding 20 + 文本)
+        const SCROLLER_MAX = 400;       // RecycleScroller 高度封顶
+        const SCROLLER_ITEM = 64;       // 单个模板行高度
+
+        let h = TABLE_HEADER;
+        for (const row of this.tableData) {
+          h += MAIN_ROW;
+          const key = row.sampleCode || row.__rowKey;
+          const expanded = this.expandedKeys.includes(key);
+          if (expanded) {
+            const tplLen = (row.qualitySampleTemplateList || []).length;
+            h += EXPAND_HEADER + EXPAND_WRAPPER_PAD;
+            h += tplLen > 0
+              ? Math.min(tplLen * SCROLLER_ITEM, SCROLLER_MAX)
+              : EXPAND_EMPTY;
+          }
+        }
+        return h;
+      }
+    },
 
     created() {},
+    beforeDestroy() {
+      // 解绑滚动监听
+      this.unbindScrollListener();
+    },
     methods: {
       async openDia(index, row, type, list, isStatus, sList, pageName) {
         this.clientEnvironmentId =
           this.$store.state.user.info.clientEnvironmentId;
-        this.tableData = [];
         this.rowIndex = index;
         this.type = type;
         this.pageName = pageName;
-        // console.log(list, 'lklllllllllllllllllkkkkkkkkkkkkkkkkkkkkk', list[0].qualityResults);
-        //如果检测项qualitySampleTemplateList只有一条数据且检测结果是让步接收
-        if (list[0].qualitySampleTemplateList.length == 1) {
-          for (let i = 0; i < list[0].qualitySampleTemplateList.length; i++) {
-            list[0].qualitySampleTemplateList[i].qualityResults = list[0]
-              .qualityResults
-              ? list[0].qualityResults
-              : 1;
+
+        // 1. 保存原始数据到 sourceMap(用于滚动懒加载 templateList)
+        this.sourceMap = new Map();
+        list.forEach((item, idx) => {
+          const key = item.sampleCode || `__idx_${idx}`;
+          this.sourceMap.set(key, item);
+        });
+
+        // 2. 弹框立刻显示,tableData 立即填满所有样品(templateList 懒加载为空)
+        // 所有样品默认展开,expand 区域始终可见;只对可见样品加载 templateList
+        this.tableData = list.map((sample, idx) => {
+          const key = sample.sampleCode || `__idx_${idx}`;
+          return {
+            ...sample,
+            __rowKey: key,
+            qualitySampleTemplateList: []
+          };
+        });
+        this.expandedKeys = list.map(
+          (sample, idx) => sample.sampleCode || `__idx_${idx}`
+        );
+        this.visible = true;
+
+        // 3. 预加载前 N 个样品的检测项(持久 buffer,滚动时不会被卸载)
+        this.loadedKeys = [];
+        const preloadCount = Math.min(this.PRELOAD_COUNT, list.length);
+        for (let i = 0; i < preloadCount; i++) {
+          const key = list[i].sampleCode || `__idx_${i}`;
+          this.loadedKeys.push(key);
+        }
+        // 批量加载模板(loadSampleTemplates 内部有幂等检查,但这里首次加载必然为空数组所以会执行)
+        for (let i = 0; i < preloadCount; i++) {
+          this.loadSampleTemplates(this.loadedKeys[i]);
+        }
+
+        // 4. 下一帧:绑定滚动事件(不再调用 autoExpandVisibleSamples,避免初始就把预加载的卸载掉)
+        this.$nextTick(() => {
+          this.bindScrollListener();
+          // 重置全屏状态并重新计算表格高度(防止上次打开残留全屏)
+          this.isFullscreen = false;
+          this.observeBodyResize();
+        });
+      },
+
+      // 绑定 el-table body 的滚动事件
+      bindScrollListener() {
+        if (this.scrollBound) return;
+        this.$nextTick(() => {
+          const el = this.$refs.mainTable && this.$refs.mainTable.$el;
+          if (!el) return;
+          const body = el.querySelector('.el-table__body-wrapper');
+          if (body) {
+            body.addEventListener('scroll', this.handleScroll);
+            this.scrollBound = true;
+          }
+        });
+      },
+
+      // 解绑滚动事件 + ResizeObserver
+      unbindScrollListener() {
+        if (this.scrollBound) {
+          const el = this.$refs.mainTable && this.$refs.mainTable.$el;
+          const body = el && el.querySelector('.el-table__body-wrapper');
+          if (body) {
+            body.removeEventListener('scroll', this.handleScroll);
           }
+          this.scrollBound = false;
         }
-        // 默认合格
-        list.map((item) => {
-          item.qualitySampleTemplateList &&
-            item.qualitySampleTemplateList.map((el) => {
-              el.qualityResults = el.qualityResults ? el.qualityResults : 1;
-            });
+        if (this.resizeObserver) {
+          this.resizeObserver.disconnect();
+          this.resizeObserver = null;
+        }
+      },
+
+      // 全屏切换事件(@update:fullscreen)
+      handleFullscreenChange(fullscreen) {
+        this.isFullscreen = !!fullscreen;
+        // 等待 dialog wrap 的 class 切换 + 浏览器布局完成
+        this.$nextTick(() => {
+          requestAnimationFrame(() => {
+            this.recomputeTableHeight();
+            this.observeBodyResize();
+          });
         });
-        this.tableData = JSON.parse(JSON.stringify(list));
-        console.log(this.tableData, '33333333333333333');
+      },
 
-        this.visible = true;
+      // 重新计算表格高度:仅全屏时需要——把 dialog body 高度写进 _fullscreenTableHeight;
+      // 非全屏由 computed tableHeight 直接用 DEFAULT_TABLE_HEIGHT,无需重算
+      recomputeTableHeight() {
+        if (!this.isFullscreen) return;
+        const tableEl = this.$refs.mainTable && this.$refs.mainTable.$el;
+        if (!tableEl) return;
+        // 全屏:沿 DOM 找到 .el-dialog,再取 .el-dialog__body 的内容区高度
+        // (不用 .ele-modal-wrap 这个 class——实际类名是 ele-modal-wrap-fullscreen)
+        const dialog = tableEl.closest('.el-dialog');
+        if (!dialog) return;
+        const body = dialog.querySelector('.el-dialog__body');
+        if (!body) return;
+        const style = getComputedStyle(body);
+        const padTop = parseFloat(style.paddingTop) || 0;
+        const padBottom = parseFloat(style.paddingBottom) || 0;
+        this._fullscreenTableHeight = body.clientHeight - padTop - padBottom;
+      },
+
+      // 监听 dialog body 尺寸变化(拖拽 / resize / 全屏切换都会触发)
+      observeBodyResize() {
+        if (this.resizeObserver || typeof ResizeObserver === 'undefined') return;
+        const tableEl = this.$refs.mainTable && this.$refs.mainTable.$el;
+        if (!tableEl) return;
+        const body = tableEl.closest('.el-dialog__body');
+        if (!body) return;
+        this.resizeObserver = new ResizeObserver(() => {
+          // 仅当全屏时跟着 resize;非全屏保持固定高度,避免不必要重排
+          if (this.isFullscreen) {
+            this.recomputeTableHeight();
+          }
+        });
+        this.resizeObserver.observe(body);
+      },
+
+      // 滚动节流
+      handleScroll() {
+        if (this.scrollRAF) return;
+        this.scrollRAF = requestAnimationFrame(() => {
+          this.scrollRAF = null;
+          this.autoExpandVisibleSamples();
+        });
+      },
+
+      // 自动展开可视区的样品行(懒加载其 templateList)
+      autoExpandVisibleSamples() {
+        const table = this.$refs.mainTable;
+        if (!table || !table.store || !table.store.states) return;
+
+        const data = this.tableData;
+        if (!data.length) return;
+
+        // 直接从 DOM 找可视区顶部的行(避免累加高度估算误差)
+        const targetIdx = this.findFirstVisibleRowIdx();
+        if (targetIdx < 0) return;
+
+        // 展开 targetIdx 上下各 1(buffer)
+        const minIdx = Math.max(0, targetIdx - 1);
+        const maxIdx = Math.min(data.length - 1, targetIdx + 1);
+
+        for (let idx = minIdx; idx <= maxIdx; idx++) {
+          const sample = data[idx];
+          if (!sample) continue;
+          const sampleCode = sample.sampleCode || sample.__rowKey;
+          if (!sampleCode) continue;
+          // 所有样品默认都在 expandedKeys 中,expandedKeys.includes 永远为 true,
+          // 直接调用 loadSampleTemplates 即可(内部有 length 幂等检查)
+          this.loadSampleTemplates(sampleCode);
+        }
+
+        // 卸载已滚出可视区的样品的 templateList(保留展开状态)
+        // loadedKeys 中的样品是预加载 buffer,跳过卸载(永驻内存)
+        const unloadIdx = [];
+        for (let idx = 0; idx < data.length; idx++) {
+          if (idx >= minIdx && idx <= maxIdx) continue;
+          const sample = data[idx];
+          if (!sample) continue;
+          const sampleCode = sample.sampleCode || sample.__rowKey;
+          if (this.loadedKeys.includes(sampleCode)) continue;
+          if (sample.qualitySampleTemplateList &&
+              sample.qualitySampleTemplateList.length) {
+            unloadIdx.push(idx);
+          }
+        }
+        unloadIdx.forEach((idx) => {
+          this.$set(
+            this.tableData[idx],
+            'qualitySampleTemplateList',
+            []
+          );
+        });
+      },
+
+      // 直接从 DOM 找第一个可视的主行(避开累加估算误差和 CSS transition 干扰)
+      // Element UI 的 expand 行有 transition 动画,getBoundingClientRect 测不准,
+      // 所以改用"行底边是否在 body 顶部之下"判断可见性
+      findFirstVisibleRowIdx() {
+        const table = this.$refs.mainTable;
+        if (!table || !table.$el) return -1;
+        const body = table.$el.querySelector('.el-table__body-wrapper');
+        if (!body) return -1;
+
+        const bodyRect = body.getBoundingClientRect();
+        const mainRows = body.querySelectorAll(
+          'tr.el-table__row:not(.el-table__row--expand)'
+        );
+
+        // 找到第一个底边在 body 顶部之下的行(即可见或部分可见)
+        for (let i = 0; i < mainRows.length; i++) {
+          const rect = mainRows[i].getBoundingClientRect();
+          if (rect.bottom > bodyRect.top) {
+            return i;
+          }
+        }
+        return mainRows.length > 0 ? mainRows.length - 1 : -1;
+      },
+
+      // 懒加载某个样品的 qualitySampleTemplateList
+      loadSampleTemplates(sampleCode) {
+        const idx = this.tableData.findIndex(
+          (s) => (s.sampleCode || s.__rowKey) === sampleCode
+        );
+        if (idx < 0) return;
+        // 已加载过直接返回
+        if (this.tableData[idx].qualitySampleTemplateList.length) return;
+
+        const source = this.sourceMap.get(sampleCode);
+        if (!source || !source.qualitySampleTemplateList) return;
+
+        const tpl = source.qualitySampleTemplateList;
+        const newTpl = new Array(tpl.length);
+        for (let j = 0; j < tpl.length; j++) {
+          newTpl[j] = {
+            __vsKey: `${idx}-${j}-${tpl[j].id || ''}`,
+            ...tpl[j]
+          };
+        }
+
+        // 默认值处理:让步接收 / 默认合格
+        if (tpl.length === 1 && source.qualityResults) {
+          newTpl[0].qualityResults = source.qualityResults || 1;
+        }
+        newTpl.forEach((el) => {
+          if (!el.qualityResults) el.qualityResults = 1;
+        });
+
+        this.$set(
+          this.tableData[idx],
+          'qualitySampleTemplateList',
+          newTpl
+        );
+      },
+
+      // 用户点击展开/折叠时(手动触发懒加载)
+      handleExpandChange(row, expandedRows) {
+        const sampleCode = row.sampleCode || row.__rowKey;
+        const isExpanded = expandedRows.some(
+          (r) => (r.sampleCode || r.__rowKey) === sampleCode
+        );
+        if (!isExpanded) {
+          // 折叠:同步从 expandedKeys 中移除,避免高度估算错位
+          this.expandedKeys = this.expandedKeys.filter((k) => k !== sampleCode);
+          const idx = this.tableData.findIndex(
+            (s) => (s.sampleCode || s.__rowKey) === sampleCode
+          );
+          if (idx >= 0) this.$set(this.tableData[idx], 'qualitySampleTemplateList', []);
+        } else {
+          // 展开:加回 expandedKeys(幂等),加载模板
+          if (!this.expandedKeys.includes(sampleCode)) {
+            this.expandedKeys.push(sampleCode);
+          }
+          this.loadSampleTemplates(sampleCode);
+        }
+      },
+
+      // 给样品行注入索引 class,用于滚动时定位
+      rowClassName({ row, rowIndex }) {
+        const code = (row && (row.sampleCode || row.__rowKey)) || '';
+        return `sample-row sample-row-${rowIndex} sample-code-${code}`;
+      },
+
+      // 给样品单元格注入 sampleCode data 属性(备用方案)
+      cellClassName({ row, column, rowIndex, columnIndex }) {
+        if (columnIndex === 0) {
+          return `cell-code-${row.sampleCode || row.__rowKey || ''}`;
+        }
+        return '';
       },
 
       handleClose() {
         this.visible = false;
+        // 重置懒加载状态
+        this.expandedKeys = [];
+        this.loadedKeys = [];
+        this.sourceMap = new Map();
+        this.tableData = [];
       },
       handleConfirm() {
+        // 立刻关弹框(用户体验优先),耗时的清理 + emit 推迟到 raf
         this.visible = false;
-        this.tableData.forEach((item) => {
-          if (!this.pageName && item.correlationId) {
-            return;
-          }
-          item.status = 1;
+        this.$nextTick(() => {
+          requestAnimationFrame(() => {
+            this.tableData.forEach((item) => {
+              if (!this.pageName && item.correlationId) {
+                return;
+              }
+              item.status = 1;
+            });
+            // 清理虚拟列表的辅助字段,避免污染返回数据
+            const cleaned = this.tableData.map((s) => {
+              const tpl = (s.qualitySampleTemplateList || []).map((t) => {
+                const { __vsKey, ...rest } = t;
+                return rest;
+              });
+              return { ...s, qualitySampleTemplateList: tpl };
+            });
+            this.$emit('handleConfirm', cleaned, this.rowIndex);
+            // 重置懒加载状态,与 handleClose 保持一致
+            this.expandedKeys = [];
+            this.loadedKeys = [];
+            this.sourceMap = new Map();
+          });
         });
-        this.$emit('handleConfirm', this.tableData, this.rowIndex);
       },
       handleDispose(index, row, type) {
         this.$emit('handleDispose', index, row, type);
@@ -402,9 +698,8 @@
           (item) => item.inspectionName == '炉水电导率'
         )?.qualityResultContent;
         let num1 = arr.find(
-          (item) => item.inspectionName =='给水电导率'
+          (item) => item.inspectionName == '给水电导率'
         )?.qualityResultContent;
-        console.log(num, num1);
 
         arr.forEach((item, i) => {
           if (num && num1 && item.inspectionName == '干度值') {
@@ -437,4 +732,102 @@
       color: red;
     }
   }
-</style>
+
+  .virtual-expand-wrapper {
+    padding: 10px;
+  }
+
+  .virtual-table-header,
+  .virtual-row {
+    display: grid;
+    grid-template-columns: 120px 120px 100px 100px 120px 220px 120px 360px 120px;
+    box-sizing: border-box;
+  }
+
+  .virtual-table-header {
+    background: #f5f7fa;
+    border: 1px solid #ebeef5;
+    border-bottom: none;
+    font-weight: bold;
+    color: #909399;
+
+    .cell {
+      padding: 8px 4px;
+      text-align: center;
+      border-right: 1px solid #ebeef5;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-size: 14px;
+
+      &:last-child {
+        border-right: none;
+      }
+    }
+  }
+
+  .virtual-scroller {
+    /* 高度由 :style 动态控制(按模板数量自适应,封顶 400) */
+    border: 1px solid #ebeef5;
+    border-top: none;
+    background: #fff;
+  }
+
+  .virtual-row {
+    height: 64px;
+    border-bottom: 1px solid #ebeef5;
+    border-left: 1px solid #ebeef5;
+    border-right: 1px solid #ebeef5;
+    background: #fff;
+    box-sizing: border-box;
+
+    &.warn {
+      color: red;
+    }
+
+    .cell {
+      padding: 4px 8px;
+      text-align: center;
+      border-right: 1px solid #ebeef5;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-size: 13px;
+
+      &:last-child {
+        border-right: none;
+      }
+
+      ::v-deep .el-input {
+        width: 100%;
+      }
+
+      ::v-deep .el-input__inner {
+        padding: 0 4px;
+        height: 32px;
+        line-height: 32px;
+      }
+
+      ::v-deep .el-select {
+        width: 100%;
+      }
+
+      ::v-deep .el-input-group__append {
+        padding: 0 6px;
+        font-size: 12px;
+      }
+    }
+
+    .cell.wide {
+      padding: 4px 8px;
+      min-width: 0;
+    }
+
+    .cell.input-col {
+      padding: 4px 8px;
+    }
+  }
+</style>

+ 8 - 1
src/views/sample/samplemanagement/index.vue

@@ -58,7 +58,7 @@
             type="primary"
             :underline="false"
             @click="goDetail(row.id)"
-            v-if="row.status !== 2"
+            v-if="row.status !== 2 && !unqualified_approval"
           >
             处置
           </el-link>
@@ -86,6 +86,7 @@ import sample from './components/addOrEditSample.vue';
 import { getList, removeItem } from '@/api/samplemanagement';
 import dictMixins from '@/mixins/dictMixins';
 import tabMixins from '@/mixins/tableColumnsMixin';
+import { parameterGetByCode } from '@/api/main/index';
 
 export default {
   mixins: [dictMixins, tabMixins],
@@ -94,6 +95,7 @@ export default {
   },
   data() {
     return {
+      unqualified_approval: 0,
       cacheKeyUrl: 'qsm-c2e9664a-samplemanagement',
       columns: [
         {
@@ -208,6 +210,11 @@ export default {
   },
   created() {
     this.requestDict('处置状态');
+    parameterGetByCode({
+      code: 'unqualifiedList_approval'
+    }).then((res) => {
+      this.unqualified_approval = res.value;
+    });
   },
 
   computed: {