浏览代码

动态决算,造价对比相关

MaiXinRong 2 年之前
父节点
当前提交
705f2fcad7
共有 4 个文件被更改,包括 273 次插入23 次删除
  1. 15 0
      app/public/css/main.css
  2. 122 18
      app/public/js/budget_compare.js
  3. 72 5
      app/public/js/spreadjs_rela/spreadjs_zh.js
  4. 64 0
      app/view/budget/compare.ejs

+ 15 - 0
app/public/css/main.css

@@ -2072,4 +2072,19 @@ animation:shake 1s .2s ease both;}
 }
 .signatureCavans{
   background: transparent!important;
+}
+.span-grey{
+    background: #657798;
+}
+.span-red{
+    background: #EE6666;
+}
+.span-blue{
+    background: #74CBED;
+}
+.span-yellow{
+    background: #FAC858;
+}
+.span-green{
+    background: #62DAAB;
 }

+ 122 - 18
app/public/js/budget_compare.js

@@ -9,6 +9,9 @@
  */
 
 $(document).ready(() => {
+    const compareTypeKey = 'budget-compareType';
+    const stackedBarCoverKey = 'budget-stackedBarCover';
+    const stackedBarKey = 'budget-stackedBar-' + window.location.pathname.split('/')[2];
     autoFlashHeight();
     const compareSpread = SpreadJsObj.createNewSpread($('#cost-compare')[0]);
     const compareSheet = compareSpread.getActiveSheet();
@@ -18,15 +21,16 @@ $(document).ready(() => {
             {title: '项目节编号', colSpan: '1', rowSpan: '2', field: 'code', hAlign: 0, width: 150, formatter: '@', cellType: 'tree'},
             {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: '投资估算|数量1/数量2', colSpan: '3|1', rowSpan: '1|1', field: 'gu_dgn_qty', hAlign: 2, width: 80},
-            {title: '|经济指标', colSpan: '|1', rowSpan: '|1', field: 'gu_dgn_price', hAlign: 2, width: 80, type: 'Number'},
-            {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'gu_tp', hAlign: 2, width: 80, type: 'Number'},
-            {title: '初步概算|数量1/数量2', colSpan: '3|1', rowSpan: '1|1', field: 'gai_dgn_qty', hAlign: 2, width: 80},
-            {title: '|经济指标', colSpan: '|1', rowSpan: '|1', field: 'gai_dgn_price', hAlign: 2, width: 80, type: 'Number'},
-            {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'gai_tp', hAlign: 2, width: 80, type: 'Number'},
-            {title: '施工图预算|数量1/数量2', colSpan: '3|1', rowSpan: '1|1', field: 'yu_dgn_qty', hAlign: 2, width: 80},
-            {title: '|经济指标', colSpan: '|1', rowSpan: '|1', field: 'yu_dgn_price', hAlign: 2, width: 80, type: 'Number'},
-            {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'yu_tp', hAlign: 2, width: 80, type: 'Number'},
+            {title: '投资估算|数量1/数量2', colSpan: '3|1', rowSpan: '1|1', field: 'gu_dgn_qty', hAlign: 2, width: 80, bc_type: 'number'},
+            {title: '|经济指标', colSpan: '|1', rowSpan: '|1', field: 'gu_dgn_price', hAlign: 2, width: 80, type: 'Number', bc_type: 'number'},
+            {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'gu_tp', hAlign: 2, width: 80, type: 'Number', bc_type: 'number'},
+            {title: '初步概算|数量1/数量2', colSpan: '3|1', rowSpan: '1|1', field: 'gai_dgn_qty', hAlign: 2, width: 80, bc_type: 'number'},
+            {title: '|经济指标', colSpan: '|1', rowSpan: '|1', field: 'gai_dgn_price', hAlign: 2, width: 80, type: 'Number', bc_type: 'number'},
+            {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'gai_tp', hAlign: 2, width: 80, type: 'Number', bc_type: 'number'},
+            {title: '施工图预算|数量1/数量2', colSpan: '3|1', rowSpan: '1|1', field: 'yu_dgn_qty', hAlign: 2, width: 80, bc_type: 'number'},
+            {title: '|经济指标', colSpan: '|1', rowSpan: '|1', field: 'yu_dgn_price', hAlign: 2, width: 80, type: 'Number', bc_type: 'number'},
+            {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'yu_tp', hAlign: 2, width: 80, type: 'Number', bc_type: 'number'},
+            {title: '数据对比', colSpan: '1', rowSpan: '2', field: 'stackedBar', hAlign: 0, width: 300, cellType: 'stackedBar', stackedBarCover: false, bc_type: 'grid', visible: false},
         ],
         emptyRows: 0,
         headRows: 2,
@@ -41,28 +45,38 @@ $(document).ready(() => {
 
     };
     sjsSettingObj.setFxTreeStyle(spreadSetting, sjsSettingObj.FxTreeStyle.jz);
-    SpreadJsObj.initSheet(compareSheet, spreadSetting);
-
     let sfSelect;
     const compareObj = {
         curFinalId() {
             return this.finalInfo ? this.finalInfo.id : undefined;
         },
         initFinalCol() {
-            if (spreadSetting.cols.length < 13) {
+            if (spreadSetting.cols.length < 14) {
+                spreadSetting.cols.pop();
                 spreadSetting.cols.push(...[
-                    {title: '台账|数量1/数量2', colSpan: '3|1', rowSpan: '1|1', field: 'dgn_qty', hAlign: 2, width: 80},
-                    {title: '|经济指标', colSpan: '|1', rowSpan: '|1', field: 'dgn_price', hAlign: 2, width: 80, type: 'Number'},
-                    {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'total_price', hAlign: 2, width: 80, type: 'Number'},
-                    {title: '决算|数量1/数量2', colSpan: '3|1', rowSpan: '1|1', field: 'final_dgn_qty', hAlign: 2, width: 80},
-                    {title: '|经济指标', colSpan: '|1', rowSpan: '|1', field: 'final_dgn_price', hAlign: 2, width: 80, type: 'Number'},
-                    {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'final_tp', hAlign: 2, width: 80, type: 'Number'},
+                    {title: '台账|数量1/数量2', colSpan: '3|1', rowSpan: '1|1', field: 'dgn_qty', hAlign: 2, width: 80, bc_type: 'number'},
+                    {title: '|经济指标', colSpan: '|1', rowSpan: '|1', field: 'dgn_price', hAlign: 2, width: 80, type: 'Number', bc_type: 'number'},
+                    {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'total_price', hAlign: 2, width: 80, type: 'Number', bc_type: 'number'},
+                    {title: '决算|数量1/数量2', colSpan: '3|1', rowSpan: '1|1', field: 'final_dgn_qty', hAlign: 2, width: 80, bc_type: 'number'},
+                    {title: '|经济指标', colSpan: '|1', rowSpan: '|1', field: 'final_dgn_price', hAlign: 2, width: 80, type: 'Number', bc_type: 'number'},
+                    {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'final_tp', hAlign: 2, width: 80, type: 'Number', bc_type: 'number'},
+                    {title: '数据对比', colSpan: '1', rowSpan: '2', field: 'stackedBar', hAlign: 0, width: 300, cellType: 'stackedBar', stackedBarCover: false, bc_type: 'grid', visible: false},
                     {title: '增幅%|数量1/数量2', colSpan: '2|1', rowSpan: '1|1', field: 'grow_dgn_qty', hAlign: 2, width: 80},
                     {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'grow_tp', hAlign: 2, width: 80, type: 'Number'},
                 ]);
+                this.initShowType();
                 SpreadJsObj.reLoadSheetHeader(compareSheet);
             };
         },
+        initShowType() {
+            const type = this.compareType;
+            spreadSetting.cols.forEach(x => {
+                if (!x.bc_type) return;
+                x.visible = x.bc_type === type;
+            });
+            const colIndex = spreadSetting.cols.findIndex(x => { return x.field === 'stackedBar'});
+            spreadSetting.cols[colIndex].stackedBarCover = parseInt(this.stackedBarCover);
+        },
         expand(tree, tag) {
             switch (tag) {
                 case "1":
@@ -77,6 +91,42 @@ $(document).ready(() => {
                     break;
             }
         },
+        getStackedBarField () {
+            let stackedBarCache = getLocalCache(stackedBarKey);
+            if (stackedBarCache === null) stackedBarCache = 'gai_tp,total_price,final_tp';
+            return stackedBarCache ? stackedBarCache.split(',') : [];
+        },
+        calcStackedBar(tree) {
+            const calcField = this.getStackedBarField();
+            const calcFieldColor = { 'gu_tp': '#657798', 'gai_tp': '#EE6666', 'yu_tp': '#74CBED', 'total_price': '#FAC858', 'final_tp': '#62DAAB' };
+            const calc = function(node, base){
+                // const parent = tree.getParent(node);
+                // if (!parent) {
+                //     base = 0;
+                //     for (const cf of calcField) {
+                //         base = Math.max(node[cf], base);
+                //     }
+                // }
+                node.stackedBar = [];
+                for (const cf of calcField) {
+                    node.stackedBar.push({color: calcFieldColor[cf], percent: ZhCalc.div(node[cf], base), field: cf});
+                }
+                if (node.children) {
+                    for (const child of node.children) {
+                        calc(child, base);
+                    }
+                }
+            };
+            let commonBase = 0;
+            tree.children.forEach(x => {
+                for (const cf of calcField) {
+                    commonBase = Math.max(x[cf], commonBase);
+                }
+            });
+            for (const child of tree.children) {
+                calc(child, commonBase);
+            }
+        },
         loadBudgetData(result) {
             const compareTree = createNewPathTree('final', {
                 id: 'id',
@@ -136,6 +186,7 @@ $(document).ready(() => {
             });
             const expandTag = getLocalCache('revise-compare-level');
             if (expandTag) compareObj.expand(compareTree, expandTag);
+            this.calcStackedBar(compareTree);
             SpreadJsObj.loadSheetData(compareSheet, SpreadJsObj.DataType.Tree, compareTree);
         },
         loadFinalData(result, msg) {
@@ -153,10 +204,44 @@ $(document).ready(() => {
             finalTree.loadDatas(result.final);
             const expandTag = getLocalCache('revise-compare-level');
             if (expandTag) compareObj.expand(finalTree, expandTag);
+            this.calcStackedBar(finalTree);
             SpreadJsObj.loadSheetData(compareSheet, SpreadJsObj.DataType.Tree, finalTree);
             if (sfSelect) sfSelect.reloadSelect(this.finalInfo.tender);
         },
+        loadCacheData(){
+            let stackedBarCache = getLocalCache(stackedBarKey);
+            if (stackedBarCache === null) stackedBarCache = 'gai_tp,total_price,final_tp';
+            this.stackedBarField = stackedBarCache ? stackedBarCache.split(',') : [];
+            this.compareType = getLocalCache(compareTypeKey);
+            this.stackedBarCover = getLocalCache(stackedBarCoverKey);
+            this.initShowType();
+        },
+        setStackedBarField(field){
+            this.stackedBarField = field;
+            setLocalCache(stackedBarKey, field.join(','));
+            this.calcStackedBar(compareSheet.zh_tree);
+            const colIndex = compareSheet.zh_setting.cols.findIndex(x => { return x.field === 'stackedBar'});
+            SpreadJsObj.reloadColData(compareSheet, colIndex);
+        },
+        setCompareType(type) {
+            this.compareType = type;
+            setLocalCache(compareTypeKey, type);
+            spreadSetting.cols.forEach(x => {
+                if (!x.bc_type) return;
+                x.visible = x.bc_type === type;
+            });
+            SpreadJsObj.refreshColumnVisible(compareSheet);
+        },
+        setStackedBarCover(cover){
+            this.stackedBarCover = cover;
+            setLocalCache(stackedBarCoverKey, cover);
+            const colIndex = compareSheet.zh_setting.cols.findIndex(x => { return x.field === 'stackedBar'});
+            compareSheet.zh_setting.cols[colIndex].stackedBarCover = parseInt(cover);
+            SpreadJsObj.reloadColData(compareSheet, colIndex);
+        }
     };
+    compareObj.loadCacheData();
+    SpreadJsObj.initSheet(compareSheet, spreadSetting);
 
     function compareCode(str1, str2, symbol = '-') {
         if (!str1) {
@@ -322,4 +407,23 @@ $(document).ready(() => {
     $('#select-final').on('shown.bs.modal', () => {
         if (!sfSelect) sfSelect = new sfObject();
     });
+    $('#stackedBar-ok').click(function() {
+        const checked = $('[name=stackedBar]:checked');
+        const field = [];
+        checked.each((i, x) => { field.push(x.value)});
+        compareObj.setStackedBarField(field);
+    });
+    $('#dp-stackedBar').click(function() {
+        const field = compareObj.getStackedBarField();
+        const checked = $('[name=stackedBar]');
+        checked.each((i, x) => { x.checked = field.indexOf(x.value) >= 0; });
+    });
+    $('a[name=showType]').click(function () {
+        const type = this.getAttribute('tag');
+        compareObj.setCompareType(type);
+    });
+    $('a[name=stackedBarCover]').click(function() {
+        const cover = this.getAttribute('tag');
+        compareObj.setStackedBarCover(cover);
+    })
 });

+ 72 - 5
app/public/js/spreadjs_rela/spreadjs_zh.js

@@ -307,7 +307,7 @@ const SpreadJsObj = {
         const setting = sheet.zh_setting;
         if (!setting || !setting.cols) { return; }
 
-
+        sheet.setColumnCount(0);
         sheet.setColumnCount(setting.cols.length);
         sheet.setRowCount(setting.headRows, spreadNS.SheetArea.colHeader);
         for (let iRow = 0; iRow < setting.headRowHeight.length; iRow ++) {
@@ -372,7 +372,6 @@ const SpreadJsObj = {
                 const col = _.find(setting.cols, {field: prop});
                 if (col) col.width = setting.localCache.colWidthCache[prop];
             }
-            this._rememberColWidth(sheet);
         }
     },
     /**
@@ -384,6 +383,7 @@ const SpreadJsObj = {
         const self = this;
         this.beginMassOperation(sheet);
         this._loadCacheSetting(sheet, setting);
+        this._rememberColWidth(sheet);
         setting.pos = sheet.getParent().pos;
         if (!setting.tree) setting.tree = {};
         sheet.zh_setting = setting;
@@ -411,6 +411,7 @@ const SpreadJsObj = {
     reLoadSheetHeader: function (sheet) {
         if (sheet.zh_setting) {
             this.beginMassOperation(sheet);
+            this._loadCacheSetting(sheet, sheet.zh_setting);
             this._initSheetHeader(sheet);
             this.endMassOperation(sheet);
         }
@@ -718,6 +719,13 @@ const SpreadJsObj = {
             }
             sheet.getRange(-1, col, -1, 1).cellType(sheet.extendCellType.mouseTouch);
         }
+        if (colSetting.cellType === 'stackedBar') {
+            if (!sheet.extendCellType.stackedBar) {
+                sheet.extendCellType.stackedBar = this.CellType.getStackedBarCellType();
+                SpreadJsObj._addActivePaintEvents(sheet, sheet.extendCellType.stackedBar);
+            }
+            sheet.getRange(-1, col, -1, 1).cellType(sheet.extendCellType.stackedBar);
+        }
         if (colSetting.formatter) {
             sheet.getRange(-1, col, -1, 1).formatter(colSetting.formatter);
             if(colSetting.type === 'Number') {
@@ -2258,13 +2266,13 @@ const SpreadJsObj = {
                         ctx.save();
                         ctx.rect(x, y, w, h);
                         ctx.clip();
-                        ctx.drawImage(img, x + 2, y + 2)
+                        ctx.drawImage(img, x + 2, y + 2);
                         ctx.restore();
                         cell.tag(null);
                         return;
                     }
                     catch (err) {
-                        GC.Spread.Sheets.CustomCellType.prototype.paint.apply(this, [ctx, "#HTMLError", x, y, w, h, style, context])
+                        GC.Spread.Sheets.CustomCellType.prototype.paint.apply(this, [ctx, "#HTMLError", x, y, w, h, style, context]);
                         cell.tag(null);
                         return;
                     }
@@ -2526,7 +2534,66 @@ const SpreadJsObj = {
                 return false;
             };
             return new mouseTouchCellType();
-        }
+        },
+        getStackedBarCellType: function () {
+            const stackedBarCellType = function(){};
+            stackedBarCellType.prototype = new spreadNS.CellTypes.Text();
+            const indent = 3, defaultR = 3, minHeight = 2;
+            /**
+             * 画一个长条
+             * @param {Object} canvas - 画布
+             * @param {Object} rect - 方框区域
+             * @param {String} lineColor - 画线颜色
+             * @param {String} fillColor - 填充颜色
+             */
+            const drawBar = function (canvas, x, y, w, h, r, color) {
+                canvas.save();
+                // 设置偏移量
+                canvas.translate(0.5, 0.5);
+                canvas.strokeStyle = color;
+                canvas.beginPath();
+                canvas.roundRect(x, y, w, h, r);
+                canvas.stroke();
+                canvas.fillStyle = color;
+                canvas.fill();
+                canvas.restore();
+            };
+            const proto = stackedBarCellType.prototype;
+            /**
+             * 绘制方法
+             * @param {Object} canvas - 画布
+             * @param value - cell.value
+             * @param {Number} x - 单元格左顶点坐标 x
+             * @param {Number} y - 单元格左顶点坐标 y
+             * @param {Number} w - 单元格宽度
+             * @param {Number} h - 单元格高度
+             * @param {Object} style - cell.style
+             * @param {Object} options
+             */
+            proto.paint = function (canvas, value, x, y, w, h, style, options) {
+                // 清理 画布--单元格范围 旧数据
+                canvas.save();
+                canvas.fillStyle = style.backColor || 'white';
+                canvas.fillRect(x, y, w, h);
+                canvas.restore();
+
+                const col = options.sheet.zh_setting.cols[options.col];
+                const barData = value;
+                const left = x + indent;
+                const startTop = y + indent;
+                const validHeight = h - indent - indent - (col.stackedBarCover || barData.length === 1 ? 1 : barData.length / 2 + 1);
+                const height = col.stackedBarCover || barData.length === 0 ? validHeight : Math.max(validHeight/barData.length, minHeight);
+                const validWidth = w - indent - indent - 1;
+                for (const [i, bd] of barData.entries()) {
+                    let width = ZhCalc.mul(validWidth, bd.percent, 2);
+                    if (width < defaultR) continue;
+                    const top = col.stackedBarCover ? startTop : startTop + height * i + i - 1;
+                    console.log(top, height, y, h);
+                    drawBar(canvas, left, top, width, height, defaultR, bd.color);
+                }
+            };
+            return new stackedBarCellType();
+        },
     },
 
     RowHeader: {

+ 64 - 0
app/view/budget/compare.ejs

@@ -6,6 +6,17 @@
             <div>
                 <div class="d-inline-block">
                     <div class="dropdown">
+                        <button class="btn btn-sm btn-light dropdown-toggle text-primary" type="button" id="dp-type" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                            <i class="fa fa-list-ol"></i> 数据形式
+                        </button>
+                        <div class="dropdown-menu" aria-labelledby="dp-type">
+                            <a class="dropdown-item" name="showType" tag="number" href="javascript: void(0);">详细数据</a>
+                            <a class="dropdown-item" name="showType" tag="grid" href="javascript: void(0);">柱状图表</a>
+                        </div>
+                    </div>
+                </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>
@@ -24,6 +35,59 @@
                 </div>
                 <div class="d-inline-block ml-2" id="final-info">
                 </div>
+                <div class="d-inline-block ml-auto">
+                    <div class="d-inline-block">
+                        <div class="dropdown">
+                            <button class="btn btn-sm btn-light dropdown-toggle text-primary" type="button" id="dp-stackedBar" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
+                                <i class="fa fa-list"></i> 图表数据
+                            </button>
+                            <div class="dropdown-menu pb-1" aria-labelledby="dp-stackedBar" x-placement="bottom-start" style="position: absolute; transform: translate3d(0px, 26px, 0px); top: 0px; left: 0px; will-change: transform;">
+                                <div class="px-3">
+                                    <div class="form-check py-1">
+                                        <input class="form-check-input" name="stackedBar" type="checkbox" id="stackedBarGu" value="gu_tp">
+                                        <label class="form-check-label" for="stackedBarGu">估算</label>
+                                        <span class="ml-1 span-grey px-2">&nbsp;&nbsp;&nbsp;</span>
+                                    </div>
+                                    <div class="form-check py-1">
+                                        <input class="form-check-input" name="stackedBar" type="checkbox" id="stackedBarGai" value="gai_tp" checked="">
+                                        <label class="form-check-label" for="stackedBarGai">概算</label>
+                                        <span class="ml-1 span-red px-2">&nbsp;&nbsp;&nbsp;</span>
+                                    </div>
+                                    <div class="form-check py-1">
+                                        <input class="form-check-input" name="stackedBar" type="checkbox" id="stackedBarYu" value="yu_tp">
+                                        <label class="form-check-label" for="stackedBarYu">预算</label>
+                                        <span class="ml-1 span-blue px-2">&nbsp;&nbsp;&nbsp;</span>
+                                    </div>
+                                    <div class="form-check py-1">
+                                        <input class="form-check-input" name="stackedBar" type="checkbox" id="stackedBarLedger" value="total_price" checked="">
+                                        <label class="form-check-label" for="stackedBarLedger">台账</label>
+                                        <span class="ml-1 span-yellow px-2">&nbsp;&nbsp;&nbsp;</span>
+                                    </div>
+                                    <div class="form-check py-1">
+                                        <input class="form-check-input" name="stackedBar" type="checkbox" id="stackedBarFinal" value="final_tp" checked="">
+                                        <label class="form-check-label" for="stackedBarFinal">决算</label>
+                                        <span class="ml-1 span-green px-2">&nbsp;&nbsp;&nbsp;</span>
+                                    </div>
+                                    <hr class="m-1">
+                                    <div class="float-right">
+                                        <button type="button" class="btn btn-sm btn-primary" id="stackedBar-ok">确定</button>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="d-inline-block">
+                        <div class="dropdown">
+                            <button class="btn btn-sm btn-light dropdown-toggle text-primary" type="button" id="dp-cover" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                                <i class="fa fa-list-ol"></i> 图表样式
+                            </button>
+                            <div class="dropdown-menu" aria-labelledby="dp-cover">
+                                <a class="dropdown-item" name="stackedBarCover" tag="0" href="javascript: void(0);">堆叠</a>
+                                <a class="dropdown-item" name="stackedBarCover" tag="1" href="javascript: void(0);">覆盖</a>
+                            </div>
+                        </div>
+                    </div>
+                </div>
             </div>
         </div>
     </div>