Jelajahi Sumber

Merge branch 'master' into 分摊功能

zhangweicheng 5 tahun lalu
induk
melakukan
7b6cdc9da6
35 mengubah file dengan 1266 tambahan dan 422 penghapusan
  1. 2 1
      modules/all_models/user.js
  2. 7 2
      modules/main/facade/ration_facade.js
  3. 99 0
      modules/main/middleware/index.js
  4. 0 52
      modules/main/middleware/system_setting.js
  5. 1 1
      modules/main/routes/ration_route.js
  6. 7 2
      modules/pm/facade/pm_facade.js
  7. 1 1
      modules/pm/routes/pm_route.js
  8. 4 1
      modules/users/controllers/login_controller.js
  9. 20 3
      modules/users/models/user_model.js
  10. 17 0
      public/common_util.js
  11. 5 5
      public/web/commonAlert.js
  12. 3 1
      public/web/common_ajax.js
  13. 81 2
      public/web/id_tree.js
  14. 91 0
      public/web/sheet/sheet_common.js
  15. 9 0
      public/web/socket/connection.js
  16. 15 10
      public/web/syntax-detection.js
  17. 9 37
      server.js
  18. 5 0
      socket.js
  19. 16 1
      web/building_saas/css/custom.css
  20. 78 0
      web/building_saas/main/js/controllers/project_controller.js
  21. 31 0
      web/building_saas/main/js/models/cache_tree.js
  22. 45 32
      web/building_saas/main/js/models/calc_base.js
  23. 34 52
      web/building_saas/main/js/models/ration.js
  24. 12 3
      web/building_saas/main/js/views/billsElf.js
  25. 171 134
      web/building_saas/main/js/views/project_view.js
  26. 52 2
      web/building_saas/main/js/views/std_billsGuidance_lib.js
  27. 6 0
      web/building_saas/pm/js/pm_gc.js
  28. 63 14
      web/building_saas/pm/js/pm_newMain.js
  29. 1 1
      web/building_saas/pm/js/pm_share.js
  30. 33 0
      web/building_saas/pm/js/pm_tree.js
  31. 3 0
      web/common/html/header.html
  32. 320 0
      web/over_write/hunan_2020.js
  33. 12 29
      web/over_write/js/anhui_2019.js
  34. 8 21
      web/over_write/js/neimeng_2019.js
  35. 5 15
      web/over_write/js/zhejiang_2005.js

+ 2 - 1
modules/all_models/user.js

@@ -111,6 +111,7 @@ let schema = {
         type: Number,
         default: 0
     },
-    welcomeShowTime:String
+    welcomeShowTime:String,
+    token: String
 };
 mongoose.model(collectionName, new Schema(schema, {versionKey: false}));

+ 7 - 2
modules/main/facade/ration_facade.js

@@ -228,12 +228,17 @@ async function createNewMaterialRation(std,quantityDecimal,projectID){
 }
 
 async function addMultiRation(datas,compilation) {
-    let rst = [];
+    /* let rst = [];
     for(let data of datas){
         let r = await addNewRation(data,compilation);
         rst.push(r);
     }
-    return rst;
+    return rst; */
+    const task = [];
+    for (const data of datas) {
+        task.push(addNewRation(data, compilation));
+    }
+    return await Promise.all(task);
 }
 
 async function getSameSectionRations(data,userId,compilationId){

+ 99 - 0
modules/main/middleware/index.js

@@ -0,0 +1,99 @@
+/**
+ * Created by zhang on 2020/1/8.
+ */
+
+module.exports = {
+    rationNumberChecking,
+    tenderNumberChecking,
+    stateChecking,
+};
+
+const mongoose = require("mongoose");
+const rationModel = mongoose.model("ration");
+const pmFacade = require('../../pm/facade/pm_facade');
+const online_logs = require('../../../logs/online_logs');
+import UserModel from '../../../modules/users/models/user_model';
+
+async function rationNumberChecking(req, res, next) {
+    if (req.session.systemSetting) {
+        let type = req.session.compilationVersion.indexOf("免费") == -1 ? "professional" : "normal";
+        let data = req.body.data;
+        if (typeof data === 'object') {
+            data = JSON.stringify(data);
+        }
+        data = JSON.parse(data);
+        let projectID = data.projectID;
+        let no = await rationModel.find({ projectID: projectID }).count();
+        if (no >= req.session.systemSetting[type].ration) {
+            let result = { error: 1, message: "您套用定额个数超限,请联系我们的客服人员。" };
+            return res.json(result);
+        }
+    }
+    next();
+}
+
+async function tenderNumberChecking(req, res, next) {
+    const data = JSON.parse(req.body.data);
+    const tenderCount = data.tenderCount;
+    if (tenderCount) {
+        const tenderOverrun = await pmFacade.isTenderOverrun(tenderCount, req.session);
+        if (tenderOverrun) {
+            return res.json({
+                error: 1,
+                message: '您创建的项目个数超限,请联系我们的客服人员,或者导出建设项目保存到本地备份,删除云上数据。'
+            });
+        }
+    }
+    next();
+}
+
+function isAjax(req) {
+    return req.headers['x-requested-with'] === 'XMLHttpRequest';
+}
+
+// 登录状态全局判断
+async function stateChecking(req, res, next) {
+    const url = req.originalUrl;
+    if (url == "\/" || /^\/login/.test(url) || /\.map|\.ico$/.test(url) || /^\/sms/.test(url) || /^\/cld/.test(url) || /^\/captcha/.test(url)) {
+        // 如果是登录页面或短信接口或cld接口则忽略判断数据
+        next();
+    } else {
+        try {
+            if (req.query.ssoID !== undefined && req.query.ssoID !== null && req.query.token !== undefined && req.query.token !== null) {
+                delete req.session.sessionUser;
+                delete req.session.sessionCompilation;
+                return res.redirect('/login' + url);
+            } else {
+                // 判断session
+                const sessionUser = req.session.sessionUser;
+                if (!sessionUser) {
+                    //处理 ajax 请求 session 过期问题
+                    if (isAjax(req)) {
+                        return res.json({ ret_code: 99, ret_msg: '登录信息失效,请您重新登录' });
+                    } else {
+                        throw 'session error';
+                    }
+                } else {
+                    const userModel = new UserModel();
+                    const isValidToken = await userModel.checkToken(sessionUser.id, sessionUser.token);
+                    if (!isValidToken) {
+                        delete req.session.sessionUser;
+                        delete req.session.sessionCompilation;
+                        if (isAjax(req)) {
+                            return res.json({ ret_code: 99, ret_msg: '' });
+                        } else {
+                            throw 'session token invalid';
+                        }
+                    }
+                }
+                res.locals.sessionUser = sessionUser;
+            }
+        } catch (error) {
+            // 最后一个页面存入session
+            req.session.lastPage = url;
+            return res.redirect('/login');
+        }
+        next();
+        await online_logs.saveOnlineTime(req);//记录登录时长
+    }
+}

+ 0 - 52
modules/main/middleware/system_setting.js

@@ -1,52 +0,0 @@
-/**
- * Created by zhang on 2020/1/8.
- */
-
-module.exports={
-    getSystemSetting,
-    rationNumberChecking:rationNumberChecking,
-    tenderNumberChecking
-};
-
-let mongoose = require("mongoose");
-let rationModel = mongoose.model("ration");
-const systemSettingModel = mongoose.model('system_setting');
-const pmFacade = require('../../pm/facade/pm_facade');
-
-// 获取系统设置,这个系统设置正常情况下有存在session中
-async function getSystemSetting() {
-    return await systemSettingModel.findOne({}).lean();
-}
-
-async function rationNumberChecking(req, res, next) {
-    if(req.session.systemSetting){
-        let type = req.session.compilationVersion.indexOf("免费") == -1?"professional":"normal";
-        let data = req.body.data;
-        if(typeof data === 'object'){
-            data = JSON.stringify(data);
-        }
-        data = JSON.parse(data);
-        let projectID = data.projectID;
-        let no = await rationModel.find({projectID:projectID}).count();
-        if(no >= req.session.systemSetting[type].ration){
-            let  result = {error:1,message:"您套用定额个数超限,请联系我们的客服人员。"};
-            return  res.json(result);
-        }
-    }
-    next();
-}
-
-async function tenderNumberChecking(req, res, next) {
-    const data = JSON.parse(req.body.data);
-    const tenderCount = data.tenderCount;
-    if (tenderCount) {
-        const tenderOverrun = await pmFacade.isTenderOverrun(tenderCount, req.session);
-        if (tenderOverrun) {
-            return res.json({
-                error: 1,
-                message: '您创建的项目个数超限,请联系我们的客服人员,或者导出建设项目保存到本地备份,删除云上数据。'
-            });
-        }
-    }
-    next();
-}

+ 1 - 1
modules/main/routes/ration_route.js

@@ -2,7 +2,7 @@
  * Created by jimiz on 2017/4/7.
  */
 let express = require('express');
-let ss_middleware = require("../middleware/system_setting");
+let ss_middleware = require("../middleware/index");
 
 module.exports = function (app) {
     let rationRouter = express.Router();

+ 7 - 2
modules/pm/facade/pm_facade.js

@@ -106,7 +106,7 @@ let qiniu = require("qiniu");
 let fs = require("fs");
 let path = require("path");
 let request = require("request");
-const systemSettingMiddleware = require('../../main/middleware/system_setting');
+const systemSettingModel = mongoose.model('system_setting');
 
 let qiniu_config = {
     "AccessKey": "_gR1ed4vi1vT2G2YITGSf4_H0fJu_nRS9Tzk3T4z",
@@ -2059,6 +2059,11 @@ function uploadToken() {
     return result
 }
 
+// 获取系统设置,这个系统设置正常情况下有存在session中
+async function getSystemSetting() {
+    return await systemSettingModel.findOne({}).lean();
+}
+
 // 有些方法无法通过中间件就检查单位工程数量是否超限
 // 需要到具体的业务代码中进行判断
 // 这个方法就是具体业务代码中,需要检查单位工程数量是否超限用
@@ -2066,7 +2071,7 @@ async function isTenderOverrun(tenderCount, session) {
     const userID = session.sessionUser.id;
     const compilation = session.sessionCompilation._id;
     const compilationVersion = session.compilationVersion || '免费';
-    let systemSetting = session.systemSetting || (session.systemSetting = await systemSettingMiddleware.getSystemSetting());
+    let systemSetting = session.systemSetting || (session.systemSetting = await getSystemSetting());
     // 这种情况只有在刚上线此功能时会出现,不考虑时间差
     if (!systemSetting) {
         return false;

+ 1 - 1
modules/pm/routes/pm_route.js

@@ -6,7 +6,7 @@ import BaseController from "../../common/base/base_controller";
 let express = require('express');
 let pmController = require('./../controllers/pm_controller');
 const baseController = new BaseController();
-const systemMiddleware = require('../../main/middleware/system_setting');
+const systemMiddleware = require('../../main/middleware/index');
 
 module.exports = function (app) {
 

+ 4 - 1
modules/users/controllers/login_controller.js

@@ -16,6 +16,7 @@ const moment = require('moment');
 const Captcha = require("../models/captcha");
 let mongoose = require("mongoose");
 let systemSettingModel = mongoose.model("system_setting");
+const uuidV1 = require('uuid/v1');
 
 class LoginController {
 
@@ -71,6 +72,7 @@ class LoginController {
                     mobile: userData.mobile,
                     qq: userData.qq,
                     isUserActive: userData.isUserActive,
+                    token: uuidV1(),
                 };
 
                 request.session.sessionUser = sessionUser;
@@ -241,7 +243,8 @@ class LoginController {
                 mobile: userData.mobile,
                 qq: userData.qq,
                 isUserActive: userData.isUserActive,
-                newLogin:true
+                newLogin:true,
+                token: uuidV1(),
             };
 
             request.session.sessionUser = sessionUser;

+ 20 - 3
modules/users/models/user_model.js

@@ -185,6 +185,7 @@ class UserModel extends BaseModel {
                 qq: userData.qq,
                 latest_login: userData.latest_login,
                 isUserActive: userData.isUserActive,
+                token: userData.token,
             };
             let updateResult = await this.updateUser(condition, UpdateData);
             if (updateResult.ok === 1) {
@@ -256,9 +257,24 @@ class UserModel extends BaseModel {
      * @param {string} ssoId
      * @return {object}
      */
-    async findDataById(id) {
-        let objId = mongoose.Types.ObjectId(id);
-        return await this.db.findOne({ _id: objId });
+    async findDataById(id, fields) {
+        const objId = mongoose.Types.ObjectId(id);
+        return fields ? await this.db.findOne({_id: objId}, fields) : await this.db.findOne({_id: objId});
+    }
+
+    /**
+     * 验证用户token正确性
+     * 一个账号不允许多处同时在线,每次登陆都会更新session和数据库的token,每个请求都会比对session和数据库的token
+     * @param {String} id - 用户ID
+     * @param {String} token - 登陆生成的token
+     * @return {Boolean}
+     */
+    async checkToken(id, token) {
+        const user = await this.findDataById(id, '-_id token');
+        if (!user.token) { // 兼容第一次上线,已登陆的用户还没token,需要返回验证正确
+            return true;
+        }
+        return user.token === token;
     }
 
     async findDataByAccount(account) {
@@ -289,6 +305,7 @@ class UserModel extends BaseModel {
             create_time: new Date().getTime(),
             latest_login: new Date().getTime(),
             isUserActive: userData.isUserActive,
+            token: userData.token
         };
         return this.db.create(insertData);
     }

+ 17 - 0
public/common_util.js

@@ -44,6 +44,22 @@
         }
     }
 
+    // 控制全屏(浏览器有限制)
+    // Element.requestFullscreen的全屏和“F11”的全屏是不一样的。前者是将相关Element变成全屏显示。后者是将浏览器导航、标签等隐藏。
+    // Fullscreen API对于全屏的判断和监听都是基于Element.requestFullscreen的,比如Document.fullscreenElement。通过F11触发的全屏Document.fullscreenElement返回null,无法正确返回全屏状态。
+    // F11全屏后,无法通过Fullscreen API对全屏状态判断,会导致F11全屏后点击按钮变成了再次调用api全屏。因此,使用window.innerHeight和window.screen.height作为判断。(打开了控制台后,此方法可能会失效:无法正确或缺innerHeight)
+    // 通过F11打开全屏后,没有办法通过代码退出全屏,只能通过F11退出:
+    // https://stackoverflow.com/questions/51114885/combining-requestfullscreen-and-f11; https://stackoverflow.com/questions/43392583/fullscreen-api-not-working-if-triggered-with-f11/44368592#44368592;
+    function handleFullscreen() {
+        const isFullscreen = window.innerHeight === window.screen.height;
+        if (isFullscreen) {
+            const p = document.exitFullscreen();
+            p.catch(() => alert('按F11即可退出全屏模式'));
+        } else {
+            document.documentElement.requestFullscreen();
+        }
+    }
+
     // 给数值加上分割
     // eg: 1234567.00 => 1,234,567.00
     function standardNumber(str) {
@@ -70,6 +86,7 @@
         isDef,
         isEmptyVal,
         getSortedTreeData,
+        handleFullscreen,
         standardNumber,
     };
 });

+ 5 - 5
public/web/commonAlert.js

@@ -8,14 +8,14 @@
  * @version
  */
 
-window.alert = function(str) {
-    /*$('#commonAlert').find('p').text(str);
-     $('#commonAlert').modal('show');*/
-    hintBox.infoBox('系统提示', str, 1);
+window.alert = function (str) {
+    if (str) {
+        hintBox.infoBox('系统提示', str, 1);
+    }
 };
 
 
-!function loadHintBox(){
+!function loadHintBox() {
     $("body").append('<div id = "hintBox_container"></div>');
     $("#hintBox_container").load("../../public/scHintBox.html");
 }();

+ 3 - 1
public/web/common_ajax.js

@@ -154,7 +154,9 @@ var CommonAjax = {
 $.ajaxSetup({
     complete: function (data) {
         if (data.responseJSON&&data.responseJSON.ret_code && data.responseJSON.ret_code == 99) {
-            alert(data.responseJSON.ret_msg);
+            if (data.responseJSON.ret_msg) {
+                alert(data.responseJSON.ret_msg);
+            }
             var top = getTopWindow();
             setTimeout('top.location.href = "/login";', 300);
         }

+ 81 - 2
public/web/id_tree.js

@@ -611,7 +611,7 @@ var idTree = {
             }
             return data;
         };
-        Tree.prototype.insertByData = function (data, parentID, nextSiblingID, uid = null) {
+        Tree.prototype.insertByData = function (data, parentID, nextSiblingID, uid = null, resort = true) {
             var parent = parentID == -1 ? null : this.nodes[this.prefix + parentID];
             var nextSibling = nextSiblingID == -1 ? null : this.nodes[this.prefix + nextSiblingID];
             var node = this.nodes[this.prefix + data[this.setting.id]];
@@ -625,13 +625,45 @@ var idTree = {
                     tools.addNodes(this, parent, [node]);
                 }
                 this.nodes[this.prefix +  data[this.setting.id]] = node;
-                tools.sortTreeItems(this);
+                if (resort) {
+                    tools.sortTreeItems(this);
+                }
                 if(!uid){
                     this.maxNodeID( data[this.setting.id]);
                 }
                 return node;
             }
         };
+        Tree.prototype.multiInsert = function (datas, preID) {
+            const newNodes = [];
+            for (const data of datas) {
+                this.nodes[this.prefix + data.ID] = new Node(this, data.ID);
+                this.nodes[this.prefix + data.ID]['data'] = data;
+                newNodes.push(this.nodes[this.prefix + data.ID]);
+            }
+            const fisrtNode = this.nodes[this.prefix + datas[0].ID];
+            const firstPre = this.nodes[this.prefix + preID];
+            if (firstPre) {
+                firstPre.nextSibling = fisrtNode;
+                firstPre.data.NextSiblingID = fisrtNode.getID();
+            }
+            const parent = this.nodes[this.prefix + datas[0].ParentID] || null;
+            const parentChildren = parent ? parent.children : this.roots;
+            let baseIndex = firstPre ? parentChildren.indexOf(firstPre) + 1 : 0;
+            datas.forEach(data => {
+                const node = this.nodes[this.prefix + data.ID];
+                node.parent = parent;
+                parentChildren.splice(baseIndex++, 0, node);
+                const next = this.nodes[this.prefix + data.NextSiblingID] || null;
+                node.nextSibling = next;
+                if (next) {
+                    next.preSibling = node;
+                }
+            });
+            this.roots = tools.reSortNodes(this.roots, true);
+            tools.sortTreeItems(this);
+            return newNodes;
+        }
         //批量新增节点,节点已有树结构数据
         Tree.prototype.insertByDatas = function (datas) {
             for(let data of datas){
@@ -897,6 +929,53 @@ var idTree = {
             tools.addUpdateDataForNextSibling(data, lastNode.nextSibling, firstNode.getID());
             return data;
         };
+          //检查树结构数据有没问题
+        Tree.prototype.check = function (roots) {
+            return isValid(roots);
+            function isValid(nodes) {
+                for (let node of nodes) {
+                    if (node.data.ParentID !== -1 &&
+                        (!node.parent || node.parent.data.ID !== node.data.ParentID)) {
+                        console.log(`${node.serialNo() + 1}:${node.data.name} parent对应错误`);
+                        return false;
+                    }
+                    if (node.data.ParentID === -1 && node.parent) {
+                        console.log(`${node.serialNo() + 1}:${node.data.name} 不应有parent`);
+                        return false;
+                    }
+                    if (node.data.NextSiblingID !== -1 &&
+                        (!node.nextSibling || node.nextSibling.data.ID !== node.data.NextSiblingID)) {
+                        console.log(`${node.serialNo() + 1}:${node.data.name} next对应错误`);
+                        return false;
+                    }
+                    if (node.data.NextSiblingID === -1 && node.nextSibling) {
+                        console.log(`${node.serialNo() + 1}:${node.data.name} 不应有next`);
+                        return false;
+                    }
+                    let sameDepthNodes = node.parent ? node.parent.children : roots,
+                        nodeIdx = sameDepthNodes.indexOf(node),
+                        nextIdx = sameDepthNodes.indexOf(node.nextSibling);
+                    if (nodeIdx !== -1 && nextIdx !== -1 && nodeIdx > nextIdx) {
+                        console.log(`${node.serialNo() + 1}:${node.data.name} node索引大于next索引`);
+                        return false;
+                    }
+                    // nextSibling跟parent children的下一节点对应不上
+                    if (nodeIdx !== -1 && 
+                        (nodeIdx === sameDepthNodes.length - 1 && nextIdx !== -1) || 
+                        (nodeIdx !== sameDepthNodes.length - 1 && nodeIdx + 1 !== nextIdx)) {
+                        console.log(`${node.serialNo() + 1}:${node.data.name} nextSibling与树显示的下一节点对应不上`);
+                        return false;
+                    }
+                    if (node.children.length) {
+                        let v = isValid(node.children);
+                        if (!v) {
+                            return false;
+                        }
+                    }
+                }
+                return true;
+            }
+        };
         return new Tree(setting);
     },
     updateType: {update: 'update', new: 'new', delete: 'delete'}

+ 91 - 0
public/web/sheet/sheet_common.js

@@ -1249,5 +1249,96 @@ var sheetCommonObj = {
             sheet.getCell(row, col).wordWrap(wordWrap);
             sheet.autoFitRow(row);
         });
+    },
+    // 获取带输入框的右键子目
+    registerInputContextMenuItem(name, html, icon, callback) {
+        $.contextMenu.types[name] = function (item, opt, root) {
+            // 因为contextMenu有自己的键盘事件处理,因此输入框的键盘控制光标需要自己定义事件实现覆盖。
+            const Direction = {
+                BACKWARD: 'backward',
+                FORWARD: 'forward'
+            };
+            function moveLeft(input, isShifting) {
+                const start = input.selectionStart;
+                const end = input.selectionEnd;
+                const direction = end === start ? Direction.BACKWARD : input.selectionDirection;
+                if (isShifting) {
+                    if (direction === Direction.FORWARD) {
+                        const curEnd = end - 1;
+                        input.setSelectionRange(start, curEnd);
+                    } else {
+                        const curStart = start - 1 < 0 ? 0 : start - 1;
+                        input.setSelectionRange(curStart, end, Direction.BACKWARD);
+                    }
+                } else {
+                    const idx = end > start
+                        ? start
+                        : start - 1 < 0 
+                            ? 0 
+                            : start - 1;
+                    input.setSelectionRange(idx, idx);
+                }
+            }
+            function moveRight(input, isShifting) {
+                const start = input.selectionStart;
+                const end = input.selectionEnd;
+                const direction = start === end ? Direction.FORWARD : input.selectionDirection;
+                if (isShifting) {
+                    if (direction === Direction.BACKWARD) {
+                        const curStart = start + 1;
+                        input.setSelectionRange(curStart, end);
+                    } else {
+                        const curEnd = end + 1;
+                        input.setSelectionRange(start, curEnd, Direction.FORWARD);
+                    }
+                } else {
+                    const idx = start < end
+                        ? end
+                        : end + 1
+                    input.setSelectionRange(idx, idx);
+                }
+            }
+            function handleConfirm() {
+                if (callback) {
+                    callback();
+                }
+                root.$menu.trigger('contextmenu:hide');
+            }
+            $(html)
+                .appendTo(this)
+                .on('input', 'input', function () {
+                    const number = +$(this).val();
+                    if (isNaN(number)) {
+                        $(this).val(1);
+                    } else if (number > 99) {
+                        $(this).val(99);
+                    }
+                })
+                .on('keydown', 'input', function (e) {
+                    const key = e.key;
+                    if (key === 'ArrowUp' || key === 'ArrowDown') {
+                        return false;
+                    }
+                    if (key === 'Enter') {
+                        handleConfirm();
+                        return false;
+                    }
+                    const input = $(this)[0];
+                    if (key === 'ArrowLeft') {
+                        moveLeft(input, e.shiftKey);
+                    } else if (key === 'ArrowRight') {
+                        moveRight(input, e.shiftKey);
+                    }
+                })
+                .parent().on('click', function (e) {
+                    if (e.target.tagName === 'INPUT') {
+                        return false;
+                    }
+                    handleConfirm();
+                });
+
+            this.addClass(`context-menu-icon context-menu-icon--fa fa ${icon}`);
+        };
+        return name;
     }
 }

+ 9 - 0
public/web/socket/connection.js

@@ -92,6 +92,15 @@ socketObject = {
         socket.on('fileDataChange', function (data) {//收到单价文件、费率文件内容修改、文件切换、另存(暂时能共用,以后有需要可分离)推送消息
             if (data.projectID && typeof projTreeObj !== 'undefined') projTreeObj.refreshWhenFileDateChange(data.projectID);
         });
+        // 项目管理树数据发生变化,提示刷新
+        socket.on('pmTreeChange', function ({ expandState, selection }) {
+            const isActive = $('#tab_pm_all').hasClass('active');
+            if (isActive) {
+                $("#message").html(`树结构发生变化,请<a href="javascript:void(0);" id="load-data">点击刷新列表</a>`);
+                $('#load-data').on('click', () => projTreeObj.handleNotifyClick(expandState, selection));
+                $("#notify").show();
+            }
+        });
     },
     getFeeRateRoomID: function () {
         return projectObj.project.FeeRate.getActivateFeeRateFileID();

+ 15 - 10
public/web/syntax-detection.js

@@ -150,6 +150,11 @@ function checkSyntax() {
             }
         }
 
+        // DOM
+        if (typeof document.documentElement.requestFullscreen !== 'function') {
+            throw new TypeError('document.documentElement.requestFullscreen is not a function');
+        }
+
     } catch (err) {
         console.log(err);
         return false;
@@ -161,16 +166,16 @@ function showBrowserTip() {
     var html = '<div class="modal fade" id="browser" data-backdrop="static">' +
         '<div class="modal-dialog modal-lg" role="document">' +
         '<div class="modal-content">' +
-            '<div class="modal-body">' +
-                '<h5>浏览器版本过低,可能会有安全风险;</h5>' +
-                '<h5>请更新 「浏览器」 或者 使用 「纵横Z+造价工作平台」 登录。</h5>' +
-                '<div class="row my-4">' +
-                    '<div class="col-6">' +
-                        '<div class="text-center"><a href="https://www.microsoft.com/zh-cn/edge" class="btn btn-primary" target="_blank">下载 Microsoft Edge</a></div>' +
-                    '</div>' +
-                    '<div class="col-6">' +
-                        '<div class="text-center"><a href="https://smartcost.com.cn/downloadzplus" class="btn btn-primary" target="_blank">下载 纵横Z+造价工作平台</a></div>' +
-                    '</div></div></div></div></div></div>';
+        '<div class="modal-body">' +
+        '<h5>浏览器版本过低,可能会有安全风险;</h5>' +
+        '<h5>请更新 「浏览器」 或者 使用 「纵横Z+造价工作平台」 登录。</h5>' +
+        '<div class="row my-4">' +
+        '<div class="col-6">' +
+        '<div class="text-center"><a href="https://www.microsoft.com/zh-cn/edge" class="btn btn-primary" target="_blank">下载 Microsoft Edge</a></div>' +
+        '</div>' +
+        '<div class="col-6">' +
+        '<div class="text-center"><a href="https://smartcost.com.cn/downloadzplus" class="btn btn-primary" target="_blank">下载 纵横Z+造价工作平台</a></div>' +
+        '</div></div></div></div></div></div>';
     $('body').append(html);
     $('#browser').modal('show');
 }

+ 9 - 37
server.js

@@ -23,7 +23,6 @@ fileUtils.getGlobbedFiles('./modules/all_models/*.js').forEach(function(modelPat
 //config.setupCache();
 let cfgCacheUtil = require("./config/cacheCfg");
 cfgCacheUtil.setupDftCache();
-let online_logs = require("./logs/online_logs");
 
 
 let app = express();
@@ -56,43 +55,16 @@ app.use(session({
 }));
 
 // 登录状态全局判断
-app.use(async function (req, res, next) {
-    let url = req.originalUrl;
-    // if (/^\/login/.test(url) || /\.map|\.ico$/.test(url) || /^\/sms/.test(url) || /^\/cld/.test(url) || /^\/captcha/.test(url)  || /^\/accountIsPro/.test(url)) {
-    if (/^\/login/.test(url) || /\.map|\.ico$/.test(url) || /^\/sms/.test(url) || /^\/cld/.test(url) || /^\/captcha/.test(url)) {
-        // 如果是登录页面或短信接口或cld接口则忽略判断数据
-        next();
-    } else {
-        try {
-            if (req.query.ssoID !== undefined && req.query.ssoID !== null && req.query.token !== undefined && req.query.token !== null) {
-                delete req.session.sessionUser;
-                delete req.session.sessionCompilation;
-                return res.redirect('/login' + url);
-            } else {
-                // 判断session
-                let sessionUser = req.session.sessionUser;
-                if (!sessionUser) {
-
-                    //处理 ajax 请求 session 过期问题
-                    if (req.headers["x-requested-with"] != null
-                        && req.headers["x-requested-with"] == "XMLHttpRequest"
-                        && req.url != "/login") {
-                        return res.json({ret_code: 99, ret_msg: '登录信息失效,请您重新登录'});
-                    } else {
-                        throw 'session error';
-                    }
-                }
-                res.locals.sessionUser = sessionUser;
-            }
-        } catch (error) {
-            // 最后一个页面存入session
-            req.session.lastPage = url;
-            return res.redirect('/login');
-        }
-        await online_logs.saveOnlineTime(req);//记录登录时长
-        next();
-    }
+/* const { stateChecking } = require('./modules/main/middleware/index');
+app.use(stateChecking); */
+app.use(function (req, res, next) {
+    // 在内部在调用,而不直接在外部require后直接作为中间件回调函数app.use(stateChecking);的原因:
+    // 由于各模块的引用不全是require,有些是import,import是预编译的。外部引用中间件可能会造成一些引用丢失。如:project_model.js中gljFacade.addMixRatioForNew会丢失
+    // 不用过分担心性能问题。require有cache机制
+    const { stateChecking } = require('./modules/main/middleware/index');
+    stateChecking(req, res, next);
 });
+
 //加载路由文件
 fileUtils.getGlobbedFiles('./modules/**/routes/*.js').forEach(function(modelPath) {
     if(modelPath.indexOf("import/routes")==-1) require(path.resolve(modelPath))(app);//排除掉导出导入服务

+ 5 - 0
socket.js

@@ -123,6 +123,11 @@ socketIO.on('connection', function(socket) {
         }
     });
 
+    // 项目管理树结构变化
+    socket.on('pmTreeChange', function ({ userID, compilationID, expandState, selection }) {
+        socket.broadcast.to(`${userID}@${compilationID}`).emit('pmTreeChange', { expandState, selection });
+    });
+
     socket.on('disconnect', function () {
         // 由于用户可以重复打开项目,因此不做唯一性处理,只删除一个数据,不删除所有的同用户数据
         if (curProjectID && userCache[curProjectID]) {

+ 16 - 1
web/building_saas/css/custom.css

@@ -460,4 +460,19 @@ input.text-right{
     left: 50% !important;
     transform: translateX(-50%) !important;
     width: 123% !important;
-}
+}
+
+/* 右键菜单input */
+.menu-input {
+    width: 2.5rem;
+    border: 1px solid rgb(221, 221, 221);
+    border-radius: 2px;
+    height: 1.3rem;
+    text-align: center;
+}
+.menu-input:focus {
+    outline: none;
+}
+/* .menu-input::selection {
+    background-color: rgb(41, 128, 185);
+} */

+ 78 - 0
web/building_saas/main/js/controllers/project_controller.js

@@ -54,6 +54,84 @@ ProjectController = {
             cbTools.refreshFormulaNodes();
         });
     },
+    /* addBillsByData: async function (postData, insertFunc) {
+        await ajaxPost('/bills/insertBills', { postData });
+        // 插入
+        const insertData = postData.filter(item => item.updateType === 'create');
+        const treeData = insertData.map(item => item.updateData);
+        // 插入清单节点
+        if (!insertFunc) {
+            insertFunc = projectObj.project.Bills.tree.insertByDatas;
+        }
+        projectObj.project.Bills.tree.insertByDatas(treeData);
+        projectObj.project.Bills.datas = projectObj.project.Bills.datas.concat(treeData);
+        // 插入主树节点
+        const newNodes = projectObj.project.mainTree.insertByDatas(treeData);
+        for (const node of newNodes) {
+            node.source = projectObj.project.Bills.tree.nodes[projectObj.project.Bills.tree.prefix + node.getID()];
+            node.data = node.source.data;
+            node.sourceType = projectObj.project.Bills.getSourceType();
+        }
+        ProjectController.syncDisplayNewNodes(projectObj.mainController, newNodes, true);
+        return newNodes;
+    }, */
+    addBillsByData: async function (postData, isSameDepth = false) {
+        await ajaxPost('/bills/insertBills', { postData });
+        // 插入
+        const insertData = postData.filter(item => item.updateType === 'create');
+        const treeData = insertData.map(item => item.updateData);
+        // 插入清单节点和主树节点
+        projectObj.project.Bills.datas = projectObj.project.Bills.datas.concat(treeData);
+        let newNodes;
+        if (isSameDepth) {
+            const pre = postData.find(item => item.updateType === 'update');
+            const preID = pre && pre.updateData.ID || null;
+            projectObj.project.Bills.tree.multiInsert(treeData, preID);
+            newNodes = projectObj.project.mainTree.multiInsert(treeData, preID);
+        } else {
+            projectObj.project.Bills.tree.insertByDatas(treeData);
+            newNodes = projectObj.project.mainTree.insertByDatas(treeData);
+        }
+        for (const node of newNodes) {
+            node.source = projectObj.project.Bills.tree.nodes[projectObj.project.Bills.tree.prefix + node.getID()];
+            node.data = node.source.data;
+            node.sourceType = projectObj.project.Bills.getSourceType();
+        }
+        ProjectController.syncDisplayNewNodes(projectObj.mainController, newNodes, true);
+        return newNodes;
+    },
+    getBillsPostData: function (number) {
+        const project = projectObj.project;
+        const target = project.getParentTarget(project.mainTree.selected, 'sourceType', project.Bills.getSourceType());
+        const baseParentID = target.depth() === 0 ? target.source.getID() : target.source.getParentID();
+        const baseNextID = target.depth() === 0 ? -1 : target.source.getNextSiblingID();
+        const updateNode = target.depth() === 0 ? target.source.children[target.source.children.length - 1] : target;
+        const insertData = [];
+        for (let i = 0; i < number; i++) {
+            const data = {
+                type: billType.BILL,
+                projectID: project.ID(),
+                ID: uuid.v1(),
+                ParentID: baseParentID,
+            };
+            const pre = insertData[i - 1];
+            if (pre) {
+                pre.NextSiblingID = data.ID;
+            }
+            if (i === number - 1) {
+                data.NextSiblingID = baseNextID;
+            }
+            insertData.push(data);
+        }
+        const postData = insertData.map(item => ({
+            updateType: 'create',
+            updateData: item
+        }));
+        if (updateNode) {
+            postData.push({ updateType: 'update', updateData: { ID: updateNode.getID(), NextSiblingID: insertData[0].ID } });
+        }
+        return postData;
+    },
     addBills: function (project, sheetController, std) {
         if (!project || !sheetController) { return null; }
         let target = project.getParentTarget(project.mainTree.selected, 'sourceType', project.Bills.getSourceType());

+ 31 - 0
web/building_saas/main/js/models/cache_tree.js

@@ -408,6 +408,37 @@ var cacheTree = {
 
             return newNode;
         };
+        //  一次性插入多个连续的同层节点
+        Tree.prototype.multiInsert = function (datas, preID) {
+            const newNodes = [];
+            for (const data of datas) {
+                this.nodes[this.prefix + data.ID] = new Node(this, data.ID);
+                newNodes.push(this.nodes[this.prefix + data.ID]);
+            }
+            const fisrtNode = this.nodes[this.prefix + datas[0].ID];
+            const firstPre = this.nodes[this.prefix + preID];
+            if (firstPre) {
+                firstPre.nextSibling = fisrtNode;
+                firstPre.data.NextSiblingID = fisrtNode.getID();
+            }
+            const parent = this.nodes[this.prefix + datas[0].ParentID] || null;
+            const parentChildren = parent ? parent.children : this.roots;
+            let baseIndex = firstPre ? parentChildren.indexOf(firstPre) + 1 : 0;
+            datas.forEach(data => {
+                const node = this.nodes[this.prefix + data.ID];
+                node.parent = parent;
+                parentChildren.splice(baseIndex++, 0, node);
+                const next = this.nodes[this.prefix + data.NextSiblingID] || null;
+                node.nextSibling = next;
+                if (next) {
+                    next.preSibling = node;
+                }
+            });
+            this.roots = tools.reSortNodes(this.roots, true);
+            this.sortTreeItems();
+            return newNodes;
+        }
+        // 插入某一完整片段到某节点的子项中
         Tree.prototype.insertByDatas = function (datas) {
             let rst = [];
             for(let data of datas){

+ 45 - 32
web/building_saas/main/js/models/calc_base.js

@@ -2,7 +2,6 @@
  * Created by Zhong on 2017/11/28.
  */
 
-
 let cbTools = {
     isDef: function (v) {
         return v !== undefined && v !== null;
@@ -384,6 +383,11 @@ let cbTools = {
              if (node.data.calcBase.hasSubStr(sID)) return true;
         };
     },
+    // 获取直接关联清单节点的基数金额
+    getBaseFee: function (flag, tender, feeField) {
+        const subFeeField = tender ? 'tenderTotalFee' : 'totalFee';
+        return this.getBillsFee(flag, feeField, subFeeField);
+    },
     //获取清单节点的金额
     //@param {Number}fixedFlag(清单固定行类别) {String}feeField(外层金额字段: common) {String}subFeeField(子金额字段: totalFee)
     //@return {Number}
@@ -418,10 +422,17 @@ let cbTools = {
             ? bills.feesIndex[feeField][subFeeField]
             : 0;
     },
-    //获取扣除固定项后的金额,扣除其节点后重新汇总
-    //@param {Number}fixedFlag(基数取值固定行类别) {Array}deductFlags(扣除的固定类别组) {String}fullFeeField(完整的取费字段: 'rationCommon.totalFee')
-    //@return {Number}
-    getFeeWithDeduction: function (fixedFlag, deductFlags, fullFeeField) {
+    /**
+     * 获取扣除固定项后的金额,扣除其节点后重新汇总
+     * @param {Number} fixedFlag - 基数取值固定行类别
+     * @param {Array} deductFlags - 扣除的固定类别组
+     * @param {Boolean} tender - 是否调价
+     * @param {String} feeField - 价格字段
+     * @param {Boolean = true} isRound - 是否取舍
+     * @return {Number}
+     */
+    getFeeWithDeduction: function (fixedFlag, deductFlags, tender, feeField, isRound = true) {
+        const fullFeeField = tender ? `${feeField}.tenderTotalFee` : `${feeField}.totalFee`;
         let baseNode = this.findNodeByFlag(fixedFlag);
         if (!baseNode) {
             return 0;
@@ -434,8 +445,24 @@ let cbTools = {
                 deductNodes.push(node);
             }
         }
-        return projectObj.project.calcProgram.getTotalFee([baseNode], deductNodes, fullFeeField);
+        const fee = projectObj.project.calcProgram.getTotalFee([baseNode], deductNodes, fullFeeField);
+        return isRound ? fee.toDecimal(decimalObj.bills.totalPrice) : fee;
     },
+    /* getFeeWithDeduction: function (fixedFlag, deductFlags, fullFeeField) {
+        let baseNode = this.findNodeByFlag(fixedFlag);
+        if (!baseNode) {
+            return 0;
+        }
+        //要扣除的节点
+        let deductNodes = [];
+        for (let deFlag of deductFlags) {
+            let node = this.findNodeByFlag(deFlag);
+            if (node) {
+                deductNodes.push(node);
+            }
+        }
+        return projectObj.project.calcProgram.getTotalFee([baseNode], deductNodes, fullFeeField);
+    }, */
     //获取累进办法计算的金额
     //@param {Number}baseFee(相关基数金额) {String}name(使用累进计算的基数名称)
     //@return {Number}
@@ -537,65 +564,51 @@ let baseFigureTemplate = {
         //{定额建筑安装工程费(不含定额设备购置费及专项费用)}
         //取清单固定类别是“建筑安装工程”的定额建安费,但要扣除清单固定类别是“设备购置费”、及“专项费用”的定额建安费
         'DEJZAZGCFBHSBZX': function (tender) {
-            let fullFeeField = tender ? 'rationCommon.tenderTotalFee' : 'rationCommon.totalFee',
-                deductFlags = [fixedFlag.EQUIPMENT_ACQUISITION_FEE, fixedFlag.SPECIAL_COST];
-            return cbTools.getFeeWithDeduction(fixedFlag.CONSTRUCTION_INSTALL_FEE, deductFlags, fullFeeField).toDecimal(decimalObj.bills.totalPrice);
+            const deductFlags = [fixedFlag.EQUIPMENT_ACQUISITION_FEE, fixedFlag.SPECIAL_COST];
+            return cbTools.getFeeWithDeduction(fixedFlag.CONSTRUCTION_INSTALL_FEE, deductFlags, tender, 'rationCommon');
         },
         //{定额建筑安装工程(其中定额设备购置费按 40%计)} (定额建筑安装工程设备四十)
         //扣除设备购置费,再加上设备购置费的40%,扣除汇总算法不四舍五入,相当于汇总当中定额设备购置费就按照了40%计
         'DEJZAZGCSBSS': function (tender) {
-            let feeField = 'rationCommon',
-                subFeeField = tender ? 'tenderTotalFee' : 'totalFee',
-                deductFlags = [fixedFlag.EQUIPMENT_ACQUISITION_FEE];
+            const feeField = 'rationCommon';
+            const deductFlags = [fixedFlag.EQUIPMENT_ACQUISITION_FEE];
             //建安费扣除定额设备购置费
-            let afterDeductFee = cbTools.getFeeWithDeduction(fixedFlag.CONSTRUCTION_INSTALL_FEE, deductFlags, `${feeField}.${subFeeField}`);
+            const afterDeductFee = cbTools.getFeeWithDeduction(fixedFlag.CONSTRUCTION_INSTALL_FEE, deductFlags, tender, feeField, false);
             //定额设备购置费
-            let equipmentAcFee = cbTools.getBillsFee(deductFlags[0], feeField, subFeeField);
+            let equipmentAcFee = cbTools.getBaseFee(deductFlags[0], tender, feeField);
             return (afterDeductFee + equipmentAcFee * 0.4).toDecimal(decimalObj.bills.totalPrice);
         },
         //{建筑安装工程费(不含安全生产费)}
         // 取清单固定类别是“建筑安装工程”的金额,但要扣除清单固定类别是“安全生产费”的金额
         'JZAZGCFBHSC': function (tender) {
-            let fullFeeField = tender ? 'common.tenderTotalFee' : 'common.totalFee',
-                deductFlags = [fixedFlag.SAFE_COST];
             //建安费扣除安全生产费
-            return cbTools.getFeeWithDeduction(fixedFlag.CONSTRUCTION_INSTALL_FEE, deductFlags, fullFeeField).toDecimal(decimalObj.bills.totalPrice);
+            return cbTools.getFeeWithDeduction(fixedFlag.CONSTRUCTION_INSTALL_FEE, [fixedFlag.SAFE_COST], tender, 'common');
         },
         //{建筑安装工程费(不含设备费)}
         // 取清单固定类别是“建筑安装工程”的金额,但要扣除清单固定类别是“设备购置费”的金额
         'JZAZGCFBHSB': function (tender) {
-            let fullFeeField = tender ? 'common.tenderTotalFee' : 'common.totalFee',
-                deductFlags = [fixedFlag.EQUIPMENT_ACQUISITION_FEE];
             //建安费扣除设备费
-            return cbTools.getFeeWithDeduction(fixedFlag.CONSTRUCTION_INSTALL_FEE, deductFlags, fullFeeField).toDecimal(decimalObj.bills.totalPrice);
+            return cbTools.getFeeWithDeduction(fixedFlag.CONSTRUCTION_INSTALL_FEE, [fixedFlag.EQUIPMENT_ACQUISITION_FEE], tender, 'common');
         },
         //{建筑安装工程费}
         // 取清单固定类别是“建筑安装工程”的金额
         'JZAZGCF': function (tender) {
-            let feeField = 'common',
-                subFeeField = tender ? 'tenderTotalFee' : 'totalFee';
-            return cbTools.getBillsFee(calcBase.fixedFlag.CONSTRUCTION_INSTALL_FEE, feeField, subFeeField);
+            return cbTools.getBaseFee(calcBase.fixedFlag.CONSTRUCTION_INSTALL_FEE, tender, 'common');
         },
         //{土地使用及拆迁补偿费}
         // 取清单固定类别是“土地使用及拆迁补偿费”的金额
         'TDSYJCQBCF': function (tender) {
-            let feeField = 'common',
-                subFeeField = tender ? 'tenderTotalFee' : 'totalFee';
-            return cbTools.getBillsFee(calcBase.fixedFlag.LAND_USED_DEMOLITION, feeField, subFeeField);
+            return cbTools.getBaseFee(calcBase.fixedFlag.LAND_USED_DEMOLITION, tender, 'common');
         },
         //{养护工程其他费}
         // 取清单固定类别是“养护工程其他费”的金额
         'YHGCQTF': function (tender) {
-            let feeField = 'common',
-                subFeeField = tender ? 'tenderTotalFee' : 'totalFee';
-            return cbTools.getBillsFee(calcBase.fixedFlag.MAINTENANCE_EXPENSES, feeField, subFeeField);
+            return cbTools.getBaseFee(calcBase.fixedFlag.MAINTENANCE_EXPENSES, tender, 'common');
         },
         //{预备费}
         // 取清单固定类别是“预备费”的金额
         'YBF': function(tender) {
-            let feeField = 'common',
-                subFeeField = tender ? 'tenderTotalFee' : 'totalFee';
-            return cbTools.getBillsFee(calcBase.fixedFlag.BUDGET_FEE, feeField, subFeeField);
+            return cbTools.getBaseFee(calcBase.fixedFlag.BUDGET_FEE, tender, 'common');
         },
         //{施工场地建设费}
         //使用累进办法计算,基数为{定额建筑安装工程费(不含定额设备购置费及专项费用)}

+ 34 - 52
web/building_saas/main/js/models/ration.js

@@ -461,7 +461,7 @@ var Ration = {
                 }
             })
         };
-        ration.prototype.addMultiRation = function (items, callback) {
+        ration.prototype.addMultiRation = async function (items) {
             let me = this;
             let project = projectObj.project, sheetController = projectObj.mainController;
             let engineering = projectObj.project.projectInfo.property.engineering;
@@ -511,57 +511,39 @@ var Ration = {
                     }
                     newDatas.push({itemQuery: items[i].itemQuery, newData: newData, defaultLibID: rationLibObj.getDefaultStdRationLibID(), calQuantity: calQuantity, brUpdate: brUpdate, needInstall: needInstall})
                 }
-                let showLoding = true;
-                $.bootstrapLoading.start();
-                //保证由于异步的关系loading界面被隐藏,比如清单指引插入清单定额时,endUpdate中提前隐藏了loading
-                let interval =setInterval(function () {
-                    if(!$.bootstrapLoading.isLoading()&& showLoding){
-                        $.bootstrapLoading.start();
-                        clearInterval(interval);
-                    }
-                    else{
-                        clearInterval(interval);
-                    }
-                }, 100);
-                CommonAjax.post("/ration/addMultiRation",{projectID:me.project.ID(),newDatas: newDatas},function (rstData) {
-                    let newNodes = [];
-                    //更新缓存
-                    for(let data of rstData){
-                        me.datas.push(data.ration);
-                        me.addSubListOfRation(data,false);
-                        //插入树节点
-                        newSource = data.ration;
-                        newNode = project.mainTree.insert(billItemID, nextID, newSource.ID);
-                        newNodes.push(newNode);
-                        newNode.source = newSource;
-                        newNode.sourceType = project.Ration.getSourceType();
-                        newNode.data = newSource;
-                        ProjectController.syncDisplayNewNode(sheetController, newNode);
-                        nextID = project.mainTree.selected.getNextSiblingID();
-                    }
-                    project.projectGLJ.calcQuantity();
-                    for(let data of rstData){
-                        project.ration_glj.addToMainTree(data.ration_gljs);
-                    }
-                    projectObj.mainController.refreshTreeNode(newNodes, false);
-                    if(project.Bills.isFBFX(newNodes[0])) { //判断是否属于分部分项工程 ,是的话才需要做计取安装费计算
-                        project.installation_fee.calcInstallationFee(function (isChange,rations) {
-                            if(isChange){
-                                rations = rations.concat(newNodes);
-                                project.calcProgram.calcNodesAndSave(rations);
-                            }else {
-                                project.calcProgram.calcNodesAndSave(newNodes);
-                            }
-                        });
-                    }else {
-                        project.calcProgram.calcNodesAndSave(newNodes);
-                    }
-                    if(callback){
-                        callback();
-                    }
-                    showLoding = false;
-                    $.bootstrapLoading.end();
-                })
+                const rstData = await ajaxPost('/ration/addMultiRation', { projectID: me.project.ID(), newDatas });
+                let newNodes = [];
+                //更新缓存
+                for(let data of rstData){
+                    me.datas.push(data.ration);
+                    me.addSubListOfRation(data,false);
+                    //插入树节点
+                    newSource = data.ration;
+                    newNode = project.mainTree.insert(billItemID, nextID, newSource.ID);
+                    newNodes.push(newNode);
+                    newNode.source = newSource;
+                    newNode.sourceType = project.Ration.getSourceType();
+                    newNode.data = newSource;
+                    ProjectController.syncDisplayNewNode(sheetController, newNode);
+                    nextID = project.mainTree.selected.getNextSiblingID();
+                }
+                project.projectGLJ.calcQuantity();
+                for(let data of rstData){
+                    project.ration_glj.addToMainTree(data.ration_gljs);
+                }
+                projectObj.mainController.refreshTreeNode(newNodes, false);
+                if(project.Bills.isFBFX(newNodes[0])) { //判断是否属于分部分项工程 ,是的话才需要做计取安装费计算
+                    project.installation_fee.calcInstallationFee(function (isChange,rations) {
+                        if(isChange){
+                            rations = rations.concat(newNodes);
+                            project.calcProgram.calcNodesAndSave(rations);
+                        }else {
+                            project.calcProgram.calcNodesAndSave(newNodes);
+                        }
+                    });
+                }else {
+                    project.calcProgram.calcNodesAndSave(newNodes);
+                }
             }
         };
         ration.prototype.insertVolumePrice = function(type){

+ 12 - 3
web/building_saas/main/js/views/billsElf.js

@@ -732,11 +732,20 @@ const BillsElf = (function() {
     }
     //插入定额
     //@return {void}
-    function insertRations(addRationDatas){
+    async function insertRations(addRationDatas){
         if(addRationDatas.length > 0){
-            projectObj.project.Ration.addMultiRation(addRationDatas, function () {
+            try {
+                $.bootstrapLoading.start();
+                await projectObj.project.Ration.addMultiRation(addRationDatas);
                 projectObj.setActiveCell('quantity', true);
-            });
+            } catch (err) {
+                console.log(err);
+                if (!$('hintBox_form').is(':visible')) {
+                    alert(err);
+                }
+            } finally {
+                $.bootstrapLoading.end();
+            }
         }
     }
     //各监听事件

+ 171 - 134
web/building_saas/main/js/views/project_view.js

@@ -1164,11 +1164,68 @@ var projectObj = {
       } 
       return true
     },
-
+    // 注册自定义插入清单数量
+    registerFlexibleInsertBillMenu: function (name) {
+        const project = projectObj.project;
+        const insertBillsHtml = `<span>${name}&nbsp;&nbsp;<input id='insert-bills-number' class="menu-input" type="text" value="1" onfocus="this.select()">&nbsp;&nbsp;行</span>`;
+        return sheetCommonObj.registerInputContextMenuItem('insertBills', insertBillsHtml, 'fa-sign-in', async function () {
+            if (project.mainTree.selected.data.type == billType.DXFY) {
+                if (project.mainTree.selected.data.calcBase && project.mainTree.selected.data.calcBase != "") {
+                    alert("当前有基数计算,不能插入子项。");
+                    return;
+                }
+            }
+            try {
+                const number = +$('#insert-bills-number').val();
+                if (!number) {
+                    return;
+                }
+                $.bootstrapLoading.start();
+                const postData = ProjectController.getBillsPostData(number);
+                const newNodes = await ProjectController.addBillsByData(postData, true);
+                projectObj.mainController.setTreeSelected(newNodes[0]);
+                projectObj.selectColAndFocus(project.mainTree.selected);
+            } catch (err) {
+                console.log(err);
+                if (!$('hintBox_form').is(':visible')) {
+                    alert(err);
+                }
+            } finally {
+                $.bootstrapLoading.end();
+            }
+        });
+    },
+    // 注册自定义插入定额数量
+    registerFlexibleInsertRatoinMenu: function () {
+        const project = projectObj.project;
+        const insertRationHtml = `<span>插入定额&nbsp;&nbsp;<input id='insert-ration-number' class="menu-input" type="text" value="1" onfocus="this.select()">&nbsp;&nbsp;行</span>`;
+        return sheetCommonObj.registerInputContextMenuItem('insertRation', insertRationHtml, 'fa-sign-in', async function () {
+            try {
+                const number = +$('#insert-ration-number').val();
+                if (!number) {
+                    return;
+                }
+                $.bootstrapLoading.start();
+                const newData = [];
+                for (let i = 0; i < number; i++) {
+                    newData.push({ itemQuery: null, rationType: rationType.ration });
+                }
+                await project.Ration.addMultiRation(newData);
+                projectObj.setActiveCell('quantity', true);
+            } catch (err) {
+                console.log(err);
+                if (!$('hintBox_form').is(':visible')) {
+                    alert(err);
+                }
+            } finally {
+                $.bootstrapLoading.end();
+            }
+        });
+    },
     loadMainSpreadContextMenu: function () {
         var project = this.project, spread = this.mainSpread, controller = this.mainController;
         let insertBillsName = project.projectInfo.property && project.projectInfo.property.valuationType == commonConstants.ValuationType.BUDGET?"插入项目节":"插入清单";//右键“插入清单”改文字为“插入项目节”,工程量清单中保持不变。
-            $.contextMenu({
+        $.contextMenu({
             selector: '#billsSpread',
             selectableSubMenu: true,
             build: function ($trigger, e) {
@@ -1188,8 +1245,8 @@ var projectObj = {
                     callback: function (key, opt) {
                         ProjectController.addRootBill(project, controller);
                     },
-                    visible: function(key, opt){
-                        return project.mainTree.selected&&project.mainTree.selected.parent==null;
+                    visible: function (key, opt) {
+                        return project.mainTree.selected && project.mainTree.selected.parent == null;
                     }
                 },
                 "insertFB": {
@@ -1200,14 +1257,14 @@ var projectObj = {
                             return true;
                         }
                         let selected = project.mainTree.selected;
-                        if(projectObj.project.isBillsLocked()== false&&selected&&selected.sourceType==project.Bills.getSourceType()){
-                            if(selected.data.type==billType.FB){
+                        if (projectObj.project.isBillsLocked() == false && selected && selected.sourceType == project.Bills.getSourceType()) {
+                            if (selected.data.type == billType.FB) {
                                 return false;
                             }
-                            if(isFlag(selected.data)&&selected.data.flagsIndex.fixed.flag==fixedFlag.SUB_ENGINERRING){//焦点行是分部分项工程
-                                if(selected.children.length>0){
-                                   return selected.children[0].data.type==billType.FX ||selected.children[0].data.type==billType.BX;//焦点行是分部分项工程,且子项是分项或补项
-                                }else {
+                            if (isFlag(selected.data) && selected.data.flagsIndex.fixed.flag == fixedFlag.SUB_ENGINERRING) {//焦点行是分部分项工程
+                                if (selected.children.length > 0) {
+                                    return selected.children[0].data.type == billType.FX || selected.children[0].data.type == billType.BX;//焦点行是分部分项工程,且子项是分项或补项
+                                } else {
                                     return false
                                 }
                             }
@@ -1218,10 +1275,10 @@ var projectObj = {
                         ProjectController.addFB(project, controller);
                         projectObj.selectColAndFocus(project.mainTree.selected);
                     },
-                    visible: function(key, opt){
-                        if(project.mainTree.selected){
-                            return project.Bills.isFBFX(project.mainTree.selected );//不属于分部分项的话隐藏
-                        }else {
+                    visible: function (key, opt) {
+                        if (project.mainTree.selected) {
+                            return project.Bills.isFBFX(project.mainTree.selected);//不属于分部分项的话隐藏
+                        } else {
                             return false;
                         }
                     }
@@ -1234,21 +1291,21 @@ var projectObj = {
                             return true;
                         }
                         let selected = project.mainTree.selected;
-                        if(projectObj.project.isBillsLocked()== false&& selected&&selected.sourceType==project.Bills.getSourceType()){
-                            if(selected.data.type==billType.FX||selected.data.type==billType.BX){//焦点行是分项,有效显示
+                        if (projectObj.project.isBillsLocked() == false && selected && selected.sourceType == project.Bills.getSourceType()) {
+                            if (selected.data.type == billType.FX || selected.data.type == billType.BX) {//焦点行是分项,有效显示
                                 return false
                             }
-                            if(selected.data.type==billType.FB){//点行是分部,
-                                if(selected.children.length>0){//且有子项,子项是分部,灰显。
+                            if (selected.data.type == billType.FB) {//点行是分部,
+                                if (selected.children.length > 0) {//且有子项,子项是分部,灰显。
                                     return selected.children[0].data.type == billType.FB
-                                }else {
+                                } else {
                                     return false;
                                 }
                             }
-                            if(isFlag(selected.data)&&selected.data.flagsIndex.fixed.flag==fixedFlag.SUB_ENGINERRING){//焦点行是分部分项工程
-                                if(selected.children.length>0){
-                                    return selected.children[0].data.type==billType.FB;//焦点行是分部分项工程,且子项是分部时灰显
-                                }else {
+                            if (isFlag(selected.data) && selected.data.flagsIndex.fixed.flag == fixedFlag.SUB_ENGINERRING) {//焦点行是分部分项工程
+                                if (selected.children.length > 0) {
+                                    return selected.children[0].data.type == billType.FB;//焦点行是分部分项工程,且子项是分部时灰显
+                                } else {
                                     return false
                                 }
                             }
@@ -1259,49 +1316,37 @@ var projectObj = {
                         ProjectController.addFX(project, controller);
                         projectObj.selectColAndFocus(project.mainTree.selected);
                     },
-                    visible: function(key, opt){
-                        if(project.mainTree.selected){
-                            return project.Bills.isFBFX(project.mainTree.selected );//不属于分部分项的话隐藏
-                        }else {
+                    visible: function (key, opt) {
+                        if (project.mainTree.selected) {
+                            return project.Bills.isFBFX(project.mainTree.selected);//不属于分部分项的话隐藏
+                        } else {
                             return false;
                         }
                     }
                 },
                 "insertBills": {
-                    name: insertBillsName,
-                    icon: 'fa-sign-in',
+                    type: projectObj.registerFlexibleInsertBillMenu(insertBillsName),
                     disabled: function () {
                         if (projectReadOnly) {
                             return true;
                         }
                         let selected = project.mainTree.selected;
-                        if(!(projectObj.project.isBillsLocked()== true && project.withinBillsLocked(selected)) && selected && selected.sourceType === project.Bills.getSourceType()){
+                        if (!(projectObj.project.isBillsLocked() == true && project.withinBillsLocked(selected)) && selected && selected.sourceType === project.Bills.getSourceType()) {
                             return false
                         }
                         return true;
                     },
-                    callback: function (key, opt) {
-                        if(project.mainTree.selected.data.type == billType.DXFY){
-                            if(project.mainTree.selected.data.calcBase&&project.mainTree.selected.data.calcBase!=""){
-                                alert("当前有基数计算,不能插入子项。");
-                                return;
-                            }
+                    visible: function (key, opt) {
+                        if (project.mainTree.selected) {
+                            return project.Bills.isFBFX(project.mainTree.selected) == true ? false : true;
+                        } else {
+                            return false;
                         }
-                        ProjectController.addBills(project, controller);
-                        projectObj.selectColAndFocus(project.mainTree.selected);
-                    },
-                     visible: function(key, opt){
-                         if(project.mainTree.selected){
-                             return  project.Bills.isFBFX(project.mainTree.selected)==true?false:true;
-                         }else {
-                             return false;
-                         }
-                     }
+                    }
                 },
                 "spr1": '--------',
                 "insertRation": {
-                    name: "插入定额",
-                    icon: 'fa-sign-in',
+                    type: projectObj.registerFlexibleInsertRatoinMenu(),
                     disabled: function () {
                         if (projectReadOnly) {
                             return true;
@@ -1311,37 +1356,29 @@ var projectObj = {
                         // 工具栏要加按钮,且不能隐藏。菜单可以隐藏,两者又必须统一,所以启用新规则。怕以后又要改回来,所以保留。 CSL, 2018-01-02
                         return !project.Ration.canAdd(project.mainTree.selected);
                     },
-                    callback: function (key, opt) {
-                        project.Ration.addNewRation(null,rationType.ration,projectObj.selectColAndFocus,false);
-                       // ProjectController.addRation(project, controller, rationType.ration);
-                    }/*,
-                    visible: function(key, opt){
-                        var selected = project.mainTree.selected;
-                        return canInsertRationNode(selected);
-                    }*/
                 },
                 "insertGLJ": {
-                  name: "插入工料机",
-                  icon: 'fa-sign-in',
-                  disabled: function () {
-                      if (projectReadOnly) {
-                          return true;
-                      }
-                      // var selected = project.mainTree.selected;
-                      // return project.Ration.addRationChecking(selected);  // Vincent, 2018-01-02
-                      return !project.Ration.canAdd(project.mainTree.selected);
-                  },
-                  callback: function (key, opt) {
-                    let selected = project.mainTree.selected;
-                    if(selected.data.calcBase&&selected.data.calcBase!=""){
-                        alert("当前有基数计算,不能插入定额/量价/工料机。");
-                        return;
+                    name: "插入工料机",
+                    icon: 'fa-sign-in',
+                    disabled: function () {
+                        if (projectReadOnly) {
+                            return true;
+                        }
+                        // var selected = project.mainTree.selected;
+                        // return project.Ration.addRationChecking(selected);  // Vincent, 2018-01-02
+                        return !project.Ration.canAdd(project.mainTree.selected);
+                    },
+                    callback: function (key, opt) {
+                        let selected = project.mainTree.selected;
+                        if (selected.data.calcBase && selected.data.calcBase != "") {
+                            alert("当前有基数计算,不能插入定额/量价/工料机。");
+                            return;
+                        }
+                        getGLJData('insert');// ProjectController.addRation(project, controller, rationType.volumePrice);
+                    },
+                    visible: function (key, opt) {
+                        return false
                     }
-                    getGLJData('insert');// ProjectController.addRation(project, controller, rationType.volumePrice);
-                  },
-                  visible: function(key, opt){
-                    return false
-                  }
                 },
                 "insertLJ": {
                     name: "插入量价",//插入量价不需要自动定位到编号列
@@ -1353,29 +1390,29 @@ var projectObj = {
                         return !project.Ration.canAdd(project.mainTree.selected);
                     },
                     callback: function (key, opt) {
-                       /* project.Ration.addNewRation(null,rationType.volumePrice,function (newNode) {
-                            projectObj.selectColAndFocus(newNode,null);
-                        },true);*/
+                        /* project.Ration.addNewRation(null,rationType.volumePrice,function (newNode) {
+                             projectObj.selectColAndFocus(newNode,null);
+                         },true);*/
                     },
-                    items:{
-                        insertLabour:{
+                    items: {
+                        insertLabour: {
                             name: "人工",
                             icon: 'fa-sign-in',
-                            callback:function(key){
+                            callback: function (key) {
                                 project.Ration.insertVolumePrice(gljType.LABOUR);
                             }
                         },
-                        insertMaterial:{
-                           name:"材料" ,
+                        insertMaterial: {
+                            name: "材料",
                             icon: 'fa-sign-in',
-                            callback:function(key){
+                            callback: function (key) {
                                 project.Ration.insertVolumePrice(gljType.GENERAL_MATERIAL);
                             }
                         },
-                        insertMachine:{
-                            name:"机械" ,
+                        insertMachine: {
+                            name: "机械",
                             icon: 'fa-sign-in',
-                            callback:function(key){
+                            callback: function (key) {
                                 project.Ration.insertVolumePrice(gljType.GENERAL_MACHINE);
                             }
                         }
@@ -1392,13 +1429,13 @@ var projectObj = {
                     },
                     callback: function (key, opt) {
                         let selected = project.mainTree.selected;
-                        if(selected.data.calcBase&&selected.data.calcBase!=""){
+                        if (selected.data.calcBase && selected.data.calcBase != "") {
                             alert("当前有基数计算,不能插入定额/量价/工料机。");
                             return;
                         }
                         getGLJData('insertEquipment');// ProjectController.addRation(project, controller, rationType.volumePrice);
                     },
-                    visible: function(key, opt){//2018-11-08  新需求,这个按钮先隐藏,有需要再放开
+                    visible: function (key, opt) {//2018-11-08  新需求,这个按钮先隐藏,有需要再放开
                         let selected = project.mainTree.selected;
                         return projectObj.isInsertEquipmentVisable(selected);
                     }
@@ -1416,18 +1453,18 @@ var projectObj = {
                     callback: function (key, opt) {
                         installationFeeObj.showCalcInstallSettingDiv();
                     },
-                    visible: function(key, opt){
+                    visible: function (key, opt) {
                         return projectObj.project.isInstall();
                     }
                 },
-                "cleanzmhs":{
-                    name:'清空定额调整',
+                "cleanzmhs": {
+                    name: '清空定额调整',
                     icon: 'fa-remove',
                     callback: function (key, opt) {
                         let selected = project.mainTree.selected;
-                        projectObj.project.Ration.updateRationCodes([{'node':selected, value:selected.data.code}],true);
+                        projectObj.project.Ration.updateRationCodes([{ 'node': selected, value: selected.data.code }], true);
                     },
-                    visible: function(key, opt){
+                    visible: function (key, opt) {
                         let selected = project.mainTree.selected;
                         if (selected && selected.sourceType == ModuleNames.ration) {
                             return true;
@@ -1439,7 +1476,7 @@ var projectObj = {
                         if (projectReadOnly) {
                             return true;
                         }
-                        if(selected && selected.data.type != rationType.ration){
+                        if (selected && selected.data.type != rationType.ration) {
                             return true;
                         }
                         return false;
@@ -1539,10 +1576,10 @@ var projectObj = {
                     },
                     callback: function () {
                         var selected = project.mainTree.selected;
-                        if(selected.sourceType == project.Bills.getSourceType()&&selected.data.type==billType.FB&&selected.children.length<=0){//选中的是分部,并且没有子项,直接删除
+                        if (selected.sourceType == project.Bills.getSourceType() && selected.data.type == billType.FB && selected.children.length <= 0) {//选中的是分部,并且没有子项,直接删除
                             project.Bills.deleteSelectedNodes();//project.Bills.deleteSelectedNode();
-                        }else {
-                            $("#delete_row").modal({show:true});//弹出删除提示框;
+                        } else {
+                            $("#delete_row").modal({ show: true });//弹出删除提示框;
                         }
                     }
                 },
@@ -1557,11 +1594,11 @@ var projectObj = {
                             return true;
                         }
                     },
-                    visible:function(key, opt){//2019-11-11 新需求重新开放右键“造价计算”。
+                    visible: function (key, opt) {//2019-11-11 新需求重新开放右键“造价计算”。
                         return true;
                     }
                 },
-                "spr4":'--------',
+                "spr4": '--------',
                 "copyBlock": {
                     name: '复制整块',
                     icon: 'fa-copy',
@@ -1571,10 +1608,10 @@ var projectObj = {
                         }
                         let selection = projectObj.mainSpread.getActiveSheet().getSelections()[0];
                         let firstNode = projectObj.project.mainTree.items[selection.row];//当多选的情况,用mainTree.selected判断不正确,要用第一个选中的节点
-                        for(let i = 0;i< selection.rowCount;i++){ //多选的时候判断所有与第一个节点同级的节点
+                        for (let i = 0; i < selection.rowCount; i++) { //多选的时候判断所有与第一个节点同级的节点
                             let temNode = projectObj.project.mainTree.items[selection.row + i];
-                            if(firstNode.getParentID() == temNode.getParentID()){
-                                if(BlockController.copyBtnDisable(temNode) == true){
+                            if (firstNode.getParentID() == temNode.getParentID()) {
+                                if (BlockController.copyBtnDisable(temNode) == true) {
                                     return true;
                                 }
                             }
@@ -1588,14 +1625,14 @@ var projectObj = {
                         setTimeout(function () {
                             BlockController.copyBlock(selections[0]);
                             $.bootstrapLoading.end();
-                        },100)
+                        }, 100)
 
                     }
                 },
                 "pasteBlock": {
                     name: '粘贴整块',
                     icon: 'fa-paste',
-                    disabled: function (){
+                    disabled: function () {
                         if (projectReadOnly) {
                             return true;
                         }
@@ -1608,7 +1645,7 @@ var projectObj = {
                 "recoverDeletedNodes": {
                     name: '恢复删除节点',
                     icon: 'fa-undo',
-                    disabled: function (){
+                    disabled: function () {
                         return BlockController.recoverBlockDisabled();
                     },
                     callback: function () {
@@ -1621,14 +1658,14 @@ var projectObj = {
                     disabled: function () {
                         return projectReadOnly;
                     },
-                    visible: function(key, opt){
-                         let  selected = project.mainTree.selected;
-                         return selected.sourceType==ModuleNames.bills&&project.Bills.isEngineerEst(selected);//当焦点行是“专业工程暂估价”时,右键可见并有效。
+                    visible: function (key, opt) {
+                        let selected = project.mainTree.selected;
+                        return selected.sourceType == ModuleNames.bills && project.Bills.isEngineerEst(selected);//当焦点行是“专业工程暂估价”时,右键可见并有效。
                     },
                     callback: function () {
                         let node = project.mainTree.selected;//project.Bills.getNodeByFlag(project.mainTree.selected,fixedFlag.ENGINEERING_ESITIMATE);
-                        if(node){
-                            projectObj.editContent(node,'engineeringContent');
+                        if (node) {
+                            projectObj.editContent(node, 'engineeringContent');
                         }
                     }
                 },
@@ -1638,14 +1675,14 @@ var projectObj = {
                     disabled: function () {
                         return projectReadOnly;
                     },
-                    visible: function(key, opt){
-                        let  selected = project.mainTree.selected;
-                        return selected.sourceType==ModuleNames.bills&&project.Bills.isTotalService(selected);//当焦点行是“总承包服务费”时,右键可见并有效。
+                    visible: function (key, opt) {
+                        let selected = project.mainTree.selected;
+                        return selected.sourceType == ModuleNames.bills && project.Bills.isTotalService(selected);//当焦点行是“总承包服务费”时,右键可见并有效。
                     },
                     callback: function () {
                         let node = project.mainTree.selected;//project.Bills.getNodeByFlag(project.mainTree.selected,fixedFlag.TURN_KEY_CONTRACT);
-                        if(node){
-                            projectObj.editContent(node,'serviceContent');
+                        if (node) {
+                            projectObj.editContent(node, 'serviceContent');
                         }
                     }
                 },
@@ -1655,48 +1692,48 @@ var projectObj = {
                     disabled: function () {
                         return projectReadOnly;
                     },
-                    visible: function(key, opt){
-                        let  selected = project.mainTree.selected;
-                        return selected.sourceType==ModuleNames.bills&&project.Bills.isClaimVisa(selected);//当焦点行是“签证及索赔计价”时,右键可见并有效。
+                    visible: function (key, opt) {
+                        let selected = project.mainTree.selected;
+                        return selected.sourceType == ModuleNames.bills && project.Bills.isClaimVisa(selected);//当焦点行是“签证及索赔计价”时,右键可见并有效。
                     },
                     callback: function () {
                         let node = project.mainTree.selected;//project.Bills.getNodeByFlag(project.mainTree.selected,fixedFlag.CLAIM_VISA);
-                        if(node){
-                            projectObj.editContent(node,'claimVisa');
+                        if (node) {
+                            projectObj.editContent(node, 'claimVisa');
                         }
                     }
                 },
-                "replaceMaterial":{
-                    name:'智能材料替换',
+                "replaceMaterial": {
+                    name: '智能材料替换',
                     icon: 'fa-edit',
-                    disabled:function (key,opt) {
+                    disabled: function (key, opt) {
                         if (projectReadOnly) {
                             return true;
                         }
-                        let  selected = project.mainTree.selected;
-                        return selected.sourceType==ModuleNames.bills ?!(project.Bills.isFXorBX(selected)||selected.source.children.length ==0):true//是分项、补项或叶子清单才有效;
+                        let selected = project.mainTree.selected;
+                        return selected.sourceType == ModuleNames.bills ? !(project.Bills.isFXorBX(selected) || selected.source.children.length == 0) : true//是分项、补项或叶子清单才有效;
                     },
-                    callback:function(){
+                    callback: function () {
                         MaterialController.replaceMaterial([project.mainTree.selected]);
                     },
-                    visible: function(key, opt){//2018-11-15 暂时隐藏
+                    visible: function (key, opt) {//2018-11-15 暂时隐藏
                         return false
                     }
                 },
-                "createBlocks":{
-                    name:'生成组价模板',
+                "createBlocks": {
+                    name: '生成组价模板',
                     icon: 'fa-puzzle-piece',
-                    disabled:function (key,opt) {
+                    disabled: function (key, opt) {
                         if (projectReadOnly) {
                             return true;
                         }
                         let selected = project.mainTree.selected;
                         return selected.sourceType != ModuleNames.bills;
                     },
-                    callback: function(){
+                    callback: function () {
                         blockLibObj.checkShow();
                     },
-                    visible: function(key, opt){
+                    visible: function (key, opt) {
                         return G_SHOW_BLOCK_LIB;
                     }
                 }

+ 52 - 2
web/building_saas/main/js/views/std_billsGuidance_lib.js

@@ -138,7 +138,57 @@ const billsGuidance = (function () {
         }
     };
     //插入清单
-    function insertBills(lowestNodes) {
+    async function insertBills(lowestNodes) {
+        try {
+            let selTree = getSelTree(lowestNodes);
+            const { errMsg, parent, mainTreeFragment } = overwrite.getFragment();
+            if (errMsg) {
+                alert(errMsg);
+                return;
+            }
+            let compareData = compareTree(parent, mainTreeFragment, selTree.roots);
+            let sheet = projectObj.mainSpread.getActiveSheet(),
+                row = sheet.getActiveColumnIndex(),
+                col = sheet.getActiveColumnIndex();
+            if (compareData.postData.length > 0) {
+                //如果插入的是固定清单,则需要判断该固定清单在造价书中是否已存在,造价书中不可存在相同的固定清单
+                let fixedDatas = compareData.postData.filter((data) =>
+                data.updateType === updateType.create && Array.isArray(data.updateData.flags));
+                if (fixedDatas.length > 0) {
+                    //提示已存在此固定清单并且定位
+                    let firstFixed = fixedDatas[0].updateData;
+                    let existNode = projectObj.project.mainTree.items.find((node) =>
+                    node.data && node.data.flagsIndex && node.data.flagsIndex.fixed && node.data.flagsIndex.fixed.flag === firstFixed.flags[0].flag);
+                    if (existNode) {
+                        alert(`固定清单<strong>“${firstFixed.name}”</strong>已被第${existNode.serialNo() + 1}行清单占用。`);
+                        locateAtSpread(sheet, existNode.serialNo(), col);
+                        return;
+                    }
+                }
+                isInserting = true;
+                const newNodes = await ProjectController.addBillsByData(compareData.postData);
+                row = newNodes[newNodes.length - 1].serialNo();
+                //有新的节点插入,也有可能定位至旧节点(批量选用的情况下)
+                if (compareData.locateNode) {
+                    //该清单节点在主树的位置
+                    row = projectObj.project.mainTree.nodes[projectObj.project.mainTree.prefix + compareData.locateNode.data.ID].serialNo();
+                }
+                locateAtSpread(sheet, row, col);
+            } else if (compareData.locateNode) {
+                //该清单节点在主树的位置
+                row = projectObj.project.mainTree.nodes[projectObj.project.mainTree.prefix + compareData.locateNode.data.ID].serialNo();
+                locateAtSpread(sheet, row, col);
+            }
+        } catch (err) {
+            console.log(err);
+            if (!$('hintBox_form').is(':visible')) {
+                alert(err);
+            }
+        } finally {
+            isInserting = false;
+        }
+    }
+    /* function insertBills(lowestNodes) {
         let selTree = getSelTree(lowestNodes);
         const { errMsg, parent, mainTreeFragment } = overwrite.getFragment();
         if (errMsg) {
@@ -199,7 +249,7 @@ const billsGuidance = (function () {
             row = projectObj.project.mainTree.nodes[projectObj.project.mainTree.prefix + compareData.locateNode.data.ID].serialNo();
             locateAtSpread(sheet, row, col);
         }
-    }
+    } */
     function locateAtSpread(sheet, row, col) {
         sheet.setSelection(row, col, 1, 1);
         projectObj.mainController.setTreeSelected(projectObj.mainController.tree.items[row]);//触发树节点选中事件

+ 6 - 0
web/building_saas/pm/js/pm_gc.js

@@ -905,8 +905,10 @@ function e_recFiles(btn){
             let findData = type === fileType.unitPriceFile ? {id: recObjs[i].id} : {ID: recObjs[i].id};
             updateDatas.push(getUpdateObj(type, findData, {deleteInfo: null, name: delPostFix(recObjs[i].name) + decDate}));
         }
+        let isRecoverProj = false;
         //恢复建设项目
         if(updateDatas.length > 0 && deleted(selected)){
+            isRecoverProj = true;
             updateDatas.push(getUpdateObj(projectType.project, {ID: selected.data.ID}, {deleteInfo: null, name: delPostFix(selected.data.name) + decDate}));
         }
         updateDatas = deWeightName(updateDatas);
@@ -925,6 +927,9 @@ function e_recFiles(btn){
                     gcTreeObj.refreshNodeData(selected);
                 }
             }
+            if (isRecoverProj) {
+                projTreeObj.emitTreeChange();
+            }
         }
     });
 }
@@ -986,6 +991,7 @@ function e_recProj(btn){
             }
             v_removeNode(selected);
             v_refreshNode(selected, true);
+            projTreeObj.emitTreeChange();
         }
     });
 }

+ 63 - 14
web/building_saas/pm/js/pm_newMain.js

@@ -503,6 +503,7 @@ const projTreeObj = {
             }
             projTreeObj.moveTo(selected, null, parent, next, null, action);
             $.bootstrapLoading.end();
+            projTreeObj.emitTreeChange();
         });
     },
     //升级后选中节点的后兄弟节点不成为其子节点,因为有层级类型限制(相当于选中节点移动到父项后成为其后兄弟)
@@ -1623,6 +1624,40 @@ const projTreeObj = {
         let result =await ajaxGet("/pm/api/getUploadToken");
         $("#confirm-import").show();
         projTreeObj.uptoken=result.uptoken;
+    },
+    // 树数据发生变化,触发推送
+    emitTreeChange: function () {
+        const compilationID = compilationData._id;
+        // 获取当前树节点展开状态和焦点行
+        const isActive = $('#tab_pm_all').hasClass('active');
+        const expandState = isActive ? this.tree.getExpState(this.tree.items) : null;
+        const selection = isActive && this.tree.selected ? { row: this.tree.selected.serialNo(), rowCount: 1 } : null;
+        socket.emit('pmTreeChange', { userID, compilationID, expandState, selection });
+    },
+    initTree: function (refresh = false, callback, expandCallback) {
+        if (gcTreeObj.workBook) {
+            gcTreeObj.workBook.destroy();
+            gcTreeObj.workBook = null;
+        }
+        gcTreeObj.tree = null;
+        init(refresh, callback, expandCallback);
+    },
+    handleNotifyClick: function (expandState, selection) {
+        $('#notify').hide();
+        const callback = () => {
+            const sheet = this.workBook.getSheet(0);
+            if (selection && this.tree.items[selection.row]) {
+                this.initSelection(selection, { row: 0, rowCount: 1 }, sheet);
+                const col = sheet.getActiveColumnIndex();
+                sheet.setSelection(selection.row, col, 1, 1);
+            }
+        };
+        const expandCallback = () => {
+            if (expandState) {
+                this.tree.setExpandedByState(this.tree.items, expandState);
+            }
+        }
+        this.initTree(true, callback, expandCallback);
     }
 
 };
@@ -1651,15 +1686,8 @@ $(document).ready(function() {
 
     });
 
-    init();
-    $('#tab_pm_all').on('show.bs.tab', function () {
-        if(gcTreeObj.workBook){
-            gcTreeObj.workBook.destroy();
-            gcTreeObj.workBook = null;
-        }
-        gcTreeObj.tree = null;
-        init();
-    });
+    projTreeObj.initTree();
+    $('#tab_pm_all').on('show.bs.tab', () => projTreeObj.initTree(true));
 
     // 侧滑数据
    /* $(".poj-list").on('click', ".open-sidebar", function() {
@@ -1952,6 +1980,7 @@ $(document).ready(function() {
                 setTimeout(function () {
                     STATE.deleting = false;
                 }, 500);
+                projTreeObj.emitTreeChange();
             }, function () {
                 $.bootstrapLoading.end();
                 setTimeout(function () {
@@ -1986,6 +2015,7 @@ $(document).ready(function() {
             select.data.name = newName;
             let sheet = projTreeObj.workBook.getActiveSheet();
             projTreeObj.setCellValue({ row: sheet.getActiveRowIndex(), col: 0 }, select, sheet, projTreeObj.setting);
+            projTreeObj.emitTreeChange();
         });
     });
 
@@ -2269,7 +2299,7 @@ function initNodesVisibility(nodes, visible) {
     });
 }
 
-function initProjects(callback) {
+function initProjects(callback, expandCallback) {
     GetAllProjectData(function (datas) {
         //设置项目类别
         for (let data of datas) {
@@ -2288,7 +2318,11 @@ function initProjects(callback) {
             sheet.name('projectSheet');
             sheetCommonObj.spreadDefaultStyle(projTreeObj.workBook);
             projTreeObj.sumEngineeringCost();
-            initNodesVisibility(projTreeObj.tree.items, false);
+            if (expandCallback) {
+                expandCallback();
+            } else {
+                initNodesVisibility(projTreeObj.tree.items, false);
+            }
             projTreeObj.showTreeData(projTreeObj.tree.items, projTreeObj.setting, sheet);
             //初始选择
             const initSel = sheet.getSelections()[0] ? sheet.getSelections()[0] : {row: 0, rowCount: 1};
@@ -2307,7 +2341,7 @@ function initProjects(callback) {
 /**
  * 初始化数据
  * @return {void} */
-async function init(refresh = false) {//refresh是刷新页面时才使用的
+async function init(refresh = false, callback, expandCallback) {//refresh是刷新页面时才使用的
     try {
         billValuation = billValuation.replace(/\n/g, '\\n');
         rationValuation = rationValuation.replace(/\n/g, '\\n');
@@ -2319,7 +2353,12 @@ async function init(refresh = false) {//refresh是刷新页面时才使用的
             $("#progress_modal_body").text('首次加载例题,请稍候……');
             await ajaxPost('/pm/api/prepareInitialData', { user_id: userID });
             await importProcessChecking(null, null, () => {
-                initProjects(() => $.bootstrapLoading.progressEnd());
+                initProjects(() => {
+                    $.bootstrapLoading.progressEnd();
+                    if (callback) {
+                        callback();
+                    }
+                }, expandCallback);
             }, true);
         } else {
             await importProcessChecking(null, ({ content }) => {
@@ -2327,7 +2366,12 @@ async function init(refresh = false) {//refresh是刷新页面时才使用的
                 $("#progress_modal_body").text(content);
             }, () => {
                 $.bootstrapLoading.start();
-                initProjects(() => $.bootstrapLoading.end());
+                initProjects(() => {
+                    $.bootstrapLoading.end();
+                    if (callback) {
+                        callback();
+                    }
+                }, expandCallback);
             }, true);
         }
         engineering = engineeringList !== null && engineeringList !== undefined ? JSON.parse(engineeringList) : [];
@@ -2371,6 +2415,7 @@ function AddProject() {
         setTimeout(function () {
             STATE.addingProject = false;
         }, 500);
+        projTreeObj.emitTreeChange();
     };
     let errCB = function () {
         $.bootstrapLoading.end();
@@ -2957,6 +3002,7 @@ function AddTender() {
             setTimeout(function () {
                 STATE.addingTender = false;
             }, 500);
+            projTreeObj.emitTreeChange();
         };
         let errCB = function () {
             $.bootstrapLoading.end();
@@ -3053,6 +3099,7 @@ function AddFolder() {
         setTimeout(function () {
             STATE.addingFolder = false;
         }, 500);
+        projTreeObj.emitTreeChange();
     };
     let errCB = function () {
         $.bootstrapLoading.end();
@@ -3819,6 +3866,7 @@ function handleProjectAfterChecking(projectData) {
     const rootData = projectData.find(item => item.projType === projectType.project);
     const sorted = commonUtil.getSortedTreeData(rootData.ParentID, projectData);
     doAfterImport(sorted);
+    projTreeObj.emitTreeChange();
 }
 // 导入检查完成时,对新增单位工程的处理
 function handleTenderAfterChecking(projectData, orgTender) {
@@ -3831,6 +3879,7 @@ function handleTenderAfterChecking(projectData, orgTender) {
     const newNode = projTreeObj.insert(tenderData, parent, next);
     const refreshNodes = projTreeObj.calEngineeringCost(newNode);
     projTreeObj.refreshNodeData(refreshNodes);
+    projTreeObj.emitTreeChange();
 }
 
 async function importProcessChecking(key, processingFunc = null, completeFunc = null, immediately = false) {

+ 1 - 1
web/building_saas/pm/js/pm_share.js

@@ -923,7 +923,7 @@ const pmShare = (function () {
             $.bootstrapLoading.progressStart('拷贝项目', true);
             $("#progress_modal_body").text('正在拷贝项目,请稍候……');
             await ajaxPost('/pm/api/copyProjects', { projectMap: copyMap, user_id: userID, tenderCount: 1 });
-            importProcessChecking();
+            importProcessChecking(null, null, projTreeObj.emitTreeChange);
         } catch (err) {
             alert(err);
         }

+ 33 - 0
web/building_saas/pm/js/pm_tree.js

@@ -434,6 +434,13 @@ const pmTree = {
                             console.log(`${node.serialNo() + 1}:${node.data.name} node索引大于next索引`);
                             return false;
                         }
+                        // nextSibling跟parent children的下一节点对应不上
+                        if (nodeIdx !== -1 && 
+                            (nodeIdx === parent.children.length - 1 && nextIdx !== -1) || 
+                            (nodeIdx !== parent.children.length - 1 && nodeIdx + 1 !== nextIdx)) {
+                            console.log(`${node.serialNo() + 1}:${node.data.name} nextSibling与树显示的下一节点对应不上`);
+                            return false;
+                        }
                         if (node.nextSibling && node.parent !== node.nextSibling.parent) {
                             console.log(`${node.serialNo() + 1}:${node.data.name} 与兄弟节点 ${node.nextSibling.serialNo() + 1}:${node.nextSibling.data.name} 父节点不同`);
                             return false;
@@ -444,10 +451,36 @@ const pmTree = {
                                 return false;
                             }
                         }
+                        
                     }
                     return true;
                 }
             };
+
+            Tree.prototype.getExpState = function (nodes) {
+                let sessionExpanded = [];
+                function getStat(items){
+                    for(let item of items){
+                        sessionExpanded.push(item.expanded ? 1 : 0);
+                    }
+                }
+                getStat(nodes);
+                let expState = sessionExpanded.join('');
+                return expState;
+            };
+    
+            //节点根据展开收起列表'010101'展开收起
+            Tree.prototype.setExpandedByState = function (nodes, expState) {
+                let expStateArr = expState.split('');
+                for(let i = 0; i < nodes.length; i++){
+                    let expanded = expStateArr[i] == 1 ? true : false;
+                    if(nodes[i].expanded === expanded){
+                        continue;
+                    }
+                    nodes[i].setExpanded(expanded);
+                }
+            };
+
             Tree.prototype.setNodesExpanded = function (nodes, sheet) {
                 TREE_SHEET_HELPER.massOperationSheet(sheet, () => {
                     nodes.forEach(node => {

+ 3 - 0
web/common/html/header.html

@@ -36,6 +36,9 @@
                     <a class="dropdown-item" href="/logout">退出登录</a>
                 </div>
             </li>
+            <li class="nav-item">
+                <a id="fullscreen-a" href="javascript:void(0);" class="nav-link" onclick="commonUtil.handleFullscreen()"><span><i class="fa fa-window-maximize "></i> 全屏</span></a>
+            </li>
             <% if (!versionName.includes('免费')) {%>
             <li class="nav-item">
               <a href="user/buy" target="_blank"><img src="/web/building_saas/img/vip.png" data-toggle="tooltip" data-placement="bottom" data-original-title="专业版用户"></a>

+ 320 - 0
web/over_write/hunan_2020.js

@@ -0,0 +1,320 @@
+// 清单基数
+const progression = ['施工场地建设费', '养护单位项目管理费', '养护单位项目管理费(未单独设置项目部)', '工程监理费', '工程监理费(未实行社会监理)', '设计文件审查费', '工程设计费'];
+const deficiency = {};
+if (typeof baseFigureMap !== 'undefined') {
+    const { fixedFlag } = commonConstants;
+    const budgetMap = {
+        // 显示:除清单固定类别是“建筑安装工程费”的以外部分可显示
+        '养护工程费': {
+            base: 'YHGCF',
+            fixedFlag: fixedFlag.CONSTRUCTION_INSTALL_FEE,
+            filter: [fixedFlag.CONSTRUCTION_INSTALL_FEE],
+            pick: false,
+        },
+        // 显示:仅清单固定类别是“安全生产费”的可显示
+        '养护工程费(不含安全生产费)': {
+            base: 'YHGCFBHSC',
+            fixedFlag: null,
+            filter: [fixedFlag.SAFE_COST],
+            pick: true
+        },
+        // 显示:仅清单固定类别是“养护工程其他费用”部分可显示
+        '养护工程费(不含设备费)': {
+            base: 'YHGCFBHSB',
+            fixedFlag: null,
+            filter: [fixedFlag.MAINTENANCE_EXPENSES],
+            pick: true
+        },
+        // 显示:除清单固定类别是“建筑安装工程费”的以外部分可显示
+        '定额养护工程费': {
+            base: 'DEYHGCF',
+            fixedFlag: fixedFlag.CONSTRUCTION_INSTALL_FEE,
+            filter: [fixedFlag.CONSTRUCTION_INSTALL_FEE],
+            pick: false
+        },
+        // 显示:仅清单固定类别是“养护工程其他费用”、“施工场地建设费”的可显示
+        '定额养护工程费(不含专项费用)': {
+            base: 'DEYHGCFBHZXFY',
+            fixedFlag: null,
+            filter: [fixedFlag.MAINTENANCE_EXPENSES, fixedFlag.CONSTRUCTION_PLANT_COST],
+            pick: true,
+        },
+        // 显示:除清单固定类别是“建筑安装工程费”、“土地使用及拆迁补偿费”的以外部分可显示
+        '土地使用及拆迁补偿费': {
+            base: 'TDSYJCQBCF',
+            fixedFlag: fixedFlag.LAND_USED_DEMOLITION,
+            filter: [fixedFlag.CONSTRUCTION_INSTALL_FEE, fixedFlag.LAND_USED_DEMOLITION],
+            pick: false,
+        },
+        // 显示:除清单固定类别是“建筑安装工程费”、“土地使用及拆迁补偿费”、“工程建设其他费用”的以外部分可显示。
+        '工程建设其他费用': {
+            base: 'GCJSQTFY',
+            fixedFlag: fixedFlag.MAINTENANCE_EXPENSES,
+            filter: [fixedFlag.CONSTRUCTION_INSTALL_FEE, fixedFlag.LAND_USED_DEMOLITION, fixedFlag.MAINTENANCE_EXPENSES],
+            pick: false,
+        },
+        // 显示:仅清单固定类别是“施工场地建设费”的可显示。
+        '施工场地建设费': {
+            isProgressive: true,
+            base: 'SGCDJSF',
+            fixedFlag: null,
+            filter: [fixedFlag.CONSTRUCTION_PLANT_COST],
+            pick: true,
+        },
+        // 显示:仅清单固定类别是“养护工程其他费用”部分可显示。
+        '养护单位项目管理费': {
+            isProgressive: true,
+            base: 'YHDWXMGLF',
+            fixedFlag: fixedFlag.CONSTRUCTION_INSTALL_FEE,
+            filter: [fixedFlag.MAINTENANCE_EXPENSES],
+            pick: true,
+        },
+        // 显示:仅清单固定类别是“养护工程其他费用”部分可显示。
+        '养护单位项目管理费(未单独设置项目部)': {
+            isProgressive: true,
+            base: 'YHDWXMGLFWSXMB',
+            fixedFlag: fixedFlag.CONSTRUCTION_INSTALL_FEE,
+            filter: [fixedFlag.MAINTENANCE_EXPENSES],
+            pick: true,
+        },
+        // 显示:仅清单固定类别是“养护工程其他费用”部分可显示。
+        '工程监理费': {
+            isProgressive: true,
+            base: 'GCJLF',
+            fixedFlag: fixedFlag.CONSTRUCTION_INSTALL_FEE,
+            filter: [fixedFlag.MAINTENANCE_EXPENSES],
+            pick: true,
+        },
+        // 显示:仅清单固定类别是“养护工程其他费用”部分可显示。
+        '工程监理费(未实行社会监理)': {
+            isProgressive: true,
+            base: 'GCJLFWSXSHJL',
+            fixedFlag: fixedFlag.CONSTRUCTION_INSTALL_FEE,
+            filter: [fixedFlag.MAINTENANCE_EXPENSES],
+            pick: true,
+        },
+        // 显示:只有清单固定类别是“养护工程其他费用”部分可显示。
+        '设计文件审查费': {
+            isProgressive: true,
+            base: 'SJWJSCF',
+            fixedFlag: fixedFlag.CONSTRUCTION_INSTALL_FEE,
+            filter: [fixedFlag.MAINTENANCE_EXPENSES],
+            pick: true,
+        },
+        // 显示:只有清单固定类别是“养护工程其他费用”部分可显示。
+        '工程设计费': {
+            isProgressive: true,
+            base: 'GCSJF',
+            fixedFlag: fixedFlag.CONSTRUCTION_INSTALL_FEE,
+            filter: [fixedFlag.MAINTENANCE_EXPENSES],
+            pick: true,
+        },
+        // 显示:仅“价差预备费”可显示
+        '价差预备费': {
+            base: 'JCYBF',
+            fixedFlag: fixedFlag.CONSTRUCTION_INSTALL_FEE,
+            filter: [fixedFlag.SPREAD_BUDGET_FEE],
+            pick: true,
+        },
+    };
+    const boqMap = {
+        //仅允许用于固定类别是“第100章至700章清单”以外的清单
+        '各章清单合计': {
+            base: 'GZQDHJ',
+            fixedFlag: fixedFlag.ONE_SEVEN_BILLS,
+            filter: [fixedFlag.ONE_SEVEN_BILLS],
+            pick: false
+        },
+        //仅允许用于固定类别是“第100章至700章清单”以外的清单
+        '专项暂定合计': {
+            base: 'ZXZDHJ',
+            fixedFlag: null,
+            filter: [fixedFlag.ONE_SEVEN_BILLS],
+            pick: false
+        },
+        /*
+        *  清单固定行[第100章至700章清单]下的[第100章清单]需要允许清单可使用基数{100章以外合计}
+        *  因此{100章以外合计}不设置关联的清单固定行
+        * */
+        //仅允许用于固定类别为“100章清单”引用
+        '100章以外清单合计': {
+            base: 'YBZYHQDHJ',
+            fixedFlag: null,
+            filter: [fixedFlag.ONE_HUNDRED_BILLS],
+            pick: true
+        }
+    };
+    baseFigureMap.budget = budgetMap;
+    baseFigureMap.boq = boqMap;
+}
+
+if (typeof baseFigureTemplate !== 'undefined') {
+    const { fixedFlag } = commonConstants;
+    baseFigureTemplate.budget = {
+        // 养护工程费 算法:取清单固定类别是“建筑安装工程费”的金额。
+        YHGCF(tender) {
+            return cbTools.getBaseFee(fixedFlag.CONSTRUCTION_INSTALL_FEE, tender, 'common');
+        },
+        // 养护工程费(不含安全生产费) 算法:取清单固定类别是“建筑安装工程费”的金额,扣除“安全生产费”的金额。
+        YHGCFBHSC(tender) {
+            return cbTools.getFeeWithDeduction(fixedFlag.CONSTRUCTION_INSTALL_FEE, [fixedFlag.SAFE_COST], tender, 'common');
+        },
+        // 养护工程费(不含设备费) 算法:取清单固定类别是“建筑安装工程费”的金额,扣除设备的金额。
+        YHGCFBHSB(tender) {
+            return cbTools.getFeeWithDeduction(fixedFlag.CONSTRUCTION_INSTALL_FEE, [fixedFlag.EQUIPMENT_ACQUISITION_FEE], tender, 'common');
+        },
+        // 定额养护工程费 取清单固定类别是“建筑安装工程费”的定额建安费。
+        DEYHGCF(tender) {
+            return cbTools.getBaseFee(fixedFlag.CONSTRUCTION_INSTALL_FEE, tender, 'rationCommon');
+        },
+        // 定额养护工程费(不含专项费用) 算法:取清单固定类别是“建筑安装工程费”的“定额建安费”,扣除“专项费用”的“定额建安费”。
+        DEYHGCFBHZXFY(tender) {
+            return cbTools.getFeeWithDeduction(fixedFlag.CONSTRUCTION_INSTALL_FEE, [fixedFlag.SPECIAL_COST], tender, 'rationCommon');
+        },
+        // 土地使用及拆迁补偿费 算法:取清单固定类别是“土地使用及拆迁补偿费”的金额。
+        TDSYJCQBCF(tender) {
+            return cbTools.getBaseFee(fixedFlag.LAND_USED_DEMOLITION, tender, 'common');
+        },
+        // 工程建设其他费用 算法:取清单固定类别是“养护工程其他费用”的金额。
+        GCJSQTFY(tender) {
+            return cbTools.getBaseFee(fixedFlag.MAINTENANCE_EXPENSES, tender, 'common');
+        },
+        // 施工场地建设费 算法:以{定额养护工程费(不含专项费用)}为基数,采用累进办法计算。
+        SGCDJSF(tender) {
+            const baseFee = this['DEYHGCFBHZXFY'](tender);
+            if (!tender) {
+                calcBase.baseProgressiveFee = baseFee;
+            }
+            return calculateUtil.getProgressiveFee(baseFee, '施工场地建设费', projectObj.project.property.progressiveInterval, decimalObj.bills.totalPrice, deficiency);
+        },
+        // 养护单位项目管理费 算法:以{定额养护工程费}为基数,采用累进办法计算。
+        YHDWXMGLF(tender) {
+            const baseFee = this['DEYHGCF'](tender);
+            if (!tender) {
+                calcBase.baseProgressiveFee = baseFee;
+            }
+            return calculateUtil.getProgressiveFee(baseFee, '养护单位项目管理费', projectObj.project.property.progressiveInterval, decimalObj.bills.totalPrice, deficiency);
+        },
+        // 养护单位项目管理费(未单独设置项目部) 算法:以{定额养护工程费}为基数,采用累进办法计算。
+        YHDWXMGLFWSXMB(tender) {
+            const baseFee = this['DEYHGCF'](tender);
+            if (!tender) {
+                calcBase.baseProgressiveFee = baseFee;
+            }
+            return calculateUtil.getProgressiveFee(baseFee, '养护单位项目管理费(未单独设置项目部)', projectObj.project.property.progressiveInterval, decimalObj.bills.totalPrice, deficiency);
+        },
+        // 工程监理费 算法:以{定额养护工程费}为基数,采用累进办法计算。
+        GCJLF(tender) {
+            const baseFee = this['DEYHGCF'](tender);
+            if (!tender) {
+                calcBase.baseProgressiveFee = baseFee;
+            }
+            return calculateUtil.getProgressiveFee(baseFee, '工程监理费', projectObj.project.property.progressiveInterval, decimalObj.bills.totalPrice, deficiency);
+        },
+        // 工程监理费(未实行社会监理) 算法:以{定额养护工程费}为基数,采用累进办法计算。
+        GCJLFWSXSHJL(tender) {
+            const baseFee = this['DEYHGCF'](tender);
+            if (!tender) {
+                calcBase.baseProgressiveFee = baseFee;
+            }
+            return calculateUtil.getProgressiveFee(baseFee, '工程监理费(未实行社会监理)', projectObj.project.property.progressiveInterval, decimalObj.bills.totalPrice, deficiency);
+        },
+        // 设计文件审查费 算法:以{定额养护工程费}为基数,采用累进办法计算。
+        SJWJSCF(tender) {
+            const baseFee = this['DEYHGCF'](tender);
+            if (!tender) {
+                calcBase.baseProgressiveFee = baseFee;
+            }
+            return calculateUtil.getProgressiveFee(baseFee, '设计文件审查费', projectObj.project.property.progressiveInterval, decimalObj.bills.totalPrice, deficiency);
+        },
+        // 工程设计费 算法:以{定额养护工程费}为基数,采用累进办法计算。
+        GCSJF(tender) {
+            const baseFee = this['DEYHGCF'](tender);
+            if (!tender) {
+                calcBase.baseProgressiveFee = baseFee;
+            }
+            return calculateUtil.getProgressiveFee(baseFee, '工程设计费', projectObj.project.property.progressiveInterval, decimalObj.bills.totalPrice, deficiency);
+        },
+        /*  价差预备费 算法:以建筑安装工程费为基数,按设计文件编制年始至养护项目工程竣工年终的年数和年工程造价增涨率计算。
+            价差预备费 P * [(1+i)^(n-1) -1]
+            P——建筑安装工程费总额(元);
+            i——年工程造价增涨率(%);
+            n——设计文件编制年至养护项目开工年+养护项目建设期限(年)。
+        */
+        JCYBF(tender) {
+            //建筑安装工程费作为基数
+            const installFee = this['JZAZGCF'](tender);
+            //年造价增涨
+            const costGrowthRate = calcBase.project.property.costGrowthRate
+                ? calcBase.project.property.costGrowthRate
+                : 0;
+            //增涨计费年限
+            const growthPeriod = projectObj.project.property.growthPeriod
+                ? calcBase.project.property.growthPeriod
+                : 0;
+            //= P * [(1+i)^(n-1) -1]
+            return (installFee * (Math.pow(1 + costGrowthRate, growthPeriod - 1) - 1)).toDecimal(decimalObj.bills.totalPrice);
+        }
+
+    };
+
+    baseFigureTemplate.boq = {
+        //{各章清单合计}
+        // 取清单固定类别是“第100章至700章清单”的金额
+        'GZQDHJ': function (tender) {
+            return cbTools.getBaseFee(calcBase.fixedFlag.ONE_SEVEN_BILLS, tender, 'common');
+        },
+        //{专项暂定合计}
+        // 汇总专项暂定列有值的清单的金额
+        'ZXZDHJ': function (tender) {
+            let rst = 0,
+                feeField = 'common',
+                subFeeField = tender ? 'tenderTotalFee' : 'totalFee';
+            let billsData = calcBase.project.Bills.datas,
+                filterData = billsData.filter(function (data) {
+                    return data.specialProvisional;
+                });
+            for (let data of filterData) {
+                if (cbTools.isUnDef(data.feesIndex) || _.isEmpty(data.feesIndex) ||
+                    cbTools.isUnDef(data.feesIndex[feeField]) || cbTools.isUnDef(data.feesIndex[feeField][subFeeField])) {
+                    continue;
+                }
+                rst += data.feesIndex[feeField][subFeeField];
+            }
+            return rst.toDecimal(decimalObj.bills.totalPrice);
+        },
+        //{100章以外清单合计}
+        // 取清单固定清单[第100章至700章清单]的金额,但扣除清单100章下的金额。
+        // 如果是固定清单[第100章至700章清单]下100章以外清单引用此基数,要排除自身(目前只允许100章的清单使用,所以暂时不需要此判断)
+        'YBZYHQDHJ': function (tender) {
+            let oneToSeven = cbTools.findNodeByFlag(fixedFlag.ONE_SEVEN_BILLS);
+            if (!oneToSeven) {
+                return 0;
+            }
+            //100-700章固定节点的所有子节点
+            let allChildren = [];
+            function getChildren(nodes) {
+                allChildren = allChildren.concat(nodes);
+                for (let node of nodes) {
+                    if (node.children.length > 0) {
+                        getChildren(node.children);
+                    }
+                }
+            }
+            getChildren(oneToSeven.children);
+            //扣除的节点:100章的节点[100-200)
+            let deductNodes = allChildren.filter(cbTools.withingOneHundred);
+            //计算金额
+            let fullFeeField = tender ? 'common.tenderTotalFee' : 'common.totalFee';
+            return projectObj.project.calcProgram.getTotalFee([oneToSeven], deductNodes, fullFeeField).toDecimal(decimalObj.bills.totalPrice);
+        }
+    };
+}
+
+// CommonJS module
+if (typeof module !== 'undefined' && !module.nodeType) { // 防止module是前端的一个html标签
+    module.exports = {
+        progression,
+        deficiency
+    };
+}

+ 12 - 29
web/over_write/js/anhui_2019.js

@@ -96,7 +96,7 @@ if (typeof baseFigureMap !== 'undefined') {
         // 显示:仅清单固定类别是“安全生产费”的可显示。
         '建筑安装工程费(不含设备费)': {
             base: 'JZAZGCFBHSB',
-            fixedFlag: fixedFlag.CONSTRUCTION_INSTALL_FEE,
+            fixedFlag: null,
             filter: [fixedFlag.SAFE_COST],
             pick: true
         },
@@ -110,7 +110,7 @@ if (typeof baseFigureMap !== 'undefined') {
         // 显示:仅清单固定类别是“施工场地建设费”的可显示。
         '定额建筑安装工程费(不含定额设备购置费及专项管理费)': {
             base: 'DEJZAZGCFBHSBZXGLF',
-            fixedFlag: fixedFlag.CONSTRUCTION_INSTALL_FEE,
+            fixedFlag: null,
             filter: [fixedFlag.CONSTRUCTION_PLANT_COST],
             pick: true,
         },
@@ -230,53 +230,38 @@ if (typeof baseFigureTemplate !== 'undefined') {
     baseFigureTemplate.budget = {
         // 建筑安装工程费 算法:取清单固定类别是“建筑安装工程费”的金额。
         JZAZGCF(tender) {
-            const feeField = 'common';
-            const subFeeField = tender ? 'tenderTotalFee' : 'totalFee';
-            return cbTools.getBillsFee(fixedFlag.CONSTRUCTION_INSTALL_FEE, feeField, subFeeField);
+            return cbTools.getBaseFee(fixedFlag.CONSTRUCTION_INSTALL_FEE, tender, 'common');
         },
         // 建筑安装工程费(不含安全生产费) 算法:取清单固定类别是“建筑安装工程费”的金额,扣除“安全生产费”的金额。
         JZAZGCFBHSC(tender) {
-            const fullFeeField = tender ? 'common.tenderTotalFee' : 'common.totalFee';
-            const deductFlags = [fixedFlag.SAFE_COST];
-            return cbTools.getFeeWithDeduction(fixedFlag.CONSTRUCTION_INSTALL_FEE, deductFlags, fullFeeField).toDecimal(decimalObj.bills.totalPrice);
+            return cbTools.getFeeWithDeduction(fixedFlag.CONSTRUCTION_INSTALL_FEE, [fixedFlag.SAFE_COST], tender, 'common');
         },
         // 建筑安装工程费(不含设备费) 算法:取清单固定类别是“建筑安装工程费”的金额,扣除设备的金额。
         JZAZGCFBHSB(tender) {
-            const fullFeeField = tender ? 'common.tenderTotalFee' : 'common.totalFee';
-            const deductFlags = [fixedFlag.EQUIPMENT_ACQUISITION_FEE];
-            return cbTools.getFeeWithDeduction(fixedFlag.CONSTRUCTION_INSTALL_FEE, deductFlags, fullFeeField).toDecimal(decimalObj.bills.totalPrice);
+            return cbTools.getFeeWithDeduction(fixedFlag.CONSTRUCTION_INSTALL_FEE, [fixedFlag.EQUIPMENT_ACQUISITION_FEE], tender, 'common');
         },
         // 定额建筑安装工程费 算法:取清单固定类别是“建筑安装工程费”的定额建安费。
         DEJZAZGCF(tender) {
-            const feeField = 'rationCommon';
-            const subFeeField = tender ? 'tenderTotalFee' : 'totalFee';
-            return cbTools.getBillsFee(fixedFlag.CONSTRUCTION_INSTALL_FEE, feeField, subFeeField);
+            return cbTools.getBaseFee(fixedFlag.CONSTRUCTION_INSTALL_FEE, tender, 'rationCommon');
         },
         // 定额建筑安装工程费(不含定额设备购置费及专项管理费) 算法:取清单固定类别是“建筑安装工程费”的“定额建安费”,扣除设备的“定额设备费”,扣除“专项费用”的“定额建安费”。
         DEJZAZGCFBHSBZXGLF(tender) {
-            const fullFeeField = tender ? 'rationCommon.tenderTotalFee' : 'rationCommon.totalFee';
-            const deductFlags = [fixedFlag.EQUIPMENT_ACQUISITION_FEE, fixedFlag.SAFE_COST];
+            const deductFlags = [fixedFlag.EQUIPMENT_ACQUISITION_FEE, fixedFlag.SPECIAL_COST];
             //建安费扣除设备费
-            return cbTools.getFeeWithDeduction(fixedFlag.CONSTRUCTION_INSTALL_FEE, deductFlags, fullFeeField).toDecimal(decimalObj.bills.totalPrice);
+            return cbTools.getFeeWithDeduction(fixedFlag.CONSTRUCTION_INSTALL_FEE, deductFlags, tender, 'rationCommon');
         },
         // 定额建筑安装工程费(不含专项管理费) 算法:取清单固定类别是“建筑安装工程费”的“定额建安费”,扣除“专项费用”的“定额建安费”。
         DEJZAZGCFBHZXGLF(tender) {
-            const fullFeeField = tender ? 'rationCommon.tenderTotalFee' : 'rationCommon.totalFee';
-            const deductFlags = [fixedFlag.SAFE_COST];
             //建安费扣除设备费
-            return cbTools.getFeeWithDeduction(fixedFlag.CONSTRUCTION_INSTALL_FEE, deductFlags, fullFeeField).toDecimal(decimalObj.bills.totalPrice);
+            return cbTools.getFeeWithDeduction(fixedFlag.CONSTRUCTION_INSTALL_FEE, [fixedFlag.SPECIAL_COST], tender, 'rationCommon');
         },
         // 土地使用及拆迁补偿费 算法:取清单固定类别是“土地使用及拆迁补偿费”的金额。
         TDSYJCQBCF(tender) {
-            const feeField = 'common';
-            const subFeeField = tender ? 'tenderTotalFee' : 'totalFee';
-            return cbTools.getBillsFee(fixedFlag.LAND_USED_DEMOLITION, feeField, subFeeField);
+            return cbTools.getBaseFee(fixedFlag.LAND_USED_DEMOLITION, tender, 'common');
         },
         // 工程建设其他费用 算法:取清单固定类别是“养护工程其他费用”的金额。
         GCJSQTFY(tender) {
-            const feeField = 'common';
-            const subFeeField = tender ? 'tenderTotalFee' : 'totalFee';
-            return cbTools.getBillsFee(fixedFlag.MAINTENANCE_EXPENSES, feeField, subFeeField);
+            return cbTools.getBaseFee(fixedFlag.MAINTENANCE_EXPENSES, tender, 'common');
         },
         // 施工场地建设费 算法:以{定额建筑安装工程费(不含定额设备购置费及专项管理费) }为基数,采用累进办法计算。
         SGCDJSF(tender) {
@@ -353,9 +338,7 @@ if (typeof baseFigureTemplate !== 'undefined') {
         //{各章清单合计}
         // 取清单固定类别是“第100章至700章清单”的金额
         'GZQDHJ': function (tender) {
-            let feeField = 'common',
-                subFeeField = tender ? 'tenderTotalFee' : 'totalFee';
-            return cbTools.getBillsFee(calcBase.fixedFlag.ONE_SEVEN_BILLS, feeField, subFeeField);
+            return cbTools.getBaseFee(calcBase.fixedFlag.ONE_SEVEN_BILLS, tender, 'common');
         },
         //{专项暂定合计}
         // 汇总专项暂定列有值的清单的金额

+ 8 - 21
web/over_write/js/neimeng_2019.js

@@ -206,44 +206,33 @@ if (typeof baseFigureTemplate !== 'undefined') {
         // 定额建筑安装工程费(定额设备购置费按40%计):取清单固定类别是“建筑安装工程”的定额建安费,其中定额设备购置费按40%计算
         DEJZAZGCFSBSS(tender) {
             const feeField = 'rationCommon';
-            const subFeeField = tender ? 'tenderTotalFee' : 'totalFee';
             const deductFlags = [fixedFlag.EQUIPMENT_ACQUISITION_FEE];
             //建安费扣除定额设备购置费
-            const afterDeductFee = cbTools.getFeeWithDeduction(fixedFlag.CONSTRUCTION_INSTALL_FEE, deductFlags, `${feeField}.${subFeeField}`);
+            const afterDeductFee = cbTools.getFeeWithDeduction(fixedFlag.CONSTRUCTION_INSTALL_FEE, deductFlags, tender, feeField, false);
             //定额设备购置费
-            const equipmentAcFee = cbTools.getBillsFee(deductFlags[0], feeField, subFeeField);
+            const equipmentAcFee = cbTools.getBaseFee(deductFlags[0], tender, feeField);
             return (afterDeductFee + equipmentAcFee * 0.4).toDecimal(decimalObj.bills.totalPrice);
         },
         // 建筑安装工程费(不含设备费):取清单固定类别是“建筑安装工程”的金额,但要扣除清单固定类别是“设备购置费”的金额
         JZAZGCFBHSB(tender) {
-            const fullFeeField = tender ? 'common.tenderTotalFee' : 'common.totalFee';
-            const deductFlags = [fixedFlag.EQUIPMENT_ACQUISITION_FEE];
             //建安费扣除设备费
-            return cbTools.getFeeWithDeduction(fixedFlag.CONSTRUCTION_INSTALL_FEE, deductFlags, fullFeeField).toDecimal(decimalObj.bills.totalPrice);
+            return cbTools.getFeeWithDeduction(fixedFlag.CONSTRUCTION_INSTALL_FEE, [fixedFlag.EQUIPMENT_ACQUISITION_FEE], tender, 'common');
         },
         // 建筑安装工程费:取清单固定类别是“建筑安装工程”的金额
         JZAZGCF(tender) {
-            const feeField = 'common';
-            const subFeeField = tender ? 'tenderTotalFee' : 'totalFee';
-            return cbTools.getBillsFee(fixedFlag.CONSTRUCTION_INSTALL_FEE, feeField, subFeeField);
+            return cbTools.getBaseFee(fixedFlag.CONSTRUCTION_INSTALL_FEE, tender, 'common');
         },
         // 土地使用及拆迁补偿费:取清单固定类别是“土地使用及拆迁补偿费”的金额
         TDSYJCQBCF(tender) {
-            const feeField = 'common';
-            const subFeeField = tender ? 'tenderTotalFee' : 'totalFee';
-            return cbTools.getBillsFee(fixedFlag.LAND_USED_DEMOLITION, feeField, subFeeField);
+            return cbTools.getBaseFee(fixedFlag.LAND_USED_DEMOLITION, tender, 'common');
         },
         // 养护工程其他费用:取清单固定类别是“养护工程其他费用”的金额
         YHGCQTFY(tender) {
-            const feeField = 'common';
-            const subFeeField = tender ? 'tenderTotalFee' : 'totalFee';
-            return cbTools.getBillsFee(fixedFlag.MAINTENANCE_EXPENSES, feeField, subFeeField);
+            return cbTools.getBaseFee(fixedFlag.MAINTENANCE_EXPENSES, tender, 'common');
         },
         // 预备费:取清单固定类别是“预备费”的金额
         YBF(tender) {
-            const feeField = 'common';
-            const subFeeField = tender ? 'tenderTotalFee' : 'totalFee';
-            return cbTools.getBillsFee(fixedFlag.BUDGET_FEE, feeField, subFeeField);
+            return cbTools.getBaseFee(fixedFlag.BUDGET_FEE, tender, 'common');
         },
         // 养护单位项目管理费:以累进办法计算,计算基数为“定额建筑安装工程费(定额设备购置费按40%计)
         YHDWXMGLF(tender) {
@@ -304,9 +293,7 @@ if (typeof baseFigureTemplate !== 'undefined') {
         //{各章清单合计}
         // 取清单固定类别是“第100章至700章清单”的金额
         'GZQDHJ': function (tender) {
-            let feeField = 'common',
-                subFeeField = tender ? 'tenderTotalFee' : 'totalFee';
-            return cbTools.getBillsFee(calcBase.fixedFlag.ONE_SEVEN_BILLS, feeField, subFeeField);
+            return cbTools.getBaseFee(calcBase.fixedFlag.ONE_SEVEN_BILLS, tender, 'common');
         },
         //{专项暂定合计}
         // 汇总专项暂定列有值的清单的金额

+ 5 - 15
web/over_write/js/zhejiang_2005.js

@@ -126,21 +126,15 @@ if (typeof baseFigureTemplate !== 'undefined') {
     baseFigureTemplate.budget = {
         // 公路养护工程费:取清单固定类别是“建筑安装工程费”的金额
         GLYHGCF(tender) {
-            const feeField = 'common';
-            const subFeeField = tender ? 'tenderTotalFee' : 'totalFee';
-            return cbTools.getBillsFee(calcBase.fixedFlag.CONSTRUCTION_INSTALL_FEE, feeField, subFeeField);
+            return cbTools.getBaseFee(calcBase.fixedFlag.CONSTRUCTION_INSTALL_FEE, tender, 'common');
         },
         // 设备购置费用:取清单固定类别是“设备购置费”的金额
         SBGZFY(tender) {
-            const feeField = 'common';
-            const subFeeField = tender ? 'tenderTotalFee' : 'totalFee';
-            return cbTools.getBillsFee(calcBase.fixedFlag.EQUIPMENT_ACQUISITION_FEE, feeField, subFeeField);
+            return cbTools.getBaseFee(calcBase.fixedFlag.EQUIPMENT_ACQUISITION_FEE, tender, 'common');
         },
         // 公路养护工程其他费用:取清单固定类别是“养护工程其他费用”的金额。
         GLYHGCQTFY(tender) {
-            const feeField = 'common';
-            const subFeeField = tender ? 'tenderTotalFee' : 'totalFee';
-            return cbTools.getBillsFee(calcBase.fixedFlag.MAINTENANCE_EXPENSES, feeField, subFeeField);
+            return cbTools.getBaseFee(calcBase.fixedFlag.MAINTENANCE_EXPENSES, tender, 'common');
         },
         // 养护工程管理经费:取清单固定类别是“建筑安装工程费”金额为基数,采用累进办法计算
         YHGCGLJF(tender) {
@@ -152,9 +146,7 @@ if (typeof baseFigureTemplate !== 'undefined') {
         },
         // 一二三部分合计:取清单固定类别是“一二三部分合计”的金额
         YESBFHJ(tender) {
-            const feeField = 'common';
-            const subFeeField = tender ? 'tenderTotalFee' : 'totalFee';
-            return cbTools.getBillsFee(calcBase.fixedFlag.ONE_TO_THREE_TOTAL, feeField, subFeeField);
+            return cbTools.getBaseFee(calcBase.fixedFlag.ONE_TO_THREE_TOTAL, tender, 'common');
         },
         // 工程造价增涨预留费:以{公路养护工程费}为基数,= P * [(1+i)^(n-1) -1]
         // P:公路养护工程费总额(元)
@@ -178,9 +170,7 @@ if (typeof baseFigureTemplate !== 'undefined') {
         //{各章清单合计}
         // 取清单固定类别是“第100章至700章清单”的金额
         'GZQDHJ': function (tender) {
-            let feeField = 'common',
-                subFeeField = tender ? 'tenderTotalFee' : 'totalFee';
-            return cbTools.getBillsFee(calcBase.fixedFlag.ONE_SEVEN_BILLS, feeField, subFeeField);
+            return cbTools.getBaseFee(calcBase.fixedFlag.ONE_SEVEN_BILLS, tender, 'common');
         },
         //{专项暂定合计}
         // 汇总专项暂定列有值的清单的金额