官网地址:工作表配置 | Luckysheet文档
<template>
  <div class="report-management-container">
    <div style="margin-bottom: 16px; display: flex; gap: 10px;">
      <el-button type="primary" class="export-excel" @click="openStaffUploadDialog">上传文件</el-button>
      <el-button type="warning" class="export-excel" @click="handleExportExcel">导出Excel(含图片)</el-button>
      <el-button type="warning" class="export-excel" @click="handleExportExcelLocal">导出Excel(不含图片)</el-button>
      <el-select v-model="ExcelValue" class="export-excel" placeholder="请选择版本信息" @change="changeExcel()">
        <el-option
          v-for="item in HistoryExcel"
          :key="item.value"
          :label="item.name"
          :value="item.value"
        />
      </el-select>
      <el-select v-model="Dataheader" class="custom-tags-select" multiple collapse-tags placeholder="请选择要导出的列">
        <el-option
          v-for="(item, index) in DataheaderTitle"
          :key="index"
          :label="item"
          :value="item"
        />
      </el-select>
    </div>
    <el-form :inline="true" :model="searchDta" class="demo-form-inline">
      <el-form-item label="类型">
        <el-input v-model="searchDta.type" placeholder="类型" />
      </el-form-item>
      <el-form-item label="编码">
        <el-input v-model="searchDta.commodity_code" placeholder="编码" />
      </el-form-item>
      <el-form-item label="成本优化比例">
        <el-input v-model="searchDta.optimization_ratio" placeholder="成本优化比例" />
      </el-form-item>
      <el-form-item label="单价">
        <el-input v-model="searchDta.unit_price" placeholder="单价" />
      </el-form-item>
      <el-form-item label="人工包装">
        <el-input v-model="searchDta.manual_packaging_quote" placeholder="人工包装" />
      </el-form-item>
      <el-form-item label="赠品">
        <el-input v-model="searchDta.gift_quote" placeholder="赠品" />
      </el-form-item>
      <el-form-item label="快递费">
        <el-input v-model="searchDta.express_quote" placeholder="快递费" />
      </el-form-item>
      <el-form-item label="仓库">
        <el-input v-model="searchDta.warehouse" placeholder="仓库" />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="onSubmit">查询</el-button>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="onClear">清空</el-button>
      </el-form-item>
    </el-form>

    <div class="luckysheetBody">
      <div class="imageOper" @click="imageOperation()">
        图片操作
      </div>
      <div id="luckysheet" class="luckysheetStyle" @click="handleLuckysheetClick"
           @dblclick="handleLuckysheetClick"
      />
    </div>
    <el-button type="primary" style="margin-top: 16px;" @click="submitToBackend">保存</el-button>
    <!-- 上传文件弹窗 -->
    <el-dialog title="上传报表文件" :visible.sync="isStaffUploadDialogVisible" width="500px"
               :close-on-click-modal="false"
               class="staff-upload-dialog"
    >
      <div class="upload-container">
        <div class="upload-card">
          <el-upload class="upload-btn-group" action="#" :auto-upload="false" :on-change="handleStaffFileSelect"
                     :on-remove="handleStaffFileRemove" :file-list="staffUploadFileList" accept=".xlsx,.xls,.csv"
                     :multiple="false"
          >
            <div class="upload-btn-inner">
              <i class="el-icon-upload2 upload-icon" />
              <span class="upload-text">点击选择文件</span>
              <span class="upload-subtext">或拖拽文件至此处</span>
            </div>
          </el-upload>
          <!-- 已选文件展示 -->
          <div v-if="staffUploadFileList.length > 0" class="selected-file-card">
            <div class="file-info-row">
              <i class="el-icon-file-excel file-icon" />
              <div class="file-detail">
                <div class="file-name">{{ staffUploadFileList[0].name }}</div>
                <div class="file-meta">
                  <span>大小:{{ formatFileSize(staffUploadFileList[0].size) }}</span>
                  <el-button type="text" size="mini" class="remove-file-btn"
                             @click="handleStaffFileRemove(staffUploadFileList[0], [])"
                  >
                    移除
                  </el-button>
                </div>
              </div>
            </div>
          </div>
        </div>
        <!-- 上传规则提示 -->
        <div class="upload-rule-tip">
          <i class="el-icon-info-circle tip-icon" />
          <span>支持格式:.xlsx /.xls(单个文件≤10MB,仅允许上传1个文件)</span>
        </div>
      </div>
      <div slot="footer" class="dialog-footer">
        <el-button @click="closeStaffUploadDialog">取消</el-button>
        <el-button type="primary" @click="confirmStaffFileUpload">确认上传</el-button>
      </div>
    </el-dialog>
    <!-- 查看图片弹窗   -->
    <el-dialog title="查看图片" :visible.sync="imageDailog" width="500px"
               :close-on-click-modal="false"
               class="staff-upload-dialog"
    >
      <div class="upload-container">
        <el-image :src="imageUrlData" />
      </div>
    </el-dialog>
    <!-- 成本价弹窗导出 -->
    <ConstPrice :cost-price-show.sync="costPriceShow" :cost-price-list="costPriceList" />
  </div>
</template>

<script>
import {
  apiUploadFinancialQuotationExcel,
  apiGetFinancialQuotationExcel,
  apiUploadWebPage,
  apiGetExcelList,
  apiGetCostPrice,
  apiUploadExcel,
} from '@/api/financialExcel'
import axios from 'axios'
import ExcelJS from 'exceljs'
import FileSaver from 'file-saver'
import ConstPrice from './components/ConstPrice.vue'
import 'luckysheet/dist/plugins/css/pluginsCss.css'
import 'luckysheet/dist/plugins/plugins.css'
import luckysheet from 'luckysheet'

export default {
  name: 'ExcelTest',
  components: {ConstPrice},
  props: {widthd: {type: String, default: ''}},
  data() {
    return {
      showExcel: false,
      isStaffUploadDialogVisible: false,
      staffUploadFileList: [],
      currentTableData: null, // 保存原始表格数据用于点击事件
      HistoryExcel: [], // 保存历史版本信息
      ExcelValue: null, // 存放历史版本信息
      imageUrlData: null, // 存放查看图片地址
      imageDailog: false, // 存放图片弹窗开启与否
      clickTimer: null, // 计时器
      clickCount: 0, // 点击次数
      doubleClickThreshold: 300, // 双击时间阈值(毫秒)
      costPriceShow: false, // 成本价弹窗
      costPriceList: [], // 成本价列表
      DataheaderTitle: ['日期', '大类', '类型', '产品图片', '报价依据', '商品编码', '产品明细', '数量', '成本价', '成本优化比例', '特殊加价', '单价', '人工包装报价', '人工包装优化比例', '人工包装', '赠品报价', '赠品优化比例', '赠品', '干冰报价', '干冰优化比例', '干冰', '快递报价', '快递优化比例', '快递费', '合计', '仓库', '备注'],
      Dataheader: [],
      searchDta: { // 用于存放搜索条件
        type: null, // 类型
        commodity_code: null, // 编码
        optimization_ratio: null, // 优化比例
        unit_price: null, // 单价
        manual_packaging_quote: null, // 人工包装
        gift_quote: null, // 赠品
        dry_ice_quote: null, // 干冰
        express_quote: null, // 快递费
        warehouse: null, // 仓库
      },
      merges: [], // 用于存储当前表格的合并规则
      rowIndexDta: '',
      columnIndexData: ''
    }
  },
  mounted() {
    window.luckysheet = luckysheet
    // 获取最新excel数据
    this.getFinancialQuotationList()
    // 获取excel历史数据
    this.getHistoryExcelList()
  },
  beforeDestroy() {
    if (window.luckysheet && window.luckysheet.destroy) {
      window.luckysheet.destroy()
    }
  },
  methods: {
    // 查询
    onSubmit() {
      this.getFinancialQuotationList()
    },
    // 清空
    onClear() {
      this.searchDta = { // 用于存放搜索条件
        type: null, // 类型
        commodity_code: null, // 编码
        optimization_ratio: null, // 优化比例
        unit_price: null, // 单价
        manual_packaging_quote: null, // 人工包装
        gift_quote: null, // 赠品
        dry_ice_quote: null, // 干冰
        express_quote: null, // 快递费
        warehouse: null, // 仓库
      }
      this.ExcelValue = null
      this.Dataheader = []
      this.getFinancialQuotationList()
    },
    // 处理单元格点击事件
    handleLuckysheetClick(event) {
      // 关键修改:判断事件类型,如果是双击,则直接返回,不执行任何自定义逻辑
      // 这样 Luckysheet 的默认双击编辑行为就会被触发
      if (event.type === 'dblclick') {
        return
      }
      // --- 以下是你原有的单击逻辑,保持不变 ---
      if (!window.luckysheet) return

      let rowIndex, columnIndex
      const cellEl = event.target.closest('.luckysheet-cell')

      if (cellEl) {
        rowIndex = parseInt(cellEl.getAttribute('data-r'), 10)
        columnIndex = parseInt(cellEl.getAttribute('data-c'), 10)
      } else {
        const selectedRange = window.luckysheet.getRange()
        if (!selectedRange || !selectedRange[0].row || !selectedRange[0].column) return

        rowIndex = selectedRange[0].row[0]
        columnIndex = selectedRange[0].column[0]
      }
      this.rowIndexDta = rowIndex + 1
      this.columnIndexData = columnIndex + 1
      this.costList(this.rowIndexDta, this.columnIndexData)
    },
    // 判断点击的是产品图片还是报价依据
    imageOperation() {
      if (this.rowIndexDta <= 1 || (this.columnIndexData !== 4 && this.columnIndexData !== 5)) {
        this.$message.error('请先选择产品图片,或者报价依据列')
        return
      }
      this.processCellClick(this.rowIndexDta, this.columnIndexData)
      this.processCellClickN(this.rowIndexDta, this.columnIndexData)
    },
    // 调用成本价表格
    costList(rowIndex, columnIndex) {
      // console.log('单机才出现')
      if (rowIndex <= 1 || columnIndex !== 6) return
      const dataRowIndex = rowIndex - 2
      let commodity_code = ''

      // 优先从表格获取最新地址
      const sheetIndex = window.luckysheet.currentSheetIndex
      const cellValue = window.luckysheet.getCellValue(rowIndex - 1, columnIndex - 1, sheetIndex)
      if (cellValue && typeof cellValue === 'string' && (cellValue.startsWith('http') || cellValue.startsWith('https'))) {
        commodity_code = cellValue
      } else {
        commodity_code = this.currentTableData?.defaultData?.[dataRowIndex]?.commodity_code || ''
      }
      // console.log(commodity_code, '======')
      // 获取数据
      this.costPriceShow = false
      this.$nextTick(() => {
        this.getCostPrice(commodity_code)
      })
    },
    // 获取数据
    async getCostPrice(row) {
      const params = {
        commodity_code: row
      }
      const res = await apiGetCostPrice(params)
      // console.log(res, 'popopo')
      if (res.meta.status === 200) {
        if (res.data && Array.isArray(res.data)) {
          res.data = res.data.map(item => ({
            ...item,
            date: item.date_type + item.date,
          }))
        }
        this.costPriceList = res.data
        this.costPriceShow = true
      } else {
        this.$message.error(res.meta.msg || '获取失败')
      }
    },
    // 处理图片预览
    processCellClick(rowIndex, columnIndex) {
      // 1. 检查是否点击在数据区的产品图片列(第4列)
      //    rowIndex 和 columnIndex 是从1开始的
      if (rowIndex <= 1 || columnIndex !== 4) return

      // 2. 处理合并单元格,找到合并区域的主单元格(左上角)
      //    this.merges 中存储的是从0开始的索引
      const mergeInfo = this.merges.find(merge => {
        // 检查当前点击的单元格(转换为0开始)是否在某个合并范围内
        return (rowIndex - 1) >= merge.r1 && (rowIndex - 1) <= merge.r2 &&
          (columnIndex - 1) >= merge.c1 && (columnIndex - 1) <= merge.c2
      })

      let targetRow, targetCol
      if (mergeInfo) {
        // 如果是合并单元格,目标单元格为主单元格(转换回1开始的索引)
        targetRow = mergeInfo.r1 + 1
        targetCol = mergeInfo.c1 + 1
      } else {
        // 如果不是合并单元格,目标单元格就是点击的单元格
        targetRow = rowIndex
        targetCol = columnIndex
      }

      // 3. 获取当前目标单元格的图片地址
      let imageUrl = ''
      const dataRowIndex = targetRow - 2 // 计算对应原始数据数组的索引

      // 优先从 luckysheet 的当前数据中获取
      const sheetIndex = window.luckysheet.currentSheetIndex
      // getCellValue 需要传入从0开始的索引
      const cellValue = window.luckysheet.getCellValue(targetRow - 1, targetCol - 1, sheetIndex)
      if (cellValue && typeof cellValue === 'string' && (cellValue.startsWith('http') || cellValue.startsWith('https'))) {
        imageUrl = cellValue
      } else {
        // 如果表格中没有,则从原始数据中获取
        imageUrl = this.currentTableData?.defaultData?.[dataRowIndex]?.product_image_base64 || ''
      }

      // 4. 根据是否有图片地址,弹出不同的确认框
      if (imageUrl) {
        // 自定义确认框配置
        const confirmConfig = {
          confirmButtonText: '修改图片',
          cancelButtonText: '查看图片',
          type: 'warning',
          cancelButtonClass: 'custom-cancel-btn',
          beforeClose: (action, instance, done) => {
            const cancelBtn = instance.$el.querySelector('.custom-cancel-btn')
            const isClickCancelBtn = cancelBtn && document.activeElement === cancelBtn

            if (action === 'confirm') {
              // 点击“修改图片”
              this.triggerImageUploadForModify(targetRow, targetCol)
              done()
            } else if (action === 'cancel' && isClickCancelBtn) {
              // 点击“查看图片”
              this.imageDailog = true
              this.imageUrlData = imageUrl
              done()
            } else {
              // 点击X号或ESC关闭
              done()
            }
          }
        }

        this.$confirm('请选择您的操作?', '提示', confirmConfig)
          .then(() => {})
          .catch(() => {})
      } else {
        // 如果没有图片地址,则弹出“新增图片”的提示
        this.$confirm('该单元格暂无图片,是否上传?', '提示', {
          confirmButtonText: '新增图片',
          showCancelButton: false,
          type: 'info'
        }).then(() => {
          this.triggerImageUploadForModify(targetRow, targetCol)
        }).catch(() => {})
      }
    },
    // 触发图片上传(用于“修改”或“新增”场景)
    triggerImageUploadForModify(targetRow, targetCol) {
      const _this = this
      // 验证是否为产品图片列(第4列,索引3)
      if (targetCol !== 4) {
        _this.$message.warning('请在"产品图片"列(第4列)操作图片')
        return
      }

      // 创建文件选择框
      const fileInput = document.createElement('input')
      fileInput.type = 'file'
      fileInput.accept = 'image/*'
      fileInput.style.display = 'none'
      document.body.appendChild(fileInput)

      // 选择图片后触发上传
      fileInput.onchange = async (e) => {
        const file = e.target.files[0]
        const formData = new FormData()
        formData.append('file', file)
        const response = await axios.post(
          this.$baseApiUrl + '/api/upload', // 全局基础地址 + 接口路径
          formData,
          {
            headers: {
              'Content-Type': 'multipart/form-data'
            }
          }
        )
        const newUrl = response.data.data.img_url
        if (targetCol === 4) {
          // 直接设置单元格值并刷新
          window.luckysheet.setCellValue(targetRow - 1, targetCol - 1, newUrl)
          window.luckysheet.refresh() // 刷新表格显示最新值
          _this.$message.success('图片上传成功,已更新第4列表格数据')
          // ========== 新增:设置焦点到目标单元格输入框 ==========
          // 先获取目标单元格的 DOM 元素
          const cellElement = document.querySelector(`.luckysheet-cell[data-r="${targetRow - 1}"][data-c="${targetCol - 1}"]`)
          if (cellElement) {
            // 触发单元格编辑状态,使输入框获得焦点
            window.luckysheet.editCell(targetRow - 1, targetCol - 1)
          }
        } else {
          _this.$message.warning('仅允许更新第4列(产品图片列)的数据')
        }
        // 移除临时文件输入框
        document.body.removeChild(fileInput)
      }

      // 触发文件选择框点击
      fileInput.click()
    },
    processCellClickN(rowIndex, columnIndex) {
      // 1. 检查是否点击在数据区的产品图片列(第4列)
      //    rowIndex 和 columnIndex 是从1开始的
      if (rowIndex <= 1 || columnIndex !== 5) return

      // 2. 处理合并单元格,找到合并区域的主单元格(左上角)
      //    this.merges 中存储的是从0开始的索引
      const mergeInfo = this.merges.find(merge => {
        // 检查当前点击的单元格(转换为0开始)是否在某个合并范围内
        return (rowIndex - 1) >= merge.r1 && (rowIndex - 1) <= merge.r2 &&
          (columnIndex - 1) >= merge.c1 && (columnIndex - 1) <= merge.c2
      })

      let targetRow, targetCol
      if (mergeInfo) {
        // 如果是合并单元格,目标单元格为主单元格(转换回1开始的索引)
        targetRow = mergeInfo.r1 + 1
        targetCol = mergeInfo.c1 + 1
      } else {
        // 如果不是合并单元格,目标单元格就是点击的单元格
        targetRow = rowIndex
        targetCol = columnIndex
      }

      // 3. 获取当前目标单元格的图片地址
      let imageUrl = ''
      const dataRowIndex = targetRow - 2 // 计算对应原始数据数组的索引

      // 优先从 luckysheet 的当前数据中获取
      const sheetIndex = window.luckysheet.currentSheetIndex
      // getCellValue 需要传入从0开始的索引
      const cellValue = window.luckysheet.getCellValue(targetRow - 1, targetCol - 1, sheetIndex)
      if (cellValue && typeof cellValue === 'string' && (cellValue.startsWith('http') || cellValue.startsWith('https'))) {
        imageUrl = cellValue
      } else {
        // 如果表格中没有,则从原始数据中获取
        imageUrl = this.currentTableData?.defaultData?.[dataRowIndex]?.quotation_basis || ''
      }

      // 4. 根据是否有图片地址,弹出不同的确认框
      if (imageUrl) {
        // 自定义确认框配置
        const confirmConfig = {
          confirmButtonText: '修改图片',
          cancelButtonText: '查看图片',
          type: 'warning',
          cancelButtonClass: 'custom-cancel-btn',
          beforeClose: (action, instance, done) => {
            const cancelBtn = instance.$el.querySelector('.custom-cancel-btn')
            const isClickCancelBtn = cancelBtn && document.activeElement === cancelBtn

            if (action === 'confirm') {
              // 点击“修改图片”
              this.triggerImageUploadForModifyN(targetRow, targetCol)
              done()
            } else if (action === 'cancel' && isClickCancelBtn) {
              // 点击“查看图片”
              this.imageDailog = true
              this.imageUrlData = imageUrl
              done()
            } else {
              // 点击X号或ESC关闭
              done()
            }
          }
        }

        this.$confirm('请选择您的操作?', '提示', confirmConfig)
          .then(() => {})
          .catch(() => {})
      } else {
        // 如果没有图片地址,则弹出“新增图片”的提示
        this.$confirm('该单元格暂无图片,是否上传?', '提示', {
          confirmButtonText: '新增图片',
          showCancelButton: false,
          type: 'info'
        }).then(() => {
          this.triggerImageUploadForModifyN(targetRow, targetCol)
        }).catch(() => {})
      }
    },
    // 触发图片上传(用于“修改”或“新增”场景)
    triggerImageUploadForModifyN(targetRow, targetCol) {
      const _this = this
      // 验证是否为产品图片列(第4列,索引3)
      if (targetCol !== 5) {
        _this.$message.warning('请在"报价依据"列(第5列)操作图片')
        return
      }

      // 创建文件选择框
      const fileInput = document.createElement('input')
      fileInput.type = 'file'
      fileInput.accept = 'image/*'
      fileInput.style.display = 'none'
      document.body.appendChild(fileInput)

      // 选择图片后触发上传
      fileInput.onchange = async (e) => {
        const file = e.target.files[0]
        const formData = new FormData()
        formData.append('file', file) // 后端接收文件的字段名,需与后端一致
        const response = await axios.post(
          this.$baseApiUrl + '/api/upload', // 全局基础地址 + 接口路径
          formData,
          {
            headers: {
              'Content-Type': 'multipart/form-data'
            }
          }
        )
        const newUrl = response.data.data.img_url
        if (targetCol === 5) {
          // 直接设置单元格值并刷新
          window.luckysheet.setCellValue(targetRow - 1, targetCol - 1, newUrl)
          window.luckysheet.refresh() // 刷新表格显示最新值
          _this.$message.success('图片上传成功,已更新第5列表格数据')
          // ========== 新增:设置焦点到目标单元格输入框 ==========
          // 先获取目标单元格的 DOM 元素
          const cellElement = document.querySelector(`.luckysheet-cell[data-r="${targetRow - 1}"][data-c="${targetCol - 1}"]`)
          if (cellElement) {
            // 触发单元格编辑状态,使输入框获得焦点
            window.luckysheet.editCell(targetRow - 1, targetCol - 1)
          }
        } else {
          _this.$message.warning('仅允许更新第5列(报价依据列)的数据')
        }
        // 移除临时文件输入框
        document.body.removeChild(fileInput)
      }

      // 触发文件选择框点击
      fileInput.click()
    },
    // 格式化文件大小
    formatFileSize(size) {
      if (size < 1024) return `${size} B`
      if (size < 1024 * 1024) return `${(size / 1024).toFixed(2)} KB`
      return `${(size / (1024 * 1024)).toFixed(2)} MB`
    },
    // 打开上传弹窗
    openStaffUploadDialog() {
      this.isStaffUploadDialogVisible = true
    },
    // 关闭上传弹窗
    closeStaffUploadDialog() {
      this.isStaffUploadDialogVisible = false
      this.staffUploadFileList = []
    },
    // 处理文件选择
    handleStaffFileSelect(file, fileList) {
      if (fileList.length > 1) {
        this.staffUploadFileList = [fileList[fileList.length - 1]]
        this.$message.info('仅支持上传单个文件,已自动保留最新选择的文件')
      } else {
        this.staffUploadFileList = fileList
      }
    },
    // 移除文件
    handleStaffFileRemove(file, fileList) {
      this.staffUploadFileList = fileList
    },
    // 确认上传文件
    async confirmStaffFileUpload() {
      if (this.staffUploadFileList.length === 0) {
        this.$message.warning('请选择要上传的文件')
        return
      }

      const file = this.staffUploadFileList[0].raw
      if (!file || !(file instanceof File)) {
        this.$message.error('文件解析失败,请重新选择')
        return
      }

      const employee_id = this.$store.getters.employeeId || '513433090121037835'
      const formData = new FormData()
      formData.append('file', file)
      formData.append('employee_id', employee_id)

      try {
        const res = await apiUploadFinancialQuotationExcel(formData)
        if (res.meta.status === 200) {
          this.$message.success('文件上传成功')
          this.closeStaffUploadDialog()
          // 先处理ExcelValue值为空,调取最新的一版
          // this.ExcelValue = null
          this.onClear()
          // this.getFinancialQuotationList() // 调用接口方法集成于onClear方法中暂定隐藏
          this.getHistoryExcelList()
        } else {
          this.$message.error(res.meta.msg || '文件上传失败')
        }
      } catch (error) {
        console.error('文件上传失败:', error)
        this.$message.error('文件上传出错,请重试')
      }
    },
    // 获取数据
    async getFinancialQuotationList() {
      this.searchDta.version = this.ExcelValue
      const res = await apiGetFinancialQuotationExcel(this.searchDta)
      if (res.meta.status === 200) {
        res.data.testData.header = this.DataheaderTitle
        if (res.data.testData.defaultData && Array.isArray(res.data.testData.defaultData)) {
          res.data.testData.defaultData = res.data.testData.defaultData.map(item => ({
            ...item,
            date: item.date_type + item.date,
            product_image_base64: item.product_image_url,
          }))
        }
        this.currentTableData = res.data.testData

        // 【修改】接收包含calcChain的数据
        const convertedData = this.convertTestDataToLuckysheet(res.data.testData)

        // 【修改】调用初始化方法,并把sheetData和calcChain传过去
        this.initializeTableWithData(convertedData.sheetData, convertedData.calcChain)
      }
    },
    async getHistoryExcelList() {
      const res = await apiGetExcelList()
      // console.log('获取excel数据成功', res)
      if (res.meta.status === 200) {
        // 处理数据,拆分为键值对的形式
        this.HistoryExcel = res.data.map(item => {
          const key = Object.keys(item)[0]
          const [name] = key.split(': ') // 按第一个": "分割键名
          return {
            name: name.trim(),
            value: item[key]
          }
        })
        // console.log(this.HistoryExcel, 'pppppp')
      }
    },
    // 版本信息改变的时候重新调用数据
    changeExcel() {
      // console.log(this.ExcelValue, '[][][]')
      this.getFinancialQuotationList()
    },
    // 转换数据为Luckysheet格式
    convertTestDataToLuckysheet(row) {
      const { sheetName, header, defaultData } = row
      const celldata = []
      const dynamicMerges = [] // 存储合并规则
      const calcChain = [] // 【新增】用于收集公式链信息

      // 大类合并逻辑
      const majorCategoryGroups = {}
      defaultData.forEach((item, rowIndexInData) => {
        const category = item.category
        const luckysheetRowIndex = rowIndexInData + 1 // 数据行从1开始(0是表头)
        if (!majorCategoryGroups[category]) {
          majorCategoryGroups[category] = { startRow: luckysheetRowIndex, rows: 0 }
        }
        majorCategoryGroups[category].rows++
      })
      Object.values(majorCategoryGroups).forEach((group) => {
        if (group.rows > 1) {
          dynamicMerges.push({
            r1: group.startRow,
            c1: 1,
            r2: group.startRow + group.rows - 1,
            c2: 1,
          })
        }
      })

      // 类型、产品图片、报价依据合并逻辑(列索引2-4)
      const fourFieldGroups = {}
      defaultData.forEach((item, rowIndexInData) => {
        const groupKey = [item.category, item.type, item.product_image_base64, item.quotation_basis].join('|')
        const luckysheetRowIndex = rowIndexInData + 1
        if (!fourFieldGroups[groupKey]) {
          fourFieldGroups[groupKey] = { startRow: luckysheetRowIndex, rows: 0 }
        }
        fourFieldGroups[groupKey].rows++
      })
      Object.values(fourFieldGroups).forEach((group) => {
        if (group.rows > 1) {
          for (let col = 2; col <= 4; col++) {
            dynamicMerges.push({
              r1: group.startRow,
              c1: col,
              r2: group.startRow + group.rows - 1,
              c2: col,
            })
          }
        }
      })

      // 处理表头
      header.forEach((headerText, colIndex) => {
        const mergeInfo = this.findMergeInfo(0, colIndex, dynamicMerges)
        const cellVConfig = {
          v: headerText,
          t: 's',
          font: { bold: true },
        }

        if (mergeInfo) {
          if (mergeInfo.isStartCell) {
            cellVConfig.mc = {
              r: mergeInfo.startR,
              c: mergeInfo.startC,
              rs: mergeInfo.r2 - mergeInfo.startR + 1,
              cs: mergeInfo.c2 - mergeInfo.startC + 1,
            }
          } else {
            cellVConfig.mc = {
              r: mergeInfo.startR,
              c: mergeInfo.startC,
            }
          }
          cellVConfig.border = {
            top: { style: 'none' },
            bottom: { style: 'none' },
            left: { style: 'none' },
            right: { style: 'none' },
          }
        } else {
          cellVConfig.border = {
            top: { style: 'thin', color: '#e6e6e6' },
            bottom: { style: 'thin', color: '#e6e6e6' },
            left: { style: 'thin', color: '#e6e6e6' },
            right: { style: 'thin', color: '#e6e6e6' },
          }
        }

        celldata.push({ r: 0, c: colIndex, v: cellVConfig })
      })

      // 处理数据行
      defaultData.forEach((rowData, rowIndexInData) => {
        const luckysheetRowIndex = rowIndexInData + 1
        const rowValues = [
          rowData.date, rowData.category, rowData.type, rowData.product_image_base64, rowData.quotation_basis,
          rowData.commodity_code, rowData.product_details, rowData.quantity, rowData.cost_price,
          rowData.optimization_ratio, rowData.special_markup, rowData.unit_price, rowData.manual_packaging_quote,
          rowData.manual_packaging_optimization_ratio, rowData.manual_packaging_cost, rowData.gift_quote,
          rowData.gift_optimization_ratio, rowData.gift_cost, rowData.dry_ice_quote, rowData.dry_ice_optimization_ratio,
          rowData.dry_ice_cost, rowData.express_quote, rowData.express_optimization_ratio, rowData.express_fee,
          rowData.total, rowData.warehouse, rowData.remarks,
        ]

        rowValues.forEach((cellValue, colIndex) => {
          const mergeInfo = this.findMergeInfo(luckysheetRowIndex, colIndex, dynamicMerges)
          let cellVConfig
          const dependencyColumns = [7, 8, 9, 10] // H, I, J, K列的索引

          // 产品图片列处理(索引3)
          if (colIndex === 3) {
            if (mergeInfo && mergeInfo.isStartCell) {
              cellVConfig = {
                v: cellValue || '',
                t: 's',
                ht: 0,
                vt: 0,
                color: '#409eff',
                underline: true,
                border: {
                  top: { style: 'thin', color: '#e6e6e6' },
                  bottom: { style: 'thin', color: '#e6e6e6' },
                  left: { style: 'thin', color: '#e6e6e6' },
                  right: { style: 'thin', color: '#e6e6e6' },
                },
              }
            } else {
              cellVConfig = {
                v: '',
                t: 's',
                border: { top: { style: 'none' }, bottom: { style: 'none' }, left: { style: 'none' }, right: { style: 'none' } },
              }
            }
          }
          // 【核心修改】公式列处理
          else if ([11, 14, 17, 20, 23, 24].includes(colIndex)) {
            let formula = ''
            switch (colIndex) {
              case 11: // 单价 (L列)
                formula = `=I${luckysheetRowIndex + 1}*J${luckysheetRowIndex + 1}+K${luckysheetRowIndex + 1}`
                break
              case 14: // 人工包装 (O列)
                formula = `=M${luckysheetRowIndex + 1}*N${luckysheetRowIndex + 1}`
                break
              case 17: // 赠品 (R列)
                formula = `=P${luckysheetRowIndex + 1}*Q${luckysheetRowIndex + 1}`
                break
              case 20: // 干冰 (U列)
                formula = `=S${luckysheetRowIndex + 1}*T${luckysheetRowIndex + 1}`
                break
              case 23: // 快递费 (X列)
                formula = `=V${luckysheetRowIndex + 1}*W${luckysheetRowIndex + 1}`
                break
              case 24: // 合计 (Y列)
                formula = `=H${luckysheetRowIndex + 1}*L${luckysheetRowIndex + 1}+O${luckysheetRowIndex + 1}+R${luckysheetRowIndex + 1}+U${luckysheetRowIndex + 1}+X${luckysheetRowIndex + 1}`
                break
            }

            // 【新增】构建 calcChainItem 并推入数组
            const calcChainItem = {
              r: luckysheetRowIndex,
              c: colIndex,
              index: 0,
              func: [null, null, formula],
              color: 'w',
              parent: null,
              children: {},
              times: 0
            }
            calcChain.push(calcChainItem)

            cellVConfig = {
              v: cellValue, // 初始值为0,计算后会被覆盖
              t: 'n',
              f: formula, // 公式字符串
              fa: { numFmt: '0.00' },
              font: { color: '#333333' },
              border: {
                top: { style: 'thin', color: '#e6e6e6' },
                bottom: { style: 'thin', color: '#e6e6e6' },
                left: { style: 'thin', color: '#e6e6e6' },
                right: { style: 'thin', color: '#e6e6e6' },
              },
            }
          }
          // 合并列处理
          else if (mergeInfo) {
            cellVConfig = {
              v: mergeInfo.isStartCell ? (cellValue || '') : '',
              t: typeof cellValue === 'number' ? 'n' : 's',
              ht: 0,
              vt: 0,
              font: { color: '#c73232' },
              border: { top: { style: 'none' }, bottom: { style: 'none' }, left: { style: 'none' }, right: { style: 'none' } },
            }
          }
          // 普通列处理
          else {
            cellVConfig = {
              v: cellValue || '',
              t: typeof cellValue === 'number' ? 'n' : 's',
              border: {
                top: { style: 'thin', color: '#e6e6e6' },
                bottom: { style: 'thin', color: '#e6e6e6' },
                left: { style: 'thin', color: '#e6e6e6' },
                right: { style: 'thin', color: '#e6e6e6' },
              },
            }
          }

          if (dependencyColumns.includes(colIndex)) {
            cellVConfig.t = 'n' // 强制数字类型
            if (cellValue === null || cellValue === undefined || cellValue === '') {
              cellVConfig.v = 0 // 空值默认0
            }
          }

          if (mergeInfo) {
            if (mergeInfo.isStartCell) {
              cellVConfig.mc = {
                r: mergeInfo.startR,
                c: mergeInfo.startC,
                rs: mergeInfo.r2 - mergeInfo.startR + 1,
                cs: mergeInfo.c2 - mergeInfo.startC + 1,
              }
            } else {
              cellVConfig.mc = {
                r: mergeInfo.startR,
                c: mergeInfo.startC,
              }
            }
          }
          celldata.push({ r: luckysheetRowIndex, c: colIndex, v: cellVConfig })
        })
      })

      this.merges = dynamicMerges
      const cols = [
        { w: 120 }, { w: 100 }, { w: 150 }, { w: 180 }, { w: 120 }, { w: 220 }, { w: 150 },
        { w: 80 }, { w: 80 }, { w: 100 }, { w: 100 }, { w: 80 }, { w: 120 }, { w: 120 },
        { w: 100 }, { w: 120 }, { w: 120 }, { w: 100 }, { w: 120 }, { w: 120 }, { w: 100 },
        { w: 120 }, { w: 120 }, { w: 100 }, { w: 80 }, { w: 100 }, { w: 150 },
      ]
      const rows = Array(defaultData.length + 1).fill({ h: 35 })

      // 【修改】返回一个包含表格数据和calcChain的对象
      return {
        sheetData: [{
          name: sheetName || '模板表',
          celldata,
          merges: dynamicMerges,
          cols,
          rows,
          images: [],
          config: { borderInfo: [] },
        }],
        calcChain: calcChain,
      }
    },
    // 新增:查找单元格的合并信息(用于生成mc属性)
    findMergeInfo(r, c, merges) {
      if (!Array.isArray(merges) || merges.length === 0) return null
      for (const merge of merges) {
        const {r1, c1, r2, c2} = merge
        // 判断当前单元格是否在合并范围内
        if (r >= r1 && r <= r2 && c >= c1 && c <= c2) {
          return {
            isInMerge: true,
            isStartCell: (r === r1 && c === c1), // 是否为主单元格
            startR: r1, // 合并起始行(主单元格行)
            startC: c1, // 合并起始列(主单元格列)
            r2: r2, // 合并结束行
            c2: c2 // 合并结束列
          }
        }
      }
      return null
    },
    // 初始化表格
    initializeTableWithData(data, calcChain) {
      console.log(data, '------')
      // 如果表格已存在,先销毁
      if (window.luckysheet && window.luckysheet.destroy) {
        window.luckysheet.destroy()
      }

      // 构建 Luckysheet 配置对象
      const luckysheetConfig = {
        container: 'luckysheet',
        title: 'TestData表格',
        lang: 'zh',
        data: data,
        calcEnable: true, // 启用计算功能
        enableCalc: true, // 启用
        caseSensitive: false,
        autoRecalc: true, // 确保自动重计算已启用
        loadCalcMode: 0, // 设置为0表示自动计算模式
        showtoolbarConfig: {
          // ... (你的工具栏配置,保持不变)
          undoRedo: true, paintFormat: true, currencyFormat: true, percentageFormat: true,
          numberDecrease: true, numberIncrease: true, moreFormats: true, font: true, fontSize: true,
          bold: true, italic: true, strikethrough: true, underline: true, textColor: true,
          fillColor: true, border: false, mergeCell: true, horizontalAlignMode: true,
          verticalAlignMode: true, textWrapMode: true, textRotateMode: true, image: false,
          link: true, chart: false, postil: true, pivotTable: true, function: true,
          frozenMode: true, sortAndFilter: true, conditionalFormat: true, dataVerification: true,
          splitColumn: true, screenshot: true, findAndReplace: false, protection: true, print: false,
        },
        cellRightClickConfig: {
          // ... (你的右键菜单配置,保持不变)
          copy: true, copyAs: false, paste: false, insertRow: false, insertColumn: false,
          deleteRow: false, deleteColumn: false, deleteCell: false, hideRow: false, hideColumn: false,
          rowHeight: false, columnWidth: false, clear: false, matrix: false, sort: false,
          filter: true, chart: false, image: false, link: false, data: false, cellFormat: false,
        },
        showinfobar: false,
        showsheetbar: true,
        showtoolbar: true,
        allowCopy: true,
        defaultRowHeight: 35,
        defaultColWidth: 150,
        showchart: false,
        chart: { enabled: false },
        gridLines: { show: false },
        enableSelectBorder: false,
      }

      // 如果 calcChain 存在且不为空,则添加到配置中
      if (calcChain && calcChain.length > 0) {
        luckysheetConfig.calcChain = calcChain
        console.log(' Luckysheet calcChain 已加载,共', calcChain.length, '条公式。')
      }

      // 初始化表格
      window.luckysheet.create(luckysheetConfig)

      setTimeout(() => {
        this.checkFormulas()
      }, 1000)
      console.log(luckysheetConfig, '000000')
    },
    // 验证公式是否可以执行
    isValidFormula(formula, c, v) {
      if (!formula || !formula.startsWith('=')) return false
      // 简单验证公式格式
      const formulaContent = formula.substring(1)
      if (!formulaContent.trim()) return false
      // 尝试设置公式并获取值来验证
      try {
        const testCell = { f: formula }
        window.luckysheet.setCellValue(c, v, testCell)
        const value = window.luckysheet.getCellValue(0, 0)
        return value !== null && value !== undefined
      } catch (e) {
        console.error('公式验证错误:', e)
        return false
      }
    },
    checkFormulas() {
      if (!window.luckysheet) return
      // 强制刷新并等待计算完成
      window.luckysheet.refresh()
      setTimeout(() => {
        // 获取当前工作表数据
        const sheetData = window.luckysheet.getSheetData()
        if (!sheetData) return
        // 检查并执行所有公式
        const results = []
        sheetData.forEach((row, rowIndex) => {
          row &&
          row.forEach((cell, colIndex) => {
            if (cell && cell.f) {
              const isValid = this.isValidFormula(cell.f, rowIndex, colIndex)
              results.push({
                row: rowIndex,
                col: colIndex,
                formula: cell.f,
                isValid: isValid,
                value: isValid
                  ? window.luckysheet.getCellValue(rowIndex, colIndex)
                  : null
              })
            }
          })
        })

        console.log('公式验证结果:', results)
        // 再次刷新确保计算完成
        window.luckysheet.refresh()
      }, 500)
    },
    // 保存数据到后端
    async submitToBackend() {
      // 获取所有工作表数据
      // const tableData = window.luckysheet.getAllSheets()
      const sheetIndex = window.luckysheet.currentSheetIndex
      // 获取当前工作表的原始数据(二维数组形式,行->列)
      const originalData = window.luckysheet.getSheetData(sheetIndex)

      // 过滤空行:判断一行中是否所有单元格都为空(null/undefined/空字符串)
      const filteredData = originalData.filter(row => {
        // 如果行不存在,视为空行
        if (!row) return false
        // 检查行中是否有至少一个有效数据
        return row.some(cell => {
          // 单元格有值且不是空字符串,视为有效数据
          return cell !== null && cell !== undefined && cell !== ''
        })
      })

      // console.log('原始数据(包含空行):', originalData)
      // console.log('过滤后数据(移除空行):', filteredData)
      // console.log(tableData, '[][][]')
      // return
      const employee_id = this.$store.getters.employeeId || '513433090121037835'
      const params = {
        data: filteredData,
        employee_id: employee_id
      }
      try {
        const res = await apiUploadWebPage(params)
        if (res.meta.status === 201) {
          this.$message.success('文件保存成功')
          // 先处理ExcelValue值为空,调取最新的一版
          // this.ExcelValue = null
          this.onClear()
          // this.getFinancialQuotationList() // 调用接口方法集成于onClear方法中暂定隐藏
          this.getHistoryExcelList()
        } else {
          this.$message.error(res.meta.msg || '文件保存失败')
        }
      } catch (error) {
        console.error('文件保存失败:', error)
        this.$message.error('文件保存出错,请重试')
      }
    },
    // 导出Excel(含图片)
    async handleExportExcel() {
      const sheetIndex = window.luckysheet.currentSheetIndex
      // 获取当前工作表的原始数据(二维数组形式,行->列)
      const originalData = window.luckysheet.getSheetData(sheetIndex)
      // 过滤空行:判断一行中是否所有单元格都为空(null/undefined/空字符串)
      const filteredData = originalData.filter(row => {
        // 如果行不存在,视为空行
        if (!row) return false
        // 检查行中是否有至少一个有效数据
        return row.some(cell => {
          // 单元格有值且不是空字符串,视为有效数据
          return cell !== null && cell !== undefined && cell !== ''
        })
      })
      // const dataToExport = this.filterColumnsByHeader(filteredData, this.Dataheader) // 前端进行数据筛选,现在导出前端做暂时不需要
      const params = {
        data: filteredData
      }
      // console.log(params)
      try {
        const res = await apiUploadExcel(params)
        // console.log(res)
        if (res.meta.status === 200) {
          this.$message.success('导出任务已建立,请去报价任务列表查看对应导出任务')
        } else {
          // this.$message.error(res.meta.msg || '导出失败')
        }
      } catch (error) {
        console.error('导出失败:', error)
      }
    },
    // 如果导出列数组长度不为空,进行数据筛选再传给接口
    filterColumnsByHeader(filteredData, Dataheader) {
      // filteredData 为空(无有效数据),直接返回
      if (!filteredData || filteredData.length === 0) {
        return []
      }

      // Dataheader 为空(未指定列),保留全部数据
      if (!Dataheader || Dataheader.length === 0) {
        return filteredData
      }

      // 获取表头行(第一行是列名),建立「列名-索引」映射
      const headerRow = filteredData[0]
      const columnMap = new Map()

      headerRow.forEach((cell, index) => {
        let columnName = ''
        // 从单元格对象中提取列名
        if (cell && typeof cell === 'object' && 'v' in cell) {
          columnName = cell.v
        } else if (typeof cell === 'string') {
          columnName = cell
        }

        // 确保列名有效
        if (typeof columnName === 'string' && columnName.trim() !== '') {
          const normalizedHeader = columnName.trim().toLowerCase()
          columnMap.set(normalizedHeader, index)
        }
      })

      // 筛选出 Dataheader 中「存在于表头」的列索引
      const targetColumnIndexes = Dataheader
        .map(name => name.trim().toLowerCase())
        .filter(name => columnMap.has(name))
        .map(name => columnMap.get(name))

      // 如果没有匹配到任何列,返回空数组
      if (targetColumnIndexes.length === 0) {
        // console.log('警告:未在表头中匹配到任何指定的列。')
        return []
      }

      // 遍历所有行,只保留目标列索引对应的「单元格对象」
      const result = filteredData.map(row => {
        if (!row) return row
        // 直接返回单元格对象
        return targetColumnIndexes.map(index => row[index])
      })

      return result
    },
    // 导出Excel(不含图片)
    async handleExportExcelLocal() {
      const exportData = window.luckysheet.getluckysheetfile()
      this.exportExcel(exportData)
    },
    // 导出核心逻辑
    async exportExcel(luckysheet) {
      const workbook = new ExcelJS.Workbook()
      luckysheet.every((table) => {
        if (table.data.length === 0) return true
        const worksheet = workbook.addWorksheet(table.name)
        const headerRow = table.data[0] // 表头行数据
        // -------- 步骤1: 动态查找目标列的原始索引 --------
        // 定义需要合并的目标列名
        const targetColumnNames = ['大类', '类型', '产品图片', '报价依据']
        // 用于存储找到的列索引
        const targetColumnIndexes = []
        if (Array.isArray(headerRow)) {
          headerRow.forEach((cell, index) => {
            const cellValue = cell?.v
            // 如果当前单元格的值是我们要找的目标列名之一
            if (targetColumnNames.includes(cellValue)) {
              targetColumnIndexes.push(index)
              // console.log(`找到"${cellValue}"列,原始索引:`, index)
            }
          })
        }
        // -------------------------------------------------------

        // -------- 步骤2: 根据找到的索引过滤合并规则 --------
        let filteredMerges = []
        if (Array.isArray(table.merges) && targetColumnIndexes.length > 0) {
          filteredMerges = table.merges.filter(merge => {
            const { c1, c2 } = merge
            // 一个有效的纵向合并,c1 应该等于 c2
            // 并且这个列索引应该在我们找到的目标列索引数组中
            return c1 === c2 && targetColumnIndexes.includes(c1)
          })
          // console.log('过滤后的合并规则数量:', filteredMerges.length)
        }
        // -------------------------------------------------------

        // 关键:判断是否使用 Dataheader 列名导出,并处理合并规则映射
        let finalMerges = filteredMerges
        if (this.Dataheader && this.Dataheader.length > 0 && this.currentTableData?.header) {
          // 建立列名映射:Dataheader列名 → 原始表头索引
          this.columnMap = this.Dataheader.map(targetColName => {
            return this.currentTableData.header.findIndex(
              originColName => originColName === targetColName
            )
          }).filter(index => index !== -1)

          // 当使用自定义列导出时,需要重新映射合并规则的列索引
          finalMerges = filteredMerges.map(merge => {
            // 找到原始列索引在新列顺序中的位置
            const newColIndex = this.columnMap.indexOf(merge.c1)
            return {
              ...merge,
              c1: newColIndex,
              c2: newColIndex
            }
          }).filter(merge => merge.c1 !== -1) // 过滤掉不在导出列中的合并规则

          this.setCustomHeaderStyleAndValue(table.data, worksheet)
        } else {
          this.setStyleAndValue(table.data, worksheet)
        }

        // -------- 步骤3: 应用处理后的合并规则 --------
        this.setMerge(finalMerges, worksheet)
        // -------------------------------------

        this.setBorder(table.config.borderInfo, worksheet)
        return true
      })

      const buffer = await workbook.xlsx.writeBuffer()
      FileSaver.saveAs(
        new Blob([buffer], {type: 'application/octet-stream'}),
        `导出的表格_${new Date().toLocaleDateString().replace(/\//g, '-')}.xlsx`
      )
      return buffer
    },
    // 按 Dataheader 列名导出
    setCustomHeaderStyleAndValue(cellArr, worksheet) {
      if (!Array.isArray(cellArr) || !this.Dataheader || this.Dataheader.length === 0 || !this.columnMap) return

      // 第一步:设置表头(按 Dataheader 顺序)
      this.Dataheader.forEach((headerName, colIndex) => {
        if (this.columnMap[colIndex] === undefined) return
        const headerCell = worksheet.getCell(1, colIndex + 1)
        headerCell.value = headerName
        headerCell.font = {name: '微软雅黑', size: 10, color: {argb: '#000000'}, bold: true}
        headerCell.alignment = {vertical: 'middle', horizontal: 'center'}
        headerCell.border = {
          top: {style: 'thin', color: '#e6e6e6'},
          bottom: {style: 'thin', color: '#e6e6e6'},
          left: {style: 'thin', color: '#e6e6e6'},
          right: {style: 'thin', color: '#e6e6e6'}
        }
      })

      // 第二步:设置数据行(按映射关系提取对应列数据)
      cellArr.forEach((row, rowid) => {
        if (rowid === 0) return // 跳过原始表头行
        this.Dataheader.forEach((_, colIndex) => {
          const originColIndex = this.columnMap[colIndex]
          if (originColIndex === undefined || !row[originColIndex]) return

          const cell = row[originColIndex]
          const targetCell = worksheet.getCell(rowid + 1, colIndex + 1)
          const font = this.fontConvert(cell.ff, cell.fc, cell.bl, cell.it, cell.fs, cell.cl, cell.ul)
          const alignment = this.alignmentConvert(cell.vt, cell.ht, cell.tb, cell.tr)

          // 关键修改:只使用计算结果 cell.v,不携带公式
          const value = cell.v

          targetCell.font = font
          targetCell.alignment = alignment
          targetCell.value = value
          targetCell.border = {
            top: {style: 'thin', color: '#e6e6e6'},
            bottom: {style: 'thin', color: '#e6e6e6'},
            left: {style: 'thin', color: '#e6e6e6'},
            right: {style: 'thin', color: '#e6e6e6'}
          }
        })
      })

      // 第三步:同步列宽
      this.Dataheader.forEach((_, colIndex) => {
        const originColIndex = this.columnMap[colIndex]
        if (originColIndex === undefined || !this.currentTableData?.cols) return
        if (this.currentTableData.cols[originColIndex]) {
          worksheet.getColumn(colIndex + 1).width = this.currentTableData.cols[originColIndex].w / 7
        }
      })
    },
    // 设置单元格样式和值(修改后,用于非自定义表头场景)
    setStyleAndValue(cellArr, worksheet) {
      if (!Array.isArray(cellArr)) return
      cellArr.forEach((row, rowid) => {
        row.every((cell, columnid) => {
          if (!cell) return true
          const font = this.fontConvert(
            cell.ff, cell.fc, cell.bl, cell.it, cell.fs, cell.cl, cell.ul
          )
          const alignment = this.alignmentConvert(cell.vt, cell.ht, cell.tb, cell.tr)

          // 关键修改:只使用计算结果 cell.v,不携带公式
          const value = cell.v

          const target = worksheet.getCell(rowid + 1, columnid + 1)
          target.font = font
          target.alignment = alignment
          target.value = value
          return true
        })
      })
    },
    // 设置合并单元格
    setMerge(luckyMerge, worksheet) {
      if (!luckyMerge) return

      // 处理数组形式的合并信息
      if (Array.isArray(luckyMerge)) {
        luckyMerge.forEach(merge => {
          worksheet.mergeCells(
            merge.r1 + 1, // 转换为Excel的行号(从1开始)
            merge.c1 + 1, // 转换为Excel的列号(从1开始)
            merge.r2 + 1,
            merge.c2 + 1
          )
        })
      }
      // 处理对象形式的合并信息
      else if (typeof luckyMerge === 'object') {
        const mergearr = Object.values(luckyMerge)
        mergearr.forEach(elem => {
          worksheet.mergeCells(
            elem.r + 1,
            elem.c + 1,
            elem.r + elem.rs,
            elem.c + elem.cs
          )
        })
      }
    },
    // 设置边框
    setBorder(luckyBorderInfo, worksheet) {
      if (!Array.isArray(luckyBorderInfo)) return
      luckyBorderInfo.forEach(elem => {
        const val = elem.value
        const border = {}
        const luckyToExcel = {
          style: {
            0: 'none', 1: 'thin', 2: 'hair', 3: 'dotted', 4: 'dashDot',
            5: 'dashDot', 6: 'dashDotDot', 7: 'double', 8: 'medium',
            9: 'mediumDashed', 10: 'mediumDashDot', 11: 'mediumDashDotDot',
            12: 'slantDashDot', 13: 'thick'
          }
        }

        if (val) {
          if (val.t) border.top = {style: luckyToExcel.style[val.t.style], color: val.t.color}
          if (val.r) border.right = {style: luckyToExcel.style[val.r.style], color: val.r.color}
          if (val.b) border.bottom = {style: luckyToExcel.style[val.b.style], color: val.b.color}
          if (val.l) border.left = {style: luckyToExcel.style[val.l.style], color: val.l.color}
          worksheet.getCell(val.row_index + 1, val.col_index + 1).border = border
        }
      })
    },
    // 字体转换
    fontConvert(ff = 0, fc = '#000000', bl = 0, it = 0, fs = 10, cl = 0, ul = 0) {
      const luckyToExcel = {
        0: '微软雅黑', 1: '宋体', 2: '黑体', 3: '楷体', 4: '仿宋', 5: '新宋体',
        6: '华文新魏', 7: '华文行楷', 8: '华文隶书', 9: 'Arial', 10: 'Times New Roman',
        11: 'Tahoma', 12: 'Verdana',
        num2bl: num => num !== 0
      }

      return {
        name: luckyToExcel[ff],
        size: fs,
        color: {argb: fc.replace('#', '')},
        bold: luckyToExcel.num2bl(bl),
        italic: luckyToExcel.num2bl(it),
        underline: luckyToExcel.num2bl(ul),
        strike: luckyToExcel.num2bl(cl)
      }
    },
    // 对齐方式转换
    alignmentConvert(vt = 'default', ht = 'default', tb = 'default', tr = 'default') {
      const luckyToExcel = {
        vertical: {0: 'middle', 1: 'top', 2: 'bottom', default: 'top'},
        horizontal: {0: 'center', 1: 'left', 2: 'right', default: 'left'},
        wrapText: {0: false, 1: false, 2: true, default: false},
        textRotation: {0: 0, 1: 45, 2: -45, 3: 'vertical', 4: 90, 5: -90, default: 0}
      }

      return {
        vertical: luckyToExcel.vertical[vt],
        horizontal: luckyToExcel.horizontal[ht],
        wrapText: luckyToExcel.wrapText[tb],
        textRotation: luckyToExcel.textRotation[tr]
      }
    }
  }
}
</script>

<style scoped lang="scss">
::v-deep .custom-tags-select .el-select__tags {
  top: 35%;
}

.report-management-container {
  width: 100%;
  min-height: 100vh;
  box-sizing: border-box;
  padding: 16px;
  background-color: #f5f5f5;
}
.luckysheetBody {
  width: 100%;
  height: 800px;
  position: relative;
}

.imageOper {
  position: absolute;
  top: 0.8vh;
  right: 17.5vw;
  color: #000000;
  width: 80px;
  height: 30px;
  text-align: center;
  line-height: 30px ;
  z-index: 999;
  font-size: 12px;
  cursor: pointer
}

.imageOper:hover {
  background-color: #EBEBED;
}

.luckysheetStyle {
  width: 100%;
  height: 800px;
  border: 1px solid #ccc;
}

.upload-excel {
  margin-bottom: 10px;
}

.export-excel {
  height: 35px;
  margin-bottom: 10px;
}
.Version {
  .el-select__tags {
    white-space: nowrap;
    overflow: hidden;
  }
}

/* 上传弹窗样式 */
::v-deep .staff-upload-dialog {
  .el-dialog__header {
    padding: 16px 24px;
    border-bottom: 1px solid #f5f7fa;

    .el-dialog__title {
      font-size: 16px;
      font-weight: 500;
      color: #303133;
    }
  }

  .el-dialog__footer {
    padding: 16px 24px;
    border-top: 1px solid #f5f7fa;
    margin-top: 10px;
  }
}

.upload-container {
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 10px 24px 0;
}

.upload-card {
  width: 100%;
  padding: 24px;
  border: 2px dashed #e4e7ed;
  border-radius: 8px;
  text-align: center;
  transition: all 0.3s ease;

  &:hover {
    border-color: #409eff;
    background-color: #f9fafc;
  }
}

.upload-btn-group {
  width: 100%;

  ::v-deep .el-upload {
    width: 100%;
  }
}

.upload-btn-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  cursor: pointer;

  .upload-icon {
    font-size: 32px;
    color: #409eff;
  }

  .upload-text {
    font-size: 14px;
    color: #303133;
    font-weight: 500;
  }

  .upload-subtext {
    font-size: 12px;
    color: #909399;
  }
}
</style>

合并的单元格数据处理时一定要在数据中加入mc并且添加到merges中

添加公式时需要添加公式链calcChain初始化时还需要重新计算具体方法checkFormulas

cdn方式引入

  <!-- 1. CDN 引入 jQuery(3.7.1 版本,与项目依赖一致) -->
  <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
  <!-- 2. CDN 引入 jquery-mousewheel(3.1.13 版本,兼容 Luckysheet) -->
  <script src="https://cdn.bootcdn.net/ajax/libs/jquery-mousewheel/3.1.13/jquery.mousewheel.min.js"></script>
  <!-- 3. CDN 引入 Luckysheet 样式(顺序:pluginsCss → plugins → 核心样式) -->
  <!-- 2. 替换为 unpkg 稳定 CDN 的 Luckysheet 样式(顺序不变) -->
<!--  <link rel="stylesheet" href="https://unpkg.com/luckysheet@2.1.13/dist/plugins/css/pluginsCss.css">-->
<!--  <link rel="stylesheet" href="https://unpkg.com/luckysheet@2.1.13/dist/plugins/plugins.css">-->
  <link rel="stylesheet" href="https://unpkg.com/luckysheet@2.1.13/dist/css/luckysheet.css">
  <link rel="stylesheet" href="https://unpkg.com/luckysheet@2.1.13/dist/assets/iconfont/iconfont.css">

  <!-- 3. 替换为 unpkg 稳定 CDN 的 Luckysheet 脚本(顺序不变) -->
<!--  <script src="https://unpkg.com/luckysheet@2.1.13/dist/plugins/js/plugin.js"></script>-->
<!--  <script src="https://unpkg.com/luckysheet@2.1.13/dist/luckysheet.umd.js"></script>-->

注意cdn方式引入时的顺序问题

直接下载依赖包

import axios from 'axios'
import ExcelJS from 'exceljs'
import FileSaver from 'file-saver'
import ConstPrice from './components/ConstPrice.vue'
import 'luckysheet/dist/plugins/css/pluginsCss.css'
import 'luckysheet/dist/plugins/plugins.css'
import luckysheet from 'luckysheet'
备注:样式没有import引入是因为项目会和样式冲突所以直接cdn方式

pnpm安装命令: pnpm add  xlsx-populate@1.21.0 file-saver@2.0.5    pnpm add luckysheet exceljs file-saver

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐