exportExcel.js 12 KB

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