exportExcel.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. class ExportExcel {
  2. workBook = null;
  3. sheet = null;
  4. sheetIndex = 0;
  5. border = new GC.Spread.Sheets.LineBorder('#000', GC.Spread.Sheets.LineStyle.thin);
  6. excelIo = new GC.Spread.Excel.IO();
  7. $info = $('#excel-info');
  8. // 表格当前画到的行
  9. curRow = 0;
  10. curMaxCol = 0;
  11. billTree = null;
  12. libID = '';
  13. // 分部章节单元格数据
  14. sections = [];
  15. // 清单ID - 清单精灵映射
  16. billIDElfMap = {};
  17. // 叶子清单ID
  18. leafIDs = [];
  19. // 叶子清单总数量
  20. total = 0;
  21. // 清单ID - 最大列映射
  22. maxColMap = {};
  23. // 叶子节点的父项ID
  24. leafParentIDs = [];
  25. constructor(workBook, billTree, libID) {
  26. this.workBook = workBook;
  27. this.sheet = this.workBook.getSheet(0);
  28. this.billTree = billTree;
  29. this.libID = libID;
  30. this.leafIDs = billTree.items.filter(node => !node.children.length).map(node => node.data.ID);
  31. this.total = this.leafIDs.length;
  32. }
  33. // 导出
  34. async export() {
  35. if (!this.workBook) {
  36. return;
  37. }
  38. const json = this.workBook.toJSON();
  39. this.excelIo.save(json, function (blob) {
  40. saveAs(blob, '清单精灵排版.xlsx');
  41. }, function (e) {
  42. // process error
  43. alert(e);
  44. console.log(e);
  45. });
  46. }
  47. // 将数据画在工作簿上
  48. async paintOnWorkBook() {
  49. this.updateProcessInfo();
  50. // 根据专业区分不同sheet
  51. for (const root of this.billTree.roots) {
  52. this.initSheet(this.sheetIndex++, root.data.name);
  53. const nodes = [root, ...root.getPosterity()];
  54. await this.paintOnSheet(nodes);
  55. }
  56. this.afterPaint();
  57. }
  58. /* ========================================================以下为私有方法============================================== */
  59. // 当前到多少条叶子清单
  60. get curProcessCount () {
  61. return this.total - this.leafIDs.length;
  62. }
  63. // 将数据画在表格上
  64. async paintOnSheet(nodes) {
  65. this.updateProcessInfo();
  66. let i = 0;
  67. this.sheet.suspendPaint();
  68. this.sheet.suspendEvent();
  69. for (const billNode of nodes) {
  70. if (this.curRow >= this.sheet.getRowCount()) {
  71. this.sheet.addRows(this.curRow, 2);
  72. }
  73. this.curMaxCol = 0;
  74. if (billNode.children.length) {
  75. // 清单分部章节,合并的列数依赖主体表格,所以先存数据画空行,后续再画具体内容
  76. this.sections.push({ ID: billNode.data.ID, isNotLeaf: true, row: this.curRow, col: 0, text: `${billNode.data.code} ${billNode.data.name}`, rowCount: 1, colCount: 1 });
  77. this.curRow++;
  78. } else {
  79. if (!this.leafParentIDs.includes(billNode.data.ParentID)) {
  80. this.leafParentIDs.push(billNode.data.ParentID);
  81. }
  82. if (this.maxColMap[billNode.data.ParentID] === undefined) {
  83. this.maxColMap[billNode.data.ParentID] = 0;
  84. }
  85. // 叶子清单,合并的列数依赖主体表格,所以先存数据画空行,后续再画具体内容
  86. const billText = `编码:${billNode.data.code} 名称:${billNode.data.name} 单位:${billNode.data.unit}`;
  87. this.sections.push({ ID: billNode.data.ID, row: this.curRow, col: 0, text: billText, rowCount: 1, colCount: 1 });
  88. this.curRow++;
  89. // 画清单精灵表格
  90. const elfTree = await this.getElfTree(billNode.data.ID);
  91. if (!elfTree) {
  92. continue;
  93. }
  94. this.drawBlock(elfTree);
  95. if (this.maxColMap[billNode.data.ParentID] < this.curMaxCol) {
  96. this.maxColMap[billNode.data.ParentID] = this.curMaxCol;
  97. }
  98. i++;
  99. }
  100. /* if (i === 10) {
  101. break;
  102. } */
  103. }
  104. // 画章节
  105. this.drawSection(this.sections);
  106. const range = this.sheet.getRange(0, 0, this.sheet.getRowCount(), this.sheet.getColumnCount());
  107. range.vAlign(GC.Spread.Sheets.VerticalAlign.center);
  108. range.wordWrap(true);
  109. this.sheet.resumeEvent();
  110. this.sheet.resumePaint();
  111. }
  112. // 画完表格后的处理
  113. afterPaint() {
  114. $('#excel-dialog').width('800px');
  115. $('#excel-spread').show();
  116. this.workBook.refresh();
  117. $('#export-excel-confirm').show();
  118. }
  119. // 超出范围追加行列
  120. checkRange(blockRange) {
  121. // 不够行数,追加行数
  122. const needRows = this.curRow + blockRange.rowCount;
  123. const curRowCount = this.sheet.getRowCount();
  124. if (curRowCount < needRows) {
  125. this.sheet.addRows(this.curRow, needRows - curRowCount + 5);
  126. }
  127. // 不够列数,追加列数
  128. const curColCount = this.sheet.getColumnCount();
  129. if (curColCount < blockRange.colCount) {
  130. this.sheet.addColumns(curColCount - 1, blockRange.colCount - curColCount);
  131. }
  132. }
  133. // 画单元格、合并单元格
  134. drawCell(block, addCurRow) {
  135. block.forEach(item => {
  136. const row = addCurRow ? item.row + this.curRow : item.row;
  137. this.sheet.addSpan(row, item.col, item.rowCount, item.colCount);
  138. this.sheet.setFormatter(row, item.col, '@');
  139. if (item.isTitle) {
  140. // 标题水平居中,字体加粗
  141. const range = this.sheet.getRange(row, item.col, 1, 1);
  142. range.hAlign(GC.Spread.Sheets.VerticalAlign.center);
  143. this.sheet.getCell(row, item.col).font('bold normal 15px normal');
  144. } else if (item.isNotLeaf) {
  145. const range = this.sheet.getRange(row, item.col, 1, 1);
  146. range.hAlign(GC.Spread.Sheets.VerticalAlign.center);
  147. }
  148. this.sheet.setText(row, item.col, item.text);
  149. });
  150. }
  151. // 画边框
  152. drawBorder(range) {
  153. range.setBorder(this.border, { all: true })
  154. }
  155. // 更新进度信息
  156. updateProcessInfo() {
  157. this.$info.text(`导出排版中: ${this.curProcessCount} / ${this.total}`)
  158. }
  159. // 完善章节清单ID - 最大列映射
  160. setSectionMaxCol() {
  161. this.leafParentIDs.forEach(ID => {
  162. const node = this.billTree.findNode(ID);
  163. if (!node) {
  164. return;
  165. }
  166. node.children.forEach(child => {
  167. this.maxColMap[child.data.ID] = this.maxColMap[ID];
  168. });
  169. let parent = node.parent;
  170. while (parent && this.maxColMap[parent.data.ID] === undefined) {
  171. this.maxColMap[parent.data.ID] = this.maxColMap[ID];
  172. parent = parent.parent;
  173. }
  174. });
  175. }
  176. // 画章节
  177. drawSection(sections) {
  178. this.setSectionMaxCol();
  179. sections.forEach(section => {
  180. const maxCol = this.maxColMap[section.ID] || 0;
  181. section.colCount = maxCol + 1;
  182. });
  183. this.drawCell(sections, false);
  184. }
  185. // 画主体表格
  186. async drawBlock(elfTree) {
  187. const block = this.getBlock(elfTree);
  188. if (!block.length) {
  189. return;
  190. }
  191. const blockRange = this.getBlockRange(elfTree);
  192. this.checkRange(blockRange);
  193. const range = this.sheet.getRange(block[0].row + this.curRow, block[0].col, blockRange.rowCount, blockRange.colCount)
  194. // 画单元格
  195. this.drawCell(block, true);
  196. // 画边框
  197. this.drawBorder(range);
  198. this.curRow += blockRange.rowCount;
  199. }
  200. // 精灵树数据转换为表格块单元格数据
  201. getBlock(elfTree) {
  202. const block = [];
  203. // 表格正文
  204. const blockContent = this.getBlockContent(elfTree);
  205. // 表格标题:获取完正文,才知道标题的合并列数,因此先获取表格正文,再获取表格标题
  206. const blockTitle = this.getBlockTitle();
  207. // 表格正文的行号,整体下移
  208. blockContent.forEach(cellInfo => {
  209. cellInfo.row += 1;
  210. });
  211. block.push(...blockTitle);
  212. block.push(...blockContent);
  213. return block;
  214. }
  215. // 获取表格块正文单元格数据
  216. getBlockContent(elfTree) {
  217. const block = elfTree.items.map(node => {
  218. // rowCount、colCount标记合并单元格范围
  219. const rowCount = node.posterityLeafCount() || 1;
  220. const parentRow = node.parent && node.parent.cellInfo ? node.parent.cellInfo.row : 0;
  221. // let prevRowCount = node.preSibling && node.preSibling.cellInfo ? node.preSibling.cellInfo.row + node.preSibling.cellInfo.rowCount - parentRow : 0;
  222. const prev = node.prevNode();
  223. let prevRowCount = prev && prev.cellInfo ? prev.cellInfo.row + prev.cellInfo.rowCount - parentRow : 0;
  224. const row = parentRow + prevRowCount;
  225. const col = node.depth();
  226. const name = node.data.type === itemType.ration ? node.data.name.split(' ')[0] : node.data.name;
  227. const text = `${isProcessNode(node) && node.data.required ? '* ' : ''}${name}`
  228. node.cellInfo = {
  229. row,
  230. col,
  231. rowCount,
  232. };
  233. if (this.curMaxCol < col) {
  234. this.curMaxCol = col;
  235. }
  236. return {
  237. row,
  238. col,
  239. rowCount,
  240. text,
  241. isRation: node.data.type === itemType.ration,
  242. colCount: 1,
  243. }
  244. });
  245. this.moveRationTextToLastCol(block);
  246. return block;
  247. }
  248. // 获取表格标题单元格数据,计算标题合并列依赖正文表格
  249. getBlockTitle() {
  250. return [
  251. { row: 0, col: 0, rowCount: 1, colCount: 1, text: '工序', isTitle: true },
  252. { row: 0, col: 1, rowCount: 1, colCount: this.curMaxCol === 0 ? 1 : this.curMaxCol - 1, text: '选项', isTitle: true },
  253. { row: 0, col: this.curMaxCol === 0 ? 2 : this.curMaxCol, rowCount: 1, colCount: 1, text: '定额', isTitle: true },
  254. ];
  255. }
  256. // 将定额文本挪到表格最大列处显示
  257. moveRationTextToLastCol(block) {
  258. block.forEach(cellInfo => {
  259. if (cellInfo.isRation) {
  260. cellInfo.col = this.curMaxCol;
  261. }
  262. });
  263. }
  264. // 从表格中获取最大列号
  265. getMaxCol(block) {
  266. return Math.max(...block.map(cellInfo => cellInfo.col));
  267. }
  268. // 根据精灵树数据,获取表格块range
  269. getBlockRange(elfTree) {
  270. const rowCount = elfTree.roots.reduce((prev, node) => prev + (node.posterityLeafCount() || 1), 0) + 1; // +1是因为有一行标题
  271. const a = Date.now();
  272. const colCount = Math.max(...elfTree.items.map(node => node.depth())) + 1;
  273. console.log(Date.now() - a);
  274. return {
  275. rowCount,
  276. colCount: colCount === 1 ? 3 : colCount, // 有些表格没有选项和定额
  277. }
  278. }
  279. // 获取清单的精灵树
  280. async getElfTree(billID) {
  281. const items = await this.getElfItems(billID);
  282. if (!items || !items.length) {
  283. return null;
  284. }
  285. const tree = idTree.createNew({ id: 'ID', pid: 'ParentID', nid: 'NextSiblingID', rootId: -1, autoUpdate: true });
  286. tree.loadDatas(items);
  287. return tree;
  288. }
  289. // 获取清单的精灵数据
  290. async getElfItems(billID) {
  291. if (this.billIDElfMap[billID]) {
  292. return this.billIDElfMap[billID];
  293. }
  294. if (!this.leafIDs.length) {
  295. return null;
  296. }
  297. const count = 20; // 每次拉取数据条数
  298. const billIDs = this.leafIDs.splice(0, count)
  299. await setTimeoutSync(null, 100);
  300. const items = await ajaxPost('/billsGuidance/api/getItemsByBillIDs', { guidanceLibID: this.libID, billIDs });
  301. this.updateProcessInfo();
  302. items.forEach(item => {
  303. (this.billIDElfMap[item.billsID] || (this.billIDElfMap[item.billsID] = [])).push(item)
  304. });
  305. return this.billIDElfMap[billID];
  306. }
  307. initSheet(index, name) {
  308. this.curRow = 0;
  309. this.sections = [];
  310. this.leafParentIDs = [];
  311. if (index !== 0) {
  312. // 工作簿自身会有一张表
  313. this.workBook.addSheet(index);
  314. }
  315. this.sheet = this.workBook.getSheet(index);
  316. this.sheet.name(name);
  317. for (let col = 0; col < this.sheet.getColumnCount(); col++) {
  318. this.sheet.setColumnWidth(col, 100);
  319. }
  320. }
  321. }