Procházet zdrojové kódy

feat: 信息价总表相关

vian před 1 rokem
rodič
revize
5570042a2d

+ 15 - 0
modules/all_models/std_price_info_summary.js

@@ -0,0 +1,15 @@
+const mongoose = require('mongoose');
+
+const Schema = mongoose.Schema;
+const collectionName = 'std_price_info_summary';
+
+const modelSchema = {
+  ID: { type: String, required: true },
+  code: String, // 主从对应码
+  classCode: String, // 别名编码
+  expString: String, // 计算式,
+  name: String, // 材料名称
+  specs: String, // 规格型号
+  unit: String, // 单位
+};
+mongoose.model(collectionName, new Schema(modelSchema, { versionKey: false, collection: collectionName }));

+ 36 - 2
modules/price_info_lib/controllers/index.js

@@ -219,9 +219,9 @@ class PriceInfoController extends BaseController {
 
     async calcPriceIndex(req, res) {
         try {
-            const { period, libID ,compilationID} = JSON.parse(req.body.data);
+            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);
+            const data = await facade.calcPriceIndex(libID, period, areaID, compilationID);
             res.json({ error: 0, message: 'getCLass success', data });
         } catch (err) {
             console.log(err);
@@ -240,6 +240,28 @@ class PriceInfoController extends BaseController {
         }
     }
 
+    async getPriceEmptyData(req, res) {
+        try {
+            const { libID, compilationID } = JSON.parse(req.body.data);
+            const data = await facade.getPriceEmptyData(compilationID, libID);
+            res.json({ error: 0, message: 'getPriceEmptyData success', data });
+        } catch (err) {
+            console.log(err);
+            res.json({ error: 1, message: err.toString() });
+        }
+    }
+
+    async getRecommendPriceSummaryData(req, res) {
+        try {
+            const { keyword } = JSON.parse(req.body.data);
+            const data = await facade.getRecommendPriceSummaryData(keyword);
+            res.json({ error: 0, message: 'getRecommendPriceSummaryData 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);
@@ -262,6 +284,18 @@ class PriceInfoController extends BaseController {
         }
     }
 
+    // 匹配总表
+    async matchSummary(req, res) {
+        try {
+            const { compilationID, libID } = JSON.parse(req.body.data);
+            await facade.matchSummary(compilationID, libID);
+            res.json({ error: 0, message: 'matchSummary success' });
+        } catch (err) {
+            console.log(err);
+            res.json({ error: 1, message: err.toString() });
+        }
+    }
+
 }
 
 module.exports = {

+ 144 - 4
modules/price_info_lib/facade/index.js

@@ -11,7 +11,8 @@ 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');
-
+const priceInfoSummaryModel = mongoose.model('std_price_info_summary');
+const { getWordArray, alias } = require('../../../public/cut_word/segmentit');
 
 
 async function getLibs(query) {
@@ -477,17 +478,28 @@ const UpdateType = {
 async function editPriceData(postData) {
     const bulks = [];
     postData.forEach(data => {
+        const filter = { ID: data.ID };
+        // 为了命中索引,ID暂时还没添加索引,数据量太大,担心内存占用太多
+        if (data.areaID) {
+            filter.areaID = data.areaID;
+        }
+        if (data.compilationID) {
+            filter.compilationID = data.compilationID;
+        }
+        if (data.period) {
+            filter.period = data.period;
+        }
         if (data.type === UpdateType.UPDATE) {
             bulks.push({
                 updateOne: {
-                    filter: { ID: data.ID },
+                    filter,
                     update: { ...data.data }
                 }
             });
         } else if (data.type === UpdateType.DELETE) {
             bulks.push({
                 deleteOne: {
-                    filter: { ID: data.ID }
+                    filter,
                 }
             });
         } else {
@@ -616,6 +628,131 @@ async function calcPriceIndex(libID, period, areaID, compilationID) {
     return message;
 }
 
+const getMatchSummaryKey = (item) => {
+    const props = ['name', 'specs', 'unit'];
+    return props.map(prop => {
+        const subKey = item[prop] ? item[prop].trim() : '';
+        return subKey;
+    }).join('@');
+
+}
+
+const getSummaryMap = (items) => {
+    const map = {};
+    items.forEach(item => {
+        const key = getMatchSummaryKey(item);
+        map[key] = item;
+    });
+    return map;
+}
+
+// 匹配总表
+// 按规则匹配信息价的编码、别名编码、计算式(只匹配珠海建筑,要单独标记珠海地区);
+// 匹配规则:名称+规格型号+单位,与总表一致则自动填入编码、别名编码、计算式(珠海建筑);
+const matchSummary = async (compilationID, libID) => {
+    const updateBulks = [];
+    const areas = await priceInfoAreaModel.find({ compilationID }, '-_id ID name').lean();
+    const areaNameMap = {};
+    areas.forEach(area => {
+        areaNameMap[area.ID] = area.name;
+    });
+    const priceItems = await priceInfoItemModel.find({ libID }, '-_id ID compilationID name specs unit areaID period').lean();
+    const summaryItems = await priceInfoSummaryModel.find({}, '-_id ID name specs unit code classCode expString').lean();
+    const summaryMap = getSummaryMap(summaryItems);
+    priceItems.forEach(priceItem => {
+        const key = getMatchSummaryKey(priceItem);
+        const matched = summaryMap[key];
+        if (matched) {
+            const updateObj = {
+                code: matched.code,
+                classCode: matched.classCode,
+            }
+            console.log(matched);
+            console.log(updateObj);
+            const areaName = areaNameMap[priceItem.areaID];
+            if (/珠海/.test(areaName)) {
+                updateObj.expString = matched.expString;
+            }
+            updateBulks.push({
+                updateOne: {
+                    filter: { ID: priceItem.ID, compilationID: priceItem.compilationID, areaID: priceItem.areaID, period: priceItem.period },
+                    update: updateObj
+                }
+            })
+        }
+    });
+    if (updateBulks.length) {
+        console.log(`updateBulks.length`, updateBulks.length);
+        await priceInfoItemModel.bulkWrite(updateBulks);
+    }
+}
+
+// 获取空数据(没有别名编码)
+const getPriceEmptyData = async (compilationID, libID) => {
+    const lib = await priceInfoLibModel.findOne({ ID: libID }).lean();
+    if (!lib) {
+        return [];
+    }
+    const priceItems = await priceInfoItemModel.find({ compilationID, libID, period: lib.period }).lean();
+    return priceItems.filter(item => !item.classCode);
+};
+
+const getMatchPrice = (allInfoPrice, nameArray, needHandleLongWord = true) => {
+    let items = [];
+    let maxNum = 0; // 最大匹配数
+    const matchMap = {}; // 匹配储存
+    let handleLongWord = false;
+    if (needHandleLongWord) {
+        for (const na of nameArray) {
+            if (na.length >= 5) handleLongWord = true;
+        }
+    }
+
+    for (const info of allInfoPrice) {
+        // specs
+        const matchString = alias(info.name + info.specs); // 组合名称和规格型号
+        info.matchString = matchString;
+        let matchCount = 0;
+        for (const na of nameArray) {
+            if (matchString.indexOf(na) !== -1) {
+                matchCount += 1;
+                if (needHandleLongWord && na.length >= 5) handleLongWord = false; // 有5个字的,并且匹配上了,这里就为false不用再处理一次了
+            }
+        }
+        if (matchCount > 0) {
+            if (matchMap[matchCount]) {
+                matchMap[matchCount].push(info);
+            } else {
+                matchMap[matchCount] = [info];
+            }
+            if (matchCount > maxNum) maxNum = matchCount;
+        }
+    }
+    if (maxNum > 0) items = matchMap[maxNum];
+
+    return { items, handleLongWord };
+}
+
+// 获取推荐总表数据
+const getRecommendPriceSummaryData = async (keyword) => {
+    const nameArray = getWordArray(keyword);
+    console.log(`nameArray`);
+    console.log(nameArray);
+    const allItems = await priceInfoSummaryModel.find({}).lean();
+    let { items } = getMatchPrice(allItems, nameArray);
+
+    // 按匹配位置排序 如[ '橡胶', '胶圈', '给水' ] 先显示橡胶
+    items = _.sortBy(items, item => {
+        const ms = item.matchString;
+        for (let i = 0; i < nameArray.length; i += 1) {
+            if (ms.indexOf(nameArray[i]) !== -1) return i;
+        }
+        return 0;
+    });
+
+    return items;
+}
+
 module.exports = {
     getLibs,
     createLib,
@@ -633,5 +770,8 @@ module.exports = {
     calcPriceIndex,
     getPriceData,
     editPriceData,
-    editClassData
+    editClassData,
+    matchSummary,
+    getPriceEmptyData,
+    getRecommendPriceSummaryData,
 }

+ 4 - 1
modules/price_info_lib/routes/index.js

@@ -25,7 +25,10 @@ module.exports = function (app) {
     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);
-
+    router.post("/matchSummary", priceInfoController.auth, priceInfoController.init, priceInfoController.matchSummary);
+    router.post("/getPriceEmptyData", priceInfoController.auth, priceInfoController.init, priceInfoController.getPriceEmptyData);
+    router.post("/getRecommendPriceSummaryData", priceInfoController.auth, priceInfoController.init, priceInfoController.getRecommendPriceSummaryData);
+    
     app.use("/priceInfo", router);
 };
 

+ 59 - 0
modules/price_info_summary/controllers/index.js

@@ -0,0 +1,59 @@
+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 PriceInfoSummaryController extends BaseController {
+    async main(req, res) {
+        const renderData = {
+            title: '材料信息价总表',
+            userAccount: req.session.managerData.username,
+            userID: req.session.managerData.userID,
+            LicenseKey: config.getLicenseKey(process.env.NODE_ENV),
+        };
+        res.render("maintain/price_info_summary/html/main.html", renderData);
+    }
+
+    // 获取分页数据
+    async getPagingData(req, res) {
+        try {
+            const { page, pageSize, searchStr } = JSON.parse(req.body.data);
+            const data = await facade.getPagingData(page, pageSize, searchStr);
+            res.json({ error: 0, message: 'getData success', data });
+        } catch (err) {
+            console.log(err);
+        }
+    }
+
+    // 编辑总表
+    async editSummaryData(req, res) {
+        try {
+            const { postData } = JSON.parse(req.body.data);
+            await facade.editSummaryData(postData);
+            res.json({ error: 0, message: 'editPrice success' });
+        } catch (err) {
+            console.log(err);
+            res.json({ error: 1, message: err.toString() });
+        }
+    }
+
+    // 保存至总表
+    async saveInSummary(req, res) {
+        try {
+            const { documents } = JSON.parse(req.body.data);
+            await facade.saveInSummary(documents);
+            res.json({ error: 0, message: 'saveInSummary success' });
+        } catch (err) {
+            console.log(err);
+            res.json({ error: 1, message: err.toString() });
+        }
+    }
+
+}
+
+module.exports = {
+    priceInfoSummaryController: new PriceInfoSummaryController()
+};

+ 64 - 0
modules/price_info_summary/facade/index.js

@@ -0,0 +1,64 @@
+const mongoose = require('mongoose');
+
+const priceInfoSummaryModel = mongoose.model('std_price_info_summary');
+
+// 获取分页数据
+const getPagingData = async (page, pageSize, searchStr) => {
+    let query = {};
+    if (searchStr) {
+        const nameReg = new RegExp(searchStr);
+        query = {
+            $or: [{ classCode: searchStr }, { name: { $regex: nameReg } }]
+        }
+    }
+    const totalCount = await priceInfoSummaryModel.count(query);
+    const items = await priceInfoSummaryModel.find(query).lean().sort({ classCode: -1 }).skip(page * pageSize).limit(pageSize);
+    return { items, totalCount };
+}
+
+const UpdateType = {
+    UPDATE: 'update',
+    DELETE: 'delete',
+    CREATE: 'create',
+};
+
+// 编辑表格
+async function editSummaryData(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 priceInfoSummaryModel.bulkWrite(bulks);
+    }
+}
+
+// 保存至总表
+async function saveInSummary(documents) {
+    await priceInfoSummaryModel.insertMany(documents);
+}
+
+module.exports = {
+    getPagingData,
+    editSummaryData,
+    saveInSummary,
+}

+ 18 - 0
modules/price_info_summary/routes/index.js

@@ -0,0 +1,18 @@
+/**
+ * Created by zhang on 2018/9/3.
+ */
+
+const express = require("express");
+const router = express.Router();
+const { priceInfoSummaryController } = require('../controllers/index');
+
+module.exports = function (app) {
+    router.get("/main", priceInfoSummaryController.auth, priceInfoSummaryController.init, priceInfoSummaryController.main);
+    router.post("/getPagingData", priceInfoSummaryController.auth, priceInfoSummaryController.init, priceInfoSummaryController.getPagingData);
+    router.post("/editSummaryData", priceInfoSummaryController.auth, priceInfoSummaryController.init, priceInfoSummaryController.editSummaryData);
+    router.post("/saveInSummary", priceInfoSummaryController.auth, priceInfoSummaryController.init, priceInfoSummaryController.saveInSummary);
+
+    app.use("/priceInfoSummary", router);
+};
+
+

+ 6 - 5
package.json

@@ -29,13 +29,14 @@
     "bluebird": "^3.5.0",
     "jszip": "^3.1.3",
     "log4js": "~2.3.3",
-    "multiparty": "^4.1.3",
+    "lz-string": "^1.4.4",
     "moment-timezone": "^0.5.27",
+    "multiparty": "^4.1.3",
+    "node-schedule": "^1.3.0",
     "node-xlsx": "^0.11.2",
     "pdfkit": "^0.8.2",
-    "ueditor": "^1.2.3",
-    "node-schedule": "^1.3.0",
-    "lz-string": "^1.4.4"
+    "segmentit": "^2.0.3",
+    "ueditor": "^1.2.3"
   },
   "scripts": {
     "start": "C:\\Users\\mai\\AppData\\Roaming\\npm\\babel-node.cmd operation.js",
@@ -50,4 +51,4 @@
     "prod_sc_server": "SET NODE_ENV=prod_sc&& babel-node operation.js",
     "local2prod_hw_server": "SET NODE_ENV=local2prod_hw&& babel-node operation.js"
   }
-}
+}

+ 139 - 0
public/cut_word/segmentit.js

@@ -0,0 +1,139 @@
+const { Segment, Tokenizer, dicts, synonyms, stopwords, modules } = require('segmentit');
+
+const scWords = ['硅酸盐水泥', '综合工', '铜芯', '螺纹钢', 'mm2', 'mm'];
+
+/* const buffer = readFileSync(join(__dirname, '../dict/scWords.utf8'));
+const dicText = buffer.toString();
+export const scWords = dicText.split(/\r?\n/); */
+
+class CusTokenizer extends Tokenizer {
+  segment;
+
+  split(words) {
+    // 需要的话可以获取到 this.segment 里的各种信息
+    // const TABLE = this.segment.getDict('TABLE');
+    const ret = [];
+    // 这个拦截器放在第一位,优先级最高words 一般只有一个元素
+    for (const word of words) {
+      let source = word.w;
+      for (const scWord of scWords) {
+        if (source.includes(scWord)) {
+          // ret.push({ w: scWord, p: TABLE[scWord].p });
+          ret.push({ w: scWord, p: 1000 }); // 这个p 表示词性,有值时,后一个分词器就不会再对这个词进行处理了, 对于我们使用场景来说没什么用,给个默认值就好
+          const reg = new RegExp(`${scWord}`, 'g');
+          source = source.replace(reg, '');
+        }
+      }
+      if (source) ret.push({ w: source });
+    }
+
+    return ret;
+  }
+}
+
+const useDefault = (segmentInstance) => {
+  segmentInstance.use([CusTokenizer, ...modules]);
+  segmentInstance.loadDict(dicts); // dicText,
+  segmentInstance.loadSynonymDict(synonyms);
+  segmentInstance.loadStopwordDict(stopwords);
+  return segmentInstance;
+};
+
+const segmentit = useDefault(new Segment());
+
+const scCut = (text) => {
+  const options = { stripPunctuation: true, simple: true };
+  return segmentit.doSegment(text, options);
+};
+
+// 获取分词数组。
+const getCutWords = (aName, aSpecs) => {
+  const cutNames = scCut(aName);
+  const cutSpecs = scCut(aSpecs); // 规格字符混杂,分词效果极差,使用 cutHMM()效果略好。
+  const rst = [...cutNames, ...cutSpecs];
+  return rst;
+}
+
+// 返回匹配数。
+const getMatchCount = (aUserWords, aMatchStr) => {
+  let count = 0;
+  for (const word of aUserWords) {
+    if (aMatchStr.indexOf(word) > -1) count += 1;
+  }
+  return count;
+}
+
+// 分词算法相关:别名替换。返回替换后的新字符串。
+const alias = (aStr) => {
+  if (!aStr) {
+    return aStr;
+  }
+  // 替换前
+  const a1 = ['φ', '混凝土'];
+  // 替换后,标准。用户自定义词库以a2中的词为标准录入。
+  const a2 = ['Ф', '砼'];
+  for (const key in a1) {
+    const patt = new RegExp(a1[key], 'g');
+    aStr = aStr.replace(patt, a2[key]);
+  }
+  return aStr;
+}
+
+const handelThreeWord = (word) => {
+  function getArr(tem, list) {
+    const nameArray = scCut(tem);
+    // 说明是一个词
+    if (nameArray.length === 1) list.push(tem);
+  }
+
+  const arr = [];
+  getArr(word[0] + word[1], arr);
+  getArr(word[1] + word[2], arr);
+  if (arr.length > 0) return arr;
+  return [word];
+}
+
+// 自定义特殊处理
+const cusSegment = (nameArray, keyword) => {
+  const temArr = [];
+  for (let a of nameArray) {
+    // 混凝土 和 砼 统一认成砼
+    if (a === '混凝土') a = '砼';
+    if (a === '砼' || a === '砖') {
+      temArr.push(a);
+    } else if (a.length > 1) {
+      // 如果是三个字的词,优先识别成两个字
+      if (a.length === 3 && !scWords.includes(a)) {
+        const sArr = handelThreeWord(a);
+        temArr.push(...sArr);
+      } else {
+        temArr.push(a);
+      }
+    }
+  }
+  if (keyword.length === 1 && temArr.length === 0) temArr.push(keyword);
+  return temArr;
+}
+
+const getWordArray = (keyword) => {
+  let wordArray = [];
+  if (keyword.length < 3) {
+    // 小于3个字的直接按一个词处理
+    wordArray.push(keyword);
+  } else {
+    wordArray = scCut(keyword);
+  }
+
+  // 自定义分词特殊处理
+  wordArray = cusSegment(wordArray, keyword);
+  // console.log(`分词结果:${wordArray}`);
+  return wordArray;
+}
+
+module.exports = {
+  scCut,
+  getCutWords,
+  getMatchCount,
+  alias,
+  getWordArray,
+};

+ 17 - 10
public/web/lock_util.js

@@ -20,11 +20,11 @@ const lockUtil = (() => {
         const $btns = $range.find('.lock-btn-control');
         const toolList = [];
         for (const $btn of $btns) {
-            toolList.push({$ref: $($btn), type: 'button'});
+            toolList.push({ $ref: $($btn), type: 'button' });
         }
         const $texts = $range.find('.lock-text-control');
         for (const $text of $texts) {
-            toolList.push({$ref: $($text), type: 'text'});
+            toolList.push({ $ref: $($text), type: 'text' });
         }
         toolList.forEach(item => {
             switch (item.type) {
@@ -44,7 +44,7 @@ const lockUtil = (() => {
         spreads.forEach(spread => {
             spread.unbind(GC.Spread.Sheets.Events.ButtonClicked);
             const sheetCount = spread.getSheetCount();
-            for(let i = 0; i < sheetCount; i++){
+            for (let i = 0; i < sheetCount; i++) {
                 const sheet = spread.getSheet(i);
                 sheet.unbind(GC.Spread.Sheets.Events.ButtonClicked);
                 sheet.unbind(GC.Spread.Sheets.Events.EditStarting);
@@ -60,8 +60,8 @@ const lockUtil = (() => {
                 sheet.options.isProtected = true;
                 const rowCount = sheet.getRowCount();
                 const colCount = sheet.getColumnCount();
-                for(let row = 0; row < rowCount; row++){
-                    for(let col = 0; col < colCount; col++){
+                for (let row = 0; row < rowCount; row++) {
+                    for (let col = 0; col < colCount; col++) {
                         sheet.getCell(row, col).locked(true);
                     }
                 }
@@ -77,14 +77,20 @@ const lockUtil = (() => {
         const curURL = reg.test(originURL) ? originURL.replace(reg, `locked=${locked}`) : `${originURL}&locked=${locked}`;
         $url.prop('href', curURL);
     }
+
+    function displayLock($lock, locked) {
+        $lock.data('locked', locked);
+        const innerHtml = locked ? '<i class="fa fa-unlock-alt"></i>' : '<i class="fa fa-lock"></i>';
+        $lock.html(innerHtml);
+        const title = locked ? '解锁' : '锁定';
+        $lock.prop('title', title);
+    }
+
+
     // 库列表页面,锁定按钮点击操作
     function handleLockClick($lock) {
         const curLocked = !$lock.data().locked;
-        $lock.data('locked', curLocked);
-        const innerHtml = curLocked ? '<i class="fa fa-unlock-alt"></i>' : '<i class="fa fa-lock"></i>';
-        $lock.html(innerHtml);
-        const title = curLocked ? '解锁' : '锁定';
-        $lock.prop('title', title);
+        displayLock($lock, curLocked);
         const $url = $lock.parent().parent().children(':first-child').children(':first-child');
         lockURL(curLocked, $url);
         const $range = $lock.parent().parent();
@@ -103,6 +109,7 @@ const lockUtil = (() => {
         lockTools,
         lockSpreads,
         lockURL,
+        displayLock,
         handleLockClick,
         lockSpreadsAndTools
     }

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

@@ -117,4 +117,25 @@ body {
 
 .main .right .bottom {
     height: 30%;
+}
+
+.empty-spread {
+    height: 300px;
+}
+
+.recommend {
+    margin: 4px 0;
+}
+
+.recommend-spread {
+    height: 200px;
+}
+
+.recommend-input {
+    display: inline-block;
+    width: 200px !important;
+}
+
+#save-in-summary {
+    margin-bottom: 4px;
 }

+ 34 - 2
web/maintain/price_info_lib/html/edit.html

@@ -19,7 +19,10 @@
             <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>     
+               <button id="calc-price-index">计算指数</button>
+               <button id="match-summary">匹配总表</button> 
+               <button id="show-empty">显示空数据</button> 
+               <button id="check-repeat">检测重复别名编码</button> 
             </div>
 
         </nav>
@@ -83,6 +86,28 @@
             </div>
         </div>
     </div>
+
+    <div class="modal fade in" id="empty-area" 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">
+                    <button id="save-in-summary">保存至总表</button> 
+                    <div class="empty-spread"></div>
+                    <div class="recommend">
+                        <span class="recommend-title">相似材料数据</span>
+                        <input class="form-control form-control-sm recommend-input" id="recommend-search" type="text" value="" placeholder="输入回车进行搜索">
+                    </div>
+                    <div class="recommend-spread"></div>
+                </div>
+            </div>
+        </div>
+    </div>
     <!-- JS. -->
     <script src="/lib/jquery/jquery.min.js"></script>
     <script src="/lib/jquery-contextmenu/jquery.contextMenu.min.js"></script>
@@ -108,7 +133,14 @@
         const compilationID = '<%- compilationID %>';
         const curLibPeriod = '<%- period %>';
     </script>
-    <script src="/web/maintain/price_info_lib/js/index.js"></script>
+        <script src="/web/maintain/price_info_lib/js/common.js"></script>
+        <script src="/web/maintain/price_info_lib/js/priceArea.js"></script>
+        <script src="/web/maintain/price_info_lib/js/priceClass.js"></script>
+        <script src="/web/maintain/price_info_lib/js/priceKeyword.js"></script>
+        <script src="/web/maintain/price_info_lib/js/priceItem.js"></script>
+        <script src="/web/maintain/price_info_lib/js/index.js"></script>
+        <script src="/web/maintain/price_info_lib/js/priceRecommend.js"></script>
+        <script src="/web/maintain/price_info_lib/js/priceEmpty.js"></script>
 </body>
 
 </html>

+ 87 - 0
web/maintain/price_info_lib/js/common.js

@@ -0,0 +1,87 @@
+
+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);
+}
+
+// 获取当前表中行数据
+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;
+}
+
+const TIME_OUT = 1000 * 20;
+const libID = window.location.search.match(/libID=([^&]+)/)[1];
+
+const UpdateType = {
+  UPDATE: 'update',
+  DELETE: 'delete',
+  CREATE: 'create',
+};
+
+const DEBOUNCE_TIME = 200;
+
+const locked = lockUtil.getLocked();

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

@@ -1,898 +1,7 @@
-
-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: 60, dataCode: 'serialNo', dataType: 'Number', hAlign: 'center', vAlign: 'center' },
-            { headerName: '地区', headerWidth: $('#area-spread').width() - 80, 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 = [];
-        let reSort = false;
-        changedCells.forEach(({ row, col }) => {
-            const field = setting.header[col].dataCode;
-            let value = sheet.getValue(row, col);
-            if (field === 'serialNo') {
-                reSort = true;
-                value = +value;
-            }
-            updateData.push({
-                row,
-                field,
-                value,
-                ID: cache[row].ID,
-            });
-        });
-        try {
-            await ajaxPost('/priceInfo/editArea', { updateData }, TIME_OUT);
-            updateData.forEach(({ row, field, value }) => cache[row][field] = value);
-            if (reSort) {
-                cache.sort((a, b) => a.serialNo - b.serialNo);
-                showData(sheet, cache, setting.header);
-            }
-        } catch (err) {
-            // 恢复各单元格数据
-            sheetCommonObj.renderSheetFunc(sheet, () => {
-                changedCells.forEach(({ row, col, field }) => {
-                    sheet.setValue(row, col, cache[row][field]);
-                });
-            });
-        }
-    }
-    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);
-
-
-
-
 });

+ 179 - 0
web/maintain/price_info_lib/js/priceArea.js

@@ -0,0 +1,179 @@
+// 地区表
+const AREA_BOOK = (() => {
+  const cache = areaList;
+  const setting = {
+    header: [
+      { headerName: '序号', headerWidth: 60, dataCode: 'serialNo', dataType: 'Number', hAlign: 'center', vAlign: 'center' },
+      { headerName: '地区', headerWidth: $('#area-spread').width() - 80, 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 = [];
+    let reSort = false;
+    changedCells.forEach(({ row, col }) => {
+      const field = setting.header[col].dataCode;
+      let value = sheet.getValue(row, col);
+      if (field === 'serialNo') {
+        reSort = true;
+        value = +value;
+      }
+      updateData.push({
+        row,
+        field,
+        value,
+        ID: cache[row].ID,
+      });
+    });
+    try {
+      await ajaxPost('/priceInfo/editArea', { updateData }, TIME_OUT);
+      updateData.forEach(({ row, field, value }) => cache[row][field] = value);
+      if (reSort) {
+        cache.sort((a, b) => a.serialNo - b.serialNo);
+        showData(sheet, cache, setting.header);
+      }
+    } catch (err) {
+      // 恢复各单元格数据
+      sheetCommonObj.renderSheetFunc(sheet, () => {
+        changedCells.forEach(({ row, col, field }) => {
+          sheet.setValue(row, col, cache[row][field]);
+        });
+      });
+    }
+  }
+  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, name: '' };
+  // 焦点变更处理
+  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;
+    curArea.name = areaItem && areaItem.name || '';
+    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,
+    cache,
+  }
+
+})();

+ 488 - 0
web/maintain/price_info_lib/js/priceClass.js

@@ -0,0 +1,488 @@
+// 分类表
+const CLASS_BOOK = (() => {
+  const setting = {
+    header: [{ headerName: '分类', headerWidth: $('#area-spread').width(), dataCode: 'name', dataType: 'String', hAlign: 'left', vAlign: 'center' }],
+    controller: {
+      cols: [
+        {
+          data: {
+            field: 'name',
+            vAlign: 1,
+            hAlign: 0,
+            font: 'Arial'
+          },
+        }
+      ],
+      headRows: 1,
+      headRowHeight: [30],
+      emptyRows: 0,
+      treeCol: 0
+    },
+    tree: {
+      id: 'ID',
+      pid: 'ParentID',
+      nid: 'NextSiblingID',
+      rootId: -1
+    }
+  };
+  // 初始化表格
+  const workBook = initSheet($('#class-spread')[0], setting);
+  workBook.options.allowExtendPasteRange = false;
+  workBook.options.allowUserDragDrop = true;
+  workBook.options.allowUserDragFill = true;
+  const sheet = workBook.getSheet(0);
+
+  let tree;
+  let controller;
+  // 初始化数据
+  async function initData(libID, areaID) {
+    if (!areaID) {
+      tree = null;
+      controller = null;
+      sheet.setRowCount(0);
+      PRICE_BOOK.clear();
+      return;
+    }
+    $.bootstrapLoading.start();
+    try {
+      const data = await ajaxPost('/priceInfo/getClassData', { libID, areaID }, TIME_OUT);
+      tree = idTree.createNew(setting.tree);
+      tree.loadDatas(data);
+      tree.selected = tree.items.length > 0 ? tree.items[0] : null;
+      controller = TREE_SHEET_CONTROLLER.createNew(tree, sheet, setting.controller, false);
+      controller.showTreeData();
+      handleSelectionChanged(0);
+      sheet.setSelection(0, 0, 1, 1);
+      lockUtil.lockSpreads([workBook], locked);
+    } catch (err) {
+      tree = null;
+      controller = null;
+      sheet.setRowCount(0);
+      alert(err);
+    } finally {
+      $.bootstrapLoading.end();
+    }
+  }
+
+  // 编辑处理
+  async function handleEdit(changedCells) {
+    const updateData = [];
+    changedCells.forEach(({ row, col }) => {
+      updateData.push({
+        row,
+        type: UpdateType.UPDATE,
+        filter: { ID: tree.items[row].data.ID },
+        update: { name: sheet.getValue(row, col) }
+      });
+    });
+    try {
+      await ajaxPost('/priceInfo/editClassData', { updateData }, TIME_OUT);
+      updateData.forEach(({ row, update: { name } }) => tree.items[row].data.name = name);
+    } catch (err) {
+      // 恢复各单元格数据
+      sheetCommonObj.renderSheetFunc(sheet, () => {
+        changedCells.forEach(({ row }) => {
+          sheet.setValue(tree.items[row].data.name);
+        });
+      });
+    }
+  }
+  sheet.bind(GC.Spread.Sheets.Events.ValueChanged, function (e, info) {
+    const changedCells = [{ row: info.row, col: info.col }];
+    handleEdit(changedCells);
+  });
+  sheet.bind(GC.Spread.Sheets.Events.RangeChanged, function (e, info) {
+    handleEdit(info.changedCells);
+  });
+
+  // 树操作相关
+  const $insert = $('#tree-insert');
+  const $remove = $('#tree-remove');
+  const $upLevel = $('#tree-up-level');
+  const $downLevel = $('#tree-down-level');
+  const $downMove = $('#tree-down-move');
+  const $upMove = $('#tree-up-move');
+  const $calcPriceIndex = $('#calc-price-index');
+  const $matchSummary = $('#match-summary');
+  const $showEmpty = $('#show-empty');
+
+  // 插入
+  let canInsert = true;
+  async function insert() {
+    try {
+      if (!canInsert) {
+        return false;
+      }
+      canInsert = false;
+      $.bootstrapLoading.start();
+      const updateData = [];
+      const selected = tree.selected;
+      const newItem = {
+        libID,
+        areaID: AREA_BOOK.curArea.ID,
+        ID: uuid.v1(),
+        name: '',
+        ParentID: '-1',
+        NextSiblingID: '-1'
+      };
+      if (selected) {
+        newItem.ParentID = selected.data.ParentID;
+        updateData.push({
+          type: UpdateType.UPDATE,
+          filter: { ID: selected.data.ID },
+          update: { NextSiblingID: newItem.ID }
+        });
+        if (selected.nextSibling) {
+          newItem.NextSiblingID = selected.nextSibling.data.ID;
+        }
+      }
+      updateData.push({
+        type: UpdateType.CREATE,
+        document: newItem
+      });
+      await ajaxPost('/priceInfo/editClassData', { updateData });
+      controller.insertByID(newItem.ID);
+      handleSelectionChanged(sheet.getActiveRowIndex());
+    } catch (err) {
+      alert(err);
+    } finally {
+      canInsert = true;
+      $.bootstrapLoading.end();
+    }
+  }
+
+  $insert.click(_.debounce(insert, DEBOUNCE_TIME, { leading: true }));
+
+  // 删除
+  let canRemove = true;
+  async function remove() {
+    try {
+      if (!canRemove) {
+        return false;
+      }
+      canRemove = false;
+      $.bootstrapLoading.start();
+      const updateData = [];
+      const selected = tree.selected;
+      const children = selected.getPosterity();
+      [selected, ...children].forEach(node => updateData.push({
+        type: UpdateType.DELETE,
+        filter: { ID: node.data.ID }
+      }));
+      if (selected.preSibling) {
+        updateData.push({
+          type: UpdateType.UPDATE,
+          filter: { ID: selected.preSibling.data.ID },
+          update: { NextSiblingID: selected.data.NextSiblingID }
+        });
+      }
+      await ajaxPost('/priceInfo/editClassData', { updateData });
+      controller.delete();
+      handleSelectionChanged(sheet.getActiveRowIndex());
+    } catch (err) {
+      alert(err);
+    } finally {
+      canRemove = true;
+      $.bootstrapLoading.end();
+    }
+  }
+
+  $remove.click(_.debounce(remove, DEBOUNCE_TIME, { leading: true }));
+
+  // 升级
+  let canUpLevel = true;
+  async function upLevel() {
+    try {
+      if (!canUpLevel) {
+        return false;
+      }
+      canUpLevel = false;
+      $.bootstrapLoading.start();
+      const updateData = [];
+      const selected = tree.selected;
+      if (selected.preSibling) {
+        updateData.push({
+          type: UpdateType.UPDATE,
+          filter: { ID: selected.preSibling.data.ID },
+          update: { NextSiblingID: -1 }
+        });
+      }
+      if (selected.parent) {
+        updateData.push({
+          type: UpdateType.UPDATE,
+          filter: { ID: selected.parent.data.ID },
+          update: { NextSiblingID: selected.data.ID }
+        });
+      }
+      updateData.push({
+        type: UpdateType.UPDATE,
+        filter: { ID: selected.data.ID },
+        update: { ParentID: selected.parent.data.ParentID, NextSiblingID: selected.parent.data.NextSiblingID }
+      });
+      let curNode = selected.nextSibling;
+      while (curNode) {
+        updateData.push({
+          type: UpdateType.UPDATE,
+          filter: { ID: curNode.data.ID },
+          update: { ParentID: selected.data.ID }
+        });
+        curNode = curNode.nextSibling;
+      }
+      await ajaxPost('/priceInfo/editClassData', { updateData });
+      controller.upLevel();
+      refreshTreeButton(tree.selected);
+    } catch (err) {
+      alert(err);
+    } finally {
+      canUpLevel = true;
+      $.bootstrapLoading.end();
+    }
+  }
+
+  $upLevel.click(_.debounce(upLevel, DEBOUNCE_TIME, { leading: true }));
+
+  // 降级
+  let canDownLevel = true;
+  async function downLevel() {
+    try {
+      if (!canDownLevel) {
+        return false;
+      }
+      canDownLevel = false;
+      $.bootstrapLoading.start();
+      const updateData = [];
+      const selected = tree.selected;
+      if (selected.preSibling) {
+        updateData.push({
+          type: UpdateType.UPDATE,
+          filter: { ID: selected.preSibling.data.ID },
+          update: { NextSiblingID: selected.data.NextSiblingID }
+        });
+        const preSiblingLastChild = selected.preSibling.children[selected.preSibling.children.length - 1];
+        if (preSiblingLastChild) {
+          updateData.push({
+            type: UpdateType.UPDATE,
+            filter: { ID: preSiblingLastChild.data.ID },
+            update: { NextSiblingID: selected.data.ID }
+          });
+        }
+        updateData.push({
+          type: UpdateType.UPDATE,
+          filter: { ID: selected.data.ID },
+          update: { ParentID: selected.preSibling.data.ID, NextSiblingID: -1 }
+        });
+      }
+      await ajaxPost('/priceInfo/editClassData', { updateData });
+      controller.downLevel();
+      refreshTreeButton(tree.selected);
+    } catch (err) {
+      alert(err);
+    } finally {
+      canDownLevel = true;
+      $.bootstrapLoading.end();
+    }
+  }
+
+  $downLevel.click(_.debounce(downLevel, DEBOUNCE_TIME, { leading: true }));
+
+  // 下移
+  let canDownMove = true;
+  async function downMove() {
+    try {
+      if (!canDownMove) {
+        return false;
+      }
+      canDownMove = false;
+      $.bootstrapLoading.start();
+      const updateData = [];
+      const selected = tree.selected;
+      if (selected.preSibling) {
+        updateData.push({
+          type: UpdateType.UPDATE,
+          filter: { ID: selected.preSibling.data.ID },
+          update: { NextSiblingID: selected.data.NextSiblingID }
+        });
+      }
+      updateData.push({
+        type: UpdateType.UPDATE,
+        filter: { ID: selected.data.ID },
+        update: { NextSiblingID: selected.nextSibling.data.NextSiblingID }
+      });
+      updateData.push({
+        type: UpdateType.UPDATE,
+        filter: { ID: selected.nextSibling.data.ID },
+        update: { NextSiblingID: selected.data.ID }
+      });
+      await ajaxPost('/priceInfo/editClassData', { updateData });
+      controller.downMove();
+      refreshTreeButton(tree.selected);
+    } catch (err) {
+      alert(err);
+    } finally {
+      canDownMove = true;
+      $.bootstrapLoading.end();
+    }
+  }
+
+  $downMove.click(_.debounce(downMove, DEBOUNCE_TIME, { leading: true }));
+
+  // 上移
+  let canUpMove = true;
+  async function upMove() {
+    try {
+      if (!canUpMove) {
+        return false;
+      }
+      canUpMove = false;
+      $.bootstrapLoading.start();
+      const updateData = [];
+      const selected = tree.selected;
+      if (selected.preSibling) {
+        updateData.push({
+          type: UpdateType.UPDATE,
+          filter: { ID: selected.preSibling.data.ID },
+          update: { NextSiblingID: selected.data.NextSiblingID }
+        });
+      }
+      const prePreSibling = selected.preSibling.preSibling;
+      if (prePreSibling) {
+        updateData.push({
+          type: UpdateType.UPDATE,
+          filter: { ID: prePreSibling.data.ID },
+          update: { NextSiblingID: selected.data.ID }
+        });
+      }
+      updateData.push({
+        type: UpdateType.UPDATE,
+        filter: { ID: selected.data.ID },
+        update: { NextSiblingID: selected.preSibling.data.ID }
+      });
+      await ajaxPost('/priceInfo/editClassData', { updateData });
+      controller.upMove();
+      refreshTreeButton(tree.selected);
+    } catch (err) {
+      alert(err);
+    } finally {
+      canUpMove = true;
+      $.bootstrapLoading.end();
+    }
+  }
+
+  $upMove.click(_.debounce(upMove, DEBOUNCE_TIME, { leading: true }));
+
+
+  // 刷新树操作按钮有效性
+  function refreshTreeButton(selected) {
+    if (locked) {
+      return;
+    }
+    $insert.removeClass('disabled');
+    $remove.removeClass('disabled');
+    $upLevel.removeClass('disabled');
+    $downLevel.removeClass('disabled');
+    $downMove.removeClass('disabled');
+    $upMove.removeClass('disabled');
+    if (!selected) {
+      $remove.addClass('disabled');
+      $upLevel.addClass('disabled');
+      $downLevel.addClass('disabled');
+      $downMove.addClass('disabled');
+      $upMove.addClass('disabled');
+    } else {
+      if (!selected.preSibling) {
+        $downLevel.addClass('disabled');
+        $upMove.addClass('disabled');
+      }
+      if (!selected.nextSibling) {
+        $downMove.addClass('disabled');
+      }
+      if (!selected.parent) {
+        $upLevel.addClass('disabled');
+      }
+    }
+  }
+
+  // 焦点变更处理
+  const curClass = { ID: null };
+  function handleSelectionChanged(row) {
+    curRow = 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);
+
+  const reload = () => {
+    if (curClass.ID) {
+      const node = tree.nodes[tree.prefix + curClass.ID];
+      if (node) {
+        handleSelectionChanged(node.serialNo())
+      }
+    }
+  }
+
+
+
+  $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 }));
+
+  // 匹配总表
+  $matchSummary.click(_.debounce(async () => {
+    $.bootstrapLoading.progressStart('匹配总表', true);
+    $("#progress_modal_body").text('正在匹配总表,请稍后...');
+    try {
+      await ajaxPost('/priceInfo/matchSummary', { libID, compilationID }, 1000 * 60 * 10);
+      $.bootstrapLoading.progressEnd();
+      window.location.reload()
+    } catch (error) {
+      alert(error)
+      console.log(error);
+      $.bootstrapLoading.progressEnd();
+    }
+  }, DEBOUNCE_TIME, { leading: true }));
+
+  // 显示空数据
+  $showEmpty.click(() => {
+    $('#empty-area').modal('show');
+  });
+
+
+  return {
+    initData,
+    handleSelectionChanged,
+    curClass,
+    reload,
+  }
+
+})();

+ 314 - 0
web/maintain/price_info_lib/js/priceEmpty.js

@@ -0,0 +1,314 @@
+// 空数据表
+const EMPTY_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: 100, dataCode: 'expString', dataType: 'String', hAlign: 'left', vAlign: 'center' },
+      { headerName: '材料名称', headerWidth: 275, dataCode: 'name', dataType: 'String', hAlign: 'left', vAlign: 'center' },
+      { headerName: '规格型号', headerWidth: 180, dataCode: 'specs', dataType: 'String', hAlign: 'left', vAlign: 'center' },
+      { headerName: '单位', headerWidth: 80, dataCode: 'unit', dataType: 'String', hAlign: 'center', vAlign: 'center' },
+    ],
+  };
+  // 初始化表格
+  const workBookObj = {
+    book: null,
+    sheet: null,
+  };
+  const buildWorkBook = () => {
+    workBookObj.book = initSheet($('.empty-spread')[0], setting);
+    workBookObj.book.options.allowUserDragDrop = true;
+    workBookObj.book.options.allowUserDragFill = true;
+    workBookObj.book.options.allowExtendPasteRange = false;
+    lockUtil.lockSpreads([workBookObj.book], locked);
+    workBookObj.sheet = workBookObj.book.getSheet(0);
+  }
+
+
+  // 总数据(数据没合并前)
+  let totalData = [];
+  let totalMap = {};
+
+  // 表格显示的数据(合并后)
+  const cache = [];
+
+  const getCompareKey = (item) => {
+    const props = ['name', 'specs', 'unit'];
+    return props.map(prop => item[prop] ? item[prop].trim() : '').join('@');
+  }
+
+  const setTotalMap = (items) => {
+    totalMap = {};
+    items.forEach(item => {
+      const key = getCompareKey(item);
+      if (totalMap[key]) {
+        totalMap[key].push(item);
+      } else {
+        totalMap[key] = [item];
+      }
+    })
+  }
+
+  // 获取表格数据,汇总空数据,多地区可能存在相同材料,按名称规格单位做筛选,重复材料仅显示一条即可
+  const getTableData = (items) => {
+    const map = {};
+    items.forEach(item => {
+      const key = getCompareKey(item);
+      if (!map[key]) {
+        map[key] = { ...item };
+      }
+    });
+    return Object.values(map);
+  }
+
+  // 根据表格数据,获取实际信息价数据(一对多)
+  const getItemsFromTableItem = (item) => {
+    const key = getCompareKey(item);
+    return totalMap[key] || [];
+  }
+
+  // 获取材料关键字: 名称 规格
+  const getKeyword = (item) => {
+    return item ? `${item.name} ${item.specs}` : '';
+  }
+
+  // 改变关键字
+  const changeKeyword = (item) => {
+    const keyword = getKeyword(item);
+    $('#recommend-search').val(keyword);
+    if (!keyword) {
+      RECOMMEND_BOOK.clear();
+    } else {
+      RECOMMEND_BOOK.loadRecommendData(keyword);
+    }
+  }
+
+  // 清空
+  function clear() {
+    cache.length = 0;
+    workBookObj.sheet.setRowCount(0);
+  }
+
+  let curRow = 0;
+
+  // 初始化数据
+  async function initData() {
+    clear();
+    curRow = 0;
+    $.bootstrapLoading.start();
+    try {
+      totalData = await ajaxPost('/priceInfo/getPriceEmptyData', { libID, compilationID }, 1000 * 60 * 10);
+      setTotalMap(totalData);
+      const tableData = getTableData(totalData);
+      cache.push(...tableData)
+      showData(workBookObj.sheet, cache, setting.header);
+      changeKeyword(cache[0]);
+    } catch (err) {
+      clear();
+      alert(err);
+    } finally {
+      $.bootstrapLoading.end();
+    }
+  }
+
+  // 编辑处理
+  async function handleEdit(changedCells, diffMap, needRefresh) {
+    const postData = []; // 请求用
+    // 更新缓存用
+    const updateData = [];
+    const deleteData = [];
+    const insertData = [];
+    try {
+      changedCells.forEach(({ row }) => {
+        if (cache[row]) {
+          const rowData = getRowData(workBookObj.sheet, row, setting.header);
+          if (Object.keys(rowData).length) { // 还有数据,更新
+            let diffData;
+            if (diffMap) {
+              diffData = diffMap[row];
+            } else {
+              diffData = getRowDiffData(rowData, cache[row], setting.header);
+            }
+            if (diffData) {
+              // 改一行, 实际可能是改多行,表格一行数据是多行合并显示的
+              const items = getItemsFromTableItem(cache[row]);
+              items.forEach(item => {
+                // 只有珠海才更新计算式
+                const updateObj = { ...diffData };
+                const area = AREA_BOOK.cache.find(areaItem => areaItem.ID === item.areaID);
+                if (!area || !area.name || !/珠海/.test(area.name)) {
+                  delete updateObj.expString;
+                }
+                postData.push({ type: UpdateType.UPDATE, ID: item.ID, areaID: area.ID, compilationID, period: curLibPeriod, data: updateObj });
+              });
+              updateData.push({ row, data: diffData });
+            }
+          } else { // 该行无数据了,删除
+            const items = getItemsFromTableItem(cache[row]);
+            items.forEach(item => {
+              const area = AREA_BOOK.cache.find(areaItem => areaItem.ID === item.areaID);
+              postData.push({ type: UpdateType.DELETE, areaID: area.ID, compilationID, period: curLibPeriod, ID: item.ID });
+            });
+            deleteData.push(cache[row]);
+          }
+        }
+      });
+      if (postData.length) {
+        $.bootstrapLoading.start();
+        await ajaxPost('/priceInfo/editPriceData', { postData }, TIME_OUT);
+        // 更新缓存,先更新然后删除,最后再新增,防止先新增后缓存数据的下标与更新、删除数据的下标对应不上
+        updateData.forEach(item => {
+          // 更新总表
+          const curItem = cache[item.row];
+          const compareKey = getCompareKey(curItem);
+          const totalItems = totalMap[compareKey];
+          if (totalItems) {
+            const newCompareKey = getCompareKey({ ...curItem, ...item.data });
+            totalItems.forEach(totalItem => {
+              Object.assign(totalItem, item.data);
+            });
+            if (newCompareKey !== compareKey) {
+              totalMap[newCompareKey] = totalItems;
+              delete totalMap[compareKey];
+            }
+          }
+          // 更新表格缓存
+          Object.assign(cache[item.row], item.data);
+        });
+        deleteData.forEach(item => {
+          // 更新总表
+          const compareKey = getCompareKey(item);
+          const totalItems = totalMap[compareKey];
+          if (totalItems) {
+            const totalItemIDs = totalItems.map(item => item.ID);
+            totalData = totalData.filter(totalItem => !totalItemIDs.includes(totalItem));
+            delete totalMap[compareKey];
+          }
+          // 更新表格缓存
+          const index = cache.indexOf(item);
+          if (index >= 0) {
+            cache.splice(index, 1);
+          }
+        });
+        insertData.forEach(item => cache.push(item));
+        if (deleteData.length || insertData.length || needRefresh) {
+          showData(workBookObj.sheet, cache, setting.header);
+        }
+        $.bootstrapLoading.end();
+        CLASS_BOOK.reload();
+      }
+    } catch (err) {
+      // 恢复各单元格数据
+      showData(workBookObj.sheet, cache, setting.header);
+      $.bootstrapLoading.end();
+    }
+  }
+
+  // 跟新行的编号、编码编码
+  async function updateRowCode(code, classCode, expString) {
+    const item = cache[curRow];
+    if (!item) {
+      return;
+    }
+    const diffData = { code, classCode, expString };
+    await handleEdit([{ row: curRow }], { [curRow]: diffData }, true);
+  }
+
+  const bindEvent = () => {
+    workBookObj.sheet.bind(GC.Spread.Sheets.Events.ValueChanged, function (e, info) {
+      const changedCells = [{ row: info.row }];
+      handleEdit(changedCells);
+    });
+    workBookObj.sheet.bind(GC.Spread.Sheets.Events.SelectionChanged, function (e, info) {
+      const row = info.newSelections && info.newSelections[0] ? info.newSelections[0].row : 0;
+      if (curRow !== row) {
+        const item = cache[row];
+        changeKeyword(item);
+      }
+      curRow = row;
+    });
+    workBookObj.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);
+    });
+  }
+
+  // 将空表的表格保存至总表
+  const saveInSummary = async () => {
+    const documents = [];
+    const removeIDs = [];
+    cache.filter(item => item.code && item.classCode).forEach(item => {
+      removeIDs.push(item.ID);
+      documents.push({
+        ID: uuid.v1(),
+        code: item.code,
+        classCode: item.classCode,
+        expString: item.expString,
+        name: item.name,
+        specs: item.specs,
+        unit: item.unit,
+      });
+    });
+    if (!documents.length) {
+      alert('不存在可保存数据');
+      return;
+    }
+    console.log(documents);
+    try {
+      $.bootstrapLoading.progressStart('保存至总表', true);
+      $("#progress_modal_body").text('正在保存至总表,请稍后...');
+      await ajaxPost('/priceInfoSummary/saveInSummary', { documents }, 1000 * 60 * 5);
+      setTimeout(() => {
+        $.bootstrapLoading.progressEnd();
+        alert('保存成功');
+        const filterCache = cache.filter(item => !removeIDs.includes(item.ID));
+        cache.length = 0;
+        cache.push(...filterCache);
+        showData(workBookObj.sheet, cache, setting.header);
+      }, 1000);
+    } catch (error) {
+      setTimeout(() => {
+        $.bootstrapLoading.progressEnd();
+      }, 500);
+      console.log(error);
+      alert(error);
+    }
+  }
+
+
+  return {
+    buildWorkBook,
+    bindEvent,
+    clear,
+    initData,
+    workBookObj,
+    updateRowCode,
+    saveInSummary,
+  }
+})();
+
+$(document).ready(() => {
+  $('#empty-area').on('shown.bs.modal', function () {
+    if (!EMPTY_BOOK.workBookObj.book) {
+      EMPTY_BOOK.buildWorkBook();
+      EMPTY_BOOK.bindEvent();
+    }
+    EMPTY_BOOK.initData();
+  });
+
+  $('#empty-area').on('hidden.bs.modal', function () {
+    EMPTY_BOOK.clear();
+  });
+
+  // 保存至总表
+  $('#save-in-summary').click(() => {
+    EMPTY_BOOK.saveInSummary();
+  });
+});

+ 184 - 0
web/maintain/price_info_lib/js/priceItem.js

@@ -0,0 +1,184 @@
+// 价格信息表
+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);
+  }
+
+  // 是否正在检测重复数据
+  let isCheckingRepeat = false;
+
+  // 初始化数据
+  async function initData(classIDList) {
+    isCheckingRepeat = false;
+    $('#check-repeat').text('检测重复别名编码');
+    if (!classIDList || !classIDList.length) {
+      return clear();
+    }
+    $.bootstrapLoading.start();
+    try {
+      cache = await ajaxPost('/priceInfo/getPriceData', { classIDList }, 1000 * 60 * 10);
+      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();
+    }
+  }
+
+  // 编辑处理
+  async function handleEdit(changedCells, needLoading = true) {
+    $.bootstrapLoading.start();
+    const areaID = AREA_BOOK.curArea.ID;
+    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, areaID, compilationID, period: curLibPeriod, data: diffData });
+              updateData.push({ row, data: diffData });
+            }
+          } else { // 该行无数据了,删除
+            postData.push({ type: UpdateType.DELETE, ID: cache[row].ID, areaID, compilationID, period: curLibPeriod });
+            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, isCheckingRepeat ? 0 : 5);
+        }
+      }
+    } catch (err) {
+      // 恢复各单元格数据
+      showData(sheet, cache, setting.header, 5);
+    }
+    $.bootstrapLoading.end();
+  }
+  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);
+  });
+
+  // 显示重复别名编码数据
+  let orgCache = [...cache];
+  const showRepeatData = () => {
+    if (isCheckingRepeat) {
+      $('#check-repeat').text('检测重复别名编码');
+      isCheckingRepeat = false;
+      cache = orgCache;
+      showData(sheet, cache, setting.header, 5);
+      return;
+    }
+    $('#check-repeat').text('检测重复别名编码(检测中)');
+    isCheckingRepeat = true;
+    const tableData = [];
+    const classCodeMap = {};
+    cache.forEach(item => {
+      const classCode = item.classCode || '';
+      if (!classCodeMap[classCode]) {
+        classCodeMap[classCode] = 1;
+      } else {
+        classCodeMap[classCode] = classCodeMap[classCode] + 1;
+      }
+    });
+    cache.forEach(item => {
+      const classCode = item.classCode || '';
+      if (classCodeMap[classCode] > 1) {
+        tableData.push(item);
+      }
+    });
+    orgCache = [...cache];
+    cache = tableData;
+    showData(sheet, cache, setting.header);
+  }
+
+  return {
+    clear,
+    initData,
+    showRepeatData,
+  }
+})();
+
+$(document).ready(() => {
+  // 检测重复别名编码
+  $('#check-repeat').click(() => {
+    PRICE_BOOK.showRepeatData();
+  });
+});

+ 27 - 0
web/maintain/price_info_lib/js/priceKeyword.js

@@ -0,0 +1,27 @@
+// 关键字表
+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
+  }
+})();

+ 96 - 0
web/maintain/price_info_lib/js/priceRecommend.js

@@ -0,0 +1,96 @@
+// 推荐相似材料
+const RECOMMEND_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: 100, dataCode: 'expString', dataType: 'String', hAlign: 'left', vAlign: 'center' },
+      { headerName: '材料名称', headerWidth: 275, dataCode: 'name', dataType: 'String', hAlign: 'left', vAlign: 'center' },
+      { headerName: '规格型号', headerWidth: 180, dataCode: 'specs', dataType: 'String', hAlign: 'left', vAlign: 'center' },
+      { headerName: '单位', headerWidth: 80, dataCode: 'unit', dataType: 'String', hAlign: 'center', vAlign: 'center' },
+    ],
+  };
+  // 初始化表格
+  // 初始化表格
+  const workBookObj = {
+    book: null,
+    sheet: null,
+  };
+  const buildWorkBook = () => {
+    workBookObj.book = initSheet($('.recommend-spread')[0], setting);
+    workBookObj.book.options.allowUserDragDrop = true;
+    workBookObj.book.options.allowUserDragFill = true;
+    workBookObj.book.options.allowExtendPasteRange = false;
+    lockUtil.lockSpreads([workBookObj.book], true);
+    workBookObj.sheet = workBookObj.book.getSheet(0);
+  }
+
+  // 表格显示的数据
+  const cache = [];
+
+  // 清空
+  function clear() {
+    cache.length = 0;
+    workBookObj.sheet.setRowCount(0);
+  }
+  // 初始化数据
+  async function loadRecommendData(keyword) {
+    clear();
+    $.bootstrapLoading.start();
+    try {
+      const items = await ajaxPost('/priceInfo/getRecommendPriceSummaryData', { keyword }, 1000 * 60 * 5);
+      cache.push(...items);
+      showData(workBookObj.sheet, cache, setting.header);
+    } catch (err) {
+      clear();
+      alert(err);
+    } finally {
+      $.bootstrapLoading.end();
+    }
+  }
+
+  // 双击更新空表编码和别名编码
+  const handleDbClick = (sender, args) => {
+    const item = cache[args.row];
+    if (item) {
+      // 只有珠海才更新计算式
+      //const expString = /珠海/.test(AREA_BOOK.curArea.name || '') ? item.expString : undefined;
+      EMPTY_BOOK.updateRowCode(item.code || '', item.classCode || '', item.expString || '');
+    }
+  }
+
+  const bindEvent = () => {
+    workBookObj.book.bind(GC.Spread.Sheets.Events.CellDoubleClick, handleDbClick)
+  }
+
+
+  return {
+    buildWorkBook,
+    bindEvent,
+    clear,
+    loadRecommendData,
+    workBookObj,
+  }
+})();
+
+$(document).ready(() => {
+  $('#empty-area').on('shown.bs.modal', function () {
+    // 生成表格
+    if (!RECOMMEND_BOOK.workBookObj.book) {
+      RECOMMEND_BOOK.buildWorkBook();
+      RECOMMEND_BOOK.bindEvent();
+    }
+  });
+
+  $('#empty-area').on('hidden.bs.modal', function () {
+    RECOMMEND_BOOK.clear();
+  });
+
+  // 回车搜索分词
+  $('#recommend-search').bind('keydown', function (event) {
+    if (event.keyCode === 13) {
+      const searchStr = $(this).val();
+      RECOMMEND_BOOK.loadRecommendData(searchStr);
+    }
+  })
+});

+ 91 - 0
web/maintain/price_info_summary/css/index.css

@@ -0,0 +1,91 @@
+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%;
+}
+
+.search {
+    padding: 5px 10px;
+    /* background-color: #f7f7f7; */
+}
+
+.search-input {
+    width: 250px;
+}
+
+.main {
+    height: calc(100% - 78px);
+}
+
+.sheet {
+    height: 100%;
+}

+ 53 - 0
web/maintain/price_info_summary/html/main.html

@@ -0,0 +1,53 @@
+<!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_summary/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">材料信息价总表</span>
+            <a class="lock" data-locked="true" href="javascript:void(0);" title="解锁"><i
+                class="fa fa-unlock-alt"></i></a>
+        </nav>
+    </div>
+    <div class="search">
+        <input class="form-control form-control-sm search-input" id="summary-search" type="text" value="" placeholder="输入回车进行搜索">
+    </div>
+
+    <div class="main">
+        <div class="sheet" id="summary-spread"></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 src="/web/maintain/price_info_summary/js/summarySheet.js"></script>
+    <script src="/web/maintain/price_info_summary/js/index.js"></script>
+</body>
+</html>

+ 21 - 0
web/maintain/price_info_summary/js/index.js

@@ -0,0 +1,21 @@
+
+
+$(document).ready(() => {
+    //init();
+    // $('[data-toggle="tooltip"]').tooltip();
+    // AREA_BOOK.handleSelectionChanged(0);
+    // 锁定、解锁
+    lockUtil.displayLock($('.lock'), lockUtil.getLocked());
+    $('.lock').click(function () {
+        window.location.search = `?locked=${!lockUtil.getLocked()}`;
+    });
+
+    // 搜索
+    $('#summary-search').bind('keydown', function (event) {
+        if (event.keyCode === 13) {
+            // 回车搜索
+            const searchStr = $(this).val();
+            SUMMARY_BOOK.handleSearch(searchStr);
+        }
+    })
+});

+ 259 - 0
web/maintain/price_info_summary/js/summarySheet.js

@@ -0,0 +1,259 @@
+
+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 style = new GC.Spread.Sheets.Style();
+  style.wordWrap = true; */
+  const fuc = () => {
+    sheet.setRowCount(data.length);
+    data.forEach((item, row) => {
+      headers.forEach(({ dataCode }, col) => {
+        //sheet.setStyle(row, col, style, GC.Spread.Sheets.SheetArea.viewport);
+        sheet.setValue(row, col, item[dataCode] || '');
+      });
+      sheet.autoFitRow(row);
+    });
+    if (emptyRows) {
+      sheet.addRows(data.length, emptyRows);
+    }
+    //sheet.autoFitRow(0);
+  };
+  sheetCommonObj.renderSheetFunc(sheet, fuc);
+}
+
+// 获取当前表中行数据
+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;
+}
+
+const UpdateType = {
+  UPDATE: 'update',
+  DELETE: 'delete',
+  CREATE: 'create',
+};
+
+const TIME_OUT = 20000;
+
+const SUMMARY_BOOK = (() => {
+  const locked = lockUtil.getLocked();
+  const setting = {
+    header: [
+      { headerName: '主从对应码', headerWidth: 200, dataCode: 'code', dataType: 'String', hAlign: 'left', vAlign: 'center', formatter: "@" },
+      { headerName: '别名编码', headerWidth: 100, dataCode: 'classCode', dataType: 'String', hAlign: 'left', vAlign: 'center', formatter: "@" },
+      { headerName: '计算式', headerWidth: 100, dataCode: 'expString', dataType: 'String', hAlign: 'left', vAlign: 'center' },
+      { headerName: '材料名称', headerWidth: 350, dataCode: 'name', dataType: 'String', hAlign: 'left', vAlign: 'center' },
+      { headerName: '规格型号', headerWidth: 200, dataCode: 'specs', dataType: 'String', hAlign: 'left', vAlign: 'center' },
+      { headerName: '单位', headerWidth: 80, dataCode: 'unit', dataType: 'String', hAlign: 'center', vAlign: 'center' },
+    ],
+  };
+  // 初始化表格
+  const workBook = initSheet($('#summary-spread')[0], setting);
+  workBook.options.allowUserDragDrop = true;
+  workBook.options.allowUserDragFill = true;
+  lockUtil.lockSpreads([workBook], locked);
+  const sheet = workBook.getSheet(0);
+
+  // 当前数据缓存
+  const cache = [];
+  // 清空
+  function clear() {
+    cache.length = 0;
+    sheet.setRowCount(0);
+  }
+
+  let loading = false;
+  // 当前页面数据总量
+  let totalCount = 0;
+  // 当前页数
+  let curPage = 0;
+
+  // 搜索内容
+  let searchStr = '';
+
+  // 加载分页数据
+  const loadPageData = async (page) => {
+    curPage = page;
+    loading = true;
+    const data = await ajaxPost('/priceInfoSummary/getPagingData', { page, searchStr, pageSize: 100 }, TIME_OUT);
+    totalCount = data.totalCount;
+    cache.push(...data.items);
+    showData(sheet, cache, setting.header, 5);
+    loading = false;
+  }
+
+  // 搜索
+  const handleSearch = (val) => {
+    if (val) {
+      // 处理特殊字符,否则正则搜不到
+      searchStr = val
+        .replace(/\\/g, '\\\\')
+        .replace(/\[/g, '\\[')
+        .replace(/\]/g, '\\]')
+        .replace(/\(/g, '\\(')
+        .replace(/\)/g, '\\)')
+        .replace(/\+/g, '\\+')
+        .replace(/\?/g, '\\?')
+        .replace(/\*/g, '\\*')
+        .replace(/\$/g, '\\$')
+        .replace(/\^/g, '\\^')
+    } else {
+      searchStr = '';
+    }
+    clear();
+    loadPageData(0);
+  }
+
+
+  // 无限滚动加载
+  const onTopRowChanged = (sender, args) => {
+    const bottomRow = args.sheet.getViewportBottomRow(1);
+    console.log(cache.length, totalCount, loading, cache.length - 1, bottomRow)
+    if (cache.length >= totalCount || loading) {
+      return;
+    }
+    if (cache.length - 1 <= bottomRow) {
+      loadPageData(curPage + 1, searchStr);
+    }
+  }
+  sheet.bind(GC.Spread.Sheets.Events.TopRowChanged, _.debounce(onTopRowChanged, 100));
+
+  // 编辑处理
+  async function handleEdit(changedCells) {
+    $.bootstrapLoading.start();
+    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();
+            postData.push({ type: UpdateType.CREATE, data: rowData });
+            insertData.push(rowData);
+          }
+        }
+      });
+      if (postData.length) {
+        await ajaxPost('/priceInfoSummary/editSummaryData', { postData }, 1000 * 60 * 2);
+        // 更新缓存,先更新然后删除,最后再新增,防止先新增后缓存数据的下标与更新、删除数据的下标对应不上
+        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);
+    }
+    $.bootstrapLoading.end();
+  }
+  sheet.bind(GC.Spread.Sheets.Events.ValueChanged, function (e, info) {
+    const changedCells = [{ row: info.row }];
+    handleEdit(changedCells);
+  });
+  sheet.bind(GC.Spread.Sheets.Events.RangeChanged, function (e, info) {
+    const changedRows = [];
+    let preRow;
+    info.changedCells.forEach(({ row }) => {
+      if (row !== preRow) {
+        changedRows.push({ row });
+      }
+      preRow = row;
+    });
+    handleEdit(changedRows);
+  });
+
+  const initData = async () => {
+    try {
+      $.bootstrapLoading.start();
+      await loadPageData(0);
+    } catch (error) {
+      console.log(error);
+      alert(error.message);
+    }
+    $.bootstrapLoading.end();
+  }
+
+  initData();
+
+  return {
+    sheet,
+    handleSearch,
+  }
+})()