Browse Source

统计分析,台账对比

maixinrong 5 years ago
parent
commit
025c9ecb7d

+ 74 - 0
app/controller/spss_controller.js

@@ -0,0 +1,74 @@
+'use strict';
+
+/**
+ * 标段对比 控制器
+ *
+ * @author Mai
+ * @date 2020/2/21
+ * @version
+ */
+
+const measureType = require('../const/tender').measureType;
+
+module.exports = app => {
+
+    class SpssController extends app.BaseController {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局context
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            ctx.showProject = true;
+        }        
+
+        /**
+         * 初始化用户信息页面
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        async compareTz(ctx) {
+            try {
+                const renderData = {
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.compare.tz)
+                }
+                await this.layout('spss/compare_tz.ejs', renderData, 'spss/compare_tz_modal.ejs');
+            } catch (err) {
+                console.log(err);
+                ctx.helper.log(err);
+            }
+        }
+
+        async loadCompareTz(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                const responseData = {err: 0, msg: '', data: {}};
+
+                const tender1 = await ctx.service.tender.getTender(data.tid1);
+                responseData.data.tender1 = {
+                    name: tender1.name,
+                    bills: await ctx.service.ledger.getData(data.tid1),
+                    pos: tender1.measure_type === measureType.tz.value 
+                        ? await ctx.service.pos.getPosData({tid: data.tid1}) : []             
+                };
+                const tender2 = await ctx.service.tender.getTender(data.tid2);
+                responseData.data.tender2 = {
+                    name: tender2.name,
+                    bills: await ctx.service.ledger.getData(data.tid2),
+                    pos: tender2.measure_type === measureType.tz.value 
+                        ? await ctx.service.pos.getPosData({tid: data.tid2}) : []             
+                };
+                ctx.body = responseData;
+            } catch (err) {
+                ctx.helper.log(err);
+                ctx.body = this.ajaxErrorBody(err, '查询数据错误');
+            }
+        }
+    }
+
+    return SpssController;
+};

+ 250 - 0
app/public/js/compare_tz.js

@@ -0,0 +1,250 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+
+$(document).ready(function () {
+    autoFlashHeight();
+    // 根据设置整理Spread设置
+    const ledgerSpreadSetting = {
+        cols: [
+            { title: '项目节编号', colSpan: '1', rowSpan: '2', field: 'code', hAlign: 0, width: 150, formatter: '@', cellType: 'tree' },
+            { title: '清单编号', colSpan: '1', rowSpan: '2', field: 'b_code', hAlign: 0, width: 80, formatter: '@' },
+            { title: '名称', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 230, formatter: '@' },
+            { title: '单位', colSpan: '1', rowSpan: '2', field: 'unit', hAlign: 1, width: 50, formatter: '@', cellType: 'unit' },
+            { title: '单价', colSpan: '1', rowSpan: '2', field: 'unit_price', hAlign: 2, width: 60, type: 'Number' },
+            { title: '标段1|数量', colSpan: '2|1', rowSpan: '1|1', field: 'qty_1', hAlign: 2, width: 60, type: 'Number' },
+            { title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'tp_1', hAlign: 2, width: 60, type: 'Number' },
+            { title: '标段2|数量', colSpan: '2|1', rowSpan: '1|1', field: 'qty_2', hAlign: 2, width: 60, type: 'Number' },
+            { title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'tp_2', hAlign: 2, width: 60, type: 'Number' },
+            { title: '标段1-标段2|数量', colSpan: '2|1', rowSpan: '1|1', field: 'qty_differ', hAlign: 2, width: 60, type: 'Number' },
+            { title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'tp_differ', hAlign: 2, width: 60, type: 'Number' },
+        ],
+        emptyRows: 2,
+        headRows: 2,
+        headRowHeight: [25, 25],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+        readOnly: true,
+        getColor: function (sheet, data, col, defaultColor) {
+            function checkDiffer(data) {
+                return !checkZero(data.qty_differ) && !checkZero(data.tp_differ);
+            }
+            return data && checkDiffer(data) ? '#F2DEDE' : defaultColor;
+        }
+    }
+    const posSpreadSetting = {
+        cols: [
+            { title: '计量单元', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 230, formatter: '@' },
+            { title: '数量|标段1', colSpan: '3|1', rowSpan: '1|1', field: 'qty_1', hAlign: 2, width: 80, type: 'Number' },
+            { title: '|标段2', colSpan: '|1', rowSpan: '|1', field: 'qty_2', hAlign: 2, width: 80, type: 'Number' },
+            { title: '|标段1-标段2', colSpan: '|1', rowSpan: '|1', field: 'qty_differ', hAlign: 2, width: 80, type: 'Number' },
+        ],
+        emptyRows: 3,
+        headRows: 2,
+        headRowHeight: [25, 25],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+        readOnly: true,
+        getColor: function (sheet, data, col, defaultColor) {
+            function checkDiffer(data) {
+                const fieldSufs = sheet.zh_setting.fieldSufs;
+                if (fieldSufs.length <= 1) return false;
+                const base = data['gather_qty' + fieldSufs[0]];
+                for (let i = 1; i < fieldSufs.length; i++) {
+                    const compare = data['gather_qty' + fieldSufs[i]];
+                    if ((base || compare) && (compare !== base)) return true;
+                }
+            }
+            return checkDiffer(data) ? '#F2DEDE' : defaultColor;
+        }
+    };
+    // 初始化台账
+    const billsSpread = SpreadJsObj.createNewSpread($('#bills-spread')[0]);
+    const billsSheet = billsSpread.getActiveSheet();
+    SpreadJsObj.initSheet(billsSheet, ledgerSpreadSetting);
+    // 初始化部位
+    const posSpread = SpreadJsObj.createNewSpread($('#pos-spread')[0]);
+    const posSheet = posSpread.getActiveSheet();
+    SpreadJsObj.initSheet(posSheet, posSpreadSetting);
+
+    // const billsTree = new CompareTree({
+    //     id: 'id',
+    //     pid: 'pid',
+    //     order: 'order',
+    //     level: 'level',        
+    //     rootId: -1,
+    //     loadInfo1: function (node, source) {
+    //         node.qty_1 = source.quantity;
+    //         node.tp_1 = source.total_price;
+    //     },
+    //     loadInfo2: function (node, source) {
+    //         node.qty_2 = source.quantity;
+    //         node.tp_2 = source.total_price;
+    //     },
+    //     calcDiffer: function (node) {
+    //         node.qty_differ = ZhCalc.sub(node.qty_1, node.qty_2);
+    //         node.tp_differ = ZhCalc.sub(node.tp_1, node.tp_2);
+    //     },
+    // });
+    const billsTree = createNewPathTree('compare', {
+        id: 'id',
+        pid: 'pid',
+        order: 'order',
+        level: 'level',        
+        rootId: -1,
+        loadInfo1: function (node, source) {
+            node.qty_1 = source.quantity;
+            node.tp_1 = source.total_price;
+        },
+        loadInfo2: function (node, source) {
+            node.qty_2 = source.quantity;
+            node.tp_2 = source.total_price;
+        },
+        calcDiffer: function (node) {
+            node.qty_differ = ZhCalc.sub(node.qty_1, node.qty_2);
+            node.tp_differ = ZhCalc.sub(node.tp_1, node.tp_2);
+        },
+    });
+
+    // // 获取部位明细数据
+    // function loadPosData(iRow) {
+    //     const node = billsSheet.zh_tree.nodes[iRow];
+    //     if (node) {
+    //         SpreadJsObj.loadSheetData(posSheet, SpreadJsObj.DataType.Data, node.pos);
+    //     } else {
+    //         SpreadJsObj.loadSheetData(posSheet, SpreadJsObj.DataType.Data, []);
+    //     }
+    //     SpreadJsObj.resetTopAndSelect(posSheet);
+    // }
+    // // 切换清单行,读取所属项目节数据
+    // billsSheet.bind(spreadNS.Events.SelectionChanged, function (e, info) {
+    //     if (info.newSelections) {
+    //         const iNewRow = info.newSelections[0].row;
+    //         if (info.oldSelections) {
+    //             const iOldRow = info.oldSelections[0].row;
+    //             if (iNewRow !== iOldRow) {
+    //                 loadPosData(iNewRow);
+    //             }
+    //         } else {
+    //             loadPosData(iNewRow);
+    //         }
+    //     }
+    // });
+
+    // 选择
+    $('#compare-ok').click(function () {
+        let id1, id2;
+        try {
+            id1 = parseInt($('input[name=id1]').val());
+            id2 = parseInt($('input[name=id2]').val());
+
+            if (id1 > 0 && id2 > 0) {
+                postData(window.location.pathname + '/load', { tid1: id1, tid2: id2 }, function (result) {
+                    // 初始化基础数据分析
+                    const tenderTreeSetting = {
+                        id: 'ledger_id',
+                        pid: 'ledger_pid',
+                        order: 'order',
+                        level: 'level',
+                        rootId: -1,
+                        keys: ['id', 'tender_id', 'ledger_id'],
+                        calcFields: ['deal_tp', 'sgfh_tp', 'sjcl_tp', 'qtcl_tp', 'total_price'],
+                    };
+                    const tender1 = {
+                        billsTree: createNewPathTree('ledger', tenderTreeSetting),
+                    };
+                    tender1.billsTree.loadDatas(result.tender1.bills);
+                    treeCalc.calculateAll(tender1.billsTree);
+                    const col1 = ledgerSpreadSetting.cols.find(function (x) {
+                        return x.field === 'qty_1';
+                    })
+                    col1.title = result.tender1.name + '|数量';
+
+                    const tender2 = {
+                        billsTree: createNewPathTree('ledger', tenderTreeSetting),
+                    };
+                    tender2.billsTree.loadDatas(result.tender2.bills);
+                    treeCalc.calculateAll(tender2.billsTree);
+                    const col2 = ledgerSpreadSetting.cols.find(function (x) {
+                        return x.field === 'qty_2';
+                    })
+                    col2.title = result.tender2.name + '|数量';
+                    
+                    SpreadJsObj.initSheet(billsSheet, ledgerSpreadSetting);
+                    billsTree.loadCompareData(tender1, tender2);
+                    SpreadJsObj.loadSheetData(billsSheet, SpreadJsObj.DataType.Tree, billsTree);
+                    $('#compare-select').modal('hide');
+                });
+            }
+        } catch (err) {
+            toastr.error('输入的标段ID非法');
+        }
+    });
+    // 显示层次
+    (function (select, sheet) {
+        $(select).click(function () {
+            if (!sheet.zh_tree) return;
+            const tag = $(this).attr('tag');
+            const tree = sheet.zh_tree;
+            switch (tag) {
+                case "1":
+                case "2":
+                case "3":
+                case "4":
+                case "5":
+                    tree.expandByLevel(parseInt(tag));
+                    SpreadJsObj.refreshTreeRowVisible(sheet);
+                    break;
+                case "last":
+                    tree.expandByCustom(() => { return true; });
+                    SpreadJsObj.refreshTreeRowVisible(sheet);
+                    break;
+                case "leafXmj":
+                    tree.expandToLeafXmj();
+                    SpreadJsObj.refreshTreeRowVisible(sheet);
+                    break;
+                case "curMeasure":
+                    tree.expandByCalcFields();
+                    SpreadJsObj.refreshTreeRowVisible(sheet);
+                    break;
+            }
+        });
+    })('a[name=showLevel]', billsSheet);
+    // 导航栏
+    $.subMenu({
+        menu: '#sub-menu', miniMenu: '#sub-mini-menu', miniMenuList: '#mini-menu-list',
+        toMenu: '#to-menu', toMiniMenu: '#to-mini-menu',
+        key: 'menu.1.0.0',
+        miniHint: '#sub-mini-hint', hintKey: 'menu.hint.1.0.1',
+        callback: function (info) {
+            if (info.mini) {
+                $('.panel-title').addClass('fluid');
+                $('#sub-menu').removeClass('panel-sidebar');
+            } else {
+                $('.panel-title').removeClass('fluid');
+                $('#sub-menu').addClass('panel-sidebar');
+            }
+            autoFlashHeight();
+            billsSpread.refresh();
+            posSpread.refresh();
+        }
+    });
+    // 上下窗口resizer
+    $.divResizer({
+        select: '#main-resize',
+        callback: function () {
+            billsSpread.refresh();
+            let bcontent = $(".bcontent-wrap").length > 0 ? $(".bcontent-wrap").height() : 0;
+            $(".sp-wrap").height(bcontent - 30);
+            posSpread.refresh();
+        }
+    });
+});

+ 125 - 41
app/public/js/path_tree.js

@@ -16,7 +16,7 @@ class PosData {
      * 构造函数
      * @param {id|Number, masterId|Number} setting
      */
-    constructor (setting) {
+    constructor(setting) {
         // 无索引
         this.datas = [];
         // 以key为索引
@@ -114,7 +114,7 @@ class PosData {
      * 移除数据 - 根据分类id
      * @param mid
      */
-    removeDatasByMasterId (mid) {
+    removeDatasByMasterId(mid) {
         const masterKey = itemsPre + mid;
         const range = this.ledgerPos[masterKey];
         if (range) {
@@ -190,7 +190,7 @@ class MasterPosData extends PosData {
      * 构造函数
      * @param {id|Number, masterId|Number} setting
      */
-    constructor (setting) {
+    constructor(setting) {
         super(setting);
         // 关联索引
         this.masterItems = {};
@@ -199,7 +199,7 @@ class MasterPosData extends PosData {
      * 加载主数据
      * @param datas
      */
-    loadDatas (datas) {
+    loadDatas(datas) {
         super.loadDatas(datas);
         // 清空旧数据
         this.masterItems = {};
@@ -252,7 +252,7 @@ const createNewPathTree = function (type, setting) {
         /**
          * 构造函数
          */
-        constructor (setting) {
+        constructor(setting) {
             // 无索引
             this.datas = [];
             // 以key为索引
@@ -267,7 +267,7 @@ const createNewPathTree = function (type, setting) {
         /**
          * 树结构根据显示排序
          */
-        sortTreeNode (isResort) {
+        sortTreeNode(isResort) {
             const self = this;
             const addSortNodes = function (nodes) {
                 if (!nodes) { return }
@@ -298,7 +298,7 @@ const createNewPathTree = function (type, setting) {
          * 加载数据(初始化), 并给数据添加部分树结构必须数据
          * @param datas
          */
-        loadDatas (datas) {
+        loadDatas(datas) {
             // 清空旧数据
             this.items = {};
             this.nodes = [];
@@ -342,7 +342,7 @@ const createNewPathTree = function (type, setting) {
          * @param {Number} id
          * @returns {Object}
          */
-        getItems (id) {
+        getItems(id) {
             return this.items[itemsPre + id];
         };
         /**
@@ -350,10 +350,10 @@ const createNewPathTree = function (type, setting) {
          * @param {Object} node
          * @returns {Object}
          */
-        getParent (node) {
+        getParent(node) {
             return this.getItems(node[this.setting.pid]);
         };
-        getAllParents (node) {
+        getAllParents(node) {
             const parents = [];
             if (node.full_path && node.full_path !== '') {
                 const parentIds = node.full_path.split('-');
@@ -404,7 +404,7 @@ const createNewPathTree = function (type, setting) {
          * 根据path查找完整节点
          * @param {Number} path
          */
-        getFullPathNodes (path) {
+        getFullPathNodes(path) {
             const self = this, ids = path.split('-');
             if (ids.length > 0) {
                 return this.nodes.filter((x) => {
@@ -419,7 +419,7 @@ const createNewPathTree = function (type, setting) {
          * @param {Object} node
          * @returns {Array}
          */
-        getChildren (node) {
+        getChildren(node) {
             const setting = this.setting;
             const pid = node ? node[setting.id] : setting.rootId;
             const children = this.datas.filter(function (x) {
@@ -437,7 +437,7 @@ const createNewPathTree = function (type, setting) {
          * @returns {*}
          * @private
          */
-        _recursiveGetPosterity (node) {
+        _recursiveGetPosterity(node) {
             let posterity = node.children;
             for (const c of node.children) {
                 posterity = posterity.concat(this._recursiveGetPosterity(c));
@@ -449,7 +449,7 @@ const createNewPathTree = function (type, setting) {
          * @param {Object} node
          * @returns {Array}
          */
-        getPosterity (node) {
+        getPosterity(node) {
             if (node.full_path !== '') {
                 const reg = new RegExp('^' + node.full_path + '-');
                 return this.datas.filter(function (x) {
@@ -464,16 +464,16 @@ const createNewPathTree = function (type, setting) {
          * @param {Object} node
          * @returns {boolean}
          */
-        isLastSibling (node) {
+        isLastSibling(node) {
             const siblings = this.getChildren(this.getParent(node));
-            return (siblings && siblings.length > 0) ?  node.order === siblings[siblings.length - 1].order : false;
+            return (siblings && siblings.length > 0) ? node.order === siblings[siblings.length - 1].order : false;
         };
         /**
          * 刷新子节点是否可见
          * @param {Object} node
          * @private
          */
-        _refreshChildrenVisible (node) {
+        _refreshChildrenVisible(node) {
             if (!node.children) {
                 node.children = this.getChildren(node);
             }
@@ -489,7 +489,7 @@ const createNewPathTree = function (type, setting) {
          * @param {Object} node
          * @param {Boolean} expanded
          */
-        setExpanded (node, expanded) {
+        setExpanded(node, expanded) {
             node.expanded = expanded;
             this._refreshChildrenVisible(node);
         };
@@ -499,7 +499,7 @@ const createNewPathTree = function (type, setting) {
          * @param {Object} node - 节点
          * @returns {key}
          */
-        getNodeKeyData (node) {
+        getNodeKeyData(node) {
             const data = {};
             for (const key of this.setting.keys) {
                 data[key] = node[key];
@@ -511,7 +511,7 @@ const createNewPathTree = function (type, setting) {
          * @param node
          * @returns {*}
          */
-        getNodeKey (node) {
+        getNodeKey(node) {
             return node[this.setting.id];
         };
 
@@ -569,7 +569,7 @@ const createNewPathTree = function (type, setting) {
          * @return {Array} 加载到树的数据
          * @privateA
          */
-        _updateData (datas) {
+        _updateData(datas) {
             datas = datas instanceof Array ? datas : [datas];
             let loadedData = [];
             for (const data of datas) {
@@ -608,7 +608,7 @@ const createNewPathTree = function (type, setting) {
          * @return {Array} 加载到树的数据
          * @privateA
          */
-        _loadData (datas) {
+        _loadData(datas) {
             datas = datas instanceof Array ? datas : [datas];
             const loadedData = [], resortData = [];
             for (const data of datas) {
@@ -663,7 +663,7 @@ const createNewPathTree = function (type, setting) {
          * @param datas
          * @private
          */
-        _freeData (datas) {
+        _freeData(datas) {
             datas = datas instanceof Array ? datas : [datas];
             const freeDatas = [];
             const removeArrayData = function (array, data) {
@@ -687,7 +687,7 @@ const createNewPathTree = function (type, setting) {
                     removeArrayData(this.datas, node);
                 }
             }
-            for(const node of freeDatas) {
+            for (const node of freeDatas) {
                 removeArrayData(this.nodes, node);
             }
             return freeDatas;
@@ -698,7 +698,7 @@ const createNewPathTree = function (type, setting) {
          * @returns {Array}
          * @private
          */
-        _loadExpandData (datas) {
+        _loadExpandData(datas) {
             datas = datas instanceof Array ? datas : [datas];
             const loadedData = [], existData = [], expandData = [], resortData = [];
             for (const data of datas) {
@@ -771,10 +771,10 @@ const createNewPathTree = function (type, setting) {
          * @param {Object} node
          * @param {function} callback
          */
-        loadChildren (node, callback) {
+        loadChildren(node, callback) {
             if (this.setting.url !== '') {
                 const self = this;
-                postData(this.setting.url, {postType: 'load-child', id: this.getNodeKeyData(node)}, function (data) {
+                postData(this.setting.url, { postType: 'load-child', id: this.getNodeKeyData(node) }, function (data) {
                     self._loadData(data);
                     callback();
                 });
@@ -783,7 +783,7 @@ const createNewPathTree = function (type, setting) {
     }
 
     class MeasureTree extends BaseTree {
-        addData (datas) {
+        addData(datas) {
             const loadedData = [];
             for (const data of datas) {
                 let node = this.getItems(data[this.setting.id]);
@@ -814,7 +814,7 @@ const createNewPathTree = function (type, setting) {
             }
             return loadedData;
         }
-        removeData (datas) {
+        removeData(datas) {
             datas.sort(function (a, b) {
                 return b.level - a.level;
             });
@@ -835,7 +835,7 @@ const createNewPathTree = function (type, setting) {
                 }
             }
         };
-        loadLeafData (data) {
+        loadLeafData(data) {
             const datas = data instanceof Array ? data : [data];
             for (const d of datas) {
                 let node = this.getItems(d[this.setting.id]);
@@ -953,7 +953,7 @@ const createNewPathTree = function (type, setting) {
                 }
                 // 最底层项目节,也需要计算
                 //if (this.getItems(node.ledger_id) && node.children.length > 0) {
-                    reCalcNodes.push(node);
+                reCalcNodes.push(node);
                 //}
             }
         }
@@ -1014,7 +1014,7 @@ const createNewPathTree = function (type, setting) {
         /**
          * 构造函数
          */
-        constructor (setting) {
+        constructor(setting) {
             super(setting);
             // stage关联索引
             this.stageItems = {};
@@ -1023,7 +1023,7 @@ const createNewPathTree = function (type, setting) {
          * 加载数据(初始化), 并给数据添加部分树结构必须数据
          * @param datas
          */
-        loadDatas (datas) {
+        loadDatas(datas) {
             super.loadDatas(datas);
             // 清空旧数据
             this.stageItems = {};
@@ -1067,7 +1067,7 @@ const createNewPathTree = function (type, setting) {
          * @return {Array} 加载到树的数据
          * @privateA
          */
-        _updateData (datas) {
+        _updateData(datas) {
             datas = datas instanceof Array ? datas : [datas];
             let loadedData = [];
             for (const data of datas) {
@@ -1103,7 +1103,7 @@ const createNewPathTree = function (type, setting) {
          * @return {Array} 加载到树的数据
          * @privateA
          */
-        _updateStageData (datas) {
+        _updateStageData(datas) {
             datas = datas instanceof Array ? datas : [datas];
             const loadedData = [];
             for (const data of datas) {
@@ -1195,7 +1195,7 @@ const createNewPathTree = function (type, setting) {
         /**
          * 构造函数
          */
-        constructor (setting) {
+        constructor(setting) {
             super(setting);
             // 关联索引
             this.masterItems = {};
@@ -1204,7 +1204,7 @@ const createNewPathTree = function (type, setting) {
          * 加载数据(初始化), 并给数据添加部分树结构必须数据
          * @param datas
          */
-        loadDatas (datas) {
+        loadDatas(datas) {
             super.loadDatas(datas);
             // 清空旧数据
             this.masterItems = {};
@@ -1235,7 +1235,7 @@ const createNewPathTree = function (type, setting) {
          */
         loadMinorData(datas, fieldSuf, fields, calcFields) {
             for (const cf of calcFields) {
-                this.setting.calcFields.push(cf+fieldSuf);
+                this.setting.calcFields.push(cf + fieldSuf);
             }
             if (!datas) return;
             datas = datas instanceof Array ? datas : [datas];
@@ -1289,7 +1289,7 @@ const createNewPathTree = function (type, setting) {
     }
 
     class GatherTree extends BaseTree {
-        clearDatas () {
+        clearDatas() {
             this.items = {};
             this.nodes = [];
             this.datas = [];
@@ -1340,7 +1340,7 @@ const createNewPathTree = function (type, setting) {
                     return fun(a[field], b[field]);
                 });
                 for (const [i, node] of nodes.entries()) {
-                    node.order = i+1;
+                    node.order = i + 1;
                 }
             };
             const addSortNodes = function (nodes) {
@@ -1364,6 +1364,88 @@ const createNewPathTree = function (type, setting) {
         }
     }
 
+    class CompareTree extends FxTree {
+
+        constructor(setting) {
+            super(setting);
+            this._newId = 1;
+        }
+
+        get newId() {
+            return this._newId++;
+        }
+
+        loadCompareNode(node, parent, loadFun) {
+            const siblings = parent ? parent.children : this.children;
+            let cur = siblings.find(function (x) {
+                return node.b_code
+                    ? x.b_code === node.b_code && x.name === node.name && x.unit === node.unit && x.unit_price === node.unit_price
+                    : x.code === node.code && x.name === node.name;
+            });
+            if (!cur) {
+                const id = this.newId;
+                cur = {
+                    id: id,
+                    pid: parent ? parent.id : this.setting.rootId,
+                    full_path: parent ? parent.full_path + '-' + id : '' + id,
+                    level: parent ? parent.level + 1 : 1,
+                    order: siblings.length + 1,
+                    children: [],
+                    code: node.code, b_code: node.b_code, name: node.name,
+                    unit: node.unit, unit_price: node.unit_price,
+                };
+                siblings.push(cur);
+                this.datas.push(cur);
+            }
+            loadFun(cur, node);
+            for (const c of node.children) {
+                this.loadCompareNode(c, cur, loadFun);
+            }
+        }
+    
+        generateSortNodes() {
+            const self = this;
+            const addSortNode = function (node) {
+                self.nodes.push(node);
+                for (const c of node.children) {
+                    addSortNode(c);
+                }
+            }
+            this.nodes = [];
+            for (const n of this.children) {
+                addSortNode(n);
+            }
+        }
+    
+        loadCompareTree(data, loadFun) {
+            for (const c of data.billsTree.children) {
+                this.loadCompareNode(c, null, loadFun);
+            }
+            // todo load Pos Data;
+        }
+    
+        calculateDiffer() {
+            if (this.setting.calcDiffer) {
+                for (const d of this.datas) {
+                    this.setting.calcDiffer(d);
+                }
+            }
+        }
+    
+        loadCompareData(data1, data2) {
+            this.loadCompareTree(data1, this.setting.loadInfo1);
+            this.loadCompareTree(data2, this.setting.loadInfo2);
+            for (const d of this.datas) {
+                d.is_leaf = d.children.length === 0;
+                d.expanded = true;
+                d.visible = true;
+                this.items[itemsPre + d[this.setting.id]] = d;
+            }
+            this.generateSortNodes();
+            this.calculateDiffer();
+        }
+    }
+
     if (type === 'base') {
         return new BaseTree(setting);
     } else if (type === 'fx') {
@@ -1382,6 +1464,8 @@ const createNewPathTree = function (type, setting) {
         return new FilterTree(setting);
     } else if (type === 'gather') {
         return new GatherTree(setting);
+    } else if (type === 'compare') {
+        return new CompareTree(setting);
     }
 };
 
@@ -1402,7 +1486,7 @@ const treeCalc = {
         return [maxLevel, map];
     },
     getMaxLevel: function (tree) {
-        return Math.max.apply(Math, tree.datas.map(function(o) {return o.level}));
+        return Math.max.apply(Math, tree.datas.map(function (o) { return o.level }));
     },
     calculateNode: function (tree, node) {
         if (node.children && node.children.length > 0) {

+ 8 - 0
app/router.js

@@ -80,6 +80,7 @@ module.exports = app => {
     app.post('/list/add', sessionAuth, 'tenderController.addTender');
     app.post('/list/update', sessionAuth, 'tenderController.updateTender');
     app.post('/list/del', sessionAuth, 'tenderController.deleteTender');
+
     // 标段概况
     app.get('/tender/:id', sessionAuth, tenderCheck, 'tenderController.tenderInfo');
     app.get('/tender/:id/type', sessionAuth, 'tenderController.tenderType');
@@ -95,6 +96,7 @@ module.exports = app => {
     app.post('/tender/:id/ledger/get-children', sessionAuth, tenderCheck, 'ledgerController.getChildren');
     app.post('/tender/:id/ledger/update', sessionAuth, tenderCheck, 'ledgerController.update');
     app.post('/tender/:id/ledger/upload-excel/:ueType', sessionAuth, tenderCheck, 'ledgerController.uploadExcel');
+    //app.post('/tender/:id/ledger/upload-excel', sessionAuth, tenderCheck, 'ledgerController.uploadExcel');
     app.get('/tender/:id/ledger/download/:file', sessionAuth, tenderCheck, 'ledgerController.download');
     //app.post('/tender/:id/pos', sessionAuth, tenderCheck, 'ledgerController.pos');
     app.post('/tender/:id/pos/update', sessionAuth, tenderCheck, 'ledgerController.posUpdate');
@@ -167,6 +169,8 @@ module.exports = app => {
     app.post('/tender/:id/measure/stage/:order/detail/save', sessionAuth, tenderCheck, stageCheck, 'stageController.saveDetailData');
     app.post('/tender/:id/measure/stage/:order/detail/add-img', sessionAuth, tenderCheck, stageCheck, 'stageController.addCalcImage');
     app.post('/tender/:id/measure/stage/:order/detail/merge-img', sessionAuth, tenderCheck, stageCheck, 'stageController.mergeCalcImage');
+    // 其他台账
+    //app.get('/tender/:id/measure/stage/:order/extra', sessionAuth, tenderCheck, stageCheck, 'stageController.extra');
     // 合同支付
     app.get('/tender/:id/measure/stage/:order/pay', sessionAuth, tenderCheck, stageCheck, 'stageController.pay');
     app.post('/tender/:id/measure/stage/:order/pay/detail', sessionAuth, tenderCheck, stageCheck, 'stageController.chapterDetail');
@@ -265,4 +269,8 @@ module.exports = app => {
 
     // 示例
     app.get('/template/:file', sessionAuth, 'templateController.download');
+
+    // 标段对比
+    app.get('/compare/tz', sessionAuth, 'spssController.compareTz');
+    app.post('/compare/tz/load', sessionAuth, 'spssController.loadCompareTz');
 };

+ 53 - 0
app/view/spss/compare_tz.ejs

@@ -0,0 +1,53 @@
+<% include ./sub_menu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main d-flex">
+            <% include ./sub_mini_menu.ejs %>
+            <div>
+                <div class="d-inline-block">
+                    <div class="dropdown">
+                        <button class="btn btn-sm btn-light dropdown-toggle text-primary" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                            <i class="fa fa-list-ol"></i> 显示层级
+                        </button>
+                        <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
+                            <a class="dropdown-item" name="showLevel" tag="1" href="javascirpt: void(0);">第一层</a>
+                            <a class="dropdown-item" name="showLevel" tag="2" href="javascirpt: void(0);">第二层</a>
+                            <a class="dropdown-item" name="showLevel" tag="3" href="javascirpt: void(0);">第三层</a>
+                            <a class="dropdown-item" name="showLevel" tag="4" href="javascirpt: void(0);">第四层</a>
+                            <a class="dropdown-item" name="showLevel" tag="5" href="javascirpt: void(0);">第五层</a>
+                            <a class="dropdown-item" name="showLevel" tag="last" href="javascirpt: void(0);">最底层</a>
+                            <a class="dropdown-item" name="showLevel" tag="leafXmj" href="javascirpt: void(0);">只显示项目节</a>
+                        </div>
+                    </div>
+                </div>
+                <div class="d-inline-block">
+                    <button href="#compare-select" class="btn btn-sm btn-light text-primary" data-toggle="modal" data-target="#compare-select"><i class="fa fa-clone"></i> 选择比较标段</button>
+                </div>
+            </div>
+            <div class="ml-auto"></div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="c-header p-0"></div>
+        <div class="c-body">
+            <div class="sjs-height-1" id="bills-spread">
+            </div>
+            <div class="bcontent-wrap" id="main-bottom">
+                <div id="main-resize" class="resize-y"  r-Type="height" div1="#bills-spread" div2="#main-bottom" store-id="compare-main" store-version="1.0.0" min="100"></div>
+                <div class="bc-bar mb-1">
+                    <ul class="nav nav-tabs">
+                        <li class="nav-item">
+                            <a class="nav-link active" data-toggle="tab" href="#xmujie" role="tab">计量单元</a>
+                        </li>
+                    </ul>
+                </div>
+                <div class="tab-content">
+                    <div class="tab-pane active" id="xmujie">
+                        <div class="sp-wrap" id="pos-spread">
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>

+ 26 - 0
app/view/spss/compare_tz_modal.ejs

@@ -0,0 +1,26 @@
+<!--弹出对比标段-->
+<div class="modal fade" id="compare-select" data-backdrop="static">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">选择对比标段</h5>
+            </div>
+            <div class="modal-body">
+                <div class="form-group">
+                    <label>对比标段1<b class="text-danger">*</b></label>
+                    <input class="form-control form-control-sm"  placeholder="输入标段1的ID" type="text" name="id1">
+                    <input class="form-control form-control-sm"  placeholder="输入标段1的对比期序号" type="text" name="stage1" style="display: none;">
+                </div>               
+                <div class="form-group">
+                    <label>对比标段2<b class="text-danger">*</b></label>
+                    <input class="form-control form-control-sm"  placeholder="输入标段2的ID" type="text" name="id2">
+                    <input class="form-control form-control-sm"  placeholder="输入标段2的对比期序号" type="text" name="stage1" style="display: none;">
+                </div>               
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
+                <button type="button" class="btn btn-primary btn-sm" id="compare-ok">确定</button>
+            </div>
+        </div>
+    </div>
+</div>

+ 32 - 0
app/view/spss/sub_menu.ejs

@@ -0,0 +1,32 @@
+<div class="panel-sidebar" id="sub-menu">
+    <div class="panel-title">
+        <div class="title-bar">
+            <h2 class="text-truncate" style="white-space:nowrap; overflow:hidden; text-overflow:ellipsis;" data-toggle="tooltip" data-placement="right" title=""  data-original-title="统计分析">统计分析</h2>
+        </div>
+    </div>
+    <div class="scrollbar-auto">
+        <div class="nav-box">
+            <h3><i class="fa fa-list-alt"></i> 审核比较</h3>
+            <ul class="nav-list list-unstyled">
+                <li class="<% if (ctx.url === '/gather/tz') { %>active<% } %>">
+                    <a href="/compare/tz"><span class="ml-3">台账对比</span></a>
+                </li>
+                <li class="<% if (ctx.url === '/gather/tz') { %>active<% } %>">
+                    <a href="/compare/stage"><span class="ml-3">期计量对比</span></a>
+                </li>
+            </ul>
+        </div>
+        <div class="nav-box">
+            <h3><i class="fa fa-list-alt"></i> 标段汇总</h3>
+            <ul class="nav-list list-unstyled">
+                <li class="<% if (ctx.url === '/gather/tz') { %>active<% } %>">
+                    <a href="/gather/tz"><span class="ml-3">台账汇总</span></a>
+                </li>
+                <li class="<% if (ctx.url === '/gather/tz') { %>active<% } %>">
+                    <a href="/gather/stage"><span class="ml-3">期计量汇总</span></a>
+                </li>
+            </ul>
+        </div>
+        <div class="side-fold"><a href="javascript: void(0)" data-toggle="tooltip" data-placement="top" data-original-title="折叠侧栏" id="to-mini-menu"><i class="fa fa-upload fa-rotate-270"></i></a></div>
+    </div>
+</div>

+ 32 - 0
app/view/spss/sub_mini_menu.ejs

@@ -0,0 +1,32 @@
+<!--折起的菜单-->
+<div class="min-side" id="sub-mini-menu" style="display: none;">
+    <div id="sub-mini-hint" class="side-switch" data-container="body" data-toggle="popover" data-placement="bottom" data-content="这里打开收起的菜单栏"></div>
+    <div class="side-switch">
+        <i class="fa fa-bars"></i>
+    </div>
+    <div class="side-menu" id="mini-menu-list" style="display: none">
+        <div class="nav-box">
+            <h3><i class="fa fa-list-alt"></i> 审核比较</h3>
+            <ul class="nav-list list-unstyled">
+                <li class="<% if (ctx.url === '/compare/tz') { %>active<% } %>">
+                    <a href="/compare/tz"><span class="ml-3">台账对比</span></a>
+                </li>
+                <li class="<% if (ctx.url === '/compare/stage') { %>active<% } %>">
+                    <a href="/compare/stage"><span class="ml-3">期计量对比</span></a>
+                </li>
+            </ul>
+        </div>
+        <div class="nav-box">
+            <h3><i class="fa fa-list-alt"></i> 标段汇总</h3>
+            <ul class="nav-list list-unstyled">
+                <li class="<% if (ctx.url === '/gather/tz') { %>active<% } %>">
+                    <a href="/gather/tz"><span class="ml-3">台账汇总</span></a>
+                </li>
+                <li class="<% if (ctx.url === '/gather/tz') { %>active<% } %>">
+                    <a href="/gather/stage"><span class="ml-3">期计量汇总</span></a>
+                </li>
+            </ul>
+        </div>
+        <div class="side-fold"><a href="javascript: void(0);" data-toggle="tooltip" data-placement="top" data-original-title="展开侧栏" id="to-menu"><i class="fa fa-upload fa-rotate-90"></i></a></div>
+    </div>
+</div>

+ 5 - 0
app/view/tender/sub_menu.ejs

@@ -31,6 +31,11 @@
                 <!--</div>-->
             <!--</div>-->
         </div>
+        <% if (ctx.app.config.is_debug) { %>
+        <div>
+            <a href="/compare/tz" class="btn btn-sm btn-primary pull-right" target="_blank">统计分析</a>
+        </div>
+        <% } %>
         <% if (userPermission !== null && userPermission.tender !== undefined && userPermission.tender.indexOf('1') !== -1) { %>
         <div>
             <a href="#add-bd" name="add" data-toggle="modal" data-target="#add-bd" class="btn btn-sm btn-primary pull-right">新建标段</a>

+ 33 - 0
config/web.js

@@ -272,6 +272,22 @@ const JsFiles = {
                 ],
                 mergeFile: 'stage_detail',
             },
+            extra: {
+                files: [
+                    "/public/js/spreadjs/sheets/v11/gc.spread.sheets.all.11.2.2.min.js",
+                    "/public/js/decimal.min.js",
+                ],
+                mergeFiles: [
+                    "/public/js/sub_menu.js",
+                    "/public/js/div_resizer.js",
+                    "/public/js/spreadjs_rela/spreadjs_zh.js",
+                    "/public/js/zh_calc.js",
+                    "/public/js/path_tree.js",
+                    "/public/js/stage_extra.js",
+                    "/public/js/stage_audit.js",
+                ],
+                mergeFile: 'stage_extra',
+            },
             pay: {
                 files: [
                     "/public/js/spreadjs/sheets/v11/gc.spread.sheets.all.11.2.2.min.js",
@@ -405,6 +421,23 @@ const JsFiles = {
                 mergeFile: 'material_list',
             },
         },
+        compare: {
+            tz: {
+                files: [
+                    "/public/js/spreadjs/sheets/v11/gc.spread.sheets.all.11.2.2.min.js",
+                    "/public/js/decimal.min.js",
+                ],
+                mergeFiles: [
+                    "/public/js/sub_menu.js",
+                    "/public/js/div_resizer.js",
+                    "/public/js/spreadjs_rela/spreadjs_zh.js",
+                    "/public/js/zh_calc.js",
+                    "/public/js/path_tree.js",
+                    "/public/js/compare_tz.js",
+                ],
+                mergeFile: 'compare_tz',
+            },
+        }
     }
 
 };