Browse Source

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

zhangweicheng 4 years ago
parent
commit
44a74287b0
64 changed files with 5743 additions and 1191 deletions
  1. 11 0
      config/config.js
  2. 26 0
      modules/all_models/import_logs.js
  3. 0 31
      modules/all_models/product.js
  4. 2 2
      modules/all_models/stdGlj_glj.js
  5. 10 0
      modules/all_models/std_price_info_areas.js
  6. 13 0
      modules/all_models/std_price_info_class.js
  7. 42 0
      modules/all_models/std_price_info_items.js
  8. 12 0
      modules/all_models/std_price_info_lib.js
  9. 3 0
      modules/all_models/system_setting.js
  10. 5 1
      modules/all_models/user.js
  11. 9 0
      modules/common/std/std_ration_lib_map_model.js
  12. 255 0
      modules/price_info_lib/controllers/index.js
  13. 463 0
      modules/price_info_lib/facade/index.js
  14. 31 0
      modules/price_info_lib/routes/index.js
  15. 138 3
      modules/ration_repository/models/ration_item.js
  16. 1 1
      modules/ration_repository/models/repository_map.js
  17. 107 0
      modules/reports/controllers/rpt_tpl_controller.js
  18. 1 0
      modules/reports/routes/rpt_tpl_router.js
  19. 1 0
      modules/std_glj_lib/controllers/gljController.js
  20. 80 0
      modules/std_glj_lib/models/gljModel.js
  21. 1 11
      modules/sys_tools/controllers/sys_controller.js
  22. 0 30
      modules/sys_tools/models/product_model.js
  23. 76 1
      modules/sys_tools/models/sys_model.js
  24. 0 1
      modules/sys_tools/routes/routes.js
  25. 15 2
      modules/users/controllers/compilation_controller.js
  26. 5 0
      modules/users/controllers/login_controller.js
  27. 41 9
      modules/users/controllers/system_controller.js
  28. 0 3
      modules/users/controllers/tool_controller.js
  29. 18 0
      modules/users/models/engineering_lib_model.js
  30. 225 0
      modules/users/models/sms.js
  31. 1 0
      modules/users/routes/compilation_route.js
  32. 22 1
      operation.js
  33. 987 865
      package-lock.json
  34. 3 1
      package.json
  35. 16 0
      public/constants/price_info_constant.js
  36. 70 16
      public/web/PerfectLoad.js
  37. 3 4
      public/web/common_ajax.js
  38. 9 0
      public/web/sheet/sheet_common.js
  39. 2 2
      public/web/tools_const.js
  40. 4 2
      public/web/tree_sheet/tree_sheet_controller.js
  41. 1 0
      web/maintain/bills_lib/html/neirong.html
  42. 38 0
      web/maintain/bills_lib/html/qingdan.html
  43. 2 0
      web/maintain/bills_lib/html/tezheng.html
  44. 13 2
      web/maintain/bills_lib/scripts/bills_lib_ajax.js
  45. 144 54
      web/maintain/common/css/main.css
  46. 3 0
      web/maintain/common/html/layout.html
  47. 112 0
      web/maintain/price_info_lib/css/index.css
  48. 85 0
      web/maintain/price_info_lib/html/edit.html
  49. 228 0
      web/maintain/price_info_lib/html/main.html
  50. 794 0
      web/maintain/price_info_lib/js/index.js
  51. 255 0
      web/maintain/price_info_lib/js/main.js
  52. 5 0
      web/maintain/report/html/rpt_tpl_dtl_pre_hdl_sort.html
  53. 115 39
      web/maintain/report/js/rpt_tpl_main.js
  54. 16 0
      web/maintain/report/js/rpt_tpl_pre_handle.js
  55. 956 0
      web/over_write/js/chongqing_2018_price_crawler.js
  56. 2 2
      web/over_write/js/guangdong_2018.js
  57. 2 2
      web/over_write/js/neimenggu_2017.js
  58. 24 0
      web/users/css/custom.css
  59. 129 22
      web/users/js/compilation.js
  60. 13 13
      web/users/js/system.js
  61. 3 2
      web/users/views/compilation/engineering.html
  62. 24 2
      web/users/views/compilation/modal.html
  63. 71 25
      web/users/views/system/index.html
  64. 0 42
      web/users/views/tool/index.html

+ 11 - 0
config/config.js

@@ -40,6 +40,17 @@ module.exports = {
             connectTimeoutMS: 50000,
             useMongoClient: true
         }},
+    uat:{  server: "112.74.42.187",
+    port: "27017",
+    options:{
+        user:'smartcost',
+        pass:'SmartCost3850888',
+        auth: {
+            "authdb": "admin"
+        },
+        connectTimeoutMS: 50000,
+        useMongoClient: true
+    }},
     prod_s:{  server: "112.74.42.187",
         port: "28066",
         options:{

+ 26 - 0
modules/all_models/import_logs.js

@@ -0,0 +1,26 @@
+/**
+ * Created by zhang on 2019/12/27.
+ */
+const mongoose = require('mongoose');
+
+let Schema = mongoose.Schema;
+let collectionName = 'import_logs';
+
+let modelSchema = {
+    // 日志类型
+    key: {type: String, index: true},
+    // 日志简单内容
+    content: String,
+    // 关联用户id
+    userID: String,
+    // 费用定额Id
+    compilationID: String,
+    // 状态
+    status:String,
+    // 建设项目ID
+    projectID: Array,
+    // 创建时间
+    create_time: Number,
+    errorMsg:""
+};
+mongoose.model(collectionName, new Schema(modelSchema, {versionKey: false, collection: collectionName}));

+ 0 - 31
modules/all_models/product.js

@@ -1,31 +0,0 @@
-'use strict';
-
-/**
- *
- *
- * @author Zhong
- * @date 2019/3/21
- * @version
- */
-/*
-* 与产品绑定的信息都可以在此设置
-* */
-const mongoose = require('mongoose');
-const Schema = mongoose.Schema;
-const productSchema = new Schema({
-    name: {
-        type: String,
-        default: '纵横建筑计价'
-    },
-    company: {
-        type: String,
-        default: '珠海纵横创新软件有限公司'
-    },
-    icp: {
-        type: String,
-        default: '粤ICP备14032472号'
-    },
-    version: String
-}, {versionKey: false});
-
-mongoose.model('product', productSchema, 'product');

+ 2 - 2
modules/all_models/stdGlj_glj.js

@@ -40,11 +40,11 @@ const std_glj = new Schema({
     materialType: Number, //三材类型:钢材1、钢筋2、木材3、水泥4、标准砖5
     materialCoe: Number, //三材系数
     model: Number, //机型
-    //经济指标数据s
+    //经济指标数据
     materialIndexType:String,//工料指标类别
     materialIndexUnit:String,//工料指标单位
     materialIndexCoe:Number//单位转换系数
 
 },{versionKey: false});
 
-mongoose.model('std_glj_lib_gljList', std_glj, 'std_glj_lib_gljList');
+mongoose.model('std_glj_lib_gljList', std_glj, 'std_glj_lib_gljList');

+ 10 - 0
modules/all_models/std_price_info_areas.js

@@ -0,0 +1,10 @@
+// 信息价地区(一个费用定额共用地区)
+const mongoose = require('mongoose');
+
+const Schema = mongoose.Schema;
+const priceInfoArea = new Schema({
+    ID: String,
+    compilationID: String,
+    name: String
+}, {versionKey: false});
+mongoose.model('std_price_info_areas', priceInfoArea, 'std_price_info_areas');

+ 13 - 0
modules/all_models/std_price_info_class.js

@@ -0,0 +1,13 @@
+// 信息价类型
+const mongoose = require('mongoose');
+
+const Schema = mongoose.Schema;
+const priceInfoClass = new Schema({
+    ID: String,
+    ParentID: String,
+    NextSiblingID: String,
+    name: String,
+    areaID: String,
+    libID: String
+}, {versionKey: false});
+mongoose.model('std_price_info_class', priceInfoClass, 'std_price_info_class');

+ 42 - 0
modules/all_models/std_price_info_items.js

@@ -0,0 +1,42 @@
+// 信息价数据
+const mongoose = require('mongoose');
+
+const Schema = mongoose.Schema;
+const priceInfoItems = new Schema({
+    ID: String,
+    libID: String,
+    classID: String, // 分类
+    code: {
+        type: String,
+        default: ''
+    },
+    name: {
+        type: String,
+        default: ''
+    },
+    specs: {
+        type: String,
+        default: ''
+    },
+    unit: {
+        type: String,
+        default: ''
+    },
+    taxPrice: {
+        type: String,
+        default: ''
+    }, // 含税价格
+    noTaxPrice: {
+        type: String,
+        default: ''
+    }, // 不含税价格
+    // 以下冗余数据为方便前台信息价功能处理
+    period: String, // 期数 eg: 2020-05
+    areaID: String, // 地区
+    compilationID: String, // 费用定额
+    remark: {
+        type: String,
+        default: ''
+    }
+}, { versionKey: false });
+mongoose.model('std_price_info_items', priceInfoItems, 'std_price_info_items');

+ 12 - 0
modules/all_models/std_price_info_lib.js

@@ -0,0 +1,12 @@
+// 信息价库
+const mongoose = require('mongoose');
+
+const Schema = mongoose.Schema;
+const priceInfoLib = new Schema({
+    ID: String,
+    name: String,
+    period: String, // 期数 eg: 2020-05
+    compilationID: String,
+    createDate: Number,
+}, {versionKey: false});
+mongoose.model('std_price_info_lib', priceInfoLib, 'std_price_info_lib');

+ 3 - 0
modules/all_models/system_setting.js

@@ -21,5 +21,8 @@ let modelSchema = {
         project: Number,
         ration:Number
     },
+    company: String, // 软件供应商
+    product: String, // 产品名
+    version: String // 版本号
 };
 mongoose.model(collectionName, new Schema(modelSchema, {versionKey: false, collection: collectionName}));

+ 5 - 1
modules/all_models/user.js

@@ -14,7 +14,11 @@ let upgrade = mongoose.Schema({
     compilationID:String,//编办ID
     upgrade_time:Number,
     isUpgrade:Boolean,
-    remark:String//描述:广东办刘飞 2018-06-17 启用/关闭
+    remark:String,//描述:广东办刘飞 2018-06-17 启用/关闭
+    deadline: {
+        type:String,
+        default: '',
+    },
 }, { _id: false })
 
 

+ 9 - 0
modules/common/std/std_ration_lib_map_model.js

@@ -52,6 +52,15 @@ class STDRationLibMapModel extends BaseModel {
         return result;
     }
 
+    // 获取所有费用定额的所有定额库
+    async getAllRationLibs() {
+        const libs = await this.findDataByCondition({ deleted: false }, null, false);
+        return libs.map(libData => ({
+            id: libData.ID,
+            name: libData.dispName
+        }));
+    }
+
 }
 
 export default STDRationLibMapModel;

+ 255 - 0
modules/price_info_lib/controllers/index.js

@@ -0,0 +1,255 @@
+import BaseController from "../../common/base/base_controller";
+import CompilationModel from '../../users/models/compilation_model';
+const multiparty = require('multiparty');
+const excel = require('node-xlsx');
+const fs = require('fs');
+const facade = require('../facade/index');
+const config = require("../../../config/config.js");
+
+class PriceInfoController extends BaseController {
+    async main(req, res) {
+        const compilationModel = new CompilationModel();
+        const compilationList = await compilationModel.getCompilationList({ _id: 1, name: 1 });
+        compilationList.unshift({ _id: 'all', name: '所有' });
+        const activeCompilation = compilationList.find(compilation => compilation._id.toString() === req.query.filter);
+        if (activeCompilation) {
+            activeCompilation.active = 'active';
+        } else {
+            compilationList[0].active = 'active'
+        }
+        const filter = req.query.filter ? { compilationID: req.query.filter } : {};
+        const libs = await facade.getLibs(filter);
+        libs.forEach(lib => {
+            compilationList.forEach(compilation => {
+                if (compilation._id.toString() === lib.compilationID) {
+                    lib.compilationName = compilation.name;
+                }
+            });
+        });
+        const listItem = `
+            <li class="nav-item">
+                <a class="nav-link" href="javascript:void(0);" aria-haspopup="true" aria-expanded="false" data-toggle="modal" data-target="#crawl">导入材料价格信息</a>
+            </li>`
+        const renderData = {
+            title: '材料信息价库',
+            userAccount: req.session.managerData.username,
+            userID: req.session.managerData.userID,
+            libs: libs,
+            compilationList: compilationList,
+            listItem,
+            layout: 'maintain/common/html/layout'
+        };
+        res.render("maintain/price_info_lib/html/main.html", renderData);
+    }
+
+    async editView(req, res) {
+        const { libID } = req.query;
+        const libs = await facade.getLibs({ ID: libID });
+        if (!libs.length) {
+            return res.send(404);
+        }
+        const areaList = await facade.getAreas(libs[0].compilationID);
+        const renderData = {
+            compilationID: libs[0].compilationID,
+            libName: libs[0].name,
+            period: libs[0].period,
+            areaList: JSON.stringify(areaList),
+            userAccount: req.session.managerData.username,
+            userID: req.session.managerData.userID,
+            LicenseKey: config.getLicenseKey(process.env.NODE_ENV),
+        };
+        res.render("maintain/price_info_lib/html/edit.html", renderData);
+    }
+
+    async addLib(req, res) {
+        try {
+            const { name, period, compilationID } = req.body;
+            await facade.createLib(name, period, compilationID)
+        } catch (err) {
+            console.log(err);
+        }
+        res.redirect(req.headers.referer);
+    }
+
+    async renameLib(req, res) {
+        try {
+            const { libID, name } = JSON.parse(req.body.data);
+            await facade.updateLib({ ID: libID }, { name });
+            res.json({ error: 0, message: 'rename success' });
+        } catch (err) {
+            console.log(err);
+            res.json({ error: 1, message: err.toString() });
+        }
+    }
+
+    async deleteLib(req, res) {
+        try {
+            const { libID } = JSON.parse(req.body.data);
+            await facade.deleteLib(libID);
+            res.json({ error: 0, message: 'delete success' });
+        } catch (err) {
+            console.log(err);
+            res.json({ error: 1, message: err.toString() });
+        }
+    }
+
+    async processChecking(req, res) {
+        try {
+            const { key } = JSON.parse(req.body.data);
+            const data = await facade.processChecking(key);
+            res.json({ error: 0, message: 'processChecking', data });
+        } catch (err) {
+            res.json({ error: 1, message: err.toString() });
+        }
+    }
+
+    // 爬取数据
+    async crawlData(req, res) {
+        try {
+            const { from, to, compilationID } = JSON.parse(req.body.data);
+            //res.setTimeout(1000 * 60 * 60 * 2); // 不设置的话,处理时间过长,会触发默认的响应超时,报错(前端报错,后台还继续在处理)
+            await facade.crawlDataByCompilation(compilationID, from, to);
+            res.json({ error: 0, message: 'crawl processing' });
+        } catch (err) {
+            console.log(err);
+            res.json({ error: 1, message: err.toString() });
+        }
+    }
+
+    async importExcel(req, res) {
+        let responseData = {
+            err: 0,
+            msg: ''
+        };
+        res.setTimeout(1000 * 60 * 10);
+        const allowHeader = ['application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'];
+        const uploadOption = {
+            uploadDir: './public'
+        };
+        const form = new multiparty.Form(uploadOption);
+        let uploadFullName;
+        form.parse(req, async function (err, fields, files) {
+            try {
+                const libID = fields.libID !== undefined && fields.libID.length > 0 ?
+                    fields.libID[0] : null;
+                if (!libID) {
+                    throw '参数错误。';
+                }
+                const file = files.file !== undefined ? files.file[0] : null;
+                if (err || file === null) {
+                    throw '上传失败。';
+                }
+                // 判断类型
+                if (file.headers['content-type'] === undefined || allowHeader.indexOf(file.headers['content-type']) < 0) {
+                    throw '不支持该类型';
+                }
+                // 重命名文件名
+                uploadFullName = uploadOption.uploadDir + '/' + file.originalFilename;
+                fs.renameSync(file.path, uploadFullName);
+
+                const sheet = excel.parse(uploadFullName);
+                if (sheet[0] === undefined || sheet[0].data === undefined || sheet[0].data.length <= 0) {
+                    throw 'excel没有对应数据。';
+                }
+                // 提取excel数据并入库
+                await facade.importExcelData(libID, sheet[0].data);
+                // 删除文件
+                if (uploadFullName && fs.existsSync(uploadFullName)) {
+                    fs.unlink(uploadFullName);
+                }
+                res.json(responseData);
+            }
+            catch (error) {
+                console.log(error);
+                if (uploadFullName && fs.existsSync(uploadFullName)) {
+                    fs.unlink(uploadFullName);
+                }
+                responseData.err = 1;
+                responseData.msg = error.toString();
+                res.json(responseData);
+            }
+        });
+    }
+
+    async editArea(req, res) {
+        try {
+            const { updateData } = JSON.parse(req.body.data);
+            await facade.updateAres(updateData);
+            res.json({ error: 0, message: 'update areas success' });
+        } catch (err) {
+            console.log(err);
+            res.json({ error: 1, message: err.toString() });
+        }
+    }
+
+    async insertArea(req, res) {
+        try {
+            const { insertData } = JSON.parse(req.body.data);
+            await facade.insertAreas(insertData);
+            res.json({ error: 0, message: 'update areas success' });
+        } catch (err) {
+            console.log(err);
+            res.json({ error: 1, message: err.toString() });
+        }
+    }
+
+    async deleteArea(req, res) {
+        try {
+            const { deleteData } = JSON.parse(req.body.data);
+            await facade.deleteAreas(deleteData);
+            res.json({ error: 0, message: 'update areas success' });
+        } catch (err) {
+            console.log(err);
+            res.json({ error: 1, message: err.toString() });
+        }
+    }
+
+    async getClassData(req, res) {
+        try {
+            const { libID, areaID } = JSON.parse(req.body.data);
+            const data = await facade.getClassData(libID, areaID);
+            res.json({ error: 0, message: 'getCLass success', data });
+        } catch (err) {
+            console.log(err);
+            res.json({ error: 1, message: err.toString() });
+        }
+    }
+
+    async getPriceData(req, res) {
+        try {
+            const { classIDList } = JSON.parse(req.body.data);
+            const data = await facade.getPriceData(classIDList);
+            res.json({ error: 0, message: 'getPriceData success', data });
+        } catch (err) {
+            console.log(err);
+            res.json({ error: 1, message: err.toString() });
+        }
+    }
+
+    async editPriceData(req, res) {
+        try {
+            const { postData } = JSON.parse(req.body.data);
+            await facade.editPriceData(postData);
+            res.json({ error: 0, message: 'editPrice success' });
+        } catch (err) {
+            console.log(err);
+            res.json({ error: 1, message: err.toString() });
+        }
+    }
+
+    async editClassData(req, res) {
+        try {
+            const { updateData } = JSON.parse(req.body.data);
+            await facade.editClassData(updateData);
+            res.json({ error: 0, message: 'editClass success' });
+        } catch (err) {
+            console.log(err);
+            res.json({ error: 1, message: err.toString() });
+        }
+    }
+
+}
+
+module.exports = {
+    priceInfoController: new PriceInfoController()
+};

+ 463 - 0
modules/price_info_lib/facade/index.js

@@ -0,0 +1,463 @@
+const mongoose = require('mongoose');
+const uuidV1 = require('uuid/v1');
+const { CRAWL_LOG_KEY, ProcessStatus } = require('../../../public/constants/price_info_constant');
+
+const priceInfoLibModel = mongoose.model('std_price_info_lib');
+const priceInfoClassModel = mongoose.model('std_price_info_class');
+const priceInfoItemModel = mongoose.model('std_price_info_items');
+const priceInfoAreaModel = mongoose.model('std_price_info_areas');
+const compilationModel = mongoose.model('compilation');
+const importLogsModel = mongoose.model('import_logs');
+
+async function getLibs(query) {
+    return await priceInfoLibModel.find(query).lean();
+}
+
+async function createLib(name, period, compilationID) {
+    // 将2020-01变成2020年01月
+    const reg = /(\d{4})-(\d{2})/;
+    const formattedPeriod = period.replace(reg, '$1年-$2月');
+    const lib = {
+        ID: uuidV1(),
+        name,
+        period: formattedPeriod,
+        compilationID,
+        createDate: Date.now(),
+    };
+    await priceInfoLibModel.create(lib);
+    return lib;
+}
+
+async function updateLib(query, updateData) {
+    await priceInfoLibModel.update(query, updateData);
+}
+
+async function deleteLib(libID) {
+    await priceInfoClassModel.remove({ libID });
+    await priceInfoItemModel.remove({ libID });
+    await priceInfoLibModel.remove({ ID: libID });
+}
+
+async function processChecking(key) {
+    const logData = key
+        ? await importLogsModel.findOne({ key })
+        : await importLogsModel.findOne({ key: CRAWL_LOG_KEY });
+    if (!logData) {
+        return { status: ProcessStatus.FINISH };
+    }
+    if (logData.status === ProcessStatus.FINISH || logData.status === ProcessStatus.ERROR) {
+        await importLogsModel.remove({ key: logData.key });
+    }
+    return { status: logData.status, errorMsg: logData.errorMsg || '', key: logData.key };
+}
+
+// 爬取数据
+async function crawlDataByCompilation(compilationID, from, to) {
+    if (!compilationID) {
+        throw '无有效费用定额。';
+    }
+    const compilationData = await compilationModel.findOne({ _id: mongoose.Types.ObjectId(compilationID) }, 'overWriteUrl').lean();
+    if (!compilationData || !compilationData.overWriteUrl) {
+        throw '无有效费用定额。';
+    }
+    // 从overWriteUrl提取并组装爬虫文件
+    const reg = /\/([^/]+)\.js/;
+    const matched = compilationData.overWriteUrl.match(reg);
+    const crawlURL = `${matched[1]}_price_crawler.js`;
+    let crawlData;
+    try {
+        const crawler = require(`../../../web/over_write/js/${crawlURL}`);
+        crawlData = crawler.crawlData;
+    } catch (e) {
+        throw '该费用定额无可用爬虫方法。'
+    }
+    //await crawlData(from, to);
+    // 异步不等结果,结果由checking来获取
+    crawlDataByMiddleware(crawlData, from, to);
+}
+
+// 爬取数据中间件,主要处理checking初始化
+async function crawlDataByMiddleware(crawlFunc, from, to) {
+    const logUpdateData = { status: ProcessStatus.FINISH };
+    try {
+        const logData = {
+            key: CRAWL_LOG_KEY,
+            content: '正在爬取数据,请稍候……',
+            status: ProcessStatus.START,
+            create_time: Date.now()
+        };
+        await importLogsModel.create(logData);
+        await crawlFunc(from, to);
+    } catch (err) {
+        logUpdateData.errorMsg = err;
+        logUpdateData.status = ProcessStatus.ERROR;
+    } finally {
+        await importLogsModel.update({ key: CRAWL_LOG_KEY }, logUpdateData);
+    }
+}
+
+// 导入excel数据,格式如下
+// 格式1:
+//地区	    分类	        编码	名称	        规格型号    单位	不含税价	含税价
+//江北区	黑色及有色金属		    热轧光圆钢筋	  φ6(6.5)	         3566.37	4030
+//江北区	木、竹材料及其制品		柏木门套线	      60×10	             8.76	    9.9
+// 格式2:
+//地区	    分类	            编码	名称	        规格型号	    不含税价	含税价
+//江北区	黑色及有色金属		        热轧光圆钢筋	  φ6(6.5)	     3566.37	4030
+//			                          柏木门套线	    60×10	       8.76	      9.9
+//			                          沥青混凝土	    AC-13	       982.3	 1110
+//						
+//北碚区	木、竹材料及其制品		    热轧光圆钢筋	  φ6(6.5)	      3566.37	4030
+async function importExcelData(libID, sheetData) {
+    const libs = await getLibs({ ID: libID });
+    const compilationID = libs[0].compilationID;
+    // 建立区映射表:名称-ID映射、ID-名称映射
+    const areaList = await getAreas(compilationID);
+    const areaMap = {};
+    areaList.forEach(({ ID, name }) => {
+        areaMap[name] = ID;
+        areaMap[ID] = name;
+    });
+    // 建立分类映射表:地区名称@分类名称:ID映射
+    /*  const classMap = {};
+     const classList = await getClassData(libID);
+     classList.forEach(({ ID, areaID, name }) => {
+         const areaName = areaMap[areaID] || '';
+         classMap[`${areaName}@${name}`] = ID;
+     }); */
+    // 第一行获取行映射
+    const colMap = {};
+    for (let col = 0; col < sheetData[0].length; col++) {
+        const cellText = sheetData[0][col];
+        switch (cellText) {
+            case '地区':
+                colMap.area = col;
+                break;
+            case '分类':
+                colMap.class = col;
+                break;
+            case '编码':
+                colMap.code = col;
+                break;
+            case '名称':
+                colMap.name = col;
+                break;
+            case '规格型号':
+                colMap.specs = col;
+                break;
+            case '单位':
+                colMap.unit = col;
+                break;
+            case '不含税价':
+                colMap.noTaxPrice = col;
+                break;
+            case '含税价':
+                colMap.taxPrice = col;
+                break;
+        }
+    }
+    // 提取数据
+    const data = [];
+    const classData = [];
+    const areaClassDataMap = {};
+    let curAreaName;
+    let curClassName;
+    let curClassID;
+    for (let row = 1; row < sheetData.length; row++) {
+        const areaName = sheetData[row][colMap.area] || '';
+        const className = sheetData[row][colMap.class] || '';
+        const code = sheetData[row][colMap.code] || '';
+        const name = sheetData[row][colMap.name] || '';
+        const specs = sheetData[row][colMap.specs] || '';
+        const unit = sheetData[row][colMap.unit] || '';
+        const noTaxPrice = sheetData[row][colMap.noTaxPrice] || '';
+        const taxPrice = sheetData[row][colMap.taxPrice] || '';
+        if (!className && !code && !name && !specs && !noTaxPrice && !taxPrice) { // 认为是空数据
+            continue;
+        }
+        if (areaName && areaName !== curAreaName) {
+            curAreaName = areaName;
+        }
+        const areaID = areaMap[curAreaName];
+        if (!areaID) {
+            continue;
+        }
+        if (className && className !== curClassName) {
+            curClassName = className;
+            const classItem = {
+                libID,
+                areaID,
+                ID: uuidV1(),
+                ParentID: '-1',
+                NextSiblingID: '-1',
+                name: curClassName
+            };
+            curClassID = classItem.ID;
+            classData.push(classItem);
+            (areaClassDataMap[areaID] || (areaClassDataMap[areaID] = [])).push(classItem);
+            const preClassItem = areaClassDataMap[areaID][areaClassDataMap[areaID].length - 2];
+            if (preClassItem) {
+                preClassItem.NextSiblingID = classItem.ID;
+            }
+        }
+        if (!curClassID) {
+            continue;
+        }
+        data.push({
+            ID: uuidV1(),
+            compilationID,
+            libID,
+            areaID,
+            classID: curClassID,
+            period: libs[0].period,
+            code,
+            name,
+            specs,
+            unit,
+            noTaxPrice,
+            taxPrice
+        });
+    }
+    if (classData.length) {
+        await priceInfoClassModel.remove({ libID });
+        await priceInfoClassModel.insertMany(classData);
+    }
+    if (data.length) {
+        await priceInfoItemModel.remove({ libID });
+        await priceInfoItemModel.insertMany(data);
+    } else {
+        throw 'excel没有有效数据。'
+    }
+}
+/* async function importExcelData(libID, sheetData) {
+    const libs = await getLibs({ ID: libID });
+    const compilationID = libs[0].compilationID;
+    // 建立区映射表:名称-ID映射、ID-名称映射
+    const areaList = await getAreas(compilationID);
+    const areaMap = {};
+    areaList.forEach(({ ID, name }) => {
+        areaMap[name] = ID;
+        areaMap[ID] = name;
+    });
+    // 建立分类映射表:地区名称@分类名称:ID映射
+    const classMap = {};
+    const classList = await getClassData(libID);
+    classList.forEach(({ ID, areaID, name }) => {
+        const areaName = areaMap[areaID] || '';
+        classMap[`${areaName}@${name}`] = ID;
+    });
+    // 第一行获取行映射
+    const colMap = {};
+    for (let col = 0; col < sheetData[0].length; col++) {
+        const cellText = sheetData[0][col];
+        switch (cellText) {
+            case '地区':
+                colMap.area = col;
+                break;
+            case '分类':
+                colMap.class = col;
+                break;
+            case '编码':
+                colMap.code = col;
+                break;
+            case '名称':
+                colMap.name = col;
+                break;
+            case '规格型号':
+                colMap.specs = col;
+                break;
+            case '单位':
+                colMap.unit = col;
+                break;
+            case '不含税价':
+                colMap.noTaxPrice = col;
+                break;
+            case '含税价':
+                colMap.taxPrice = col;
+                break;
+        }
+    }
+    // 提取数据
+    const data = [];
+    let curAreaName;
+    let curClassName;
+    for (let row = 1; row < sheetData.length; row++) {
+        const areaName = sheetData[row][colMap.area] || '';
+        const className = sheetData[row][colMap.class] || '';
+        const code = sheetData[row][colMap.code] || '';
+        const name = sheetData[row][colMap.name] || '';
+        const specs = sheetData[row][colMap.specs] || '';
+        const unit = sheetData[row][colMap.unit] || '';
+        const noTaxPrice = sheetData[row][colMap.noTaxPrice] || '';
+        const taxPrice = sheetData[row][colMap.taxPrice] || '';
+        if (!code && !name && !specs && !noTaxPrice && !taxPrice) { // 认为是空数据
+            continue;
+        }
+        if (areaName && areaName !== curAreaName) {
+            curAreaName = areaName;
+        }
+        if (className && className !== curClassName) {
+            curClassName = className;
+        }
+        const areaID = areaMap[curAreaName];
+        if (!areaID) {
+            continue;
+        }
+        const classID = classMap[`${curAreaName}@${curClassName}`];
+        if (!classID) {
+            continue;
+        }
+        data.push({
+            ID: uuidV1(),
+            compilationID,
+            libID,
+            areaID,
+            classID,
+            period: libs[0].period,
+            code,
+            name,
+            specs,
+            unit,
+            noTaxPrice,
+            taxPrice
+        });
+    }
+    if (data.length) {
+        await priceInfoItemModel.remove({ libID });
+        await priceInfoItemModel.insertMany(data);
+    } else {
+        throw 'excel没有有效数据。'
+    }
+} */
+
+// 获取费用定额的地区数据
+async function getAreas(compilationID) {
+    return await priceInfoAreaModel.find({ compilationID }, '-_id ID name').lean();
+}
+
+async function updateAres(updateData) {
+    const bulks = [];
+    updateData.forEach(({ ID, name }) => bulks.push({
+        updateOne: {
+            filter: { ID },
+            update: { name }
+        }
+    }));
+    if (bulks.length) {
+        await priceInfoAreaModel.bulkWrite(bulks);
+    }
+}
+
+async function insertAreas(insertData) {
+    await priceInfoAreaModel.insertMany(insertData);
+}
+
+async function deleteAreas(deleteData) {
+    await priceInfoClassModel.remove({ areaID: { $in: deleteData } });
+    await priceInfoItemModel.remove({ areaID: { $in: deleteData } });
+    await priceInfoAreaModel.remove({ ID: { $in: deleteData } });
+}
+
+async function getClassData(libID, areaID) {
+    if (libID && areaID) {
+        return await priceInfoClassModel.find({ libID, areaID }, '-_id').lean();
+    }
+    if (libID) {
+        return await priceInfoClassModel.find({ libID }, '-_id').lean();
+    }
+    if (areaID) {
+        return await priceInfoClassModel.find({ areaID }, '-_id').lean();
+    }
+
+}
+
+async function getPriceData(classIDList) {
+    return await priceInfoItemModel.find({ classID: { $in: classIDList } }, '-_id').lean();
+}
+
+const UpdateType = {
+    UPDATE: 'update',
+    DELETE: 'delete',
+    CREATE: 'create',
+};
+
+async function editPriceData(postData) {
+    const bulks = [];
+    postData.forEach(data => {
+        if (data.type === UpdateType.UPDATE) {
+            bulks.push({
+                updateOne: {
+                    filter: { ID: data.ID },
+                    update: { ...data.data }
+                }
+            });
+        } else if (data.type === UpdateType.DELETE) {
+            bulks.push({
+                deleteOne: {
+                    filter: { ID: data.ID }
+                }
+            });
+        } else {
+            bulks.push({
+                insertOne: {
+                    document: data.data
+                }
+            });
+        }
+    });
+    if (bulks.length) {
+        await priceInfoItemModel.bulkWrite(bulks);
+    }
+}
+
+async function editClassData(updateData) {
+    const bulks = [];
+    const deleteIDList = [];
+    updateData.forEach(({ type, filter, update, document }) => {
+        if (type === UpdateType.UPDATE) {
+            bulks.push({
+                updateOne: {
+                    filter,
+                    update
+                }
+            });
+        } else if (type === UpdateType.DELETE) {
+            deleteIDList.push(filter.ID);
+            bulks.push({
+                deleteOne: {
+                    filter
+                }
+            });
+        } else {
+            bulks.push({
+                insertOne: {
+                    document
+                }
+            });
+        }
+    });
+    if (deleteIDList.length) {
+        await priceInfoItemModel.remove({ classID: { $in: deleteIDList } });
+    }
+    if (bulks.length) {
+        await priceInfoClassModel.bulkWrite(bulks);
+    }
+}
+
+module.exports = {
+    getLibs,
+    createLib,
+    updateLib,
+    deleteLib,
+    processChecking,
+    crawlDataByCompilation,
+    importExcelData,
+    getAreas,
+    updateAres,
+    insertAreas,
+    deleteAreas,
+    getClassData,
+    getPriceData,
+    editPriceData,
+    editClassData
+}

+ 31 - 0
modules/price_info_lib/routes/index.js

@@ -0,0 +1,31 @@
+/**
+ * Created by zhang on 2018/9/3.
+ */
+
+const express = require("express");
+const router = express.Router();
+const { priceInfoController } = require('../controllers/index');
+
+module.exports = function (app) {
+
+    router.get("/main", priceInfoController.auth, priceInfoController.init, priceInfoController.main);
+    router.get("/edit", priceInfoController.auth, priceInfoController.init, priceInfoController.editView);
+    router.post("/addLib", priceInfoController.auth, priceInfoController.init, priceInfoController.addLib);
+    router.post("/renameLib", priceInfoController.auth, priceInfoController.init, priceInfoController.renameLib);
+    router.post("/deleteLib", priceInfoController.auth, priceInfoController.init, priceInfoController.deleteLib);
+    router.post("/processChecking", priceInfoController.auth, priceInfoController.init, priceInfoController.processChecking);
+    router.post("/crawlData", priceInfoController.auth, priceInfoController.init, priceInfoController.crawlData);
+    router.post("/importExcel", priceInfoController.auth, priceInfoController.init, priceInfoController.importExcel);
+
+    router.post("/editArea", priceInfoController.auth, priceInfoController.init, priceInfoController.editArea);
+    router.post("/insertArea", priceInfoController.auth, priceInfoController.init, priceInfoController.insertArea);
+    router.post("/deleteArea", priceInfoController.auth, priceInfoController.init, priceInfoController.deleteArea);
+    router.post("/getClassData", priceInfoController.auth, priceInfoController.init, priceInfoController.getClassData);
+    router.post("/getPriceData", priceInfoController.auth, priceInfoController.init, priceInfoController.getPriceData);
+    router.post("/editPriceData", priceInfoController.auth, priceInfoController.init, priceInfoController.editPriceData);
+    router.post("/editClassData", priceInfoController.auth, priceInfoController.init, priceInfoController.editClassData);
+
+    app.use("/priceInfo", router);
+};
+
+

+ 138 - 3
modules/ration_repository/models/ration_item.js

@@ -18,9 +18,146 @@ const installationDao = new InstallationDao();
 import GljDao from "../../std_glj_lib/models/gljModel";
 const stdGljDao = new GljDao();
 import stdgljutil  from "../../../public/cache/std_glj_type_util";
-
+const stdGLJItemModel = mongoose.model('std_glj_lib_gljList');
 var rationItemDAO = function(){};
 
+// 处理部颁数据
+rationItemDAO.prototype.handleBBData = async function (rationLibID, gljLibID) {
+    const rations = await rationItemModel.find({ rationRepId: rationLibID }, '-_id code ID rationGljList').lean();
+    const gljs = await stdGLJItemModel.find({ repositoryId: gljLibID, 'component.0': {$exists: true} }, '-_id ID component').lean();
+    const gljIDMap = {};
+    gljs.forEach(glj => gljIDMap[glj.ID] = glj);
+    const updateData = [];
+    const errorRange = 0.004;
+    for (const ration of rations) {
+        if (!ration.rationGljList) {
+            continue;
+        }
+        const componentAmtMap = {};
+        for (const rGLJ of ration.rationGljList) {
+            const stdGLJ = gljIDMap[rGLJ.gljId];
+            if (!stdGLJ) {
+                continue;
+            }
+            for (const c of stdGLJ.component) {
+                const amt = c.consumeAmt * rGLJ.consumeAmt;
+                if (componentAmtMap[c.ID]) {
+                    componentAmtMap[c.ID] += amt;
+                } else {
+                    componentAmtMap[c.ID] = amt;
+                }
+            }
+        }
+        const newRationGljList = [];
+        let isChanged = false;
+        for (let i = 0; i < ration.rationGljList.length; i++) {
+            const rGLJ = ration.rationGljList[i];
+            if (componentAmtMap[rGLJ.gljId]) {
+                isChanged = true;
+                const diff = scMathUtil.roundTo(rGLJ.consumeAmt - componentAmtMap[rGLJ.gljId], -3);
+                if (diff > errorRange || diff < -errorRange) {
+                    // 扣减
+                    rGLJ.consumeAmt = diff;
+                    if (diff < 0) {
+                        console.log(`ration.code`);
+                        console.log(ration.code);
+                    }
+                    newRationGljList.push(rGLJ);    
+                }
+            } else {
+                newRationGljList.push(rGLJ);
+            }
+        }
+        if (isChanged) {
+            updateData.push({
+                updateOne: {
+                    filter: { ID: ration.ID },
+                    update: { rationGljList: newRationGljList }
+                }
+            });
+        }
+    }
+    if (updateData.length) {
+        await rationItemModel.bulkWrite(updateData);
+    }
+};
+
+/* rationItemDAO.prototype.copyLib = async function (sourceLibID, targetLibID) {
+    // coe-list
+    const coeIDMap = {};
+    const newCoeData = [];
+    const sourceCoeData = await _stdRationCoeModel.find({ libID: sourceLibID }, '-_id').lean();
+    const coeCount = await counter.counterDAO.getIDAfterCount(counter.moduleName.coeList, sourceCoeData.length);
+    const coeIdx = coeCount.sequence_value - (sourceCoeData.length - 1);
+    sourceCoeData.forEach((coe, index) => {
+        coeIDMap[coe.ID] = coeIdx + index;
+        newCoeData.push({
+            ...coe,
+            libID: targetLibID,
+            ID: coeIDMap[coe.ID]
+        });
+    });
+    await stdRationCoeModel.insertMany(newCoeData);
+    // ration-section
+    const sectionIDMap = {};
+    const newSectionData = [];
+    const sourceSectionData = await _stdRationSectionModel.find({ rationRepId: sourceLibID }, '-_id').lean();
+    const sectionCount = await counter.counterDAO.getIDAfterCount(counter.moduleName.rationTree, sourceSectionData.length);
+    const sectionIdx = sectionCount.sequence_value - (sourceSectionData.length - 1);
+    sourceSectionData.forEach((section, index) => {
+        sectionIDMap[section.ID] = sectionIdx + index;
+    });
+    sourceSectionData.forEach(section => {
+        newSectionData.push({
+            ...section,
+            rationRepId: targetLibID,
+            ID: sectionIDMap[section.ID],
+            ParentID: sectionIDMap[section.ParentID] || -1,
+            NextSiblingID: sectionIDMap[section.NextSiblingID] || -1
+        });
+    });
+    await stdRationSectionModel.insertMany(newSectionData);
+    // glj
+    const sourceGLJData = await stdGLJModel.find({ repositoryId: 182 }, '-_id').lean();
+    const gljIDMap = {};
+    sourceGLJData.forEach(glj => {
+        gljIDMap[glj.orgID] = glj.ID;
+    });
+    const newGLJData = sourceGLJData.map(glj => {
+        delete glj.orgID;
+        return glj;
+    });
+    await stdGLJModel.remove({ repositoryId: 182 }); // 5412
+    await stdGLJModel.insertMany(newGLJData);
+    // ration
+    const newRationData = [];
+    const sourceRationData = await _rationItemModel.find({ rationRepId: sourceLibID }, '-_id').lean();
+    const rationCount = await counter.counterDAO.getIDAfterCount(counter.moduleName.rations, sourceRationData.length);
+    const rationIdx = rationCount.sequence_value - (sourceRationData.length - 1);
+    sourceRationData.forEach((ration, index) => {
+        const rationID = rationIdx + index;
+        const sectionID = sectionIDMap[ration.sectionId];
+        const newRationCoeList = (ration.rationCoeList || []).map(rCoe => ({
+            no: rCoe.no,
+            ID: coeIDMap[rCoe.ID]
+        }));
+        const newRationGLJList = (ration.rationGljList || []).map(rGLJ => ({
+            ...rGLJ,
+            gljId: gljIDMap[rGLJ.gljId],
+        }));
+        newRationData.push({
+            ...ration,
+            rationRepId: targetLibID,
+            ID: rationID,
+            sectionId: sectionID,
+            rationCoeList: newRationCoeList,
+            rationGljList: newRationGLJList
+        });
+    });
+    await rationItemModel.insertMany(newRationData);
+
+};
+ */
 // 由于导入excel时,excel数据存在负的工程量,所以导入后一些定额人材机的消耗量可能为负,需要处理
 rationItemDAO.prototype.handleMinusQuantity = async function() {
     const updateTask = [];
@@ -46,8 +183,6 @@ rationItemDAO.prototype.handleMinusQuantity = async function() {
     if (updateTask.length) {
         await rationItemModel.bulkWrite(updateTask);
     }
-    console.log(`repIDs`);
-    console.log(repIDs);
 };
 
 rationItemDAO.prototype.prepareInitData = async function (rationRepId) {

+ 1 - 1
modules/ration_repository/models/repository_map.js

@@ -197,7 +197,7 @@ rationRepositoryDao.prototype.updateName = function(oprtor, renameObj, callback)
                     callback(err, '更新最近操作者失败!');
                 }
                 else{
-                    engLibModel.update({'ration_lib.id': renameObj.ID}, {$set: {'ration_lib.$.name': renameObj.newName}}, {multi: true}, function (err) {
+                    engLibModel.update({'ration_lib.id': {$in: [String(renameObj.ID), +renameObj.ID]}}, {$set: {'ration_lib.$.name': renameObj.newName}}, {multi: true}, function (err) {
                         if(err){
                             callback(err, '更新工程专业引用失败!');
                         }

+ 107 - 0
modules/reports/controllers/rpt_tpl_controller.js

@@ -222,6 +222,113 @@ let mExport = {
             }
         })
     },
+    partialUpdateTreeNode: function (req, res) {
+        const params = JSON.parse(req.body.params);
+        const pathArray = params.pathArray;
+        const nodeArray = params.nodeArray;
+
+        const _getCurrentNodeSerialOrder = function(parentItems, pathName) {
+            let rst = -1;
+            if (parentItems) {
+                for (let idx = 0; idx < parentItems.length; idx++) {
+                    if (parentItems[idx].name === pathName) {
+                        rst = idx;
+                        break;
+                    }
+                }
+            }
+            return rst;
+        };
+
+        const _updateNodeByPath = function(path, node, topNodeItems) {
+            let rst = false;
+            let tmpParentItems = topNodeItems;
+            for (let idx = 0; idx < path.node_path.length; idx++) {
+                const nIdx = _getCurrentNodeSerialOrder(tmpParentItems, path.node_path[idx]);
+                if (nIdx >= 0) {
+                    if (idx === path.node_path.length - 1) {
+                        tmpParentItems[nIdx] = node;
+                        rst = true;
+                    } else {
+                        tmpParentItems = tmpParentItems[nIdx].items;
+                    }
+                }
+            }
+            return rst;
+        };
+        const _addNodeByPath = function(path, node, topNodeItems) {
+            let rst = false;
+            let tmpParentItems = topNodeItems;
+            if (path === null || path === '' || path.node_path.length === 0) {
+                topNodeItems.push(node);
+                rst = true;
+            } else {
+                for (let idx = 0; idx < path.node_path.length; idx++) {
+                    const nIdx = _getCurrentNodeSerialOrder(tmpParentItems, path.node_path[idx]);
+                    if (nIdx >= 0) {
+                        if (idx === path.node_path.length - 1) {
+                            if (tmpParentItems[nIdx].nodeType === 1) {
+                                if (!tmpParentItems[nIdx].items) {
+                                    tmpParentItems[nIdx].items = [];
+                                }
+                                tmpParentItems[nIdx].items.push(node);
+                                rst = true;
+                            }
+                        } else {
+                            tmpParentItems = tmpParentItems[nIdx].items;
+                        }
+                    }
+                }
+            }
+            return rst;
+        };
+        const _deleteNodeByPath = function(path, node, topNodeItems) {
+            let rst = false;
+            let tmpParentItems = topNodeItems;
+            for (let idx = 0; idx < path.node_path.length; idx++) {
+                const nIdx = _getCurrentNodeSerialOrder(tmpParentItems, path.node_path[idx]);
+                if (nIdx >= 0) {
+                    if (idx === path.node_path.length - 1) {
+                        tmpParentItems.splice(nIdx, 1);
+                        rst = true;
+                    } else {
+                        tmpParentItems = tmpParentItems[nIdx].items;
+                    }
+                }
+            }
+            return rst;
+        };
+        // console.log('compilationId: ' + compilationId);
+        rttFacade.findTplTree(params.compilationId, params.userId).then(function (targetTplTreeNode) {
+            if (pathArray && pathArray.length > 0 && nodeArray && nodeArray.length === pathArray.length) {
+                let doc = targetTplTreeNode._doc ? targetTplTreeNode._doc[0] : targetTplTreeNode[0];
+                // console.log(doc);
+                const topNodeItems = doc.items;
+
+                for (let idx = 0; idx < pathArray.length; idx++) {
+                    if (pathArray[idx].operation_type === 'update') {
+                        _updateNodeByPath(pathArray[idx], nodeArray[idx], topNodeItems);
+                    } else if (pathArray[idx].operation_type === 'add') {
+                        _addNodeByPath(pathArray[idx], nodeArray[idx], topNodeItems);
+                    } else if (pathArray[idx].operation_type === 'delete') {
+                        _deleteNodeByPath(pathArray[idx], nodeArray[idx], topNodeItems);
+                    } else {
+                        // out of control
+                    }
+                }
+                // console.log(topNodeItems);
+                rttFacade.updateTree(doc.compilationId, doc.engineerId, doc.userId, doc).then(function (rst) {
+                    if (rst) {
+                        //success
+                        callback(req,res, false, "", rst);
+                    } else {
+                        //failed
+                        callback(req,res, true, "更新失败!", null);
+                    }
+                })
+            }
+        })
+    },
     updateTopNodeName: function (req, res) {
         //备注:因设计的更改,此方法将被放弃
         let params = JSON.parse(req.body.params),

+ 1 - 0
modules/reports/routes/rpt_tpl_router.js

@@ -21,6 +21,7 @@ module.exports = function (app) {
 
     rptTplRouter.post('/createTreeRootNode', reportTplController.createTreeRootNode);
     rptTplRouter.post('/updateTreeRootNode', reportTplController.updateTreeRootNode);
+    rptTplRouter.post('/partialUpdateTreeNode', reportTplController.partialUpdateTreeNode);
     rptTplRouter.post('/updateTopNodeName', reportTplController.updateTopNodeName);
     rptTplRouter.post('/updateSubLevelOneNode', reportTplController.updateSubLevelOneNode);
     rptTplRouter.post('/removeTreeRootNode', reportTplController.removeTreeRootNode);

+ 1 - 0
modules/std_glj_lib/controllers/gljController.js

@@ -208,6 +208,7 @@ class GljController extends BaseController{
             const info = await gljDao.getReference(repositoryId, gljId);
             res.json({error: 0, message: 'success', data: info});
         } catch (err) {
+            console.log(err);
             res.json({error: 1, message: 'fail', data: null});
         }
     }

+ 80 - 0
modules/std_glj_lib/models/gljModel.js

@@ -17,6 +17,86 @@ import counter from "../../../public/counter/counter";
 import async from "async";
 
 class GljDao  extends OprDao{
+    // 自动计算组含有组成物的人材机的定额价
+    async calcPriceForComposition(gljLibID) {
+        const gljs = await gljModel.find({ repositoryId: gljLibID }, '-_id ID component basePrice').lean();
+        const updateData = [];
+        const toCalcGLJs = [];
+        const gljIDMap = {};
+        for (let i = 0; i < gljs.length; i++) {
+            const glj = gljs[i];
+            gljIDMap[glj.ID] = glj;
+            if (glj.component && glj.component.length) {
+                toCalcGLJs.push(glj);
+            }
+        }
+        for (let i = 0; i < toCalcGLJs.length; i++) {
+            const glj = toCalcGLJs[i];
+            let sum = 0;
+            for (let j = 0; j < glj.component.length; j++) {
+                const c = glj.component[j];
+                if (!gljIDMap[c.ID]) {
+                    continue;
+                }
+                sum += c.consumeAmt * gljIDMap[c.ID].basePrice;
+            }
+            sum = scMathUtil.roundTo(sum, -2);
+            updateData.push({
+                updateOne: {
+                    filter: { ID: glj.ID },
+                    update: { basePrice: sum }
+                }
+            });
+        }
+        if (updateData.length) {
+            await gljModel.bulkWrite(updateData);
+        }
+    }
+
+    async copyLib(sourceLibID, targetLibID) {
+        const task = [
+            this.copyClassData(sourceLibID, targetLibID),
+            this.copyGLJData(sourceLibID, targetLibID)
+        ];
+        await Promise.all(task);
+    }
+
+    async copyClassData(sourceLibID, targetLibID) {
+        const sourceClassData = await gljClassModel.find({ repositoryId: sourceLibID }, '-_id').lean();
+        const insertData = sourceClassData.map(item => ({
+            ... item,
+            repositoryId: targetLibID
+        }));
+        if (insertData.length) {
+            await gljClassModel.insertMany(insertData);
+        }
+    }
+
+    async copyGLJData(sourceLibID, targetLibID) {
+        const sourceGLJData = await gljModel.find({ repositoryId: sourceLibID }, '-_id').lean();
+        const IDMapping = {};
+        const countData = await counter.counterDAO.getIDAfterCount(counter.moduleName.GLJ, sourceGLJData.length);
+        const countIdx = countData.sequence_value - (sourceGLJData.length - 1);
+        sourceGLJData.forEach((glj, index) => {
+            IDMapping[glj.ID] = countIdx + index;
+        });
+        const insertData = sourceGLJData.map(glj => {
+            const newComponent = (glj.component || []).map(c => ({
+                ID: IDMapping[c.ID],
+                consumeAmt: c.consumeAmt
+            }));
+            return {
+                ...glj,
+                repositoryId: targetLibID,
+                ID: IDMapping[glj.ID],
+                component: newComponent
+            };
+        });
+        if (insertData.length) {
+            await gljModel.insertMany(insertData);
+        }
+    }
+
     async getReference(repositoryId, gljId) {
         const gljLib = await gljMapModel.findOne({ID: repositoryId});
         const rationLibIds = gljLib.rationLibs.map(lib => lib.ID);

+ 1 - 11
modules/sys_tools/controllers/sys_controller.js

@@ -14,13 +14,13 @@ import multiparty from 'multiparty';
 import BaseController from "../../common/base/base_controller";
 //import sysSchedule from '../models/sys_model';
 let sysSchedule = require('../models/sys_model');
-let productData = require('../models/product_model');
 let callback = function(req, res, err, message, data){
     res.json({error: err, message: message, data: data});
 };
 const shareDir = 'public/share/';
 class SysTools extends BaseController{
     clearJunkData(req, res){
+        res.setTimeout(1000 * 60 * 60);
         sysSchedule.clearJunkData(function (err) {
             let msg = '清除成功';
             let errCode = 0;
@@ -90,16 +90,6 @@ class SysTools extends BaseController{
         });
     }
 
-    async changeProductInfo(req, res) {
-        try {
-            let version = req.body.version;
-            await productData.changeInfo({version});
-        } catch (error) {
-            console.log(error);
-        }
-        res.redirect(req.headers.referer);
-    }
-
 }
 
 export {SysTools as default};

+ 0 - 30
modules/sys_tools/models/product_model.js

@@ -1,30 +0,0 @@
-'use strict';
-
-/**
- *
- *
- * @author Zhong
- * @date 2019/3/21
- * @version
- */
-
-import mongoose from 'mongoose';
-let productModel = mongoose.model('product');
-
-async function changeInfo(updateData) {
-    await productModel.update({}, {$set: updateData}, {upsert: true});
-}
-
-async function getInfo() {
-    let data = await productModel.findOne({});
-    if (!data) {
-        await productModel.create({version: ''});
-        return await productModel.findOne({});
-    }
-    return data;
-}
-
-module.exports = {
-    getInfo,
-    changeInfo
-};

+ 76 - 1
modules/sys_tools/models/sys_model.js

@@ -10,6 +10,8 @@
 
 import mongoose from 'mongoose';
 import async from 'async';
+import moment from 'moment';
+const SMS = require('../../users/models/sms');
 const projectModel = mongoose.model('projects');
 const projSettingModel = mongoose.model('proj_setting');
 const calcProgramModel = mongoose.model('calc_programs');
@@ -33,6 +35,8 @@ const evaluateListModel = mongoose.model("evaluate_list");
 const bidListModel = mongoose.model("bid_evaluation_list");
 const contractorListModel = mongoose.model("contractor_list");
 const shareListModel = mongoose.model('share_list');
+const userModel = mongoose.model('users');
+const compilationModel = mongoose.model('compilation');
 
 
 //删除垃圾数据
@@ -182,9 +186,80 @@ async function clearFakeData(callback) {
     });
 }
 
+/*
+* 每天定时清理用户限期的专业版编办,降为免费公用版。
+* */
+async function checkUserCompilationStatus(callback) {
+    let functions = [];
+    let today = moment(new Date()).format('YYYY-MM-DD');
+    let userList = await userModel.find({upgrade_list: {$elemMatch:{ deadline: today }}});
+    if (userList.length > 0) {
+        for (let user of userList) {
+            let ssoId = JSON.parse(JSON.stringify(user)).ssoId; // 坑啊,没法直接获取到user.ssoId,要转义
+            for (let cul of user.upgrade_list) {
+                if (cul.deadline === today) {
+                    // cul.deadline = '';
+                    cul.isUpgrade = false;
+                }
+            }
+            functions.push(function (cb) {
+                userModel.update({ssoId: ssoId}, {upgrade_list: user.upgrade_list}, { safe: true }, cb);
+            });
+        }
+    }
+    if(functions.length > 0){
+        async.parallel(functions, async function(err, result){
+            if(callback){
+                callback(err);
+            }
+        });
+    } else {
+        if(callback) callback(0);
+    }
+
+}
+
+/*
+* 为每天降为免费公用版的用户发送降级短信。
+* */
+async function sendCompilationStatusSms(callback) {
+    let functions = [];
+    let today = moment(new Date()).format('YYYY-MM-DD');
+    let userList = await userModel.find({upgrade_list: {$elemMatch:{ deadline: today }}});
+    if (userList.length > 0) {
+        const Sms = new SMS();
+        for (let user of userList) {
+            // let ssoId = JSON.parse(JSON.stringify(user)).ssoId;
+            for (let cul of user.upgrade_list) {
+                if (cul.deadline === today) {
+                    // cul.deadline = '';
+                    // cul.isUpgrade = false;
+                    // 发送短信
+                    let compilationData = await compilationModel.findOne({_id: cul.compilationID});
+                    await Sms.sendProductMsg(user.mobile, 2, user.real_name, compilationData.name, '');
+                }
+            }
+            // functions.push(function (cb) {
+            //     userModel.update({ssoId: ssoId}, {upgrade_list: user.upgrade_list}, { safe: true }, cb);
+            // });
+        }
+    }
+    if(functions.length > 0){
+        async.parallel(functions, async function(err, result){
+            if(callback){
+                callback(err);
+            }
+        });
+    } else {
+        if(callback) callback(0);
+    }
+}
+
 const sysSchedule = {
     clearJunkData,
-    clearFakeData
+    clearFakeData,
+    checkUserCompilationStatus,
+    sendCompilationStatusSms,
 };
 
 //export {sysSchedule as default}

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

@@ -18,7 +18,6 @@ module.exports = function (app) {
     router.post('/clearFakeData', sysToolsController.auth, sysToolsController.init, sysToolsController.clearFakeData);
 
     router.post('/uploadUserGuide', sysToolsController.auth, sysToolsController.init, sysToolsController.uploadFile);
-    router.post('/changeProductInfo', sysToolsController.auth, sysToolsController.init, sysToolsController.changeProductInfo)
 
     app.use("/sysTools/api", router);
 

+ 15 - 2
modules/users/controllers/compilation_controller.js

@@ -215,7 +215,8 @@ class CompilationController extends BaseController {
 
             // 获取定额库
             let stdRationLibMapModel = new STDRationLibMapModel();
-            rationList = await stdRationLibMapModel.getRationLib(selectedCompilation._id);
+            //rationList = await stdRationLibMapModel.getRationLib(selectedCompilation._id);
+            rationList = await stdRationLibMapModel.getAllRationLibs();
 
             // 获取工料机库
             let stdGLJLibMapModel = new STDGLJLibMapModel();
@@ -234,7 +235,7 @@ class CompilationController extends BaseController {
             calculationList = await stdCalcProgramModel.getProgramList(selectedCompilation._id);
 
             //获取列设置库
-             mainTreeColList = await mainColFacade.getColLibsByCompilationID(selectedCompilation._id);
+            mainTreeColList = await mainColFacade.getColLibsByCompilationID(selectedCompilation._id);
 
              //获取清单模板库
             billTemplateList = await billTemplateFacade.getTemplateLibByCompilationID(selectedCompilation._id);
@@ -692,6 +693,18 @@ class CompilationController extends BaseController {
         }
     }
 
+    async copyRationLibs(req, res) {
+        const { valuationID, engineeringID } = JSON.parse(req.body.data);
+        try {
+            const engineeringLibModel = new EngineeringLibModel();
+            await engineeringLibModel.copyRationLibsToOthers(valuationID, engineeringID);
+            res.json({ error: 0, message: '复制成功', data: null });
+        } catch (err) {
+            res.json({ error: 1, message: String(err), data: null });
+        }
+
+    }
+
     async addEngineer(request,response){
         let engineeringLibModel = new EngineeringLibModel();
         try {

+ 5 - 0
modules/users/controllers/login_controller.js

@@ -121,6 +121,10 @@ class LoginController extends BaseController {
                             let permissionArray = permissionIdList[per];
                             for (let pa of permissionArray) {
                                 let permissionInfo = await permissionModel.findDataByCondition({_id:pa});
+                                if (!permissionInfo) {
+                                    console.log(`pa`);
+                                    console.log(pa);
+                                }
                                 if (permissionInfo.isMenu) {
                                     toolMenuData.push({
                                         title: permissionInfo.name,
@@ -222,6 +226,7 @@ class LoginController extends BaseController {
                 throw {code: 44003, err: '更新登录信息失败!'};
             }
         } catch (error) {
+            console.log(error);
             responseData.error = error.code;
             responseData.msg = error.err;
         }

+ 41 - 9
modules/users/controllers/system_controller.js

@@ -6,30 +6,62 @@ let mongoose = require("mongoose");
 let systemSettingModel = mongoose.model("system_setting");
 const uuidV1 = require('uuid/v1');
 let config = require("../../../config/config.js");
+const company = '珠海纵横创新软件有限公司';
+const product = '大司空云计价';
 
 class SystemController extends BaseController {
-    async  index(request, response){
+    async index(request, response) {
         let setting = await systemSettingModel.findOne({});
-        if(!setting){
-            setting = {professional:{project:100,ration:2000},normal:{project:50,ration:1000}};
+        if (!setting) {
+            setting = { 
+                professional: { project: 100, ration: 2000 },
+                normal: { project: 50, ration: 1000 },
+                company,
+                product,
+                version: ''
+            };
         }
+        if (!setting.company) {
+            setting.company = company;
+        }
+        if (!setting.product) {
+            setting.product = product;
+        }
+
         // 渲染数据
         let renderData = {
             layout: 'users/views/layout/layout',
-            LicenseKey:config.getLicenseKey(process.env.NODE_ENV),
-            setting:setting
+            LicenseKey: config.getLicenseKey(process.env.NODE_ENV),
+            setting: setting,
+            superAdmin: request.session.managerData.superAdmin
         };
         response.render('users/views/system/index', renderData);
     }
 
-    async save(request, response){
+    async save(request, response) {
         let data = request.body;
-        let setting = {professional:{project:data.professional_project,ration:data.professional_ration},normal:{project:data.normal_project,ration:data.normal_ration}};
-        if(!data.ID || data.ID == ""){
+        const superAdmin = request.session.managerData.superAdmin;
+        let setting = {
+            professional: {
+                project: data.professional_project,
+                ration: data.professional_ration
+            },
+            normal: {
+                project: data.normal_project,
+                ration: data.normal_ration
+            }
+        };
+        // 超级管理员才能修改
+        if (superAdmin === 1) {
+            setting.company = data.company;
+            setting.product = data.product;
+            setting.version = data.version;
+        }
+        if (!data.ID || data.ID == "") {
             setting.ID = uuidV1();
             await systemSettingModel.create(setting);
         } else {
-            await systemSettingModel.update({ID:data.ID},setting);
+            await systemSettingModel.update({ ID: data.ID }, setting);
         }
         response.redirect(request.headers.referer);
     }

+ 0 - 3
modules/users/controllers/tool_controller.js

@@ -7,7 +7,6 @@
  */
 import BaseController from "../../common/base/base_controller";
 let config = require("../../../config/config.js");
-let productData = require('../../sys_tools/models/product_model');
 
 class ToolController extends BaseController {
 
@@ -20,11 +19,9 @@ class ToolController extends BaseController {
      */
     async index(request, response) {
         let toolMenuData = request.session.managerData.toolMenuData;
-        let productInfo = await productData.getInfo();
         console.log(toolMenuData);
         let renderData = {
             layout: 'users/views/layout/layout',
-            productInfo: productInfo,
             toolMenu: toolMenuData,
             LicenseKey:config.getLicenseKey(process.env.NODE_ENV)
         };

+ 18 - 0
modules/users/models/engineering_lib_model.js

@@ -289,6 +289,24 @@ class EngineeringLibModel extends BaseModel {
         return result;
     }
 
+    async copyRationLibsToOthers(valuationID, engineeringID) {
+        const compilationModel = new CompilationModel();
+        const compilation = await compilationModel.model.findOne({ $or: [{ 'bill_valuation.id': valuationID }, { 'ration_valuation.id': valuationID }]});
+        if (!compilation) {
+            return;
+        }
+        const valuationIDList = [];
+        const allValuation = compilation.ration_valuation.concat(compilation.bill_valuation);
+        for (const valuation of allValuation) {
+            valuationIDList.push(valuation.id);
+        }
+        const engineering = await this.findDataByCondition({ _id: engineeringID });
+        if (!engineering) {
+            return;
+        }
+        await this.model.updateMany({ valuationID: { $in: valuationIDList } }, {$set: { ration_lib: engineering.ration_lib }});
+    }
+
 }
 
 export default EngineeringLibModel;

+ 225 - 0
modules/users/models/sms.js

@@ -0,0 +1,225 @@
+'use strict';
+
+import Request from "request";
+
+/**
+ * 建筑短信发送相关接口
+ *
+ * @author CaiAoLin
+ * @date 2018/1/25
+ * @version
+ */
+
+const crypto = require('crypto');
+
+class SMS {
+
+    /**
+     * 构造函数
+     *
+     * @return {void}
+     */
+    constructor() {
+        this.url = 'http://www.sendcloud.net/smsapi/send';
+        this.smsUser = 'smartcost';
+        this.smskey = 'kuGmqTt10n6vBXivhxXsAuG8aoCsQ1x6';
+    }
+
+    /**
+     * 发送信息
+     *
+     * @param {String|Array} mobile - 发送的电话号码
+     * @param {String} code - 验证码
+     * @return {Boolean} - 发送结果
+     */
+    async send(mobile, code) {
+        try {
+            const formData = {
+                smsUser: this.smsUser,
+                templateId: 25595,
+                msgType: 0,
+                phone: mobile,
+                vars: '{"%code%":'+ code +'}',
+            };
+            const signature = await this.getSignature(this.sortDict(formData), this.smskey);
+            formData.signature = signature;
+
+            let postData = {
+                url: this.url,
+                form: formData,
+                encoding: 'utf8'
+            };
+
+            return new Promise(function (resolve, reject) {
+                try {
+                    // 请求接口
+                    Request.post(postData, function (err, postResponse, body) {
+                        if (err) {
+                            throw '请求错误';
+                        }
+                        if (postResponse.statusCode !== 200) {
+                            throw '短信发送失败!';
+                        }
+                        resolve(body);
+                    });
+                } catch (error) {
+                    reject([]);
+                }
+            });
+        } catch (error) {
+           console.log(error);
+        }
+    }
+
+    async sendLoginMsg(mobile, name, date, time, local, ip) {
+        console.log(mobile, name, time, local, ip);
+        try {
+            const formData = {
+                smsUser: this.smsUser,
+                templateId: 27561,
+                msgType: 0,
+                phone: mobile,
+                vars: '{"%name%": "' + name + '", "%date%": "' + date + '", "%time%": "' + time + '", "%local%": "' + local + '", "%IP%": "' + ip + '"}',
+            };
+            const signature = await this.getSignature(this.sortDict(formData), this.smskey);
+            formData.signature = signature;
+
+            let postData = {
+                url: this.url,
+                form: formData,
+                encoding: 'utf8'
+            };
+
+            return new Promise(function (resolve, reject) {
+                try {
+                    // 请求接口
+                    Request.post(postData, function (err, postResponse, body) {
+                        if (err) {
+                            throw '请求错误';
+                        }
+                        if (postResponse.statusCode !== 200) {
+                            throw '短信发送失败!';
+                        }
+                        resolve(body);
+                    });
+                } catch (error) {
+                    reject([]);
+                }
+            });
+        } catch (error) {
+            console.log(error);
+        }
+    }
+
+    async sendProductMsg(mobile, status, name, product, deadline) {
+        try {
+            let templateId = 0;
+            switch (status) {
+                case 1: templateId = 746377;break;// 产品升级通知
+                case 2: templateId = 746376;break;// 产品降级通知
+                case 3: templateId = 746378;break;// 产品延期通知
+            }
+            const formData = {
+                smsUser: this.smsUser,
+                templateId: templateId,
+                msgType: 0,
+                phone: mobile,
+            };
+            formData.vars = '{"%name%": "' + name + '", "%product%": "' + product + '"' + (status !== 2 ? ', "%deadline%": "' + deadline + '"' : '') +'}';
+            const signature = await this.getSignature(this.sortDict(formData), this.smskey);
+            formData.signature = signature;
+
+            let postData = {
+                url: this.url,
+                form: formData,
+                encoding: 'utf8'
+            };
+
+            return new Promise(function (resolve, reject) {
+                try {
+                    // 请求接口
+                    Request.post(postData, function (err, postResponse, body) {
+                        if (err) {
+                            throw '请求错误';
+                        }
+                        if (postResponse.statusCode !== 200) {
+                            throw '短信发送失败!';
+                        }
+                        resolve(body);
+                    });
+                } catch (error) {
+                    reject([]);
+                }
+            });
+        } catch (error) {
+            console.log(error);
+        }
+    }
+
+    md5(data) {
+        var str = data;
+        return crypto.createHash("md5").update(str).digest("hex");
+    }
+
+    sortDict(dict){
+        var dict2={},
+            keys = Object.keys(dict).sort();
+        for (var i = 0, n = keys.length, key; i < n; ++i) {
+            key = keys[i];
+            dict2[key] = dict[key];
+        }
+        return dict2;
+    }
+
+    async getSignature (sorted_param, smsKey) {
+        var param_str = "";
+        for(var key in sorted_param)
+            param_str += (key + '=' + sorted_param[key] + '&')
+        var param_str = smsKey + '&' + param_str + smsKey;
+        var sign = this.md5(param_str);
+        return sign.toUpperCase();
+    }
+
+    /**
+     * 生成随机字符串
+     *
+     * @param {Number} length - 需要生成字符串的长度
+     * @param {Number} type - 1为数字和字符 2为纯数字 3为纯字母
+     * @return {String} - 返回生成结果
+     */
+    generateRandomString(length, type = 1) {
+        length = parseInt(length);
+        length = isNaN(length) ? 1 : length;
+        let randSeed = [];
+        let numberSeed = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
+        let stringSeed = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
+            'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
+            'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'];
+
+        switch (type) {
+            case 1:
+                randSeed = stringSeed.concat(numberSeed);
+                stringSeed = numberSeed = null;
+                break;
+            case 2:
+                randSeed = numberSeed;
+                break;
+            case 3:
+                randSeed = stringSeed;
+                break;
+            default:
+                break;
+        }
+
+        const seedLength = randSeed.length - 1;
+        let result = '';
+        for (let i = 0; i < length; i++) {
+            const index = Math.ceil(Math.random() * seedLength);
+            result += randSeed[index];
+        }
+
+        return result;
+    }
+}
+
+module.exports = SMS;

+ 1 - 0
modules/users/routes/compilation_route.js

@@ -31,6 +31,7 @@ module.exports = function (app) {
     router.post('/valuation/:section/enable', compilationController.auth, compilationController.init, compilationController.enableSwitch);
     router.post('/template/:section/:id/:engineering/update', compilationController.auth, compilationController.init, compilationController.updateBillsTemplate);
     router.post('/addEngineer', compilationController.auth, compilationController.init, compilationController.addEngineer);
+    router.post('/copyRationLibs', compilationController.auth, compilationController.init, compilationController.copyRationLibs);
 
     router.post('/changeCategory', compilationController.auth, compilationController.init, compilationController.changeCategory);
 

+ 22 - 1
operation.js

@@ -108,7 +108,28 @@ schedule.scheduleJob({hour: 3, minute: 30, dayOfWeek: 7}, function(){
     })
 });
 
+schedule.scheduleJob({hour: 0, minute: 1}, function(){
+    sysSchedule.checkUserCompilationStatus(function (err) {
+        if(err){
+            console.log('更新失败');
+        }
+        else{
+            console.log('更新成功');
+        }
+    })
+});
+
+schedule.scheduleJob({hour: 10, minute: 0}, function(){
+    sysSchedule.sendCompilationStatusSms(function (err) {
+        if(err){
+            console.log('短信发送失败');
+        }
+        else{
+            console.log('短信发送成功');
+        }
+    })
+});
 
-app.listen(6080, function(){
+app.listen(6080, function () {
     console.log("server started!");
 });

File diff suppressed because it is too large
+ 987 - 865
package-lock.json


+ 3 - 1
package.json

@@ -23,8 +23,10 @@
     "uuid": "^3.1.0"
   },
   "dependencies": {
+    "axios": "^0.19.2",
     "babel-core": "^6.26.0",
     "bluebird": "^3.5.0",
+    "cheerio": "^1.0.0-rc.3",
     "jszip": "^3.1.3",
     "log4js": "~2.3.3",
     "lz-string": "^1.4.4",
@@ -37,6 +39,6 @@
   },
   "scripts": {
     "start": "C:\\Users\\mai\\AppData\\Roaming\\npm\\babel-node.cmd operation.js",
-    "dev_server":"SET NODE_ENV=qa&& babel-node operation.js"
+    "dev_server": "SET NODE_ENV=qa&& babel-node operation.js"
   }
 }

+ 16 - 0
public/constants/price_info_constant.js

@@ -0,0 +1,16 @@
+((factory) => {
+    if (typeof module !== 'undefined' && !module.nodeType) {
+        module.exports = factory();
+    } else {
+        window.PRICE_INFO_CONST = factory();
+    }
+})(() => {
+    return {
+        CRAWL_LOG_KEY: 'OPERATION_CRAWL_DATA',
+        ProcessStatus: {
+            START: 'start',
+            FINISH: 'finish',
+            ERROR: 'error'
+        }
+    };
+});

File diff suppressed because it is too large
+ 70 - 16
public/web/PerfectLoad.js


+ 3 - 4
public/web/common_ajax.js

@@ -119,7 +119,7 @@ var CommonAjax = {
     }
 };
 
-async function ajaxPost(url, data) {
+async function ajaxPost(url, data, timeout = 50000) {
     return new Promise(function (resolve, reject) {
         $.ajax({
             type:"POST",
@@ -127,7 +127,7 @@ async function ajaxPost(url, data) {
             data: {'data': JSON.stringify(data)},
             dataType: 'json',
             cache: false,
-            timeout: 50000,
+            timeout,
             success: function(result){
                 if (result.error === 0 ||result.error ===false) {
                     resolve(result.data);
@@ -145,12 +145,11 @@ async function ajaxPost(url, data) {
     });
 }
 
-
 function ajaxErrorInfo(jqXHR, textStatus, errorThrown) {
     if(textStatus == 'timeout'){
         alert('网络连接超时,请刷新您的网页。');
     }else {
-        alert('url: ' + url +' error ' + textStatus + " " + errorThrown);
+        alert('error ' + textStatus + " " + errorThrown);
     }
 }
 

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

@@ -465,4 +465,13 @@ var sheetCommonObj = {
             sheet.resumePaint();
         }
     },
+    renderSheetFunc: function (sheet, func){
+        sheet.suspendEvent();
+        sheet.suspendPaint();
+        if(func){
+            func();
+        }
+        sheet.resumeEvent();
+        sheet.resumePaint();
+    }
 }

+ 2 - 2
public/web/tools_const.js

@@ -7,8 +7,8 @@
  * @date 2018/8/15
  * @version
  */
-//允许使用的工料机类型:人工、普通材料、混凝土、砂浆、配合比、商品混凝土、商品砂浆、机械台班、机械组成物、机上人工、主材、设备
-let allowGljType = [1, 201, 202, 203, 204, 205, 206, 301, 302, 303, 4, 5];
+//允许使用的工料机类型:人工、普通材料、混凝土、砂浆、配合比、商品混凝土、商品砂浆、其他材料费 、机械台班、机械组成物、机上人工、主材、设备
+let allowGljType = [1, 201, 202, 203, 204, 205, 206, 207, 301, 302, 303, 4, 5];
 
 //允许含有组成物的工料机类型:混凝土、砂浆、配合比、机械台班、主材
 let allowComponent = [202, 203, 204, 301, 4];

+ 4 - 2
public/web/tree_sheet/tree_sheet_controller.js

@@ -3,7 +3,7 @@
  */
 
 var TREE_SHEET_CONTROLLER = {
-    createNew: function (tree, sheet, setting) {
+    createNew: function (tree, sheet, setting, loadHeader = true) {
         var controller = function () {
             this.tree = tree;
             this.sheet = sheet;
@@ -12,7 +12,9 @@ var TREE_SHEET_CONTROLLER = {
                 refreshBaseActn: null,
                 treeSelectedChanged: null
             };
-            TREE_SHEET_HELPER.loadSheetHeader(this.setting, this.sheet);
+            if (loadHeader) {
+                TREE_SHEET_HELPER.loadSheetHeader(this.setting, this.sheet);
+            }
         };
 
         controller.prototype.showTreeData = function () {

+ 1 - 0
web/maintain/bills_lib/html/neirong.html

@@ -229,6 +229,7 @@
         tools.redirect(billsLibId, 'stdBillsmain');
         let userAccount = '<%= userAccount%>'
         let spreadAllJobs = new GC.Spread.Sheets.Workbook($('#spreadAllJobs')[0], {sheetCount: 1});
+        spreadAllJobs.options.allowUserDragFill = false;
         sheetCommonObj.bindEscKey(spreadAllJobs, [{sheet: spreadAllJobs.getSheet(0), editStarting: totalJobsController.onEditStart, editEnded: totalJobsController.onEditEnded}]);
         let orgJobData;
         let maxJobNumer;

+ 38 - 0
web/maintain/bills_lib/html/qingdan.html

@@ -380,8 +380,10 @@
     tools.redirect(billsLibId, 'stdBillsmain');
     let billsSpread;
     let jobsSpread = new GC.Spread.Sheets.Workbook($("#spreadJobs")[0], {sheetCount: 1});
+    jobsSpread.options.allowUserDragFill = false;
     sheetCommonObj.bindEscKey(jobsSpread, [{sheet: jobsSpread.getSheet(0), editStarting: jobsController.onEditStart, editEnded: jobsController.onEditEnded}]);
     let itemsSpread = new GC.Spread.Sheets.Workbook($("#spreadItems")[0], {sheetCount: 1});
+    itemsSpread.options.allowUserDragFill = false;
     sheetCommonObj.bindEscKey(itemsSpread, [{sheet: itemsSpread.getSheet(0), editStarting: itemsController.onEditStart, editEnded: itemsController.onEditEnded}]);
     $(document).ready(function(){
         $("#aStdJobs").attr('href', function(){
@@ -488,6 +490,7 @@
         controller.setTreeSelected(controller.tree.findNode(controller.sheet.getTag(0, 0)));
         //粘贴事件
         bindPasteBills(controller, billsSpread.getActiveSheet(), setting);
+        bindBillsRangeChanged(controller, billsSpread.getActiveSheet(), setting);
         //补注内容改变
         rechargeChange(controller);
         //焦点控制
@@ -822,6 +825,41 @@
             sheet.setRowCount(sheetBillsDatas.datas.length);
         });
     }
+
+    function bindBillsRangeChanged(controller, sheet, setting) {
+        sheet.bind(GC.Spread.Sheets.Events.RangeChanged, function (sender, info) {
+            const postData = [];
+            let curRow;
+            let curData;
+            info.changedCells.forEach(({ row, col }) => {
+                const node = controller.tree.items[row];
+                if (curRow !== row) {
+                    curData = { ID: node.data.ID };
+                    postData.push(curData);
+                }
+                curRow = row;
+                const field = setting.cols[col].data.field;
+                const value = sheet.getValue(row, col);
+                curData[field] = value;
+            });
+            billsAjax.pasteBills(userAccount, billsLibId, postData, function () {
+                info.changedCells.forEach(({ row, col }) => {
+                    const node = controller.tree.items[row];
+                    const field = setting.cols[col].data.field;
+                    const value = sheet.getValue(row, col);
+                    node.data[field] = value;
+                    sheetBillsDatas.datasIdx['rowIdx' + row][field] = value;
+                });
+            }, function () {
+                info.changedCells.forEach(({ row, col }) => {
+                    const node = controller.tree.items[row];
+                    const field = setting.cols[col].data.field;
+                    sheet.setValue(row, col, node.data[field] || '');
+                });
+            });
+        });
+    }
+
     function bindPasteRel(sheet, controller, totalJobs, setting){
         //sheetDatas = tools.getsheetDatas(sheet, 'jobs');
         sheet.bind(GC.Spread.Sheets.Events.ClipboardPasting, function (sender, args) {

+ 2 - 0
web/maintain/bills_lib/html/tezheng.html

@@ -227,8 +227,10 @@
     <SCRIPT type="text/javascript">
         const locked = lockUtil.getLocked();
         let spread = new GC.Spread.Sheets.Workbook($('#spreadAllItems')[0], {sheetCount: 1});
+        spread.options.allowUserDragFill = false;
         sheetCommonObj.bindEscKey(spread, [{sheet: spread.getSheet(0), editStarting: totalItemsController.onEditStart, editEnded: totalItemsController.onEditEnded}]);
         let spreadVal = new GC.Spread.Sheets.Workbook($('#spreadEigenvalue')[0], {sheetCount: 1});
+        spreadVal.options.allowUserDragFill = false;
         sheetCommonObj.bindEscKey(spreadVal, [{sheet: spreadVal.getSheet(0), editStarting: valueController.onEditStart, editEnded: valueController.onEditEnded}])
         let billsLibId = getQueryString('billsLibId');
         tools.redirect(billsLibId, 'stdBillsmain');

+ 13 - 2
web/maintain/bills_lib/scripts/bills_lib_ajax.js

@@ -370,14 +370,25 @@ var billsAjax = {
             }
         });
     },
-    pasteBills: function(lastOperator, billsLibId, datas){
+    pasteBills: function(lastOperator, billsLibId, datas, successCallback, errorCallback){
         $.ajax({
             type: 'post',
             url: 'stdBillsEditor/pasteBills',
             data: {data: JSON.stringify({lastOperator: lastOperator, billsLibId: billsLibId, datas: datas})},
             dataType: 'json',
             success: function(result){
-
+                if (!result.error && successCallback) {
+                    successCallback();
+                } else if (result.error && errorCallback) {
+                    alert(result.message);
+                    errorCallback();
+                }
+            },
+            error: function () {
+                alert('服务器出现错误,请稍后再试。')
+                if (errorCallback) {
+                    errorCallback();
+                }
             }
         });
     },

+ 144 - 54
web/maintain/common/css/main.css

@@ -1,15 +1,21 @@
 /*building SAAS 0.1*/
+
 /*bootstrap 初始化*/
+
 body {
     font-size: 0.9rem
 }
+
 .dropdown-menu {
     font-size: 0.9rem
 }
+
 /*自定义css*/
+
 .header {
     background: #e1e1e1
 }
+
 .header .header-logo {
     background: #ff6501;
     color: #fff;
@@ -20,28 +26,50 @@ body {
     font-size: 1.25rem;
     line-height: inherit
 }
-.top-msg{
+
+.top-msg {
     position: fixed;
-    top:0;
-    width:100%;
+    top: 0;
+    width: 100%;
     z-index: 999
 }
-.in-1{padding-left:0rem!important}
-.in-2{padding-left:1rem!important}
-.in-3{padding-left:1.5rem!important}
-.in-4{padding-left:2rem!important}
-.in-5{padding-left:2.5rem!important}
-.in-6{padding-left:3rem!important}
+
+.in-1 {
+    padding-left: 0rem!important
+}
+
+.in-2 {
+    padding-left: 1rem!important
+}
+
+.in-3 {
+    padding-left: 1.5rem!important
+}
+
+.in-4 {
+    padding-left: 2rem!important
+}
+
+.in-5 {
+    padding-left: 2.5rem!important
+}
+
+.in-6 {
+    padding-left: 3rem!important
+}
+
 .main {
     position: relative;
     background: #f7f7f9;
 }
+
 .main-nav {
     position: absolute;
     text-align: center;
     z-index: 999;
     padding: 2px 0 0 2px
 }
+
 .main-nav .nav a {
     display: block;
     width: 28px;
@@ -51,81 +79,101 @@ body {
     padding: 10px 0;
     border-right: 1px solid #ccc;
 }
+
 .main-nav .nav a:hover {
     background: #fff;
     color: #333;
     text-decoration: none;
 }
+
 .main-nav .nav a.active {
     border: 1px solid #ccc;
     border-right: 1px solid #fff;
     background: #fff;
     color: #333
 }
+
 .content {
     background: #fff
 }
+
 .tools-btn {
     height: 30px;
     line-height: 30px;
 }
+
 .toolsbar .tools-btn.btn:hover {
     background: #f7f7f9;
 }
+
 .main-side {
     border-right: 1px solid #ccc;
     border-left: 1px solid #ccc;
-    overflow:hidden;
+    overflow: hidden;
 }
+
 .main-side .tab-bar {
-    padding:5px 10px;
-    height:38px;
-    position:fixed;
+    padding: 5px 10px;
+    height: 38px;
+    position: fixed;
 }
+
 .main-side .tab-content {
     margin-top: 38px
 }
+
 .top-content, .fluid-content {
     overflow: hidden;
     border-bottom: 1px solid #ccc;
 }
+
 .warp-p2 {
     padding: 2px
 }
-.bottom-content .nav,.top-content .nav {
+
+.bottom-content .nav, .top-content .nav {
     background: #f7f7f9;
-    padding:0 0 0 2px
+    padding: 0 0 0 2px
 }
-.bottom-content .nav-tabs .nav-link, .side-tabs .nav-tabs .nav-link,.top-content .nav-tabs .nav-link {
+
+.bottom-content .nav-tabs .nav-link, .side-tabs .nav-tabs .nav-link, .top-content .nav-tabs .nav-link {
     border-radius: 0;
     padding: 0.2em 0.5em
 }
+
 .side-tabs .nav-tabs .nav-item {
     z-index: 999
 }
+
 .side-tabs .nav-tabs {
     border-bottom: none;
     margin-bottom: -1px
 }
+
 .side-tabs .nav-tabs .nav-link {
     border-radius: 0;
     padding: 0em 0.5em;
     line-height: 30px;
     z-index: 999
 }
+
 .bottom-content .nav-tabs .nav-link.active {
     border-top: 1px solid #f7f7f9
 }
+
 .side-tabs .nav-tabs .nav-link.active {
     border-top: none;
-    border-bottom:1px solid #fff
+    border-bottom: 1px solid #fff
 }
+
 .side-tabs a.active, .sub-nav a.active {
     background: #ccc
 }
+
 .poj-manage {
     background: #fff
 }
+
 .slide-sidebar {
     border-left: 1px solid #E1E1E1;
     box-shadow: 0px 15px 15px rgba(0, 0, 0, 0.1);
@@ -137,10 +185,12 @@ body {
     z-index: 999;
     width: 0px;
 }
+
 .new-msg {
     -webkit-animation: tada 1s infinite .2s ease both;
     -moz-animation: tada 1s infinite .2s ease both;
 }
+
 @-webkit-keyframes tada {
     0% {
         -webkit-transform: scale(1)
@@ -158,6 +208,7 @@ body {
         -webkit-transform: scale(1) rotate(0)
     }
 }
+
 @-moz-keyframes tada {
     0% {
         -moz-transform: scale(1)
@@ -175,11 +226,13 @@ body {
         -moz-transform: scale(1) rotate(0)
     }
 }
+
 .has-danger {
     -webkit-animation: shake 1s .2s ease both;
     -moz-animation: shake 1s .2s ease both;
     animation: shake 1s .2s ease both;
 }
+
 @-webkit-keyframes shake {
     0%, 100% {
         -webkit-transform: translateX(0);
@@ -191,6 +244,7 @@ body {
         -webkit-transform: translateX(10px);
     }
 }
+
 @-moz-keyframes shake {
     0%, 100% {
         -moz-transform: translateX(0);
@@ -202,6 +256,7 @@ body {
         -moz-transform: translateX(10px);
     }
 }
+
 @keyframes shake {
     0%, 100% {
         transform: translateX(0);
@@ -213,89 +268,109 @@ body {
         transform: translateX(10px);
     }
 }
+
 .bottom-content {
     height: 370px;
     overflow: hidden;
 }
-.bottom-content .tab-content .main-data-bottom{
+
+.bottom-content .tab-content .main-data-bottom {
     height: 340px;
     overflow: auto;
 }
+
 .form-signin {
     max-width: 500px;
     margin: 150px auto;
 }
+
 .poj-list, .side-content {
     overflow: auto;
 }
+
 .poj-list span.poj-icon {
-    padding-right:10px;
-    color:#ccc
+    padding-right: 10px;
+    color: #ccc
 }
-.print-toolsbar{
-    padding:5px
+
+.print-toolsbar {
+    padding: 5px
 }
+
 .print-toolsbar .panel {
-    display:inline-block;
-    vertical-align:top;
-    background:#f7f7f9
+    display: inline-block;
+    vertical-align: top;
+    background: #f7f7f9
 }
-.print-toolsbar .panel .panel-foot{
+
+.print-toolsbar .panel .panel-foot {
     text-align: center;
     font-size: 12px
 }
+
 .print-list {
-    border-right:1px solid #ccc
+    border-right: 1px solid #ccc
 }
+
 .print-list .form-list {
     overflow: auto
 }
-.print-list .list-tools{
-    height:50px;
-    padding:10px 0;
-    border-bottom:1px solid #f2f2f2
+
+.print-list .list-tools {
+    height: 50px;
+    padding: 10px 0;
+    border-bottom: 1px solid #f2f2f2
 }
+
 .pageContainer {
     background: #ededed;
     text-align: center
 }
-.pageContainer .page{
-    border:9px solid transparent;
+
+.pageContainer .page {
+    border: 9px solid transparent;
     display: inline-block;
 }
-.pageContainer .page img{
-    width:inherit;
+
+.pageContainer .page img {
+    width: inherit;
     height: inherit;
 }
-.codeList{
+
+.codeList {
     max-height: 200px;
-    overflow:auto;
+    overflow: auto;
 }
-.main-data-top,.main-data-bottom,.main-data,.main-side,.main-data-side-q{
+
+.main-data-top, .main-data-bottom, .main-data, .main-side, .main-data-side-q {
     overflow: hidden;
 }
+
 .modal-fixed-height {
-    height:400px;
-    overflow-y:auto;
+    height: 400px;
+    overflow-y: auto;
 }
+
 .modal-fixed-height2 {
-    height:368px;
-    overflow-y:auto;
+    height: 368px;
+    overflow-y: auto;
 }
+
 .btn.disabled, .btn:disabled {
     cursor: not-allowed;
     opacity: .65;
-    color:#666
+    color: #666
 }
+
 .modal-lgx {
     max-width: 1022px
 }
 
-.second_header{
+.second_header {
     background: #e1e1e1;
 }
 
-.input-group-addon{
+.input-group-addon {
     padding: 6px 12px;
     font-size: 14px;
     font-weight: 400;
@@ -307,36 +382,41 @@ body {
     border-radius: 4px;
 }
 
-.input-sm{
+.input-sm {
     height: 30px;
     padding: 5px 10px;
     font-size: 12px;
     line-height: 1.5;
     border-radius: 3px;
 }
-.btn-default{
+
+.btn-default {
     color: #333;
     background-color: #fff;
     border-color: #ccc;
 }
-.checkbox{
+
+.checkbox {
     position: relative;
     display: block;
     margin-top: 10px;
     margin-bottom: 10px;
 }
-input[type=checkbox]{
+
+input[type=checkbox] {
     position: absolute;
     margin-top: 5px;
     margin-left: -20px;
 }
-.col-md-2{
+
+.col-md-2 {
     position: relative;
     min-height: 1px;
     padding-right: 15px;
-    padding-left:15px;
-    width:16.66666667%
+    padding-left: 15px;
+    width: 16.66666667%
 }
+
 .checkbox label, .radio label {
     min-height: 20px;
     padding-left: 20px;
@@ -346,7 +426,7 @@ input[type=checkbox]{
     width: 200px;
 }
 
-.close{
+.close {
     float: right;
     font-size: 21px;
     font-weight: 700;
@@ -360,5 +440,15 @@ input[type=checkbox]{
 .disabled {
     pointer-events: none;
     opacity: .65;
-    color:#666;
+    color: #666;
+}
+
+.split {
+    padding: .5rem .75rem;
+    font-size: 1rem;
+    line-height: 1.25;
+}
+
+.tips {
+    font-size: .5rem;
 }

+ 3 - 0
web/maintain/common/html/layout.html

@@ -23,6 +23,9 @@
             <li class="nav-item">
                 <a class="nav-link" href="javascript:void(0);" aria-haspopup="true" aria-expanded="false" data-toggle="modal" data-target="#add">新建<%= title%></a>
             </li>
+            <% if (typeof listItem !=='undefined') { %>
+                <%- listItem %>
+            <% } %>
         </ul>
     </nav>
 </div>

+ 112 - 0
web/maintain/price_info_lib/css/index.css

@@ -0,0 +1,112 @@
+html {
+    height: 100%;
+}
+
+body {
+    height: 100%;
+    font-size: 0.9rem;
+}
+
+.dropdown-menu {
+    font-size: 0.9rem
+}
+
+/*自定义css*/
+
+.header {
+    position: relative;
+    background: #e1e1e1
+}
+
+.header .header-logo {
+    background: #ff6501;
+    color: #fff;
+    float: left;
+    padding-top: .25rem;
+    padding-bottom: .25rem;
+    margin-right: 1rem;
+    font-size: 1.25rem;
+    line-height: inherit
+}
+
+.top-msg {
+    position: fixed;
+    top: 0;
+    width: 100%;
+    z-index: 999
+}
+
+.in-1 {
+    padding-left: 0rem!important
+}
+
+.in-2 {
+    padding-left: 1rem!important
+}
+
+.in-3 {
+    padding-left: 1.5rem!important
+}
+
+.in-4 {
+    padding-left: 2rem!important
+}
+
+.in-5 {
+    padding-left: 2.5rem!important
+}
+
+.in-6 {
+    padding-left: 3rem!important
+}
+
+.disabled {
+    pointer-events: none;
+    opacity: .65;
+    color: #666;
+}
+
+.wrapper {
+    position: absolute;
+    top: 38px;
+    bottom: 0;
+    width: 100%;
+}
+
+.main {
+    height: 100%;
+    width: 100%;
+}
+
+.main .left {
+    float: left;
+    width: 40%;
+    height: 100%;
+}
+
+.main .left .top {
+    height: 40%;
+}
+
+.main .left .bottom {
+    position: relative;
+    height: 60%;
+}
+
+.main .left .bottom .tab-bar {
+    padding: 5px 10px;
+    height: 38px;
+}
+
+.main .left .bottom .spread {
+    position: absolute;
+    top: 38px;
+    bottom: 0;
+    width: 100%;
+}
+
+.main .right {
+    float: left;
+    width: 59.9%;
+    height: 100%;
+}

+ 85 - 0
web/maintain/price_info_lib/html/edit.html

@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+    <meta http-equiv="x-ua-compatible" content="ie=edge">
+    <title>材料信息价库编辑器</title>
+    <link rel="stylesheet" href="/lib/bootstrap/css/bootstrap.min.css">
+    <link rel="stylesheet" href="/web/maintain/price_info_lib/css/index.css">
+    <link rel="stylesheet" href="/lib/font-awesome/font-awesome.min.css">
+    <link rel="stylesheet" href="/lib/spreadjs/sheets/css/gc.spread.sheets.sc.css" type="text/css">
+    <link rel="stylesheet" href="/lib/jquery-contextmenu/jquery.contextMenu.css" type="text/css">
+</head>
+
+<body>
+    <div class="header">
+        <nav class="navbar navbar-toggleable-lg navbar-light bg-faded p-0 ">
+            <span class="header-logo px-2">Smartcost</span>
+            <div class="navbar-text"><a href="/priceInfo/main">信息价库</a><i
+                    class="fa fa-angle-right fa-fw"></i><%= libName  %></div>
+        </nav>
+    </div>
+    <div class="wrapper">
+        <div class="main">
+            <div class="left">
+                <div class="top" id="area-spread"></div>
+                <div class="bottom">
+                    <div class="tab-bar">
+                        <a href="javascript:void(0);" id="tree-insert" class="btn btn-sm lock-btn-control disabled"
+                            data-toggle="tooltip" data-placement="bottom" title="" data-original-title="插入"><i
+                                class="fa fa-plus" aria-hidden="true"></i></a>
+                        <a href="javascript:void(0);" id="tree-remove" class="btn btn-sm lock-btn-control disabled"
+                            data-toggle="tooltip" data-placement="bottom" title="" data-original-title="删除"><i
+                                class="fa fa-remove" aria-hidden="true"></i></a>
+                        <a href="javascript:void(0);" id="tree-up-level" class="btn btn-sm lock-btn-control disabled"
+                            data-toggle="tooltip" data-placement="bottom" title="" data-original-title="升级"><i
+                                class="fa fa-arrow-left" aria-hidden="true"></i></a>
+                        <a href="javascript:void(0);" id="tree-down-level" class="btn btn-sm lock-btn-control disabled"
+                            data-toggle="tooltip" data-placement="bottom" title="" data-original-title="降级"><i
+                                class="fa fa-arrow-right" aria-hidden="true"></i></a>
+                        <a href="javascript:void(0);" id="tree-down-move" class="btn btn-sm lock-btn-control disabled"
+                            data-toggle="tooltip" data-placement="bottom" title="" data-original-title="下移"><i
+                                class="fa fa-arrow-down" aria-hidden="true"></i></a>
+                        <a href="javascript:void(0);" id="tree-up-move" class="btn btn-sm lock-btn-control disabled"
+                            data-toggle="tooltip" data-placement="bottom" title="" data-original-title="上移"><i
+                                class="fa fa-arrow-up" aria-hidden="true"></i></a>
+                    </div>
+                    <div class="spread" id="class-spread"></div>
+                </div>
+            </div>
+            <div class="right">
+                <div id="price-spread" style="width: 100%; height: 100%"></div>
+            </div>
+        </div>
+    </div>
+    <!-- JS. -->
+    <script src="/lib/jquery/jquery.min.js"></script>
+    <script src="/lib/jquery-contextmenu/jquery.contextMenu.min.js"></script>
+    <script src="/lib/jquery-contextmenu/jquery.ui.position.js"></script>
+    <script src="/lib/tether/tether.min.js"></script>
+    <script src="/lib/bootstrap/bootstrap.min.js"></script>
+    <script src="/public/web/PerfectLoad.js"></script>
+    <script src="/lib/spreadjs/sheets/gc.spread.sheets.all.11.1.2.min.js"></script>
+    <script>GC.Spread.Sheets.LicenseKey = '<%- LicenseKey %>';</script>
+    <script src="/public/web/uuid.js"></script>
+    <script src="/lib/lodash/lodash.js"></script>
+    <script src="/public/web/scMathUtil.js"></script>
+    <script src="/public/web/treeDataHelper.js"></script>
+    <script src="/public/web/common_ajax.js"></script>
+    <script src="/public/web/lock_util.js"></script>
+    <script src="/public/web/id_tree.js"></script>
+    <script src="/public/web/tools_const.js"></script>
+    <script src="/public/web/tree_sheet/tree_sheet_controller.js"></script>
+    <script src="/public/web/tree_sheet/tree_sheet_helper.js"></script>
+    <script src="/public/web/sheet/sheet_common.js"></script>
+    <script>
+        const areaList = JSON.parse('<%- areaList %>');
+        const compilationID = '<%- compilationID %>';
+        const curLibPeriod = '<%- period %>';
+    </script>
+    <script src="/web/maintain/price_info_lib/js/index.js"></script>
+</body>
+
+</html>

+ 228 - 0
web/maintain/price_info_lib/html/main.html

@@ -0,0 +1,228 @@
+<div class="main">
+    <div class="content">
+        <div class="container-fluid">
+            <div class="row">
+                <div class="col-md-2">
+                    <div class="list-group mt-3">
+                        <% for (let compilation of compilationList) { %>
+                        <% if (compilation._id === 'all') { %>
+                        <a href="/priceInfo/main"
+                            class="list-group-item list-group-item-action <%= compilation.active %>">
+                            所有
+                        </a>
+                        <% } else { %>
+                        <a id="<%= compilation._id %>" href="/priceInfo/main?filter=<%= compilation._id %>"
+                            class="list-group-item list-group-item-action <%= compilation.active %>">
+                            <%= compilation.name %>
+                        </a>
+                        <% }} %>
+                    </div>
+                </div>
+                <div class="col-md-10">
+                    <div class="warp-p2 mt-3">
+                        <table class="table table-hover table-bordered">
+                            <thead>
+                                <tr>
+                                    <th>材料信息价库名称</th>
+                                    <th width="160">期数</th>
+                                    <th width="160">费用定额</th>
+                                    <th width="160">添加时间</th>
+                                    <th width="70">操作</th>
+                                    <th width="70">原始数据</th>
+                                </tr>
+                            </thead>
+                            <tbody id="showArea">
+                                <% for(let lib of libs){ %>
+                                <tr class="libTr">
+                                    <td id="<%= lib.ID%>"><a
+                                            href="/priceInfo/edit/?libID=<%= lib.ID%>&locked=true"><%= lib.name%></a>
+                                    </td>
+                                    <td><%= lib.period %></td>
+                                    <td><%= lib.compilationName%></td>
+                                    <td><%= moment(lib.createDate).format('YYYY-MM-DD')%></td>
+                                    <td>
+                                        <a class="lock-btn-control disabled" href="javascript:void(0);"
+                                            style="color: #0275d8" onclick='handleEditClick("<%= lib.ID%>")'
+                                            title="编辑"><i class="fa fa-pencil-square-o"></i></a>
+                                        <a class="text-danger lock-btn-control disabled" href="javascript:void(0);"
+                                            onclick='handleDeleteClick("<%= lib.ID%>")' title="删除"><i
+                                                class="fa fa-remove"></i></a>
+                                        <a class="lock" data-locked="true" href="javascript:void(0);" title="解锁"><i
+                                                class="fa fa-unlock-alt"></i></a>
+                                    </td>
+                                    <td>
+                                        <a class="btn btn-secondary btn-sm import-data lock-btn-control disabled"
+                                            onclick='handleImportClick("<%= lib.ID%>")' href="javacript:void(0);"
+                                            title="导入数据"><i class="fa fa-sign-in fa-rotate-90"></i>导入</a>
+                                    </td>
+                                </tr>
+                                <% } %>
+                            </tbody>
+                        </table>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+
+<!--弹出添加-->
+<div class="modal fade" id="add" data-backdrop="static" style="display: none;" aria-hidden="true">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">添加材料信息价库</h5>
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">×</span>
+                </button>
+            </div>
+            <div class="modal-body">
+                <form id="add-lib-form" method="post" action="/priceInfo/addLib"
+                    enctype="application/x-www-form-urlencoded">
+                    <div class="form-group">
+                        <label>材料信息价库名称</label>
+                        <input id="name" name="name" class="form-control" placeholder="请输入库名称" type="text">
+                        <small class="form-text text-danger" id="name-error" style="display: none">请输入库名称。</small>
+                    </div>
+                    <div class="form-group">
+                        <label>编办名称</label>
+                        <select class="form-control" name="compilationID">
+                            <% for (let compilation of compilationList) { %>
+                            <% if (compilation.name !== '所有') { %>
+                            <option value="<%= compilation._id %>"><%= compilation.name %></option>
+                            <% }} %>
+                        </select>
+                    </div>
+                    <div class="form-group">
+                        <label>期数</label>
+                        <input id="period" name="period" class="form-control" placeholder="请输入期数" type="text">
+                        <small class="form-text text-danger" id="period-error" style="display: none">请输入有效期数(eg:
+                            2020-01)。</small>
+                    </div>
+                    <input type="hidden" name="userAccount" value="<%= userAccount%>">
+                </form>
+            </div>
+            <div class="modal-footer">
+                <button id="add-lib" class="btn btn-primary">新建</button>
+                <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
+            </div>
+        </div>
+    </div>
+</div>
+
+<!--弹出编辑-->
+<div class="modal fade" id="edit" data-backdrop="static" style="display: none;" aria-hidden="true">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">材料信息价库</h5>
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">×</span>
+                </button>
+            </div>
+            <div class="modal-body">
+                <form>
+                    <div class="form-group">
+                        <label>材料信息价库名称</label>
+                        <input id="rename-text" class="form-control" placeholder="输入名称" type="text" value="">
+                        <small class="form-text text-danger" id="rename-error" style="display: none">请输入名称。</small>
+                    </div>
+                </form>
+            </div>
+            <div class="modal-footer">
+                <a id="rename" href="javascript: void(0);" class="btn btn-primary">确定</a>
+                <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
+            </div>
+        </div>
+    </div>
+</div>
+
+<!--弹出导入-->
+<div class="modal fade" id="crawl" data-backdrop="static" style="display: none;" aria-hidden="true">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">材料信息价库</h5>
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">×</span>
+                </button>
+            </div>
+            <div class="modal-body">
+                <div class="row">
+                    <div class="col-5 form-group">
+                        <input id="period-start" class="form-control" name="from" placeholder="起始期数" type="text"
+                            value="" autocomplete="off">
+                    </div>
+                    <div class="col-2 form-group">
+                        <label class="split">——</label>
+                    </div>
+                    <div class="col-5 form-group">
+                        <input id="period-end" class="form-control" name="to" placeholder="结束期数" type="text" value=""
+                            autocomplete="off">
+                    </div>
+                </div>
+                <small class="form-text text-danger" id="crawl-error" style="display: none">请输入有效期数。</small>
+                <small>期数格式:2020-01</label>
+            </div>
+            <div class="modal-footer">
+                <a id="crawl-confirm" href="javascript: void(0);" class="btn btn-primary">确定</a>
+                <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
+            </div>
+        </div>
+    </div>
+</div>
+
+<!--弹出删除-->
+<div class="modal fade" id="del" data-backdrop="static" style="display: none;" aria-hidden="true">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">删除确认</h5>
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">×</span>
+                </button>
+            </div>
+            <div class="modal-body">
+                <h5 class="text-danger">删除后无法恢复,确认是否删除?(需确认三次)</h5>
+            </div>
+            <div class="modal-footer">
+                <a id="delete" href="javascript:void(0);" class="btn btn-danger">确认</a>
+                <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
+            </div>
+        </div>
+    </div>
+</div>
+
+<!--弹出导入数据-->
+<div class="modal fade" id="import" data-backdrop="static" style="display: none;" aria-hidden="true">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">导入数据</h5>
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">×</span>
+                </button>
+            </div>
+            <div class="modal-body">
+                <div class="alert alert-warning" role="alert">
+                    导入操作会覆盖数据,请谨慎操作!!
+                </div>
+                <form>
+                    <div class="form-group">
+                        <label>请选择Excel格式文件</label>
+                        <input class="form-control-file" type="file" accept=".xlsx,.xls" name="import_data" />
+                    </div>
+                </form>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-primary" id="import-confirm">确定导入</button>
+                <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
+            </div>
+        </div>
+    </div>
+</div>
+
+<script src="/public/web/lock_util.js"></script>
+<script src="/public/web/PerfectLoad.js"></script>
+<script src="/public/constants/price_info_constant.js"></script>
+<script src="/web/maintain/price_info_lib/js/main.js"></script>

+ 794 - 0
web/maintain/price_info_lib/js/index.js

@@ -0,0 +1,794 @@
+
+function setAlign(sheet, headers) {
+    const fuc = () => {
+        headers.forEach(({ hAlign, vAlign }, index) => {
+            sheetCommonObj.setAreaAlign(sheet.getRange(-1, index, -1, 1), hAlign, vAlign)
+        });
+    };
+    sheetCommonObj.renderSheetFunc(sheet, fuc);
+}
+
+function initSheet(dom, setting) {
+    const workBook = sheetCommonObj.buildSheet(dom, setting);
+    const sheet = workBook.getSheet(0);
+    setAlign(sheet, setting.header);
+    return workBook;
+}
+
+function showData(sheet, data, headers, emptyRows) {
+    const fuc = () => {
+        sheet.setRowCount(data.length);
+        data.forEach((item, row) => {
+            headers.forEach(({ dataCode }, col) => {
+                sheet.setValue(row, col, item[dataCode] || '');
+            });
+        });
+        if (emptyRows) {
+            sheet.addRows(data.length, emptyRows);
+        }
+    };
+    sheetCommonObj.renderSheetFunc(sheet, fuc);
+}
+
+const TIME_OUT = 10000;
+const libID = window.location.search.match(/libID=([^&]+)/)[1];
+
+const UpdateType = {
+    UPDATE: 'update',
+    DELETE: 'delete',
+    CREATE: 'create',
+};
+
+const DEBOUNCE_TIME = 200;
+
+const locked = lockUtil.getLocked();
+
+// 地区表
+const AREA_BOOK = (() => {
+    const cache = areaList;
+    const setting = {
+        header: [{ headerName: '地区', headerWidth: $('#area-spread').width(), dataCode: 'name', dataType: 'String', hAlign: 'center', vAlign: 'center' }]
+    };
+    // 初始化表格
+    const workBook = initSheet($('#area-spread')[0], setting);
+    lockUtil.lockSpreads([workBook], locked);
+    workBook.options.allowExtendPasteRange = false;
+    workBook.options.allowUserDragDrop = true;
+    workBook.options.allowUserDragFill = true;
+    const sheet = workBook.getSheet(0);
+
+    // 显示数据
+    showData(sheet, cache, setting.header);
+
+    // 编辑处理
+    async function handleEdit(changedCells) {
+        const updateData = [];
+        changedCells.forEach(({ row, col }) => {
+            updateData.push({
+                row,
+                ID: cache[row].ID,
+                name: sheet.getValue(row, col)
+            });
+        });
+        try {
+            await ajaxPost('/priceInfo/editArea', { updateData }, TIME_OUT);
+            updateData.forEach(({ row, name }) => cache[row].name = name);
+        } catch (err) {
+            // 恢复各单元格数据
+            sheetCommonObj.renderSheetFunc(sheet, () => {
+                changedCells.forEach(({ row }) => {
+                    sheet.setValue(cache[row].name);
+                });
+            });
+        }
+    }
+    sheet.bind(GC.Spread.Sheets.Events.ValueChanged, function (e, info) {
+        const changedCells = [{ row: info.row, col: info.col }];
+        handleEdit(changedCells);
+    });
+    sheet.bind(GC.Spread.Sheets.Events.RangeChanged, function (e, info) {
+        handleEdit(info.changedCells);
+    });
+
+    const curArea = { ID: null };
+    // 焦点变更处理
+    const debounceSelectionChanged = _.debounce(function (e, info) {
+        const row = info.newSelections && info.newSelections[0] ? info.newSelections[0].row : 0;
+        handleSelectionChanged(row);
+    }, DEBOUNCE_TIME, { leading: true }); // leading = true : 先触发再延迟
+    function handleSelectionChanged(row) {
+        const areaItem = cache[row];
+        curArea.ID = areaItem && areaItem.ID || null;
+        CLASS_BOOK.initData(libID, curArea.ID);
+    }
+    sheet.bind(GC.Spread.Sheets.Events.SelectionChanged, debounceSelectionChanged);
+
+    // 新增
+    async function insert() {
+        const data = {
+            compilationID,
+            ID: uuid.v1(),
+            name: '',
+        };
+        try {
+            $.bootstrapLoading.start();
+            await ajaxPost('/priceInfo/insertArea', { insertData: [data] });
+            // 新增的数据总是添加在最后
+            sheet.addRows(cache.length, 1);
+            cache.push(data);
+            const lastRow = cache.length - 1;
+            sheet.setSelection(lastRow, 0, 1, 1);
+            sheet.showRow(lastRow, GC.Spread.Sheets.VerticalPosition.top);
+            handleSelectionChanged(lastRow);
+        } catch (err) {
+            alert(err);
+        } finally {
+            $.bootstrapLoading.end();
+        }
+    }
+
+    // 删除
+    async function del() {
+        try {
+            $.bootstrapLoading.start();
+            await ajaxPost('/priceInfo/deleteArea', { deleteData: [curArea.ID] });
+            const index = cache.findIndex(item => item.ID === curArea.ID);
+            sheet.deleteRows(index, 1);
+            cache.splice(index, 1);
+            const row = sheet.getActiveRowIndex();
+            handleSelectionChanged(row);
+        } catch (err) {
+            alert(err);
+        } finally {
+            $.bootstrapLoading.end();
+        }
+    }
+
+    // 右键功能
+    function buildContextMenu() {
+        $.contextMenu({
+            selector: '#area-spread',
+            build: function ($triggerElement, e) {
+                // 控制允许右键菜单在哪个位置出现
+                const offset = $('#area-spread').offset();
+                const x = e.pageX - offset.left;
+                const y = e.pageY - offset.top;
+                const target = sheet.hitTest(x, y);
+                if (target.hitTestType === 3) { // 在表格内
+                    const sel = sheet.getSelections()[0];
+                    if (sel && sel.rowCount === 1 && typeof target.row !== 'undefined') {
+                        const orgRow = sheet.getActiveRowIndex();
+                        if (orgRow !== target.row) {
+                            sheet.setActiveCell(target.row, target.col);
+                            handleSelectionChanged(target.row);
+                        }
+                    }
+                    return {
+                        items: {
+                            insert: {
+                                name: '新增',
+                                icon: "fa-arrow-left",
+                                disabled: function () {
+                                    return locked;
+                                },
+                                callback: function (key, opt) {
+                                    insert();
+                                }
+                            },
+                            del: {
+                                name: '删除',
+                                icon: "fa-arrow-left",
+                                disabled: function () {
+                                    return locked || !cache[target.row];
+                                },
+                                callback: function (key, opt) {
+                                    del();
+                                }
+                            },
+                        }
+                    };
+                }
+                else {
+                    return false;
+                }
+            }
+        });
+    }
+    buildContextMenu();
+
+    return {
+        handleSelectionChanged,
+        curArea,
+    }
+
+})();
+
+// 分类表
+const CLASS_BOOK = (() => {
+    const setting = {
+        header: [{ headerName: '分类', headerWidth: $('#area-spread').width(), dataCode: 'name', dataType: 'String', hAlign: 'left', vAlign: 'center' }],
+        controller: {
+            cols: [
+                {
+                    data: {
+                        field: 'name',
+                        vAlign: 1,
+                        hAlign: 0,
+                        font: 'Arial'
+                    },
+                }
+            ],
+            headRows: 1,
+            headRowHeight: [30],
+            emptyRows: 0,
+            treeCol: 0
+        },
+        tree: {
+            id: 'ID',
+            pid: 'ParentID',
+            nid: 'NextSiblingID',
+            rootId: -1
+        }
+    };
+    // 初始化表格
+    const workBook = initSheet($('#class-spread')[0], setting);
+    workBook.options.allowExtendPasteRange = false;
+    workBook.options.allowUserDragDrop = true;
+    workBook.options.allowUserDragFill = true;
+    const sheet = workBook.getSheet(0);
+
+    let tree;
+    let controller;
+    // 初始化数据
+    async function initData(libID, areaID) {
+        if (!areaID) {
+            tree = null;
+            controller = null;
+            sheet.setRowCount(0);
+            PRICE_BOOK.clear();
+            return;
+        }
+        $.bootstrapLoading.start();
+        try {
+            const data = await ajaxPost('/priceInfo/getClassData', { libID, areaID }, TIME_OUT);
+            tree = idTree.createNew(setting.tree);
+            tree.loadDatas(data);
+            tree.selected = tree.items.length > 0 ? tree.items[0] : null;
+            controller = TREE_SHEET_CONTROLLER.createNew(tree, sheet, setting.controller, false);
+            controller.showTreeData();
+            handleSelectionChanged(0);
+            sheet.setSelection(0, 0, 1, 1);
+            lockUtil.lockSpreads([workBook], locked);
+        } catch (err) {
+            tree = null;
+            controller = null;
+            sheet.setRowCount(0);
+            alert(err);
+        } finally {
+            $.bootstrapLoading.end();
+        }
+    }
+
+    // 编辑处理
+    async function handleEdit(changedCells) {
+        const updateData = [];
+        changedCells.forEach(({ row, col }) => {
+            updateData.push({
+                row,
+                type: UpdateType.UPDATE,
+                filter: { ID: tree.items[row].data.ID },
+                update: { name: sheet.getValue(row, col) }
+            });
+        });
+        try {
+            await ajaxPost('/priceInfo/editClassData', { updateData }, TIME_OUT);
+            updateData.forEach(({ row, update: { name } }) => tree.items[row].data.name = name);
+        } catch (err) {
+            // 恢复各单元格数据
+            sheetCommonObj.renderSheetFunc(sheet, () => {
+                changedCells.forEach(({ row }) => {
+                    sheet.setValue(tree.items[row].data.name);
+                });
+            });
+        }
+    }
+    sheet.bind(GC.Spread.Sheets.Events.ValueChanged, function (e, info) {
+        const changedCells = [{ row: info.row, col: info.col }];
+        handleEdit(changedCells);
+    });
+    sheet.bind(GC.Spread.Sheets.Events.RangeChanged, function (e, info) {
+        handleEdit(info.changedCells);
+    });
+
+    // 树操作相关
+    const $insert = $('#tree-insert');
+    const $remove = $('#tree-remove');
+    const $upLevel = $('#tree-up-level');
+    const $downLevel = $('#tree-down-level');
+    const $downMove = $('#tree-down-move');
+    const $upMove = $('#tree-up-move');
+
+    // 插入
+    let canInsert = true;
+    async function insert() {
+        try {
+            if (!canInsert) {
+                return false;
+            }
+            canInsert = false;
+            $.bootstrapLoading.start();
+            const updateData = [];
+            const selected = tree.selected;
+            const newItem = {
+                libID,
+                areaID: AREA_BOOK.curArea.ID,
+                ID: uuid.v1(),
+                name: '',
+                ParentID: '-1',
+                NextSiblingID: '-1'
+            };
+            if (selected) {
+                newItem.ParentID = selected.data.ParentID;
+                updateData.push({
+                    type: UpdateType.UPDATE,
+                    filter: { ID: selected.data.ID },
+                    update: { NextSiblingID: newItem.ID }
+                });
+                if (selected.nextSibling) {
+                    newItem.NextSiblingID = selected.nextSibling.data.ID;
+                }
+            }
+            updateData.push({
+                type: UpdateType.CREATE,
+                document: newItem
+            });
+            await ajaxPost('/priceInfo/editClassData', { updateData });
+            controller.insertByID(newItem.ID);
+            handleSelectionChanged(sheet.getActiveRowIndex());
+        } catch (err) {
+            alert(err);
+        } finally {
+            canInsert = true;
+            $.bootstrapLoading.end();
+        }
+    }
+
+    $insert.click(_.debounce(insert, DEBOUNCE_TIME, { leading: true }));
+
+    // 删除
+    let canRemove = true;
+    async function remove() {
+        try {
+            if (!canRemove) {
+                return false;
+            }
+            canRemove = false;
+            $.bootstrapLoading.start();
+            const updateData = [];
+            const selected = tree.selected;
+            const children = selected.getPosterity();
+            [selected, ...children].forEach(node => updateData.push({
+                type: UpdateType.DELETE,
+                filter: { ID: node.data.ID }
+            }));
+            if (selected.preSibling) {
+                updateData.push({
+                    type: UpdateType.UPDATE,
+                    filter: { ID: selected.preSibling.data.ID },
+                    update: { NextSiblingID: selected.data.NextSiblingID }
+                });
+            }
+            await ajaxPost('/priceInfo/editClassData', { updateData });
+            controller.delete();
+            handleSelectionChanged(sheet.getActiveRowIndex());
+        } catch (err) {
+            alert(err);
+        } finally {
+            canRemove = true;
+            $.bootstrapLoading.end();
+        }
+    }
+
+    $remove.click(_.debounce(remove, DEBOUNCE_TIME, { leading: true }));
+
+    // 升级
+    let canUpLevel = true;
+    async function upLevel() {
+        try {
+            if (!canUpLevel) {
+                return false;
+            }
+            canUpLevel = false;
+            $.bootstrapLoading.start();
+            const updateData = [];
+            const selected = tree.selected;
+            if (selected.preSibling) {
+                updateData.push({
+                    type: UpdateType.UPDATE,
+                    filter: { ID: selected.preSibling.data.ID },
+                    update: { NextSiblingID: -1 }
+                });
+            }
+            if (selected.parent) {
+                updateData.push({
+                    type: UpdateType.UPDATE,
+                    filter: { ID: selected.parent.data.ID },
+                    update: { NextSiblingID: selected.data.ID }
+                });
+            }
+            updateData.push({
+                type: UpdateType.UPDATE,
+                filter: { ID: selected.data.ID },
+                update: { ParentID: selected.parent.data.ParentID, NextSiblingID: selected.parent.data.NextSiblingID }
+            });
+            let curNode = selected.nextSibling;
+            while (curNode) {
+                updateData.push({
+                    type: UpdateType.UPDATE,
+                    filter: { ID: curNode.data.ID },
+                    update: { ParentID: selected.data.ID }
+                });
+                curNode = curNode.nextSibling;
+            }
+            await ajaxPost('/priceInfo/editClassData', { updateData });
+            controller.upLevel();
+            refreshTreeButton(tree.selected);
+        } catch (err) {
+            alert(err);
+        } finally {
+            canUpLevel = true;
+            $.bootstrapLoading.end();
+        }
+    }
+
+    $upLevel.click(_.debounce(upLevel, DEBOUNCE_TIME, { leading: true }));
+
+    // 降级
+    let canDownLevel = true;
+    async function downLevel() {
+        try {
+            if (!canDownLevel) {
+                return false;
+            }
+            canDownLevel = false;
+            $.bootstrapLoading.start();
+            const updateData = [];
+            const selected = tree.selected;
+            if (selected.preSibling) {
+                updateData.push({
+                    type: UpdateType.UPDATE,
+                    filter: { ID: selected.preSibling.data.ID },
+                    update: { NextSiblingID: selected.data.NextSiblingID }
+                });
+                const preSiblingLastChild = selected.preSibling.children[selected.preSibling.children.length - 1];
+                if (preSiblingLastChild) {
+                    updateData.push({
+                        type: UpdateType.UPDATE,
+                        filter: { ID: preSiblingLastChild.data.ID },
+                        update: { NextSiblingID: selected.data.ID }
+                    });
+                }
+                updateData.push({
+                    type: UpdateType.UPDATE,
+                    filter: { ID: selected.data.ID },
+                    update: { ParentID: selected.preSibling.data.ID, NextSiblingID: -1 }
+                });
+            }
+            await ajaxPost('/priceInfo/editClassData', { updateData });
+            controller.downLevel();
+            refreshTreeButton(tree.selected);
+        } catch (err) {
+            alert(err);
+        } finally {
+            canDownLevel = true;
+            $.bootstrapLoading.end();
+        }
+    }
+
+    $downLevel.click(_.debounce(downLevel, DEBOUNCE_TIME, { leading: true }));
+
+    // 下移
+    let canDownMove = true;
+    async function downMove() {
+        try {
+            if (!canDownMove) {
+                return false;
+            }
+            canDownMove = false;
+            $.bootstrapLoading.start();
+            const updateData = [];
+            const selected = tree.selected;
+            if (selected.preSibling) {
+                updateData.push({
+                    type: UpdateType.UPDATE,
+                    filter: { ID: selected.preSibling.data.ID },
+                    update: { NextSiblingID: selected.data.NextSiblingID }
+                });
+            }
+            updateData.push({
+                type: UpdateType.UPDATE,
+                filter: { ID: selected.data.ID },
+                update: { NextSiblingID: selected.nextSibling.data.NextSiblingID }
+            });
+            updateData.push({
+                type: UpdateType.UPDATE,
+                filter: { ID: selected.nextSibling.data.ID },
+                update: { NextSiblingID: selected.data.ID }
+            });
+            await ajaxPost('/priceInfo/editClassData', { updateData });
+            controller.downMove();
+            refreshTreeButton(tree.selected);
+        } catch (err) {
+            alert(err);
+        } finally {
+            canDownMove = true;
+            $.bootstrapLoading.end();
+        }
+    }
+
+    $downMove.click(_.debounce(downMove, DEBOUNCE_TIME, { leading: true }));
+
+    // 上移
+    let canUpMove = true;
+    async function upMove() {
+        try {
+            if (!canUpMove) {
+                return false;
+            }
+            canUpMove = false;
+            $.bootstrapLoading.start();
+            const updateData = [];
+            const selected = tree.selected;
+            if (selected.preSibling) {
+                updateData.push({
+                    type: UpdateType.UPDATE,
+                    filter: { ID: selected.preSibling.data.ID },
+                    update: { NextSiblingID: selected.data.NextSiblingID }
+                });
+            }
+            const prePreSibling = selected.preSibling.preSibling;
+            if (prePreSibling) {
+                updateData.push({
+                    type: UpdateType.UPDATE,
+                    filter: { ID: prePreSibling.data.ID },
+                    update: { NextSiblingID: selected.data.ID }
+                });
+            }
+            updateData.push({
+                type: UpdateType.UPDATE,
+                filter: { ID: selected.data.ID },
+                update: { NextSiblingID: selected.preSibling.data.ID }
+            });
+            await ajaxPost('/priceInfo/editClassData', { updateData });
+            controller.upMove();
+            refreshTreeButton(tree.selected);
+        } catch (err) {
+            alert(err);
+        } finally {
+            canUpMove = true;
+            $.bootstrapLoading.end();
+        }
+    }
+
+    $upMove.click(_.debounce(upMove, DEBOUNCE_TIME, { leading: true }));
+
+
+    // 刷新树操作按钮有效性
+    function refreshTreeButton(selected) {
+        if (locked) {
+            return;
+        }
+        $insert.removeClass('disabled');
+        $remove.removeClass('disabled');
+        $upLevel.removeClass('disabled');
+        $downLevel.removeClass('disabled');
+        $downMove.removeClass('disabled');
+        $upMove.removeClass('disabled');
+        if (!selected) {
+            $remove.addClass('disabled');
+            $upLevel.addClass('disabled');
+            $downLevel.addClass('disabled');
+            $downMove.addClass('disabled');
+            $upMove.addClass('disabled');
+        } else {
+            if (!selected.preSibling) {
+                $downLevel.addClass('disabled');
+                $upMove.addClass('disabled');
+            }
+            if (!selected.nextSibling) {
+                $downMove.addClass('disabled');
+            }
+            if (!selected.parent) {
+                $upLevel.addClass('disabled');
+            }
+        }
+    }
+
+    // 焦点变更处理
+    const curClass = { ID: null };
+    function handleSelectionChanged(row) {
+        const classNode = tree.items[row] || null;
+        tree.selected = classNode;
+        refreshTreeButton(classNode);
+        curClass.ID = classNode && classNode.data && classNode.data.ID || null;
+        const classIDList = []
+        if (classNode) {
+            classIDList.push(classNode.data.ID);
+            const children = classNode.getPosterity();
+            children.forEach(child => classIDList.push(child.data.ID));
+        }
+        PRICE_BOOK.initData(classIDList);
+    }
+    const debounceSelectionChanged = _.debounce(function (e, info) {
+        const row = info.newSelections && info.newSelections[0] ? info.newSelections[0].row : 0;
+        handleSelectionChanged(row);
+    }, DEBOUNCE_TIME, { leading: true });
+    sheet.bind(GC.Spread.Sheets.Events.SelectionChanged, debounceSelectionChanged);
+
+    return {
+        initData,
+        handleSelectionChanged,
+        curClass,
+    }
+
+})();
+
+// 价格信息表
+const PRICE_BOOK = (() => {
+    const setting = {
+        header: [
+            { headerName: '编码', headerWidth: 100, dataCode: 'code', dataType: 'String', hAlign: 'left', vAlign: 'center' },
+            { headerName: '名称', headerWidth: 200, dataCode: 'name', dataType: 'String', hAlign: 'left', vAlign: 'center' },
+            { headerName: '规格型号', headerWidth: 120, dataCode: 'specs', dataType: 'String', hAlign: 'left', vAlign: 'center' },
+            { headerName: '单位', headerWidth: 80, dataCode: 'unit', dataType: 'String', hAlign: 'center', vAlign: 'center' },
+            { headerName: '不含税价', headerWidth: 80, dataCode: 'noTaxPrice', dataType: 'String', hAlign: 'right', vAlign: 'center' },
+            { headerName: '含税价', headerWidth: 80, dataCode: 'taxPrice', dataType: 'String', hAlign: 'right', vAlign: 'center' },
+        ],
+    };
+    // 初始化表格
+    const workBook = initSheet($('#price-spread')[0], setting);
+    workBook.options.allowUserDragDrop = true;
+    workBook.options.allowUserDragFill = true;
+    lockUtil.lockSpreads([workBook], locked);
+    const sheet = workBook.getSheet(0);
+
+    let cache = [];
+    // 清空
+    function clear() {
+        cache = [];
+        sheet.setRowCount(0);
+    }
+    // 初始化数据
+    async function initData(classIDList) {
+        if (!classIDList || !classIDList.length) {
+            return clear();
+        }
+        $.bootstrapLoading.start();
+        try {
+            cache = await ajaxPost('/priceInfo/getPriceData', { classIDList }, TIME_OUT);
+            showData(sheet, cache, setting.header, 5);
+        } catch (err) {
+            cache = [];
+            sheet.setRowCount(0);
+            alert(err);
+        } finally {
+            $.bootstrapLoading.end();
+        }
+    }
+
+    // 获取当前表中行数据
+    function getRowData(sheet, row, headers) {
+        const item = {};
+        headers.forEach(({ dataCode }, index) => {
+            const value = sheet.getValue(row, index) || '';
+            if (value) {
+                item[dataCode] = value;
+            }
+        });
+        return item;
+    }
+
+    // 获取表数据和缓存数据的不同数据
+    function getRowDiffData(curRowData, cacheRowData, headers) {
+        let item = null;
+        headers.forEach(({ dataCode }) => {
+            const curValue = curRowData[dataCode];
+            const cacheValue = cacheRowData[dataCode];
+            if (!cacheValue && !curValue) {
+                return;
+            }
+            if (cacheValue !== curValue) {
+                if (!item) {
+                    item = {};
+                }
+                item[dataCode] = curValue || '';
+            }
+        });
+        return item;
+    }
+
+    // 编辑处理
+    async function handleEdit(changedCells) {
+        const postData = []; // 请求用
+        // 更新缓存用
+        const updateData = [];
+        const deleteData = [];
+        const insertData = [];
+        try {
+            changedCells.forEach(({ row }) => {
+                if (cache[row]) {
+                    const rowData = getRowData(sheet, row, setting.header);
+                    if (Object.keys(rowData).length) { // 还有数据,更新
+                        const diffData = getRowDiffData(rowData, cache[row], setting.header);
+                        if (diffData) {
+                            postData.push({ type: UpdateType.UPDATE, ID: cache[row].ID, data: diffData });
+                            updateData.push({ row, data: diffData });
+                        }
+                    } else { // 该行无数据了,删除
+                        postData.push({ type: UpdateType.DELETE, ID: cache[row].ID });
+                        deleteData.push(cache[row]);
+                    }
+                } else { // 新增
+                    const rowData = getRowData(sheet, row, setting.header);
+                    if (Object.keys(rowData).length) {
+                        rowData.ID = uuid.v1();
+                        rowData.libID = libID;
+                        rowData.compilationID = compilationID;
+                        rowData.areaID = AREA_BOOK.curArea.ID;
+                        rowData.classID = CLASS_BOOK.curClass.ID;
+                        rowData.period = curLibPeriod;
+                        postData.push({ type: UpdateType.CREATE, data: rowData });
+                        insertData.push(rowData);
+                    }
+                }
+            });
+            if (postData.length) {
+                await ajaxPost('/priceInfo/editPriceData', { postData }, TIME_OUT);
+                // 更新缓存,先更新然后删除,最后再新增,防止先新增后缓存数据的下标与更新、删除数据的下标对应不上
+                updateData.forEach(item => {
+                    Object.assign(cache[item.row], item.data);
+                });
+                deleteData.forEach(item => {
+                    const index = cache.indexOf(item);
+                    if (index >= 0) {
+                        cache.splice(index, 1);
+                    }
+                });
+                insertData.forEach(item => cache.push(item));
+                if (deleteData.length || insertData.length) {
+                    showData(sheet, cache, setting.header, 5);
+                }
+            }
+        } catch (err) {
+            // 恢复各单元格数据
+            showData(sheet, cache, setting.header, 5);
+        }
+    }
+    sheet.bind(GC.Spread.Sheets.Events.ValueChanged, function (e, info) {
+        const changedCells = [{ row: info.row }];
+        handleEdit(changedCells);
+    });
+    sheet.bind(GC.Spread.Sheets.Events.RangeChanged, function (e, info) {
+        const changedRows = [];
+        let preRow;
+        info.changedCells.forEach(({ row }) => {
+            if (row !== preRow) {
+                changedRows.push({ row });
+            }
+            preRow = row;
+        });
+        handleEdit(changedRows);
+    });
+
+    return {
+        clear,
+        initData,
+    }
+})();
+
+$(document).ready(() => {
+    $('[data-toggle="tooltip"]').tooltip();
+    AREA_BOOK.handleSelectionChanged(0);
+    const $range = $(document.body);
+    lockUtil.lockTools($range, locked);
+});

+ 255 - 0
web/maintain/price_info_lib/js/main.js

@@ -0,0 +1,255 @@
+// 节流
+function throttle(fn, time) {
+    let canRun = true;
+    return function () {
+        if (!canRun) {
+            return;
+        }
+        canRun = false;
+        const rst = fn.apply(this, arguments);
+        // 事件返回错误,说明没有发起请求,允许直接继续触发事件,不进行节流处理
+        if (rst === false) {
+            canRun = true;
+            return;
+        }
+        setTimeout(() => canRun = true, time);
+    }
+}
+
+const periodReg = /\d{4}-(0[1-9])|(1[0-2])$/;
+function createLib() {
+    const name = $('#name').val();
+    if (!name) {
+        $('#name-error').show();
+        return false;
+    }
+    const period = $('#period').val();
+    if (!period || !periodReg.test(period)) {
+        $('#period-error').show();
+        return false;
+    }
+    $('#add-lib-form').submit();
+}
+
+let curLib = {};
+
+//设置当前库
+function setCurLib(libID) {
+    curLib.id = libID;
+    curLib.name = $(`#${libID}`).text();
+}
+
+// 点击编辑按钮
+function handleEditClick(libID) {
+    setCurLib(libID);
+    $('#edit').modal('show');
+}
+
+// 点击确认编辑
+function handleEditConfirm() {
+    const rename = $('#rename-text').val();
+    if (!rename) {
+        $('#rename-error').show();
+        return false;
+    }
+    $('#edit').modal('hide');
+    ajaxPost('/priceInfo/renameLib', { libID: curLib.id, name: rename })
+        .then(() => $(`#${curLib.id} a`).text(rename));
+}
+
+// 删除需要连需点击三次才可删除
+let curDelCount = 0;
+// 点击删除按钮
+function handleDeleteClick(libID) {
+    setCurLib(libID);
+    curDelCount = 0;
+    $('#del').modal('show');
+}
+
+// 删除确认
+function handleDeleteConfirm() {
+    curDelCount++;
+    if (curDelCount === 3) {
+        $.bootstrapLoading.start();
+        curDelCount = -10; // 由于需要连续点击,因此没有对此事件进行节流,为防止多次请求,一旦连续点击到三次,马上清空次数。
+        $('#del').modal('hide');
+        ajaxPost('/priceInfo/deleteLib', { libID: curLib.id })
+            .then(() => $(`#${curLib.id}`).parent().remove())
+            .finally(() => $.bootstrapLoading.end());
+    }
+}
+
+// 点击导入按钮
+function handleImportClick(libID) {
+    setCurLib(libID);
+    $('#import').modal('show');
+}
+
+// 导入确认
+function handleImportConfirm() {
+    $.bootstrapLoading.start();
+    const self = $(this);
+    try {
+        const formData = new FormData();
+        const file = $("input[name='import_data']")[0];
+        if (file.files.length <= 0) {
+            throw '请选择文件!';
+        }
+        formData.append('file', file.files[0]);
+        formData.append('libID', curLib.id);
+        $.ajax({
+            url: '/priceInfo/importExcel',
+            type: 'POST',
+            data: formData,
+            cache: false,
+            contentType: false,
+            processData: false,
+            beforeSend: function () {
+                self.attr('disabled', 'disabled');
+                self.text('上传中...');
+            },
+            success: function (response) {
+                self.removeAttr('disabled');
+                self.text('确定导入');
+                if (response.err === 0) {
+                    $.bootstrapLoading.end();
+                    const message = response.msg !== undefined ? response.msg : '';
+                    if (message !== '') {
+                        alert(message);
+                    }
+                    // 成功则关闭窗体
+                    $('#import').modal("hide");
+                } else {
+                    $.bootstrapLoading.end();
+                    const message = response.msg !== undefined ? response.msg : '上传失败!';
+                    alert(message);
+                }
+            },
+            error: function () {
+                $.bootstrapLoading.end();
+                alert("与服务器通信发生错误");
+                self.removeAttr('disabled');
+                self.text('确定导入');
+            }
+        });
+    } catch (error) {
+        alert(error);
+        $.bootstrapLoading.end();
+    }
+}
+
+const { ProcessStatus, CRAWL_LOG_KEY } = window.PRICE_INFO_CONST;
+
+const CHECKING_TIME = 5000;
+// 检测爬取、导入是否完成
+function processChecking(key, cb) {
+    checking();
+
+    function checking() {
+        ajaxPost('/priceInfo/processChecking', { key })
+            .then(handleResolve)
+            .catch(handleReject)
+    }
+
+    let timer;
+    function handleResolve({ key, status, errorMsg }) {
+        if (status === ProcessStatus.START) {
+            if (!$('#progressModal').is(':visible')) {
+                const title = key === CRAWL_LOG_KEY ? '爬取数据' : '导入数据';
+                const text = key === CRAWL_LOG_KEY ? '正在爬取数据,请稍候……' : '正在导入数据,请稍候……';
+                $.bootstrapLoading.progressStart(title, true);
+                $("#progress_modal_body").text(text);
+            }
+            timer = setTimeout(checking, CHECKING_TIME);
+        } else if (status === ProcessStatus.FINISH) {
+            if (timer) {
+                clearTimeout(timer);
+            }
+            $.bootstrapLoading.progressEnd();
+            if (cb) {
+                cb();
+            }
+        } else {
+            if (timer) {
+                clearTimeout(timer);
+            }
+            if (errorMsg) {
+                alert(errorMsg);
+            }
+            $.bootstrapLoading.progressEnd();
+        }
+    }
+    function handleReject(err) {
+        if (timer) {
+            clearInterval(timer);
+        }
+        alert(err);
+        $.bootstrapLoading.progressEnd();
+    }
+}
+
+const matched = window.location.search.match(/filter=(.+)/);
+const compilationID = matched && matched[1] || '';
+// 爬取数据确认
+function handleCrawlConfirm() {
+    const from = $('#period-start').val();
+    const to = $('#period-end').val();
+    if (!periodReg.test(from) || !periodReg.test(to)) {
+        $('#crawl-error').show();
+        return false;
+    }
+    $('#crawl').modal('hide');
+    ajaxPost('/priceInfo/crawlData', { from, to, compilationID }, 0) // 没有timeout
+        .then(() => {
+            processChecking(CRAWL_LOG_KEY, () => window.location.reload());
+        })
+}
+/* function handleCrawlConfirm() {
+    const from = $('#period-start').val();
+    const to = $('#period-end').val();
+    if (!periodReg.test(from) || !periodReg.test(to)) {
+        $('#crawl-error').show();
+        return false;
+    }
+    $('#crawl').modal('hide');
+    $.bootstrapLoading.progressStart('爬取数据', true);
+    $("#progress_modal_body").text('正在爬取数据,请稍候……');
+    // 不用定时器的话,可能finally处理完后,进度条界面才显示,导致进度条界面没有被隐藏
+    const time = setInterval(() => {
+        if ($('#progressModal').is(':visible')) {
+            clearInterval(time);
+            ajaxPost('/priceInfo/crawlData', { from, to, compilationID }, 0) // 没有timeout
+                .then(() => {
+                    window.location.reload();
+                })
+                .finally(() => $.bootstrapLoading.progressEnd());
+        }
+    }, 500);
+} */
+
+const throttleTime = 1000;
+$(document).ready(function () {
+    processChecking();
+
+    // 锁定、解锁
+    $('.lock').click(function () {
+        lockUtil.handleLockClick($(this));
+    });
+    // 新增
+    $('#add-lib').click(throttle(createLib, throttleTime));
+    // 重命名
+    $('#rename').click(throttle(handleEditConfirm, throttleTime));
+    // 删除
+    $('#delete').click(handleDeleteConfirm);
+    // 爬取数据
+    $('#crawl-confirm').click(throttle(handleCrawlConfirm, throttleTime));
+    // 导入excel
+    $('#import-confirm').click(throttle(handleImportConfirm, throttleTime));
+
+    $('#add').on('hidden.bs.modal', () => {
+        $('#name-error').hide();
+        $('#period-error').hide();
+    });
+    $('#edit').on('hidden.bs.modal', () => $('#rename-error').hide());
+    $('#crawl').on('hidden.bs.modal', () => $('#crawl-error').hide());
+});

+ 5 - 0
web/maintain/report/html/rpt_tpl_dtl_pre_hdl_sort.html

@@ -50,3 +50,8 @@
         </div>
     </div>
 </div>
+<div class="form-group row" id="div_sort_self_define">
+    <div class="col-md-6">
+        <textarea rows="15" cols="50" style="width: 100%; height: 100%; overflow: auto; work-break: break-all;" id="selfDefineSort" onkeyup="preHandleSortObj.changeSelfDefineExpression(this)"></textarea>
+    </div>
+</div>

+ 115 - 39
web/maintain/report/js/rpt_tpl_main.js

@@ -87,6 +87,22 @@ let zTreeOprObj = {
         params.doc = rawNode;
         CommonAjax.postEx("report_tpl_api/updateTreeRootNode", params, 5000, isAsync, callback, failCallback, null);
     },
+    partialUpdateTreeNode: function (rawNode, pathArray, nodeArray, isAsync, callback, failCallback) {
+        // 这个是局部刷新,原理是根据topNodeId找到后台的topNode,根据路径(有多个路径,可以实现多个子节点同时刷新)来替换(nodeArray里的)后台数据库的相关节点
+        // 这里需要一个调整,就是新增目录及模板需要加给后缀(new(Date)).getTime() ,以保证不重叠(真重叠了算倒霉)
+        // path描述:{ operation_type: '', // 操作类型:‘update’ ‘add’、‘delete’
+        //            node_path: [],      // 节点路径:如:['02.广东', '03.增城北绕线项目定制报表'],表示Top节点的items下的那个‘02.广东’子节点(要full scan)的items下的 ‘03.增城北绕线项目定制报表’子节点
+        //          }
+        // nodeArray: 与pathArray一一对应
+        //
+        let params = {};
+        params.compilationId = rawNode.compilationId;
+        params.engineerId = rawNode.engineerId;
+        params.userId = rawNode.userId;
+        params.pathArray = pathArray;
+        params.nodeArray = nodeArray;
+        CommonAjax.postEx("report_tpl_api/partialUpdateTreeNode", params, 5000, isAsync, callback, failCallback, null);
+    },
     updateTopNodeName: function (topNode, isAsync, callback, failCallback) {
         let params = {};
         params.compilationId = topNode.compilationId;
@@ -128,6 +144,21 @@ let zTreeOprObj = {
         }
         return rst;
     },
+    getNodePath: function(node, includeCurrentNode) {
+        const rst = [];
+        if (includeCurrentNode && node.level >= 1) {
+            rst.push(node.name);
+        }
+        let parentNode = node.getParentNode();
+        while (parentNode && parentNode.level > 0) { //顶节点是dummy的
+            rst.unshift(parentNode.name);
+            parentNode = parentNode.getParentNode();
+            if (parentNode === null || parentNode === undefined || parentNode.level === 0) {
+                rst.splice(0,1); // 删除头节点(后台不需要)
+            }
+        }
+        return rst;
+    },
     buildSubRootNodeDoc: function(subNode) {
         let me = this, rst = null;
         if (subNode) {
@@ -181,40 +212,40 @@ let zTreeOprObj = {
         let me = zTreeOprObj, sObj = $("#" + treeNode.tId + "_span");
         if (treeNode.editNameFlag || $("#addBtn_"+treeNode.tId).length > 0 || treeNode.nodeType === RT.NodeType.TEMPLATE) return;
         if (treeNode.level === 0) {
-            let addStr = "<span class='button star' id='addBtn_" + treeNode.tId + "' title='新增编办类型' onfocus='this.blur();'></span>";
-            sObj.after(addStr);
-            let btn = $("#addBtn_"+treeNode.tId);
-            if (btn) btn.bind("click", function(){
-                let rawNode = me.createIniComilationNode();
-                if (!me.chkIfDupCompilationNode(rawNode, treeNode)) {
-                    rawNode.userId = treeNode.userId;
-                    me.addNewNodeEx(rawNode, function(rst){
-                        if (rst) {
-                            let newNodes = [], isSilent = false;
-                            rawNode.isParent = true;
-                            newNodes.push(rawNode);
-                            if (treeNode.items && treeNode.items.length > 0) {
-                                let insertIdx = -1;
-                                for (let i = 0; i < treeNode.items.length; i++) {
-                                    if (treeNode.items[i].compilationId === rawNode.compilationId) {
-                                        if (treeNode.items[i].engineerId > rawNode.engineerId) {
-                                            insertIdx = i;
-                                            break;
-                                        }
-                                    }
-                                }
-                                me.treeObj.addNodes(treeNode, insertIdx, newNodes, isSilent);
-                            } else {
-                                me.treeObj.addNodes(treeNode, 0, newNodes, isSilent);
-                            }
-                        } else {
-                            alert("后台创建失败,请确认是否有重复类型跟节点!")
-                        }
-                    }, null);
-                } else {
-                    alert("有重复编办!");
-                }
-            });
+            // let addStr = "<span class='button star' id='addBtn_" + treeNode.tId + "' title='新增编办类型' onfocus='this.blur();'></span>";
+            // sObj.after(addStr);
+            // let btn = $("#addBtn_"+treeNode.tId);
+            // if (btn) btn.bind("click", function(){
+            //     let rawNode = me.createIniComilationNode();
+            //     if (!me.chkIfDupCompilationNode(rawNode, treeNode)) {
+            //         rawNode.userId = treeNode.userId;
+            //         me.addNewNodeEx(rawNode, function(rst){
+            //             if (rst) {
+            //                 let newNodes = [], isSilent = false;
+            //                 rawNode.isParent = true;
+            //                 newNodes.push(rawNode);
+            //                 if (treeNode.items && treeNode.items.length > 0) {
+            //                     let insertIdx = -1;
+            //                     for (let i = 0; i < treeNode.items.length; i++) {
+            //                         if (treeNode.items[i].compilationId === rawNode.compilationId) {
+            //                             if (treeNode.items[i].engineerId > rawNode.engineerId) {
+            //                                 insertIdx = i;
+            //                                 break;
+            //                             }
+            //                         }
+            //                     }
+            //                     me.treeObj.addNodes(treeNode, insertIdx, newNodes, isSilent);
+            //                 } else {
+            //                     me.treeObj.addNodes(treeNode, 0, newNodes, isSilent);
+            //                 }
+            //             } else {
+            //                 alert("后台创建失败,请确认是否有重复类型跟节点!")
+            //             }
+            //         }, null);
+            //     } else {
+            //         alert("有重复编办!");
+            //     }
+            // });
         } else {
             let addStr = "<span class='button add' id='addBtn_" + treeNode.tId + "' title='新增子目录' onfocus='this.blur();'></span>";
             sObj.after(addStr);
@@ -223,8 +254,10 @@ let zTreeOprObj = {
                 me.getNewNodeID(1, function (newNodeID) {
                     let rawNode = me.createIniNode();
                     rawNode.nodeType = RT.NodeType.NODE;
-                    rawNode.name = "新增子节点";
+                    rawNode.name = "新增子节点" + (new Date()).getTime();
                     rawNode.ID = newNodeID;
+                    rawNode.isParent = true;
+                    rawNode.items = [];
                     let newNodes = [], isSilent = false;
                     newNodes.push(rawNode);
                     if (me.treeObj) {
@@ -242,12 +275,28 @@ let zTreeOprObj = {
                         me.treeObj.addNodes(treeNode, insertIdx, newNodes, isSilent);
                         let tn = me.getParentNodeByNodeLevel(treeNode, NODE_LEVEL_COMPILATION_NEW);
                         let newTopNode = me.buildRootNodeDoc(tn);
+
+                        //*
+                        let pathArr = [];
+                        let nodeArr = [];
+                        let path = {operation_type: 'add', node_path: []};
+                        path.node_path = me.getNodePath(treeNode, true);
+                        pathArr.push(path);
+                        nodeArr.push(rawNode);
+                        me.partialUpdateTreeNode(newTopNode, pathArr, nodeArr, true, function(rst){
+                            if (!(rst)) {
+                                alert("新增节点失败!");
+                            }
+                            me.refreshNodes();
+                        }, null);
+                        /*/
                         me.updateTreeRootNode(newTopNode, true, function(rst){
                             if (!(rst)) {
                                 alert("新增节点失败!");
                             }
                             me.refreshNodes();
                         }, null);
+                        //*/
                     }
                 });
             });
@@ -260,7 +309,7 @@ let zTreeOprObj = {
                     me.getNewNodeID(1, function (newNodeID) {
                         let rawNode = me.createIniNode();
                         rawNode.nodeType = RT.NodeType.TEMPLATE;
-                        rawNode.name = "新增报表模板";
+                        rawNode.name = "新增报表模板" + (new Date()).getTime();
                         rawNode.ID = newNodeID;
                         rawNode.released = false;
                         let newNodes = [], isSilent = false;
@@ -268,12 +317,27 @@ let zTreeOprObj = {
                         me.treeObj.addNodes(treeNode, -1, newNodes, isSilent);
                         let tn = me.getParentNodeByNodeLevel(treeNode, NODE_LEVEL_COMPILATION_NEW);
                         let topNode = me.buildRootNodeDoc(tn);
+                        //*
+                        let pathArr = [];
+                        let nodeArr = [];
+                        let path = {operation_type: 'add', node_path: []};
+                        path.node_path = me.getNodePath(treeNode, true);
+                        pathArr.push(path);
+                        nodeArr.push(rawNode);
+                        me.partialUpdateTreeNode(topNode, pathArr, nodeArr, true, function(rst){
+                            if (!(rst)) {
+                                alert("新增节点失败!");
+                            }
+                            me.refreshNodes();
+                        }, null);
+                        /*/
                         me.updateTreeRootNode(topNode, true, function(rst){
                             if (!(rst)) {
                                 alert("新增空白模板失败!");
                             }
                             me.refreshNodes();
                         }, null);
+                        //*/
                     });
                 }
             });
@@ -691,13 +755,25 @@ let zTreeOprObj = {
                     }
                 });
             } else {
-                me.updateTreeRootNode(rawNode, true, function(rst){
+                // me.updateTreeRootNode(rawNode, true, function(rst){
+                //     if (!(rst)) {
+                //         alert("删除请求失败!");
+                //     }
+                // });
+                let pathArr = [];
+                let nodeArr = [];
+                let path = {operation_type: 'delete', node_path: []};
+                path.node_path = me.getNodePath(treeNode, true);
+                pathArr.push(path);
+                nodeArr.push('');
+                me.partialUpdateTreeNode(topPNode, pathArr, nodeArr, true, function (rst) {
                     if (!(rst)) {
-                        alert("删除请求失败!");
+                        alert("删除模板失败!");
                     }
-                });
+                }, null);
             }
             me.refreshNodes();
+            //*/
         }
     },
     beforeEditName: function (treeId, treeNode) {

+ 16 - 0
web/maintain/report/js/rpt_tpl_pre_handle.js

@@ -336,6 +336,7 @@ let preHandleObj = {
         $("#div_sort_type_according_to_parent")[0].style.display = "none";
         $("#div_sort_type_parent_data")[0].style.display = "none";
         $("#div_sort_tree")[0].style.display = "none";
+        $("#div_sort_self_define")[0].style.display = "none";
     },
     onPreHandleClick: function(event,treeId,treeNode) {
         //点击预处理环节 节点
@@ -525,6 +526,7 @@ let preHandleSortObj = {
                 break;
             case 3 :
                 //self define
+                dest[JV.PROP_SORT_TYPE_SELF_DEFINE_LOGIC] = src[JV.PROP_SORT_TYPE_SELF_DEFINE_LOGIC];
                 break;
             default:
                 break;
@@ -605,6 +607,8 @@ let preHandleSortObj = {
                     break;
                 case 3 :
                     //self define
+                    $("#div_sort_self_define")[0].style.display = "";
+                    $("#selfDefineSort")[0].value = preHandleObj.currentNode[JV.PROP_SORT_TYPE_SELF_DEFINE_LOGIC];
                     break;
                 default:
                     break;
@@ -722,9 +726,20 @@ let preHandleSortObj = {
         } else if (dom.selectedIndex === 1) {
             $("#div_sort_tree")[0].style.display = "";
             preHandleSortObj.normalTreeObj = $.fn.zTree.init($("#bills_top_nodes"), sortingTreeSetting, fixed_top_bills_nodes);
+        } else {
+            //自定义
+            $("#div_sort_self_define")[0].style.display = "";
+            $("#selfDefineSort")[0].value = "";
+            me.currentNode[JV.PROP_SORT_TYPE_SELF_DEFINE_LOGIC] = '';
         }
         me.currentNode[JV.PROP_SORT_TYPE] = dom.value;
     },
+    changeSelfDefineExpression: function (dom) {
+        let me = preHandleObj;
+        if (me.currentNode) {
+            me.currentNode[JV.PROP_SORT_TYPE_SELF_DEFINE_LOGIC] = dom.value;
+        }
+    },
     extractTabFields: function (handleObj) {
         let me = this, rst = {};
         rst[JV.PROP_HANDLE_TYPE] = handleObj[JV.PROP_HANDLE_TYPE];
@@ -760,6 +775,7 @@ let preHandleSortObj = {
                 break;
             case 3 :
                 //self define
+                rst[JV.PROP_SORT_TYPE_SELF_DEFINE_LOGIC] = handleObj[JV.PROP_SORT_TYPE_SELF_DEFINE_LOGIC];
                 break;
             default:
                 break;

+ 956 - 0
web/over_write/js/chongqing_2018_price_crawler.js

@@ -0,0 +1,956 @@
+/**
+ * @author vian
+ * 重庆材料信息价爬虫
+ * 由于headless chrome “puppeteer”占用资源比较大,且材料信息价的数据是ssr的静态内容,因此不需要使用puppeteer。
+ * 数据获取使用cheerio(解析html,可用类jquery语法操作生成的数据)
+ */
+
+module.exports = {
+    crawlData,
+};
+
+const cheerio = require('cheerio');
+const axios = require('axios');
+const querystring = require('querystring');
+const uuidV1 = require('uuid/v1');
+const mongoose = require('mongoose');
+const { isDef } = require('../../../public/common_util');
+const { SSL_OP_SSLEAY_080_CLIENT_DH_BUG } = require('constants');
+
+const compilationModel = mongoose.model('compilation');
+const priceInfoLibModel = mongoose.model('std_price_info_lib');
+const priceInfoClassModel = mongoose.model('std_price_info_class');
+const priceInfoItemModel = mongoose.model('std_price_info_items');
+const priceInfoAreaModel = mongoose.model('std_price_info_areas');
+
+const isDebug = true;
+
+function debugConsole(str, type = 'log') {
+    if (isDebug) {
+        console[type](str);
+    }
+}
+
+// 页面类型
+const PageType = {
+    GENERAL: '/Index.aspx',
+    AREA: '/AreaIndex.aspx',
+    MIXED: '/ReadyMixedIndex.aspx',
+};
+
+/**
+ * 获取主要材料信息价格页面表单数据
+ * @param {Object} $ - 页面内容
+ * @param {Object} props - 提交属性
+ */
+function getGeneralDataBody($, props) {
+    const body = {
+        __EVENTTARGET: props.eventTarget || '',
+        __EVENTARGUMENT: '',
+        __VIEWSTATE: $('#__VIEWSTATE').val(),
+        __VIEWSTATEGENERATOR: $('#__VIEWSTATEGENERATOR').val(),
+        ID_ucPrice$linkvv: props.period,
+        ID_ucPrice$linkcategory: props.materialClass || '',
+        ID_ucPrice$LinkValue: `${props.classID},${props.period},${props.materialClass || ''}`,
+        ID_ucPrice$txtsonclass: `sonclass${props.classID}`,
+        ID_ucPrice$txtfatherclass: $('#ID_ucPrice_txtfatherclass').val(),
+        ID_ucPrice$txtClassId: props.classID || '',
+        ID_ucPrice$ddlSearchYear: '请选择',
+        ID_ucPrice$ddlSearchMonth: '请选择',
+        ID_ucPrice$txtSearchCailiao: '',
+        ID_ucPrice$UcPager1$listPage: props.page && String(props.page) || '1',
+    };
+    if (!props.eventTarget) {
+        body.ID_ucPrice$btnLink = $('#ID_ucPrice_btnLink').val();
+    }
+    return body;
+}
+
+/**
+ * 获取各区县地方材料工地价格页面表单数据
+ * @param {Object} $ - 页面内容
+ * @param {Object} props - 提交属性
+ */
+function getAreaDataBody($, props) {
+    if (!props || !Object.keys(props).length) {
+        return {};
+    }
+    const body = {
+        __EVENTTARGET: props.eventTarget || '',
+        __EVENTARGUMENT: '',
+        __VIEWSTATE: $('#__VIEWSTATE').val(),
+        __VIEWSTATEGENERATOR: $('#__VIEWSTATEGENERATOR').val(),
+        ID_ucAreaPrice$linkvv: props.period,
+        ID_ucAreaPrice$LinkValue: '',
+        ID_ucAreaPrice$dropArea: 'code',
+        ID_ucAreaPrice$txtSearchCailiao: '',
+        ID_ucAreaPrice$UcPager1$listPage: props.page && String(props.page) || '1',
+    };
+    if (!props.eventTarget) {
+        body.ID_ucAreaPrice$btnAreaMaster = 'Button';
+    }
+    return body;
+}
+
+/**
+ * 获取预拌砂浆信息价格页面表单数据
+ * @param {Object} $ - 页面内容
+ * @param {Object} props - 提交属性
+ */
+function getMixedDataBody($, props) {
+    if (!props || !Object.keys(props).length) {
+        return {};
+    }
+    const body = {
+        __EVENTTARGET: props.eventTarget || '',
+        __EVENTARGUMENT: '',
+        __VIEWSTATE: $('#__VIEWSTATE').val(),
+        __VIEWSTATEGENERATOR: $('#__VIEWSTATEGENERATOR').val(),
+        ID_ucReadyMixedPrice$linkvv: props.period,
+        ID_ucReadyMixedPrice$LinkValue: '',
+        ID_ucReadyMixedPrice$dropArea: 'code',
+        ID_ucReadyMixedPrice$txtSearchCailiao: '',
+        ID_ucReadyMixedPrice$UcPager1$listPage: props.page && String(props.page) || '1',
+    };
+    if (!props.eventTarget) {
+        body.ID_ucReadyMixedPrice$btnAreaMaster = 'Button';
+    }
+    return body;
+}
+
+// 获取提交
+
+const TIME_OUT = 60000;
+
+// 创建axios实例
+const axiosInstance = axios.create({
+    baseURL: 'http://www.cqsgczjxx.org/Jgxx/',
+    timeout: TIME_OUT,
+    /* proxy: {
+        host: "127.0.0.1", port: "8888" // Fiddler抓包,需要打开Fiddler否则会报connect error
+    }, */
+    headers: {
+        'Cache-Control': 'max-age=0',
+        'Content-Type': 'application/x-www-form-urlencoded',
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36',
+        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
+        'Accept-Encoding': 'gzip, deflate',
+        'Accept-Language': 'zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7,zh-CN;q=0.6',
+    },
+    responseType: 'document'
+});
+
+// 响应拦截器
+axiosInstance.interceptors.response.use(function (response) {
+    return response;
+}, function (error) {
+    // 对响应错误做点什么
+    if (error.message.includes('timeout')) {
+        return Promise.reject(`目标网络超时,请稍后再试。(${TIME_OUT}ms)`);
+    } else {
+        return Promise.reject(error);
+    }
+});
+
+// 发起请求需要携带Cookie,否则一些请求会返回500错误(应该是网站的反爬措施)
+let curCookie = '';
+
+/**
+ * 加载页面,获取可用类jquery操作的数据
+ * @param {String} url - 拼接的url
+ * @param {Object} body - 表单数据
+ * @return {DOM-LIKE} - cheerio解析html得到的类dom数据
+ */
+async function loadPage(url, body) {
+    const config = {};
+    if (curCookie) {
+        config.headers = { Cookie: curCookie };
+    }
+    const rst = body ?
+        await axiosInstance.post(url, querystring.stringify(body), config) :
+        await axiosInstance.post(url, null, config);
+    // 更新cookie
+    const cookies = rst.headers['set-cookie'];
+    if (Object.prototype.toString.call(cookies) === '[object Array]') {
+        curCookie = cookies[0].split(';')[0];
+    }
+    return cheerio.load(rst.data);
+
+}
+
+const monthMap = {
+    '1': '01月',
+    '2': '02月',
+    '3': '03月',
+    '4': '04月',
+    '5': '05月',
+    '6': '06月',
+    '7': '07月',
+    '8': '08月',
+    '9': '09月',
+    '10': '10月',
+    '11': '11月',
+    '12': '12月',
+};
+
+/**
+ * 获取期数数据
+ * @param {String} from - 从哪一期开始 eg: 2020-01
+ * @param {String} to - 从哪一期结束 eg: 2020-05
+ * @param {Object} $index - cheerio加载的初始页面内容
+ * @return {Array<object> || Null} eg: {period: '2020-05', uid: 'XCCXXXXX-XX'}
+ */
+function getPeriodData(from, to, $index) {
+    if (from > to) {
+        return null;
+    }
+    const $period = $index('#PriceLMenu')
+    // 根据区间获取期数列表
+    const reg = /(\d+)-(\d+)/;
+    const fromMatch = from.match(reg);
+    const fromYear = +fromMatch[1];
+    const fromMonth = +fromMatch[2];
+    const toMatch = to.match(reg);
+    const toYear = +toMatch[1];
+    const toMonth = +toMatch[2];
+    let curYear = fromYear;
+    let curMonth = fromMonth;
+    const list = [];
+    while (curYear <= toYear && curMonth <= toMonth) {
+        const uid = getPeriodUID(curYear, curMonth, $period);
+        // 存在无效期数,直接返回空
+        if (!uid) {
+            return null;
+        }
+        list.push({
+            period: `${curYear}年-${monthMap[curMonth]}`,
+            uid
+        });
+        if (curMonth === 12) {
+            curYear++;
+            curMonth = 1;
+        } else {
+            curMonth++;
+        }
+    }
+    return list;
+
+    function getPeriodUID(year, month, $period) {
+        const $year = $period.find('.MenuOneTitle').filter(function () {
+            return $index(this).text() === `${year}年`;
+        });
+        if (!$year.length) {
+            return null;
+        }
+        const $month = $year.parent().next().find('a').filter(function () {
+            return $index(this).text() === `${month}月`;
+        });
+        if (!$month.length) {
+            return null;
+        }
+        // 期数uid在onclick中,需要提取出来
+        const onclickText = $month.attr('onclick').toString();
+        const reg = /Onlink\('([^']+)'/;
+        const matched = onclickText.match(reg);
+        if (!matched || !matched[1]) {
+            return null;
+        }
+        return matched[1];
+    }
+}
+
+// 表格类型
+const TableType = {
+    BUILDING: 1, // 主要材料中的建安工程材料和绿色
+    GARDEN: 2, // 主要材料中的园林绿化
+    ENERGY: 3, // 主要材料中的节能建筑工程材料
+    AREA: 4, // 地区相关(各区县材料)
+    MIXED: 5, // 地区相关(预拌砂浆)
+};
+
+/**
+ * 爬取表格数据
+ * @param {Object} $page - 页面内容
+ * @param {Number} type - 表格类型
+ * @return {Array<object>}
+ */
+function crawlTableData($page, type) {
+    switch (type) {
+        case TableType.BUILDING:
+        case TableType.ENERGY:
+            return crawlNormalTable($page);
+        case TableType.GARDEN:
+            return crawlGardenTable($page);
+        case TableType.AREA:
+            return crawlAreaTable($page, '#ID_ucAreaPrice_gridView');
+        case TableType.MIXED:
+            return crawlAreaTable($page, '#ID_ucReadyMixedPrice_gridView');
+    }
+    return [];
+}
+
+/**
+ * 爬取表格数据,表格列为:
+ * 序号	| 材料名称 | 规格型号 | 单位 | 含税价(元) | 不含税价(元) | 备注
+ * @param {Object} $page - 页面内容
+ * @return {Array<object>}
+ */
+function crawlNormalTable($page) {
+    const colMap = {
+        0: 'name',
+        1: 'specs',
+        2: 'unit',
+        3: 'taxPrice',
+        4: 'noTaxPrice',
+        5: 'remark'
+    };
+    const data = [];
+    let cur;
+    const $tdList = $page('#ID_ucPrice_gridView').find('tr td span').filter(index => index % 7 !== 0); // 排除表头和序号列
+    $tdList.each(function (index) {
+        const col = index % 6;
+        if (col === 0) {
+            cur = {}
+        }
+        cur[colMap[col]] = $page(this).text();
+        if (col === 5) {
+            data.push(cur);
+        }
+    });
+    debugConsole(data);
+    return data;
+}
+/**
+ * 爬取表格数据,表格列为:
+ * 序号 | 科属 | 品名 | 高度(CM) | 干径(CM) | 冠径(CM) | 分枝高(CM) | 单位 | 含税价(元) | 不含税价(元) | 备注
+ * @param {Object} $page - 页面内容
+ * @return {Array<object>}
+ */
+function crawlGardenTable($page) {
+    const colMap = {
+        0: 'genera',
+        1: 'name',
+        2: 'height',
+        3: 'branchDiameter',
+        4: 'crownDiameter',
+        5: 'branchHeight',
+        6: 'unit',
+        7: 'taxPrice',
+        8: 'noTaxPrice',
+        9: 'remark',
+    };
+    const data = [];
+    let cur;
+    const $tdList = $page('#ID_ucPrice_gridView').find('tr td span').filter(index => index % 11 !== 0); // 排除表头和序号列
+    $tdList.each(function (index) {
+        const col = index % 10;
+        if (col === 0) {
+            cur = {}
+        }
+        cur[colMap[col]] = $page(this).text();
+        if (col === 9) {
+            data.push(cur);
+        }
+    });
+    debugConsole(data);
+    return data;
+}
+/**
+ * 爬取表格数据,表格列为:
+ * 序号 | 所属区县 | 材料名称 | 规格及型号 | 计量单位 | 含税价(元) | 不含税价(元)
+ * @param {Object} $page - 页面内容
+ * @param {String} viewSelector - 表格选择器(ID)
+ * @return {Array<object>}
+ */
+function crawlAreaTable($page, viewSelector) {
+    const colMap = {
+        0: 'area',
+        1: 'name',
+        2: 'specs',
+        3: 'unit',
+        4: 'taxPrice',
+        5: 'noTaxPrice',
+    };
+    const data = [];
+    let cur;
+    const $tdList = $page(viewSelector).find('tr td span').filter(index => index % 7 !== 0); // 排除表头和序号列
+    $tdList.each(function (index) {
+        const col = index % 6;
+        if (col === 0) {
+            cur = {}
+        }
+        cur[colMap[col]] = $page(this).text();
+        if (col === 5) {
+            data.push(cur);
+        }
+    });
+    debugConsole(data);
+    return data;
+}
+
+// 事件触发类型
+const EventTarget = {
+    GENERAL_NEXT: 'ID_ucPrice$UcPager1$btnNext',
+    AREA_NEXT: 'ID_ucAreaPrice$UcPager1$btnNext',
+    MIXED_NEXT: 'ID_ucReadyMixedPrice_UcPager1_btnNext',
+};
+
+/**
+ * 爬取一页一页的表格数据
+ * @param {Object} $index - 索引页面内容
+ * @param {Object} props - 提交的表单内容
+ * @param {String} pageType - 页面类型
+ * @param {Number} tableType - 表格类型
+ */
+async function crawlPagesData($index, props, pageType, tableType) {
+    let body;
+    let pageStateSelector;
+    if (pageType === PageType.GENERAL) {
+        body = getGeneralDataBody($index, props);
+        pageStateSelector = '#ID_ucPrice_UcPager1_lbPage';
+    } else if (pageType === PageType.AREA) {
+        body = getAreaDataBody($index, props);
+        pageStateSelector = '#ID_ucAreaPrice_UcPager1_lbPage';
+    } else {
+        body = getMixedDataBody($index, props);
+        pageStateSelector = '#ID_ucReadyMixedPrice_UcPager1_lbPage';
+    }
+    const $firstPage = await loadPage(pageType, body);
+    const rst = [];
+    // 获取第一页数据
+    rst.push(...crawlTableData($firstPage, tableType));
+    if (!rst.length) { // 第一页都没数据,后续不需要操作了
+        return rst;
+    }
+    // 获取除第一页的数据
+    // 获取页码
+    const pageState = $firstPage(pageStateSelector).text(); // eg: 1/10
+    const totalPage = +pageState.split('/')[1];
+    const asyncCount = 6; // 最高批量次数
+    let curCount = 0;
+    let task = [];
+    for (let page = 1; page < totalPage; page++) {
+        task.push(crawlPageData(page));
+        curCount++;
+        if (curCount === asyncCount) {
+            const allData = await Promise.all(task);
+            allData.forEach(data => rst.push(...data));
+            curCount = 0;
+            task = [];
+        }
+    }
+    if (task.length) {
+        const allData = await Promise.all(task);
+        allData.forEach(data => rst.push(...data));
+    }
+    return rst;
+
+    // 爬取页码数据
+    async function crawlPageData(page) {
+        const pageProps = { ...props, page };
+        let body;
+        if (pageType === PageType.GENERAL) {
+            pageProps.eventTarget = EventTarget.GENERAL_NEXT;
+            body = getGeneralDataBody($firstPage, pageProps);
+        } else if (pageType === PageType.AREA) {
+            pageProps.eventTarget = EventTarget.AREA_NEXT;
+            body = getAreaDataBody($firstPage, pageProps);
+        } else {
+            pageProps.eventTarget = EventTarget.MIXED_NEXT;
+            body = getMixedDataBody($firstPage, pageProps);
+        }
+        const $page = await loadPage(pageType, body);
+        return crawlTableData($page, tableType);
+    }
+}
+
+/**
+ * 爬取建安工程材料和绿色、园林绿化工程材料、节能建筑工程材料
+ * @param {String} period - 期数uid
+ * @param {String} classID - 工程分类id 
+ * @param {Object} $index - 初始页面内容
+ * @param {Number} type - 表格类型
+ * @return {Array<object>} eg: [{ materialClass: '一、黑色及有色金属', items: [...] }]
+ */
+async function crawlGeneralSubData(period, classID, $index, type) {
+    const body = getGeneralDataBody($index, { period, classID });
+    const $engineeringClassPage = await loadPage(PageType.GENERAL, body);
+    const rst = [];
+    if (type === TableType.BUILDING) {
+        const classList = crawlMaterialClassList($index('#ID_ucPrice_CategoryLabel'));
+        if (!classList.length) {
+            throw '无法爬取到材料分类。';
+        }
+        const reg = /[一二三四五六七八九十]+、/;
+        for (const materialClass of classList) {
+            const obj = { materialClass: materialClass.replace(reg, ''), items: [] }; // 材料分类去除序号
+            obj.items = await crawlPagesData($engineeringClassPage, { period, classID, materialClass }, PageType.GENERAL, type);
+            rst.push(obj);
+        }
+    } else {
+        const items = await crawlPagesData($engineeringClassPage, { period, classID, materialClass: '' }, PageType.GENERAL, type);
+        rst.push(...items);
+    }
+    return rst;
+
+    // 爬取材料分类表
+    function crawlMaterialClassList($class) {
+        const list = [];
+        $class.find('a').each(function () {
+            const text = $engineeringClassPage(this).text();
+            list.push(text);
+        });
+        return list;
+    }
+}
+
+
+/**
+ * 爬取主要材料信息价格(这部分作为通用库)
+ * @param {String} period - 期数uid
+ * @param {Object} $index - 初始页面内容
+ * @return {Object}
+ */
+async function crawlGeneralData(period, $index) {
+    const { building, garden, energy } = crawlClass($index('#ID_ucPrice_tabNewBar'));
+    const rst = {};
+    if (building) {
+        rst.building = await crawlGeneralSubData(period, building, $index, TableType.BUILDING);
+    }
+    if (garden) {
+        // 园林绿化工程材料下的数据所属分类为数据的"科属"列
+        rst.garden = await crawlGeneralSubData(period, garden, $index, TableType.GARDEN);
+    }
+    if (energy) {
+        // 绿色、节能建筑工程材料下的所有数据,所属分类均为“绿色、节能建筑工程材料”。
+        rst.energy = await crawlGeneralSubData(period, energy, $index, TableType.ENERGY);
+    }
+    return rst;
+
+    // 爬取工程分类
+    function crawlClass($class) {
+        // 工程分类
+        let building; // 建安工程材料
+        let garden; // 园林绿化工程材料
+        let energy; // 绿色、节能建筑工程材料
+        const reg = /OnClassson\('([^']+)'/;
+        $class.find('a').each(function () {
+            const text = $index(this).text();
+            const onclickText = $index(this).attr('onclick').toString();
+            const matched = onclickText.match(reg);
+            if (!matched || !matched[1]) {
+                throw '无法爬取到工程分类。';
+            }
+            if (text === '建安工程材料') {
+                building = matched[1];
+            } else if (text === '园林绿化工程材料') {
+                garden = matched[1];
+            } else if (text === '绿色、节能建筑工程材料') {
+                energy = matched[1];
+            }
+        });
+        return { building, garden, energy };
+    }
+}
+
+/**
+ * 爬取各区县地方材料工地价格
+ * @param {String} period - 期数uid
+ * @return {Array<object>}
+ */
+async function crawlAreaData(period) {
+    // 获取各区材料初始页
+    const $index = await loadPage(PageType.AREA);
+    // 获取地区材料
+    return await crawlPagesData($index, { period }, PageType.AREA, TableType.AREA);
+}
+
+/**
+ * 爬取预拌砂浆信息价格
+ * @param {String} period - 期数uid
+ * @return {Array<object>}
+ */
+async function crawlMixedData(period) {
+    // 获取各区材料初始页
+    const $index = await loadPage(PageType.MIXED);
+    // 获取地区材料
+    return await crawlPagesData($index, { period }, PageType.MIXED, TableType.MIXED);
+}
+
+/**
+ * 转换价格数据(一条源数据可能需要分割成多条数据)
+ * @param {String} libID - 库ID
+ * @param {String} classID - 所属分类ID
+ * @param {String} period - 期数 eg:2020年01月
+ * @param {String} areaID - 地区ID
+ * @param {String} compilationID - 费用定额ID
+ * @param {Array<object>} items - 爬取的信息价源数据
+ * @param {Number} tableType - 表格类型
+ * @return {Array<obejct>}
+ */
+function transformPriceItems(libID, classID, period, areaID, compilationID, items, tableType) {
+    const rst = [];
+    if (tableType === TableType.GARDEN) {
+        // 有的数据 高度(CM) | 干径(CM) | 冠径(CM) | 分枝高(CM) | 不含税价(元) = ‘’ | 14-17 | 大于400 | 200-300 | 430-780
+        // 则此数据需要分为:
+        // 1. { name: 名称-最低价, specs: 干径14-17CM 冠径大于400CM 分枝高200-300CM, noTaxPrice: 430 }
+        // 2. { name: 名称-最高价, specs: 干径14-17CM 冠径大于400CM 分枝高200-300CM, noTaxPrice: 780 }
+        const unit = 'CM';
+        const duplicateReg = /-/;
+        items.forEach(item => {
+            // 拼接规格型号
+            const specsList = [];
+            if (item.height) {
+                specsList.push(`高度${item.height}${unit}`);
+            }
+            if (item.branchDiameter) {
+                specsList.push(`干径${item.branchDiameter}${unit}`);
+            }
+            if (item.crownDiameter) {
+                specsList.push(`冠径${item.crownDiameter}${unit}`);
+            }
+            if (item.branchHeight) {
+                specsList.push(`分枝高${item.branchHeight}${unit}`);
+            }
+            const specs = specsList.join(' ');
+            // 分成最高低价最高价数据
+            const isDuplicate = duplicateReg.test(item.taxPrice) || duplicateReg.test(item.noTaxPrice);
+            if (isDuplicate) {
+                const taxPriceList = item.taxPrice.split('-');
+                const noTaxPriceList = item.noTaxPrice.split('-');
+                const minItem = {
+                    ...item,
+                    name: `${item.name}-最低价`,
+                    specs,
+                    taxPrice: taxPriceList[0],
+                    noTaxPrice: noTaxPriceList[0]
+                };
+                const maxItem = {
+                    ...item,
+                    name: `${item.name}-最高价`,
+                    specs,
+                    taxPrice: taxPriceList[1] || '',
+                    noTaxPrice: noTaxPriceList[1] || ''
+                };
+                rst.push(transfromPriceItem(libID, classID, period, areaID, compilationID, minItem));
+                rst.push(transfromPriceItem(libID, classID, period, areaID, compilationID, maxItem));
+            } else {
+                rst.push(transfromPriceItem(libID, classID, period, areaID, compilationID, item));
+            }
+        })
+    } else {
+        const duplicateReg = /\//;
+        // 有的数据:规格型号 | 含税价(元) | 不含税价(元) = φ6(6.5)/φ8 HPB300 | 4030.00/3880.00 | 3566.37/3433.63,则这条数据需要分成两条数据
+        items.forEach(item => {
+            item.taxPrice = item.taxPrice === '-' ? '' : item.taxPrice;
+            item.noTaxPrice = item.noTaxPrice === '-' ? '' : item.noTaxPrice;
+            const isDuplicate = duplicateReg.test(item.taxPrice) || duplicateReg.test(item.noTaxPrice); // 以价格被分割,作为数据需要分割的判断
+            if (isDuplicate) {
+                // 提取规格型号分割部分和公共部分:Q390/Q420 δ=20-30 => Q390 δ=20-30; Q420 δ=20-30
+                // 获取公共规格型号部分
+                const commonReg = /\s+([^/]*)$/;
+                const commonMatched = item.specs.match(commonReg);
+                const commonSpecs = commonMatched && commonMatched[1] ? ' ' + commonMatched[1] : '';
+                // 获取分割规格型号
+                const specsList = item.specs
+                    .replace(commonReg, '')
+                    .split('/');
+                const taxPriceList = item.taxPrice.split('/');
+                const noTaxPriceList = item.noTaxPrice.split('/');
+                specsList.forEach((specs, index) => {
+                    const newItem = {
+                        ...item,
+                        specs: `${specs}${commonSpecs}`,
+                        taxPrice: taxPriceList[index] || taxPriceList[0],
+                        noTaxPrice: noTaxPriceList[index] || noTaxPriceList[0]
+                    };
+                    if (areaID) {
+                        newItem.areaID = areaID;
+                    }
+                    rst.push(transfromPriceItem(libID, classID, period, areaID, compilationID, newItem));
+                });
+            } else {
+                rst.push(transfromPriceItem(libID, classID, period, areaID, compilationID, item));
+            }
+        });
+    }
+    return rst;
+}
+
+// 转换单条的价格数据
+function transfromPriceItem(libID, classID, period, areaID, compilationID, item) {
+    // 源数据中的规格型号存在多个无意义的空格,合并为一个
+    const reg = /\s{2,}/g;
+    item.specs = item.specs ? item.specs.replace(reg, ' ') : '';
+    return {
+        ID: uuidV1(),
+        libID,
+        classID,
+        code: '',
+        name: item.name,
+        specs: item.specs,
+        unit: item.unit,
+        taxPrice: item.taxPrice,
+        noTaxPrice: item.noTaxPrice,
+        remark: item.remark || '',
+        // 以下冗余数据为方便前台信息价功能处理
+        period,
+        areaID,
+        compilationID,
+    }
+}
+
+/**
+ * 转换主要材料
+ * @param {String} period - 日期: 2020年01月
+ * @param {String} compilationID - 费用定额ID
+ * @param {Object} generalData - 主要材料{ building, garden, energy }
+ * @return {Object} { libData, classData, priceData, compilationAreas }
+ */
+async function transfromGeneralData(period, compilationID, generalData) {
+    const area = '通用';
+    // 爬取数据的时候,地区数据先匹配名称,如果费用定额已有此地区,不新增
+    const matchedArea = await priceInfoAreaModel.findOne({ compilationID, name: area }).lean();
+    const areaID = matchedArea && matchedArea.ID || uuidV1();
+    const compilationAreas = [];
+    const libData = {
+        ID: uuidV1(),
+        name: `信息价(${period})`,
+        period,
+        areas: [],
+        compilationID,
+        createDate: Date.now(),
+    };
+    const classData = [];
+    let curClassIndex = 0;
+    const priceData = [];
+    const { building, garden, energy } = generalData;
+    handleClassAndItems(building, TableType.BUILDING);
+    // 园林分类数据为:苗木-科属(genera)
+    const gardenRoot = { materialClass: '苗木', treeData: { ID: uuidV1(), ParentID: '-1' } };
+    const gardenData = [gardenRoot];
+    garden.forEach(item => {
+        const pre = gardenData[gardenData.length - 1];
+        if (item.genera !== pre.materialClass) {
+            gardenData.push({ materialClass: item.genera, treeData: { ParentID: gardenRoot.treeData.ID }, items: [item] });
+        } else {
+            pre.items.push(item);
+        }
+    });
+    handleClassAndItems(gardenData, TableType.GARDEN)
+    // 绿色节能分类数据:绿色、节能建筑工程材料
+    const energyData = [{ materialClass: '绿色、节能建筑工程材料', items: energy }];
+    handleClassAndItems(energyData, TableType.ENERGY);
+    // 有数据才将地区push入areas中(费用定额共用)
+    if ((classData.length || priceData.length) && !matchedArea) {
+        compilationAreas.push({ compilationID, ID: areaID, name: area })
+    }
+    return { libData, classData, priceData, compilationAreas };
+
+    function handleClassAndItems(sourceData, tableType) {
+        if (!sourceData) {
+            return;
+        }
+        sourceData.forEach(({ materialClass, treeData, items }) => {
+            const classItem = {
+                ID: treeData && treeData.ID || uuidV1(),
+                ParentID: treeData && treeData.ParentID || '-1',
+                NextSiblingID: treeData && treeData.NextSiblingID || '-1',
+                name: materialClass,
+                libID: libData.ID,
+                areaID,
+            };
+            // 设置上一个节点数据的NextID
+            let count = 1;
+            let pre = classData[curClassIndex - 1];
+            while (pre && pre.ParentID !== classItem.ParentID) {
+                count++;
+                pre = classData[curClassIndex - count];
+            }
+            if (pre && pre.ParentID === classItem.ParentID) {
+                pre.NextSiblingID = classItem.ID;
+            }
+            curClassIndex++;
+            classData.push(classItem);
+            // 转换价格数据
+            if (items && items.length) {
+                const newItems = transformPriceItems(libData.ID, classItem.ID, period, areaID, compilationID, items, tableType);
+                newItems.forEach(item => priceData.push(item));
+            }
+        });
+    }
+}
+
+/**
+ * 转换跟地区相关的数据
+ * 地区作为期数库的子项
+ * @param {String} period - 日期: 2020年01月
+ * @param {String} compilationID - 费用定额ID
+ * @param {String} className - 分类名称 
+ * @param {Object} libData - 当前期数库数据
+ * @param {Array<object>} areaData - 各区县地方材料工地价格
+ * @param {Array<object>} mixedData - 预拌砂浆信息价格
+ * @return {Object}
+ */
+async function transformAreaData(period, compilationID, libData, areaData, mixedData) {
+    // 根据地区进行分类
+    const data = [];
+    const hashMap = {}; // 保证地区顺序跟网页爬取数据的顺序一致。(object for in无法保证顺序)
+    function hash(area) {
+        if (!isDef(hashMap[area])) {
+            hashMap[area] = Object.keys(hashMap).length
+        }
+        return hashMap[area];
+    }
+    const areaClass = '地方材料信息价';
+    const mixedClass = '预拌商品砂浆';
+    function buildData(sourceData) {
+        sourceData.forEach(item => {
+            const idx = hash(item.area);
+            if (!data[idx]) {
+                data[idx] = { area: item.area, subData: [] };
+            }
+            if (sourceData === areaData) {
+                // 存在地区数据,需要生成分类“地方材料信息价”
+                if (!data[idx].subData[0]) {
+                    data[idx].subData[0] = { className: areaClass, items: [] };
+                }
+                data[idx].subData[0].items.push(item);
+            } else if (sourceData === mixedData) {
+                // 存在地区数据,需要生成分类“地方材料信息价”
+                if (!data[idx].subData[1]) {
+                    data[idx].subData[1] = { className: mixedClass, items: [] };
+                }
+                data[idx].subData[1].items.push(item);
+            }
+        });
+    }
+    buildData(areaData);
+    buildData(mixedData);
+    const compilationAreas = [];
+    const classData = [];
+    const priceData = [];
+    for (const { area, subData } of data) {
+        const matchedArea = await priceInfoAreaModel.findOne({ compilationID, name: area }).lean();
+        const areaID = matchedArea && matchedArea.ID || uuidV1();
+        if (!matchedArea) {
+            compilationAreas.push({ compilationID, ID: areaID, name: area });
+        }
+        let preClass;
+        subData.forEach(subItem => {
+            if (!subItem) {
+                return;
+            }
+            const { className, items } = subItem;
+            const classItem = {
+                ID: uuidV1(),
+                ParentID: '-1',
+                NextSiblingID: '-1',
+                name: className,
+                libID: libData.ID,
+                areaID,
+            };
+            classData.push(classItem);
+            if (preClass) {
+                preClass.NextSiblingID = classItem.ID;
+            }
+            preClass = classItem;
+            const newItems = transformPriceItems(libData.ID, classItem.ID, period, areaID, compilationID, items, TableType.AREA);
+            newItems.forEach(item => priceData.push(item));
+        });
+    }
+    return { classData, priceData, compilationAreas };
+}
+
+/**
+ * 数据入库
+ * 生成一个通用库及各地区
+ * @param {String} period 期数 eg: '2020年05月'
+ * @param {Object} generalData - 主要材料{ building, garden, energy }
+ * @param {Array<object>} areaData - 各地区材料
+ * @param {Array<object>} mixedData - 各地区预拌砂浆
+ */
+async function save(period, generalData, areaData, mixedData) {
+    const overWriteUrl = '/web/over_write/js/chongqing_2018.js';
+    const compilation = await compilationModel.findOne({ overWriteUrl }, '_id').lean();
+    if (!compilation) {
+        throw '没有找到正确配置overWriteUrl的费用定额。';
+    }
+    const compilationID = compilation._id;
+    // 转换数据
+    const generalSaveData = await transfromGeneralData(period, compilationID, generalData);
+    const libData = generalSaveData.libData;
+    const areaSaveData = await transformAreaData(period, compilationID, libData, areaData, mixedData);
+    // 入库
+    const classData = [...generalSaveData.classData, ...areaSaveData.classData];
+    const priceData = [...generalSaveData.priceData, ...areaSaveData.priceData];
+    const compilationAreas = [...generalSaveData.compilationAreas, ...areaSaveData.compilationAreas]
+    // 删除已有的相同期数数据
+    const originalLibs = await priceInfoLibModel.find({ period }, '-_id ID').lean();
+    const originalLibIDList = originalLibs.reduce((acc, cur) => {
+        acc.push(cur.ID);
+        return acc;
+    }, []);
+    if (originalLibIDList.length) {
+        await priceInfoItemModel.deleteMany({ period });
+        await priceInfoClassModel.deleteMany({ libID: { $in: originalLibIDList } });
+        await priceInfoLibModel.deleteMany({ period });
+    }
+    // 插入数据
+    if (priceData.length) {
+        await priceInfoItemModel.insertMany(priceData);
+    }
+    if (classData.length) {
+        await priceInfoClassModel.insertMany(classData);
+    }
+    if (libData) {
+        await priceInfoLibModel.insertMany([libData]);
+    }
+    if (compilationAreas) {
+        await priceInfoAreaModel.insertMany(compilationAreas);
+    }
+}
+
+/**
+ * 爬取数据
+ * @param {String} from - 从哪一期开始 eg: 2020-01
+ * @param {String} to - 从哪一期结束 eg: 2020-05
+ * @return {Object}
+ */
+async function crawlData(from, to) {
+    let curPeriod;
+    try {
+        const $index = await loadPage(PageType.GENERAL);
+        const periodData = getPeriodData(from, to, $index);
+        if (!periodData) {
+            throw '无效的期数区间。';
+        }
+        // 一期一期爬取数据
+        debugConsole('allTime', 'time');
+        for (const periodItem of periodData) {
+            debugConsole('peroidTime', 'time');
+            // 爬取主要材料信息价格
+            const generalData = await crawlGeneralData(periodItem.uid, $index); // 初始页面就是主要材料信息价的页面
+            // 爬取各区县地方材料工地价格
+            const areaData = await crawlAreaData(periodItem.uid);
+            // 爬取预拌砂浆信息价格
+            const mixedData = await crawlMixedData(periodItem.uid);
+            // 转换数据并入库
+            await save(periodItem.period, generalData, areaData, mixedData);
+            curPeriod = periodItem.period;
+            debugConsole('peroidTime', 'timeEnd');
+        }
+        debugConsole('allTime', 'timeEnd');
+    } catch (err) {
+        console.log(err);
+        // 错误时提示已经成功爬取的期数
+        let errTip = '';
+        if (curPeriod) {
+            errTip += `\n成功爬取期数为:${from}到${curPeriod}`;
+        }
+        const errStr = String(err) + errTip;
+        console.log(`err`);
+        console.log(errStr);
+        throw errStr;
+    }
+}

+ 2 - 2
web/over_write/js/guangdong_2018.js

@@ -1,8 +1,8 @@
 'use strict';
-//允许使用的工料机类型:人工、普通材料、混凝土、砂浆、配合比、商品混凝土、商品砂浆
+//允许使用的工料机类型:人工、普通材料、其他材料费、混凝土、砂浆、配合比、商品混凝土、商品砂浆
 //机械台班、机上人工、机械组成物、主材、设备、企业管理费
 if(typeof allowGljType !== 'undefined'){
-    allowGljType = [1, 201, 202, 203, 204, 205, 206, 301, 302, 303, 4,5, 6];
+    allowGljType = [1, 201, 202, 203, 204, 205, 206, 207, 301, 302, 303, 4,5, 6];
 }
 if(typeof allowComponent !== 'undefined'){
     //允许含有组成物的工料机类型:混凝土、砂浆、配合比、机械台班、主材

+ 2 - 2
web/over_write/js/neimenggu_2017.js

@@ -8,9 +8,9 @@
  * @version
  */
 
-//允许使用的工料机类型:人工、普通材料、混凝土、砂浆、配合比、商品混凝土、商品砂浆、机械台班、机械组成物、机上人工、主材、设备、企业管理费、利润
+//允许使用的工料机类型:人工、普通材料、混凝土、砂浆、配合比、商品混凝土、商品砂浆、其他材料费、机械台班、机械组成物、机上人工、主材、设备、企业管理费、利润
 if(typeof allowGljType !== 'undefined'){
-    allowGljType = [1, 201, 202, 203, 204, 205, 206, 301, 302, 303, 4, 5, 6, 7];
+    allowGljType = [1, 201, 202, 203, 204, 205, 206, 207, 301, 302, 303, 4, 5, 6, 7];
 }
 if(typeof allowComponent !== 'undefined'){
     //允许含有组成物的工料机类型:混凝土、砂浆、配合比、机械台班、主材

+ 24 - 0
web/users/css/custom.css

@@ -18,4 +18,28 @@
 
 .btn-link:focus, .btn-link:hover{
   text-decoration: none
+}
+.highlight {
+  background: #eee;
+}
+.dragging {
+  opacity: .5;
+  background: #eee;
+}
+.cursor-default {
+  cursor: default;
+}
+.cursor-move {
+  cursor: move;
+}
+/* 不禁止的话,drag过程中经过子元素也会触发dragleave事件导致屏闪 */
+[draggable=true]:hover {
+  background: rgb(240, 240, 240);
+}
+[draggable=true] span{
+  pointer-events: none;
+}
+select.multiple {
+  min-height: 150px;
+  max-height: 300px;
 }

+ 129 - 22
web/users/js/compilation.js

@@ -134,33 +134,140 @@ $(document).ready(function() {
     });
 
     //新增定额库
-    $("#add-ration").click(function () {
-         let rationLib = $("select[name='ration_lib']").children("option:selected").val();
-         let rationLibString = $("select[name='ration_lib']").children("option:selected").text();
-         if(rationLib == undefined || rationLib ==''){
-             alert("请选择定额库");
-             return;
-         }
-        if($("input:hidden[name=ration_lib][data-id = "+rationLib+"]").length <= 0){
+    /* $("#add-ration").click(function () {
+        let rationLib = $("select[name='ration_lib']").children("option:selected").val();
+        let rationLibString = $("select[name='ration_lib']").children("option:selected").text();
+        if (rationLib == undefined || rationLib == '') {
+            alert("请选择定额库");
+            return;
+        }
+        if ($("input:hidden[name=ration_lib][data-id = " + rationLib + "]").length <= 0) {
             let tem = {
-                id:rationLib,
-                name:rationLibString,
-                isDefault:false
+                id: rationLib,
+                name: rationLibString,
+                isDefault: false
             };
             let htmlString = ` 
-                <tr class='ration_tr'>
-                     <td><span>${tem.name}</span></td>
-                     <td><label class="form-check-label"> <input class="form-check-input" name="ration_isDefault"  value="${tem.id}" type="radio"></td>  
-                     <td>
-                            <a class='btn btn-link btn-sm ' style="padding: 0px" onclick='deleteTableTr(this,"ration_tr")'>删除</a>
-                            <input type="hidden" name="ration_lib" data-id="${tem.id}" value='${JSON.stringify(tem)}'>
-                      </td>
+                <tr class='ration_tr' draggable="true">
+                    <td><span class="cursor-default">${tem.name}</span></td>
+                    <td><label class="form-check-label"> <input class="form-check-input" name="ration_isDefault"  value="${tem.id}" type="radio"></td>  
+                    <td>
+                        <a class='btn btn-link btn-sm ' style="padding: 0px" onclick='deleteTableTr(this,"ration_tr")'>删除</a>
+                        <input type="hidden" name="ration_lib" data-id="${tem.id}" value='${JSON.stringify(tem)}'>
+                    </td>
                 </tr>`
             $("#ration_tbody").append(htmlString);
-        }else {
+        } else {
             alert('已存在相同的定额库')
         }
         $("#addRation").modal('hide');
+    }); */
+    $("#add-ration").click(function () {
+        const options = $("select[name='ration_lib']").children("option:selected");
+        let alertArr = [];
+        let htmlString = '';
+        for (const option of options) {
+            const rationLib = $(option).val();
+            const rationLibString = $(option).text();
+            if (!rationLib) {
+                alertString.push(`“${rationLibString}”为无效定额库`);
+                continue;
+            }
+            if ($("input:hidden[name=ration_lib][data-id = " + rationLib + "]").length > 0) {
+                alertArr.push(`“${rationLibString}”已存在`);
+                continue;
+            }
+            const tem = {
+                id: rationLib,
+                name: rationLibString,
+                isDefault: false
+            };
+            htmlString += ` 
+                <tr class='ration_tr' draggable="true">
+                    <td><span class="cursor-default">${tem.name}</span></td>
+                    <td><label class="form-check-label"> <input class="form-check-input" name="ration_isDefault"  value="${tem.id}" type="radio"></td>  
+                    <td>
+                        <a class='btn btn-link btn-sm ' style="padding: 0px" onclick='deleteTableTr(this,"ration_tr")'>删除</a>
+                        <input type="hidden" name="ration_lib" data-id="${tem.id}" value='${JSON.stringify(tem)}'>
+                    </td>
+                </tr>`;
+        }
+        if (alertArr.length) {
+            alert(alertArr.join('\n'));
+        } else {
+            $("#ration_tbody").append(htmlString);
+            $("#addRation").modal('hide');
+        }
+    });
+
+    // 复制定额库
+    $('#copy-lib-confirm').click(async function () {
+        try {
+            $.bootstrapLoading.start();
+            const [valuationID, engineeringID] = window.location.pathname.split('/').slice(-2);
+            await ajaxPost('/compilation/copyRationLibs', { valuationID, engineeringID });
+        } catch (err) {
+            console.log(err);
+        } finally {
+            $.bootstrapLoading.end();
+        }
+    });
+
+    // 拖动排序
+    const dragSelector = '.ration_tr[draggable=true]';
+    const rationBodySelector = '#ration_tbody';
+    const wrapper = $('.panel-content')[0];
+    let dragged;
+    let rID = null;
+    const scrollStep = 6;
+    // 表格数据过多的时候,靠下方的条目想要移动到上方,需要滚动条滚动到相应位置,滚动条向上滚动需要代码自行处理
+    function scroll(ele, step) {
+        wrapper.scrollTop -= step;
+        rID = window.requestAnimationFrame(() => {
+            scroll(ele, step);
+        });
+    }
+    // 动态绑定(新增的也能监听到)
+    $(rationBodySelector).on('drag', dragSelector, function (ev) {
+        const { clientX, clientY } = ev;
+        const dom = document.elementFromPoint(clientX, clientY);
+        if (dom.tagName === 'H2' && !rID) {
+            rID = window.requestAnimationFrame(() => {
+                scroll(wrapper, scrollStep);
+            })
+        } else if (dom.tagName !== 'H2' && rID) {
+            window.cancelAnimationFrame(rID);
+            rID = null;
+        }
+    });
+    $(rationBodySelector).on('dragstart', dragSelector, function (ev) {
+        dragged = this;
+        $(this).addClass('dragging');
+        ev.originalEvent.dataTransfer.effectAllowed = 'move';
+    });
+    $(rationBodySelector).on('dragend', dragSelector, function (ev) {
+        $(this).removeClass('dragging');
+        if (rID) {
+            window.cancelAnimationFrame(rID);
+            rID = null;
+        }
+    });
+    $(rationBodySelector).on('dragover', dragSelector, function (ev) {
+        ev.preventDefault(); // 必须调用此方法,否则drop事件不触发
+    });
+    $(rationBodySelector).on('dragenter', dragSelector, function (ev) {
+        if (this !== dragged) {
+            $(this).addClass('highlight');
+        }
+    });
+    $(rationBodySelector).on('dragleave', dragSelector, function (ev) {
+        if (this !== dragged) {
+            $(this).removeClass('highlight');
+        }
+    });
+    $(rationBodySelector).on('drop', dragSelector, function (ev) {
+        $(this).removeClass('highlight');
+        $(this).after($(dragged));
     });
 
     // 新增计价规则
@@ -502,9 +609,9 @@ function initCompilation() {
         TREE_SHEET_HELPER.showTreeData(mainTreeColObj, colSpread.getActiveSheet(), billsTemplateTree);
     }*/
 
-    if (billListData.length <= 0 || rationLibData.length <= 0 || gljLibData.length <= 0) {
+    /* if (billListData.length <= 0 || rationLibData.length <= 0 || gljLibData.length <= 0) {
         return false;
-    }
+    } */
 
     // 标准清单
     let html = '';
@@ -520,7 +627,7 @@ function initCompilation() {
         let tmpHtml = '<option value="' + tmp.id + '">' + tmp.name + '</option>';
         html += tmpHtml;
     }
-    $("select[name='ration_lib']").children("option").first().after(html);
+    $("select[name='ration_lib']").html(html);
 
     // 工料机库
     html = '';

+ 13 - 13
web/users/js/system.js

@@ -1,20 +1,20 @@
 /**
  * Created by zhang on 2020/1/2.
  */
-$(document).ready(function() {
-   $("#system_save").click(function () {
-       for(let ele of $("input")){
-           if(ele.name == "ID") continue;
-           if(!isNum($(ele).val())) return alert($(ele).parent().prevAll("legend").text() + $(ele).prev().text()+"输入的数据类型有误,请重新输入!");
-       }
-       $("form").submit();
-   });
+$(document).ready(function () {
+    $("#system_save").click(function () {
+        for (let ele of $("input")) {
+            if (ele.name == "ID") continue;
+            if ($(ele).attr('type') === 'number' && !isNum($(ele).val())) return alert($(ele).parent().prevAll("legend").text() + $(ele).prev().text() + "输入的数据类型有误,请重新输入!");
+        }
+        $("form").submit();
+    });
 
-   function isNum(thisValue){
-       var regPos = /^\d+(\.\d+)?$/; //非负浮点数
-       var regNeg = /^(-(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*)))$/; //负浮点数
-       return (regPos.test(thisValue) || regNeg.test(thisValue));
-   }
+    function isNum(thisValue) {
+        var regPos = /^\d+(\.\d+)?$/; //非负浮点数
+        var regNeg = /^(-(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*)))$/; //负浮点数
+        return (regPos.test(thisValue) || regNeg.test(thisValue));
+    }
 
 });
 

+ 3 - 2
web/users/views/compilation/engineering.html

@@ -294,6 +294,7 @@
                     <div class="col-md-12" style="padding-top:20px">
                             <legend>定额库</legend>
                             <a data-toggle="modal" data-target="#addRation" class="btn btn-link btn-sm " id="addRatioBtn" style="margin-right:5px">添加</a>
+                            <a data-toggle="modal" data-target="#copy-lib" class="btn btn-link btn-sm " style="margin-right:5px">复制到</a>
                             <table class="table engineer_table">
                                 <thead>
                                 <tr>
@@ -305,9 +306,9 @@
                                 <tbody id="ration_tbody">
                                 <% if (Object.keys(libData).length > 0 && libData.ration_lib.length > 0) { %>
                                 <% libData.ration_lib.forEach(function (ration, index){ %>
-                                <tr class='ration_tr'>
+                                <tr class='ration_tr' draggable="true">
                                     <td>
-                                        <span><%= ration.name%></span>
+                                        <span class="cursor-default"><%= ration.name%></span>
                                     </td>
                                     <td>
                                         <label class="form-check-label">

+ 24 - 2
web/users/views/compilation/modal.html

@@ -398,7 +398,7 @@
                     <label>定额库</label>
                     <div class="row">
                         <div class="col-xs-12">
-                            <select class="form-control" name="ration_lib">
+                            <select class="form-control multiple" name="ration_lib" multiple="multiple">
                                 <option value="">请选择定额库</option>
                             </select>
                         </div>
@@ -413,6 +413,28 @@
     </div>
 </div>
 
-
+<div class="modal fade in" id="copy-lib" data-backdrop="static">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">复制确认</h5>
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">×</span>
+                </button>
+            </div>
+            <div class="modal-body">
+                <div class="form-group">
+                    <div>
+                        <label>确认要复制定额库配置到当前费用定额下所有工程专业中?</label>
+                    </div>
+                </div>
+            </div>
+            <div class="modal-footer" style="justify-content: center">
+                <button type="button" class="btn btn-primary" data-dismiss="modal" id="copy-lib-confirm">是</button>
+                <button type="button" class="btn btn-primary" data-dismiss="modal">否</button>
+            </div>
+        </div>
+    </div>
+</div>
 
 <script type="text/javascript" src="/web/users/js/col_setting.js"></script>

+ 71 - 25
web/users/views/system/index.html

@@ -1,40 +1,86 @@
 <div class="panel-content">
     <div class="panel-title fluid">
-        <div class="title-main"><h2>系统设置<a href="javascript:void(0);" id="system_save" class="btn btn-primary btn-sm pull-right">确定修改</a></h2></div>
+        <div class="title-main">
+            <h2>系统设置<a href="javascript:void(0);" id="system_save" class="btn btn-primary btn-sm pull-right">确定修改</a>
+            </h2>
+        </div>
     </div>
     <div class="content-wrap">
-        <div class="c-header">
-            <h4>版本差异</h4>
-        </div>
-        <div class="c-body">
-            <form method="post" action="/system/save" enctype="application/x-www-form-urlencoded21">
+        <form method="post" action="/system/save" enctype="application/x-www-form-urlencoded21">
+            <div class="c-header">
+                <h4>产品信息</h4>
+            </div>
+            <div class="c-body">
                 <div class="row">
-                    <input type="hidden" name="ID" value="<%= setting.ID%>">
-                <div class="col-lg-4">
-                    <legend>免费版</legend>
-                    <div class="form-group">
-                        <label>单位工程可创建数量</label>
-                        <input type="number" step="10" min="50" class="form-control" name ="normal_project"  value="<%= setting.normal.project%>">
+                    <div class="form-group col-lg-4">
+                        <label>软件供应商</label>
+                        <% if (superAdmin === 1) { %>
+                            <input class="form-control" type="text" name="company" value="<%= setting.company %>">
+
+                        <% } else { %>
+                            <input class="form-control" type="text" name="company" value="<%= setting.company %>" disabled="disabled">
+
+                        <% } %>
+                    </div>
+                </div>
+                <div class="row">
+                    <div class="form-group col-lg-4">
+                        <label>名称</label>
+                        <% if (superAdmin === 1) { %>
+                            <input class="form-control" type="text" name="product" value="<%= setting.product %>">
+
+                        <% } else { %>
+                            <input class="form-control" type="text" name="product" value="<%= setting.product %>" disabled="disabled">
+
+                        <% } %>
                     </div>
-                    <div class="form-group">
-                        <label>定额可创建数量</label>
-                        <input type="number" step="10" min="500" class="form-control" name ="normal_ration"  value="<%= setting.normal.ration%>">
+                </div>
+                <div class="row">
+                    <div class="form-group col-lg-4">
+                        <label>版本号</label>
+                        <% if (superAdmin === 1) { %>
+                            <input class="form-control" type="text" value="<%= setting.version %>" name="version" placeholder="请输入产品版本号">
+                        <% } else { %>
+                                <input class="form-control" type="text" value="<%= setting.version %>" name="version" disabled="disabled">
+                        <% } %>
                     </div>
                 </div>
-                <div class="col-lg-4">
-                    <legend>专业版</legend>
-                    <div class="form-group">
-                        <label>单位工程可创建数量</label>
-                        <input type="number" step="10" min="50" class="form-control" name="professional_project" value="<%= setting.professional.project%>">
+            </div>
+            <div class="c-header">
+                <h4>版本差异</h4>
+            </div>
+            <div class="c-body">
+                <div class="row">
+                    <input type="hidden" name="ID" value="<%= setting.ID%>">
+                    <div class="col-lg-4">
+                        <legend>免费版</legend>
+                        <div class="form-group">
+                            <label>单位工程可创建数量</label>
+                            <input type="number" step="10" min="50" class="form-control" name="normal_project"
+                                value="<%= setting.normal.project%>">
+                        </div>
+                        <div class="form-group">
+                            <label>定额可创建数量</label>
+                            <input type="number" step="10" min="500" class="form-control" name="normal_ration"
+                                value="<%= setting.normal.ration%>">
+                        </div>
                     </div>
-                    <div class="form-group">
-                        <label>定额可创建数量</label>
-                        <input type="number" step="10" min="500"  class="form-control" name="professional_ration" value="<%= setting.professional.ration%>">
+                    <div class="col-lg-4">
+                        <legend>专业版</legend>
+                        <div class="form-group">
+                            <label>单位工程可创建数量</label>
+                            <input type="number" step="10" min="50" class="form-control" name="professional_project"
+                                value="<%= setting.professional.project%>">
+                        </div>
+                        <div class="form-group">
+                            <label>定额可创建数量</label>
+                            <input type="number" step="10" min="500" class="form-control" name="professional_ration"
+                                value="<%= setting.professional.ration%>">
+                        </div>
                     </div>
                 </div>
             </div>
-            </form>
-        </div>
+        </form>
     </div>
 </div>
 

+ 0 - 42
web/users/views/tool/index.html

@@ -18,8 +18,6 @@
                         <a id="uploadUserGuide" href="javascript:void(0);" class="btn btn-primary pull-right">上传</a>
                         <% } else if (tool.url === '/sysTools/api/uploadUpgradeGuide' ) { %>
                         <a id="uploadUpgradeGuide" href="javascript:void(0);" class="btn btn-primary pull-right">上传</a>
-                        <% } else if (tool.url === '/sysTools/api/changProductInfo') { %>
-                        <a id="productInfo" href="javascript:void(0);" class="btn btn-primary pull-right">更改</a>
                         <% } else { %>
                         <a id="<%= tool.controller %>" href="<%= tool.url %>" target="_blank" class="btn btn-primary pull-right">进入</a>
                         <% } %>
@@ -187,41 +185,6 @@
         </div>
     </div>
 </div>
-<!--产品信息-->
-<div class="modal fade" id="product" role="dialog" tabindex="-1" style="display: none;">
-    <div class="modal-dialog" role="document">
-        <div class="modal-content">
-            <div class="modal-header">
-                <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
-                <h4 class="modal-title">产品信息</h4>
-            </div>
-            <div class="modal-body">
-                <form id="changeInfoForm" method="post" action="/sysTools/api/changeProductInfo" enctype="application/x-www-form-urlencoded21">
-                    <div class="form-group">
-                        <label>软件供应商</label>
-                        <input class="form-control" type="text" value="<%= productInfo.company %>" name="version" disabled>
-                    </div>
-                    <div class="form-group">
-                        <label>名称</label>
-                        <input class="form-control" type="text" value="<%= productInfo.name %>" name="version" disabled>
-                    </div>
-                    <div class="form-group">
-                        <label>ICP</label>
-                        <input class="form-control" type="text" value="<%= productInfo.icp %>" name="version" disabled>
-                    </div>
-                    <div class="form-group">
-                        <label>版本号</label>
-                        <input class="form-control" type="text" value="<%= productInfo.version %>" name="version" placeholder="请输入产品版本号">
-                    </div>
-                </form>
-            </div>
-            <div class="modal-footer">
-                <button type="button" class="btn btn-default" data-dismiss="modal">关闭</button>
-                <button type="button" class="btn btn-primary" id="changeProductInfo">确定更改</button>
-            </div>
-        </div>
-    </div>
-</div>
 <script type="text/javascript" src="/public/web/common_ajax.js"></script>
 <script type="text/javascript" src="/public/web/PerfectLoad.js"></script>
 <script type="text/javascript">
@@ -343,10 +306,5 @@
                 }
             });
         });
-        //产品信息
-        $('#productInfo').click(() => $('#product').modal('show'));
-        $('#changeProductInfo').click(function () {
-            $('#changeInfoForm').submit();
-        });
     });
 </script>