onlineSheetExcelImport.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. import LuckyExcel from 'luckyexcel'
  2. import * as XLSX from 'xlsx'
  3. /**
  4. * 修复luckysheet中的公式解析问题
  5. * 主要解决以下问题:
  6. * 1. Excel公式 =IF(E6=100%,"Y",N) 被luckyexcel解析为 =IF(E6=100%,"Y",_xleta.N) 的问题
  7. * 2. 百分比格式问题:将100%转换为数值1
  8. * 3. 计算链缺失问题:确保所有包含公式的单元格都被正确添加到calcChain中
  9. * 4. 通用公式语法修复
  10. *
  11. * @param {Array} sheetData - luckysheet数据
  12. * @returns {Array} 修复后的数据
  13. */
  14. export function fixLuckySheetFormulas(sheetData) {
  15. try {
  16. if (!sheetData || !Array.isArray(sheetData)) return sheetData
  17. // 遍历所有sheet
  18. return sheetData.map(sheet => {
  19. if (!sheet) return sheet
  20. // 第一步:修复公式语法问题(百分比格式、_xleta前缀等)
  21. sheet = fixFormulaSyntax(sheet)
  22. // 第二步:修复计算链,确保所有公式单元格都能正常计算
  23. sheet = fixCalcChain(sheet)
  24. return sheet
  25. })
  26. } catch (error) {
  27. return sheetData
  28. }
  29. }
  30. /**
  31. * 修复公式语法问题
  32. * 包括:_xleta前缀修复、百分比格式转换等
  33. *
  34. * @param {Object} sheet - 单个工作表数据
  35. * @returns {Object} 修复后的工作表
  36. */
  37. function fixFormulaSyntax(sheet) {
  38. if (!sheet) return sheet
  39. // 修复celldata中的公式
  40. if (Array.isArray(sheet.celldata)) {
  41. sheet.celldata.forEach(cell => {
  42. if (cell && cell.v && cell.v.f) {
  43. const formula = cell.v.f
  44. let newFormula = formula
  45. // 修复 _xleta.N 问题
  46. // 检查公式模式: =IF(E6=100%,"Y",_xleta.N)
  47. // 修复为: =IF(E6=100%,"Y","N")
  48. // 更精确的匹配:只匹配在函数参数位置且不在函数名位置的_xleta.N
  49. const regex = /(IF\([^,]+,[^,]+,)_xleta\.(N)(\))/g
  50. const matches = [...formula.matchAll(regex)]
  51. if (matches.length > 0) {
  52. matches.forEach(match => {
  53. const before = match[1]
  54. const variable = match[2]
  55. const after = match[3]
  56. // 对于字母N、Y等,作为字符串处理
  57. const isSimpleLetter = /^[A-Za-z]$/.test(variable)
  58. if (isSimpleLetter) {
  59. // 对于简单字母变量,替换为字符串
  60. const replacement = `${before}"${variable}"${after}`
  61. newFormula = newFormula.replace(match[0], replacement)
  62. }
  63. })
  64. }
  65. // 更通用的修复:处理其他_xleta前缀的变量
  66. // 例如: _xleta.Y, _xleta.VAR 等
  67. const genericRegex = /_xleta\.([A-Za-z]+)/g
  68. const genericMatches = [...newFormula.matchAll(genericRegex)]
  69. if (genericMatches.length > 0) {
  70. genericMatches.forEach(match => {
  71. const fullMatch = match[0]
  72. const variableName = match[1]
  73. // 判断是否为简单变量名(单个字母)
  74. if (/^[A-Za-z]$/.test(variableName)) {
  75. newFormula = newFormula.replace(fullMatch, `"${variableName}"`)
  76. }
  77. })
  78. }
  79. // 智能修复百分比格式问题
  80. // 对于公式 =IF(E6=100%,"Y","N"),需要将100%转换为luckysheet能理解的格式
  81. // 同时需要处理小数百分比,如 =IF(E15<=0.01%,"Y","N"),应该转换为 =IF(E15<=0.0001,"Y","N")
  82. // 第一步:先修复可能存在的重复小数点问题(如0.0.01应该修正为0.0001)
  83. // 这种问题可能发生在luckyexcel解析过程中
  84. // 问题:0.01% 被错误解析为 0.0.01,实际上应该是 0.0001
  85. const duplicateDotRegex = /(\d+)\.(\d+)\.(\d+)/g
  86. const duplicateDotMatches = [...newFormula.matchAll(duplicateDotRegex)]
  87. if (duplicateDotMatches.length > 0) {
  88. duplicateDotMatches.forEach(match => {
  89. const fullMatch = match[0]
  90. const integerPart = match[1] // 如 "0"
  91. const firstDecimalPart = match[2] // 如 "0"
  92. const secondDecimalPart = match[3] // 如 "01"
  93. // 处理常见的百分比解析错误模式
  94. // 1. 0.01% 被解析为 0.0.01,应该修正为 0.0001
  95. // 2. 0.1% 被解析为 0.0.1,应该修正为 0.001
  96. // 3. 0.001% 被解析为 0.0.001,应该修正为 0.00001
  97. // 经过分析发现规律:0.0.xxx 模式通常表示 xxx/100 的百分比
  98. // 例如:0.0.01 表示 0.01% = 0.0001,0.0.1 表示 0.1% = 0.001
  99. let correctNumber
  100. if (integerPart === '0' && firstDecimalPart === '0') {
  101. // 针对 0.0.xxx 模式的特殊修复
  102. // 规则:转换为 0.00xxx,其中 xxx 是 secondDecimalPart
  103. // 例如:0.0.01 → 0.0001,0.0.1 → 0.001,0.0.001 → 0.00001
  104. correctNumber = parseFloat(`0.00${secondDecimalPart}`)
  105. } else {
  106. // 其他情况:使用更保守的修复
  107. // 将 X.Y.Z 合并为 X.YZ(假设第一个小数部分是整数部分的小数)
  108. correctNumber = parseFloat(`${integerPart}.${firstDecimalPart}${secondDecimalPart}`)
  109. }
  110. newFormula = newFormula.replace(fullMatch, correctNumber.toString())
  111. })
  112. }
  113. // 第二步:处理百分比转换为数值的问题
  114. // 支持整数和小数百分比,如 100%、0.01%、1.5%
  115. const percentRegex = /(\d+(?:\.\d+)?)%/g
  116. const percentMatches = [...newFormula.matchAll(percentRegex)]
  117. if (percentMatches.length > 0) {
  118. percentMatches.forEach(match => {
  119. const fullMatch = match[0]
  120. const number = match[1]
  121. const percentValue = parseFloat(number) / 100
  122. // 将百分比转换为数值,因为Excel中100%就是数值1,0.01%就是0.0001
  123. newFormula = newFormula.replace(fullMatch, percentValue.toString())
  124. })
  125. }
  126. if (newFormula !== formula) {
  127. cell.v.f = newFormula
  128. }
  129. }
  130. })
  131. }
  132. // 修复data数组中的公式
  133. if (Array.isArray(sheet.data)) {
  134. sheet.data.forEach((row, rowIndex) => {
  135. if (Array.isArray(row)) {
  136. row.forEach((cell, colIndex) => {
  137. if (cell && cell.f) {
  138. const formula = cell.f
  139. let newFormula = formula
  140. // 修复 _xleta.N 问题(与上面相同的逻辑)
  141. const regex = /(IF\([^,]+,[^,]+,)_xleta\.(N)(\))/g
  142. const matches = [...formula.matchAll(regex)]
  143. if (matches.length > 0) {
  144. matches.forEach(match => {
  145. const before = match[1]
  146. const variable = match[2]
  147. const after = match[3]
  148. const isSimpleLetter = /^[A-Za-z]$/.test(variable)
  149. if (isSimpleLetter) {
  150. const replacement = `${before}"${variable}"${after}`
  151. newFormula = newFormula.replace(match[0], replacement)
  152. }
  153. })
  154. }
  155. // 通用修复
  156. const genericRegex = /_xleta\.([A-Za-z]+)/g
  157. const genericMatches = [...newFormula.matchAll(genericRegex)]
  158. if (genericMatches.length > 0) {
  159. genericMatches.forEach(match => {
  160. const fullMatch = match[0]
  161. const variableName = match[1]
  162. if (/^[A-Za-z]$/.test(variableName)) {
  163. newFormula = newFormula.replace(fullMatch, `"${variableName}"`)
  164. }
  165. })
  166. }
  167. // 智能修复百分比格式问题
  168. // 处理整数和小数百分比,如 100% 转换为 1,0.01% 转换为 0.0001
  169. // 第一步:先修复可能存在的重复小数点问题(如0.0.01应该修正为0.0001)
  170. const duplicateDotRegex = /(\d+)\.(\d+)\.(\d+)/g
  171. const duplicateDotMatches = [...newFormula.matchAll(duplicateDotRegex)]
  172. if (duplicateDotMatches.length > 0) {
  173. duplicateDotMatches.forEach(match => {
  174. const fullMatch = match[0]
  175. const integerPart = match[1] // 如 "0"
  176. const firstDecimalPart = match[2] // 如 "0"
  177. const secondDecimalPart = match[3] // 如 "01"
  178. // 处理常见的百分比解析错误模式
  179. let correctNumber
  180. if (integerPart === '0' && firstDecimalPart === '0') {
  181. // 针对 0.0.xxx 模式的特殊修复
  182. // 规则:转换为 0.00xxx,其中 xxx 是 secondDecimalPart
  183. correctNumber = parseFloat(`0.00${secondDecimalPart}`)
  184. } else {
  185. // 其他情况:使用更保守的修复
  186. correctNumber = parseFloat(`${integerPart}.${firstDecimalPart}${secondDecimalPart}`)
  187. }
  188. newFormula = newFormula.replace(fullMatch, correctNumber.toString())
  189. })
  190. }
  191. // 第二步:处理百分比转换为数值的问题
  192. const percentRegex = /(\d+(?:\.\d+)?)%/g
  193. const percentMatches = [...newFormula.matchAll(percentRegex)]
  194. if (percentMatches.length > 0) {
  195. percentMatches.forEach(match => {
  196. const fullMatch = match[0]
  197. const number = match[1]
  198. const percentValue = parseFloat(number) / 100
  199. // 将百分比转换为数值
  200. newFormula = newFormula.replace(fullMatch, percentValue.toString())
  201. })
  202. }
  203. if (newFormula !== formula) {
  204. cell.f = newFormula
  205. }
  206. }
  207. })
  208. }
  209. })
  210. }
  211. return sheet
  212. }
  213. /**
  214. * 修复计算链(calcChain)
  215. * 问题描述:某些包含公式的单元格没有被正确添加到calcChain中,导致公式不计算
  216. * 例如:G11和H11公式工作,但I11及以后的公式不工作
  217. *
  218. * 解决方案:
  219. * 1. 遍历所有包含公式的单元格(包括celldata和data数组)
  220. * 2. 检查这些单元格是否已经在calcChain中
  221. * 3. 如果不在,则添加到calcChain中
  222. * 4. 确保计算链的正确性,使所有公式都能正常计算
  223. *
  224. * @param {Object} sheet - 单个工作表数据
  225. * @returns {Object} 修复后的工作表
  226. */
  227. function fixCalcChain(sheet) {
  228. if (!sheet) return sheet
  229. // 确保calcChain存在
  230. if (!sheet.calcChain) {
  231. sheet.calcChain = []
  232. }
  233. // 创建现有计算链的快速查找映射
  234. // 使用 "行_列" 作为键,例如 "10_6" 表示第11行G列
  235. const existingCalcMap = new Map()
  236. sheet.calcChain.forEach(item => {
  237. if (item && typeof item.r === 'number' && typeof item.c === 'number') {
  238. const key = `${item.r}_${item.c}`
  239. existingCalcMap.set(key, true)
  240. }
  241. })
  242. /**
  243. * 辅助函数:添加单元格到计算链
  244. * @param {number} row - 行索引(0-based)
  245. * @param {number} col - 列索引(0-based)
  246. */
  247. const addToCalcChain = (row, col) => {
  248. const key = `${row}_${col}`
  249. // 如果已经存在,跳过
  250. if (existingCalcMap.has(key)) {
  251. return
  252. }
  253. // 添加到计算链
  254. sheet.calcChain.push({
  255. r: row,
  256. c: col,
  257. index: sheet.index || "0" // 使用工作表索引,默认为"0"
  258. })
  259. // 更新映射
  260. existingCalcMap.set(key, true)
  261. }
  262. // 第一步:遍历celldata,查找有公式的单元格
  263. if (Array.isArray(sheet.celldata)) {
  264. sheet.celldata.forEach(cell => {
  265. if (cell && typeof cell.r === 'number' && typeof cell.c === 'number') {
  266. // 检查是否有公式
  267. const hasFormula = cell.v && cell.v.f
  268. if (hasFormula) {
  269. addToCalcChain(cell.r, cell.c)
  270. }
  271. }
  272. })
  273. }
  274. // 第二步:遍历data数组,查找有公式的单元格
  275. if (Array.isArray(sheet.data)) {
  276. sheet.data.forEach((row, rowIndex) => {
  277. if (Array.isArray(row)) {
  278. row.forEach((cell, colIndex) => {
  279. if (cell && cell.f) {
  280. addToCalcChain(rowIndex, colIndex)
  281. }
  282. })
  283. }
  284. })
  285. }
  286. // 第三步:处理特殊情况 - 检查是否有合并单元格影响公式计算
  287. // 如果单元格是合并单���格的一部分,可能需要特殊处理
  288. if (sheet.config && sheet.config.merge) {
  289. // 可以在这里添加合并单元格的特殊处理逻辑
  290. // 例如:确保合并单元格的主单元格在计算链中
  291. }
  292. return sheet
  293. }
  294. /**
  295. * 处理.xls文件导入
  296. * @param {ArrayBuffer} arrayBuffer - .xls文件的ArrayBuffer
  297. * @returns {Promise} 返回包含luckysheet数据的Promise
  298. */
  299. export function handleXlsFile(arrayBuffer) {
  300. return new Promise((resolve, reject) => {
  301. try {
  302. // 使用 xlsx 读取 .xls,保留单元格样式
  303. const workbook = XLSX.read(arrayBuffer, {
  304. type: 'array',
  305. cellStyles: true // 保留样式信息
  306. })
  307. // 转换为 .xlsx 格式的 ArrayBuffer,保留样式
  308. const xlsxArrayBuffer = XLSX.write(workbook, {
  309. bookType: 'xlsx',
  310. type: 'array',
  311. cellStyles: true, // 保留样式
  312. bookSST: false
  313. })
  314. // 用 LuckyExcel 解析转换后的数据
  315. handleXlsxFile(xlsxArrayBuffer)
  316. .then(resolve)
  317. .catch(reject)
  318. } catch (error) {
  319. reject(new Error('导入失败:.xls 文件格式不支持或已损坏!'))
  320. }
  321. })
  322. }
  323. /**
  324. * 处理.xlsx文件导入
  325. * @param {ArrayBuffer} arrayBuffer - .xlsx文件的ArrayBuffer
  326. * @returns {Promise} 返回包含luckysheet数据的Promise
  327. */
  328. export function handleXlsxFile(arrayBuffer) {
  329. return new Promise((resolve, reject) => {
  330. LuckyExcel.transformExcelToLucky(arrayBuffer, (exportJson, luckysheetfile) => {
  331. if (exportJson.sheets && exportJson.sheets.length > 0) {
  332. // 修复导入后的公式问题
  333. const fixedSheets = fixLuckySheetFormulas(exportJson.sheets)
  334. resolve(fixedSheets)
  335. } else {
  336. reject(new Error('导入失败:Excel 文件为空或格式错误!'))
  337. }
  338. })
  339. })
  340. }
  341. /**
  342. * 导入Excel文件的主函数
  343. * @param {File} file - Excel文件
  344. * @returns {Promise} 返回包含luckysheet数据的Promise
  345. */
  346. export async function importExcelFile(file) {
  347. return new Promise((resolve, reject) => {
  348. const fileName = file.name
  349. const fileExt = fileName.substring(fileName.lastIndexOf('.')).toLowerCase()
  350. if (fileExt !== '.xlsx' && fileExt !== '.xls') {
  351. reject(new Error('仅支持导入 .xlsx 和 .xls 格式的 Excel 文件!'))
  352. return
  353. }
  354. const reader = new FileReader()
  355. reader.onload = (evt) => {
  356. try {
  357. if (fileExt === '.xls') {
  358. // .xls 文件先用 xlsx 转换为 .xlsx
  359. handleXlsFile(evt.target.result)
  360. .then(resolve)
  361. .catch(reject)
  362. } else {
  363. // .xlsx 文件直接用 LuckyExcel 解析
  364. handleXlsxFile(evt.target.result)
  365. .then(resolve)
  366. .catch(reject)
  367. }
  368. } catch (error) {
  369. reject(new Error(`导入失败:${error.message || '文件解析错误,请检查文件格式是否正确'}`))
  370. }
  371. }
  372. reader.onerror = () => {
  373. reject(new Error('导入失败:文件读取错误!'))
  374. }
  375. reader.readAsArrayBuffer(file)
  376. })
  377. }
  378. /**
  379. * 获取导入文件的格式提示
  380. * @param {string} fileExt - 文件扩展名
  381. * @returns {string} 提示信息
  382. */
  383. export function getFormatTip(fileExt) {
  384. if (fileExt === '.xls') {
  385. return '.xls 格式导入时可能无法保留单元格样式(如颜色、边框等)。建议将文件另存为 .xlsx 格式后再导入,以完整保留样式。是否继续导入?'
  386. }
  387. return ''
  388. }
  389. export default {
  390. fixLuckySheetFormulas,
  391. handleXlsFile,
  392. handleXlsxFile,
  393. importExcelFile,
  394. getFormatTip
  395. }