徐展城 6 днів тому
батько
коміт
49e951c7d8

+ 13 - 0
yudao-ui-admin-vue3/src/api/pressure2/equipboiler/index.ts

@@ -168,4 +168,17 @@ export const EquipBoilerApi = {
   getPressureParts: async (boilerId: string) => {
     return await request.get({ url: `/pressure2/equip-boiler/pressure-parts`, params: { boilerId } })
   },
+
+  // 导入锅炉受压元件 Excel
+  importPressureParts: async (file: File) => {
+    const data = new FormData()
+    data.append('file', file)
+    const res = await request.upload({ url: `/pressure2/equip-boiler/pressure-parts/import`, data })
+    return res.data
+  },
+
+  // 下载锅炉受压元件导入模板
+  downloadPressurePartsTemplate: async () => {
+    return await request.download({ url: `/pressure2/equip-boiler/pressure-parts/get-import-template` })
+  },
 }

+ 3 - 1
yudao-ui-admin-vue3/src/components/DynamicReport/BatchUploadFile.vue

@@ -53,7 +53,9 @@ const handleTemplateImage = async () => {
   handleCloseShowUploadDialog()
 }
 const handleApply = async (file) => {
-  const base = await urlToBase64(buildFileUrl(file.url))
+  // file.url 可能是已处理过的完整 URL(来自 CustomUploadFile 单独"应用"),也可能是原始路径(来自"一键应用")
+  const url = /^https?:\/\//.test(file.url) ? file.url : buildFileUrl(file.url)
+  const base = await urlToBase64(url)
   let path = props.colList.find(item => item.colCode === "Illustration").colCode
   if (!path) {
     path = props.colList[0].colCode

+ 407 - 46
yudao-ui-admin-vue3/src/components/DynamicReport/SpreadViewer.vue

@@ -161,6 +161,7 @@ const loading = ref(true)
 const showSpread=ref(true)
 const previewContainer = ref(null)
 let sheetData = {};
+let arrayFieldOffsetInfos = [];
 const designerKey = import.meta.env.VITE_SPREADJS_DESIGNER_KEY
 const nodeEnv = import.meta.env.VITE_NODE_ENV
 const licenseKey = import.meta.env.VITE_SPREADJS_LICENSE_KEY
@@ -245,7 +246,8 @@ const initPreview = async () => {
       const res = await initCommonData();
 
       // 2. 调用后端接口获取处理好的 SJS
-      const apiMethod = nodeEnv === 'dev' ? ExcelApi.process : ExcelApi.excel;
+      const apiMethod = ExcelApi.excel;
+      // const apiMethod = nodeEnv === 'dev' ? ExcelApi.process : ExcelApi.excel;
       const sjsBlob: any = await apiMethod({ templateId, refId });
 
       // 3. 打开 SJS 后在回调中绑定数据源
@@ -513,26 +515,90 @@ const selectWingdings = (item) => {
 const handleSave = () => {
   loading.value = true;
   let dataSource = {};
+
+  const allSheetsData = []
   previewSpread.sheets.forEach((sheet) => {
     if (sheet.name() && sheet.name() !== '试用版警告') {
-      // 收集 sheet 的数据源(兼容 table 绑定字段)
       const sheetData = collectSheetDataSource(sheet);
-      console.log(sheetData)
-      for (const key in sheetData) {
-        if (dataSource[key] && sheetData[key] == sheetData[key]) {
-        } else {
-          dataSource[key] = sheetData[key];
+      const pageNo = getPageNoFromSheet(sheet);
+      allSheetsData.push({ pageNo, data: sheetData, sheet });
+    }
+  });
+
+  let copyRangePathSet = new Set();
+  const hasContinuationPages = allSheetsData.some(({ pageNo }) => pageNo > 1);
+  let pageNoBindingPath = null
+  if (hasContinuationPages && generateData && generateData.copyRange) {
+    const rangeInfo = getCopyRangeInfo();
+    const templateSheet = previewSpread.getSheetFromName(generateData.sheetName);
+    if (templateSheet && rangeInfo) {
+      getCopyRangeBindingPaths(templateSheet, rangeInfo).forEach(p => copyRangePathSet.add(p));
+      pageNoBindingPath = templateSheet.getBindingPath(rangeInfo.row, rangeInfo.col)
+    }
+  }
+
+  for (const { pageNo, data, sheet } of allSheetsData) {
+    for (const key in data) {
+      if (pageNo > 1 && key === pageNoBindingPath) continue
+      if (arrayFieldOffsetInfos.some(oi => key.startsWith(oi.prefix + '_'))) continue
+      if (hasContinuationPages && copyRangePathSet.has(key)) {
+        if (!dataSource[key]) {
+          dataSource[key] = [];
+        }
+        if (Array.isArray(dataSource[key])) {
+          while (dataSource[key].length < pageNo) {
+            dataSource[key].push(null);
+          }
+          dataSource[key][pageNo - 1] = data[key];
+        }
+      } else if (hasContinuationPages && Array.isArray(sheetData?.[key]) && !arrayFieldOffsetInfos.some(oi => key === oi.prefix)) {
+        if (!dataSource[key]) {
+          dataSource[key] = [];
+        }
+        if (Array.isArray(data[key])) {
+          dataSource[key] = dataSource[key].concat(data[key]);
+        }
+      } else {
+        if (!dataSource.hasOwnProperty(key) && data[key] != null) {
+          dataSource[key] = data[key];
         }
       }
-      // 收集形状(浮动图片)数据
-      collectShapesIntoDataSource(sheet, dataSource);
     }
-  });
+    collectShapesIntoDataSource(sheet, dataSource);
+  }
+
+  for (const oi of arrayFieldOffsetInfos) {
+    const { prefix, fields } = oi
+    const copyMinIndex = Math.min(...fields.map(f => f.index))
+    const copyFieldNames = new Set(fields.map(f => f.path))
+    const result = []
+    const templateSheetData = allSheetsData.find(s => s.sheet.name() === generateData.sheetName)
+    if (templateSheetData) {
+      for (let i = 1; i < copyMinIndex; i++) {
+        const fieldName = `${prefix}_${i}`
+        if (!copyFieldNames.has(fieldName)) {
+          const val = templateSheetData.data[fieldName]
+          if (val != null) {
+            result.push(Array.isArray(val) ? val[0] || null : val)
+          } else {
+            result.push(null)
+          }
+        }
+      }
+    }
+    for (const { data, sheet } of allSheetsData) {
+      if (sheet.name() !== generateData.sheetName && !sheet.name().startsWith(generateData.sheetName + ' (')) continue
+      for (const f of fields) {
+        result.push(data[f.path] != null ? data[f.path] : null)
+      }
+    }
+    dataSource[`${prefix}_1`] = result
+  }
+
   if (Object.keys(dataSource).length > 0) {
-    // 序列化为后端存储格式(对象/数组 → JSON 字符串)
     const serializedData = serializeDataSourceForStorage(dataSource);
     console.log(serializedData);
-    
+
     DynamicTbValApi.saveAllColValue(insId.value, serializedData).then(res => {
       if (res) {
         ElMessage.success('保存成功')
@@ -552,6 +618,23 @@ const handleSave = () => {
   }
 }
 
+const distributePageDataSources = () => {
+  if (!generateData || !generateData.copyRange) return
+  previewSpread.sheets.forEach((sheet) => {
+    if (sheet.name() && sheet.name() !== '试用版警告' && sheet.name() !== generateData.sheetName) {
+      const pageNo = getPageNoFromSheet(sheet)
+      if (pageNo > 1 && sheetData && Object.keys(sheetData).length > 0) {
+        const pageData = buildPageDataSource(sheetData, pageNo)
+        const dataSource = new GC.Spread.Sheets.Bindings.CellBindingSource(pageData)
+        sheet.setDataSource(dataSource)
+      }
+    }
+  })
+}
+
+
+
+
 const handleCopyRow = () => {
   if (!previewSpread) return ElMessage.warning('请先加载文档')
   const activeSheet = previewSpread.getActiveSheet()
@@ -613,6 +696,238 @@ const close = () => {
   emit('close')
 }
 
+const getPageNoFromSheet = (sheet) => {
+  const tag = sheet.tag()
+  if (tag !== undefined && tag !== null && tag !== '' && !isNaN(parseInt(tag))) {
+    return parseInt(tag)
+  }
+  const name = sheet.name()
+  const match = name.match(/\((\d+)\)\s*$/)
+  if (match) {
+    return parseInt(match[1])
+  }
+  return 1
+}
+
+const getCopyRangeInfo = () => {
+  if (!generateData || !generateData.copyRange) return null
+  const col = parseInt(generateData.copyRange.topLeft.split(',')[0])
+  const row = parseInt(generateData.copyRange.topLeft.split(',')[1])
+  const colCount = parseInt(generateData.copyRange.topRight.split(',')[0]) - col
+  const rowCount = parseInt(generateData.copyRange.bottomLeft.split(',')[1]) - row
+  return { col, row, colCount, rowCount }
+}
+
+const getCopyRangeBindingPaths = (sheet, rangeInfo) => {
+  if (!sheet || !rangeInfo) return []
+  const { col, row, colCount, rowCount } = rangeInfo
+  const paths = []
+  for (let i = 0; i <= colCount; i++) {
+    for (let j = 0; j <= rowCount; j++) {
+      const bindingPath = sheet.getBindingPath(row + j, col + i)
+      if (bindingPath) {
+        paths.push(bindingPath)
+      }
+    }
+  }
+  return paths
+}
+
+const buildPageDataSource = (baseData, pageNo) => {
+  if (!generateData || !generateData.copyRange) return baseData
+  const rangeInfo = getCopyRangeInfo()
+  if (!rangeInfo) return baseData
+  const sheet = previewSpread.getSheetFromName(generateData.sheetName)
+  if (!sheet) return baseData
+  const pageData = { ...baseData }
+  const bindingPaths = getCopyRangeBindingPaths(sheet, rangeInfo)
+  const bindingPathSet = new Set(bindingPaths)
+  for (const path of bindingPaths) {
+    const val = baseData[path]
+    if (Array.isArray(val)) {
+      pageData[path] = val[pageNo - 1] !== undefined ? val[pageNo - 1] : null
+    } else if (pageNo > 1) {
+      pageData[path] = null
+    }
+  }
+  const rowsPerPage = rangeInfo.rowCount + 1
+  for (const key in baseData) {
+    if (bindingPathSet.has(key)) continue
+    const val = baseData[key]
+    if (Array.isArray(val)) {
+      const start = (pageNo - 1) * rowsPerPage
+      pageData[key] = val.slice(start, start + rowsPerPage)
+    }
+  }
+  for (const oi of arrayFieldOffsetInfos) {
+    const { fullArray, fields } = oi
+    const copyMinIndex = Math.min(...fields.map(f => f.index))
+    const start = (copyMinIndex - 1) + (pageNo - 1) * oi.rowsPerPage
+    let firstIdx = -1, lastIdx = -1
+    let firstVal = undefined
+    for (const f of fields) {
+      const idx = start + (f.index - copyMinIndex)
+      if (firstIdx === -1) { firstIdx = idx; firstVal = idx < fullArray.length ? fullArray[idx] : null }
+      lastIdx = idx
+      pageData[f.path] = idx < fullArray.length ? fullArray[idx] : null
+    }
+    console.log(`[buildPageDataSource] pageNo=${pageNo}, prefix=${oi.prefix}, start=${start}, idxRange=[${firstIdx},${lastIdx}], firstIdxVal=${JSON.stringify(firstVal)}, fullArrayLen=${fullArray.length}, copyMinIndex=${copyMinIndex}, rowsPerPage=${oi.rowsPerPage}`)
+  }
+  return pageData
+}
+
+const distributeArrayFieldToCells = () => {
+  if (!generateData || !generateData.copyRange) {
+    console.log('[distributeArrayFieldToCells] 跳过: generateData/copyRange 为空')
+    return
+  }
+  if (!sheetData || !Object.keys(sheetData).length) {
+    console.log('[distributeArrayFieldToCells] 跳过: sheetData 为空')
+    return
+  }
+  const rangeInfo = getCopyRangeInfo()
+  if (!rangeInfo) {
+    console.log('[distributeArrayFieldToCells] 跳过: rangeInfo 为空')
+    return
+  }
+  const baseSheet = previewSpread.getSheetFromName(generateData.sheetName)
+  if (!baseSheet) {
+    console.log('[distributeArrayFieldToCells] 跳过: 找不到基础 sheet')
+    return
+  }
+
+  const pattern = /^(.+)_(\d+)$/
+  const prefixGroups = {}
+  for (let r = 0; r <= rangeInfo.rowCount; r++) {
+    for (let c = 0; c <= rangeInfo.colCount; c++) {
+      const bp = baseSheet.getBindingPath(rangeInfo.row + r, rangeInfo.col + c)
+      if (bp) {
+        const match = bp.match(pattern)
+        if (match) {
+          const pfx = match[1]
+          if (!prefixGroups[pfx]) prefixGroups[pfx] = []
+          prefixGroups[pfx].push({
+            path: bp, prefix: pfx, index: parseInt(match[2]), rowInRange: r, col: c
+          })
+        }
+      }
+    }
+  }
+
+  console.log('[distributeArrayFieldToCells] copyRange 扫描到的前缀组:', Object.keys(prefixGroups))
+
+  arrayFieldOffsetInfos = []
+  const prefixSet = new Set()
+
+  for (const pfx of Object.keys(prefixGroups)) {
+    const fields = prefixGroups[pfx]
+    let fullArray = null
+    for (let i = 0; i <= 1000; i++) {
+      const candidate = `${pfx}_${i}`
+      const val = sheetData[candidate]
+      if (Array.isArray(val) && val.length > 0) {
+        fullArray = val
+        break
+      }
+    }
+    if (!fullArray) {
+      console.log(`[distributeArrayFieldToCells] 前缀 "${pfx}" 未找到数组字段`)
+      continue
+    }
+
+    for (let i = 0; i < fullArray.length; i++) {
+      const key = `${pfx}_${i + 1}`
+      sheetData[key] = fullArray[i]
+    }
+
+    arrayFieldOffsetInfos.push({ prefix: pfx, fullArray, rowsPerPage: rangeInfo.rowCount + 1, fields })
+    prefixSet.add(pfx)
+    console.log(`[distributeArrayFieldToCells] 前缀 "${pfx}" 成功: 数组长度=${fullArray.length}, rowsPerPage=${rangeInfo.rowCount}, copyRange 字段数=${fields.length}`)
+  }
+
+  if (arrayFieldOffsetInfos.length === 0) {
+    console.log('[distributeArrayFieldToCells] 无有效数组,结束')
+    return
+  }
+
+  previewSpread.suspendPaint()
+  try {
+    const copyStartRow = rangeInfo.row
+    const copyRangeRowSet = new Set()
+    for (let r = 0; r < rangeInfo.rowCount; r++) {
+      copyRangeRowSet.add(copyStartRow + r)
+    }
+    for (const oi of arrayFieldOffsetInfos) {
+      for (const f of oi.fields) {
+        const idx = f.index - 1
+        const cellRow = copyStartRow + f.rowInRange
+        if (idx < oi.fullArray.length) {
+          baseSheet.setValue(cellRow, rangeInfo.col + f.col, oi.fullArray[idx])
+        }
+      }
+    }
+    for (let r = 0; r < baseSheet.getRowCount(); r++) {
+      if (copyRangeRowSet.has(r)) continue
+      for (let c = 0; c < baseSheet.getColumnCount(); c++) {
+        const bp = baseSheet.getBindingPath(r, c)
+        if (bp) {
+          const match = bp.match(pattern)
+          if (match && prefixSet.has(match[1])) {
+            const idx = parseInt(match[2]) - 1
+            const oi = arrayFieldOffsetInfos.find(o => o.prefix === match[1])
+            if (oi && idx < oi.fullArray.length) {
+              baseSheet.setValue(r, c, oi.fullArray[idx])
+            }
+          }
+        }
+      }
+    }
+
+    const baseDS = baseSheet.getDataSource()
+    if (baseDS && baseDS.rT) {
+      for (const oi of arrayFieldOffsetInfos) {
+        oi.fullArray.forEach((val, idx) => {
+          baseDS.rT[`${oi.prefix}_${idx + 1}`] = val
+        })
+      }
+    }
+  } finally {
+    previewSpread.resumePaint()
+  }
+}
+
+const adjustBaseSheetArrays = () => {
+  if (!generateData || !generateData.copyRange || !sheetData || !Object.keys(sheetData).length) return
+  const rangeInfo = getCopyRangeInfo()
+  if (!rangeInfo) return
+  const baseSheet = previewSpread.getSheetFromName(generateData.sheetName)
+  if (!baseSheet) return
+  const bindingPaths = getCopyRangeBindingPaths(baseSheet, rangeInfo)
+  const bindingPathSet = new Set(bindingPaths)
+  const rowsPerPage = rangeInfo.rowCount + 1
+  previewSpread.suspendPaint()
+  try {
+    for (let row = 0; row < baseSheet.getRowCount(); row++) {
+      for (let col = 0; col < baseSheet.getColumnCount(); col++) {
+        const bp = baseSheet.getBindingPath(row, col)
+        if (bp && !bindingPathSet.has(bp) && Array.isArray(sheetData[bp])) {
+          const val = baseSheet.getValue(row, col)
+          if (Array.isArray(val)) {
+            baseSheet.setValue(row, col, val.slice(0, rowsPerPage))
+          }
+        }
+      }
+    }
+  } finally {
+    previewSpread.resumePaint()
+  }
+}
+
+const setPageNoToSheet = (sheet, pageNo, rangeInfo) => {
+  if (!rangeInfo) return
+  sheet.tag(String(pageNo))
+}
+
 let generateData
 let sheetNum = ref(2)
 /**
@@ -631,42 +946,53 @@ const handleGenerate = async (isSetTimeout) => {
   const generate = async (isAdd) => {
 
     const sheet = previewSpread.getSheetFromName(generateData.sheetName)
-    if (sheet.visible()) {
-      previewSpread.commandManager().execute({
-        cmd: "copySheet",
-        sheetName: generateData.sheetName,
-        targetIndex: 999,
-        newName: generateData.sheetName + " (" + sheetNum.value + ")",
-        includeBindingSource: true
-      });
-    } else {
+    if (!sheet.visible()) {
       sheet.visible(true)
-      return
     }
+    console.log('生成续页',generateData.sheetName + " (" + sheetNum.value + ")");
+      
+    previewSpread.commandManager().execute({
+      cmd: "copySheet",
+      sheetName: generateData.sheetName,
+      targetIndex: 999,
+      newName: generateData.sheetName + " (" + sheetNum.value + ")",
+      includeBindingSource: true
+    });
     // 范围内的字段叠加
     let col = parseInt(generateData.copyRange.topLeft.split(',')[0])
     let row = parseInt(generateData.copyRange.topLeft.split(',')[1])
     let colCount = parseInt(generateData.copyRange.topRight.split(',')[0]) - col
     let rowCount = parseInt(generateData.copyRange.bottomLeft.split(',')[1]) - row
     const sheet2 = previewSpread.getSheetFromName(generateData.sheetName + " (" + sheetNum.value + ")")
-    let colPathList = []
-    let dynamicTbColRespVOListCode = dynamicTbColRespVOList.map(i => i.colCode)
-    for (let i = 0; i <= colCount; i++) {
-      for (let j = 0; j <= rowCount; j++) {
-        const bindingPath = sheet.getBindingPath(row + j, col + i)
-        if (bindingPath) {
-          let newPath = bindingPath.split('_')[0] + "_" + (parseInt(bindingPath.split('_')[1]) + (rowCount + 1) * (sheetNum.value - 1))
-          sheet2.setBindingPath(row + j, col + i, newPath)
-          if (!dynamicTbColRespVOListCode.includes(newPath)){
-            colPathList.push(newPath)
-          }
+    const rangeInfo = getCopyRangeInfo()
+    const pageNo = sheetNum.value
+    sheetNum.value = sheetNum.value + 1
+    // 先设页码(tag 持久化),后续 dataSource 会覆盖左上角单元格为正确数据
+    setPageNoToSheet(sheet2, pageNo, rangeInfo)
+    // 解绑 copySheet 带来的旧数据源,再绑定新数据源,防止旧数据覆盖新值
+    if (sheet2) {
+      sheet2.setDataSource(null)
+    }
+    if (sheet2 && sheetData && Object.keys(sheetData).length > 0) {
+      const pageData = buildPageDataSource(sheetData, pageNo)
+      const dataSource = new GC.Spread.Sheets.Bindings.CellBindingSource(pageData)
+      sheet2.setDataSource(dataSource)
+
+      // 验证:读取续页 copyRange 第一行和最后一行实际单元格值
+      if (rangeInfo) {
+        const firstRowVals = []
+        const lastRowVals = []
+        for (let c = 0; c <= rangeInfo.colCount; c++) {
+          const bp = sheet2.getBindingPath(rangeInfo.row, rangeInfo.col + c)
+          const firstVal = sheet2.getValue(rangeInfo.row, rangeInfo.col + c)
+          const lastVal = sheet2.getValue(rangeInfo.row + rangeInfo.rowCount, rangeInfo.col + c)
+          firstRowVals.push(`${bp || '?'}=${JSON.stringify(firstVal)}`)
+          lastRowVals.push(`${bp || '?'}=${JSON.stringify(lastVal)}`)
         }
+        console.log(`[generate] pageNo=${pageNo} 续页第一行:`, firstRowVals.join(' | '))
+        console.log(`[generate] pageNo=${pageNo} 续页最后行:`, lastRowVals.join(' | '))
       }
     }
-    sheetNum.value = sheetNum.value + 1
-    if (colPathList.length != 0){
-      await DynamicTbColApi.createBatchByCodes(colPathList,props.initData.templateId)
-    }
     if (isAdd){
       let col = parseInt(generateData.copyRange.topLeft.split(',')[0])
       let row = parseInt(generateData.copyRange.topLeft.split(',')[1])
@@ -676,9 +1002,9 @@ const handleGenerate = async (isSetTimeout) => {
         return;
       }
       if (generateData.hidden) {
-        // 判断第一行,如果全部为空,则隐藏
         let isEmpty = true;
-        for (let i = 0; i < colCount; i++) {
+        for (let i = 0; i <= colCount; i++) {
+          if (i === 0) continue
           const range = sheet2.getCell(row, col + i)
           if (range.value()) {
             isEmpty = false;
@@ -689,15 +1015,28 @@ const handleGenerate = async (isSetTimeout) => {
           sheet2.visible(false)
         }
       }
-      // 判断最后一行,如果不为空,自动续页
       let isNotEmpty = false;
-      for (let i = 0; i < colCount; i++) {
+      for (let i = 0; i <= colCount; i++) {
         const range = sheet2.getCell(row + rowCount, col + i)
         if (range.value()) {
           isNotEmpty = true;
           break;
         }
       }
+      console.log(`[generate] pageNo=${pageNo} 单元格检测 isNotEmpty=${isNotEmpty} (检查最后行 row=${row + rowCount}, 列范围=[${col},${col + colCount}])`)
+
+      if (!isNotEmpty && arrayFieldOffsetInfos.length > 0) {
+        for (const oi of arrayFieldOffsetInfos) {
+          const copyMinIndex = Math.min(...oi.fields.map(f => f.index))
+          const usedSoFar = (copyMinIndex - 1) + pageNo * oi.rowsPerPage
+          console.log(`[generate] 续页检查 pageNo=${pageNo}, prefix=${oi.prefix}, usedSoFar=${usedSoFar}, fullArrayLen=${oi.fullArray.length}, 是否需续页=${oi.fullArray.length > usedSoFar}`)
+          if (oi.fullArray.length > usedSoFar) {
+            isNotEmpty = true
+            break
+          }
+        }
+      }
+
       if (isNotEmpty) {
         await generate(true)
       }
@@ -739,14 +1078,17 @@ const hiddenPage = async () => {
   let row = parseInt(generateData.copyRange.topLeft.split(',')[1])
   let colCount = parseInt(generateData.copyRange.topRight.split(',')[0]) - col
   let rowCount = parseInt(generateData.copyRange.bottomLeft.split(',')[1]) - row
+
+  distributeArrayFieldToCells()
+
   const sheet = previewSpread.getSheetFromName(generateData.sheetName)
   if (!sheet) {
     return;
   }
   if (generateData.hidden) {
-    // 判断第一行,如果全部为空,则隐藏
     let isEmpty = true;
-    for (let i = 0; i < colCount; i++) {
+    for (let i = 0; i <= colCount; i++) {
+      if (i === 0) continue
       const range = sheet.getCell(row, col + i)
       if (range.value()) {
         isEmpty = false;
@@ -757,17 +1099,36 @@ const hiddenPage = async () => {
       sheet.visible(false)
     }
   }
-  // 判断最后一行,如果不为空,自动续页
   let isNotEmpty = false;
-  for (let i = 0; i < colCount; i++) {
+  for (let i = 0; i <= colCount; i++) {
     const range = sheet.getCell(row + rowCount, col + i)
     if (range.value()) {
       isNotEmpty = true;
       break;
     }
   }
+
+  console.log('[hiddenPage] 单元格检测 isNotEmpty:', isNotEmpty, '| arrayFieldOffsetInfos:', arrayFieldOffsetInfos.length)
+
+  if (arrayFieldOffsetInfos.length > 0) {
+    for (const oi of arrayFieldOffsetInfos) {
+      const copyMinIndex = Math.min(...oi.fields.map(f => f.index))
+      const usedOnPage1 = (copyMinIndex - 1) + oi.rowsPerPage
+      console.log(`[hiddenPage] 前缀=${oi.prefix}, 数组长度=${oi.fullArray.length}, copyMinIndex=${copyMinIndex}, rowsPerPage=${oi.rowsPerPage}, usedOnPage1=${usedOnPage1}`)
+      if (oi.fullArray.length > usedOnPage1) {
+        isNotEmpty = true
+        break
+      }
+    }
+  }
+
+  console.log('[hiddenPage] 最终 isNotEmpty:', isNotEmpty)
+
   if (isNotEmpty) {
-    handleGenerate(false)
+    await handleGenerate(false)
+    adjustBaseSheetArrays()
+  } else {
+    distributePageDataSources()
   }
 }
 /**

+ 5 - 6
yudao-ui-admin-vue3/src/utils/reportUtil.ts

@@ -272,22 +272,21 @@ export const handleCopy = (spread) => {
       for (let row = fillRange.row; row < fillRange.row + fillRange.rowCount; row++) {
         for (let col = fillRange.col; col < fillRange.col + fillRange.colCount; col++) {
           // 获取当前单元格的bindingPath
-          const path = info.sheet.getBindingPath(row, col);
+          const path = info.sheet.getBindingPath(row, col)
 
           // Matthew:注意,没有path时也不能跳过,因为绑定路径为undefined也是一种绑定路径
           // 如果path存在,则记录;否则,跳过该单元格
           // 使用复合键存储信息
-          const compositeKey = `${row},${col}`;
+          const compositeKey = `${row},${col}`
           bindingPathRecMap.set(compositeKey, {
             row: row,
             col: col,
-            path: path,
-          });
+            path: path
+          })
         }
       }
-
     } catch (getErr) {
-      console.error("Error getting binding path:", getErr);
+      console.error('Error getting binding path:', getErr)
     }
   });
 

+ 67 - 0
yudao-ui-admin-vue3/src/views/pressure2/equipboiler/EquipBoilerForm.vue

@@ -551,6 +551,13 @@
               <el-button type="primary" size="small" @click="handleAddPressurePart">
                 <Icon icon="ep:plus" class="mr-5px" /> 添加
               </el-button>
+              <el-button type="warning" size="small" plain @click="handleImportPressureParts">
+                <Icon icon="ep:upload" class="mr-5px" /> 导入
+              </el-button>
+              <el-button type="info" size="small" plain @click="handleDownloadTemplate">
+                <Icon icon="ep:download" class="mr-5px" /> 下载模板
+              </el-button>
+              <input ref="fileInputRef" type="file" accept=".xlsx,.xls" style="display:none" @change="handleFileChange" />
             </div>
             <el-table :data="pressureParts" border size="small" style="width: 100%">
               <el-table-column label="部件" width="180" align="center">
@@ -865,6 +872,8 @@ import dayjs from "dayjs";
 import Copy from "@/views/system/equipcontainer/components/Copy.vue";
 import { useEmitt } from '@/hooks/web/useEmitt'
 import { Icon } from '@/components/Icon'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import download from '@/utils/download'
 
 /** 锅炉设备 表单 */
 defineOptions({ name: 'EquipBoilerForm' })
@@ -1334,6 +1343,64 @@ const handleRemovePressurePart = (index: number) => {
   pressureParts.value.splice(index, 1)
 }
 
+// 文件输入引用
+const fileInputRef = ref<HTMLInputElement>()
+
+// 导入受压元件 Excel(点击触发隐藏文件选择)
+const handleImportPressureParts = () => {
+  fileInputRef.value?.click()
+}
+
+// 下载导入模板
+const handleDownloadTemplate = async () => {
+  const data = await EquipBoilerApi.downloadPressurePartsTemplate()
+  download.excel(data, '锅炉受压元件导入模板.xls')
+}
+
+// 文件选择后的处理:通过受压元件名称匹配,已存在则修改、不存在则新增
+const handleFileChange = async (event: Event) => {
+  const target = event.target as HTMLInputElement
+  if (!target.files || target.files.length === 0) return
+  const file = target.files[0]
+  try {
+    const importedList = await EquipBoilerApi.importPressureParts(file)
+    if (importedList && importedList.length > 0) {
+      let addCount = 0
+      let updateCount = 0
+      importedList.forEach((item: any) => {
+        const partName = item.partName || ''
+        const existingIndex = pressureParts.value.findIndex(p => p.partName === partName && partName !== '')
+        if (existingIndex >= 0) {
+          // 已存在,修改
+          pressureParts.value[existingIndex] = {
+            partType: item.partType || '',
+            partName: item.partName || '',
+            specification: item.specification || '',
+            material: item.material || '',
+            remark: item.remark || ''
+          }
+          updateCount++
+        } else {
+          // 不存在,新增
+          pressureParts.value.push({
+            partType: item.partType || '',
+            partName: item.partName || '',
+            specification: item.specification || '',
+            material: item.material || '',
+            remark: item.remark || ''
+          })
+          addCount++
+        }
+      })
+      message.success(`导入完成:新增 ${addCount} 条,更新 ${updateCount} 条`)
+    } else {
+      message.warning('导入的数据为空')
+    }
+  } finally {
+    target.value = ''
+  }
+}
+
 // 打开单位选择框
 const handleOpenUnitDialog = (ev) => {
   ev.target.blur()