Преглед изворни кода

fix(sampleListDialog): 优化样品列表展示,添加虚拟滚动和懒加载功能(处理大数据导致的卡顿问题)

xieyong пре 2 дана
родитељ
комит
d2ea97ff32
1 измењених фајлова са 594 додато и 248 уклоњено
  1. 594 248
      src/views/inspectionWork/components/sampleListDialog.vue

+ 594 - 248
src/views/inspectionWork/components/sampleListDialog.vue

@@ -9,231 +9,192 @@
     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"
+              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 +205,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 +286,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 +301,346 @@
             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, // 默认(非全屏)表格高度
+        tableHeight: 560, // 当前表格高度(全屏时由 recomputeTableHeight 重算)
+        PRELOAD_COUNT: 3 // 预加载样品数(打开弹框时立即加载,滚动时永驻内存不卸载)
       };
     },
     computed: {},
 
     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.tableHeight = this.DEFAULT_TABLE_HEIGHT;
+          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,非全屏回到默认值
+      recomputeTableHeight() {
+        const tableEl = this.$refs.mainTable && this.$refs.mainTable.$el;
+        if (!tableEl) return;
+        if (!this.isFullscreen) {
+          this.tableHeight = this.DEFAULT_TABLE_HEIGHT;
+          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.tableHeight = 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 +651,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 +685,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 {
+    height: 400px;
+    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>