Bläddra i källkod

feat: 增加信息价选项

zhangweicheng 2 år sedan
förälder
incheckning
836f792065

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

+ 11 - 0
modules/all_models/std_price_info_areas.js

@@ -0,0 +1,11 @@
+// 信息价地区(一个费用定额共用地区)
+const mongoose = require('mongoose');
+
+const Schema = mongoose.Schema;
+const priceInfoArea = new Schema({
+    serialNo: Number,
+    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');

+ 15 - 0
modules/all_models/std_price_info_index.js

@@ -0,0 +1,15 @@
+// 信息价类型
+const mongoose = require('mongoose');
+
+const Schema = mongoose.Schema;
+const priceInfoClass = new Schema({
+    ID: String,
+    code: String,//编码前4位
+    period: String, // 期数 eg: 2020-05
+    areaID: String,
+    compilationID: String, // 费用定额
+    index: Number//指数,保留两位小数
+}, {versionKey: false});
+priceInfoClass.index({ areaID:1});
+priceInfoClass.index({ period:1});
+mongoose.model('std_price_info_index', priceInfoClass, 'std_price_info_index');

+ 88 - 0
modules/all_models/std_price_info_items.js

@@ -0,0 +1,88 @@
+// 信息价数据
+const mongoose = require('mongoose');
+
+const Schema = mongoose.Schema;
+
+const keywordSchema = new Schema({
+    keyword: {
+        type: String,
+        default: ''
+    }, // 关键字
+    coe: {
+        type: String,
+        default: ''
+    }, // 系数(关键字效果)
+    unit: {
+        type: String,
+        default: ''
+    }, // 单位
+    group: {
+        type: String,
+        default: ''
+    }, // 组别
+    optionCode: {
+        type: String,
+        default: ''
+    }, // 选项号
+}, { _id: false });
+
+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: ''
+    },
+    // 别名编码
+    classCode: {
+        type: String,
+        default: ''
+    },
+    // 计算式
+    expString: {
+        type: String,
+        default: ''
+    },
+    // 月份、价格备注
+    dateRemark: {
+        type: String,
+        default: ''
+    },
+    // 关键字
+    keywordList: {
+        type: [keywordSchema],
+        default: []
+    }
+}, { versionKey: false });
+priceInfoItems.index({ areaID:1,period:1});
+
+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');

+ 55 - 0
modules/all_models/std_price_info_source.js

@@ -0,0 +1,55 @@
+// 信息价源数据(造价通接口源数据)
+const mongoose = require('mongoose');
+
+const Schema = mongoose.Schema;
+const priceInfoSourceItems = new Schema({
+    period: { // 期数 eg: 2020-05
+        type: String,
+        default: ''
+    },
+    area: { // eg: 广东-广州市-广州市。 不拆分成province、city等多个字段,是为了后续兼容性,不同省份的地区划分可能不同
+        type: String,
+        default: ''
+    },
+    industry: {
+        type: Number,
+        default: 1
+    },
+    subcid: { // 分类编号
+        type: String,
+        default: ''
+    },
+    code: {
+        type: String,
+        default: ''
+    },
+    name: {
+        type: String,
+        default: ''
+    },
+    specs: {
+        type: String,
+        default: ''
+    },
+    unit: {
+        type: String,
+        default: ''
+    },
+    price: {
+        type: String,
+        default: ''
+    },
+    taxPrice: {
+        type: String,
+        default: ''
+    },
+    noTaxPrice: {
+        type: String,
+        default: ''
+    },
+    remark: {
+        type: String,
+        default: ''
+    }
+}, { versionKey: false });
+mongoose.model('std_price_info_source', priceInfoSourceItems, 'std_price_info_source');

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

@@ -0,0 +1,269 @@
+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;
+                const importType = fields.importType !== undefined && fields.importType.length > 0 ?
+                    fields.importType[0] : null;
+                if (!libID || !importType) {
+                    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数据并入库
+                importType === 'originalData' ? await facade.importExcelData(libID, sheet[0].data) : await facade.importKeyData(libID, sheet[0].data, sheet[1].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 calcPriceIndex(req, res) {
+        try {
+            const { period, libID ,compilationID} = JSON.parse(req.body.data);
+            const areaID = '971fb9a0-0f93-11eb-b53c-45271c1df90f';//写死珠海地区
+            const data = await facade.calcPriceIndex(libID,period, areaID,compilationID);
+            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()
+};

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

@@ -0,0 +1,637 @@
+const mongoose = require('mongoose');
+const uuidV1 = require('uuid/v1');
+const _ = require('lodash');
+const scMathUtil = require('../../../public/scMathUtil').getUtil();
+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');
+const priceInfoIndexModel = mongoose.model('std_price_info_index');
+
+
+
+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/crawler/${crawlURL}`);
+        crawlData = crawler.crawlData;
+    } catch (e) {
+        console.log(e);
+        throw '该费用定额无可用爬虫方法。'
+    }
+    //await crawlData(from, to);
+    // 异步不等结果,结果由checking来获取
+    crawlDataByMiddleware(crawlData, from, to, compilationID);
+}
+
+// 爬取数据中间件,主要处理checking初始化
+async function crawlDataByMiddleware(crawlFunc, from, to, compilationID) {
+    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, compilationID);
+    } catch (err) {
+        console.log(err);
+        logUpdateData.errorMsg = String(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没有有效数据。'
+    }
+}
+
+// 导入excel关键字数据(主表+副表),目前只针对珠海,根据列号导入
+/* 
+主表:主从对应码	别名编码	材料名称	规格	单位	含税价(元)	除税价(元)	月份备注	计算式
+副表:主从对应码	关键字	单位	关键字效果	组别	选项号
+ */
+async function importKeyData(libID, mainData, subData) {
+    const lib = await priceInfoLibModel.findOne({ ID: libID }).lean();
+    if (!lib) {
+        throw new Error('库不存在');
+    }
+    const zh = await priceInfoAreaModel.findOne({ name: { $regex: '珠海' } }).lean();
+    if (!zh) {
+        throw new Error('该库不存在珠海地区');
+    }
+    // 删除珠海地区所有材料
+    await priceInfoItemModel.deleteMany({ libID, areaID: zh.ID });
+
+    const classItems = await priceInfoClassModel.find({ libID, areaID: zh.ID }).lean();
+    // 分类树前四位编码 - 分类节点ID映射表
+    let otherClassID = '';
+    const classMap = {};
+    classItems.forEach(item => {
+        if (item.name) {
+            if (!otherClassID && /其他/.test(item.name)) {
+                otherClassID = item.ID;
+            }
+            const code = item.name.substr(0, 4);
+            if (/\d{4}/.test(code)) {
+                classMap[code] = item.ID;
+            }
+        }
+    });
+
+    // 主从对应码 - 关键字数组映射
+    const keywordMap = {};
+    for (let row = 1; row < subData.length; row++) {
+        const rowData = subData[row];
+        const keywordItem = {
+            code: rowData[0] ? String(rowData[0]) : '',
+            keyword: rowData[1] || '',
+            unit: rowData[2] || '',
+            coe: rowData[3] || '',
+            group: rowData[4] || '',
+            optionCode: rowData[5] || '',
+        };
+        if (!keywordItem.code) {
+            continue;
+        }
+        (keywordMap[keywordItem.code] || (keywordMap[keywordItem.code] = [])).push(keywordItem);
+    }
+
+    const priceItems = [];
+    for (let row = 1; row < mainData.length; row++) {
+        const rowData = mainData[row];
+        const code = rowData[0] ? String(rowData[0]) : '';
+        if (!code) {
+            continue;
+        }
+        const matchCode = code.substring(0, 4);
+        const classID = classMap[matchCode] || otherClassID;
+        const priceItem = {
+            code,
+            libID,
+            classID,
+            ID: uuidV1(),
+            compilationID: lib.compilationID,
+            areaID: zh.ID,
+            period: lib.period,
+            classCode: rowData[1] || '',
+            name: rowData[2] || '',
+            specs: rowData[3] || '',
+            unit: rowData[4] || '',
+            taxPrice: rowData[5] || '',
+            noTaxPrice: rowData[6] || '',
+            dateRemark: rowData[7] || '',
+            expString: rowData[8] ||  '',
+            keywordList: keywordMap[code] || [],
+        }
+        priceItems.push(priceItem);
+    }
+    if (priceItems.length) {
+        await priceInfoItemModel.insertMany(priceItems);
+    }
+}
+
+/* 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 serialNo').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);
+    }
+}
+
+//计算指标平均值
+function calcIndexAvg (period, areaID,compilationID,preCodeMap){
+    const newData = []; 
+    for(const code in preCodeMap){
+        const indexArr = preCodeMap[code];
+        let total = 0;
+
+        for(const index of indexArr){
+            total  =   scMathUtil.roundForObj(total + index,2); 
+        }
+        const avg = scMathUtil.roundForObj(total/indexArr.length,2); 
+        newData.push({ID:uuidV1(),code,period,areaID,compilationID,index:avg})
+    }
+    return newData
+}
+
+//一个月里有classCode相同,但是价格不同的情况,取平均值
+function getClassCodePriceAvgMap  (items){
+    const classCodeMap = {};
+    for(const b of items){
+        classCodeMap[b.classCode]?classCodeMap[b.classCode].push(b):classCodeMap[b.classCode]=[b];
+    }
+
+    for(const classCode in classCodeMap){
+        const baseItems = classCodeMap[classCode];
+        const item = baseItems[0];
+        if(baseItems.length > 1){
+            let sum = 0;
+            for(const b of baseItems){
+                sum += parseFloat(b.noTaxPrice);       
+            }      
+            classCodeMap[classCode] = {code:item.code,name:item.name,price:scMathUtil.roundForObj(sum/baseItems.length,2)};
+        }else{
+            classCodeMap[classCode] = {code:item.code,name:item.name,price:parseFloat(item.noTaxPrice)}
+        }
+
+    }
+
+    return classCodeMap
+  
+}
+
+async function calcPriceIndex(libID,period, areaID,compilationID){
+    const baseItems = await priceInfoItemModel.find({areaID,period:'2022年-01月'}).lean();//以珠海 22年1月的数据为基准
+    const currentItems =  await priceInfoItemModel.find({areaID,period}).lean();
+    const preCodeMap = {};//编码前4位-指数映射
+    const baseAvgMap = getClassCodePriceAvgMap(baseItems);
+    const currentAvgMap = getClassCodePriceAvgMap(currentItems);
+    
+    let message = '';
+
+    for(const classCode in currentAvgMap){
+        const c = currentAvgMap[classCode];
+        const preCode = c.code.substr(0,4);
+        let index = 1;
+        const baseItem = baseAvgMap[classCode];
+        const tem = {index,classCode,name:c.name,code:c.code};
+        
+        if(baseItem && baseItem.price){//一个月份里有多个值时,先取平均再计算
+            index =  scMathUtil.roundForObj(c.price/baseItem.price,2);
+            tem.baseName = baseItem.name;
+        }
+        tem.index = index;
+        if(Math.abs(index - 1) > 0.2){
+            const string = `classCode:${tem.classCode},编号:${tem.code},基础名称:${tem.baseName},当前库中名称:${tem.name},指数:${tem.index};\n`;
+            message +=string;
+            console.log(string)
+        } 
+
+      preCodeMap[preCode]?preCodeMap[preCode].push(index):preCodeMap[preCode]=[index];
+    }
+    const newIndexData = calcIndexAvg(period, areaID,compilationID,preCodeMap)
+    //删除旧数据
+    await priceInfoIndexModel.deleteMany({areaID,period});
+    //插入新数据
+    await priceInfoIndexModel.insertMany(newIndexData);
+    return message;
+}
+
+module.exports = {
+    getLibs,
+    createLib,
+    updateLib,
+    deleteLib,
+    processChecking,
+    crawlDataByCompilation,
+    importExcelData,
+    importKeyData,
+    getAreas,
+    updateAres,
+    insertAreas,
+    deleteAreas,
+    getClassData,
+    calcPriceIndex,
+    getPriceData,
+    editPriceData,
+    editClassData
+}

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

@@ -0,0 +1,32 @@
+/**
+ * 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("/calcPriceIndex", priceInfoController.auth, priceInfoController.init, priceInfoController.calcPriceIndex);
+    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);
+};
+
+

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

+ 54 - 0
public/web/PerfectLoad.js

@@ -85,5 +85,59 @@ jQuery.bootstrapLoading = {
     },
     end: function () {
         $("#loadingPage").remove();
+    },
+    progressStop: true,
+    progressStart: async function (title = "导出文件", autoBar = false) {
+        function setTimeoutSync(handle, time) {
+            return new Promise(function (resolve, reject) {
+                setTimeout(function () {
+                    if (handle && typeof handle === 'function') {
+                        handle();
+                    }
+                    resolve();
+                }, time);
+            });
+        }
+        if ($("#progressModal").length == 0) {
+            let phtml = `<div class="modal fade" id="progressModal" data-backdrop="static">
+                            <div class="modal-dialog" role="document">
+                                <div class="modal-content">
+                                    <div class="modal-header">
+                                         <h5 class="modal-title" id="progress_modal_title">${title}</h5>
+                                    </div>
+                                     <div class="modal-body">
+                                        <!--正在生成-->
+                                        <h5 class="my-3" id="progress_modal_body">正在${title}</h5>
+                                        <div class="progress mb-3">
+                                        <div id="progress_modal_bar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="75" aria-valuemin="0" aria-valuemax="100" style="width: 10%"></div>
+                                        </div>
+                                     </div>
+                                   </div>
+                                </div>
+                            </div>`;
+            $("body").append(phtml);
+        } else {
+            $("#progress_modal_title").text(title);
+            $("#progress_modal_body").text(`正在${title}`);
+        }
+        $("#progress_modal_bar").css('width', '0%');
+        $("#progressModal").modal('show');
+        if (autoBar == true) {//假的进度条
+            $.bootstrapLoading.progressStop = false;
+            let width = 0;
+            while ($.bootstrapLoading.progressStop == false) {
+                await setTimeoutSync(null, 1000);
+                width += 5;
+                if (width > 90) width -= 50;
+                $("#progress_modal_bar").css('width', `${width}%`);
+            }
+        }
+    },
+    progressEnd: function () {
+        if ($('#progressModal').is(':visible')) {
+            $("#progress_modal_bar").css('width', '100%');
+            $.bootstrapLoading.progressStop = true;
+            $("#progressModal").modal('hide');
+        }
     }
 }

+ 32 - 0
public/web/id_tree.js

@@ -215,6 +215,38 @@ var idTree = {
              }) + node.children.count;*/
         };
 
+
+        Node.prototype.posterityLeafCount = function () {
+            return this.getPosterity().filter(item => !item.children.length).length;
+        };
+
+
+        // 获取节点所有后代节点
+        Node.prototype.getPosterity = function() {
+            let posterity = [];
+            getNodes(this.children);
+            return posterity;
+            function getNodes(nodes) {
+                for (let node of nodes) {
+                    posterity.push(node);
+                    if (node.children.length > 0){
+                        getNodes(node.children);
+                    }
+                }
+            }
+        };
+
+        // 担心链有问题,preSibling不靠谱的话,按照显示顺序算preSibling
+        Node.prototype.prevNode = function() {
+            const parent = this.parent || this.tree.roots;
+            if (!parent) {
+                return null;
+            }
+            const children = parent === this.tree.roots ? this.tree.roots :  parent.children;
+            const index = children.indexOf(this);
+            return children[index - 1] || null;
+        }
+
         Node.prototype.setExpanded = function (expanded) {
             var setNodesVisible = function (nodes, visible) {
                 nodes.forEach(function (node) {

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

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

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

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

@@ -0,0 +1,120 @@
+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%;
+}
+
+.main .right .top {
+    height: 70%;
+}
+
+.main .right .bottom {
+    height: 30%;
+}

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

@@ -0,0 +1,114 @@
+<!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  %>
+               <button id="calc-price-index">计算指数</button>     
+            </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 class="top" id="price-spread"></div>
+                <div class="bottom" id="keyword-spread"></div>
+            </div>
+        </div>
+    </div>
+
+
+    <div class="modal fade in" id="result-info" data-backdrop="static">
+        <div class="modal-dialog" role="document">
+            <div class="modal-content" style=" width: 900px;">
+                <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>以下指数偏差较大,请确认:</div>
+                        <div id="result-info-body">
+                         
+                        </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>
+                </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>

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

@@ -0,0 +1,234 @@
+<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>
+                                    <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%>", "originalData")' href="javacript:void(0);"
+                                            title="导入数据"><i class="fa fa-sign-in fa-rotate-90"></i>导入</a>
+                                    </td>
+                                    <td>
+                                        <a class="btn btn-secondary btn-sm import-data lock-btn-control disabled"
+                                            onclick='handleImportClick("<%= lib.ID%>", "keys")' 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>

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

@@ -0,0 +1,883 @@
+
+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 setFormatter(sheet, headers) {
+    const fuc = () => {
+        headers.forEach(({ formatter }, index) => {
+            if (formatter) {
+                sheet.setFormatter(-1, index, formatter);
+            }
+        });
+    };
+    sheetCommonObj.renderSheetFunc(sheet, fuc);
+}
+
+function initSheet(dom, setting) {
+    const workBook = sheetCommonObj.buildSheet(dom, setting);
+    const sheet = workBook.getSheet(0);
+    setAlign(sheet, setting.header);
+    setFormatter(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);
+
+    // 排序显示
+    cache.sort((a, b) => a.serialNo - b.serialNo);
+
+    // 显示数据
+    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) {
+            console.log(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');
+    const $calcPriceIndex = $('#calc-price-index');
+
+    // 插入
+    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) {
+            console.log(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);
+
+
+
+    $calcPriceIndex.click(_.debounce(async()=>{
+        $.bootstrapLoading.start();
+        try {
+        const data = await ajaxPost('/priceInfo/calcPriceIndex', { libID, period:curLibPeriod,compilationID }, TIME_OUT);
+          //alert(data);
+          
+          if(data){
+              const htmlStr = data.replace(/\n/gm,'<br>'); //replaceAll('\n','<br>',data);
+              $("#result-info-body").html(htmlStr);
+              $("#result-info").modal('show');
+          }else{
+              alert('计算完成!')
+          }  
+
+
+        } catch (error) {
+            console.log(error);
+        }
+        $.bootstrapLoading.end();
+
+    }, DEBOUNCE_TIME, { leading: true }));
+
+
+    return {
+        initData,
+        handleSelectionChanged,
+        curClass,
+    }
+
+})();
+
+// 关键字表
+const KEYWORD_BOOK = (() => {
+    const setting = {
+        header: [
+            { headerName: '关键字', headerWidth: 200, dataCode: 'keyword', dataType: 'String', hAlign: 'left', vAlign: 'center' },
+            { headerName: '单位', headerWidth: 70, dataCode: 'unit', dataType: 'String', hAlign: 'center', vAlign: 'center' },
+            { headerName: '关键字效果', headerWidth: 100, dataCode: 'coe', dataType: 'String', hAlign: 'center', vAlign: 'center' },
+            { headerName: '组别', headerWidth: 50, dataCode: 'group', dataType: 'String', hAlign: 'center', vAlign: 'center' },
+            { headerName: '选项号', headerWidth: 70, dataCode: 'optionCode', dataType: 'String', hAlign: 'center', vAlign: 'center' },
+        ],
+    };
+    // 初始化表格
+    const workBook = initSheet($('#keyword-spread')[0], setting);
+    workBook.options.allowUserDragDrop = false;
+    workBook.options.allowUserDragFill = false;
+    lockUtil.lockSpreads([workBook], true);
+    const sheet = workBook.getSheet(0);
+
+    // 显示关键字数据
+    const showKeywordData = (keywordList) => {
+        showData(sheet, keywordList, setting.header);
+    }
+
+    return {
+        showKeywordData 
+    }
+})();
+
+// 价格信息表
+const PRICE_BOOK = (() => {
+    const setting = {
+        header: [
+            { headerName: '编码', headerWidth: 100, dataCode: 'code', dataType: 'String', hAlign: 'left', vAlign: 'center' ,formatter: "@"},
+            { headerName: '别名编码', headerWidth: 70, dataCode: 'classCode', dataType: 'String', hAlign: 'left', vAlign: 'center' ,formatter: "@"},
+            { 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' },
+            { headerName: '月份备注', headerWidth: 140, dataCode: 'dateRemark', dataType: 'String', hAlign: 'left', vAlign: 'center' },
+            { headerName: '计算式', headerWidth: 100, dataCode: 'expString', dataType: 'String', hAlign: 'left', 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);
+            cache = _.sortBy(cache,'classCode');
+            showData(sheet, cache, setting.header, 5);
+            const row = sheet.getActiveRowIndex();
+            const keywordList = cache[row] && cache[row].keywordList || [];
+            KEYWORD_BOOK.showKeywordData(keywordList);
+        } 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.SelectionChanged, function (e, info) {
+        const row = info.newSelections && info.newSelections[0] ? info.newSelections[0].row : 0;
+        // 显示关键字数据
+        const keywordList = cache[row] && cache[row].keywordList || [];
+        KEYWORD_BOOK.showKeywordData(keywordList);
+    });
+    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(() => {
+    console.log('进入信息价');
+    $('[data-toggle="tooltip"]').tooltip();
+    AREA_BOOK.handleSelectionChanged(0);
+    const $range = $(document.body);
+    lockUtil.lockTools($range, locked);
+
+
+
+
+});

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

@@ -0,0 +1,269 @@
+// 节流
+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());
+    }
+}
+
+let importType = 'originalData';
+
+// 点击导入按钮
+function handleImportClick(libID, type) {
+    setCurLib(libID);
+    importType = type;
+    $('#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);
+        formData.append('importType', importType);
+        $.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);
+            }
+            if (cb) {
+                cb();
+            }
+            $.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());
+});
+
+$.ajax({
+    url: 'http://api.zjtcn.com/user/dyn_code',
+    type: 'post',
+    data: { service_id: '2020090003' },
+    contentType: 'application/x-www-form-urlencoded',
+})