Pārlūkot izejas kodu

Merge branch 'master' of http://192.168.1.12:3000/MaiXinRong/IndexSystem

# Conflicts:
#	app/base/base_service.js
#	config/config.qa.js
laiguoran 7 gadi atpakaļ
vecāks
revīzija
02677c34e3

+ 2 - 0
app.js

@@ -9,7 +9,9 @@
  */
 
 const BaseController = require('./app/base/base_controller');
+const BaseService = require('./app/base/base_service');
 module.exports = app => {
     // app内定义基类,方便继承
     app.BaseController = BaseController;
+    app.BaseService = BaseService;
 }

+ 4 - 77
app/base/base_service.js

@@ -3,14 +3,13 @@
 /**
  * 业务逻辑基类
  *
- * @author CaiAoLin
- * @date 2017/10/11
+ * @author Mai
+ * @date 2018/4/19
  * @version
  */
 
 const Service = require('egg').Service;
-// sql拼装器
-const SqlBuilder = require('../lib/sql_builder');
+
 class BaseService extends Service {
     /**
      * 构造函数
@@ -21,9 +20,6 @@ class BaseService extends Service {
     constructor(ctx) {
         super(ctx);
         this.db = this.app.mysql;
-        this.cache = this.app.redis;
-        this.transaction = null;
-        this.sqlBuilder = null;
     }
 
     /**
@@ -46,17 +42,6 @@ class BaseService extends Service {
     }
 
     /**
-     * 初始化sqlBuilder
-     *
-     * @return {void}
-     */
-    initSqlBuilder() {
-        if (this.sqlBuilder === null) {
-            this.sqlBuilder = new SqlBuilder();
-        }
-    }
-
-    /**
      * 根据id查找数据
      *
      * @param {Number} id - 数据库中的id
@@ -77,7 +62,7 @@ class BaseService extends Service {
     }
 
     /**
-     * 根据条件查找单条数据
+     * 根据条件查找数据
      *
      * @param {Object} condition - 筛选条件
      * @return {Array} - 返回数据
@@ -109,64 +94,6 @@ class BaseService extends Service {
     }
 
     /**
-     * 更新数据
-     *
-     * @param {Object} data - 需要更新的数据
-     * @param {Object} condition - 更新的条件筛选
-     * @return {Boolean} - 返回更新结果
-     */
-    async update(data, condition) {
-        if (Object.keys(data).length <= 0 || Object.keys(condition).length <= 0) {
-            return false;
-        }
-        const sqlParam = [];
-        const param = [];
-        for (const key in data) {
-            param.push(key + ' = ?');
-            sqlParam.push(data[key]);
-        }
-        const paramString = param.join(',');
-
-        const where = [];
-        for (const key in condition) {
-            where.push(key + ' = ?');
-            sqlParam.push(condition[key]);
-        }
-        const whereString = where.join(' AND ');
-
-        const sql = 'UPDATE ' + this.tableName + ' SET ' + paramString + ' WHERE ' + whereString;
-        const result = await this.db.query(sql, sqlParam);
-        return result.affectedRows > 0;
-    }
-
-    /**
-     * 获取分页数据(SqlBuilder版本)
-     *
-     * @return {Array} - 返回分页数据
-     */
-    async getListWithBuilder() {
-
-        // 由于egg-mysql不提供like、不等于操作,所以需要自己组sql
-        if (this.sqlBuilder === null) {
-            return [];
-        }
-
-        // 分页相关
-        this.sqlBuilder.limit = this.app.config.pageSize;
-        this.sqlBuilder.offset = this.app.config.pageSize * (this.ctx.page - 1);
-
-        // 数据筛选
-        if (this.ctx.sort !== undefined && this.ctx.sort.length > 0) {
-            this.sqlBuilder.orderBy = [this.ctx.sort];
-        }
-
-        const [sql, sqlParam] = this.sqlBuilder.build(this.tableName);
-        const list = await this.db.query(sql, sqlParam);
-
-        return list;
-    }
-
-    /**
      * 获取分页数据
      *
      * @param {Object} condition - 搜索条件

+ 47 - 1
app/controller/template_controller.js

@@ -8,6 +8,9 @@
  * @version
  */
 
+// excel解析
+const excel = require('node-xlsx');
+const sendToWormhole = require('stream-wormhole');
 module.exports = app => {
     class TemplateController extends app.BaseController {
         /**
@@ -17,7 +20,50 @@ module.exports = app => {
          * @return {void}
          */
         async index (ctx) {
-            await this.layout('template/index.ejs', {}, 'template/modal.ejs');
+            const id = ctx.queries.id ? ctx.queries.id[0] : 1;
+            const node = await ctx.service.templateNode.getAllDataByCondition({template_id: 1});
+            for (const n of node) {
+                n.url = '/template?id=' + n.node_id;
+                n.target = '_self';
+            }
+            const treeNode = ctx.helper.convertData(node, true, 'node_id', 'node_pid');
+            const condition = {template_id: 1, node_id: id};
+            const selectNode = await ctx.service.templateNode.getDataByCondition(condition);
+            const selectIndex = await ctx.service.templateIndex.getAllDataByCondition({ where: condition });
+            const renderData = {
+                nodes: JSON.stringify(treeNode),
+                selectNode: selectNode,
+                selectIndex: selectIndex,
+            }
+            await this.layout('template/index.ejs', renderData, 'template/modal.ejs');
+        }
+
+        async uploadExcel(ctx) {
+            let stream;
+            try {
+                stream = await ctx.getFileStream();
+                const fileName = this.app.baseDir + '/app/public/template/uploads/' + stream.filename;
+                // 保存文件
+                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.templateNode.importData(sheet);
+                if (!result) {
+                    throw '导入数据失败';
+                }
+                ctx.redirect('/template');
+            } catch (err) {
+                console.log(err);
+                // 失败需要消耗掉stream 以防卡死
+                if (stream) {
+                    await sendToWormhole(stream);
+                }
+                //this.setMessage(err.toString(), this.messageType.ERROR);
+                ctx.redirect('/template');
+            }
         }
     }
 

+ 154 - 0
app/extend/helper.js

@@ -0,0 +1,154 @@
+'use strict';
+
+/**
+ * 指标模板控制器
+ *
+ * @author Mai
+ * @data 2018/4/19
+ * @version
+ */
+
+const fs = require('fs');
+const streamToArray = require('stream-to-array');
+
+module.exports = {
+    /**
+     * 转换数据
+     *
+     * @param {Array} list - 数据库查找的数据
+     * @param {Boolean} tree - 是否展示为树形结构
+     * @param {String} id - id属性名
+     * @param {String} pid - pid属性名
+     * @return {Object} - 返回转换后的数据
+     */
+    convertData(list, tree = true, id, pid) {
+        const rootData = [];
+        const childData = {};
+        let listData = [];
+        for (const tmp of list) {
+            if (tmp[pid] === 0 || tmp[pid] === -1) {
+                rootData.push(tmp);
+                continue;
+            }
+            if (childData[tmp[pid]] === undefined) {
+                childData[tmp[pid]] = [];
+            }
+            childData[tmp[pid]].push(tmp);
+        }
+        // 递归组织数据
+        if (tree) {
+            this._recursionTreeData(rootData, childData, id);
+        } else {
+            this._recursionData(rootData, childData, listData, id);
+        }
+
+        return tree ? rootData : listData;
+    },
+    /**
+     * 递归组织数组结构数据
+     *
+     * @param {Array} parent - 父节点
+     * @param {Object} childData - 子元素(以pid为key的对象)
+     * @param {Array} outputData - 处理后的数据
+     * @param {String} id - id属性名
+     * @return {void}
+     */
+    _recursionData(parent, childData, outputData = [], id) {
+        for (const index in parent) {
+            // 父节点先进数组
+            outputData.push(parent[index]);
+            // 判断是否存在子项
+            if (childData[parent[index][id]] !== undefined) {
+                // 子项作为父节点递归查找子项中是否还有子项
+                const tmpParent = childData[parent[index][id]];
+                // 已用的子项删除
+                delete childData[parent[index][id]];
+                // 递归元素
+                this._recursionData(tmpParent, childData, outputData);
+            }
+        }
+    },
+    /**
+     * 递归组织树结构数据
+     *
+     * @param {Array} parent - 父节点
+     * @param {Object} childData - 子元素(以pid为key的对象)
+     * @param {String} id - id属性名
+     * @return {void}
+     */
+    _recursionTreeData(parent, childData = {}, id) {
+        if (Object.keys(childData).length <= 0) {
+            return;
+        }
+        for (const index in parent) {
+            // 判断是否存在子项
+            if (childData[parent[index][id]] !== undefined) {
+                // 子项作为父节点递归查找子项中是否还有子项
+                const tmpParent = childData[parent[index][id]];
+                // 已用的子项删除
+                delete childData[parent[index][id]];
+
+                this._recursionTreeData(tmpParent, childData, id);
+                // 递归完赋值回原数据
+                parent[index].children = tmpParent;
+            }
+        }
+    },
+
+    /**
+     * 将文件流的数据保存至本地文件
+     * @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}
+     */
+    ValidTemplateCode(code) {
+        const reg1 = /(^[a-z]+)([a-z0-9\-]*)/i;
+        const reg2 = /([a-z0-9]+$)/i;
+        return reg1.test(code) && reg2.test(code);
+    },
+    /**
+     * 检查code 是否是 指标节点编号
+     * @param {String} code
+     * @returns {boolean}
+     */
+    ValidTemplateNodeCode(code) {
+        const reg1 = /(^[a-z]+)([a-z0-9\-]*$)/i;
+        const reg2 = /([a-z0-9]+$)/i;
+        const reg3 = /([\-][0-9]+$)/i;
+        return reg1.test(code) && reg2.test(code) && (!reg3.test(code));
+    },
+    /**
+     * 检查code 是否是 指标节点编号
+     * @param {String} code
+     * @returns {boolean}
+     */
+    ValidTemplateIndexCode(code) {
+        const reg1 = /(^[a-z]+)([a-z0-9\-]*$)/i;
+        const reg2 = /([0-9]+$)/i;
+        return reg1.test(code) && reg2.test(code);
+    },
+
+    findObj(arr, field, value) {
+        if (arr.length === 0) { return undefined; }
+        for (const a of arr) {
+            if (a[field] && a[field] === value) {
+                return a;
+            }
+        }
+        return undefined;
+    }
+}

+ 6 - 2
app/public/css/main.css

@@ -40,8 +40,8 @@ body {
 	background-color: #999;
 	-webkit-border-radius: 6px;
 }
-.sjs-height-1,.sjs-height-2{
-  overflow: hidden;
+.sjs-height-1,.sjs-height-2,.sjs-height-3{
+  overflow: auto;
 }
 .sjs-bottom{
   height:400px;
@@ -405,6 +405,10 @@ body {
   height:500px;
   overflow: hidden
 }
+.modal-height-500-scroll{
+  height:500px;
+  overflow: auto;
+}
 .modal-lgx {
   max-width:1000px
 }

+ 1 - 0
app/router.js

@@ -19,6 +19,7 @@ module.exports = app => {
 
     // 指标模板
     app.get('/template', sessionAuth, 'templateController.index');
+    app.post('/template/uploadExcel', sessionAuth, 'templateController.uploadExcel');
 
     // 指标对比
     app.get('/compare', sessionAuth, 'compareController.index');

+ 1 - 2
app/service/customer.js

@@ -8,11 +8,10 @@
  * @version
  */
 
-const Service = require('egg').Service;
 // 加密类
 const crypto = require('crypto');
 module.exports = app => {
-    class Customer extends Service {
+    class Customer extends app.BaseService {
         async validLogin (data) {
             let result = false;
 

+ 37 - 0
app/service/template_index.js

@@ -0,0 +1,37 @@
+'use strict';
+
+/**
+ * 指标节点业务类
+ *
+ * @author Mai
+ * @date 2018/4/19
+ * @version
+ */
+
+module.exports = app => {
+    class TemplateIndex extends app.BaseService {
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局context
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'template_index';
+        }
+
+        async importData(datas, transaction) {
+            console.log(datas[241]);
+            console.log(datas[242]);
+            console.log(datas[243]);
+            await transaction.delete(this.tableName, {template_id: 1});
+            const insertResult = await transaction.insert(this.tableName, datas);
+            if (insertResult.affectedRows !== datas.length) {
+                throw '导入指标错误';
+            }
+        }
+    };
+
+    return TemplateIndex;
+};

+ 136 - 0
app/service/template_node.js

@@ -0,0 +1,136 @@
+'use strict';
+
+/**
+ * 指标节点业务类
+ *
+ * @author Mai
+ * @date 2018/4/19
+ * @version
+ */
+
+module.exports = app => {
+    class TemplateNode extends app.BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局context
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'template_node';
+        }
+
+        /**
+         * 查找父节点(根据编号),忽略大小写
+         * e.g. z1(z), z1-e(z1), z1-e-a(z1-e)
+         * @param code
+         * @param nodes
+         * @returns {*}
+         * @private
+         */
+        _findParentId(code, nodes) {
+            if (nodes.length === 0) { return -1; }
+            const codeList = code.split('-');
+            if (codeList.length > 1) {
+                codeList.splice(codeList.length - 1);
+                const parentCode = codeList.join('-');
+                for (const node of nodes) {
+                    if (parentCode.toLowerCase() === node.code.toLowerCase()) {
+                        return node.node_id;
+                    }
+                }
+            } else {
+                for (const node of nodes) {
+                    if (code.toLowerCase().search(node.code.toLowerCase()) === 0) {
+                        return node.node_id;
+                    }
+                }
+            }
+        }
+        /**
+         * 解析一个Excel工作表内的全部 指标节点 和 指标
+         * @param {Object} excelSheet
+         * @param {Array} nodes - 解析后的指标节点
+         * @param {Array} indexes - 解析后的指标
+         * @private
+         */
+        _parseSheetData(excelSheet, nodes, indexes) {
+            for (const row of excelSheet.data) {
+                if (!row[0]) { continue; }
+                if (this.ctx.helper.ValidTemplateNodeCode(row[0])) {
+                    if (!this.ctx.helper.findObj(nodes, 'code', row[0])) {
+                        const node = {
+                            template_id: 1,
+                            node_id: nodes.length + 1,
+                            node_pid: this._findParentId(row[0], nodes) || -1,
+                            code: row[0],
+                            name: row[1],
+                        };
+                        nodes.push(node);
+                    }
+                } else if (this.ctx.helper.ValidTemplateIndexCode(row[0])) {
+                    const index = {
+                        code: row[0],
+                        name: row[1],
+                        unit1: row[2],
+                        unit2: row[3],
+                        node_id: nodes.length,
+                        index_id: indexes.length + 1,
+                        rule: row[9]
+                    };
+                    if (row[4] === '√') {
+                        index.index_type = 1;
+                    } else if (row[5] === '√') {
+                        index.index_type = 2;
+                    } else if (row[6] === '√') {
+                        index.index_type = 3;
+                    } else if (row[7] === '√') {
+                        index.index_type = 4;
+                    }
+                    indexes.push(index);
+                }
+            }
+        }
+
+        /**
+         * 导入Excel数据
+         *
+         * @param {Array} excelSheets - Excel文件中的全部工作表
+         * @returns {Promise<boolean>}
+         */
+        async importData(excelSheets) {
+            let result = false;
+            const limit = 30000;
+            const transaction = await this.db.beginTransaction();
+            try {
+                const nodes = [], indexes = [];
+                for (const sheet of excelSheets) {
+                    this._parseSheetData(sheet, nodes, indexes);
+                }
+
+                if (nodes.length > 0) {
+                    await transaction.delete(this.tableName, {template_id: 1});
+                    const insertResult = await transaction.insert(this.tableName, nodes);
+                    if (insertResult.affectedRows !== nodes.length) {
+                        throw '导入指标节点错误';
+                    }
+                    await this.ctx.service.templateIndex.importData(indexes, transaction);
+                } else {
+                    throw 'Excel文件中无标准的指标数据';
+                }
+
+                await transaction.commit();
+                result = true;
+            } catch(err) {
+                await transaction.rollback();
+                throw err;
+            }
+            return result;
+        }
+
+    }
+
+    return TemplateNode;
+};

+ 4 - 0
app/view/layout/layout.ejs

@@ -9,10 +9,14 @@
     <link rel="stylesheet" href="public/css/bootstrap/bootstrap.min.css">
     <link rel="stylesheet" href="public/css/main.css">
     <link rel="stylesheet" href="public/css/font-awesome/font-awesome.min.css">
+    <!--zTree-->
+    <link rel="stylesheet" href="public/css/ztree/zTreeStyle.css" type="text/css">
 
     <script src="/public/js/jquery/jquery-3.2.1.min.js"></script>
     <script src="/public/js/popper/popper.min.js"></script>
     <script src="/public/js/bootstrap/bootstrap.min.js"></script>
+    <script type="text/javascript" src="/public/js/ztree/jquery.ztree.core.js"></script>
+    <script type="text/javascript" src="/public/js/ztree/jquery.ztree.excheck.js"></script>
 </head>
 
 <body>

+ 71 - 33
app/view/template/index.ejs

@@ -1,39 +1,77 @@
+<div class="panel-sidebar">
+    <div class="panel-title">
+        <div class="title-bar">
+            <h2 class="">项目节 <a href="#upload" data-toggle="modal" data-target="#upload" class="pull-right mr-3" title="导入项目节"><i class="fa fa-upload"></i></a></h2>
+
+        </div>
+    </div>
+    <div class="scrollbar-auto">
+        <div class="nav-box">
+            <ul id="templateNode" class="ztree"></ul>
+        </div>
+    </div>
+</div>
 <div class="panel-content">
-    <div class="panel-title fluid">
-        <div class="title-main"><h2></h2></div>
+    <div class="panel-title">
+        <div class="title-main">
+            <h2>
+                <%= selectNode.code %> <%= selectNode.name %>
+                <a href="#add-index" data-toggle="modal" data-target="#add-index" class="btn btn-primary btn-sm pull-right">添加指标</a>
+            </h2>
+        </div>
     </div>
     <div class="content-wrap">
-        <div class="c-header m-0">
-        </div>
         <div class="c-body">
-            <table class="table table-bordered">
-                <tr>
-                    <th>指标编号</th>
-                    <th>项目或费用名称</th>
-                    <th colspan="2">指标单位</th>
-                    <th>合价(元)</th>
-                    <th>计算规则</th>
-                    <th>设置规则</th>
-                </tr>
-                <tr>
-                    <td>z2-e</td>
-                    <td>排水工程</td>
-                    <td></td>
-                    <td>km</td>
-                    <td></td>
-                    <td>指路基公里长度</td>
-                    <td></td>
-                </tr>
-                <tr>
-                    <td>z2-e-1</td>
-                    <td>排水工程公路公里造价</td>
-                    <td>元</td>
-                    <td>公路公里</td>
-                    <td></td>
-                    <td>合价/路线总长度</td>
-                    <td>hj(合价)/lxzcd(路线总长度)<a href="#set-count" data-toggle="modal" data-target="#set-count"><i class="fa fa-cog"></i></a></td>
-                </tr>
-            </table>
+            <div class="sjs-height-1">
+                <table class="table table-bordered">
+                    <tr>
+                        <th>指标编号</th>
+                        <th>指标名称</th>
+                        <th colspan="2">指标单位</th>
+                        <th>计算规则</th>
+                        <th>设置规则</th>
+                    </tr>
+                    <% for (const index of selectIndex) { %>
+                    <tr>
+                        <td><%= index.code %></td>
+                        <td><%= index.name %></td>
+                        <td><%= index.unit1 %></td>
+                        <td><%= index.unit2 %></td>
+                        <td><%= index.rule %></td>
+                        <td><%= index.rule %><a href="#set-count" data-toggle="modal" data-target="#set-count"><i class="fa fa-cog"></i></a></td>
+                    </tr>
+                    <% } %>
+                </table>
+            </div>
         </div>
     </div>
-</div>
+</div>
+<script type="text/javascript">
+    const treeSetting = {
+        treeId: 'template',
+        view: {showIcon: false},
+        data: {
+            key: {
+                name: 'text',
+            }
+        }
+    };
+    const selectNode = '<%- selectNode.node_id %>';
+    const treeNode = '<%- nodes %>';
+
+    $(document).ready(function(){
+        const loadText = function (arr) {
+            for (const a of arr) {
+                a.text = a.code + ' ' + a.name;
+                if (a.children && a.children.length > 0) {
+                    loadText(a.children);
+                }
+            }
+        }
+        const treeNodeData = treeNode !== '' ? JSON.parse(treeNode) : [];
+        loadText(treeNodeData);
+        const treeObj = $.fn.zTree.init($("#templateNode"), treeSetting, treeNodeData);
+        const node = treeObj.getNodeByParam('node_id', selectNode);
+        treeObj.selectNode(node);
+    });
+</script>

+ 55 - 20
app/view/template/modal.ejs

@@ -1,3 +1,4 @@
+<!-- 设置计算式 -->
 <div id="set-count" class="modal fade" tabindex="-1" role="dialog" aria-hidden="true">
     <div class="modal-dialog">
         <div class="modal-content">
@@ -6,29 +7,40 @@
                 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
             </div>
             <div class="modal-body">
-                <h5 class="mt-3">hj(合价)/lxzcd(路线总长度)</h5>
+                <h5>当前规则:</h5>
+                <h5>
+                    <span class="badge badge-light" title="合价">合价 <!--<a href="#" class="text-danger" title="移除"><i class="fa fa-remove"></i></a>--></span>
+                    <span class="badge badge-light" title="/">/ <!--<a href="#" class="text-danger" title="移除"><i class="fa fa-remove"></i></a>--></span>
+                    <span class="badge badge-light" title="路线总长度">路线总长度 <a href="#" class="text-danger" title="移除"><i class="fa fa-remove"></i></a></span>
+                </h5>
                 <div class="form-group">
-                    <label>设置计算规则</label>
-                    <input class="form-control" value="hj/lxzcd" type="text">
+                    <select class="form-control">
+                        <option>全局参数</option>
+                        <option>本项目节参数</option>
+                        <option>计算式</option>
+                    </select>
                 </div>
-                <div class="row">
-                    <div class="col-6">
-                        <table class="table table-bordered">
-                            <legend>全局参数</legend>
-                            <tr><th>名称</th><th>代码</th><th>描述</th></tr>
-                            <tr><td>总造价</td><td>zzj</td><td><a href=""><i class="fa fa-info"></i></a></td></tr>
-                            <tr><td>路线总长度</td><td>lxzcd</td><td><a href=""><i class="fa fa-info"></i></a></td></tr>
-                            <tr><td>输入框</td><td>input</td><td><a href=""><i class="fa fa-info"></i></a></td></tr>
-                        </table>
-                    </div>
-                    <div class="col-6">
-                        <table class="table table-bordered">
-                            <legend>本项目节参数</legend>
-                            <tr><th>名称</th><th>代码</th><th>描述</th></tr>
-                            <tr><td>合价</td><td>hj</td><td><a href=""><i class="fa fa-info"></i></a></td></tr>
-                        </table>
-                    </div>
+                <!--全局参数-->
+                <div class="form-group">
+                    <select class="form-control">
+                        <option>总造价</option>
+                        <option>路线总长度</option>
+                        <option>计算式</option>
+                    </select>
+                </div>
+                <!--本项目节参数-->
+                <div class="form-group">
+                    <select class="form-control">
+                        <option>合价</option>
+                    </select>
                 </div>
+                <!--计算式-->
+                <div class="form-group">
+                    <select class="form-control">
+                        <option>/</option>
+                    </select>
+                </div>
+                <button class="btn btn-outline-primary">添加</button>
             </div>
             <div class="modal-footer">
                 <button class="btn btn-primary">确定</button>
@@ -36,4 +48,27 @@
             </div>
         </div>
     </div>
+</div>
+<!-- 导入项目节 -->
+<div id="upload" class="modal fade" tabindex="-1" role="dialog" aria-hidden="true">
+    <form class="modal-dialog" action="/template/uploadExcel?_csrf=<%= ctx.csrf %>" method="post" enctype="multipart/form-data">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">导入项目节</h5>
+                <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
+            </div>
+            <div class="modal-body">
+                <div class="form-group">
+                    <label for="exampleFormControlFile1">上传项目节文件</label>
+                    <div class="form-control">
+                        <input class="form-control-file" name="file" type="file">
+                    </div>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="submit" class="btn btn-primary">确认导入</button>
+                <button class="btn btn-secondary" data-dismiss="modal" aria-hidden="true">关闭</button>
+            </div>
+        </div>
+    </form>
 </div>

+ 8 - 0
config/config.default.js

@@ -44,9 +44,17 @@ module.exports = appInfo => {
         httpOnly: true,
         encrypt: true,
     };
+    // 表名前缀
+    config.tablePrefix = 'is_';
 
     // add your config here
     config.middleware = ['urlParse'];
 
+    // 上传设置
+    config.multipart = {
+        whitelist: ['.xls', '.xlsx'],
+        fileSize: '10mb',
+    };
+
     return config;
 };

+ 60 - 0
config/config.local.js

@@ -0,0 +1,60 @@
+'use strict';
+
+const path = require('path');
+module.exports = appInfo => {
+    const config = exports = {};
+
+    // use for cookie sign key, should change to your own and keep security
+    config.keys = appInfo.name + '_smartcost3850888';
+
+    // 数据库配置
+    config.mysql = {
+        client: {
+            // host
+            host: '127.0.0.1',
+            // 端口号
+            port: '3306',
+            // 用户名
+            user: 'root',
+            // 密码
+            password: 'admin',
+            // 数据库名
+            database: 'index_sys',
+        },
+        // 是否加载到 app 上,默认开启
+        app: true,
+        // 是否加载到 agent 上,默认关闭
+        agent: false,
+    };
+    // 表名前缀
+    config.tablePrefix = 'is_';
+
+    // view
+    config.view = {
+        mapping: {
+            '.ejs': 'ejs',
+        },
+        root: [
+            path.join(appInfo.baseDir, 'app/view'),
+        ].join(','),
+    };
+
+    // session
+    config.session = {
+        key: 'ZHC_SESS',
+        maxAge: 3600 * 1000,
+        httpOnly: true,
+        encrypt: true,
+    };
+
+    // add your config here
+    config.middleware = ['urlParse'];
+
+    // 上传设置
+    config.multipart = {
+        whitelist: ['.xls', '.xlsx'],
+        fileSize: '10mb',
+    };
+
+    return config;
+};

+ 30 - 16
config/config.qa.js

@@ -1,14 +1,12 @@
 'use strict';
-/**
- * 开发环境相关配置
- *
- * @author CaiAoLin
- * @date 2017/8/29
- * @version
- */
 
+const path = require('path');
 module.exports = appInfo => {
-    const config = {};
+    const config = exports = {};
+
+    // use for cookie sign key, should change to your own and keep security
+    config.keys = appInfo.name + '_smartcost3850888';
+
     // 数据库配置
     config.mysql = {
         client: {
@@ -31,15 +29,31 @@ module.exports = appInfo => {
     // 表名前缀
     config.tablePrefix = 'is_';
 
-    // redis设置
-    config.redis = {
-        client: {
-            host: '192.168.1.139',
-            port: '6379',
-            password: 'test',
-            db: '0',
+    // view
+    config.view = {
+        mapping: {
+            '.ejs': 'ejs',
         },
-        agent: true,
+        root: [
+            path.join(appInfo.baseDir, 'app/view'),
+        ].join(','),
+    };
+
+    // session
+    config.session = {
+        key: 'ZHC_SESS',
+        maxAge: 3600 * 1000,
+        httpOnly: true,
+        encrypt: true,
+    };
+
+    // add your config here
+    config.middleware = ['urlParse'];
+
+    // 上传设置
+    config.multipart = {
+        whitelist: ['.xls', '.xlsx'],
+        fileSize: '10mb',
     };
 
     return config;

+ 5 - 0
config/plugin.js

@@ -6,4 +6,9 @@
 exports.ejs = {
     enable: true,
     package: 'egg-view-ejs',
+};
+// mysql插件
+exports.mysql = {
+    enable: true,
+    package: 'egg-mysql',
 };

+ 7 - 2
package.json

@@ -9,7 +9,10 @@
     "egg-redis": "^2.0.0",
     "egg-scripts": "^2.5.0",
     "egg-view": "^2.1.0",
-    "egg-view-ejs": "^2.0.0"
+    "egg-view-ejs": "^2.0.0",
+    "node-xlsx": "^0.12.0",
+    "stream-to-array": "^2.3.0",
+    "stream-wormhole": "^1.0.3"
   },
   "devDependencies": {
     "autod": "^3.0.1",
@@ -27,7 +30,9 @@
   "scripts": {
     "start": "egg-scripts start --daemon --title=egg-server-index_sys",
     "stop": "egg-scripts stop --title=egg-server-index_sys",
-    "dev": "egg-bin dev",
+    "dev": "egg-bin dev --port 7003",
+    "dev-qa": "set EGG_SERVER_ENV=qa && egg-bin dev --port 7003",
+    "dev-local": "set EGG_SERVER_ENV=local && egg-bin dev --port 7003",
     "debug": "egg-bin debug",
     "test": "npm run lint -- --fix && npm run test-local",
     "test-local": "egg-bin test",

+ 30 - 0
test/app/extend/helper.test.js

@@ -0,0 +1,30 @@
+'use strict';
+
+/**
+ * 辅助方法扩展 单元测试
+ *
+ * @author Mai
+ * @data 2018/4/20
+ * @version
+ */
+
+const { app, assert } = require('egg-mock/bootstrap');
+
+describe('test/app/extend/helper.test.js', () => {
+    it('ValidTemplateCode test', function () {
+        const ctx = app.mockContext();
+        assert(ctx.helper.ValidTemplateCode('z') === true);
+        assert(ctx.helper.ValidTemplateCode('Z') === true);
+        assert(ctx.helper.ValidTemplateCode('-') === false);
+        assert(ctx.helper.ValidTemplateCode('1') === false);
+        assert(ctx.helper.ValidTemplateCode('z1') === true);
+        assert(ctx.helper.ValidTemplateCode('z1-') === false);
+        assert(ctx.helper.ValidTemplateCode('zav') === true);
+        assert(ctx.helper.ValidTemplateCode('z1-e') === true);
+        assert(ctx.helper.ValidTemplateCode('z1-e-1') === true);
+    });
+    it('ValidTemplateIndexCode test', function () {
+        const ctx = app.mockContext();
+        assert(ctx.helper.ValidTemplateIndexCode('z2-d-d-1~z2-d-d-5   高填方路段处理   同z2-d-c-1~z2-d-c-5') === false);
+    });
+});

+ 27 - 0
test/app/service/template_node.test.js

@@ -0,0 +1,27 @@
+'use strict';
+
+/**
+ * 指标节点业务逻辑 单元测试
+ *
+ * @author Mai
+ * @data 2018/4/20
+ * @version
+ */
+
+const { app, assert } = require('egg-mock/bootstrap');
+// excel解析
+const excel = require('node-xlsx');
+
+describe('test/app/service/template_node.test.js', () => {
+    it('_parseSheetData test', function () {
+        const ctx = app.mockContext();
+        const fileName = app.baseDir + '/test/app/service/test.xls';
+        const sheets = excel.parse(fileName);
+        const nodes = [], indexes = [];
+        for (const sheet of sheets) {
+            ctx.service.templateNode._parseSheetData(sheet, nodes, indexes);
+        }
+        assert(nodes.length === 5);
+        assert(indexes.length === 42);
+    });
+});