Browse Source

feat: 信息价库显示空数据、检测重复别名数据

vian 1 year ago
parent
commit
1fdeb1c3a4

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

@@ -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);

+ 82 - 3
modules/price_info_lib/facade/index.js

@@ -12,7 +12,7 @@ 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) {
     return await priceInfoLibModel.find(query).lean();
@@ -477,17 +477,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 {
@@ -693,6 +704,72 @@ const matchSummary = async (compilationID, libID) => {
     }
 }
 
+// 获取空数据(没有别名编码)
+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,
@@ -713,4 +790,6 @@ module.exports = {
     editClassData,
     exportExcelData,
     matchSummary,
+    getPriceEmptyData,
+    getRecommendPriceSummaryData,
 }

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

@@ -26,6 +26,8 @@ module.exports = function (app) {
     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);
 
     router.get("/export", priceInfoController.auth, priceInfoController.init, priceInfoController.exportPriceData);
 

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

@@ -40,6 +40,18 @@ class PriceInfoSummaryController extends BaseController {
         }
     }
 
+    // 保存至总表
+    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 = {

+ 7 - 1
modules/price_info_summary/facade/index.js

@@ -12,7 +12,7 @@ const getPagingData = async (page, pageSize, searchStr) => {
         }
     }
     const totalCount = await priceInfoSummaryModel.count(query);
-    const items = await priceInfoSummaryModel.find(query).lean().sort({ classCode: 1 }).skip(page * pageSize).limit(pageSize);
+    const items = await priceInfoSummaryModel.find(query).lean().sort({ classCode: -1 }).skip(page * pageSize).limit(pageSize);
     return { items, totalCount };
 }
 
@@ -52,7 +52,13 @@ async function editSummaryData(postData) {
     }
 }
 
+// 保存至总表
+async function saveInSummary(documents) {
+    await priceInfoSummaryModel.insertMany(documents);
+}
+
 module.exports = {
     getPagingData,
     editSummaryData,
+    saveInSummary,
 }

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

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

+ 1 - 0
package.json

@@ -36,6 +36,7 @@
     "node-schedule": "^1.3.0",
     "node-xlsx": "^0.11.2",
     "pdfkit": "^0.8.2",
+    "segmentit": "^2.0.3",
     "ueditor": "^1.2.3"
   },
   "scripts": {

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

+ 86 - 83
public/web/sheet/sheet_common.js

@@ -4,7 +4,7 @@
 var sheetCommonObj = {
     // createSpread、initSheet 在一个Spread多个Sheet分别调用时的情况下使用。
     // buildSheet 在一个Spread、一个Sheet的情况下使用。
-    createSpread: function(container, SheetCount){
+    createSpread: function (container, SheetCount) {
         var me = this;
         var spreadBook = new GC.Spread.Sheets.Workbook(container, { sheetCount: SheetCount });
         spreadBook.options.allowCopyPasteExcelStyle = false;
@@ -20,7 +20,7 @@ var sheetCommonObj = {
         return spreadBook;
     },
 
-    initSheet: function(sheet, setting, rowCount) {
+    initSheet: function (sheet, setting, rowCount) {
         var me = this;
         var spreadNS = GC.Spread.Sheets;
         sheet.suspendPaint();
@@ -41,7 +41,7 @@ var sheetCommonObj = {
         sheet.resumePaint();
     },
 
-    buildSheet: function(container, setting, rowCount) {
+    buildSheet: function (container, setting, rowCount, spreadOptions) {
         var me = this;
         var spreadBook = new GC.Spread.Sheets.Workbook(container, { sheetCount: 1 });
         spreadBook.options.tabStripVisible = false;
@@ -54,6 +54,9 @@ var sheetCommonObj = {
         spreadBook.options.allowUserDragFill = false;
         spreadBook.options.allowUndo = false;
         spreadBook.options.allowContextMenu = false;
+        if (spreadOptions) {
+            Object.assign(spreadBook.options, spreadOptions)
+        }
         var spreadNS = GC.Spread.Sheets;
         var sheet = spreadBook.getSheet(0);
         sheet.suspendPaint();
@@ -77,13 +80,13 @@ var sheetCommonObj = {
         sheet.resumePaint();
         return spreadBook;
     },
-    buildHeader: function(sheet, setting){
+    buildHeader: function (sheet, setting) {
         var me = this, ch = GC.Spread.Sheets.SheetArea.colHeader;
         for (var i = 0; i < setting.header.length; i++) {
             sheet.setValue(0, i, setting.header[i].headerName, ch);
             sheet.setColumnWidth(i, setting.header[i].headerWidth ? setting.header[i].headerWidth : 100);
         }
-        if(setting.headerHeight)  sheet.setRowHeight(0, setting.headerHeight, GC.Spread.Sheets.SheetArea.colHeader);
+        if (setting.headerHeight) sheet.setRowHeight(0, setting.headerHeight, GC.Spread.Sheets.SheetArea.colHeader);
     },
     cleanData: function (sheet, setting, rowCount) {
         sheet.suspendPaint();
@@ -93,7 +96,7 @@ var sheetCommonObj = {
         sheet.resumeEvent();
         sheet.resumePaint();
     },
-    cleanSheet: function(sheet, setting, rowCount) {
+    cleanSheet: function (sheet, setting, rowCount) {
         sheet.suspendPaint();
         sheet.suspendEvent();
         sheet.clear(-1, 0, -1, setting.header.length, GC.Spread.Sheets.SheetArea.viewport, GC.Spread.Sheets.StorageType.data);
@@ -102,7 +105,7 @@ var sheetCommonObj = {
         sheet.resumeEvent();
         sheet.resumePaint();
     },
-    setAreaAlign: function(area, hAlign, vAlign){
+    setAreaAlign: function (area, hAlign, vAlign) {
         if (!(hAlign) || hAlign === "left") {
             area.hAlign(GC.Spread.Sheets.HorizontalAlign.left);
         } else if (hAlign === "right") {
@@ -122,118 +125,118 @@ var sheetCommonObj = {
             area.vAlign(GC.Spread.Sheets.VerticalAlign.center);
         }
     },
-    showData: function(sheet, setting, data,distTypeTree) {
+    showData: function (sheet, setting, data, distTypeTree) {
         var me = this, ch = GC.Spread.Sheets.SheetArea.viewport;
         sheet.suspendPaint();
         sheet.suspendEvent();
         //sheet.addRows(row, 1);
 
         sheet.clear(0, 0, sheet.getRowCount(), sheet.getColumnCount(), GC.Spread.Sheets.SheetArea.viewport, GC.Spread.Sheets.StorageType.data);
-        if(sheet.getRowCount()<data.length){
-            data.length<30? sheet.setRowCount(30):sheet.setRowCount(data.length);
-        }else if(sheet.getRowCount()==0){
+        if (sheet.getRowCount() < data.length) {
+            data.length < 30 ? sheet.setRowCount(30) : sheet.setRowCount(data.length);
+        } else if (sheet.getRowCount() == 0) {
             sheet.setRowCount(30);
         }
         for (var col = 0; col < setting.header.length; col++) {
             var hAlign = "left", vAlign = "center";
             if (setting.header[col].hAlign) {
                 hAlign = setting.header[col].hAlign;
-            } else if (setting.header[col].dataType !== "String"){
+            } else if (setting.header[col].dataType !== "String") {
                 hAlign = "right";
             }
-            vAlign = setting.header[col].vAlign?setting.header[col].vAlign:vAlign;
+            vAlign = setting.header[col].vAlign ? setting.header[col].vAlign : vAlign;
             me.setAreaAlign(sheet.getRange(-1, col, -1, 1), hAlign, vAlign);
             if (setting.header[col].formatter) {
                 sheet.setFormatter(-1, col, setting.header[col].formatter, GC.Spread.Sheets.SheetArea.viewport);
             }
-            if(setting.header[col].cellType === "checkBox"||setting.header[col].cellType === "button"){//clear and reset
+            if (setting.header[col].cellType === "checkBox" || setting.header[col].cellType === "button") {//clear and reset
                 var me = this, header = GC.Spread.Sheets.SheetArea.colHeader;
-                sheet.deleteColumns(col,1);
+                sheet.deleteColumns(col, 1);
                 sheet.addColumns(col, 1);
                 sheet.setValue(0, col, setting.header[col].headerName, header);
-                sheet.setColumnWidth(col, setting.header[col].headerWidth?setting.header[col].headerWidth:100);
+                sheet.setColumnWidth(col, setting.header[col].headerWidth ? setting.header[col].headerWidth : 100);
             }
-            if(setting.header[col].visible === false){
-                sheet.setColumnVisible(col,false);
+            if (setting.header[col].visible === false) {
+                sheet.setColumnVisible(col, false);
             }
             sheet.getCell(0, col, GC.Spread.Sheets.SheetArea.colHeader).wordWrap(true);
         }
         for (var row = 0; row < data.length; row++) {
             //var cell = sheet.getCell(row, col, GC.Spread.Sheets.SheetArea.viewport);
-            this.showRowData(sheet,setting,row,data,distTypeTree);
-            if(setting.getStyle && setting.getStyle(data[row])){
+            this.showRowData(sheet, setting, row, data, distTypeTree);
+            if (setting.getStyle && setting.getStyle(data[row])) {
                 sheet.setStyle(row, -1, setting.getStyle(data[row]));
             }
         }
-        this.lockCells(sheet,setting);
+        this.lockCells(sheet, setting);
         sheet.resumeEvent();
         sheet.resumePaint();
         //me.shieldAllCells(sheet);
     },
-    getCheckBox(threeState = false){
+    getCheckBox(threeState = false) {
         var c = new GC.Spread.Sheets.CellTypes.CheckBox();
         c.isThreeState(threeState);
         return c
     },
     // 无法勾选的复选框
-    getReadOnlyCheckBox (threeState = false) {
-        function ReadOnlyCheckBox() {}
+    getReadOnlyCheckBox(threeState = false) {
+        function ReadOnlyCheckBox() { }
         ReadOnlyCheckBox.prototype = this.getCheckBox(threeState);
         ReadOnlyCheckBox.prototype.processMouseUp = function () {
             return;
         };
         return new ReadOnlyCheckBox();
     },
-    showRowData:function (sheet,setting,row,data,distTypeTree=null) {
+    showRowData: function (sheet, setting, row, data, distTypeTree = null) {
         let ch = GC.Spread.Sheets.SheetArea.viewport;
         for (var col = 0; col < setting.header.length; col++) {
             //var cell = sheet.getCell(row, col, GC.Spread.Sheets.SheetArea.viewport);
             var val = data[row][setting.header[col].dataCode];
-            if(val&&setting.header[col].dataType === "Number"){
-                if(setting.header[col].hasOwnProperty('tofix')){
-                    val =scMathUtil.roundToString(val,setting.header[col].tofix);
+            if (val && setting.header[col].dataType === "Number") {
+                if (setting.header[col].hasOwnProperty('tofix')) {
+                    val = scMathUtil.roundToString(val, setting.header[col].tofix);
                 } else {
-                    val =val+'';
+                    val = val + '';
                 }
             }
-            if(val!=null&&setting.header[col].cellType === "checkBox"){
-                this.setCheckBoxCell(row,col,sheet,val)
+            if (val != null && setting.header[col].cellType === "checkBox") {
+                this.setCheckBoxCell(row, col, sheet, val)
             }
-            if(setting.header[col].cellType === "comboBox"){
-                this.setComboBox(row,col,sheet,setting.header[col].options,setting.header[col].editorValueType);
+            if (setting.header[col].cellType === "comboBox") {
+                this.setComboBox(row, col, sheet, setting.header[col].options, setting.header[col].editorValueType);
             }
-            if(setting.header[col].getText){
-                val = setting.getText[setting.header[col].getText](data[row],val)
+            if (setting.header[col].getText) {
+                val = setting.getText[setting.header[col].getText](data[row], val)
             }
             sheet.setValue(row, col, val, ch);
         }
-        this.setRowStyle(row,sheet,data[row].bgColour);
-        if(setting.autoFit==true){
+        this.setRowStyle(row, sheet, data[row].bgColour);
+        if (setting.autoFit == true) {
             sheet.getRange(row, -1, 1, -1, GC.Spread.Sheets.SheetArea.viewport).wordWrap(true);
             sheet.autoFitRow(row);
         }
     },
-    setCheckBoxCell(row,col,sheet,val){
+    setCheckBoxCell(row, col, sheet, val) {
         var c = new GC.Spread.Sheets.CellTypes.CheckBox();
         c.isThreeState(false);
-        sheet.setCellType(row, col,c,GC.Spread.Sheets.SheetArea.viewport);
+        sheet.setCellType(row, col, c, GC.Spread.Sheets.SheetArea.viewport);
         sheet.getCell(row, col).value(val);
         sheet.getCell(row, col).hAlign(GC.Spread.Sheets.HorizontalAlign.center);
 
     },
-    setComboBox(row,col,sheet,options,editorValueType){
+    setComboBox(row, col, sheet, options, editorValueType) {
         //let combo = new GC.Spread.Sheets.CellTypes.ComboBox();
         let dynamicCombo = sheetCommonObj.getDynamicCombo(true);
-        if(options){
+        if (options) {
             dynamicCombo.itemHeight(options.length).items(options);
-            if(editorValueType==true){
+            if (editorValueType == true) {
                 dynamicCombo.editorValueType(GC.Spread.Sheets.CellTypes.EditorValueType.value);
             }
         }
-        sheet.setCellType(row, col,dynamicCombo,GC.Spread.Sheets.SheetArea.viewport);
+        sheet.setCellType(row, col, dynamicCombo, GC.Spread.Sheets.SheetArea.viewport);
     },
-    setRowStyle(row,sheet,bgColour) {
-        if(bgColour){
+    setRowStyle(row, sheet, bgColour) {
+        if (bgColour) {
             let style = new GC.Spread.Sheets.Style();
             style.backColor = bgColour;
             style.borderLeft = new GC.Spread.Sheets.LineBorder("#D4D4D4", GC.Spread.Sheets.LineStyle.thin);
@@ -243,7 +246,7 @@ var sheetCommonObj = {
             sheet.setStyle(row, -1, style);
         }
     },
-    analyzePasteData: function(setting, pastedInfo) {
+    analyzePasteData: function (setting, pastedInfo) {
         var rst = [], propId = pastedInfo.cellRange.col, preStrIdx = 0, itemObj = {};
         for (var i = 0; i < pastedInfo.pasteData.text.length; i++) {
             if (pastedInfo.pasteData.text[i] === "\n") {
@@ -271,17 +274,17 @@ var sheetCommonObj = {
         }
         return rst;
     },
-    combineRowData: function(sheet, setting, row) {
+    combineRowData: function (sheet, setting, row) {
         var rst = {};
         for (var col = 0; col < setting.header.length; col++) {
             rst[setting.header[col].dataCode] = sheet.getValue(row, col);
         }
         return rst;
     },
-    shieldAllCells: function(sheet) {
+    shieldAllCells: function (sheet) {
         sheet.options.isProtected = true;
     },
-    unShieldAllCells: function(sheet) {
+    unShieldAllCells: function (sheet) {
         sheet.options.isProtected = false;
     },
     unLockAllCells: function (sheet) {
@@ -301,21 +304,21 @@ var sheetCommonObj = {
         let defaultStyle = new GC.Spread.Sheets.Style();
         defaultStyle.locked = true;
         sheet.setDefaultStyle(defaultStyle, GC.Spread.Sheets.SheetArea.viewport);
-        for(let i = 0; i < sheet.getRowCount(); i++){
+        for (let i = 0; i < sheet.getRowCount(); i++) {
             sheet.setStyle(i, 0, defaultStyle);
         }
         sheet.options.isProtected = true;
         sheet.resumePaint();
         sheet.resumeEvent();
     },
-    lockCells: function(sheet, setting){
+    lockCells: function (sheet, setting) {
         sheet.suspendPaint();
         sheet.suspendEvent();
         if (setting && setting.view.lockColumns && setting.view.lockColumns.length > 0) {
             sheet.options.isProtected = true;
             sheet.getRange(-1, 0, -1, setting.header.length, GC.Spread.Sheets.SheetArea.viewport).locked(false);
             for (var i = 0; i < setting.view.lockColumns.length; i++) {
-                sheet.getRange(-1,setting.view.lockColumns[i] , -1, 1, GC.Spread.Sheets.SheetArea.viewport).locked(true);
+                sheet.getRange(-1, setting.view.lockColumns[i], -1, 1, GC.Spread.Sheets.SheetArea.viewport).locked(true);
             }
         }
         sheet.resumePaint();
@@ -324,13 +327,13 @@ var sheetCommonObj = {
     setLockCol: function (sheet, col, isLocked) {
         sheet.suspendPaint();
         sheet.suspendEvent();
-        for(let row = 0, len = sheet.getRowCount(); row < len; row++){
+        for (let row = 0, len = sheet.getRowCount(); row < len; row++) {
             sheet.getCell(row, col).locked(isLocked);
         }
         sheet.resumePaint();
         sheet.resumeEvent();
     },
-    chkIfEmpty: function(rObj, setting) {
+    chkIfEmpty: function (rObj, setting) {
         var rst = true;
         if (rObj) {
             for (var i = 0; i < setting.header.length; i++) {
@@ -350,7 +353,7 @@ var sheetCommonObj = {
         ComboCellForActiveCell.prototype.paintValue = function (ctx, value, x, y, w, h, style, options) {
             let sheet = options.sheet;
             if (options.row === sheet.getActiveRowIndex() && options.col === sheet.getActiveColumnIndex()) {
-                    GC.Spread.Sheets.CellTypes.ComboBox.prototype.paintValue.apply(this, arguments);
+                GC.Spread.Sheets.CellTypes.ComboBox.prototype.paintValue.apply(this, arguments);
             } else {
                 GC.Spread.Sheets.CellTypes.Base.prototype.paintValue.apply(this, arguments);
             }
@@ -369,14 +372,14 @@ var sheetCommonObj = {
         let me = this;
         sheet.suspendPaint();
         let combo = me.getDynamicCombo();
-        if(itemsHeight) {
+        if (itemsHeight) {
             combo.itemHeight(itemsHeight);
             combo._maxDropDownItems = itemsHeight + 5;
         }
-        if(itemsType === 'value') combo.items(items).editorValueType(GC.Spread.Sheets.CellTypes.EditorValueType.value);
-        else if(itemsType === 'text') combo.items(items).editorValueType(GC.Spread.Sheets.CellTypes.EditorValueType.text);
+        if (itemsType === 'value') combo.items(items).editorValueType(GC.Spread.Sheets.CellTypes.EditorValueType.value);
+        else if (itemsType === 'text') combo.items(items).editorValueType(GC.Spread.Sheets.CellTypes.EditorValueType.text);
         else combo.items(items);
-        for(let i = 0, len = rowCount; i < len; i++){
+        for (let i = 0, len = rowCount; i < len; i++) {
             sheet.getCell(beginRow + i, col).cellType(combo);
         }
         sheet.resumePaint();
@@ -384,10 +387,10 @@ var sheetCommonObj = {
     setStaticCombo: function (sheet, beginRow, col, rowCount, items, itemsHeight, itemsType) {
         sheet.suspendPaint();
         let combo = new GC.Spread.Sheets.CellTypes.ComboBox();
-        for(let i = 0, len = rowCount; i < len; i++){
-            if(itemsHeight) combo.itemHeight(itemsHeight);
-            if(itemsType === 'value') combo.items(items).editorValueType(GC.Spread.Sheets.CellTypes.EditorValueType.value);
-            else if(itemsType === 'text') combo.items(items).editorValueType(GC.Spread.Sheets.CellTypes.EditorValueType.text);
+        for (let i = 0, len = rowCount; i < len; i++) {
+            if (itemsHeight) combo.itemHeight(itemsHeight);
+            if (itemsType === 'value') combo.items(items).editorValueType(GC.Spread.Sheets.CellTypes.EditorValueType.value);
+            else if (itemsType === 'text') combo.items(items).editorValueType(GC.Spread.Sheets.CellTypes.EditorValueType.text);
             else combo.items(items);
             sheet.getCell(beginRow + i, col).cellType(combo);
         }
@@ -401,45 +404,45 @@ var sheetCommonObj = {
     },
     //解决esc后触发了编辑结束的保存事件,显示与实际数据不同问题
     bindEscKey: function (workBook, sheets) {
-        function isDef(v){
+        function isDef(v) {
             return typeof v !== 'undefined' && v !== null;
         }
         workBook.commandManager().register('myEsc', function () {
             let activeSheet = workBook.getActiveSheet();
             let hasTheSheet = false;
-            for(let sheetObj of sheets){
+            for (let sheetObj of sheets) {
                 let sheet = sheetObj.sheet;
-                if(sheet === activeSheet){
+                if (sheet === activeSheet) {
                     hasTheSheet = true;
                     let editStarting = sheetObj.editStarting;
                     let editEnded = sheetObj.editEnded;
-                    if(editStarting){
+                    if (editStarting) {
                         sheet.unbind(GC.Spread.Sheets.Events.EditStarting);
                     }
-                    if(editEnded){
+                    if (editEnded) {
                         sheet.unbind(GC.Spread.Sheets.Events.EditEnded);
                     }
                     let row = sheet.getActiveRowIndex();
                     let col = sheet.getActiveColumnIndex();
                     let orgV = sheet.getValue(row, col);
-                    if(!isDef(orgV)){
+                    if (!isDef(orgV)) {
                         orgV = '';
                     }
-                    if(sheet.isEditing()){
+                    if (sheet.isEditing()) {
                         sheet.endEdit();
                         sheet.setValue(row, col, orgV);
                     }
-                    if(editStarting){
+                    if (editStarting) {
                         sheet.bind(GC.Spread.Sheets.Events.EditStarting, editStarting);
                     }
-                    if(editEnded){
+                    if (editEnded) {
                         sheet.bind(GC.Spread.Sheets.Events.EditEnded, editEnded);
                     }
                 }
             }
             //容错处理,以防没把所有工作簿的表格信息传入参数
-            if(!hasTheSheet){
-                if(activeSheet.isEditing()){
+            if (!hasTheSheet) {
+                if (activeSheet.isEditing()) {
                     activeSheet.endEdit();
                 }
             }
@@ -451,8 +454,8 @@ var sheetCommonObj = {
     initColMapping: function (obj, headers) {
         //colToField 列下标与列字段映射
         //fieldToCol 列字段与列下标映射
-        let colMapping = {colToField: {}, fieldToCol: {}};
-        for(let header of headers){
+        let colMapping = { colToField: {}, fieldToCol: {} };
+        for (let header of headers) {
             colMapping['colToField'][headers.indexOf(header)] = header.dataCode;
             colMapping['fieldToCol'][header.dataCode] = headers.indexOf(header);
         }
@@ -460,17 +463,17 @@ var sheetCommonObj = {
         obj.colMapping = colMapping
     },
     //动态根据工作簿宽度和各列宽度比例设置宽度
-    setColumnWidthByRate: function (workBookWidth, workBook, headers){
-        if(workBook){
+    setColumnWidthByRate: function (workBookWidth, workBook, headers) {
+        if (workBook) {
             const sheet = workBook.getActiveSheet();
             sheet.suspendEvent();
             sheet.suspendPaint();
-            for(let col = 0; col < headers.length; col++){
-                if(headers[col]['rateWidth'] !== undefined && headers[col]['rateWidth'] !== null && headers[col]['rateWidth'] !== ''){
+            for (let col = 0; col < headers.length; col++) {
+                if (headers[col]['rateWidth'] !== undefined && headers[col]['rateWidth'] !== null && headers[col]['rateWidth'] !== '') {
                     sheet.setColumnWidth(col, workBookWidth * headers[col]['rateWidth'], GC.Spread.Sheets.SheetArea.colHeader)
                 }
                 else {
-                    if(headers[col]['headerWidth'] !== undefined && headers[col]['headerWidth'] !== null && headers[col]['headerWidth'] !== ''){
+                    if (headers[col]['headerWidth'] !== undefined && headers[col]['headerWidth'] !== null && headers[col]['headerWidth'] !== '') {
                         sheet.setColumnWidth(col, headers[col]['headerWidth'], GC.Spread.Sheets.SheetArea.colHeader)
                     }
                 }
@@ -479,10 +482,10 @@ var sheetCommonObj = {
             sheet.resumePaint();
         }
     },
-    renderSheetFunc: function (sheet, func){
+    renderSheetFunc: function (sheet, func) {
         sheet.suspendEvent();
         sheet.suspendPaint();
-        if(func){
+        if (func) {
             func();
         }
         sheet.resumeEvent();

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

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

@@ -21,6 +21,8 @@
                     class="fa fa-angle-right fa-fw"></i><%= libName  %>
                <button id="calc-price-index">计算指数</button>     
                <button id="match-summary">匹配总表</button> 
+               <button id="show-empty">显示空数据</button> 
+               <button id="check-repeat">检测重复别名编码</button> 
             </div>
 
         </nav>
@@ -84,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>
@@ -115,6 +139,8 @@
     <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>

+ 33 - 2
web/maintain/price_info_lib/js/common.js

@@ -42,7 +42,38 @@ function showData(sheet, data, headers, emptyRows) {
   sheetCommonObj.renderSheetFunc(sheet, fuc);
 }
 
-const TIME_OUT = 10000;
+// 获取当前表中行数据
+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 = {
@@ -53,4 +84,4 @@ const UpdateType = {
 
 const DEBOUNCE_TIME = 200;
 
-const locked = lockUtil.getLocked();
+const locked = lockUtil.getLocked();

+ 3 - 2
web/maintain/price_info_lib/js/priceArea.js

@@ -23,7 +23,6 @@ const AREA_BOOK = (() => {
 
   // 编辑处理
   async function handleEdit(changedCells) {
-    debugger;
     const updateData = [];
     let reSort = false;
     changedCells.forEach(({ row, col }) => {
@@ -64,7 +63,7 @@ const AREA_BOOK = (() => {
     handleEdit(info.changedCells);
   });
 
-  const curArea = { ID: null };
+  const curArea = { ID: null, name: '' };
   // 焦点变更处理
   const debounceSelectionChanged = _.debounce(function (e, info) {
     const row = info.newSelections && info.newSelections[0] ? info.newSelections[0].row : 0;
@@ -73,6 +72,7 @@ const AREA_BOOK = (() => {
   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);
@@ -173,6 +173,7 @@ const AREA_BOOK = (() => {
   return {
     handleSelectionChanged,
     curArea,
+    cache,
   }
 
 })();

+ 18 - 1
web/maintain/price_info_lib/js/priceClass.js

@@ -104,6 +104,7 @@ const CLASS_BOOK = (() => {
   const $upMove = $('#tree-up-move');
   const $calcPriceIndex = $('#calc-price-index');
   const $matchSummary = $('#match-summary');
+  const $showEmpty = $('#show-empty');
 
   // 插入
   let canInsert = true;
@@ -404,6 +405,7 @@ const CLASS_BOOK = (() => {
   // 焦点变更处理
   const curClass = { ID: null };
   function handleSelectionChanged(row) {
+    curRow = row;
     const classNode = tree.items[row] || null;
     tree.selected = classNode;
     refreshTreeButton(classNode);
@@ -422,6 +424,15 @@ const CLASS_BOOK = (() => {
   }, 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 () => {
@@ -459,13 +470,19 @@ const CLASS_BOOK = (() => {
       console.log(error);
       $.bootstrapLoading.progressEnd();
     }
-  }, DEBOUNCE_TIME, { leading: true }))
+  }, DEBOUNCE_TIME, { leading: true }));
+
+  // 显示空数据
+  $showEmpty.click(() => {
+    $('#empty-area').modal('show');
+  });
 
 
   return {
     initData,
     handleSelectionChanged,
     curClass,
+    reload,
   }
 
 })();

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

@@ -0,0 +1,309 @@
+// 空数据表
+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) {
+    $.bootstrapLoading.start();
+    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) {
+        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 = cache.map(item => ({
+      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;
+    }
+    if (documents.some(doc => !doc.classCode)) {
+      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('保存成功');
+      }, 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();
+  });
+});

+ 53 - 36
web/maintain/price_info_lib/js/priceItem.js

@@ -26,8 +26,14 @@ const PRICE_BOOK = (() => {
     cache = [];
     sheet.setRowCount(0);
   }
+
+  // 是否正在检测重复数据
+  let isCheckingRepeat = false;
+
   // 初始化数据
   async function initData(classIDList) {
+    isCheckingRepeat = false;
+    $('#check-repeat').text('检测重复别名编码');
     if (!classIDList || !classIDList.length) {
       return clear();
     }
@@ -48,40 +54,10 @@ const PRICE_BOOK = (() => {
     }
   }
 
-  // 获取当前表中行数据
-  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) {
+  async function handleEdit(changedCells, needLoading = true) {
     $.bootstrapLoading.start();
+    const areaID = AREA_BOOK.curArea.ID;
     const postData = []; // 请求用
     // 更新缓存用
     const updateData = [];
@@ -94,11 +70,11 @@ const PRICE_BOOK = (() => {
           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 });
+              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 });
+            postData.push({ type: UpdateType.DELETE, ID: cache[row].ID, areaID, compilationID, period: curLibPeriod });
             deleteData.push(cache[row]);
           }
         } else { // 新增
@@ -129,7 +105,7 @@ const PRICE_BOOK = (() => {
         });
         insertData.forEach(item => cache.push(item));
         if (deleteData.length || insertData.length) {
-          showData(sheet, cache, setting.header, 5);
+          showData(sheet, cache, setting.header, isCheckingRepeat ? 0 : 5);
         }
       }
     } catch (err) {
@@ -160,8 +136,49 @@ const PRICE_BOOK = (() => {
     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();
+  });
+});

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