Ver código fonte

签名功能和个人页修改

laiguoran 5 anos atrás
pai
commit
8e8e72c7f8

+ 102 - 5
app/controller/profile_controller.js

@@ -8,11 +8,24 @@
  * @version
  */
 
+const profileMenu = require('../../config/menu').profileMenu;
+const qr = require('qr-image');
+
 module.exports = app => {
 
     class ProfileController extends app.BaseController {
 
         /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局context
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            ctx.subMenu = profileMenu;
+        }
+        /**
          * 账号资料页面
          *
          * @param {Object} ctx - egg全局变量
@@ -29,14 +42,9 @@ module.exports = app => {
             const baseRule = ctx.service.projectAccount.rule('profileBase');
             const baseJsValidator = await this.jsValidator.convert(baseRule).setSelector('#base-form').build();
 
-            // 获取修改密码的字段规则
-            const passwordRule = ctx.service.projectAccount.rule('modifyPassword');
-            const passwordJsValidator = await this.jsValidator.convert(passwordRule).setSelector('#password-form').build();
-
             const renderData = {
                 accountData,
                 baseJsValidator,
-                passwordJsValidator,
             };
             await this.layout('profile/info.ejs', renderData);
         }
@@ -156,6 +164,95 @@ module.exports = app => {
             }
             ctx.redirect(ctx.request.header.referer);
         }
+
+        /**
+         * 短信通知
+         *
+         * @param {object} ctx - egg全局变量
+         * @return {void}
+         */
+        async sms(ctx) {
+            // 获取当前用户数据
+            const sessionUser = ctx.session.sessionUser;
+
+            // 获取账号数据
+            const accountData = await ctx.service.projectAccount.getDataByCondition({ id: sessionUser.accountId });
+
+            const renderData = {
+                accountData,
+            };
+            await this.layout('profile/sms.ejs', renderData);
+        }
+
+        /**
+         * 电子签名
+         *
+         * @param {object} ctx - egg全局变量
+         * @return {void}
+         */
+        async sign(ctx) {
+            // 获取当前用户数据
+            const sessionUser = ctx.session.sessionUser;
+
+            // 获取账号数据
+            const accountData = await ctx.service.projectAccount.getDataByCondition({ id: sessionUser.accountId });
+
+            const renderData = {
+                accountData,
+            };
+            await this.layout('profile/sign.ejs', renderData);
+        }
+
+        /**
+         * 生成二维码
+         *
+         * @param {object} ctx - egg全局变量
+         * @return {void}
+         */
+        async qrCode(ctx) {
+            const size = 5;
+            const margin = 1;
+            try {
+                // 获取当前用户数据
+                const sessionUser = ctx.session.sessionUser;
+
+                const text = ctx.request.header.host + '/sign?user_id=' + sessionUser.accountId + '&app_token=' + sessionUser.sessionToken;
+
+                // 大小默认5,二维码周围间距默认1
+                const img = qr.image(text || '', { type: 'png', size: size || 5, margin: margin || 1 });
+                ctx.status = 200;
+                ctx.type = 'image/png';
+                ctx.body = img;
+            } catch (e) {
+                ctx.status = 414;
+                ctx.set('Content-Type', 'text/html');
+                ctx.body = '<h1>414 Request-URI Too Large</h1>';
+            }
+        }
+
+        /**
+         * 账号安全
+         *
+         * @param {object} ctx - egg全局变量
+         * @return {void}
+         */
+        async safe(ctx) {
+            // 获取当前用户数据
+            const sessionUser = ctx.session.sessionUser;
+
+            // 获取账号数据
+            const accountData = await ctx.service.projectAccount.getDataByCondition({ id: sessionUser.accountId });
+
+            // 获取修改密码的字段规则
+            const passwordRule = ctx.service.projectAccount.rule('modifyPassword');
+            const passwordJsValidator = await this.jsValidator.convert(passwordRule).setSelector('#password-form').build();
+
+            const renderData = {
+                accountData,
+                passwordJsValidator,
+            };
+            await this.layout('profile/safe.ejs', renderData);
+        }
     }
 
     return ProfileController;

+ 73 - 0
app/controller/sign_controller.js

@@ -0,0 +1,73 @@
+'use strict';
+
+/**
+ * 签名相关控制器
+ *
+ * @author EllisRan
+ * @date 2019/8/14
+ * @version
+ */
+const moment = require('moment');
+const path = require('path');
+const sendToWormhole = require('stream-wormhole');
+const fs = require('fs');
+
+module.exports = app => {
+
+    class SignController extends app.BaseController {
+        /**
+         * 电子签名页面
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        async index(ctx) {
+            const renderData = {
+                error: false,
+            };
+            try {
+                const userinfo = await ctx.service.projectAccount.getDataById(ctx.query.user_id);
+                if (userinfo && userinfo.session_token && userinfo.session_token === ctx.query.app_token) {
+                    renderData.id = userinfo.id;
+                    renderData.name = userinfo.name;
+                    renderData.role = userinfo.role;
+                } else {
+                    throw '参数有误, 无法访问本页.';
+                }
+            } catch (error) {
+                console.log(error);
+                renderData.error = true;
+            }
+            await ctx.render('sign/info.ejs', renderData);
+        }
+
+        /**
+         * 保存签名
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        async save(ctx) {
+            try {
+                const stream = await ctx.getFileStream({ requireFile: false });
+                const create_time = Date.parse(new Date()) / 1000;
+                const dirName = 'app/public/upload/sign/';
+                const fileName = moment().format('YYYYMMDD') + '_sign_' + create_time + '.png';
+                await ctx.helper.saveStreamFile(stream, path.join(this.app.baseDir, dirName, fileName));
+                await sendToWormhole(stream);
+
+                const result = await ctx.service.projectAccount.update({ sign_path: fileName }, { id: stream.fields.id });
+                if (result) {
+                    ctx.body = { err: 0, msg: '' };
+                } else {
+                    throw '添加数据库失败';
+                }
+            } catch (err) {
+                this.log(err);
+                ctx.body = { err: 1, msg: err.toString() };
+            }
+        }
+    }
+
+    return SignController;
+};

BIN
app/public/images/baobiao3.png


BIN
app/public/images/user-sign.PNG


+ 241 - 0
app/public/js/draw.js

@@ -0,0 +1,241 @@
+/**
+ * Created by louizhai on 17/6/30.
+ * description: Use canvas to draw.
+ */
+function Draw(canvas, degree, config = {}) {
+  if (!(this instanceof Draw)) {
+    return new Draw(canvas, config);
+  }
+  if (!canvas) {
+    return;
+  }
+  let { width, height } = window.getComputedStyle(canvas, null);
+  // width = width.replace('px', '');
+  height = height.replace('px', '');
+  width = height.replace('px', '')*2;
+
+  this.canvas = canvas;
+  this.context = canvas.getContext('2d');
+  this.width = width;
+  this.height = height;
+  const context = this.context;
+
+  // 根据设备像素比优化canvas绘图
+  const devicePixelRatio = window.devicePixelRatio;
+  if (devicePixelRatio) {
+    canvas.style.width = `${width}px`;
+    canvas.style.height = `${height}px`;
+    canvas.height = height * devicePixelRatio;
+    canvas.width = width * devicePixelRatio;
+    context.scale(devicePixelRatio, devicePixelRatio);
+  } else {
+    canvas.width = width;
+    canvas.height = height;
+  }
+
+  context.lineWidth = 6;
+  context.strokeStyle = 'black';
+  context.lineCap = 'round';
+  context.lineJoin = 'round';
+  Object.assign(context, config);
+
+  const { left, top } = canvas.getBoundingClientRect();
+  const point = {};
+  const isMobile = /phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone/i.test(navigator.userAgent);
+  // 移动端性能太弱, 去掉模糊以提高手写渲染速度
+  if (!isMobile) {
+    context.shadowBlur = 1;
+    context.shadowColor = 'black';
+  }
+  let pressed = false;
+
+  const paint = (signal) => {
+    switch (signal) {
+      case 1:
+        context.beginPath();
+        context.moveTo(point.x, point.y);
+      case 2:
+        context.lineTo(point.x, point.y);
+        context.stroke();
+        break;
+      default:
+    }
+  };
+  const create = signal => (e) => {
+    e.preventDefault();
+    if (signal === 1) {
+      pressed = true;
+    }
+    if (signal === 1 || pressed) {
+      e = isMobile ? e.touches[0] : e;
+      point.x = e.clientX - left;
+      point.y = e.clientY - top;
+      paint(signal);
+    }
+  };
+  const start = create(1);
+  const move = create(2);
+  const requestAnimationFrame = window.requestAnimationFrame;
+  const optimizedMove = requestAnimationFrame ? (e) => {
+    requestAnimationFrame(() => {
+      move(e);
+    });
+  } : move;
+
+  if (isMobile) {
+    canvas.addEventListener('touchstart', start);
+    canvas.addEventListener('touchmove', optimizedMove);
+  } else {
+    canvas.addEventListener('mousedown', start);
+    canvas.addEventListener('mousemove', optimizedMove);
+    ['mouseup', 'mouseleave'].forEach((event) => {
+      canvas.addEventListener(event, () => {
+        pressed = false;
+      });
+    });
+  }
+
+  // 重置画布坐标系
+  if (typeof degree === 'number') {
+    this.degree = degree;
+    context.rotate((degree * Math.PI) / 180);
+    switch (degree) {
+      case -90:
+        context.translate(-height, 0);
+        break;
+      case 90:
+        context.translate(0, -width);
+        break;
+      case -180:
+      case 180:
+        context.translate(-width, -height);
+        break;
+      default:
+    }
+  }
+}
+Draw.prototype = {
+  scale(width, height, canvas = this.canvas) {
+    const w = canvas.width;
+    const h = canvas.height;
+    width = width || w;
+    height = height || h;
+    if (width !== w || height !== h) {
+      const tmpCanvas = document.createElement('canvas');
+      const tmpContext = tmpCanvas.getContext('2d');
+      tmpCanvas.width = width;
+      tmpCanvas.height = height;
+      tmpContext.drawImage(canvas, 0, 0, w, h, 0, 0, width, height);
+      canvas = tmpCanvas;
+    }
+    return canvas;
+  },
+  rotate(degree, image = this.canvas) {
+    degree = ~~degree;
+    if (degree !== 0) {
+      const maxDegree = 180;
+      const minDegree = -90;
+      if (degree > maxDegree) {
+        degree = maxDegree;
+      } else if (degree < minDegree) {
+        degree = minDegree;
+      }
+
+      const canvas = document.createElement('canvas');
+      const context = canvas.getContext('2d');
+      const height = image.height;
+      const width = image.width;
+      const degreePI = (degree * Math.PI) / 180;
+
+      switch (degree) {
+        // 逆时针旋转90°
+        case -90:
+          canvas.width = height;
+          canvas.height = width;
+          context.rotate(degreePI);
+          context.drawImage(image, -width, 0);
+          break;
+        // 顺时针旋转90°
+        case 90:
+          canvas.width = height;
+          canvas.height = width;
+          context.rotate(degreePI);
+          context.drawImage(image, 0, -height);
+          break;
+        // 顺时针旋转180°
+        case 180:
+          canvas.width = width;
+          canvas.height = height;
+          context.rotate(degreePI);
+          context.drawImage(image, -width, -height);
+          break;
+        default:
+      }
+      image = canvas;
+    }
+    return image;
+  },
+  getPNGImage(canvas = this.canvas) {
+    console.log(canvas.width, canvas.height);
+    return canvas.toDataURL('image/png');
+  },
+  getJPGImage(canvas = this.canvas) {
+    return canvas.toDataURL('image/jpeg', 0.5);
+  },
+  downloadPNGImage(image) {
+    const url = image.replace('image/png', 'image/octet-stream;Content-Disposition:attachment;filename=test.png');
+    window.location.href = url;
+  },
+  dataURLtoBlob(dataURL) {
+    const arr = dataURL.split(',');
+    const mime = arr[0].match(/:(.*?);/)[1];
+    const bStr = atob(arr[1]);
+    let n = bStr.length;
+    const u8arr = new Uint8Array(n);
+    while (n--) {
+      u8arr[n] = bStr.charCodeAt(n);
+    }
+    return new Blob([u8arr], { type: mime });
+  },
+  clear() {
+    let width;
+    let height;
+    switch (this.degree) {
+      case -90:
+      case 90:
+        width = this.height;
+        height = this.width;
+        break;
+      default:
+        width = this.width;
+        height = this.height;
+    }
+    this.context.clearRect(0, 0, width, height);
+  },
+  upload(blob, url, success, failure) {
+    const formData = new FormData();
+    const xhr = new XMLHttpRequest();
+    xhr.withCredentials = true;
+    formData.append('id', id);
+    formData.append('image', blob, 'sign');
+
+    xhr.open('POST', url, true);
+    xhr.setRequestHeader("x-csrf-token", csrf);
+    xhr.onload = () => {
+      if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
+        success(xhr.responseText);
+      } else {
+        failure();
+      }
+    };
+    xhr.onerror = (e) => {
+      if (typeof failure === 'function') {
+        failure(e);
+      } else {
+        console.log(`upload img error: ${e}`);
+      }
+    };
+    xhr.send(formData);
+  },
+};
+// export default Draw;

+ 1 - 1
app/public/upload/pay/page.html

@@ -1 +1 @@
-当前页存变更令附件
+当前页存合同支付附件

+ 1 - 0
app/public/upload/sign/page.html

@@ -0,0 +1 @@
+当前页存签名图片

+ 1 - 1
app/public/upload/stage/page.html

@@ -1 +1 @@
-当前页存变更令附件
+当前页存计量台账附件

+ 7 - 0
app/router.js

@@ -19,6 +19,9 @@ module.exports = app => {
     app.get('/logout', 'loginController.logout');
     app.post('/login', 'loginController.login');
 
+    app.get('/sign', 'signController.index');
+    app.post('/sign/save', 'signController.save');
+
     // 用户信息初始化相关
     app.get('/boot', sessionAuth, 'bootController.index');
     app.post('/boot', sessionAuth, 'bootController.boot');
@@ -196,10 +199,14 @@ module.exports = app => {
 
     // 个人账号相关
     app.get('/profile/info', sessionAuth, 'profileController.info');
+    app.get('/profile/sms', sessionAuth, 'profileController.sms');
+    app.get('/profile/sign', sessionAuth, 'profileController.sign');
+    app.get('/profile/safe', sessionAuth, 'profileController.safe');
     app.post('/profile/save', sessionAuth, 'profileController.saveBase');
     app.post('/profile/password', sessionAuth, 'profileController.modifyPassword');
     app.post('/profile/code', sessionAuth, 'profileController.getCode');
     app.post('/profile/bind', sessionAuth, 'profileController.bindMobile');
+    app.get('/profile/qrCode', sessionAuth, 'profileController.qrCode');
 
     // 中间计量 - 计量编制相关
     // app.get('/measure/wlist', sessionAuth, tenderSelect, 'measureController.list');

+ 5 - 3
app/service/project_account.js

@@ -166,15 +166,17 @@ module.exports = app => {
                 // 如果成功则更新登录时间
                 if (result) {
                     const currentTime = new Date().getTime() / 1000;
+                    // 加密token
+                    const sessionToken = crypto.createHmac('sha1', currentTime + '').update(accountData.account)
+                        .digest().toString('base64');
+
                     if (loginType === 2) {
                         const updateData = {
                             last_login: currentTime,
+                            session_token: sessionToken,
                         };
                         await this.update(updateData, { id: accountData.id });
                     }
-                    // 加密token
-                    const sessionToken = crypto.createHmac('sha1', currentTime + '').update(accountData.account)
-                        .digest().toString('base64');
                     // 存入session
                     this.ctx.session.sessionUser = {
                         account: accountData.account,

+ 1 - 1
app/service/stage_pay.js

@@ -216,7 +216,7 @@ module.exports = app => {
                 update.push({
                     id: sp.id,
                     tp: sp.tp,
-                    end_tp: sp.end_tp
+                    end_tp: sp.end_tp,
                 });
                 if (stage.order === 1 || sp.csorder >= stage.order) {
                     srUpdate.push({

+ 1 - 1
app/view/layout/body_header.ejs

@@ -17,4 +17,4 @@
             <% } %>
         </div>
     </div>
-</div>
+</div>

+ 2 - 2
app/view/layout/menu.ejs

@@ -25,9 +25,9 @@
             </a>
             <div class="dropdown-menu">
                 <a href="/profile/info" class="dropdown-item">账号资料</a>
-                <a href="#" class="dropdown-item">账号安全</a>
+                <a href="/profile/safe" class="dropdown-item">账号安全</a>
                 <div class="dropdown-divider"></div>
-                <a href="#" class="dropdown-item">帮助中心</a>
+                <!--<a href="#" class="dropdown-item">帮助中心</a>-->
                 <a href="/logout" class="dropdown-item">退出登录</a>
             </div>
         </div>

+ 4 - 38
app/view/profile/info.ejs

@@ -1,4 +1,4 @@
-<% include ../layout/body_header.ejs %>
+<% include ./sub_menu.ejs %>
 <div class="panel-content" id="app">
     <div class="panel-title">
         <div class="title-main">
@@ -7,8 +7,8 @@
     </div>
     <div class="content-wrap">
         <div class="c-body">
-            <div class="row">
-                <div class="col-5">
+            <div class="row m-0">
+                <div class="col-5 my-3">
                     <!--账号资料-->
                     <form action="/profile/save" method="post" id="base-form">
                         <input-text label="账号" value="<%= accountData.account %>" readonly="readonly"></input-text>
@@ -20,50 +20,16 @@
                         <input type="hidden" name="_csrf" value="<%= ctx.csrf %>">
                         <button type="submit" class="btn btn-primary" id="base-submit">确认修改</button>
                     </form>
-                    <!--账号安全-->
-                    <form action="/profile/password" method="post" style="margin-top: 20px;" id="password-form">
-                        <% if(accountData.password !== 'SSO password') { %>
-                        <input-text label="旧密码" password="true" name="password"></input-text>
-                        <input-text label="新密码" password="true" name="new_password" id="new_password"></input-text>
-                        <input-text label="确认新密码" password="true" name="confirm_password"></input-text>
-                        <input type="hidden" name="_csrf" value="<%= ctx.csrf %>">
-                        <button type="submit" class="btn btn-primary" id="modify-password">修改密码</button>
-                        <% } else { %>
-                        <p>SSO用户请到<a href="#">此处</a>修改密码</p>
-                        <% } %>
-                    </form>
-                    <!--绑定手机-->
-                    <form id="mobile-form" method="post" action="/profile/bind">
-                        <div class="form-group mt-5">
-                            <label>认证手机(用于 找回密码、接收通知)</label>
-                            <input class="form-control" placeholder="输入11位手机号码" value="<%= accountData.auth_mobile %>"
-                                   <% if(accountData.auth_mobile !== '') { %>disabled="disabled"<% } %> name="auth_mobile"
-                                    maxlength="11"/>
-                        </div>
-                        <% if (accountData.auth_mobile === '') { %>
-                        <div class="form-group">
-                            <div class="input-group mb-3">
-                                <input class="form-control" readonly="readonly" name="code"/>
-                                <input type="hidden" name="_csrf" value="<%= ctx.csrf %>">
-                                <div class="input-group-append">
-                                    <button class="btn btn-outline-secondary" type="button" id="get-code">获取验证码</button>
-                                </div>
-                            </div>
-                        </div>
-                        <button type="submit" class="btn btn-secondary disabled" id="bind-btn">确认绑定</button>
-                        <% } %>
-                    </form>
                 </div>
             </div>
         </div>
     </div>
 </div>
 <%- baseJsValidator %>
-<%- passwordJsValidator %>
 <script type="text/javascript">
     new Vue({
         el: '#app',
     });
     const csrf = '<%= ctx.csrf %>';
 </script>
-<script type="text/javascript" src="/public/js/profile.js"></script>
+<script type="text/javascript" src="/public/js/profile.js"></script>

+ 36 - 0
app/view/profile/safe.ejs

@@ -0,0 +1,36 @@
+<% include ./sub_menu.ejs %>
+<div class="panel-content" id="app">
+    <div class="panel-title">
+        <div class="title-main">
+            <h2>账号安全</h2>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="c-body">
+            <div class="row m-0">
+                <div class="col-5 my-3">
+                    <!--账号安全-->
+                    <form action="/profile/password" method="post" id="password-form">
+                        <% if(accountData.password !== 'SSO password') { %>
+                        <input-text label="旧密码" password="true" name="password"></input-text>
+                        <input-text label="新密码" password="true" name="new_password" id="new_password"></input-text>
+                        <input-text label="确认新密码" password="true" name="confirm_password"></input-text>
+                        <input type="hidden" name="_csrf" value="<%= ctx.csrf %>">
+                        <button type="submit" class="btn btn-primary" id="modify-password">修改密码</button>
+                        <% } else { %>
+                        <p>SSO用户请到<a href="#">此处</a>修改密码</p>
+                        <% } %>
+                    </form>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+<%- passwordJsValidator %>
+<script type="text/javascript">
+    new Vue({
+        el: '#app',
+    });
+    const csrf = '<%= ctx.csrf %>';
+</script>
+<script type="text/javascript" src="/public/js/profile.js"></script>

+ 58 - 0
app/view/profile/sign.ejs

@@ -0,0 +1,58 @@
+<% include ./sub_menu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main">
+            <h2>电子签名</h2>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="c-body">
+            <div class="row m-0">
+                <div class="col-5 my-3">
+                    <!--账号资料-->
+                    <form>
+                        <!--<div class="form-group">-->
+                            <!--<div class="form-check form-check-inline">-->
+                                <!--<input class="form-check-input" type="radio" name="inlineRadioOptions" id="inlineRadio2" value="option2">-->
+                                <!--<label class="form-check-label" for="inlineRadio2">在线手写签名图</label>-->
+                            <!--</div>-->
+                            <!--<div class="form-check form-check-inline">-->
+                                <!--<input class="form-check-input" type="radio" name="inlineRadioOptions" id="inlineRadio1" value="option1">-->
+                                <!--<label class="form-check-label" for="inlineRadio1">上传签名图</label>-->
+                            <!--</div>-->
+                        <!--</div>-->
+                        <!--<div class="form-group">-->
+                            <!--<label>上传签名图</label>-->
+                            <!--<input type="file" class="form-control-file" id="exampleFormControlFile1">-->
+                            <!--<small class="form-text text-danger">图片大小为600x300,格式PNG透明背景。</small>-->
+                        <!--</div>-->
+                        <div class="form-group">
+                            <label>在线手写签名</label>
+                            <div><img src="/profile/qrCode" width="150"></div>
+                            <small class="form-text text-danger">微信扫码使用在线手写程序</small>
+                        </div>
+                        <button type="submit" class="btn btn-danger">移除签名</button>
+                        <div class="form-group">
+                            <label>签名图预览</label>
+                            <div>
+                                <div class="position-relative">
+                                    <img src="/public/images/baobiao3.png">
+                                    <div class="position-absolute fixed-top" style="left:290px;top:320px">
+                                        <img src="/public/images/user-sign.png" width="90">
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </form>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+<script type="text/javascript">
+    new Vue({
+        el: '#app',
+    });
+    const csrf = '<%= ctx.csrf %>';
+</script>
+<script type="text/javascript" src="/public/js/profile.js"></script>

+ 44 - 0
app/view/profile/sms.ejs

@@ -0,0 +1,44 @@
+<% include ./sub_menu.ejs %>
+<div class="panel-content" id="app">
+    <div class="panel-title">
+        <div class="title-main">
+            <h2>短信通知</h2>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="c-body">
+            <div class="row m-0">
+                <div class="col-5 my-3">
+                    <!--绑定手机-->
+                    <form id="mobile-form" method="post" action="/profile/bind">
+                        <div class="form-group">
+                            <label>认证手机(用于 找回密码、接收通知)</label>
+                            <input class="form-control" placeholder="输入11位手机号码" value="<%= accountData.auth_mobile %>"
+                                   <% if(accountData.auth_mobile !== '') { %>disabled="disabled"<% } %> name="auth_mobile"
+                                    maxlength="11"/>
+                        </div>
+                        <% if (accountData.auth_mobile === '') { %>
+                        <div class="form-group">
+                            <div class="input-group mb-3">
+                                <input class="form-control" readonly="readonly" name="code"/>
+                                <input type="hidden" name="_csrf" value="<%= ctx.csrf %>">
+                                <div class="input-group-append">
+                                    <button class="btn btn-outline-secondary" type="button" id="get-code">获取验证码</button>
+                                </div>
+                            </div>
+                        </div>
+                        <button type="submit" class="btn btn-secondary disabled" id="bind-btn">确认绑定</button>
+                        <% } %>
+                    </form>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+<script type="text/javascript">
+    new Vue({
+        el: '#app',
+    });
+    const csrf = '<%= ctx.csrf %>';
+</script>
+<script type="text/javascript" src="/public/js/profile.js"></script>

+ 20 - 0
app/view/profile/sub_menu.ejs

@@ -0,0 +1,20 @@
+<div class="panel-sidebar">
+    <div class="panel-title">
+        <div class="title-bar">
+            <h2>项目信息</h2>
+        </div>
+    </div>
+    <div class="scrollbar-auto">
+        <div class="nav-box">
+            <ul class="nav-list list-unstyled">
+                <% for (const index in ctx.subMenu) { %>
+                <li <% if (ctx.url === ctx.subMenu[index].url) { %>class="active"<% } %>>
+                    <a href="<%- ctx.subMenu[index].url %>">
+                        <span><%- ctx.subMenu[index].name %></span>
+                    </a>
+                </li>
+                <% } %>
+            </ul>
+        </div>
+    </div>
+</div>

+ 195 - 0
app/view/sign/info.ejs

@@ -0,0 +1,195 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+    <meta http-equiv="x-ua-compatible" content="ie=edge">
+    <title>电子签名-计量支付</title>
+    <style>
+        #app {
+            position: absolute;
+            top: 0;
+            left: 0;
+            right: 0;
+            bottom: 0;
+            font-family: 'Avenir', Helvetica, Arial, sans-serif;
+            -webkit-font-smoothing: antialiased;
+            -moz-osx-font-smoothing: grayscale;
+            text-align: center;
+            color: #2c3e50;
+        }
+        .container {
+            width: 100%;
+            height: 100%;
+        }
+        #canvasBox {
+            display: flex;
+            flex-direction: column;
+            height: 100%;
+            align-items: center; /*定义body的元素垂直居中*/
+            justify-content: center; /*定义body的里的元素水平居中*/
+        }
+        .greet {
+            padding: 20px;
+            font-size: 20px;
+            user-select: none;
+        }
+        input {
+            font-size: 20px;
+        }
+        .greet select {
+            font-size: 18px;
+        }
+        canvas {
+            flex: 1;
+            cursor: crosshair;
+            border:2px dashed lightgray;
+        }
+        .image-box {
+            width: 100%;
+            height: 100%;
+        }
+        .image-box header{
+            font-size: 18px;
+        }
+        .image-box img {
+            max-width: 80%;
+            max-height: 80%;
+            margin-top: 50px;
+            border: 1px solid gray;
+        }
+    </style>
+</head>
+<body>
+<div id="app">
+    <% if (error !== undefined && error) { %>
+    <div>参数有误, 无法访问本页.</div>
+    <% } else { %>
+    <div class="container">
+        <div id="canvasBox" :style="getHorizontalStyle" v-show="!showBox">
+            <div class="greet">
+                <span>{{msg}}</span>
+                <input type="button" value="清屏" @touchstart="clear" @mousedown="clear"/>
+                <input type="button" value="生成png图片" @touchstart="savePNG" @mousedown="savePNG"/>
+            </div>
+            <canvas></canvas>
+        </div>
+        <div class="image-box" v-show="showBox">
+            <header>
+                <input type="button" value="返回" @click="showBox = false"/>
+                <input type="button" value="上传" @click="upload" :disabled="showSuccess"/>
+                <span v-show="showSuccess">已成功上传!</span>
+            </header>
+            <img :src="signImage">
+        </div>
+    </div>
+    <% } %>
+</div>
+<% if (error === false) { %>
+<script type="text/javascript" src="/public/js/vue/vue.js"></script>
+<script type="text/javascript" src="/public/js/draw.js"></script>
+<script>
+    const id = '<%- id %>';
+    const name = '<%- name %>';
+    const role = '<%- role %>';
+    const csrf = '<%= ctx.csrf %>';
+    new Vue({
+        el: '#app',
+        data() {
+            return {
+                msg: '你好,' + name + (role !== '' ? '-' + role : '') + '请在下方空白处签名',
+                degree: 90, // 屏幕整体旋转的角度, 可取 -90,90,180等值
+                signImage: null,
+                showBox: false,
+                showSuccess: false
+            };
+        },
+        components: {
+            Draw,
+        },
+        beforeCreate() {
+            // document.title = '手写签名';
+        },
+        mounted() {
+            this.canvasBox = document.getElementById('canvasBox');
+            this.initCanvas();
+        },
+        computed: {
+            getHorizontalStyle() {
+                const d = document;
+                const w = window.innerWidth || d.documentElement.clientWidth || d.body.clientWidth;
+                const h = window.innerHeight || d.documentElement.clientHeight || d.body.clientHeight;
+                let length = (h - w) / 2;
+                let width = w;
+                let height = h;
+
+                switch (this.degree) {
+                    case -90:
+                        length = -length;
+                    case 90:
+                        width = h;
+                        height = w;
+                        break;
+                    default:
+                        length = 0;
+                }
+                if (this.canvasBox) {
+                    this.canvasBox.removeChild(document.querySelector('canvas'));
+                    this.canvasBox.appendChild(document.createElement('canvas'));
+                    setTimeout(() => {
+                        this.initCanvas();
+                    }, 200);
+                }
+                return {
+                    transform: `rotate(${this.degree}deg) translate(${length}px,${length}px)`,
+                    width: `${width}px`,
+                    height: `${height}px`,
+                    transformOrigin: 'center center',
+                };
+            },
+        },
+        methods: {
+            initCanvas() {
+                const canvas = document.querySelector('canvas');
+                this.draw = new Draw(canvas, -this.degree);
+            },
+            clear() {
+                this.draw.clear();
+            },
+            download() {
+                this.draw.downloadPNGImage(this.draw.getPNGImage());
+            },
+            savePNG() {
+                this.signImage = this.draw.getPNGImage();
+                this.showBox = true;
+                this.showSuccess = false;
+            },
+            upload() {
+                if (!this.showSuccess) {
+                    const image = this.draw.getPNGImage();
+                    const blob = this.draw.dataURLtoBlob(image);
+
+                    const url = '/sign/save';
+                    const successCallback = (response) => {
+                        // console.log(response);
+                        if (JSON.parse(response).err === 0) {
+                            this.showSuccess = true;
+                        } else {
+                            this.showSuccess = false;
+                        }
+                    };
+                    const failureCallback = (error) => {
+                        // console.log(error);
+                        this.showSuccess = false;
+                    };
+                    this.draw.upload(blob, url, successCallback, failureCallback);
+                }
+            },
+        },
+    });
+</script>
+<% } %>
+</body>
+</html>
+

+ 2 - 2
config/config.default.js

@@ -108,7 +108,7 @@ module.exports = appInfo => {
             '.pdf',
             '.ppt', '.pptx',
             '.png', '.jpg', '.jpeg', '.gif', '.bmp',
-            '.zip', '.rar', '.7z'],
+            '.zip', '.rar', '.7z', ''],
         fileSize: '30mb',
     };
 
@@ -121,7 +121,7 @@ module.exports = appInfo => {
     config.gzip = {
         threshold: 2048,
         // 下载的url要用正则忽略
-        ignore: /(\w*)\/download\/file(\w*)/ig,
+        ignore: /(\w*)(\/download\/file)|(\/profile\/qrCode)(\w*)/ig,
     };
 
     config.customLogger = {

+ 29 - 0
config/menu.js

@@ -269,10 +269,39 @@ const settingMenu = {
     },
 };
 
+const profileMenu = {
+    info: {
+        name: '账号资料',
+        display: false,
+        url: '/profile/info',
+    },
+    sms: {
+        name: '短信通知',
+        display: false,
+        url: '/profile/sms',
+    },
+    sign: {
+        name: '电子签名',
+        display: false,
+        url: '/profile/sign',
+    },
+    safe: {
+        name: '账号安全',
+        display: false,
+        url: '/profile/safe',
+    },
+    // help: {
+    //     name: '帮助中心',
+    //     display: false,
+    //     url: '/',
+    // },
+};
+
 module.exports = {
     menu,
     tenderMenu,
     stageMenu,
     sumMenu,
     settingMenu,
+    profileMenu,
 };

+ 5 - 0
package-lock.json

@@ -11452,6 +11452,11 @@
         }
       }
     },
+    "qr-image": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/qr-image/-/qr-image-3.2.0.tgz",
+      "integrity": "sha1-n6gpW+rlDEoUnPn5CaHbRkqGcug="
+    },
     "qs": {
       "version": "6.5.1",
       "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz",

+ 1 - 0
package.json

@@ -27,6 +27,7 @@
     "node-uuid": "^1.4.8",
     "node-xlsx": "^0.12.0",
     "number-precision": "^1.3.1",
+    "qr-image": "^3.2.0",
     "stream-to-array": "^2.3.0",
     "stream-wormhole": "^1.1.0",
     "ueditor": "^1.2.3",