瀏覽代碼

feat: 导出清单精灵排版

vian 3 年之前
父節點
當前提交
e1fa7273e6

File diff suppressed because it is too large
+ 3 - 0
lib/fileSaver/FileSaver.min.js


File diff suppressed because it is too large
+ 36 - 0
lib/spreadjs/sheets/interop/gc.spread.excelio.11.1.2.min.js


+ 2 - 0
modules/all_models/std_billsGuidance_items.js

@@ -30,4 +30,6 @@ const stdBillsGuidanceItems = new Schema({
     isDefaultOption: {type: Boolean, default: false}, // 是否是默认选项
 }, {versionKey: false});
 
+stdBillsGuidanceItems.index({ libID: 1, billsID: 1 });
+
 mongoose.model('std_billsGuidance_items', stdBillsGuidanceItems, 'std_billsGuidance_items');

+ 11 - 0
modules/std_billsGuidance_lib/controllers/libController.js

@@ -86,6 +86,17 @@ class BillsGuideLibController extends BaseController{
         }
     }
 
+    async getItemsByBillIDs(req, res){
+        try{
+            let data = JSON.parse(req.body.data);
+            let items = await billsGuidanceFacade.getItemsByBillIDs(data.guidanceLibID, data.billIDs);
+            callback(req, res, 0, '', items);
+        }
+        catch(err){
+            callback(req, res, 1, err, null);
+        }
+    }
+
     async updateItems(req, res){
         try{
 

+ 13 - 2
modules/std_billsGuidance_lib/facade/facades.js

@@ -36,6 +36,7 @@ module.exports = {
     updateBillsGuideLib,
     getLibWithBills,
     getItemsBybills,
+    getItemsByBillIDs,
     updateItems,
     getBillMaterials,
     editBillMaterials,
@@ -359,9 +360,8 @@ function chainToArr(nodes) {
     return rst;
 }
 
-async function getItemsBybills(guidanceLibID, billsID) {
+async function checkItems(items) {
     const type = { job: 0, ration: 1 };
-    let items = await billsGuideItemsModel.find({ libID: guidanceLibID, billsID: billsID, deleted: false });
     let rationItems = _.filter(items, { type: type.ration });
     let rationIds = getAttrs('rationID', rationItems);
     let stdRations = await stdRationModel.find({ ID: { $in: rationIds }, $or: [{ isDeleted: null }, { isDeleted: false }] });
@@ -394,6 +394,17 @@ async function getItemsBybills(guidanceLibID, billsID) {
         }
         await billsGuideItemsModel.bulkWrite(bulkArr);
     }
+}
+
+async function getItemsBybills(guidanceLibID, billsID) {
+    let items = await billsGuideItemsModel.find({ libID: guidanceLibID, billsID: billsID});
+    await checkItems(items);
+    return items;
+}
+
+async function getItemsByBillIDs(guidanceLibID, billIDs) {
+    let items = await billsGuideItemsModel.find({ libID: guidanceLibID, billsID: { $in: billIDs }});
+    await checkItems(items);
     return items;
 }
 

+ 1 - 0
modules/std_billsGuidance_lib/routes/routes.js

@@ -22,6 +22,7 @@ module.exports = function (app) {
     router.post('/updateBillsGuideLib', billsGuideLibController.auth, billsGuideLibController.init, billsGuideLibController.updateBillsGuideLib);
     router.post('/getLibWithBills', billsGuideLibController.auth, billsGuideLibController.init, billsGuideLibController.getLibWithBills);
     router.post('/getItemsByBills', billsGuideLibController.auth, billsGuideLibController.init, billsGuideLibController.getItemsByBills);
+    router.post('/getItemsByBillIDs', billsGuideLibController.auth, billsGuideLibController.init, billsGuideLibController.getItemsByBillIDs);
     router.post('/updateItems', billsGuideLibController.auth, billsGuideLibController.init, billsGuideLibController.updateItems);
     router.post('/getBillMaterials', billsGuideLibController.auth, billsGuideLibController.init, billsGuideLibController.getBillMaterials);
     router.post('/editBillMaterials', billsGuideLibController.auth, billsGuideLibController.init, billsGuideLibController.editBillMaterials);

+ 16 - 0
public/web/id_tree.js

@@ -215,6 +215,11 @@ var idTree = {
              }) + node.children.count;*/
         };
 
+        Node.prototype.posterityLeafCount = function () {
+            return this.getPosterity().filter(item => !item.children.length).length;
+        };
+
+
         // 获取节点所有后代节点
         Node.prototype.getPosterity = function() {
             let posterity = [];
@@ -230,6 +235,17 @@ var idTree = {
             }
         };
 
+        // 担心链有问题,preSibling不靠谱的话,按照显示顺序算preSibling
+        Node.prototype.prevNode = function() {
+            const parent = this.parent || this.tree.roots;
+            if (!parent) {
+                return null;
+            }
+            const children = parent === this.tree.roots ? this.tree.roots :  parent.children;
+            const index = children.indexOf(this);
+            return children[index - 1] || null;
+        }
+
         Node.prototype.setExpanded = function (expanded) {
             var setNodesVisible = function (nodes, visible) {
                 nodes.forEach(function (node) {

+ 26 - 1
web/maintain/billsGuidance_lib/html/zhiyin.html

@@ -99,6 +99,7 @@
                                   <a id="upMove" href="javascript:void(0);" class="btn btn-sm lock-btn-control" data-toggle="tooltip" data-placement="bottom" title="上移"><i class="fa fa-arrow-up" aria-hidden="true"></i></a>
                                   <a id="editMaterial" href="javascript:void(0);" class="btn btn-sm"><i class="fa fa-edit" aria-hidden="true"></i> 配置材料</a>
                                   <a id="expandContract" href="javascript:void(0);" class="btn btn-sm" data-toggle="tooltip" data-placement="bottom" title="收起定额"><i class="fa fa-minus-square-o" aria-hidden="true"></i> 收起定额</a>
+                                  <a id="buildExcel" href="javascript:void(0);" class="btn btn-sm"><i class="fa fa-edit" aria-hidden="true"></i> 生成excel</a>
                                   <!-- <a id="generate-class" href="javascript:void(0);" class="btn btn-sm lock-btn-control" data-toggle="tooltip" data-placement="bottom" title="生成分类">测试生成分类算法</a> -->
                               </div>
                           </div>
@@ -227,6 +228,26 @@
             </div>
         </div>
     </div>
+    <div class="modal fade" id="excel-modal" data-backdrop="static" style="display: none;" aria-hidden="true">
+        <div class="modal-dialog" id="excel-dialog" role="document" style="max-width: none; width: 500px">
+            <div class="modal-content">
+                <div class="modal-header">
+                    <h5 class="modal-title">导出excel</h5>
+                    <button type="button"  class="close" data-dismiss="modal" aria-label="Close">
+                        <span aria-hidden="true">×</span>
+                    </button>
+                </div>
+                <div class="modal-body">
+                    <div id="excel-info"></div>
+                    <div id="excel-spread" style="height: 500px; display: none;"></div>
+                </div>
+                <div class="modal-footer">
+                    <a id="export-excel-confirm" href="javascript: void(0);" class="btn btn-primary" style="display: none;">导出</a>
+                    <button type="button" class="btn btn-secondary"  data-dismiss="modal">关闭</button>
+                </div>
+            </div>
+        </div>
+    </div>
     <div class="modal fade" id="alert" data-backdrop="static" style="display: none;" aria-hidden="true">
         <div class="modal-dialog" role="document">
             <div class="modal-content">
@@ -252,8 +273,10 @@
     <script src="/lib/bootstrap/bootstrap.min.js"></script>
     <script src="/lib/jquery-contextmenu/jquery.contextMenu.js"></script>
     <script src="/lib/jquery-contextmenu/jquery.ui.position.js"></script>
-    <script src = "/lib/spreadjs/sheets/gc.spread.sheets.all.11.1.2.min.js"></script>
+    <script src ="/lib/spreadjs/sheets/gc.spread.sheets.all.11.1.2.min.js"></script>
+    <script src="/lib/spreadjs/sheets/interop/gc.spread.excelio.11.1.2.min.js"></script>
     <script>GC.Spread.Sheets.LicenseKey =  '<%- LicenseKey %>';</script>
+    <script src="/lib/fileSaver/FileSaver.min.js"></script>
     <script src="/lib/lodash/lodash.js"></script>
     <script src="/public/web/uuid.js"></script>
     <script src="/public/web/sheet/sheet_common.js"></script>
@@ -266,6 +289,8 @@
     <script src="/public/web/id_tree.js"></script>
     <script src="/public/web/tree_sheet/tree_sheet_controller.js"></script>
     <script src="/public/web/tree_sheet/tree_sheet_helper.js"></script>
+    <script src="/web/maintain/billsGuidance_lib/js/util.js"></script>
+    <script src="/web/maintain/billsGuidance_lib/js/exportExcel.js"></script>
     <script src="/web/maintain/billsGuidance_lib/js/billsGuidance.js"></script>
     <script src="/web/common/js/slideResize.js"></script>
 </body>

+ 42 - 18
web/maintain/billsGuidance_lib/js/billsGuidance.js

@@ -8,9 +8,6 @@
  */
 
 const billsGuidance = (function () {
-    function _isDef(v) {
-        return typeof v !== 'undefined' && v !== null;
-    }
 
 
     function sortByCode(arr) {
@@ -45,6 +42,7 @@ const billsGuidance = (function () {
             return recurCompare(aArr, bArr, 0);
         });
     }
+    // 导出实例
     let curCompilationID = '';
     const locked = lockUtil.getLocked();
     let moduleName = 'stdBillsGuidance';
@@ -118,11 +116,6 @@ const billsGuidance = (function () {
     const selectedBgColor = '#DFE8F9';
     const searchBgColor = 'lemonChiffon';
 
-    //项目指引类型
-    const itemType = {
-        job: 0,
-        ration: 1
-    };
     //项目指引复制整块localStorage key
     const itemCopyBlockKey = 'guideItemCopyBlock';
     const updateType = {
@@ -438,6 +431,9 @@ const billsGuidance = (function () {
         }
     }
 
+    /* 导出excel */
+    let exportExcelWorkBook = null;
+
     // 显示清单材料数据
     function showBillMaterialData(sheet, headers, datas, emptyRow = 0) {
         let fuc = function () {
@@ -522,16 +518,6 @@ const billsGuidance = (function () {
         }
     }
 
-    // 是否为工序行
-    function isProcessNode(node) {
-        return node && node.depth() % 2 === 0 && _isDef(node.data.type) && node.data.type === itemType.job
-    }
-
-    // 是否是选项行
-    function isOptionNode(node) {
-        return node && node.depth() % 2 === 1 && _isDef(node.data.type) && node.data.type === itemType.job
-    }
-
     //渲染时方法,停止渲染
     //@param {Object}sheet {Function}func @return {void}
     function renderSheetFunc(sheet, func) {
@@ -917,6 +903,7 @@ const billsGuidance = (function () {
     //初始化各工作表
     //@param {Array}modules @return {void}
     function initWorkBooks(modules) {
+        exportExcelWorkBook = new GC.Spread.Sheets.Workbook($('#excel-spread')[0], { sheetCount: 1 });
         for (let module of modules) {
             buildSheet(module);
         }
@@ -2286,6 +2273,43 @@ const billsGuidance = (function () {
             }
         });
 
+        /* excel */
+        // 生成excel
+        $('#buildExcel').click(function () {
+            if (bills && bills.tree) {
+                $('#excel-modal').modal('show');
+            }
+        });
+        $("#excel-modal").on('hidden.bs.modal', function () {
+            $('#excel-info').text(`导出清单中:`);
+            $('#excel-spread').hide();
+            $('#export-excel-confirm').hide();
+            $('#excel-dialog').width('500px');
+            if (exportExcelWorkBook) {
+                exportExcelWorkBook.clearSheets();
+                exportExcelWorkBook.addSheet();
+            }
+        });
+        let excelInstance;
+        $("#excel-modal").on('shown.bs.modal', async function () {
+            exportExcelWorkBook.refresh();
+            if (bills.tree) {
+                excelInstance = new ExportExcel(exportExcelWorkBook, bills.tree, libID);
+                try {
+                    await excelInstance.paintOnSheet();
+                } catch (error) {
+                    console.log(error);
+                    alert(error);
+                    $("#excel-modal").modal('hide');
+                }
+            }
+        });
+        $('#export-excel-confirm').click(() => {
+            if (excelInstance) {
+                excelInstance.export();
+            }
+        })
+
         $('#insert').click(function () {
             insert([{ type: itemType.job, name: '', outputItemCharacter: true }], false);
         });

+ 237 - 0
web/maintain/billsGuidance_lib/js/exportExcel.js

@@ -0,0 +1,237 @@
+class ExportExcel {
+  workBook = null;
+
+  sheet = null;
+
+  border = new GC.Spread.Sheets.LineBorder('#000', GC.Spread.Sheets.LineStyle.thin);
+
+  excelIo = new GC.Spread.Excel.IO();
+
+  $info = $('#excel-info');
+
+  // 表格当前画到的行
+  curRow = 0;
+
+  billTree = null;
+
+  curBillNode = null;
+
+  libID = '';
+
+  // 表格块,类型为ExcelBlock实例
+  blocks = [];
+
+  // 清单ID - 清单精灵映射
+  billIDElfMap = {};
+
+  // 叶子清单ID
+  leafIDs = [];
+
+  // 叶子清单总数量
+  total = 0;
+
+  constructor(workBook, billTree, libID) {
+    this.workBook = workBook;
+    this.sheet = this.workBook.getSheet(0);
+    this.initSheet();
+    this.billTree = billTree;
+    this.libID = libID;
+    this.leafIDs = billTree.items.filter(node => !node.children.length).map(node => node.data.ID);
+    this.total = this.leafIDs.length;
+  }
+
+  // 导出
+  async export() {
+    if (!this.workBook) {
+      return;
+    }
+    const json = this.workBook.toJSON();
+    this.excelIo.save(json, function (blob) {
+      saveAs(blob, '清单精灵排版.xlsx');
+    }, function (e) {
+        // process error
+        alert(e);
+        console.log(e);
+    });
+
+    }
+
+    // 将数据画在表格上
+    async paintOnSheet() {
+      this.updateProcessInfo();
+      // let i = 0;
+      this.sheet.suspendPaint();
+      this.sheet.suspendEvent();
+      for (const billNode of this.billTree.items) {
+        // 画标题
+        if (this.curRow >= this.sheet.getRowCount()) {
+          this.sheet.addRows(this.curRow, 2);
+        }
+        this.sheet.setFormatter(this.curRow, 0, '@');
+        this.sheet.setFormatter(this.curRow, 1, '@');
+        this.sheet.setText(this.curRow, 0, billNode.data.code);
+        this.sheet.setText(this.curRow++, 1, billNode.data.name);
+        // 画表格
+        if (!billNode.children.length) {
+          this.curRow++; // 空行
+          const elfTree = await this.getElfTree(billNode.data.ID);
+          if (!elfTree) {
+            continue;
+          }
+          const block = this.getBlock(elfTree);
+          if (!block.length) {
+            continue;
+          }
+          const blockRange = this.getBlockRange(elfTree);
+          this.checkRange(blockRange);
+          const range = this.sheet.getRange(block[0].row + this.curRow, block[0].col, blockRange.rowCount, blockRange.colCount)
+          // 画单元格
+          console.log(billNode.data);
+          console.log(block);
+          this.drawCell(block);
+          // 画边框
+          this.drawBorder(range)
+          // i++;
+          this.curRow += blockRange.rowCount + 1;
+        }
+        /* if (i === 100) {
+          break;
+        } */
+      }
+      const range = this.sheet.getRange(0, 0, this.sheet.getRowCount(), this.sheet.getColumnCount());
+      range.vAlign(GC.Spread.Sheets.VerticalAlign.center);
+      range.wordWrap(true);
+      this.sheet.resumeEvent();
+      this.sheet.resumePaint();
+      if (this.total === this.curProcessCount) {
+        this.afterPaint();
+      }
+    }
+
+  /* ========================================================以下为私有方法============================================== */
+
+  // 当前到多少条叶子清单
+  get curProcessCount () {
+    return this.total - this.leafIDs.length;
+  }
+
+  // 画完表格后的处理
+  afterPaint() {
+      $('#excel-dialog').width('800px');
+      $('#excel-spread').show();
+      this.workBook.refresh();
+      $('#export-excel-confirm').show();
+  }
+
+  // 超出范围追加行列
+  checkRange(blockRange) {
+    // 不够行数,追加行数
+    const needRows = this.curRow + blockRange.rowCount;
+    const curRowCount = this.sheet.getRowCount();
+    if (curRowCount < needRows) {
+      this.sheet.addRows(this.curRow, needRows - curRowCount + 5);
+    }
+    // 不够列数,追加列数
+    const curColCount = this.sheet.getColumnCount();
+    if (curColCount < blockRange.colCount) {
+      this.sheet.addColumns(curColCount - 1, blockRange.colCount - curColCount);
+    }
+  }
+
+  // 画单元格、合并单元格
+  drawCell(block) {
+    block.forEach(item => {
+      const row = item.row + this.curRow;
+      this.sheet.addSpan(row, item.col, item.rowCount, item.colCount);
+      this.sheet.setFormatter(row, item.col, '@');
+      this.sheet.setText(row, item.col, item.text);
+    });
+  }
+
+  // 画边框
+  drawBorder(range) {
+    range.setBorder(this.border, { all: true })
+  }
+
+  // 更新进度信息
+  updateProcessInfo() {
+    this.$info.text(`导出清单中: ${this.curProcessCount} / ${this.total}`)
+  }
+
+  // 精灵树数据转换为表格块单元格数据
+  getBlock(elfTree) {
+    return elfTree.items.map(node => {
+      // rowCount、colCount标记合并单元格范围
+      const rowCount = node.posterityLeafCount() || 1;
+      const parentRow = node.parent && node.parent.cellInfo ? node.parent.cellInfo.row : 0;
+      // let prevRowCount = node.preSibling && node.preSibling.cellInfo ? node.preSibling.cellInfo.row + node.preSibling.cellInfo.rowCount - parentRow  : 0;
+      const prev = node.prevNode();
+      let prevRowCount = prev && prev.cellInfo ? prev.cellInfo.row + prev.cellInfo.rowCount - parentRow  : 0;
+      const row = parentRow + prevRowCount;
+      const col = node.depth();
+      const name = node.data.type === itemType.ration ? node.data.name.split(' ')[0] : node.data.name;
+      const text = `${isProcessNode(node) && node.data.require ? '* ' : ''}${name}`
+      node.cellInfo = {
+        row,
+        col,
+        rowCount,
+      };
+      return {
+        row,
+        col,
+        rowCount,
+        text,
+        colCount: 1,
+      }
+    })
+  }
+
+  // 根据精灵树数据,获取表格块range
+  getBlockRange(elfTree) {
+    const rowCount = elfTree.roots.reduce((prev, node) => prev + (node.posterityLeafCount() || 1), 0);
+    const a = Date.now();
+    const colCount = Math.max(...elfTree.items.map(node => node.depth())) + 1;
+    console.log(Date.now() - a);
+    return {
+      rowCount,
+      colCount
+    }
+  }
+
+  // 获取清单的精灵树
+  async getElfTree(billID) {
+    const items = await this.getElfItems(billID);
+    if (!items || !items.length) {
+      return null;
+    }
+    const tree = idTree.createNew({ id: 'ID', pid: 'ParentID', nid: 'NextSiblingID', rootId: -1, autoUpdate: true });
+    tree.loadDatas(items);
+    return tree;
+  }
+
+  // 获取清单的精灵数据
+  async getElfItems(billID) {
+    if (this.billIDElfMap[billID]) {
+      return this.billIDElfMap[billID];
+    }
+    if (!this.leafIDs.length) {
+      return null;
+    }
+    const count = 20; // 每次拉取数据条数
+    const billIDs = this.leafIDs.splice(0, count)
+    await setTimeoutSync(null, 100);
+    const items = await ajaxPost('/billsGuidance/api/getItemsByBillIDs', { guidanceLibID: this.libID, billIDs });
+    this.updateProcessInfo();
+    items.forEach(item => {
+      (this.billIDElfMap[item.billsID] || (this.billIDElfMap[item.billsID] = [])).push(item)
+    });
+    return this.billIDElfMap[billID];
+  }
+
+  initSheet() {
+    this.sheet.name('清单精灵');
+    for (let col = 0; col < this.sheet.getColumnCount(); col++) {
+      this.sheet.setColumnWidth(col, 100);
+    }
+  }
+}

+ 31 - 0
web/maintain/billsGuidance_lib/js/util.js

@@ -0,0 +1,31 @@
+
+//项目指引类型
+const itemType = {
+    job: 0,
+    ration: 1
+};
+
+function _isDef(v) {
+    return typeof v !== 'undefined' && v !== null;
+}
+
+// 是否为工序行
+function isProcessNode(node) {
+    return node && node.depth() % 2 === 0 && _isDef(node.data.type) && node.data.type === itemType.job
+}
+
+// 是否是选项行
+function isOptionNode(node) {
+    return node && node.depth() % 2 === 1 && _isDef(node.data.type) && node.data.type === itemType.job
+}
+
+function setTimeoutSync(handle, time) {
+    return new Promise(function (resolve, reject) {
+        setTimeout(function () {
+            if (handle && typeof handle === 'function') {
+                handle();
+            }
+            resolve();
+        }, time);
+    });
+}