Browse Source

Merge branch 'master' of http://192.168.1.41:3000/SmartCost/ConstructionCost

TonyKang 5 years ago
parent
commit
d28167aec7
41 changed files with 1047 additions and 635 deletions
  1. 2 1
      modules/all_models/user.js
  2. 9 0
      modules/main/controllers/bills_controller.js
  3. 0 9
      modules/main/controllers/project_controller.js
  4. 13 0
      modules/main/facade/bill_facade.js
  5. 20 3
      modules/main/facade/project_facade.js
  6. 4 5
      modules/main/facade/ration_facade.js
  7. 100 0
      modules/main/middleware/index.js
  8. 0 53
      modules/main/middleware/system_setting.js
  9. 1 0
      modules/main/routes/bills_route.js
  10. 1 1
      modules/main/routes/ration_route.js
  11. 47 40
      modules/pm/facade/pm_facade.js
  12. 1 1
      modules/pm/routes/pm_route.js
  13. 4 1
      modules/users/controllers/login_controller.js
  14. 10 7
      modules/users/models/log_model.js
  15. 20 3
      modules/users/models/user_model.js
  16. 18 1
      public/common_util.js
  17. 3 1
      public/web/commonAlert.js
  18. 3 1
      public/web/common_ajax.js
  19. 43 3
      public/web/id_tree.js
  20. 91 0
      public/web/sheet/sheet_common.js
  21. 9 0
      public/web/socket/connection.js
  22. 5 0
      public/web/syntax-detection.js
  23. 8 37
      server.js
  24. 5 0
      socket.js
  25. 12 0
      web/building_saas/css/custom.css
  26. 79 0
      web/building_saas/main/js/controllers/project_controller.js
  27. 3 0
      web/building_saas/main/js/models/bills.js
  28. 31 0
      web/building_saas/main/js/models/cache_tree.js
  29. 245 339
      web/building_saas/main/js/models/calc_program.js
  30. 1 0
      web/building_saas/main/js/models/installation_fee.js
  31. 37 58
      web/building_saas/main/js/models/ration.js
  32. 12 3
      web/building_saas/main/js/views/billsElf.js
  33. 6 1
      web/building_saas/main/js/views/character_content_view.js
  34. 1 1
      web/building_saas/main/js/views/item_increase_fee_view.js
  35. 72 34
      web/building_saas/main/js/views/project_view.js
  36. 6 0
      web/building_saas/pm/js/pm_gc.js
  37. 64 18
      web/building_saas/pm/js/pm_newMain.js
  38. 1 1
      web/building_saas/pm/js/pm_share.js
  39. 31 0
      web/building_saas/pm/js/pm_tree.js
  40. 3 0
      web/common/html/header.html
  41. 26 13
      web/over_write/js/guangdong_2018_export.js

+ 2 - 1
modules/all_models/user.js

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

+ 9 - 0
modules/main/controllers/bills_controller.js

@@ -266,6 +266,15 @@ module.exports = {
             }
 
         });
+    },
+    insertBills: async function (req, res) {
+        let data = JSON.parse(req.body.data);
+        try {
+            await bill_facade.insertBills(data.postData);
+            callback(req, res, 0, 'success', null);
+        } catch (err) {
+            callback(req, res, 1, err, null);
+        }
     }
 };
 

+ 0 - 9
modules/main/controllers/project_controller.js

@@ -98,15 +98,6 @@ module.exports = {
         res.json(result);
     },
     updateNodes:async function (req,res) {
-    /*    var data = JSON.parse(req.body.data);
-        project_facade.updateNodes(data, function (err, message, result) {
-            if (err) {
-                logger.err(err);
-                callback(req, res, err, message, null);
-            } else {
-                callback(req, res, err, message, result);
-            }
-        });*/
         let result={
             error:0
         }

+ 13 - 0
modules/main/facade/bill_facade.js

@@ -111,6 +111,19 @@ module.exports={
         return results;
 
     },
+    insertBills: async function(datas) {
+        let bulks = [];
+        for (let data of datas) {
+            if (data.updateType === 'update') {
+                bulks.push({updateOne: {filter: {ID: data.updateData.ID}, update: {NextSiblingID: data.updateData.NextSiblingID}}});
+            } else {
+                bulks.push({insertOne: {document: data.updateData}});
+            }
+        }
+        if (bulks.length > 0) {
+            await bill_Model.bulkWrite(bulks);
+        }
+    }
 };
 
 async function pasteOtherData(data) {

+ 20 - 3
modules/main/facade/project_facade.js

@@ -48,8 +48,8 @@ const billsLibDao = require("../../bills_lib/models/bills_lib_interfaces");
 async function calcInstallationFee(data) {
     let result={};
     let projectGLJList = [];
-    let billTasks  = generateTasks(data.bills,data.useID);
-    let rationTasks = generateTasks(data.ration,data.useID);
+    let billTasks  = await generateTasks(data.bills,data.useID,true);
+    let rationTasks =await generateTasks(data.ration,data.useID);
     if(billTasks.length>0){
         await bill_model.model.bulkWrite(billTasks);
     }
@@ -104,6 +104,7 @@ async function calcInstallationFee(data) {
     result.update = updateList;
     result.add = newGljList;
     result.projectGLJList = projectGLJList;
+    result.updateData = data;
     return result;
 }
 
@@ -118,7 +119,7 @@ async function createRationGLJData(glj) {
 
 
 
-function generateTasks(data,userID) {
+async function generateTasks(data,userID,isBills=false) {
     let tasks=[];
     let deleteInfo={deleted: true, deleteDateTime: new Date(), deleteBy: userID};
     if(data.delete && data.delete.length > 0){
@@ -148,6 +149,22 @@ function generateTasks(data,userID) {
     }
     if(data.add && data.add.length > 0){
         for(let n_data of data.add){
+          if(isBills == true){
+            let stdBills = await billsLibDao.getStdBillsByCode({userId: n_data.userID, billsLibId: n_data.billsLibId, code: n_data.stdCode});
+            stdBills = stdBills ? stdBills._doc : null;
+            if (stdBills) {
+                // 获取项目清单所需要的数据
+                const projectBillsData = billsLibDao.getDataToProjectBills(stdBills);
+                n_data.jobContent = projectBillsData.jobContent;
+                n_data.itemCharacter = projectBillsData.itemCharacter;
+                n_data.itemCharacterText = projectBillsData.itemCharacterText;
+                n_data.jobContentText = projectBillsData.ijobContentText;
+            } else {
+                delete n_data.billsLibId;
+            }
+            delete n_data.userID;
+            delete n_data.stdCode;
+          }
             let task = {
                 insertOne :{
                     document:n_data

+ 4 - 5
modules/main/facade/ration_facade.js

@@ -75,12 +75,11 @@ async function addNewRation(data,compilation) {
 }
 
 async function addMultiRation(datas,compilation) {
-    let rst = [];
-    for(let data of datas){
-        let r = await addNewRation(data,compilation);
-        rst.push(r);
+    const task = [];
+    for (const data of datas) {
+        task.push(addNewRation(data, compilation));
     }
-    return rst;
+    return await Promise.all(task);
 }
 
 async function deleteMultiRation(rations) {//这里是只有删除的情况,删除定额的同时删除定额下挂的其它子项目

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

@@ -0,0 +1,100 @@
+/**
+ * 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 - 53
modules/main/middleware/system_setting.js

@@ -1,53 +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 - 0
modules/main/routes/bills_route.js

@@ -21,6 +21,7 @@ module.exports = function (app) {
     billsRouter.post('/pasteBlock', billsController.pasteBlock);
     billsRouter.post('/import', billsController.import);
     billsRouter.get('/downloadExamp', billsController.downloadExample);
+    billsRouter.post('/insertBills', billsController.insertBills);
     app.use('/bills', billsRouter);
 };
 

+ 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();

+ 47 - 40
modules/pm/facade/pm_facade.js

@@ -130,7 +130,8 @@ 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",
@@ -2105,47 +2106,48 @@ async function importTenderDetail(tenderData) {
     if (tenderData.quantityDetails && tenderData.quantityDetails.length) {
         await quantityDetailModel.insertMany(tenderData.quantityDetails);
     }
-    //投标文件中,才会有下面这些数据
     if (enterDetail(tenderData)) {
         //匹配标准数据,更新一些标准数据
         await setupStdData(tenderData);
-        let task = [];
-        //定额
-        if (tenderData.ration.length) {
-            task.push(rationModel.insertMany(tenderData.ration))
-        }
-        //定额人材机
-        if (tenderData.rationGLJ.length) {
-            task.push(rationGLJModel.insertMany(tenderData.rationGLJ));
-        }
-        //定额调整系数
-        if (tenderData.rationCoe.length) {
-            task.push(rationCoeModel.insertMany(tenderData.rationCoe));
-        }
-        //项目人材机
-        if (tenderData.projectGLJ.length) {
-            task.push(gljListModel.insertMany(tenderData.projectGLJ));
-        }
-        // 承包人材料
-        if (tenderData.contractorList.length) {
-           task.push(contractorListModel.insertMany(tenderData.contractorList));
-        }
-        // 评标材料表
-        if (tenderData.bidEvaluationList.length) {
-            task.push(bidListModel.insertMany(tenderData.bidEvaluationList));
-        }
-        // 暂估价材料表
-        if (tenderData.evaluationList.length) {
-            task.push(evaluateListModel.insertMany(tenderData.evaluationList));
-        }
-        //组成物
-        if (tenderData.mixRatio.length) {
-            task.push(mixRatioModel.insertMany(tenderData.mixRatio));
-        }
-        //单价文件
-        if (tenderData.unitPrice.length) {
-            task.push(unitPriceModel.insertMany(tenderData.unitPrice));
-        }
+    }
+    let task = [];
+    //定额
+    if (tenderData.ration.length) {
+        task.push(rationModel.insertMany(tenderData.ration))
+    }
+    //定额人材机
+    if (tenderData.rationGLJ.length) {
+        task.push(rationGLJModel.insertMany(tenderData.rationGLJ));
+    }
+    //定额调整系数
+    if (tenderData.rationCoe.length) {
+        task.push(rationCoeModel.insertMany(tenderData.rationCoe));
+    }
+    //项目人材机
+    if (tenderData.projectGLJ.length) {
+        task.push(gljListModel.insertMany(tenderData.projectGLJ));
+    }
+    // 承包人材料
+    if (tenderData.contractorList.length) {
+        task.push(contractorListModel.insertMany(tenderData.contractorList));
+    }
+    // 评标材料表
+    if (tenderData.bidEvaluationList.length) {
+        task.push(bidListModel.insertMany(tenderData.bidEvaluationList));
+    }
+    // 暂估价材料表
+    if (tenderData.evaluationList.length) {
+        task.push(evaluateListModel.insertMany(tenderData.evaluationList));
+    }
+    //组成物
+    if (tenderData.mixRatio.length) {
+        task.push(mixRatioModel.insertMany(tenderData.mixRatio));
+    }
+    //单价文件
+    if (tenderData.unitPrice.length) {
+        task.push(unitPriceModel.insertMany(tenderData.unitPrice));
+    }
+    if (task.length) {
         await Promise.all(task);
     }
     //继续处理定额等数据
@@ -2960,6 +2962,11 @@ function uploadToken() {
     return result
 }
 
+// 获取系统设置,这个系统设置正常情况下有存在session中
+async function getSystemSetting() {
+    return await systemSettingModel.findOne({}).lean();
+}
+
 // 有些方法无法通过中间件就检查单位工程数量是否超限
 // 需要到具体的业务代码中进行判断
 // 这个方法就是具体业务代码中,需要检查单位工程数量是否超限用
@@ -2967,7 +2974,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');
 
 
 
@@ -78,6 +79,7 @@ class LoginController {
                     mobile: userData.mobile,
                     qq: userData.qq,
                     isUserActive: userData.isUserActive,
+                    token: uuidV1(),
                 };
 
                 request.session.sessionUser = sessionUser;
@@ -260,7 +262,8 @@ class LoginController {
                 mobile: userData.mobile,
                 qq: userData.qq,
                 isUserActive: userData.isUserActive,
-                newLogin:true
+                newLogin:true,
+                token: uuidV1(),
             };
 
             request.session.sessionUser = sessionUser;

+ 10 - 7
modules/users/models/log_model.js

@@ -59,9 +59,10 @@ class LogModel extends BaseModel {
      * @return {Promise}
      */
     async addLoginLog(userId, request) {
-        let ip = request.connection.remoteAddress;
+    /*     let ip = request.connection.remoteAddress;
         ip = ip?ip.split(':'):[];
-        ip = ip[3] === undefined ? '' : ip[3];
+        ip = ip[3] === undefined ? '' : ip[3]; */
+        let ip = request.headers["x-real-ip"]? request.headers["x-real-ip"]:"";
         // let ipInfo = '127.0.0.1';//await this.getIpInfoFromApi(ip);
         let ipInfo = await this.getIpInfoFromApi(ip);
 
@@ -114,19 +115,21 @@ class LogModel extends BaseModel {
         if (ip === '') {
             return result;
         }
-
         if (ip === '127.0.0.1') {
             return '服务器本机访问';
         }
-        let getData = {
-            url: 'http://ip.taobao.com/service/getIpInfo.php?ip=' + ip,
+        let option = {
+            url: 'https://api01.aliyun.venuscn.com/ip?ip=' + ip,
             encoding: 'utf8',
-            timeout:2000
+            timeout:2000,
+            headers: {
+                Authorization: 'APPCODE ' + '85c64bffe70445c4af9df7ae31c7bfcc',
+            }
         };
         return new Promise(function (resolve, reject) {
             try {
                 // 请求接口
-                Request.get(getData, function (err, getResponse, body) {
+                Request(option, function (err, getResponse, body) {
                     if (err) {
                         //throw '请求错误';
                         resolve("请求错误");

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

@@ -184,6 +184,7 @@ class UserModel extends BaseModel {
                 qq: userData.qq,
                 latest_login:userData.latest_login,
                 isUserActive: userData.isUserActive,
+                token: userData.token
             };
             console.log("updateUser 开始 -------------------------------");
             let updateResult = await this.updateUser(condition,UpdateData);
@@ -258,9 +259,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) {
@@ -291,6 +307,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);
     }

+ 18 - 1
public/common_util.js

@@ -88,6 +88,22 @@ function deleteEmptyObject(arr) {
             return sorted;
         }
     }
+    
+    // 控制全屏(浏览器有限制)
+    // 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();
+        }
+    }
 
     return {
         isDef,
@@ -96,6 +112,7 @@ function deleteEmptyObject(arr) {
         similarEqual,
         getRequired,
         getSortedTreeData,
-        isNotEmptyObject
+        isNotEmptyObject,
+        handleFullscreen
     };
 });

+ 3 - 1
public/web/commonAlert.js

@@ -11,7 +11,9 @@
 window.alert = function(str) {
     /*$('#commonAlert').find('p').text(str);
      $('#commonAlert').modal('show');*/
-    hintBox.infoBox('系统提示', str, 1);
+    if (str) {
+        hintBox.infoBox('系统提示', str, 1);
+    }
 };
 
 

+ 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);
         }

+ 43 - 3
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,14 +625,47 @@ 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){
                 this.nodes[this.prefix + data.ID] = new Node(this, data.ID);
@@ -831,6 +864,13 @@ var idTree = {
                         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) {

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

@@ -1505,5 +1505,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

@@ -91,6 +91,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();

+ 5 - 0
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;

+ 8 - 37
server.js

@@ -23,8 +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();
 let _rootDir = __dirname;
@@ -56,41 +54,14 @@ 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 (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
-                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');
-        }
-        next();
-        await online_logs.saveOnlineTime(req);//记录登录时长
-    }
+/* 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);
 });
 
 //加载路由文件

+ 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]) {

+ 12 - 0
web/building_saas/css/custom.css

@@ -489,4 +489,16 @@ margin-right: 100px !important;
 .form-control-inline {
   display: inline-block !important;
   width: 82%;
+}
+
+/* 右键菜单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;
 }

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

@@ -53,6 +53,85 @@ ProjectController = {
             cbTools.refreshFormulaNodes();
         });
     },
+    addBillsByData: async function (postData, isSameDepth = false) {
+        if (!postData || !postData.length) {
+            return [];
+        }
+        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, type) {
+        const project = projectObj.project;
+        const target = project.getParentTarget(project.mainTree.selected, 'sourceType', project.Bills.getSourceType());
+        const targetType = target.data.type;
+        let baseParentID;
+        let baseNextID;
+        let updateNode;
+
+        const targetIsFXOrBX = targetType === billType.FX || targetType === billType.BX;
+        const beLastChild = (type === billType.FX && targetType === billType.FB) || 
+            (type === billType.BILL && target.depth() === 0);
+        const beNextBrother = (type === billType.FX  && targetIsFXOrBX) ||
+            (type === billType.BILL && target.depth() > 0);
+        if (beLastChild) {
+            baseParentID = target.source.getID();
+            baseNextID = -1;
+            updateNode = target.source.children[target.source.children.length - 1];
+        } else if (beNextBrother) {
+            baseParentID = target.source.getParentID();
+            baseNextID =  target.source.getNextSiblingID();
+            updateNode = target;
+        } else {
+            return [];
+        }
+        const insertData = [];
+        for (let i = 0; i < number; i++) {
+            const data = {
+                //type: billType.BILL,
+                type,
+                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());

+ 3 - 0
web/building_saas/main/js/models/bills.js

@@ -987,6 +987,9 @@ var Bills = {
                 code : this.newFormatCode(code),
                 name:'安装增加费',
                 unit:'元',
+                stdCode:code,
+                userID:userID,
+                billsLibId:projectObj.project.projectInfo.engineeringInfo.bill_lib[0].id,
                 quantity:'1'
             };
             return data;

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

@@ -415,6 +415,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){

+ 245 - 339
web/building_saas/main/js/models/calc_program.js

@@ -3,92 +3,6 @@
  * 计算程序。所有定额、清单、父清单的计算都从此入。
  */
 
-/*  新版GLD 取消了默认清单模板,所以这里废弃。先留着,预防不时之需。
-let defaultBillTemplate = {
-    ID: 15,
-    name: "清单公式",
-    calcItems: [
-        {
-            ID: 1,
-            code: "1",
-            name: "定额直接费",
-            dispExpr: "F2+F3+F4",
-            statement: "人工费+材料费+机械费",
-            feeRate: null,
-            memo: ''
-        },
-        {
-            ID: 2,
-            code: "1.1",
-            name: "人工费",
-            dispExpr: "HJ",
-            statement: "合计",
-            feeRate: 50,
-            fieldName: 'labour',
-            memo: ''
-        },
-        {
-            ID: 3,
-            code: "1.2",
-            name: "材料费",
-            dispExpr: "HJ",
-            statement: "合计",
-            feeRate: 30,
-            fieldName: 'material',
-            memo: ''
-        },
-        {
-            ID: 4,
-            code: "1.3",
-            name: "机械费",
-            dispExpr: "HJ",
-            statement: "合计",
-            feeRate: 20,
-            fieldName: 'machine',
-            memo: ''
-        },
-        {
-            ID: 5,
-            code: "2",
-            name: "企业管理费",
-            dispExpr: "F1",
-            statement: "定额直接费",
-            feeRate: null,
-            fieldName: 'manage',
-            memo: ''
-        },
-        {
-            ID: 6,
-            code: "3",
-            name: "利润",
-            dispExpr: "F1",
-            statement: "定额直接费",
-            feeRate: null,
-            fieldName: 'profit',
-            memo: ''
-        },
-        {
-            ID: 7,
-            code: "4",
-            name: "风险费用",
-            dispExpr: "F1",
-            statement: "定额直接费",
-            feeRate: null,
-            fieldName: 'risk',
-            memo: ''
-        },
-        {
-            ID: 8,
-            code: "5",
-            name: "综合单价",
-            dispExpr: "F1+F5+F6+F7",
-            statement: "定额直接费+企业管理费+利润+风险费用",
-            feeRate: null,
-            fieldName: 'common',
-            memo: ''
-        }
-    ]
-};*/
 let calcTools = {
     getNodeByFlag: function (flag) {
         let bill = cbTools.findBill(flag);
@@ -284,7 +198,7 @@ let calcTools = {
         if (!feeObj) return;
         if (feeObj.fieldName == '') return;
 
-        // 初始化前先拦截末定义的情况
+        // 初始化前先拦截属性末定义、又要给该属性赋0的情况
         if (!treeNode.data.feesIndex || !treeNode.data.feesIndex[feeObj.fieldName]){
             if (feeObj.unitFee == 0 && feeObj.totalFee == 0 && feeObj.tenderUnitFee == 0 && feeObj.tenderTotalFee == 0) return;
         }
@@ -1126,12 +1040,14 @@ let calcTools = {
         }
         return totalFee > maxPrice;
     },
+    
     getTenderCalcType: function () {
         let tenderSetting = projectObj.project.property.tenderSetting;
         let ct = tenderSetting && tenderSetting.calcPriceOption? tenderSetting.calcPriceOption : "coeBase";
         if (ct == 'priceBase') ct = 'priceBase_RCJ';   // 兼容旧项目
         return ct;
     }
+
 };
 
 let rationCalcBases = {
@@ -1372,8 +1288,8 @@ let analyzer = {
             for (let base of arrBase){
                 let baseName = base.slice(1, -1);
                 if (!rationCalcBases[baseName]){
-                    analyzer.error = `定额基数${hintBox.font('[' +baseName + ']')}定义!`;
-                    // hintBox.infoBox('错误提示', `定额基数${hintBox.font('[' +baseName + ']')}定义!`, 1);
+                    analyzer.error = `定额基数${hintBox.font('[' +baseName + ']')}定义!`;
+                    // hintBox.infoBox('错误提示', `定额基数${hintBox.font('[' +baseName + ']')}定义!`, 1);
                     return false;
                 }
             };
@@ -1587,7 +1503,7 @@ let executeObj = {
             return calcTools.marketPriceToBase(me.treeNode, baseName, isTender)
         else{
             if (!rationCalcBases[baseName]){
-                hintBox.infoBox('系统提示', '定额基数“' + baseName + '”定义,计算错误。 (模板 ' + me.template.ID + ',规则 ' + me.tempCalcItem.ID +')', 1);
+                hintBox.infoBox('系统提示', '定额基数“' + baseName + '”定义,计算错误。 (模板 ' + me.template.ID + ',规则 ' + me.tempCalcItem.ID +')', 1);
                 return 0;
             }
             else
@@ -1641,7 +1557,7 @@ class CalcProgram {
         me.compiledFeeTypeMaps = {};
         me.compiledFeeTypeNames = [];
         me.compiledCalcBases = {};
-        // me.saveForReports = [];
+
 
         me.feeRates = this.project.FeeRate.datas.rates;
         me.labourCoes = this.project.labourCoe.datas.coes;
@@ -1656,19 +1572,6 @@ class CalcProgram {
         for (let t of me.templates){
             me.compileTemplate(t);
         };
-
-
-        // 存储费率临时数据,报表用。
-        // if (isInit && me.saveForReports.length > 0){
-        //     let saveDatas = {};
-        //     saveDatas.projectID = projectObj.project.projectInfo.ID;
-        //     saveDatas.calcItems = me.saveForReports;
-        //     CommonAjax.post('/calcProgram/saveCalcItems', saveDatas, function (result) {
-        //         if (result){
-        //             me.saveForReports = [];
-        //         };
-        //     });
-        // };
     };
 
     compilePublics(){
@@ -1805,275 +1708,278 @@ class CalcProgram {
         };
     };
 
+    // 删掉多余的费用。例如:①切换取费类别 ②从其它计算方式(有很多费)切换到公式计算方式(只需要common费),多出来的费要删除。
+    // 如果指定了保留字段,则按用户指定的来。如果没指定保留字段,则按默认的来:总造价清单只留common, estimate两个费用类别。其它公式清单只留common。
+    deleteUselessFees(treeNode, fieldNameArr){
+        if (!(treeNode.data.fees && treeNode.data.fees.length > 0)) return;
+        let keeps = fieldNameArr ? fieldNameArr : [];
+        // 这两个默认是要保留的
+        if (!keeps.includes('common')) keeps.push('common');
+        if (!keeps.includes('estimate')) keeps.push('estimate');
+
+        for (let i = 0; i < treeNode.data.fees.length; i++) {
+            if (!keeps.includes(treeNode.data.fees[i].fieldName)) {
+                delete treeNode.data.feesIndex[treeNode.data.fees[i].fieldName];
+                treeNode.data.fees.splice(i, 1);
+                treeNode.changed = true;
+            }
+        };
+    };
+
+    // 不能直接删除该属性,否则无法冲掉库中已存储的值。下同。
+    deleteProperties (treeNode, propNamesArr){
+        for (let pn of propNamesArr){
+            if (treeNode.data[pn]){
+                treeNode.data[pn] = null;
+                treeNode.changed = true;
+            }
+        };
+    };
+
     // 只计算treeNode自身。changedArr: 外部传来的一个数组,专门存储发生变动的节点。
     innerCalc(treeNode, changedArr, tenderType){
+        if (treeNode.sourceType === ModuleNames.ration_glj) return;             // 仅用作树节点显示的工料机不能参与计算。
+
         let me = this;
-        // 仅用作树节点显示的工料机不能参与计算。
-        if (treeNode.sourceType === ModuleNames.ration_glj) return;
         //设置定额工料机映射表
         me.setRationMap();
+
         treeNode.calcType = calcTools.getCalcType(treeNode);
-        let nQ = calcTools.uiNodeQty(treeNode);
-        let nTQ = calcTools.uiNodeTenderQty(treeNode);
 
-        function isBaseFeeType(type){
-            return ['labour', 'material', 'machine', 'mainMaterial', 'equipment'].indexOf(type) > -1;
-        };
+        // 父清单汇总子清单的费用类别
+        if (treeNode.calcType == treeNodeCalcType.ctGatherBillsFees)
+            me.innerCalcBill(treeNode, 2)
+        // 叶子清单汇总定额的费用类别
+        else if (treeNode.calcType == treeNodeCalcType.ctGatherRationsFees)
+            me.innerCalcBill(treeNode, 1)
+        // 叶子清单:公式计算
+        else if (treeNode.calcType == treeNodeCalcType.ctCalcBaseValue)
+            me.innerCalcBillExpr(treeNode)
+        // 叶子清单:手工修改单价或金额(无定额、无公式计算,什么都没有时)。
+        else if (treeNode.calcType == treeNodeCalcType.ctNull)
+            me.innerCalcBillCustom(treeNode)
+        // 定额:计算程序
+        else
+            me.innerCalcRation(treeNode, tenderType);
 
-        // 不能直接删除该属性,否则无法冲掉库中已存储的值。下同。
-        function deleteProperties (treeNode, propNamesArr){
-            for (let pn of propNamesArr){
-                if (treeNode.data[pn]){
-                    treeNode.data[pn] = null;
-                    treeNode.changed = true;
-                }
-            };
-        };
+        if (!calcTools.isTotalCostBill(treeNode))  // 已在上面的分支中计算过
+            calcTools.estimateFee(treeNode);
 
-        // 删掉多余的费用。例如:①切换取费类别 ②从其它计算方式(有很多费)切换到公式计算方式(只需要common费),多出来的费要删除。
-        // 如果指定了保留字段,则按用户指定的来。如果没指定保留字段,则按默认的来:总造价清单只留common, estimate两个费用类别。其它公式清单只留common。
-        function deleteUselessFees(treeNode, fieldNameArr){
-            if (!(treeNode.data.fees && treeNode.data.fees.length > 0)) return;
-            let keeps = fieldNameArr ? fieldNameArr : [];
-            // 这两个默认是要保留的
-            if (!keeps.includes('common')) keeps.push('common');
-            if (!keeps.includes('estimate')) keeps.push('estimate');
+        if (treeNode.changed && !changedArr.includes(treeNode)) changedArr.push(treeNode);
+    };
 
-            for (let i = 0; i < treeNode.data.fees.length; i++) {
-                if (!keeps.includes(treeNode.data.fees[i].fieldName)) {
-                    delete treeNode.data.feesIndex[treeNode.data.fees[i].fieldName];
-                    treeNode.data.fees.splice(i, 1);
-                    treeNode.changed = true;
-                }
-            };
+    // 清单部分抽取出来,供分摊清单公用。commonCalcType:1 叶子清单汇总定额的费用类别; 2 父清单汇总子清单的费用类别。3: 分摊:叶子清单汇总定额的费用类别。
+    innerCalcBill(treeNode, commonCalcType, tender = tenderTypes.ttCalc){
+        let me = this;
+        treeNode.data.programID = null;
+        calcTools.initFees(treeNode);
+
+        let nodes = [];
+        if (commonCalcType == 1){
+            calcTools.getGLJList(treeNode, true);
+            nodes = me.project.Ration.getRationNodes(treeNode);
+        }
+        else if (commonCalcType == 2)
+            nodes = treeNode.children
+        else if (commonCalcType == 3)
+            nodes = treeNode.children;
+
+        function isBaseFeeType(type){
+            return ['labour', 'material', 'machine', 'mainMaterial', 'equipment'].indexOf(type) > -1;
         };
 
-        // 叶子清单汇总定额、父清单汇总子清单的费用类别
-        if (treeNode.calcType == treeNodeCalcType.ctGatherBillsFees || treeNode.calcType == treeNodeCalcType.ctGatherRationsFees){
-            treeNode.data.programID = null;
-            calcTools.initFees(treeNode);
+        let rst = [];
+        for (let ft of cpFeeTypes) {
+            let ftObj = {};
+            ftObj.fieldName = ft.type;
+            ftObj.name = ft.name;
+            let buf = 0, btf = 0, btuf = 0, bttf = 0;
 
-            let nodes = [];
-            if (treeNode.calcType == treeNodeCalcType.ctGatherRationsFees){
-                calcTools.getGLJList(treeNode, true);
-                nodes = me.project.Ration.getRationNodes(treeNode);
-            } else { //固定清单材料(工程设备)暂估价比较特殊,不进行父项汇总(需求)
-                nodes = me.project.Bills.getGatherNodes(treeNode);
+            let nQ = calcTools.uiNodeQty(treeNode);
+            let nTQ = calcTools.uiNodeTenderQty(treeNode);
+            let bq = nQ ? nQ : 1;
+            let btq = nTQ ? nTQ : 1;
+
+            if (commonCalcType == 2){
+                for (let node of nodes) {
+                    if (node.data.feesIndex && node.data.feesIndex[ft.type]) {
+                        btf = (btf + parseFloatPlus(node.data.feesIndex[ft.type].totalFee)).toDecimal(decimalObj.process);
+                        bttf = (bttf + parseFloatPlus(node.data.feesIndex[ft.type].tenderTotalFee)).toDecimal(decimalObj.process);
+                    };
+                };
             }
-            //else nodes = treeNode.children;
-
-            let rst = [];
-            for (let ft of cpFeeTypes) {
-                let ftObj = {};
-                ftObj.fieldName = ft.type;
-                ftObj.name = ft.name;
-                let buf = 0, btf = 0, btuf = 0, bttf = 0;
-                let bq = nQ ? nQ : 1;
-                let btq = nTQ ? nTQ : 1;
-
-                if (treeNode.calcType == treeNodeCalcType.ctGatherBillsFees){
-                    for (let node of nodes) {
-                        if (node.data.feesIndex && node.data.feesIndex[ft.type]) { // 父清单不要汇总综合单价。
-                            btf = (btf + parseFloatPlus(node.data.feesIndex[ft.type].totalFee)).toDecimal(decimalObj.process);
-                            bttf = (bttf + parseFloatPlus(node.data.feesIndex[ft.type].tenderTotalFee)).toDecimal(decimalObj.process);
-                        };
+            else if ((commonCalcType == 1) || (commonCalcType == 3)){
+                let sum_rtf = 0, sum_rttf = 0;
+                for (let node of nodes) {
+                    let ruf = 0, rtuf = 0, rtf = 0, rttf = 0;
+                    if (node.data.feesIndex && node.data.feesIndex[ft.type]) {
+                        ruf = parseFloatPlus(node.data.feesIndex[ft.type].unitFee).toDecimal(decimalObj.bills.unitPrice);
+                        rtuf = parseFloatPlus(node.data.feesIndex[ft.type].tenderUnitFee).toDecimal(decimalObj.bills.unitPrice);
+                        rtf = parseFloatPlus(node.data.feesIndex[ft.type].totalFee).toDecimal(decimalObj.bills.totalPrice);
+                        rttf = parseFloatPlus(node.data.feesIndex[ft.type].tenderTotalFee).toDecimal(decimalObj.bills.totalPrice);
                     };
-                }
-                else if (treeNode.calcType == treeNodeCalcType.ctGatherRationsFees){     // 这里的算法要配合冷姐姐的神图才能看懂^_^
-                    let sum_rtf = 0, sum_rttf = 0;
-                    for (let node of nodes) {
-                        let ruf = 0, rtuf = 0, rtf = 0, rttf = 0;
-                        if (node.data.feesIndex && node.data.feesIndex[ft.type]) {
-                            ruf = parseFloatPlus(node.data.feesIndex[ft.type].unitFee).toDecimal(decimalObj.bills.unitPrice);
-                            rtuf = parseFloatPlus(node.data.feesIndex[ft.type].tenderUnitFee).toDecimal(decimalObj.bills.unitPrice);
-                            rtf = parseFloatPlus(node.data.feesIndex[ft.type].totalFee).toDecimal(decimalObj.bills.totalPrice);
-                            rttf = parseFloatPlus(node.data.feesIndex[ft.type].tenderTotalFee).toDecimal(decimalObj.bills.totalPrice);
-                        };
-                        // 取费方式为子目含量,清单行/列的XX单价应 =ROUND( ∑ROUND(定额XX单价*含量,清单单价精度),清单单价精度)
-                        if (me.project.property.billsCalcMode === leafBillGetFeeType.rationContent) {
-                            buf = (buf + (ruf * parseFloatPlus(node.data.contain)).toDecimal(decimalObj.bills.unitPrice)).toDecimal(decimalObj.process);
-                            node.data.tenderContaion = (node.data.tenderQuantity / bq).toDecimal(decimalObj.process);
-                            btuf = (btuf + (rtuf * parseFloatPlus(node.data.tenderContaion)).toDecimal(decimalObj.bills.unitPrice)).toDecimal(decimalObj.process);
-                        };
-                        sum_rtf = (sum_rtf + rtf).toDecimal(decimalObj.process);
-                        sum_rttf = (sum_rttf + rttf).toDecimal(decimalObj.process);
+                    // 取费方式为子目含量,清单行/列的XX单价应 =ROUND( ∑ROUND(定额XX单价*含量,清单单价精度),清单单价精度)
+                    if (me.project.property.billsCalcMode === leafBillGetFeeType.rationContent) {
+                        buf = (buf + (ruf * parseFloatPlus(node.data.contain)).toDecimal(decimalObj.bills.unitPrice)).toDecimal(decimalObj.process);
+                        node.data.tenderContaion = (node.data.tenderQuantity / bq).toDecimal(decimalObj.process);
+                        btuf = (btuf + (rtuf * parseFloatPlus(node.data.tenderContaion)).toDecimal(decimalObj.bills.unitPrice)).toDecimal(decimalObj.process);
                     };
+                    sum_rtf = (sum_rtf + rtf).toDecimal(decimalObj.process);
+                    sum_rttf = (sum_rttf + rttf).toDecimal(decimalObj.process);
+                };
 
-                    if (me.project.property.billsCalcMode == leafBillGetFeeType.rationPriceConverse ||
-                        me.project.property.billsCalcMode == leafBillGetFeeType.rationPrice) {
-                        buf = (sum_rtf / bq).toDecimal(decimalObj.process);
-                        btuf = (sum_rttf / btq).toDecimal(decimalObj.process);
-                    };
-                    if (isBaseFeeType(ft.type) ||
-                        (me.project.property.billsCalcMode === leafBillGetFeeType.rationPrice && ft.type == "common")){
-                        btf = sum_rtf;
-                        bttf = sum_rttf;
-                    }
-                    else{
-                        btf = (buf.toDecimal(decimalObj.bills.unitPrice) * bq).toDecimal(decimalObj.process);
-                        bttf = (btuf.toDecimal(decimalObj.bills.unitPrice) * btq).toDecimal(decimalObj.process);
-                    };
+                if (me.project.property.billsCalcMode == leafBillGetFeeType.rationPriceConverse ||
+                    me.project.property.billsCalcMode == leafBillGetFeeType.rationPrice) {
+                    buf = (sum_rtf / bq).toDecimal(decimalObj.process);
+                    btuf = (sum_rttf / btq).toDecimal(decimalObj.process);
                 };
+                if (isBaseFeeType(ft.type) ||
+                    (me.project.property.billsCalcMode === leafBillGetFeeType.rationPrice && ft.type == "common")){
+                    btf = sum_rtf;
+                    bttf = sum_rttf;
+                }
+                else{
+                    btf = (buf.toDecimal(decimalObj.bills.unitPrice) * bq).toDecimal(decimalObj.process);
+                    bttf = (btuf.toDecimal(decimalObj.bills.unitPrice) * btq).toDecimal(decimalObj.process);
+                };
+            };
 
-                ftObj.totalFee = btf.toDecimal(decimalObj.bills.totalPrice);
-                ftObj.tenderTotalFee = bttf.toDecimal(decimalObj.bills.totalPrice);
-                ftObj.unitFee = buf.toDecimal(decimalObj.bills.unitPrice);
-                ftObj.tenderUnitFee = btuf.toDecimal(decimalObj.bills.unitPrice);
+            ftObj.totalFee = btf.toDecimal(decimalObj.bills.totalPrice);
+            ftObj.tenderTotalFee = bttf.toDecimal(decimalObj.bills.totalPrice);
+            ftObj.unitFee = buf.toDecimal(decimalObj.bills.unitPrice);
+            ftObj.tenderUnitFee = btuf.toDecimal(decimalObj.bills.unitPrice);
 
-                calcTools.checkFeeField(treeNode, ftObj);
+            calcTools.checkFeeField(treeNode, ftObj);
 
-                rst.push(ftObj);
-            };
-            treeNode.data.calcTemplate = {"calcItems": rst};
+            rst.push(ftObj);
+        };
+        treeNode.data.calcTemplate = {"calcItems": rst};
+    };
+
+    innerCalcBillExpr(treeNode){
+        delete treeNode.data.gljList;
+        let me = this;
+        me.deleteProperties(treeNode, ['programID']);
+        me.deleteUselessFees(treeNode, ['common', 'rationCommon']);
+
+        let nQ = calcTools.uiNodeQty(treeNode);
+        let nTQ = calcTools.uiNodeTenderQty(treeNode);
+        let f = calcTools.getFeeRateByNode(treeNode);
+        let b = treeNode.data.calcBaseValue ? treeNode.data.calcBaseValue : 0;
+        let tb = treeNode.data.tenderCalcBaseValue ? treeNode.data.tenderCalcBaseValue : 0;
+        let q = nQ ? nQ : 1;
+        let tq = nTQ ? nTQ : 1;
+        let uf = (b * f * 0.01 / q).toDecimal(decimalObj.bills.unitPrice);
+        let tuf = (tb * f * 0.01 / tq).toDecimal(decimalObj.bills.unitPrice);
+        let tf = (me.project.property.billsCalcMode === leafBillGetFeeType.rationPrice) ? (b * f / 100) : (uf * q);
+        tf = tf.toDecimal(decimalObj.bills.totalPrice);
+        let ttf = (me.project.property.billsCalcMode === leafBillGetFeeType.rationPrice) ? (tb * f / 100) : (tuf * tq);
+        ttf = ttf.toDecimal(decimalObj.bills.totalPrice);
+
+        calcTools.checkFeeField(treeNode, {'fieldName': 'common', 'unitFee': uf, 'totalFee': tf, 'tenderUnitFee': tuf, 'tenderTotalFee': ttf});
+
+        // 总造价清单还要做单项工程、建设项目的四大项金额汇总
+        if (calcTools.isTotalCostBill(treeNode)){
+            // 公式叶子清单没有暂估费,但总造价清单除外。
+            calcTools.estimateFee(treeNode);
+            calcTools.initSummaryFee(treeNode);
+            treeNode.data.summaryFees.totalFee = tf;
+            treeNode.data.summaryFees.estimateFee = calcTools.getFee(treeNode, 'estimate.totalFee');
+            treeNode.data.summaryFees.safetyFee = calcTools.getFee(calcTools.getNodeByFlag(fixedFlag.SAFETY_CONSTRUCTION), 'common.totalFee');
+            treeNode.data.summaryFees.chargeFee = calcTools.getFee(calcTools.getNodeByFlag(fixedFlag.CHARGE), 'common.totalFee');
         }
-        // 叶子清单无子结点、无公式计算(啥都没有时)
-        else if (treeNode.calcType == treeNodeCalcType.ctNull){
-            delete treeNode.data.gljList;
-            deleteProperties(treeNode, ['calcBase', 'calcBaseValue', 'tenderCalcBaseValue', 'programID']);
-            deleteUselessFees(treeNode, ['rationCommon']);
-            // 不能直接删除该属性,否则无法冲掉库中已存储的值。下同。
-/*            if (treeNode.data.calcBase){
-                treeNode.data.calcBase = null;
-                treeNode.changed = true;
-            }
 
-            if (treeNode.data.calcBaseValue){
-                treeNode.data.calcBaseValue = null;
-                treeNode.changed = true;
-            }
+        treeNode.data.calcTemplate = {"calcItems": []};
+    };
 
-            if (treeNode.data.tenderCalcBaseValue){
-                treeNode.data.tenderCalcBaseValue = null;
+    innerCalcBillCustom(treeNode){
+        let me = this;
+        delete treeNode.data.gljList;
+        me.deleteProperties(treeNode, ['calcBase', 'calcBaseValue', 'tenderCalcBaseValue', 'programID']);
+        me.deleteUselessFees(treeNode, ['rationCommon']);
+
+        // 2017-09-27 需求改了,除了第 1 、 2.2部分以外,都可以手工修改综合单价、综合合价并参与计算
+        // 在没有公式的情况下可以手工修改综合单价并参与计算
+        if(calcTools.canCalcToTalFeeByOwn(treeNode)){
+            if (treeNode.data.feesIndex && treeNode.data.feesIndex.common){
+                let ftObj = {fieldName: 'common'};
+                let nQ = calcTools.uiNodeQty(treeNode);
+                let nTQ = calcTools.uiNodeTenderQty(treeNode);
+                ftObj.unitFee = parseFloatPlus(treeNode.data.feesIndex.common.unitFee);
+                ftObj.totalFee = (ftObj.unitFee * nQ).toDecimal(decimalObj.bills.totalPrice);
+                ftObj.tenderUnitFee = ftObj.unitFee;
+                ftObj.tenderTotalFee = (ftObj.tenderUnitFee * nTQ).toDecimal(decimalObj.bills.totalPrice);
+                calcTools.checkFeeField(treeNode, ftObj);
+            }
+        } else{
+            if (treeNode.data.fees && treeNode.data.fees.length > 0){
+                treeNode.data.fees = null;
+                treeNode.data.feesIndex = null;
                 treeNode.changed = true;
             }
+        };
 
-            if (treeNode.data.programID) {
-                treeNode.data.programID = null;
-                treeNode.changed = true;
-            };*/
-
-            // 第1、2部分以外的叶子清单在没有公式的情况下可以手工修改综合单价并参与计算。
-            // 2017-09-27 需求改了,除了第 1 、 2.2部分以外,都可以手工修改综合单价、综合合价并参与计算
-            //if(!calcTools.isFBFX(treeNode) && !calcTools.isTechMeasure(treeNode)){ // if(!calcTools.isInheritFrom(treeNode, [fixedFlag.SUB_ENGINERRING, fixedFlag.MEASURE]))
-            // 在没有公式的情况下可以手工修改综合单价并参与计算
-            if(calcTools.canCalcToTalFeeByOwn(treeNode)){
-                if (treeNode.data.feesIndex && treeNode.data.feesIndex.common){
-                    let ftObj = {};
-                    ftObj.fieldName = 'common';
-                    ftObj.unitFee = parseFloatPlus(treeNode.data.feesIndex.common.unitFee);
-                    ftObj.totalFee = (ftObj.unitFee * nQ).toDecimal(decimalObj.bills.totalPrice);
-                    ftObj.tenderUnitFee = ftObj.unitFee;
-                    ftObj.tenderTotalFee = (ftObj.tenderUnitFee * nTQ).toDecimal(decimalObj.bills.totalPrice);
-                    calcTools.checkFeeField(treeNode, ftObj);
-                }
-            } else{
-                if (treeNode.data.fees && treeNode.data.fees.length > 0){
-                    treeNode.data.fees = null;
-                    treeNode.data.feesIndex = null;
+        treeNode.data.calcTemplate = {"calcItems": []};
+    };
+
+    // 定额部分抽取出来,供分摊定额公用。
+    innerCalcRation(treeNode, tenderType = tenderTypes.ttCalc){
+        let me = this;
+        let fnArr = [];
+        calcTools.getGLJList(treeNode, true);
+
+        let nQ = calcTools.uiNodeQty(treeNode);
+        let nTQ = calcTools.uiNodeTenderQty(treeNode);
+
+        if (treeNode.calcType == treeNodeCalcType.ctRationCalcProgram) {
+            // 量价、工料机类型的定额要求"市场合价"
+            if (calcTools.isVP_or_GLJR(treeNode)){
+                let u = treeNode.data.marketUnitFee ? treeNode.data.marketUnitFee : 0;
+                let t = (u * nQ).toDecimal(decimalObj.ration.totalPrice);
+                if (treeNode.data.marketTotalFee != t){
+                    treeNode.data.marketTotalFee = t;
                     treeNode.changed = true;
-                }
+                } ;
             };
+        };
 
-            treeNode.data.calcTemplate = {"calcItems": []};
-        }
-        // 叶子清单公式计算
-        else if (treeNode.calcType == treeNodeCalcType.ctCalcBaseValue){
-            delete treeNode.data.gljList;
+        let template = me.compiledTemplates[treeNode.data.programID];
+        treeNode.data.calcTemplate = template;
 
-            if (treeNode.data.programID) {
-                treeNode.data.programID = null;
-                treeNode.changed = true;
-            }
-            let f = calcTools.getFeeRateByNode(treeNode);
-            let b = treeNode.data.calcBaseValue ? treeNode.data.calcBaseValue : 0;
-            let tb = treeNode.data.tenderCalcBaseValue ? treeNode.data.tenderCalcBaseValue : 0;
-            let q = nQ ? nQ : 1;
-            let tq = nTQ ? nTQ : 1;
-            let uf = (b * f * 0.01 / q).toDecimal(decimalObj.bills.unitPrice);
-            let tuf = (tb * f * 0.01 / tq).toDecimal(decimalObj.bills.unitPrice);
-            let tf = (me.project.property.billsCalcMode === leafBillGetFeeType.rationPrice) ? (b * f / 100) : (uf * q);
-            tf = tf.toDecimal(decimalObj.bills.totalPrice);
-            let ttf = (me.project.property.billsCalcMode === leafBillGetFeeType.rationPrice) ? (tb * f / 100) : (tuf * tq);
-            ttf = ttf.toDecimal(decimalObj.bills.totalPrice);
-            deleteUselessFees(treeNode);
-            calcTools.checkFeeField(treeNode, {'fieldName': 'common', 'unitFee': uf, 'totalFee': tf, 'tenderUnitFee': tuf, 'tenderTotalFee': ttf});
-
-            // 总造价清单还要做单项工程、建设项目的四大项金额汇总
-            if (calcTools.isTotalCostBill(treeNode)){
-                // 公式叶子清单没有暂估费,但总造价清单除外。
-                calcTools.estimateFee(treeNode);
-                calcTools.initSummaryFee(treeNode);
-                treeNode.data.summaryFees.totalFee = tf;
-                treeNode.data.summaryFees.estimateFee = calcTools.getFee(treeNode, 'estimate.totalFee');
-                treeNode.data.summaryFees.safetyFee = calcTools.getFee(calcTools.getNodeByFlag(fixedFlag.SAFETY_CONSTRUCTION), 'common.totalFee');
-                treeNode.data.summaryFees.chargeFee = calcTools.getFee(calcTools.getNodeByFlag(fixedFlag.CHARGE), 'common.totalFee');
-            }
+        if (treeNode && template && template.hasCompiled) {//2018-08-27 空行的时候,取费专业为空,template也为空,加入template非空判断
+            let $CE = executeObj;
+            $CE.treeNode = treeNode;
+            $CE.template = template;
 
-            treeNode.data.calcTemplate = {"calcItems": []};
-        }
-        // 定额或叶子清单自己的计算程序计算
-        else{
-            let fnArr = [];
-            calcTools.getGLJList(treeNode, true);
+            calcTools.initFees(treeNode);
 
-            if (treeNode.calcType == treeNodeCalcType.ctRationCalcProgram) {
-                // 量价、工料机类型的定额要求市场合价
-                if (calcTools.isVP_or_GLJR(treeNode)){
-                    let muf = treeNode.data.marketUnitFee ? treeNode.data.marketUnitFee : 0;
-                    let mtf = (muf * nQ).toDecimal(decimalObj.ration.totalPrice);
-                    if (treeNode.data.marketTotalFee != mtf){
-                        treeNode.data.marketTotalFee = mtf;
-                        treeNode.changed = true;
-                    };
+            for (let idx of template.compiledSeq) {
+                let calcItem = template.calcItems[idx];
+                $CE.tempCalcItem = calcItem;
+                let feeRate = 100;  // 100%
+                if (calcItem.feeRate != undefined)
+                    feeRate = parseFloat(calcItem.feeRate).toDecimal(decimalObj.feeRate);
+
+                calcItem.unitFee = (eval(calcItem.compiledExpr) * feeRate * 0.01).toDecimal(decimalObj.decimal('unitPrice', treeNode));
+                calcItem.totalFee = (calcItem.unitFee * nQ).toDecimal(decimalObj.decimal('totalPrice', treeNode));
+
+                let tExpr = analyzer.getCompiledTenderExpr(calcItem.compiledExpr);
+                calcItem.tenderUnitFee = (eval(tExpr) * feeRate * 0.01).toDecimal(decimalObj.decimal('unitPrice', treeNode));
+                calcItem.tenderTotalFee = (calcItem.tenderUnitFee * nTQ).toDecimal(decimalObj.decimal('totalPrice', treeNode));
+
+                if (calcItem.fieldName) {
+                    fnArr.push(calcItem.fieldName);
+                    calcTools.checkFeeField(treeNode, calcItem);
                 };
             };
-            // 2018-08-27   zhang  插入空定额的时候,取费专业也为空
-           // if (treeNode.data.programID == undefined) treeNode.data.programID = projectObj.project.projectInfo.property.engineering;
-            let template = me.compiledTemplates[treeNode.data.programID];
-            treeNode.data.calcTemplate = template;
-
-            if (treeNode && template && template.hasCompiled) {//2018-08-27 空行的时候,取费专业为空,template也为空,加入template非空判断
-                let $CE = executeObj;
-                $CE.treeNode = treeNode;
-                $CE.template = template;
-
-                calcTools.initFees(treeNode);
-
-                for (let idx of template.compiledSeq) {
-                    let calcItem = template.calcItems[idx];
-                    $CE.tempCalcItem = calcItem;
-                    let feeRate = 100;  // 100%
-                    if (calcItem.feeRate != undefined)
-                        feeRate = parseFloat(calcItem.feeRate).toDecimal(decimalObj.feeRate);
-                    // console.log(`[${calcItem.ID}]: ${calcItem.compiledExpr}`);   // for test.
-
-                    calcItem.unitFee = (eval(calcItem.compiledExpr) * feeRate * 0.01).toDecimal(decimalObj.decimal('unitPrice', treeNode));
-                    calcItem.totalFee = (calcItem.unitFee * calcTools.uiNodeQty(treeNode)).toDecimal(decimalObj.decimal('totalPrice', treeNode));
-
-                    // if (tenderType == tenderTypes.ttCalc) {
-                        let tExpr = analyzer.getCompiledTenderExpr(calcItem.compiledExpr);
-                        calcItem.tenderUnitFee = (eval(tExpr) * feeRate * 0.01).toDecimal(decimalObj.decimal('unitPrice', treeNode));
-                        calcItem.tenderTotalFee = (calcItem.tenderUnitFee * treeNode.data.tenderQuantity).toDecimal(decimalObj.decimal('totalPrice', treeNode));
-                    // };
-
-                    if (calcItem.fieldName) {
-                        fnArr.push(calcItem.fieldName);
-                        calcTools.checkFeeField(treeNode, calcItem);
-                    };
-                };
 
-                if (tenderType == tenderTypes.ttReverseRation || tenderType == tenderTypes.ttReverseGLJ)
-                    this.calcTenderReverse(treeNode, tenderType);
+            if (tenderType == tenderTypes.ttReverseRation || tenderType == tenderTypes.ttReverseGLJ)
+                this.calcTenderReverse(treeNode, tenderType);
 
-                deleteUselessFees(treeNode, fnArr);
-            };
+            me.deleteUselessFees(treeNode, fnArr);
         };
-
-        if (!calcTools.isTotalCostBill(treeNode))  // 已在上面的分支中计算过
-            calcTools.estimateFee(treeNode);
-
-        if (treeNode.changed && !changedArr.includes(treeNode)) changedArr.push(treeNode);
     };
+
     // 存储、刷新零散的多个结点。
     saveNodes(treeNodes, callback){
         if (treeNodes.length < 1) {
@@ -2100,8 +2006,6 @@ class CalcProgram {
         $.bootstrapLoading.start();
         let startTime = +new Date();
         me.project.updateNodes(dataArr, function (data) {
-            let endShowTime = +new Date();
-            console.log(`保存所需时间——${endShowTime - startTime}`);
             me.rationMap = null;
             me.pgljMap = null;
             if(callback){
@@ -2139,10 +2043,11 @@ class CalcProgram {
 
         return changedNodes;
     };
-    // 计算并保存本节点及所有会被影响到的节点。
+    // 计算并保存一个树节点。(修改一个树节点,实际上要计算和保存的是一批树结点:层层父结点、被其它结点(的公式)引用的公式结点)
     calcAndSave(treeNode, callback, tender){
-        let changedNodes = this.calculate(treeNode, true, true, tender);
-        this.saveNodes(changedNodes, callback);
+        this.calcNodesAndSave([treeNode],callback,tender);
+      /*  let changedNodes = this.calculate(treeNode, true, true, tender); 统一调用相同的方法
+        this.saveNodes(changedNodes, callback);*/
     };
 
     /* 计算所有树结点(分3种情况),并返回发生变动的零散的多个树结点。参数取值如下:
@@ -2161,6 +2066,7 @@ class CalcProgram {
                 if (node.children.length > 0) {
                     calcNodes(node.children);
                 };
+
                 if (calcType == calcAllType.catAll || calcType == node.sourceType) {
                     node.calcType = calcTools.getCalcType(node);
                     if (node.calcType != treeNodeCalcType.ctCalcBaseValue)

+ 1 - 0
web/building_saas/main/js/models/installation_fee.js

@@ -318,6 +318,7 @@ var installation_fee = {
                 $.bootstrapLoading.start();
                 CommonAjax.post("/project/calcInstallationFee",updateData,function (data) {
                     //提交后台成功后所做的操作,删除要先删除定额,再删除清单,添加要先添加清单再添加定额
+                    updateData = data.updateData;
                     project.ration_glj.addDatasToList(data.add);//添加插入的定额
                     let calRations = [];
                     for(let ur of data.update){

+ 37 - 58
web/building_saas/main/js/models/ration.js

@@ -450,8 +450,7 @@ var Ration = {
                 updateBillsOprRation();
             })
         };
-        ration.prototype.addMultiRation = function (items, callback) {
-            console.log('addMultiRation');
+        ration.prototype.addMultiRation = async function (items) {
             let me = this;
             let project = projectObj.project, sheetController = projectObj.mainController;
             let engineering = projectObj.project.projectInfo.property.engineering;
@@ -501,63 +500,43 @@ 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);
-                                itemIncreaseFeeObj.calcItemIncreaseFeeByNodes(rations);
-                            }else {
-                                project.calcProgram.calcNodesAndSave(newNodes);
-                                itemIncreaseFeeObj.calcItemIncreaseFeeByNodes(newNodes);
-                            }
-                        });
-                    }else {
-                        project.calcProgram.calcNodesAndSave(newNodes);
-                        itemIncreaseFeeObj.calcItemIncreaseFeeByNodes(newNodes);
-                    }
-                    updateBillsOprRation();
-
-                        if(callback){
-                            callback();
+                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);
+                            itemIncreaseFeeObj.calcItemIncreaseFeeByNodes(rations);
+                        }else {
+                            project.calcProgram.calcNodesAndSave(newNodes);
+                            itemIncreaseFeeObj.calcItemIncreaseFeeByNodes(newNodes);
                         }
-                        showLoding = false;
-                        $.bootstrapLoading.end();
-
-                })
+                    });
+                }else {
+                    project.calcProgram.calcNodesAndSave(newNodes);
+                    itemIncreaseFeeObj.calcItemIncreaseFeeByNodes(newNodes);
+                }
+                updateBillsOprRation();
             }
         };
         ration.prototype.insertVolumePrice = function(type){

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

@@ -920,11 +920,20 @@ const BillsSub = (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();
+            }
         }
     }
     function handleClick(getRationFunc) {

+ 6 - 1
web/building_saas/main/js/views/character_content_view.js

@@ -465,7 +465,8 @@ let characterOprObj = {
     setting: {
         header: [
             {headerName:"项目特征",headerWidth:120, rateWidth:0.9, dataCode:"character", dataType: "String", hAlign: "left", vAlign: "center", formatter: '@'},
-            {headerName:"特征值",headerWidth:160,dataCode:"eigenvalue", dataType: "String", cellType: "comboBox", hAlign: "left", vAlign: "center", formatter: '@'},
+            {headerName:"特征值",headerWidth:160,dataCode:"eigenvalue", dataType: "String", hAlign: "left", vAlign: "center", formatter: '@'},
+            //{headerName:"特征值",headerWidth:160,dataCode:"eigenvalue", dataType: "String", cellType: "comboBox", hAlign: "left", vAlign: "center", formatter: '@'},
             {headerName:"输出",headerWidth:40,dataCode:"isChecked", cellType:"checkBox", hAlign: "center", vAlign: "center"}
         ]
     },
@@ -1051,6 +1052,10 @@ let pageCCOprObj = {
             for (let row = 0; row < data.length; row++) {
                 sheet.getCell(row, 0).locked(true);//locked
                 let val = data[row][setting.header[col].dataCode];
+                if (setting.header[col].dataCode === 'eigenvalue') { // 2020-7-6: 暂时取消下拉
+                    const selectedItem = val.find(v => v.isSelected);
+                    val = selectedItem && selectedItem.value || '';
+                }
                 if(setting.header[col].cellType === "checkBox"){
                     let checkBox = new GC.Spread.Sheets.CellTypes.CheckBox();
                     checkBox.isThreeState(false);

+ 1 - 1
web/building_saas/main/js/views/item_increase_fee_view.js

@@ -412,7 +412,7 @@ let itemIncreaseFeeObj = {
                         serialNo = serialNo+1;
                         let newRationData = this.inserNewItemNodes(node.data.ID,node.data.quantity,preID,serialNo,code,s.name,total,datas);
                         preID = newRationData.ID;
-                        newRationData.manageFeeRate = manageFeeRate;
+                        if(!_.isEmpty(installationFeeObj.feeRateMap))newRationData.manageFeeRate = manageFeeRate;
                     }
                 }else { //如果total小于0,但又存在的话,删除定额(同时后端处理时记得要删除定额工料机)
                     if(ZMZJFnode){

+ 72 - 34
web/building_saas/main/js/views/project_view.js

@@ -1030,13 +1030,14 @@ var projectObj = {
                     disableSpread(that.mainSpread);
                 }
 
-                // 检查旧项目是否有调价数据,没有则自动生成
-                let node = projectObj.project.mainTree.firstNode();
-                if (node.data.feesIndex && node.data.feesIndex.common && node.data.feesIndex.common.totalFee
-                    && node.data.feesIndex.common.totalFee != 0){
-                    if (node.data.feesIndex.common.tenderTotalFee == undefined || node.data.feesIndex.common.tenderTotalFee == 0)
+                // 检查旧项目是否有调价数据,没有则自动生成 ----- 有种情况不行,如下:
+                // 未知情况:有调价数据,但调价算的不对,所以这里无论什么情况,打开时都要全局算一遍,牺牲速度体验,换来数据稳定。
+                // let node = projectObj.project.mainTree.firstNode();
+                // if (node.data.feesIndex && node.data.feesIndex.common && node.data.feesIndex.common.totalFee
+                //     && node.data.feesIndex.common.totalFee != 0){
+                //     if (node.data.feesIndex.common.tenderTotalFee == undefined || node.data.feesIndex.common.tenderTotalFee == 0)
                         projectObj.project.calcProgram.doTenderCalc();
-                };
+                // };
                 $.bootstrapLoading.end();
             }
             else {
@@ -1200,6 +1201,68 @@ var projectObj = {
         me.mainSpreadEnterCell({type: 'EnterCell'}, {sheet: sheet, sheetName: sheet.name(), cancel: false, row: newRow, col: newCol});
 
     },
+    // 注册自定义插入清单数量
+    registerFlexibleInsertBillMenu: function (type) {
+        const project = projectObj.project;
+        const name = `插入${billText[type]}`;
+        const inputID = `insert-bills-number${type}`;
+        const insertBillsHtml = `<span>${name}&nbsp;&nbsp;<input id=${inputID} class="menu-input" type="text" value="1" onfocus="this.select()">&nbsp;&nbsp;行</span>`;
+        return sheetCommonObj.registerInputContextMenuItem(`insertBills${type}`, 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 = +$(`#${inputID}`).val();
+                if (!number) {
+                    return;
+                }
+                $.bootstrapLoading.start();
+                const postData = ProjectController.getBillsPostData(number, type);
+                const newNodes = await ProjectController.addBillsByData(postData, true);
+                if (newNodes.length) {
+                    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;
         $.contextMenu({
@@ -1261,8 +1324,7 @@ var projectObj = {
                     }
                 },
                 "insertFX": {
-                    name: "插入分项",
-                    icon: 'fa-sign-in',
+                    type: projectObj.registerFlexibleInsertBillMenu(billType.FX), // 插入分项
                     disabled: function () {
                         if (projectReadOnly) {
                             return true;
@@ -1289,10 +1351,6 @@ var projectObj = {
                         }
                         return true;//除了清单,其它类型都只读
                     },
-                    callback: function (key, opt) {
-                        ProjectController.addFX(project, controller);
-                        projectObj.selectColAndFocus(project.mainTree.selected);
-                    },
                     visible: function(key, opt){
                         if(project.mainTree.selected){
                             return project.Bills.isFBFX(project.mainTree.selected );//不属于分部分项的话隐藏
@@ -1302,8 +1360,7 @@ var projectObj = {
                     }
                 },
                 "insertBills": {
-                    name: "插入清单",
-                    icon: 'fa-sign-in',
+                    type: projectObj.registerFlexibleInsertBillMenu(billType.BILL), // 插入清单
                     disabled: function () {
                         if (projectReadOnly) {
                             return true;
@@ -1314,16 +1371,6 @@ var projectObj = {
                         }
                         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;
-                            }
-                        }
-                        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;
@@ -1334,8 +1381,7 @@ var projectObj = {
                 },
                 "spr1": '--------',
                 "insertRation": {
-                    name: "插入定额",
-                    icon: 'fa-sign-in',
+                    type: projectObj.registerFlexibleInsertRatoinMenu(), // 插入定额
                     disabled: function () {
                         if (projectReadOnly) {
                             return true;
@@ -1345,14 +1391,6 @@ 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);
-                     }*/
                 },
                 "insertLJ": {
                     name: "插入量价",//插入量价不需要自动定位到编号列

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

@@ -974,8 +974,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);
@@ -994,6 +996,9 @@ function e_recFiles(btn){
                     gcTreeObj.refreshNodeData(selected);
                 }
             }
+            if (isRecoverProj) {
+                projTreeObj.emitTreeChange();
+            }
         }
     });
 }
@@ -1055,6 +1060,7 @@ function e_recProj(btn){
             }
             v_removeNode(selected);
             v_refreshNode(selected, true);
+            projTreeObj.emitTreeChange();
         }
     });
 }

+ 64 - 18
web/building_saas/pm/js/pm_newMain.js

@@ -560,6 +560,7 @@ const projTreeObj = {
             }
             projTreeObj.moveTo(selected, null, parent, next, null, action);
             $.bootstrapLoading.end();
+            projTreeObj.emitTreeChange();
         });
     },
     //升级后选中节点的后兄弟节点不成为其子节点,因为有层级类型限制(相当于选中节点移动到父项后成为其后兄弟)
@@ -1693,9 +1694,41 @@ 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);
     }
-
-
 };
 // 新建项目必填项提示框设置“ 比如:注:为响应重庆地区指标采集标准数据要求,以上工程信息及特征必填项为必填项,请正确填写。”
 function setupRequiredWarn(compilation) {
@@ -1736,15 +1769,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));
 
     //单价、费率文件删除确认
     $('#fileDelConfirm').click(function () {
@@ -2522,6 +2548,7 @@ $(document).ready(function() {
                 setTimeout(function () {
                     STATE.deleting = false;
                 }, 500);
+                projTreeObj.emitTreeChange();
             }, function () {
                 $.bootstrapLoading.end();
                 setTimeout(function () {
@@ -2573,6 +2600,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();
         });
     });
 
@@ -2705,7 +2733,6 @@ function changeFeeRate(engLib) {
 
 //根据文件类型筛选新的基本信息数据
 function getNeedfulBasicInfo(info, fileKind) {
-    debugger;
     /* let strMap = {
         1: 'tender',    //投标
         2: 'bid',       //招标
@@ -2885,7 +2912,7 @@ function initNodesVisibility(nodes, visible) {
     });
 }
 
-function initProjects(callback) {
+function initProjects(callback, expandCallback) {
     GetAllProjectData(function (datas) {
         //设置工程专业
         for (let data of datas) {
@@ -2904,9 +2931,12 @@ 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 rows = projTreeObj.tree.items.map((item, index) => index);
             //初始选择
             const initSel = sheet.getSelections()[0] ? sheet.getSelections()[0] : { row: 0, rowCount: 1 };
             projTreeObj.initSelection(initSel, null, sheet);
@@ -2924,7 +2954,7 @@ function initProjects(callback) {
 /**
  * 初始化数据
  */
-async function init(refresh = false) {
+async function init(refresh = false, callback, expandCallback) {
     console.log('init');
     //init spread and pmTree
     try {
@@ -2935,7 +2965,12 @@ async function init(refresh = false) {
             $("#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 }) => {
@@ -2943,7 +2978,12 @@ async function init(refresh = false) {
                 $("#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) : [];
@@ -3011,6 +3051,7 @@ function AddProject() {
         setTimeout(function () {
             STATE.addingProject = false;
         }, 500);
+        projTreeObj.emitTreeChange();
     };
     let errCB = function () {
         $.bootstrapLoading.end();
@@ -3684,6 +3725,7 @@ function AddEngineering() {
         setTimeout(function () {
             STATE.addingEng = false;
         }, 500);
+        projTreeObj.emitTreeChange();
     };
     let errCB = function () {
         $.bootstrapLoading.end();
@@ -3814,6 +3856,7 @@ function AddTender() {
                 setTimeout(function () {
                     STATE.addingTender = false;
                 }, 500);
+                projTreeObj.emitTreeChange();
             },
             errCB = function () {
                 $.bootstrapLoading.end();
@@ -3922,6 +3965,7 @@ function AddFolder() {
         setTimeout(function () {
             STATE.addingFolder = false;
         }, 500);
+        projTreeObj.emitTreeChange();
     };
     let errCB = function () {
         $.bootstrapLoading.end();
@@ -4756,6 +4800,7 @@ function handleProjectAfterChecking(projectData) {
     const rootData = projectData.find(item => item.projType === projectType.project);
     const sorted = commonUtil.getSortedTreeData(rootData.ParentID, projectData);
     importView.doAfterImport(sorted);
+    projTreeObj.emitTreeChange();
 }
 // 导入检查完成时,对新增单位工程的处理
 function handleTenderAfterChecking(projectData, orgTender) {
@@ -4768,6 +4813,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

@@ -937,7 +937,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);
         }

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

@@ -433,6 +433,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;
@@ -448,6 +455,30 @@ const pmTree = {
                 }
             };
 
+            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

@@ -82,6 +82,9 @@
                     <!--  <a class="dropdown-item" href="#">关于</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>
            <!-- <li class="nav-item">
                 <a class="nav-link new-msg" data-toggle="modal" data-target="#msg" href="javacript:void(0);">
                     <i class="fa fa-envelope-o" aria-hidden="true"></i>&nbsp;2

+ 26 - 13
web/over_write/js/guangdong_2018_export.js

@@ -1516,7 +1516,7 @@ const XMLStandard = (function () {
         function WorkContent(contentText, fee) {
             const attrs = [
                 // 定额工作内容
-                { name: 'Name', dName: '定额工作内容', required: true, value: contentText, minLen: 1 },
+                { name: 'Name', dName: '定额工作内容', required: true, value: contentText },
                 // 取此工作内容下定额子目/量价/定额同级人材机的综合合价之和
                 { name: 'Total', type: _type.DECIMAL, value: fee },
                 { name: 'Remark', value: '' }
@@ -1636,7 +1636,7 @@ const XMLStandard = (function () {
             const row = node.serialNo() + 1;
             const attrs = [
                 // 名称
-                { name: 'Name', dName: '名称', required: true, minLen: 1, value: bills.name, failHint: `第${row}行清单-“项目名称”`},
+                { name: 'Name', dName: '名称', required: true, minLen: 1, value: bills.name, failHint: `第${row}行清单-“项目名称”` },
                 // 金额
                 { name: 'Total', type: _type.DECIMAL, value: _util.getFee(bills.fees, 'common.totalFee') },
                 // 费用代号
@@ -2329,7 +2329,7 @@ const XMLStandard = (function () {
                 // 先计算人材机总消耗量,以供后面需要
                 gljUtil.calcProjectGLJQuantity(tenderDetail.projectGLJ.datas,
                     tenderDetail.ration_glj.datas, tenderDetail.Ration.datas, tenderDetail.Bills.datas, Decimal.GLJ, _, scMathUtil); */
-                
+
                 // 单位工程费用汇总
                 const unitWorksSummary = loadUnitWorksSummary(tenderDetail);
                 // 获取标准清单编码-取费类别映射表
@@ -2516,14 +2516,16 @@ const XMLStandard = (function () {
         // 加载分部分项清单,这部分是分部分项工程和措施项目共用的
         function loadFBFX(nodes, kind) {
             return nodes.map(node => {
+                // 措施项目是叶子:如果无单位、且无定额、且综合合价=0时,判断为DivisionalWorks;否则,判断为WorkElement。
+                const isDivisionalMeasure = kind === BillsKind.MEASURE && !node.children.length && !node.data.unit && !+_util.getFee(node.data.fees, 'common.totalFee');
                 let ele;
                 // 有子清单的是分部
-                if (node.source.children.length) {
+                if (node.source.children.length || isDivisionalMeasure) {
                     ele = new DivisionalWorks(node.data);
                     const summaryCost = new SummaryOfBasicCost(tenderDetail.mainTree.items, node);
                     // 递归获取子元素
                     ele.children = [summaryCost, ...loadFBFX(node.children, kind)];
-                } else { // 无子清单的是分项
+                } else { // 无子清单的是分项(分部分项部分)
                     ele = loadBills(node, kind);
                 }
                 return ele;
@@ -2539,16 +2541,27 @@ const XMLStandard = (function () {
             // 工程量计算表
             const expressElement = loadQuantityExpressions(tenderDetail.quantity_detail.datas, true, node.data.ID);
             workElement.children.push(...expressElement);
-            // 相同工作内容的定额进行分组
+            // 相同工作内容的定额进行分组。若工作内容为空,则定额直接下挂到清单下
             const workMap = _.groupBy(node.children, node => node.data.jobContentText || '');
-            const workContents = Object
+            const valueWorks = [];
+            const emptyWorkRationNodes = [];
+            Object
                 .entries(workMap)
-                .map(([contentText, rationNodes]) => {
-                    const workContent = new WorkContent(contentText, _util.getAggregateFee(rationNodes));
-                    workContent.children = rationNodes.map(node => loadRation(node, kind));
-                    return workContent;
+                .forEach(([contentText, rationNodes]) => {
+                    if (contentText) {
+                        valueWorks.push({ contentText, rationNodes });
+                    } else {
+                        emptyWorkRationNodes.push(...rationNodes);
+                    }
                 });
+            const workContents = valueWorks.map(({ contentText, rationNodes }) => {
+                const workContent = new WorkContent(contentText, _util.getAggregateFee(rationNodes));
+                workContent.children = rationNodes.map(node => loadRation(node, kind));
+                return workContent;
+            });
             workElement.children.push(...workContents);
+            const rations = emptyWorkRationNodes.map(node => loadRation(node, kind));
+            workElement.children.push(...rations);
             return workElement
         }
 
@@ -2681,7 +2694,7 @@ const XMLStandard = (function () {
             if (!isValidDepth) {
                 _failList.push('计日工子项超过两层')
             } else {
-                // 计日工最底层节点也需要是标题,否则检测平台会报错
+                // 计日工最底层节点也需要是标题,否则检测平台会报错(不过这样的话xsd)
                 dayworkRate.children = loadGroupAndItems(
                     daywork.children,
                     (node) => new DayWorkRateGroup(node),
@@ -3007,4 +3020,4 @@ const XMLStandard = (function () {
         saveAsFile
     };
 
-})();
+})();