关于luckysheet的前端数据处理,公式不响应,合并单元格问题
DataheaderTitle: ['日期', '大类', '类型', '产品图片', '报价依据', '商品编码', '产品明细', '数量', '成本价', '成本优化比例', '特殊加价', '单价', '人工包装报价', '人工包装优化比例', '人工包装', '赠品报价', '赠品优化比例', '赠品', '干冰报价', '干冰优化比例', '干冰', '快递报价', '快递优化比例',
·
官网地址:工作表配置 | 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
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)