UNPKG

xlsx-script

Version:

一个通过指令控制、可编程的xlsx模板类库。

407 lines (368 loc) 11.5 kB
import ExcelJS from 'exceljs' import * as utils from './utils.mjs' import scripts from './scripts.mjs' import parseScript from './parser.mjs' /** * 上下文对象 * @typedef {object} Context * @property {string} output 输出 * @property {object} ws 工作表 * @property {object} ref_data 引用数据 * @property {object[]} ref_data.data 上下文数据 * @property {object[]} ref_data.wsData 工作表数据 * @property {object} exp 表达式 * @property {Number[]} rc 渲染位置指针 * @property {Function[]} onRendered 渲染完成后执行 * @property {boolean} break 终止后续函数计算 */ class xlsx_script { /** * ExcelJS workbook 对象 */ workbook //单元格事件列表{cell,evName,call} events = [] //函数列表 scripts = scripts //日志开关 logOutput = 99 //====渲染相关====// /** * 渲染模板 * @param {Object[]} data - 数据源 */ render(data) { const groupexp = /{#([^}]+)}/ //eachSheet 不会遍历到新增的sheet this.workbook.eachSheet((ws) => { const match = ws.name.match(groupexp); //普通sheet页直接渲染 if (match === null) { this.renderSheet(ws, data) return } let keyName = ws.name.match(groupexp)[1] this.log(2, 'eachSheet', keyName) //动态sheet只给支持 {#xxx} 一种语法 const sheetData = utils.groupBy(data, keyName) const count = Object.keys(sheetData).length if (count == 0) { this.workbook.addWorksheet('无数据').orderNo = ws.orderNo + 1 this.workbook.removeWorksheet(ws.id) return } //复制n-1个模板 const sheetList = [ws] const entriesData = Object.entries(sheetData) ws.name = entriesData[0][0] //原来的模板直接作为第一个待渲染的模板 for (let i = 1; i < count; i++) { sheetList.push(this.copySheet(ws, entriesData[i][0])) } //渲染生成的模板 for (let i = 0; i < count; i++) { this.renderSheet(sheetList[i], entriesData[i][1]) } }) } /** * 渲染worksheet * @param {Object} ws - 工作簿 * @param {Object[]} data - 数据源 */ renderSheet(ws, data) { //预处理,拆分合并的单元格,避免操作过程中导致合并状态混乱 this.#preHandleMerge(ws) let ref_data = { data, wsData: data } //创建上下文对象 let newContext = (cell, rc) => ({ output: '', ws, ref_data, exp: null, cell, rc, onRendered: [], break: false }) // 检查是否包含指令 let checkExp = (cell) => { return cell != null && cell.text && cell.text.includes('{') && cell.text.includes('}') } //渲染单元格 let eachTarget = this.#eachSheetCell(ws, { skipEmpty: false, skipNoCmd: false }) for (const { cell, rc } of eachTarget) { const events = this.#filterEvents('beforeRender', [rc[0] + 1, rc[1] + 1]) const context = newContext(cell, rc); if (events.length > 0) { for (const { callBack } of events) { callBack && callBack(context) } } if (!checkExp(cell)) continue; let parsedCell = this.parseCell(cell) this.log(1, 'pos', rc) this.renderCell(context, parsedCell, rc) } //后处理 eachTarget = this.#eachSheetCell(ws, { skipEmpty: true, skipNoCmd: true }) for (const { cell } of eachTarget) { if (!checkExp(cell)) continue; const context = newContext(cell); let parsedCell = this.parseCell(cell) this.renderCell(context, parsedCell, true) } } /** * 渲染单元格 * @param {Context} context * @param {Object} parsedCell * @param {string[]} parsedCell.output * @param {string} parsedCell.raw * @param {Object[]} parsedCell.exps * @param {Object} parsedCell.cell * @param {boolean} postProcessMode */ renderCell(context, parsedCell, postProcessMode = false) { let outputs = [] for (const exp of parsedCell.exps) { if (context.break || exp.type === null || (exp.type == '@') ^ postProcessMode) { outputs.push(exp.raw) continue } context.exp = exp this.exec(context) outputs.push(context.output) } let combineStr = [] for (let i = 0; i < parsedCell.output.length; i++) { combineStr.push(parsedCell.output[i], outputs[i] || '') } let value = combineStr.join(''); if (value === "" || Number.isNaN(Number(value))) { parsedCell.cell.value = value } else { parsedCell.cell.value = Number(value) } for (const caller of context.onRendered) { caller && caller() } } #filterEvents(eventName, cell) { return this.events.filter((ev) => ev.eventName == eventName && ev.cell[0] == cell[0] && ev.cell[1] == cell[1]) } /** * 执行指令 * @param {Context} context */ exec(context) { context.output = '' let { exp } = context for (const func of exp.funcs) { if (this.scripts[func.name] === undefined) { console.error(func.name + ' 未定义') continue } this.log(2, 'call ' + exp.raw) this.scripts[func.name].apply(this, [context, ...func.args]) } } //将单元格转换为对象 parseCell(cell) { let raw = cell.text let result = { output: [], raw, exps: [], cell } if (raw === null || raw.length == 0) return result let { output, exps } = parseScript(raw) result.output = output; result.exps = exps; return result } //打散合并的单元格,并生成{@.merge(extWidth,extHeigh)}语句 #preHandleMerge(sheet) { for (const model of Object.values(sheet._merges)) { let extWidth = model.right - model.left let extHeight = model.bottom - model.top sheet.unMergeCells(model.tl) sheet.getCell(model.tl).value = sheet.getCell(model.tl).text + '{@.merge(' + extWidth + ',' + extHeight + ')}' } } //====Excel操作相关====// /** * 复制sheet 页 * @param {Object} templateSheet - 待复制的sheet对象 * @param {String} name - 复制后的名称 * @param {Number=} order - 排序位置 */ copySheet(templateSheet, name, order) { let newSheet = this.workbook.addWorksheet('Sheet') if (order === undefined) { order = newSheet.orderNo + 1 } newSheet.orderNo = order newSheet.model = { ...templateSheet.model, mergeCells: templateSheet.model.merges } //修复边框丢失的问题 for (let i = 0; i < newSheet.model.rows.length; i++) { const row = newSheet.model.rows[i] const row_t = templateSheet.model.rows[i] for (let j = 0; j < row.cells.length; j++) { const cell = row.cells[j] const cell_t = row_t.cells[j] if (Object.keys(cell_t.style).length > 0 && Object.keys(cell.style).length == 0) { newSheet.getCell(cell.address).border = { ...cell_t.style.border } } } } if (name) { newSheet.name = name } return newSheet } /** * 复制一行 * @param {Object} sheet - 操作的sheet页 * @param {Number} templateStart - 复制对象的起始行 * @param {Number} templateEnd - 复制对象的结束行 * @param {Number} insetRowNum - 复制后插入的行号 * @returns void */ copyRows(sheet, templateStart, templateEnd, insetRowNum) { if (insetRowNum <= templateEnd) { throw RangeError('insetRowNum <= templateEnd') } let rowCount = templateEnd - templateStart + 1 let newRows = [] for (let i = 0; i < rowCount; i++) { let t_row = sheet.getRow(templateStart + i) let nr = sheet.insertRow( insetRowNum + i, t_row._cells.map((c) => c.value) ) //复制样式 t_row.eachCell({ includeEmpty: true }, (cell, colNumber) => { nr.getCell(colNumber).style = Object.freeze({ ...cell.style }) }) nr.height = t_row.heigth newRows.push(nr) } //事件坐标处理 for (const ev of this.events) { if (ev.cell[0] >= insetRowNum) { ev.cell[0] += rowCount; } } } //迭代所有单元格,返回的 rc 控制当前指针{skipEmpty:true,skipNoCmd:true} *#eachSheetCell(sheet, { skipEmpty, skipNoCmd }) { let rc = [0, 0] //渲染指针,表示第几行第几列 for (; rc[0] < sheet._rows.length; rc[0]++) { const row = sheet._rows[rc[0]] if (row == null) continue //下标获取需要+1 for (rc[1] = 0; rc[1] < row._cells.length; rc[1]++) { const cell = row._cells[rc[1]] if ((skipEmpty || skipNoCmd) && (cell == null || !cell.text)) { continue; } if (skipNoCmd && !(cell.text.includes('{') && cell.text.includes('}'))) { continue; } yield { cell, rc } } } } //====载入数据和导出文件====// /** * 通过blob/buffer读取文件 * @param {ArrayBuffer|blob} buffer - 载入blob/buffer */ async loadBuffer(buffer) { this.workbook = await new ExcelJS.Workbook().xlsx.load(buffer) return this } /** * 通过blob/buffer读取文件 * @param {ArrayBuffer|blob} buffer - 载入blob/buffer */ static async loadBuffer(buffer) { return await new xlsx_script().load(buffer) } /** * 通过url读取文件 * @param {string} url - 载入url地址 */ async loadUrl(url) { let rsp = await fetch(url) let buffer = await rsp.blob() this.workbook = await new ExcelJS.Workbook().xlsx.load(buffer) return this } /** * 通过url读取文件 * @param {string} url - 载入url地址 */ static async loadUrl(url) { return await new xlsx_script().loadUrl(url) } /** * Node端读取文件 * @param {*} fileName */ async loadFile(fileName) { this.workbook = new ExcelJS.Workbook() await this.workbook.xlsx.readFile(fileName) return this } /** * Node端读取文件 * @param {*} fileName */ static async loadFile(fileName) { return await new xlsx_script().loadFile(fileName) } /** * 浏览器导出文件 * @param {String} name - 导出的文件名(不含后缀) */ async export(name) { const data = await this.workbook.xlsx.writeBuffer() const blob = new Blob([data], { type: 'application/octet-stream' }) this.#saveAs(blob, name + '.xlsx') } /** * Node端保存文件 * @param {string} 文件名 */ async save(filename) { await this.workbook.xlsx.writeFile(filename) } #saveAs(blob, fileName) { let url = URL.createObjectURL(blob) var aLink = document.createElement('a') aLink.href = url aLink.download = fileName || '' var event = new MouseEvent('click') aLink.dispatchEvent(event) } //====其他====// /** * 日志输出 * @param {Number} level 日志等级 * @param {...string} message 日志内容 */ log(level, ...message) { this.logOutput <= level && console.log(...message) } } export { xlsx_script }