Bläddra i källkod

信息价库,导入excel功能

vian 4 år sedan
förälder
incheckning
d20e8039f5

+ 29 - 8
modules/all_models/std_price_info_items.js

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

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

@@ -1,5 +1,8 @@
 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");
 
@@ -49,6 +52,7 @@ class PriceInfoController extends BaseController {
         const renderData = {
             compilationID: libs[0].compilationID,
             libName: libs[0].name,
+            period: libs[0].period,
             areaList: JSON.stringify(areaList),
             userAccount: req.session.managerData.username,
             userID: req.session.managerData.userID,
@@ -102,6 +106,61 @@ class PriceInfoController extends BaseController {
         }
     }
 
+    async importExcel(req, res) {
+        let responseData = {
+            err: 0,
+            msg: ''
+        };
+        res.setTimeout(1000 * 60 * 10);
+        const allowHeader = ['application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'];
+        const uploadOption = {
+            uploadDir: './public'
+        };
+        const form = new multiparty.Form(uploadOption);
+        let uploadFullName;
+        form.parse(req, async function (err, fields, files) {
+            try {
+                const libID = fields.libID !== undefined && fields.libID.length > 0 ?
+                    fields.libID[0] : null;
+                if (!libID) {
+                    throw '参数错误。';
+                }
+                const file = files.file !== undefined ? files.file[0] : null;
+                if (err || file === null) {
+                    throw '上传失败。';
+                }
+                // 判断类型
+                if (file.headers['content-type'] === undefined || allowHeader.indexOf(file.headers['content-type']) < 0) {
+                    throw '不支持该类型';
+                }
+                // 重命名文件名
+                uploadFullName = uploadOption.uploadDir + '/' + file.originalFilename;
+                fs.renameSync(file.path, uploadFullName);
+
+                const sheet = excel.parse(uploadFullName);
+                if (sheet[0] === undefined || sheet[0].data === undefined || sheet[0].data.length <= 0) {
+                    throw 'excel没有对应数据。';
+                }
+                // 提取excel数据并入库
+                await facade.importExcelData(libID, sheet[0].data);
+                // 删除文件
+                if (uploadFullName && fs.existsSync(uploadFullName)) {
+                    fs.unlink(uploadFullName);
+                }
+                res.json(responseData);
+            }
+            catch (error) {
+                console.log(error);
+                if (uploadFullName && fs.existsSync(uploadFullName)) {
+                    fs.unlink(uploadFullName);
+                }
+                responseData.err = 1;
+                responseData.msg = error.toString();
+                res.json(responseData);
+            }
+        });
+    }
+
     async editArea(req, res) {
         try {
             const { updateData } = JSON.parse(req.body.data);

+ 125 - 1
modules/price_info_lib/facade/index.js

@@ -59,6 +59,120 @@ async function crawlDataByCompilation(compilationID, from, to) {
     await crawlData(from, to);
 }
 
+// 导入excel数据,格式如下
+// 格式1:
+//地区	    分类	        编码	名称	        规格型号	不含税价	含税价
+//江北区	黑色及有色金属		    热轧光圆钢筋	  φ6(6.5)	 3566.37	4030
+//江北区	木、竹材料及其制品		柏木门套线	      60×10	     8.76	    9.9
+// 格式2:
+//地区	    分类	            编码	名称	        规格型号	    不含税价	含税价
+//江北区	黑色及有色金属		        热轧光圆钢筋	  φ6(6.5)	     3566.37	4030
+//			                          柏木门套线	    60×10	       8.76	      9.9
+//			                          沥青混凝土	    AC-13	       982.3	 1110
+//						
+//北碚区	木、竹材料及其制品		    热轧光圆钢筋	  φ6(6.5)	      3566.37	4030
+async function importExcelData(libID, sheetData) {
+    console.log(sheetData);
+    const libs = await getLibs({ ID: libID });
+    const compilationID = libs[0].compilationID;
+    // 建立区映射表:名称-ID映射、ID-名称映射
+    const areaList = await getAreas(compilationID);
+    const areaMap = {};
+    areaList.forEach(({ ID, name }) => {
+        areaMap[name] = ID;
+        areaMap[ID] = name;
+    });
+    // 建立分类映射表:地区名称@分类名称:ID映射
+    const classMap = {};
+    const classList = await getClassData(libID);
+    classList.forEach(({ ID, areaID, name }) => {
+        const areaName = areaMap[areaID] || '';
+        classMap[`${areaName}@${name}`] = ID;
+    });
+    // 第一行获取行映射
+    const colMap = {};
+    for (let col = 0; col < sheetData[0].length; col++) {
+        const cellText = sheetData[0][col];
+        switch (cellText) {
+            case '地区':
+                colMap.area = col;
+                break;
+            case '分类':
+                colMap.class = col;
+                break;
+            case '编码':
+                colMap.code = col;
+                break;
+            case '名称':
+                colMap.name = col;
+                break;
+            case '规格型号':
+                colMap.specs = col;
+                break;
+            case '单位':
+                colMap.unit = col;
+                break;
+            case '不含税价':
+                colMap.noTaxPrice = col;
+                break;
+            case '含税价':
+                colMap.taxPrice = col;
+                break;
+        }
+    }
+    // 提取数据
+    const data = [];
+    let curAreaName;
+    let curClassName;
+    for (let row = 1; row < sheetData.length; row++) {
+        const areaName = sheetData[row][colMap.area] || '';
+        const className = sheetData[row][colMap.class] || '';
+        const code = sheetData[row][colMap.code] || '';
+        const name = sheetData[row][colMap.name] || '';
+        const specs = sheetData[row][colMap.specs] || '';
+        const unit = sheetData[row][colMap.unit] || '';
+        const noTaxPrice = sheetData[row][colMap.noTaxPrice] || '';
+        const taxPrice = sheetData[row][colMap.taxPrice] || '';
+        if (!code && !name && !specs && !noTaxPrice && !taxPrice) { // 认为是空数据
+            continue;
+        }
+        if (areaName && areaName !== curAreaName) {
+            curAreaName = areaName;
+        }
+        if (className && className !== curClassName) {
+            curClassName = className;
+        }
+        const areaID = areaMap[curAreaName];
+        if (!areaID) {
+            continue;
+        }
+        const classID = classMap[`${curAreaName}@${curClassName}`];
+        if (!classID) {
+            continue;
+        }
+        data.push({
+            ID: uuidV1(),
+            compilationID,
+            libID,
+            areaID,
+            classID,
+            period: libs[0].period,
+            code,
+            name,
+            specs,
+            unit,
+            noTaxPrice,
+            taxPrice
+        });
+    }
+    if (data.length) {
+        await priceInfoItemModel.remove({ libID });
+        await priceInfoItemModel.insertMany(data);
+    } else {
+        throw 'excel没有有效数据。'
+    }
+}
+
 // 获取费用定额的地区数据
 async function getAreas(compilationID) {
     return await priceInfoAreaModel.find({ compilationID }, '-_id ID name').lean();
@@ -88,7 +202,16 @@ async function deleteAreas(deleteData) {
 }
 
 async function getClassData(libID, areaID) {
-    return await priceInfoClassModel.find({ libID, areaID }, '-_id').lean();
+    if (libID && areaID) {
+        return await priceInfoClassModel.find({ libID, areaID }, '-_id').lean();
+    }
+    if (libID) {
+        return await priceInfoClassModel.find({ libID }, '-_id').lean();
+    }
+    if (areaID) {
+        return await priceInfoClassModel.find({ areaID }, '-_id').lean();
+    }
+
 }
 
 async function getPriceData(classIDList) {
@@ -170,6 +293,7 @@ module.exports = {
     updateLib,
     deleteLib,
     crawlDataByCompilation,
+    importExcelData,
     getAreas,
     updateAres,
     insertAreas,

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

@@ -14,6 +14,7 @@ module.exports = function (app) {
     router.post("/renameLib", priceInfoController.auth, priceInfoController.init, priceInfoController.renameLib);
     router.post("/deleteLib", priceInfoController.auth, priceInfoController.init, priceInfoController.deleteLib);
     router.post("/crawlData", priceInfoController.auth, priceInfoController.init, priceInfoController.crawlData);
+    router.post("/importExcel", priceInfoController.auth, priceInfoController.init, priceInfoController.importExcel);
 
     router.post("/editArea", priceInfoController.auth, priceInfoController.init, priceInfoController.editArea);
     router.post("/insertArea", priceInfoController.auth, priceInfoController.init, priceInfoController.insertArea);

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

@@ -20,6 +20,7 @@ let callback = function(req, res, err, message, data){
 const shareDir = 'public/share/';
 class SysTools extends BaseController{
     clearJunkData(req, res){
+        res.setTimeout(1000 * 60 * 60);
         sysSchedule.clearJunkData(function (err) {
             let msg = '清除成功';
             let errCode = 0;

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

@@ -77,6 +77,7 @@
     <script>
         const areaList = JSON.parse('<%- areaList %>');
         const compilationID = '<%- compilationID %>';
+        const curLibPeriod = '<%- period %>';
     </script>
     <script src="/web/maintain/price_info_lib/js/index.js"></script>
 </body>

+ 37 - 1
web/maintain/price_info_lib/html/main.html

@@ -28,6 +28,7 @@
                                     <th width="160">费用定额</th>
                                     <th width="160">添加时间</th>
                                     <th width="70">操作</th>
+                                    <th width="70">原始数据</th>
                                 </tr>
                             </thead>
                             <tbody id="showArea">
@@ -49,6 +50,11 @@
                                         <a class="lock" data-locked="true" href="javascript:void(0);" title="解锁"><i
                                                 class="fa fa-unlock-alt"></i></a>
                                     </td>
+                                    <td>
+                                        <a class="btn btn-secondary btn-sm import-data lock-btn-control disabled"
+                                            onclick='handleImportClick("<%= lib.ID%>")' href="javacript:void(0);"
+                                            title="导入数据"><i class="fa fa-sign-in fa-rotate-90"></i>导入</a>
+                                    </td>
                                 </tr>
                                 <% } %>
                             </tbody>
@@ -151,7 +157,8 @@
                         <label class="split">——</label>
                     </div>
                     <div class="col-5 form-group">
-                        <input id="period-end" class="form-control" name="to" placeholder="结束期数" type="text" value="" autocomplete="off">
+                        <input id="period-end" class="form-control" name="to" placeholder="结束期数" type="text" value=""
+                            autocomplete="off">
                     </div>
                 </div>
                 <small class="form-text text-danger" id="crawl-error" style="display: none">请输入有效期数。</small>
@@ -186,6 +193,35 @@
     </div>
 </div>
 
+<!--弹出导入数据-->
+<div class="modal fade" id="import" data-backdrop="static" style="display: none;" aria-hidden="true">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">导入数据</h5>
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">×</span>
+                </button>
+            </div>
+            <div class="modal-body">
+                <div class="alert alert-warning" role="alert">
+                    导入操作会覆盖数据,请谨慎操作!!
+                </div>
+                <form>
+                    <div class="form-group">
+                        <label>请选择Excel格式文件</label>
+                        <input class="form-control-file" type="file" accept=".xlsx,.xls" name="import_data" />
+                    </div>
+                </form>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-primary" id="import-confirm">确定导入</button>
+                <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
+            </div>
+        </div>
+    </div>
+</div>
+
 <script src="/public/web/lock_util.js"></script>
 <script src="/public/web/PerfectLoad.js"></script>
 <script src="/web/maintain/price_info_lib/js/main.js"></script>

+ 4 - 2
web/maintain/price_info_lib/js/index.js

@@ -152,9 +152,9 @@ const AREA_BOOK = (() => {
                 const x = e.pageX - offset.left;
                 const y = e.pageY - offset.top;
                 const target = sheet.hitTest(x, y);
-                if (target.hitTestType === 3 && typeof target.row !== 'undefined' && typeof target.col !== 'undefined') { // 在表格内
+                if (target.hitTestType === 3) { // 在表格内
                     const sel = sheet.getSelections()[0];
-                    if (sel && sel.rowCount === 1) {
+                    if (sel && sel.rowCount === 1 && typeof target.row !== 'undefined') {
                         const orgRow = sheet.getActiveRowIndex();
                         if (orgRow !== target.row) {
                             sheet.setActiveCell(target.row, target.col);
@@ -727,8 +727,10 @@ const PRICE_BOOK = (() => {
                     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);
                     }

+ 63 - 2
web/maintain/price_info_lib/js/main.js

@@ -79,6 +79,67 @@ function handleDeleteConfirm() {
     }
 }
 
+// 点击导入按钮
+function handleImportClick(libID) {
+    setCurLib(libID);
+    $('#import').modal('show');
+}
+
+// 导入确认
+function handleImportConfirm() {
+    $.bootstrapLoading.start();
+    const self = $(this);
+    try {
+        const formData = new FormData();
+        const file = $("input[name='import_data']")[0];
+        if (file.files.length <= 0) {
+            throw '请选择文件!';
+        }
+        formData.append('file', file.files[0]);
+        formData.append('libID', curLib.id);
+        $.ajax({
+            url: '/priceInfo/importExcel',
+            type: 'POST',
+            data: formData,
+            cache: false,
+            contentType: false,
+            processData: false,
+            beforeSend: function () {
+                self.attr('disabled', 'disabled');
+                self.text('上传中...');
+            },
+            success: function (response) {
+                self.removeAttr('disabled');
+                self.text('确定导入');
+                if (response.err === 0) {
+                    $.bootstrapLoading.end();
+                    const message = response.msg !== undefined ? response.msg : '';
+                    if (message !== '') {
+                        alert(message);
+                    }
+                    // 成功则关闭窗体
+                    $('#import').modal("hide");
+                } else {
+                    $.bootstrapLoading.end();
+                    const message = response.msg !== undefined ? response.msg : '上传失败!';
+                    alert(message);
+                }
+            },
+            error: function () {
+                $.bootstrapLoading.end();
+                alert("与服务器通信发生错误");
+                self.removeAttr('disabled');
+                self.text('确定导入');
+            }
+        });
+    } catch (error) {
+        alert(error);
+        $.bootstrapLoading.end();
+    }
+}
+
+const matched = window.location.search.match(/filter=(.+)/);
+const compilationID = matched && matched[1] || '';
 // 爬取数据确认
 function handleCrawlConfirm() {
     const from = $('#period-start').val();
@@ -87,8 +148,6 @@ function handleCrawlConfirm() {
         $('#crawl-error').show();
         return false;
     }
-    const matched = window.location.search.match(/filter=(.+)/);
-    const compilationID = matched && matched[1] || '';
     $('#crawl').modal('hide');
     $.bootstrapLoading.progressStart('爬取数据', true);
     $("#progress_modal_body").text('正在爬取数据,请稍候……');
@@ -119,6 +178,8 @@ $(document).ready(function () {
     $('#delete').click(handleDeleteConfirm);
     // 爬取数据
     $('#crawl-confirm').click(throttle(handleCrawlConfirm, throttleTime));
+    // 导入excel
+    $('#import-confirm').click(throttle(handleImportConfirm, throttleTime));
 
     $('#add').on('hidden.bs.modal', () => {
         $('#name-error').hide();