Browse Source

签约清单相关代码

MaiXinRong 7 years ago
parent
commit
92a1f04c74

+ 121 - 0
app/controller/deal_bills_controller.js

@@ -0,0 +1,121 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date 2018/5/7
+ * @version
+ */
+
+const fs = require('fs');
+const path = require('path');
+const excel = require('node-xlsx');
+
+module.exports = app => {
+    class DealBillsController extends app.BaseController {
+
+        /**
+         * 获取标段的签约清单数据
+         * @param ctx
+         * @returns {Promise<void>}
+         */
+        async getData (ctx) {
+            const responseData = {
+                err: 0,
+                msg: '',
+                data: [],
+            };
+            try {
+                const tenderId = ctx.session.sessionUser.tenderId;
+                if (isNaN(tenderId) || tenderId <= 0) {
+                    throw '参数错误';
+                }
+
+                responseData.data =  await ctx.service.dealBills.getAllDataByCondition({ where: {tender_id: tenderId} });
+            } catch (error) {
+                responseData.err = 1;
+                responseData.msg = error.toString();
+            }
+
+            ctx.body = responseData;
+        }
+
+        /**
+         * 导入Excel数据
+         * @param ctx
+         * @returns {Promise<void>}
+         */
+        async loadExcel (ctx) {
+            const responseData = {
+                err: 0,
+                msg: '',
+                data: [],
+            };
+            let stream;
+            try {
+                const tenderId = ctx.session.sessionUser.tenderId;
+                if (isNaN(tenderId) || tenderId <= 0) {
+                    throw '参数错误';
+                }
+
+                stream = await ctx.getFileStream();
+                const create_time = Date.parse(new Date())/1000;
+                const fileInfo = path.parse(stream.filename);
+                const fileName = this.app.baseDir + '/app/public/deal_bills/uploads/' + 'deal_bills' + create_time + fileInfo.ext;
+                // 保存文件
+                await ctx.helper.saveStreamFile(stream, fileName);
+                // 读取文件
+                const sheet = excel.parse(fileName);
+                if (!sheet || sheet.length === 0 || sheet[0] === undefined || sheet[0].data === undefined) {
+                    throw 'excel没有对应数据';
+                }
+                const result = await ctx.service.dealBills.importData(sheet[0], tenderId);
+                if (!result) {
+                    throw '导入数据失败';
+                }
+
+                responseData.data =  await this.ctx.service.dealBills.getAllDataByCondition({ where: {tender_id: tenderId} });
+            } catch (err) {
+                console.log(err);
+                // 失败需要消耗掉stream 以防卡死
+                if (stream) {
+                    await sendToWormhole(stream);
+                }
+                this.setMessage(err.toString(), this.messageType.ERROR);
+            }
+
+            ctx.body = responseData;
+        }
+
+        /**
+         * 下载模板文件
+         * @param ctx
+         * @returns {Promise<void>}
+         */
+        async download (ctx) {
+            const file = ctx.params.file;
+            if (file) {
+                try {
+                    let fileName;
+                    if (file === 'template.xls') {
+                        fileName = this.app.baseDir + '/app/public/deal_bills/deal_template.xls';
+                    } else {
+                        const tenderId = ctx.session.sessionUser.tenderId;
+                        if (isNaN(tenderId) || data.tenderId <= 0) {
+                            throw '参数错误';
+                        }
+                        const create_time = Date.parse(new Date())/1000;
+                        fileName = this.app.baseDir + '/app/public/deal_bills/downloads/' + tenderId + '-' + create_time + '.xlsx';
+                        // todo 导出签约清单Excel
+                    }
+                    ctx.body = await fs.readFileSync(fileName);
+                } catch(err) {
+                    console.log(err);
+                }
+            }
+        }
+    }
+
+    return DealBillsController;
+};

+ 28 - 0
app/extend/helper.js

@@ -8,6 +8,8 @@
  * @version
  */
 const zeroRange = 0.0000000001;
+const fs = require('fs');
+const streamToArray = require('stream-to-array');
 module.exports = {
 
     /**
@@ -330,4 +332,30 @@ module.exports = {
 
         return result;
     },
+
+    /**
+     * 将文件流的数据保存至本地文件
+     * @param stream
+     * @param fileName
+     * @returns {Promise<void>}
+     */
+    async saveStreamFile(stream, fileName) {
+        // 读取字节流
+        const parts = await streamToArray(stream);
+        // 转化为buffer
+        const buffer = Buffer.concat(parts);
+        // 写入文件
+        await fs.writeFileSync(fileName, buffer);
+    },
+
+    /**
+     * 检查code是否是指标模板数据
+     * @param {String} code
+     * @returns {boolean}
+     */
+    validBillsCode(code) {
+        const reg1 = /(^[0-9]+)([a-z0-9\-]*)/i;
+        const reg2 = /([a-z0-9]+$)/i;
+        return reg1.test(code) && reg2.test(code);
+    },
 };

+ 107 - 1
app/public/css/main.css

@@ -12,9 +12,45 @@ body {
   color:#999
 }
 /*自定义css*/
+/*滚动条*/
+/* 滚动条 */
+::-webkit-scrollbar-thumb:horizontal { /*水平滚动条的样式*/
+	width: 5px;
+	background-color: #ddd;
+	-webkit-border-radius: 6px;
+}
+::-webkit-scrollbar-track-piece {
+	background-color: #fff; /*滚动条的背景颜色*/
+	-webkit-border-radius: 0; /*滚动条的圆角宽度*/
+}
+::-webkit-scrollbar {
+	width: 10px; /*滚动条的宽度*/
+	height: 8px; /*滚动条的高度*/
+}
+::-webkit-scrollbar-thumb:vertical { /*垂直滚动条的样式*/
+	height: 50px;
+	background-color: #ddd;
+	-webkit-border-radius: 6px;
+	outline: 1px solid #fff;
+	outline-offset: -1px;
+	border: 1px solid #fff;
+}
+::-webkit-scrollbar-thumb:hover { /*滚动条的hover样式*/
+	height: 50px;
+	background-color: #999;
+	-webkit-border-radius: 6px;
+}
 .sjs-height-1,.sjs-height-2{
   overflow: hidden;
 }
+.sjs-bottom{
+  height:400px;
+  overflow-y: auto;
+}
+.sjs-bottom-2{
+  height:360px;
+  overflow-y: auto;
+}
 .form-signin {
     max-width: 500px;
     margin: 150px auto;
@@ -158,6 +194,17 @@ body {
 .panel-title>.title-main{
   padding-left: 15px
 }
+.side-menu{
+  position: fixed;
+  right:15px;
+  top:116px
+}
+.sub-content{
+  margin:0;
+}
+.pr-46{
+  padding-right:46px
+}
 /*滚动*/
 .scrollbar-auto {
     overflow-y: auto;
@@ -348,11 +395,70 @@ body {
   padding:15px;
   background:#fff;
 }
+.right-nav{
+  width:46px
+}
+.right-nav .nav-link.active{
+  background: #fff;
+  color:#495057
+}
 .form-group .necessary{
   font-size:18px;
   color:#f90000
 }
-
 .bg-gray {
  background-color:#bbb!important;
 }
+.datepickers-container {
+  z-index: 9999
+}
+.modal-height-500{
+  height:500px;
+  overflow: hidden
+}
+.modal-lgx {
+  max-width:1000px
+}
+.title-main .nav{
+  line-height: 16px;
+  margin-top:8px
+}
+/*草图编辑器*/
+.img-view{
+  height:400px;
+  border:.2rem solid #ccc;
+  position: relative;
+  width:100%;
+  overflow: hidden;
+}
+.img-view::after{
+  content:"草图编辑区";
+  color:#ddd;
+  position: absolute;
+  left:50%;
+  top:50%;
+  margin-left:-80px;
+  margin-top:-24px;
+  font-size:36px
+}
+.img-view .img-item{
+  position: absolute;
+
+}
+.img-view .img-item .img-bar{
+  position:absolute;
+  right:0;
+  top:0;
+  display:none
+}
+.img-item:hover .img-bar{
+  display: block;
+}
+.batch-l-t,.batch-l-b{
+  height: 200px;
+  overflow: hidden
+}
+.batch-r {
+  height:400px;
+  overflow: hidden
+}

BIN
app/public/deal_bills/deal_template.xls


+ 44 - 0
app/public/js/global.js

@@ -116,4 +116,48 @@ const postData = function (url, data, successCallback, errorCallBack) {
             }
         }
     });
+};
+
+/**
+ * 动态请求数据
+ * @param {String} url - 请求链接
+ * @param data - 提交数据
+ * @param {function} successCallback - 返回成功回调
+ * @param {function} errorCallBack - 返回失败回调
+ */
+const postDataWithFile = function (url, formData, successCallback, errorCallBack) {
+    $.ajax({
+        type:"POST",
+        url: url,
+        data: formData,
+        dataType: 'json',
+        cache: false,
+        // 告诉jQuery不要去设置Content-Type请求头
+        contentType: false,
+        // 告诉jQuery不要去处理发送的数据
+        processData: false,
+        timeout: 5000,
+        beforeSend: function(xhr) {
+            let csrfToken = Cookies.get('csrfToken');
+            xhr.setRequestHeader('x-csrf-token', csrfToken);
+        },
+        success: function(result){
+            if (result.err === 0) {
+                if (successCallback) {
+                    successCallback(result.data);
+                }
+            } else {
+                toast('error: ' + result.message, 'error', 'exclamation-circle');
+                if (errorCallBack) {
+                    errorCallBack();
+                }
+            }
+        },
+        error: function(jqXHR, textStatus, errorThrown){
+            toast('error ' + textStatus + " " + errorThrown, 'error', 'exclamation-circle');
+            if (errorCallBack) {
+                errorCallBack();
+            }
+        }
+    });
 };

+ 46 - 4
app/public/js/ledger.js

@@ -32,7 +32,10 @@ $(document).ready(function() {
             {title: '备注', field: 'memo', width: 100}
         ],
         treeCol: 0,
-        emptyRows: 3
+        emptyRows: 3,
+        headRows: 2,
+        headRowHeight: [40, 40],
+        defaultRowHeight: 21,
     });
     SpreadJsObj.loadSheetData(ledgerSpread.getActiveSheet(), 'tree', ledgerTree);
 
@@ -476,9 +479,9 @@ $(document).ready(function() {
         }
     });
 
-    let stdChapter, stdBills;
+    let stdChapter, stdBills, dealBills;
     // 展开收起标准清单
-    $('a', '#std-lib').bind('click', function () {
+    $('a', '#side-menu').bind('click', function () {
         const tab = $(this), tabPanel = $(tab.attr('content'));
         const showSideTools = function (show) {
             if (show) {
@@ -490,7 +493,7 @@ $(document).ready(function() {
             }
         }
         if (!tab.hasClass('active')) {
-            $('a', '#std-lib').removeClass('active');
+            $('a', '#side-menu').removeClass('active');
             tab.addClass('active');
             showSideTools(tab.hasClass('active'));
             $('.tab-content .tab-pane').hide();
@@ -531,6 +534,18 @@ $(document).ready(function() {
                     emptyRows: 0
                 });
                 stdBills.loadLib(1);
+            } else if (tab.attr('content') === '#deal-bills' && !dealBills) {
+                dealBills = new DealBills($('#deal-bills-spread')[0], {
+                    cols: [
+                        {title: '清单编号', field: 'code', width: 120, readOnly: true, cellType: 'tree'},
+                        {title: '名称', field: 'name', width: 230, readOnly: true},
+                        {title: '单位', field: 'unit', width: 50, readOnly: true},
+                        {title: '单价', field: 'unit_price', width: 50, readOnly: true},
+                        {title: '数量', field: 'quantity', width: 50, readOnly: true},
+                    ],
+                    emptyRows: 0,
+                });
+                dealBills.loadData();
             }
         } else {
             tab.removeClass('active');
@@ -581,5 +596,32 @@ $(document).ready(function() {
             });
         }
     };
+    class DealBills {
+        constructor (obj, spreadSetting) {
+            const self = this;
+            this.obj = obj;
+            this.url = '/deal';
+            this.spreadSetting = spreadSetting;
+            this.spread = SpreadJsObj.createNewSpread(this.obj);
+            SpreadJsObj.initSheet(this.spread.getActiveSheet(), this.spreadSetting);
+            $('#upload-deal-bills').click(function () {
+                const file = $('#deal-bills-file')[0];
+                const formData = new FormData();
+                formData.append('file', file.files[0]);
+                postDataWithFile(self.url+'/upload-excel', formData, function (data) {
+                    SpreadJsObj.loadSheetData(self.spread.getActiveSheet(), 'data', data);
+                    $('#upload-deal').modal('hide');
+                }, function () {
+                    $('#upload-deal').modal('hide');
+                });
+            })
+        }
+        loadData () {
+            const self = this;
+            postData(this.url+'/get-data', {}, function (data) {
+                SpreadJsObj.loadSheetData(self.spread.getActiveSheet(), 'data', data);
+            });
+        }
+    }
 });
 

+ 5 - 0
app/router.js

@@ -42,6 +42,11 @@ module.exports = app => {
     app.get('/ledger/change', sessionAuth, 'ledgerController.change');
     app.get('/ledger/index', sessionAuth, 'ledgerController.index');
 
+    // 签约清单
+    app.post('/deal/get-data', sessionAuth, 'dealBillsController.getData');
+    app.post('/deal/upload-excel', sessionAuth, 'dealBillsController.loadExcel');
+    app.get('/deal/download/:file', sessionAuth, 'dealBillsController.download');
+
     // 个人账号相关
     app.get('/profile/info', sessionAuth, 'profileController.info');
     app.post('/profile/save', sessionAuth, 'profileController.saveBase');

+ 68 - 0
app/service/deal_bills.js

@@ -0,0 +1,68 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date 2018/5/8
+ * @version
+ */
+
+module.exports = app => {
+    class DealBills extends app.BaseService {
+
+        /**
+         * 构造函数
+         * @param ctx
+         */
+        constructor (ctx) {
+            super(ctx);
+            this.tableName = 'deal_bills';
+        }
+
+        /**
+         * 导入Excel数据
+         *
+         * @param {Array} sheet - Excel文件中的全部工作表
+         * @param {Number} tenderId - 所属标段Id
+         * @returns {Promise<boolean>}
+         */
+        async importData(sheet, tenderId) {
+            let result = false;
+            const transaction = await this.db.beginTransaction();
+            try {
+                const bills = [];
+                for (const row of sheet.data) {
+                    if (this.ctx.helper.validBillsCode(row[0])) {
+                        bills.push({
+                            deal_id: bills.length + 1,
+                            tender_id: tenderId,
+                            code: row[0],
+                            name: row[1],
+                            unit: row[2],
+                            unit_price: row[3],
+                            quantity: row[4],
+                        });
+                    }
+                }
+                if (bills.length > 0) {
+                    await transaction.delete(this.tableName, {tender_id: tenderId});
+                    const billsResult = await transaction.insert(this.tableName, bills);
+                    if (billsResult.affectedRows !== bills.length) {
+                        throw '导入签约清单数据出错';
+                    }
+                } else {
+                    throw 'Excel文件中无签约清单数据';
+                }
+                await transaction.commit();
+                result = true;
+            } catch(err) {
+                await transaction.rollback();
+                throw err;
+            }
+            return result;
+        }
+    }
+
+    return DealBills;
+}

+ 25 - 0
app/view/layout/modal.ejs

@@ -70,4 +70,29 @@
             </div>
         </div>
     </div>
+</div>
+
+<!--上传签约清单-->
+<div class="modal fade" id="upload-deal" data-backdrop="static" enctype="multipart/form-data">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">上传签约清单</h5>
+            </div>
+            <div class="modal-body">
+                <div class="form-group">
+                    <label for="exampleFormControlFile1">Excel模板</label>
+                    <div class="form-control"><a id="downloadDealTemplate" href="/deal/download/template.xls" class="btn btn-sm btn-link">下载</a></div>
+                </div>
+                <div class="form-group">
+                    <label for="exampleFormControlFile1">上传签约清单Excel文件</label>
+                    <div class="form-control"><input type="file" class="form-control-file" id="deal-bills-file"></div>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
+                <button type="button" class="btn btn-primary" id="upload-deal-bills">确认上传</button>
+            </div>
+        </div>
+    </div>
 </div>

+ 37 - 26
app/view/ledger/explode.ejs

@@ -47,38 +47,49 @@
             </div>
         </div>
     </div>
-    <div class="content-wrap row">
-        <div class="c-header p-0 col-12 d-flex flex-row-reverse">
-
-            <!--标准清单-->
-            <ul class="nav nav-tabs" id="std-lib">
+    <div class="content-wrap row pr-46">
+        <div class="c-header p-0 col-12">
+        </div>
+        <!--核心内容(两栏)-->
+        <div class="row w-100 sub-content">
+            <!--左栏-->
+            <div class="c-body col-12">
+                <div id="ledger-spread" class="sjs-height-1"></div>
+            </div>
+            <div class="c-body col-0" style="display: none;">
+                <div class="tab-content">
+                    <div id="std-chapter" class="tab-pane active">
+                        <select class="form-control form-control-sm"><option>0号计量台帐部位参考(项目节)</option></select>
+                        <div id="std-chapter-spread" class="sjs-height-2">
+                        </div>
+                    </div>
+                    <div id="std-bills" class="tab-pane">
+                        <select class="form-control form-control-sm"><option>0号计量台帐部位参考(项目节)</option></select>
+                        <div id="std-bills-spread" class="sjs-height-2">
+                        </div>
+                    </div>
+                    <div id="deal-bills" class="tab-pane">
+                        <a href="#upload-deal" data-toggle="modal" data-target="#upload-deal" class="btn btn-sm btn-primary">上传签约清单</a>
+                        <div id="deal-bills-spread" class="sjs-height-2">
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <!--右侧菜单-->
+        <div class="side-menu">
+            <ul class="nav flex-column right-nav" id="side-menu">
                 <li class="nav-item">
-                    <a class="nav-link" content="#std-chapter">项目节</a>
+                    <a class="nav-link" content="#std-chapter" href="javascript: void(0);">项目节</a>
                 </li>
                 <li class="nav-item">
-                    <a class="nav-link" content="#std-bills">工程量清单</a>
+                    <a class="nav-link" content="#std-bills" href="javascript: void(0);">工程量清单</a>
+                </li>
+                <li class="nav-item">
+                    <a class="nav-link" content="#deal-bills" href="javascript: void(0);">签约清单</a>
                 </li>
             </ul>
         </div>
-        <div class="c-body col-12">
-            <div id="ledger-spread" class="sjs-height-1"></div>
-        </div>
-        <div class="c-body col-0" style="display: none;">
-            <div class="tab-content">
-                <div id="std-chapter" class="tab-pane">
-                    <select class="form-control form-control-sm">
-                        <option>0号计量台帐部位参考(项目节)</option>
-                    </select>
-                    <div id="std-chapter-spread" class="sjs-height-2"></div>
-                </div>
-                <div id="std-bills" class="tab-pane">
-                    <select class="form-control form-control-sm">
-                        <option>0号计量台帐部位参考(项目节)</option>
-                    </select>
-                    <div id="std-bills-spread" class="sjs-height-2"></div>
-                </div>
-            </div>
-        </div>
     </div>
 </div>
 <script type="text/javascript">

+ 6 - 0
config/config.default.js

@@ -60,5 +60,11 @@ module.exports = appInfo => {
         app: true,
     };
 
+    // 上传设置
+    config.multipart = {
+        whitelist: ['.xls', '.xlsx', '.json'],
+        fileSize: '10mb',
+    };
+
     return config;
 };

+ 7 - 0
config/config.local.js

@@ -53,5 +53,12 @@ module.exports = appInfo => {
         client: {},
         app: true,
     };
+
+    // 上传设置
+    config.multipart = {
+        whitelist: ['.xls', '.xlsx', '.json'],
+        fileSize: '10mb',
+    };
+
     return config;
 };

+ 6 - 0
config/config.qa.js

@@ -54,5 +54,11 @@ module.exports = appInfo => {
         app: true,
     };
 
+    // 上传设置
+    config.multipart = {
+        whitelist: ['.xls', '.xlsx', '.json'],
+        fileSize: '10mb',
+    };
+
     return config;
 };

+ 2 - 0
package.json

@@ -15,6 +15,8 @@
     "egg-view-ejs": "^1.1.0",
     "gt3-sdk": "^2.0.0",
     "moment": "^2.20.1",
+    "node-xlsx": "^0.12.0",
+    "stream-to-array": "^2.3.0",
     "ueditor": "^1.2.3",
     "xmlreader": "^0.2.3"
   },

+ 3 - 0
test/app/service/ledger.test.js

@@ -334,6 +334,7 @@ describe('test/app/service/ledger.test.js', () => {
         const ctx = app.mockContext();
         // 选中 1-1-2 升级
         const resultData = yield ctx.service.ledger.upLevelNode(testTenderId, 13);
+        console.log(resultData);
         assert(resultData);
         assert(resultData.update.length === 6);
 
@@ -375,6 +376,8 @@ describe('test/app/service/ledger.test.js', () => {
         const ctx = app.mockContext();
         // 选中1-3 降级
         const resultData = yield ctx.service.ledger.downLevelNode(testTenderId, 4);
+        console.log(resultData);
+        // 1-3/1-3-1/1-4修改
         assert(resultData.update.length === 3);
 
         let node = findById(resultData.update, 4);

+ 56 - 0
test/app/service/std_chapter.test.js

@@ -0,0 +1,56 @@
+/**
+ * 标段--台账 模型 单元测试
+ *
+ * @author Mai
+ * @date 2018/3/15
+ * @version
+ */
+'use strict';
+
+const { app, assert } = require('egg-mock/bootstrap');
+
+describe('test/app/service/ledger.test.js', () => {
+    // 测试R类方法
+    it('test getData', function* () {
+        const ctx = app.mockContext();
+
+        // 查询前2层节点
+        const result1 = yield ctx.service.stdChapter.getData(1);
+        assert(result1.length === 32);
+        // 查询前1层节点
+        const result2 = yield ctx.service.stdChapter.getData(1, 1);
+        assert(result2.length === 6);
+    });
+    it('test getDataByDataId', function* () {
+        const ctx = app.mockContext();
+
+        // 查询节点1-10-2
+        const node = yield ctx.service.stdChapter.getDataByDataId(1, 47);
+        assert(node);
+        assert(node.code === '1-10-2');
+        assert(node.full_path === '1.210.47');
+        assert(node.source === ctx.service.stdChapter.stdType + '-' + node.list_id + '-' + node.chapter_id);
+    });
+    it('test getDataByFullPath', function* () {
+        const ctx = app.mockContext();
+
+        // 查询节点1-10-1及其子节点
+        const result = yield ctx.service.stdChapter.getDataByFullPath(1, '1.210.3002%');
+        assert(result.length === 9);
+
+        // 查询1-10-1的子孙节点
+        const result1 = yield ctx.service.stdChapter.getDataByFullPath(1, '1.210.3002.%');
+        assert(result1.length === 8);
+    });
+    it('test getFullLevelDataByFullPath', function* () {
+        const ctx = app.mockContext();
+
+        // 查询1-10-1及其全部父节点
+        const result1 = yield ctx.service.stdChapter.getFullLevelDataByFullPath(1, '1.210.3002');
+        assert(result1.length === 3);
+
+        // 查询1-10-2/1-10-3及其全部父节点
+        const result2 = yield ctx.service.stdChapter.getFullLevelDataByFullPath(1, ['1.210.47', '1.210.48']);
+        assert(result2.length === 4);
+    });
+});

BIN
test/app/test_file/deal-uploat-test.xls