Browse Source

添加帐号登录注册相关页面

reghao 2 years ago
parent
commit
ea3612f102

File diff suppressed because it is too large
+ 869 - 42
package-lock.json


+ 15 - 2
package.json

@@ -34,7 +34,12 @@
     "@liripeng/vue-audio-player": "^1.5.0",
     "mavon-editor": "^2.10.4",
     "v-viewer": "^1.6.4",
-    "vue-quill-editor": "^3.0.6"
+    "vue-quill-editor": "^3.0.6",
+    "babel-plugin-prismjs": "^2.0.1",
+    "js-cookie": "2.2.0",
+    "nprogress": "^0.2.0",
+    "prismjs": "^1.25.0",
+    "svg-sprite-loader": "^5.0.0"
   },
   "devDependencies": {
     "@vue/cli-plugin-babel": "^4.5.13",
@@ -48,6 +53,14 @@
     "eslint-plugin-promise": "^4.2.1",
     "eslint-plugin-standard": "^4.0.0",
     "eslint-plugin-vue": "^6.2.2",
-    "vue-template-compiler": "^2.6.11"
+    "vue-template-compiler": "^2.6.11",
+    "babel-plugin-import": "^1.13.1",
+    "compression-webpack-plugin": "^6.1.1",
+    "sass": "1.26.8",
+    "sass-loader": "8.0.2",
+    "style-resources-loader": "^1.4.1",
+    "vue-cli-plugin-style-resources-loader": "^0.1.4",
+    "vue-svg-component-runtime": "^1.0.1",
+    "vue-svg-icon-loader": "^2.1.1"
   }
 }

+ 56 - 0
src/api/auth.js

@@ -0,0 +1,56 @@
+// 授权相关接口
+import { get, post } from '@/utils/request'
+
+// 登录服务接口
+export const ServePubkey = () => {
+  return get('/api/account/code/pubkey')
+}
+
+// 登录服务接口
+export const ServeLogin = data => {
+  return post('/api/account/auth/signin', data)
+}
+
+// 登录注销
+export const ServeLogout = () => {
+  return post('/api/account/auth/signout')
+}
+
+// 注册服务接口
+export const ServeRegister = data => {
+  return post('/api/account/registry/create', data)
+}
+
+// 发送找回密码验证码
+export const ServeSendVerifyCode = data => {
+  return post('/api/account/code/verify', data)
+}
+
+// 发送邮箱验证码服务接口
+export const ServeRegistryCheck = data => {
+  return post('/api/account/registry/check', data)
+}
+
+export const ServeCaptcha = () => {
+  return get('/api/account/code/captcha')
+}
+
+// 刷新登录Token服务接口
+export const ServeRefreshToken = (data) => {
+  return post('/api/account/token/refresh', data)
+}
+
+// 重置密码服务
+export const ServeForgetPassword = data => {
+  return post('/api/account/password/reset', data)
+}
+
+// 获取登录记录
+export const ServeLoginRecord = () => {
+  return get('/api/account/login/record')
+}
+
+// 注销某次登录
+export const ServeLoginDeactive = (loginId) => {
+  return post('/api/account/login/deactive/' + loginId)
+}

+ 148 - 0
src/assets/css/global.scss

@@ -0,0 +1,148 @@
+@import './reset.css';
+
+.no-select {
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+}
+.no-padding {
+  padding: 0;
+}
+.no-border {
+  border: 0;
+}
+.pointer {
+  cursor: pointer;
+}
+.border-radius0 {
+  border-radius: 0;
+}
+.full-height {
+  height: 100%;
+}
+.ov-hidden {
+  overflow: hidden;
+}
+.lum-text-ellipsis {
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+.lum-scrollbar {
+  &::-webkit-scrollbar {
+    width: 3px;
+    background-color: #e4e4e5;
+  }
+  &::-webkit-scrollbar-thumb {
+    border-radius: 3px;
+    background-color: #c0bebc;
+  }
+}
+.larkc-tag {
+  font-size: 12px;
+  font-weight: 400;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  padding: 0 6px;
+  height: 20px;
+  border-radius: 2px;
+  cursor: default;
+  user-select: none;
+  background-color: #dee0e3;
+  transform: scale(0.83);
+  transform-origin: left;
+  flex-shrink: 0;
+}
+.flex-center {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.lum-dialog-mask {
+  position: fixed;
+  top: 0;
+  left: 0;
+  z-index: 999;
+  width: 100%;
+  height: 100%;
+  background-color: white;
+  overflow: hidden;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  .lum-dialog-box {
+    min-width: 200px;
+    min-height: 200px;
+    background-color: white;
+    border-radius: 10px;
+    overflow: hidden;
+    box-shadow: 0 2px 8px 0 rgba(31,35,41,0.2);
+    margin: 0 10px;
+    .container {
+      height: 100%;
+    }
+    .header {
+      padding: 0;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      border-bottom: 1px solid #f5eeee;
+      & > p {
+        &:first-child {
+          text-indent: 20px;
+        }
+      }
+      .tools {
+        height: 100%;
+        width: 100px;
+        display: flex;
+        align-items: center;
+        justify-content: flex-end;
+        padding-right: 20px;
+        i {
+          font-size: 20px;
+          cursor: pointer;
+          margin-left: 8px;
+        }
+      }
+    }
+    .main {
+      #deep.el-input__inner {
+        border-radius: 1px !important;
+      }
+      .submit-btn {
+        border-radius: 2px;
+        font-weight: 400;
+      }
+    }
+  }
+}
+.talk-notify {
+  .el-notification__title {
+    font-weight: 300;
+    font-size: 16px;
+    color: #f44336;
+  }
+  p {
+    max-height: 65px;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    display: -webkit-box;
+    -webkit-line-clamp: 3;
+    -webkit-box-orient: vertical;
+    text-indent: -7px;
+    word-break: break-all;
+  }
+}
+.im-notify {
+  padding: 14px 26px 8px 0px;
+  .el-notification__closeBtn {
+    position: absolute;
+    top: 12px;
+  }
+  .el-notification__group {
+    margin-left: 5px;
+  }
+}

+ 155 - 0
src/assets/css/login-auth.scss

@@ -0,0 +1,155 @@
+#deep.el-input__inner {
+  border-radius: 1px !important;
+}
+
+#auth-container {
+  position: fixed;
+  top: 0;
+  left: 0;
+  height: 100%;
+  width: 100%;
+  background-color: #f6f8fb;
+  #logo-name {
+    width: 200px;
+    height: 38px;
+    font-size: 34px;
+    font-family: Times New Roman, Georgia, Serif;
+    color: #2196f3;
+    margin-left: 20px;
+    margin-top: 20px;
+  }
+  #login-box {
+    position: absolute;
+    width: 350px;
+    min-height: 480px;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%,-50%);
+    background-color: white;
+    border-radius: 5px;
+    box-shadow: 0 0 0 #ccc;
+    box-shadow: 0 4px 14px 0 rgba(206,207,209,0.5);
+    padding: 10px 20px;
+    .header {
+      width: 100%;
+      height: 38px;
+      font-size: 22px;
+      margin: 25px 0 20px 0;
+    }
+    .main {
+      width: 100%;
+      .links {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        a {
+          font-weight: normal !important;
+        }
+      }
+      .send-code-btn {
+        width: 140px;
+        height: 40px;
+        line-height: 40px;
+        display: inline-block;
+        background: #f3ecec;
+        text-align: center;
+        color: #777373;
+        cursor: pointer;
+        user-select: none;
+        margin-left: 5px;
+        &:active {
+          background: #e4dbdb;
+        }
+      }
+      .send-sms-disable {
+        cursor: not-allowed !important;
+        background: #f7f7f7 !important;
+        color: silver !important;
+      }
+      .submit-btn {
+        width: 100%;
+        border-radius: 2px;
+      }
+    }
+  }
+}
+.preview-account {
+  text-align: center;
+  p {
+    height: 25px;
+    line-height: 25px;
+    color: #2d2c2c;
+    font-weight: 100;
+    font-size: 12px;
+  }
+}
+.copyright {
+  position: absolute;
+  bottom: 30px;
+  left: 0;
+  right: 0;
+  width: 70%;
+  text-align: center;
+  margin: 0 auto;
+  font-size: 12px;
+  color: #b1a0a0;
+  a {
+    color: #777272;
+    font-weight: 400;
+  }
+}
+@media screen and (max-height : 500px) {
+  .copyright {
+    display: none;
+  }
+}
+
+.fly-box {
+  .bg-fly-circle1 {
+    left: 40px;
+    top: 100px;
+    width: 100px;
+    height: 100px;
+    border-radius: 50%;
+    background: linear-gradient(to right,rgba(100,84,239,0.07) 0%,rgba(48,33,236,0.04) 100%);
+    animation: move 2.5s linear infinite;
+  }
+  .bg-fly-circle2 {
+    left: 3%;
+    top: 60%;
+    width: 150px;
+    height: 150px;
+    border-radius: 50%;
+    background: linear-gradient(to right,rgba(100,84,239,0.08) 0%,rgba(48,33,236,0.04) 100%);
+    animation: move 3s linear infinite;
+  }
+  .bg-fly-circle3 {
+    right: 2%;
+    top: 140px;
+    width: 145px;
+    height: 145px;
+    border-radius: 50%;
+    background: linear-gradient(to right,rgba(100,84,239,0.1) 0%,rgba(48,33,236,0.04) 100%);
+    animation: move 2.5s linear infinite;
+  }
+  .bg-fly-circle4 {
+    right: 5%;
+    top: 60%;
+    width: 160px;
+    height: 160px;
+    border-radius: 50%;
+    background: linear-gradient(to right,rgba(100,84,239,0.02) 0%,rgba(48,33,236,0.04) 100%);
+    animation: move 3.5s linear infinite;
+  }
+}
+@keyframes move {
+  0% {
+    transform: translateY(0px);
+  }
+  50% {
+    transform: translateY(25px);
+  }
+  100% {
+    transform: translateY(0px);
+  }
+}

+ 62 - 0
src/assets/css/reset.css

@@ -0,0 +1,62 @@
+* {
+    margin: 0;
+    padding: 0;
+}
+
+body,
+html {
+    height: 100%;
+    min-width: 500px;
+    font-family: "Microsoft YaHei";
+    font-size: 16px;
+    color: #333;
+}
+
+button,
+input,
+select,
+textarea {
+    font-size: 100%;
+    margin: 0;
+    padding: 0;
+    border: none;
+    outline: none;
+}
+
+img {
+    border: 0;
+}
+
+a,
+img {
+    -webkit-touch-callout: none
+}
+
+a {
+    text-decoration: none;
+}
+
+textarea {
+    resize: none;
+    outline: 0;
+    white-space: pre-wrap;
+    word-wrap: break-word;
+    border: none;
+    background: #fff;
+    font-family: "Microsoft YaHei";
+}
+
+:focus {
+    outline: none;
+}
+
+.clearfix {
+    clear: both;
+    content: "";
+    display: block;
+    overflow: hidden
+}
+
+.clear {
+    clear: both;
+}

+ 15 - 0
src/assets/css/variable.scss

@@ -0,0 +1,15 @@
+:root {
+  --themeBagColor: red;
+}
+.theme-default {
+  --themeBagColor: #fff;
+}
+.theme-dark {
+  --themeBagColor: black;
+}
+.theme-red {
+  --themeBagColor: red;
+}
+.theme-blue {
+  --themeBagColor: blue;
+}

File diff suppressed because it is too large
+ 5 - 0
src/assets/icon/iconfont.css


BIN
src/assets/icon/iconfont.eot


File diff suppressed because it is too large
+ 29 - 0
src/assets/icon/iconfont.svg


BIN
src/assets/icon/iconfont.ttf


BIN
src/assets/icon/iconfont.woff


BIN
src/assets/img/logo.png


+ 30 - 4
src/router/index.js

@@ -2,7 +2,6 @@ import VueRouter from 'vue-router'
 import Vue from 'vue'
 
 // 懒加载引入页面组件,es6语法
-const Login = () => import('views/Login')
 const Home = () => import('views/home/Index')
 const TimelineIndex = () => import('views/home/Timeline')
 const StatusPage = () => import('views/home/Status')
@@ -28,9 +27,36 @@ const MessageIndex = () => import('views/message/Message')
 Vue.use(VueRouter)
 const routes = [
   {
-    path: '/login',
-    name: 'login',
-    component: Login
+    path: '/sso',
+    name: 'sso',
+    redirect: '/sso/login',
+    component: () => import('@/views/sso/layout'),
+    children: [
+      {
+        path: '/sso/login',
+        meta: {
+          title: '账号登录?',
+          needLogin: false,
+        },
+        component: () => import('@/views/sso/login'),
+      },
+      {
+        path: '/sso/register',
+        meta: {
+          title: '账号注册?',
+          needLogin: false,
+        },
+        component: () => import('@/views/sso/register'),
+      },
+      {
+        path: '/sso/forget',
+        meta: {
+          title: '找回密码?',
+          needLogin: false,
+        },
+        component: () => import('@/views/sso/forget'),
+      },
+    ],
   },
   {
     path: '/',

+ 144 - 0
src/utils/date.js

@@ -0,0 +1,144 @@
+/**
+ * 对Date的扩展,将 Date 转化为指定格式的String。
+ *
+ *  月(M)、日(d)、小时(h)、分(m)、秒(s)、季度(q) 可以用 1-2 个占位符,
+ *  年(y)可以用 1-4 个占位符,毫秒(S)只能用 1 个占位符(是 1-3 位的数字)。
+ *
+ *  【示例】:
+ *  formatDate(new Date(), 'yyyy-MM-dd hh:mm:ss.S') ==> 2006-07-02 08:09:04.423
+ *  formatDate(new Date(), 'yyyy-M-d h:m:s.S')      ==> 2006-7-2 8:9:4.18
+ *  formatDate(new Date(), 'hh:mm:ss.S')            ==> 08:09:04.423
+ */
+export function formatDate(date, fmt) {
+  const o = {
+    'M+': date.getMonth() + 1, //月份
+    'd+': date.getDate(), //日
+    'h+': date.getHours(), //小时
+    'm+': date.getMinutes(), //分
+    's+': date.getSeconds(), //秒
+    'q+': Math.floor((date.getMonth() + 3) / 3), //季度
+    S: date.getMilliseconds(), //毫秒
+  }
+
+  if (/(y+)/.test(fmt)) {
+    fmt = fmt.replace(
+      RegExp.$1,
+      (date.getFullYear() + '').substr(4 - RegExp.$1.length)
+    )
+  }
+
+  for (let k in o) {
+    if (new RegExp('(' + k + ')').test(fmt)) {
+      let value =
+        RegExp.$1.length == 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length)
+      fmt = fmt.replace(RegExp.$1, value)
+    }
+  }
+
+  return fmt
+}
+
+/**
+ * 仿照微信中的消息时间显示逻辑,将时间戳(单位:毫秒)转换为友好的显示格式.
+ *
+ * 1)7天之内的日期显示逻辑是:今天、昨天(-1d)、前天(-2d)、星期?(只显示总计7天之内的星期数,即<=-4d);
+ * 2)7天之外(即>7天)的逻辑:直接显示完整日期时间。
+ *
+ * @param {[long]} timestamp 时间戳(单位:毫秒),形如:1550789954260
+ * @param {boolean} mustIncludeTime true表示输出的格式里一定会包含“时间:分钟”
+ * ,否则不包含(参考微信,不包含时分的情况,用于首页“消息”中显示时)
+ *
+ * @return {string} 输出格式形如:“刚刚”、“10:30”、“昨天 12:04”、“前天 20:51”、“星期二”、“2019/2/21 12:09”等形式
+ */
+export function formatDateShort(timestamp, mustIncludeTime) {
+  // 当前时间
+  let currentDate = new Date()
+  // 目标判断时间
+  let srcDate = new Date(parseInt(timestamp))
+
+  let currentYear = currentDate.getFullYear()
+  let currentMonth = currentDate.getMonth() + 1
+  let currentDateD = currentDate.getDate()
+
+  let srcYear = srcDate.getFullYear()
+  let srcMonth = srcDate.getMonth() + 1
+  let srcDateD = srcDate.getDate()
+
+  let ret = ''
+
+  // 要额外显示的时间分钟
+  let timeExtraStr = mustIncludeTime ? ' ' + formatDate(srcDate, 'hh:mm') : ''
+
+  // 当年
+  if (currentYear == srcYear) {
+    let currentTimestamp = currentDate.getTime()
+    let srcTimestamp = timestamp
+    // 相差时间(单位:毫秒)
+    let deltaTime = currentTimestamp - srcTimestamp
+
+    // 当天(月份和日期一致才是)
+    if (currentMonth == srcMonth && currentDateD == srcDateD) {
+      // 时间相差60秒以内
+      if (deltaTime < 60 * 1000) ret = '刚刚'
+      // 否则当天其它时间段的,直接显示“时:分”的形式
+      else ret = formatDate(srcDate, 'hh:mm')
+    }
+    // 当年 && 当天之外的时间(即昨天及以前的时间)
+    else {
+      // 昨天(以“现在”的时候为基准-1天)
+      let yesterdayDate = new Date()
+      yesterdayDate.setDate(yesterdayDate.getDate() - 1)
+
+      // 前天(以“现在”的时候为基准-2天)
+      let beforeYesterdayDate = new Date()
+      beforeYesterdayDate.setDate(beforeYesterdayDate.getDate() - 2)
+
+      // 用目标日期的“月”和“天”跟上方计算出来的“昨天”进行比较,是最为准确的(如果用时间戳差值
+      // 的形式,是不准确的,比如:现在时刻是2019年02月22日1:00、而srcDate是2019年02月21日23:00,
+      // 这两者间只相差2小时,直接用“deltaTime/(3600 * 1000)” > 24小时来判断是否昨天,就完全是扯蛋的逻辑了)
+      if (
+        srcMonth == yesterdayDate.getMonth() + 1 &&
+        srcDateD == yesterdayDate.getDate()
+      )
+        ret = '昨天' + timeExtraStr
+      // -1d
+      // “前天”判断逻辑同上
+      else if (
+        srcMonth == beforeYesterdayDate.getMonth() + 1 &&
+        srcDateD == beforeYesterdayDate.getDate()
+      )
+        ret = '前天' + timeExtraStr
+      // -2d
+      else {
+        // 跟当前时间相差的小时数
+        let deltaHour = deltaTime / (3600 * 1000)
+
+        // 如果小于或等 7*24小时就显示星期几
+        if (deltaHour <= 7 * 24) {
+          let weekday = new Array(7)
+          weekday[0] = '星期日'
+          weekday[1] = '星期一'
+          weekday[2] = '星期二'
+          weekday[3] = '星期三'
+          weekday[4] = '星期四'
+          weekday[5] = '星期五'
+          weekday[6] = '星期六'
+
+          // 取出当前是星期几
+          let weedayDesc = weekday[srcDate.getDay()]
+          ret = weedayDesc + timeExtraStr
+        }
+        // 否则直接显示完整日期时间
+        else {
+          ret = formatDate(srcDate, 'yyyy/M/d') + timeExtraStr
+        }
+      }
+    }
+  }
+  // 往年
+  else {
+    ret = formatDate(srcDate, 'yyyy/M/d') + timeExtraStr
+  }
+
+  return ret
+}

+ 564 - 0
src/utils/functions.js

@@ -0,0 +1,564 @@
+/** 公共方法类 */
+import CryptoJs from 'crypto-js'
+import encHex from 'crypto-js/enc-hex'
+
+/**
+ * 人性化时间显示
+ *
+ * @param {Object} datetime
+ */
+export function formateTime(datetime) {
+  if (datetime == null) return ''
+
+  datetime = datetime.replace(/-/g, '/')
+
+  let time = new Date()
+  let outTime = new Date(datetime)
+  if (/^[1-9]\d*$/.test(datetime)) {
+    outTime = new Date(parseInt(datetime) * 1000)
+  }
+
+  if (
+    time.getTime() < outTime.getTime() ||
+    time.getFullYear() != outTime.getFullYear()
+  ) {
+    return parseTime(outTime, '{y}-{m}-{d} {h}:{i}')
+  }
+
+  if (time.getMonth() != outTime.getMonth()) {
+    return parseTime(outTime, '{m}-{d} {h}:{i}')
+  }
+
+  if (time.getDate() != outTime.getDate()) {
+    let day = outTime.getDate() - time.getDate()
+    if (day == -1) {
+      return parseTime(outTime, '昨天 {h}:{i}')
+    }
+
+    if (day == -2) {
+      return parseTime(outTime, '前天 {h}:{i}')
+    }
+
+    return parseTime(outTime, '{m}-{d} {h}:{i}')
+  }
+
+  let diff = time.getTime() - outTime.getTime()
+
+  if (time.getHours() != outTime.getHours() || diff > 30 * 60 * 1000) {
+    return parseTime(outTime, '{h}:{i}')
+  }
+
+  let minutes = outTime.getMinutes() - time.getMinutes()
+  if (minutes == 0) {
+    return '刚刚'
+  }
+
+  minutes = Math.abs(minutes)
+  return `${minutes}分钟前`
+}
+
+/**
+ * 格式化文件大小
+ *
+ * @param {String} value 文件大小(字节)
+ */
+export function formateSize(value) {
+  if (null == value || value == '') {
+    return '0'
+  }
+  let unitArr = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
+  let index = 0
+  let srcsize = parseFloat(value)
+  index = Math.floor(Math.log(srcsize) / Math.log(1000))
+  let size = srcsize / Math.pow(1000, index)
+  size = size.toFixed(2) //保留的小数位数
+  return size + unitArr[index]
+}
+/**
+ * 获取文件后缀名
+ *
+ * @param {String} fileName
+ */
+export function getFileExt(fileName) {
+  let ext = fileName.split('.')
+  ext = ext[ext.length - 1] // 获取文件后缀名
+  return ext
+}
+
+/**
+ * 根据图片url下载图片
+ * @param {String} imgsrc
+ * @param {String} name
+ */
+export function downloadIamge(imgsrc, name) {
+  //下载图片地址和图片名
+  let image = new Image()
+  // 解决跨域 Canvas 污染问题
+  image.setAttribute('crossOrigin', 'anonymous')
+  image.onload = function() {
+    let canvas = document.createElement('canvas')
+    canvas.width = image.width
+    canvas.height = image.height
+    let context = canvas.getContext('2d')
+    context.drawImage(image, 0, 0, image.width, image.height)
+    let url = canvas.toDataURL('image/png') //得到图片的base64编码数据
+    let a = document.createElement('a') // 生成一个a元素
+    let event = new MouseEvent('click') // 创建一个单击事件
+    a.download = name || 'photo' // 设置图片名称
+    a.href = url // 将生成的URL设置为a.href属性
+    a.dispatchEvent(event) // 触发a的单击事件
+  }
+  image.src = imgsrc
+}
+
+/**
+ * 通过图片url获取图片大小
+ *
+ * @param {String} imgsrc 例如图片名: D8x5f13a53dbc4b9_350x345.png
+ */
+export function getImageInfo(imgsrc) {
+  let data = {
+    width: 0,
+    height: 0,
+  }
+
+  let arr = imgsrc.split('_')
+  if (arr.length == 1) return data
+
+  let info = arr[arr.length - 1].match(/\d+x\d+/g)
+  if (info == null) return data
+
+  info = info[0].split('x')
+  return {
+    width: parseInt(info[0]),
+    height: parseInt(info[1]),
+  }
+}
+
+/**
+ * 时间格式化方法
+ *
+ * @param {(Object|string|number)} time
+ * @param {String} cFormat
+ * @returns {String | null}
+ */
+export function parseTime(time, cFormat) {
+  if (arguments.length === 0) {
+    return null
+  }
+
+  let date
+  const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
+
+  if (typeof time === 'object') {
+    date = time
+  } else {
+    if (typeof time === 'string' && /^[0-9]+$/.test(time)) {
+      time = parseInt(time)
+    }
+    if (typeof time === 'number' && time.toString().length === 10) {
+      time = time * 1000
+    }
+
+    date = new Date(time.replace(/-/g, '/'))
+  }
+
+  const formatObj = {
+    y: date.getFullYear(),
+    m: date.getMonth() + 1,
+    d: date.getDate(),
+    h: date.getHours(),
+    i: date.getMinutes(),
+    s: date.getSeconds(),
+    a: date.getDay(),
+  }
+
+  const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => {
+    const value = formatObj[key]
+    // Note: getDay() returns 0 on Sunday
+    if (key === 'a') {
+      return ['日', '一', '二', '三', '四', '五', '六'][value]
+    }
+
+    return value.toString().padStart(2, '0')
+  })
+
+  return time_str
+}
+
+/**
+ * 去除字符串控制
+ *
+ * @param {String} str
+ */
+export function trim(str, type = null) {
+  if (type) {
+    return str.replace(/(^\s*)|(\s*$)/g, '')
+  } else if (type == 'l') {
+    return str.replace(/(^\s*)/g, '')
+  } else {
+    return str.replace(/(\s*$)/g, '')
+  }
+}
+
+/**
+ * 解析url中参数
+ *
+ * @param {String} url
+ * @returns {Object}
+ */
+export function param2Obj(url) {
+  const search = url.split('?')[1]
+
+  if (!search) return {}
+
+  return JSON.parse(
+    '{"' +
+      decodeURIComponent(search)
+        .replace(/"/g, '\\"')
+        .replace(/&/g, '","')
+        .replace(/=/g, '":"')
+        .replace(/\+/g, ' ') +
+      '"}'
+  )
+}
+
+/**
+ * @param {Object} json
+ * @returns {Array}
+ */
+export function param(json) {
+  if (!json) return ''
+  return cleanArray(
+    Object.keys(json).map(key => {
+      if (json[key] === undefined) return ''
+
+      return encodeURIComponent(key) + '=' + encodeURIComponent(json[key])
+    })
+  ).join('&')
+}
+
+/**
+ * @param {Array} actual
+ * @returns {Array}
+ */
+export function cleanArray(actual) {
+  const newArray = []
+  for (let i = 0; i < actual.length; i++) {
+    if (actual[i]) {
+      newArray.push(actual[i])
+    }
+  }
+
+  return newArray
+}
+
+/**
+ * @param {HTMLElement} element
+ * @param {String} className
+ */
+export function toggleClass(element, className) {
+  if (!element || !className) {
+    return
+  }
+
+  let classString = element.className
+  let nameIndex = classString.indexOf(className)
+  if (nameIndex === -1) {
+    classString += '' + className
+  } else {
+    classString =
+      classString.substr(0, nameIndex) +
+      classString.substr(nameIndex + className.length)
+  }
+  element.className = classString
+}
+
+/**
+ * Check if an element has a class
+ *
+ * @param {HTMLElement} elm
+ * @param {String} cls
+ * @returns {Boolean}
+ */
+export function hasClass(ele, cls) {
+  return !!ele.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)'))
+}
+
+/**
+ * Add class to element
+ *
+ * @param {HTMLElement} elm
+ * @param {String} cls
+ */
+export function addClass(ele, cls) {
+  if (!hasClass(ele, cls)) ele.className += ' ' + cls
+}
+
+/**
+ * Remove class from element
+ *
+ * @param {HTMLElement} elm
+ * @param {String} cls
+ */
+export function removeClass(ele, cls) {
+  if (hasClass(ele, cls)) {
+    const reg = new RegExp('(\\s|^)' + cls + '(\\s|$)')
+    ele.className = ele.className.replace(reg, ' ')
+  }
+}
+
+/**
+ * 通过图片Url获取图片等比例缩放的宽度和高度信息
+ *
+ * @param {String} src
+ * @param {Number} width
+ */
+export function imgZoom(src, width = 200) {
+  const info = getImageInfo(src)
+
+  if (info.width < width) {
+    return {
+      width: `${info.width}px`,
+      height: `${info.height}px`,
+    }
+  }
+
+  return {
+    width: width + 'px',
+    height: parseInt(info.height / (info.width / width)) + 'px',
+  }
+}
+
+/**
+ * 获取浏览器光标选中内容
+ *
+ * @export
+ * @returns
+ */
+export function getSelection() {
+  return window.getSelection
+    ? window.getSelection().toString()
+    : document.selection.createRange().text
+}
+
+/**
+ * 剪贴板复制功能
+ *
+ * @param {String} value 复制内容
+ * @param {Function} callback 复制成功回调方法
+ */
+export const copyTextToClipboard = (value, callback) => {
+  let textArea = document.createElement('textarea')
+  textArea.style.background = 'transparent'
+  textArea.value = value
+
+  document.body.appendChild(textArea)
+
+  textArea.select()
+
+  try {
+    document.execCommand('copy')
+    if (callback) callback()
+  } catch (err) {
+    alert('Oops, unable to copy')
+  }
+
+  document.body.removeChild(textArea)
+}
+
+/**
+ * 隐藏用户手机号中间四位
+ *
+ * @param {String} phone  手机号
+ */
+export function hidePhone(phone) {
+  return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
+}
+
+/**
+ * 人性化显示时间
+ *
+ * @param {Object} datetime
+ */
+export function beautifyTime(datetime = '') {
+  if (datetime == null) {
+    return ''
+  }
+
+  datetime = datetime.replace(/-/g, '/')
+
+  let time = new Date()
+  let outTime = new Date(datetime)
+  if (/^[1-9]\d*$/.test(datetime)) {
+    outTime = new Date(parseInt(datetime) * 1000)
+  }
+
+  if (time.getTime() < outTime.getTime()) {
+    return parseTime(outTime, '{y}/{m}/{d}')
+  }
+
+  if (time.getFullYear() != outTime.getFullYear()) {
+    return parseTime(outTime, '{y}/{m}/{d}')
+  }
+
+  if (time.getMonth() != outTime.getMonth()) {
+    return parseTime(outTime, '{m}/{d}')
+  }
+
+  if (time.getDate() != outTime.getDate()) {
+    let day = outTime.getDate() - time.getDate()
+    if (day == -1) {
+      return parseTime(outTime, '昨天 {h}:{i}')
+    }
+
+    if (day == -2) {
+      return parseTime(outTime, '前天 {h}:{i}')
+    }
+
+    return parseTime(outTime, '{m}-{d}')
+  }
+
+  if (time.getHours() != outTime.getHours()) {
+    return parseTime(outTime, '{h}:{i}')
+  }
+
+  let minutes = outTime.getMinutes() - time.getMinutes()
+  if (minutes == 0) {
+    return '刚刚'
+  }
+
+  minutes = Math.abs(minutes)
+  return `${minutes}分钟前`
+}
+
+export function getSort(fn) {
+  return function(a, b) {
+    let ret = 0
+
+    if (fn.call(this, a, b)) {
+      ret = -1
+    } else if (fn.call(this, b, a)) {
+      ret = 1
+    }
+
+    return ret
+  }
+}
+
+/**
+ * 批量排序
+ *
+ * @param {*} arr
+ */
+export function getMutipSort(arr) {
+  return function(a, b) {
+    let tmp
+    let i = 0
+
+    do {
+      tmp = arr[i++](a, b)
+    } while (tmp == 0 && i < arr.length)
+
+    return tmp
+  }
+}
+
+/**
+ * Url 替换超链接
+ *
+ * @param {String} text 文本
+ * @param {String} color 超链接颜色
+ */
+export function textReplaceLink(text, color = '#409eff') {
+  let exp = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gi
+  return text.replace(
+    exp,
+    `<a href='$1' target="_blank" style="color:${color};text-decoration: revert;">$1</a >`
+  )
+}
+
+/**
+ * 防抖
+ *
+ * @param {*} func
+ * @param {*} wait
+ * @param {*} immediate
+ * @returns
+ */
+export function debounce(func, wait, immediate) {
+  let timeout
+
+  return function() {
+    let context = this
+    let args = arguments
+
+    if (timeout) clearTimeout(timeout) // timeout 不为null
+    if (immediate) {
+      let callNow = !timeout // 第一次会立即执行,以后只有事件执行后才会再次触发
+      timeout = setTimeout(function() {
+        timeout = null
+      }, wait)
+      if (callNow) func.apply(context, args)
+    } else {
+      timeout = setTimeout(function() {
+        func.apply(context, args)
+      }, wait)
+    }
+  }
+}
+
+export function sha256sum(file) {
+  const sha256 = CryptoJs.algo.SHA256.create()
+  return new Promise((resolve, reject) => {
+    const reader = new FileReader()
+    reader.onload = ({ target }) => {
+      const wordArray = CryptoJs.lib.WordArray.create(target.result)
+      sha256.update(wordArray)
+      resolve()
+    }
+    reader.readAsArrayBuffer(file)
+  })
+}
+
+export function hashFile (file) {
+  /**
+   * 使用指定的算法计算hash值
+   */
+  function hashFileInternal(file, alog) {
+    // 指定块的大小,这里设置为20MB,可以根据实际情况进行配置
+    const chunkSize = 20 * 1024 * 1024
+    let promise = Promise.resolve()
+    // 使用promise来串联hash计算的顺序。因为FileReader是在事件中处理文件内容的,必须要通过某种机制来保证update的顺序是文件正确的顺序
+    for (let index = 0; index < file.size; index += chunkSize) {
+      promise = promise.then(() => hashBlob(file.slice(index, index + chunkSize)))
+    }
+
+    /**
+     * 更新文件块的hash值
+     */
+    function hashBlob (blob) {
+      return new Promise((resolve, reject) => {
+        const reader = new FileReader()
+        reader.onload = ({ target }) => {
+          const wordArray = CryptoJs.lib.WordArray.create(target.result)
+          // 增量更新计算结果
+          alog.update(wordArray)
+          resolve()
+        }
+        reader.readAsArrayBuffer(blob)
+      })
+    }
+
+    // 使用promise返回最终的计算结果
+    return promise.then(() => encHex.stringify(alog.finalize()))
+  }
+
+  // 同时计算文件的sha256和md5,并使用promise返回
+  return Promise.all([hashFileInternal(file, CryptoJs.algo.SHA256.create()),
+    hashFileInternal(file, CryptoJs.algo.MD5.create())])
+      .then(([sha256sum, md5sum]) => ({
+        sha256sum,
+        md5sum
+      }))
+}

+ 12 - 0
src/utils/icons.js

@@ -0,0 +1,12 @@
+/**
+ * Custom icon list
+ * All icons are loaded here for easy management
+ *
+ * 自定义图标加载表
+ * 所有图标均从这里加载,方便管理
+ */
+import SvgNotFount from '@/icons/svg/not-fount.svg?inline' // path to your '*.svg?inline' file.
+
+export {
+  SvgNotFount,
+}

+ 94 - 0
src/utils/lazy-use.js

@@ -0,0 +1,94 @@
+import Vue from 'vue'
+import 'element-ui/lib/theme-chalk/index.css'
+
+import {
+  Notification,
+  Popover,
+  Switch,
+  Dropdown,
+  DropdownMenu,
+  DropdownItem,
+  Message,
+  Container,
+  Header,
+  Aside,
+  Main,
+  Footer,
+  Menu,
+  Submenu,
+  MenuItem,
+  MenuItemGroup,
+  Button,
+  Image,
+  Loading,
+  Row,
+  Col,
+  MessageBox,
+  Form,
+  FormItem,
+  Input,
+  Divider,
+  Link,
+  Tooltip,
+  Autocomplete,
+  Scrollbar,
+  Avatar,
+  Radio,
+  RadioGroup,
+  Progress,
+  Dialog,
+  Checkbox,
+  Table,
+  TableColumn,
+  Breadcrumb,
+  BreadcrumbItem,
+  Pagination,
+} from 'element-ui'
+
+Vue.use(Popover)
+Vue.use(Switch)
+Vue.use(Dropdown)
+Vue.use(DropdownMenu)
+Vue.use(DropdownItem)
+Vue.use(Container)
+Vue.use(Header)
+Vue.use(Aside)
+Vue.use(Main)
+Vue.use(Footer)
+Vue.use(Menu)
+Vue.use(Submenu)
+Vue.use(MenuItem)
+Vue.use(MenuItemGroup)
+Vue.use(Button)
+Vue.use(Image)
+Vue.use(Row)
+Vue.use(Col)
+Vue.use(Input)
+Vue.use(Form)
+Vue.use(FormItem)
+Vue.use(Divider)
+Vue.use(Link)
+Vue.use(Tooltip)
+Vue.use(Autocomplete)
+Vue.use(Scrollbar)
+Vue.use(Avatar)
+Vue.use(Radio)
+Vue.use(Checkbox)
+Vue.use(RadioGroup)
+Vue.use(Progress)
+Vue.use(Dialog)
+Vue.use(Loading.directive)
+Vue.use(Table)
+Vue.use(TableColumn)
+Vue.use(Breadcrumb)
+Vue.use(BreadcrumbItem)
+Vue.use(Pagination)
+
+Vue.prototype.$notify = Notification
+Vue.prototype.$message = Message
+Vue.prototype.$confirm = MessageBox.confirm
+Vue.prototype.$prompt = MessageBox.prompt
+Vue.prototype.$alert = MessageBox.alert
+Vue.prototype.$dialog = MessageBox.Dialog
+
+process.env.NODE_ENV !== 'production' && console.warn('[Lumen-IM] NOTICE: element-ui use lazy-load.')

+ 76 - 0
src/utils/sms-lock.js

@@ -0,0 +1,76 @@
+/**
+ * 短信倒计时锁
+ */
+class SmsLock {
+  // 发送倒计时默认60秒
+  time = null
+
+  // 计时器
+  timer = null
+
+  // 倒计时默认60秒
+  lockTime = 60
+
+  // 锁标记名称
+  lockName = ''
+
+  /**
+   * 实例化构造方法
+   *
+   * @param {String} purpose 唯一标识
+   * @param {Number} time
+   */
+  constructor(purpose, lockTime = 60) {
+    this.lockTime = lockTime
+    this.lockName = `SMSLOCK_${purpose}`
+
+    this.init()
+  }
+
+  // 开始计时
+  start(time = null) {
+    this.time = time == null || time >= this.lockTime ? this.lockTime : time
+
+    this.clearInterval()
+
+    this.timer = setInterval(() => {
+      if (this.time == 0) {
+        this.clearInterval()
+        this.time = null
+        localStorage.removeItem(this.lockName)
+        return
+      }
+
+      this.time--
+
+      // 设置本地缓存
+      localStorage.setItem(this.lockName, this.getTime() + this.time)
+    }, 1000)
+  }
+
+  // 页面刷新初始化
+  init() {
+    let result = localStorage.getItem(this.lockName)
+
+    if (result == null) return
+
+    let time = result - this.getTime()
+    if (time > 0) {
+      this.start(time)
+    } else {
+      localStorage.removeItem(this.lockName)
+    }
+  }
+
+  // 获取当前时间
+  getTime() {
+    return Math.floor(new Date().getTime() / 1000)
+  }
+
+  // 清除计时器
+  clearInterval() {
+    clearInterval(this.timer)
+  }
+}
+
+export default SmsLock

+ 90 - 0
src/utils/util.js

@@ -0,0 +1,90 @@
+/**
+ * 时间戳
+ * @param {*} timestamp  时间戳
+ */
+const timestampToTime = (timestamp) => {
+    let date = new Date(timestamp) //时间戳为10位需*1000,时间戳为13位的话不需乘1000
+    let Y = date.getFullYear() + '-'
+    let M =
+        (date.getMonth() + 1 < 10 ?
+            '0' + (date.getMonth() + 1) :
+            date.getMonth() + 1) + '-'
+    let D =
+        (date.getDate() < 10 ? '0' + date.getDate() : date.getDate()) + ' '
+    let h =
+        (date.getHours() < 10 ? '0' + date.getHours() : date.getHours()) + ':'
+    let m =
+        (date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()) +
+        ':'
+    let s =
+        date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds()
+    return Y + M + D + h + m + s
+};
+/**
+ * 存储localStorage
+ */
+const setStore = (name, content) => {
+    if (!name) return;
+    if (typeof content !== 'string') {
+        content = JSON.stringify(content);
+    }
+    window.localStorage.setItem(name, content);
+}
+
+/**
+ * 获取localStorage
+ */
+const getStore = name => {
+    if (!name) return;
+    return window.localStorage.getItem(name);
+}
+
+/**
+ * 删除localStorage
+ */
+const removeStore = name => {
+    if (!name) return;
+    window.localStorage.removeItem(name);
+}
+
+/**
+ * 设置cookie
+ **/
+function setCookie(name, value, day) {
+    let date = new Date();
+    date.setDate(date.getDate() + day);
+    document.cookie = name + '=' + value + ';expires=' + date;
+};
+
+/**
+ * 获取cookie
+ **/
+function getCookie(name) {
+    let reg = RegExp(name + '=([^;]+)');
+    let arr = document.cookie.match(reg);
+    if (arr) {
+        return arr[1];
+    } else {
+        return '';
+    }
+};
+
+/**
+ * 删除cookie
+ **/
+function delCookie(name) {
+    setCookie(name, null, -1);
+};
+
+/**
+ * 导出 
+ **/
+export {
+    timestampToTime,
+    setStore,
+    getStore,
+    removeStore,
+    setCookie,
+    getCookie,
+    delCookie
+}

+ 54 - 0
src/utils/validate.js

@@ -0,0 +1,54 @@
+/**
+ * 检测是否是字邮箱地址
+ *
+ * @param {String} value
+ */
+export const isEmail = value => {
+  return /^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+((.[a-zA-Z0-9_-]{2,3}){1,2})$/.test(
+    value
+  )
+}
+
+/**
+ * 检测是否是手机号
+ *
+ * @param {String} value
+ */
+export const isMobile = value => {
+  return /^1[0-9]{10}$/.test(value)
+}
+
+/**
+ * 检测是否为url
+ *
+ * @param {String} value
+ */
+export const isURL = value => {
+  return /^http[s]?:\/\/.*/.test(value)
+}
+
+/**
+ * 检测是否为数字类型
+ *
+ * @param {*} value
+ */
+export const isNumber = value => {
+  return Object.prototype.toString.call(value).slice(8, -1) === 'Number'
+}
+
+/**
+ * 检测是否为 Booleanl 类型
+ *
+ * @param {*} value
+ */
+export const isBoolean = value => {
+  return Object.prototype.toString.call(value).slice(8, -1) === 'Boolean'
+}
+
+/**
+ * 检测是非是微信浏览器
+ */
+export const isWeiXin = () => {
+  let ua = navigator.userAgent.toLowerCase()
+  return ua.match(/microMessenger/i) == 'micromessenger'
+}

+ 0 - 78
src/views/Login.vue

@@ -1,78 +0,0 @@
-<template>
-  <div style="text-align: center;">
-    <h1>用户登录</h1>
-    <el-row>
-      <el-input
-        v-model="userLogin.username"
-        placeholder="请输入手机号或邮箱"
-        style="width: 70%; padding-right: 2px"
-        clearable
-      />
-      <br>
-      <br>
-      <el-input
-        v-model="userLogin.password"
-        placeholder="请输入验证码"
-        style="width: 45%; padding-right: 2px"
-      />
-      <el-button :disabled="isBtn" @click="fetchVerifyCode">{{ code }}</el-button>
-      <br>
-      <br>
-      <el-image :src="captchaCode" @click="getCaptcha" />
-      <el-input
-        v-model="userLogin.captchaCode"
-        placeholder="请输入图形验证码"
-        style="width: 45%; padding-right: 2px"
-      />
-      <br>
-      <br>
-      <span
-        class="register"
-      >账号不存在会自动注册</span>
-      <span slot="footer" class="dialog-footer">
-        <el-button
-          type="primary"
-          :loading="isLoading"
-          @click.native="loginBtn"
-        >登 录</el-button>
-      </span>
-    </el-row>
-  </div>
-</template>
-
-<script>
-import { userMixin } from 'assets/js/mixin'
-import { getUserInfo } from '@/utils/auth'
-
-export default {
-  name: 'Login',
-  mixins: [userMixin],
-  data() {
-    return {
-      user: null,
-      drawer: false
-    }
-  },
-  created() {
-    this.user = getUserInfo()
-  },
-  methods: {
-    login() {
-      this.fetchPubkey()
-      this.dialogVisible2 = false
-      this.dialogVisible = true
-    },
-    register() {
-      this.fetchPubkey()
-      this.dialogVisible2 = false
-      this.dialogVisible = true
-    },
-    goToStatus() {
-      this.$router.push('/status')
-    }
-  }
-}
-</script>
-
-<style>
-</style>

+ 262 - 0
src/views/sso/forget.vue

@@ -0,0 +1,262 @@
+<template>
+  <div id="login-box">
+    <div class="header">找回密码</div>
+    <div class="main">
+      <el-form ref="form" :model="form" :rules="rules">
+        <el-form-item prop="username">
+          <el-input
+            v-model="form.username"
+            placeholder="我的手机号"
+            class="cuborder-radius"
+            maxlength="11"
+            @keyup.enter.native="onSubmit('form')"
+          />
+        </el-form-item>
+
+        <el-form-item prop="sms_code">
+          <el-input
+            v-model="form.sms_code"
+            placeholder="短信验证码"
+            class="cuborder-radius"
+            maxlength="6"
+            @keyup.enter.native="onSubmit('form')"
+            style="width: 205px"
+          />
+          <div class="send-code-btn send-sms-disable" v-if="smsLock">
+            正在发送 ...
+          </div>
+          <div
+            class="send-code-btn"
+            v-else-if="smsLock == false && smsLockObj.time == null"
+            @click="sendSms"
+          >
+            获取短信
+          </div>
+          <div class="send-code-btn send-sms-disable" v-else>
+            重新发送({{ smsLockObj.time }}s)
+          </div>
+        </el-form-item>
+        <el-form-item prop="password">
+          <el-input
+            v-model="form.password"
+            type="password"
+            placeholder="设置新密码"
+            class="cuborder-radius"
+            @keyup.enter.native="onSubmit('form')"
+          />
+        </el-form-item>
+        <el-form-item prop="password2">
+          <el-input
+            v-model="form.password2"
+            type="password"
+            placeholder="确认新密码"
+            class="cuborder-radius"
+            @keyup.enter.native="onSubmit('form')"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button
+            type="primary"
+            class="submit-btn"
+            :loading="forgetLoading"
+            @click="onSubmit('form')"
+            >立即找回
+          </el-button>
+        </el-form-item>
+
+        <el-form-item>
+          <div class="links">
+            <el-link
+              type="primary"
+              :underline="false"
+              @click="toLink('/sso/register')"
+              >注册账号
+            </el-link>
+            <el-link
+              type="primary"
+              :underline="false"
+              @click="toLink('/sso/login')"
+              >已有账号,立即登录?
+            </el-link>
+          </div>
+        </el-form-item>
+      </el-form>
+    </div>
+  </div>
+</template>
+
+<script>
+import { ServeForgetPassword, ServeSendVerifyCode } from '@/api/auth'
+import { isMobile } from '@/utils/validate'
+import SmsLock from '@/utils/sms-lock'
+
+export default {
+  name: 'ForgetPasswordPage',
+  data() {
+    let validateMobile = (rule, value, callback) => {
+      if (value === '') {
+        callback(new Error('手机号不能为空!'))
+      } else {
+        isMobile(value) ? callback() : callback(new Error('手机号格式不正确!'))
+      }
+    }
+
+    let validatePass2 = (rule, value, callback) => {
+      if (value === '') {
+        callback(new Error('请再次输入密码'))
+      } else if (value !== this.form.password) {
+        callback(new Error('两次输入密码不一致!'))
+      } else {
+        callback()
+      }
+    }
+
+    return {
+      forgetLoading: false,
+
+      form: {
+        username: '',
+        password: '',
+        password2: '',
+        sms_code: '',
+      },
+
+      rules: {
+        username: [
+          {
+            validator: validateMobile,
+            trigger: 'blur',
+          },
+        ],
+        password: [
+          {
+            required: true,
+            message: '登录密码不能为空!',
+            trigger: 'blur',
+          },
+        ],
+        password2: [
+          {
+            validator: validatePass2,
+            trigger: 'blur',
+          },
+        ],
+        sms_code: [
+          {
+            required: true,
+            message: '验证码不能为空!',
+            trigger: 'blur',
+          },
+        ],
+      },
+
+      smsLock: false,
+      smsLockObj: null,
+    }
+  },
+  created() {
+    this.smsLockObj = new SmsLock('FORGET_PSW_SMS', 60)
+  },
+  destroyed() {
+    this.smsLockObj.clearInterval()
+  },
+  methods: {
+    toLink(url) {
+      this.$router.push({
+        path: url,
+      })
+    },
+
+    onSubmit(formName) {
+      if (this.forgetLoading) return false
+
+      this.$refs[formName].validate(valid => {
+        if (!valid) return false
+        this.forgetLoading = true
+        this.forgetAccount()
+      })
+    },
+
+    forgetAccount() {
+      ServeForgetPassword({
+        mobile: this.form.username,
+        password: this.form.password,
+        sms_code: this.form.sms_code,
+      })
+        .then(res => {
+          this.forgetLoading = false
+          if (res.code == 200) {
+            this.$notify({
+              title: '成功',
+              message: '密码修改成功,快去登录吧...',
+              type: 'success',
+            })
+
+            this.$refs.form.resetFields()
+            setTimeout(() => {
+              this.$router.push('/auth/login')
+            }, 1500)
+          } else {
+            this.$notify({
+              message: res.message,
+            })
+          }
+        })
+        .catch(() => {
+          this.forgetLoading = false
+          this.$notify({
+            message: '网络错误,请稍后再试...',
+          })
+        })
+    },
+
+    //点击发送验证码
+    sendSms() {
+      if (this.smsLock) return false
+
+      if (!isMobile(this.form.username)) {
+        this.$refs.form.validateField('username')
+        return false
+      }
+
+      this.smsLock = true
+      ServeSendVerifyCode({
+        mobile: this.form.username,
+        channel: 'forget_account',
+      })
+        .then(res => {
+          if (res.code == 200) {
+            this.smsLockObj.start()
+            this.$notify({
+              title: '成功',
+              message: '验证码发送成功...',
+              type: 'success',
+            })
+
+            if (res.data.is_debug) {
+              setTimeout(() => {
+                this.$notify({
+                  title: '提示',
+                  message: '已自动填充验证码',
+                })
+
+                this.form.sms_code = res.data.sms_code
+              }, 500)
+            }
+          } else {
+            this.$notify({
+              title: '提示',
+              message: '验证码发送失败...',
+            })
+          }
+        })
+        .finally(() => {
+          this.smsLock = false
+        })
+    },
+  },
+}
+</script>
+<style lang="scss" scoped>
+@import '~@/assets/css/login-auth.scss';
+</style>

+ 14 - 0
src/views/sso/layout.vue

@@ -0,0 +1,14 @@
+<template>
+  <div>
+    <el-container id="auth-container">
+      <el-main>
+        <router-view />
+<!--        <div class="copyright" v-html="$store.state.copyright"></div>-->
+      </el-main>
+    </el-container>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+@import '~@/assets/css/login-auth.scss';
+</style>

+ 280 - 0
src/views/sso/login.vue

@@ -0,0 +1,280 @@
+<template>
+  <div id="login-box">
+    <div class="header">登录</div>
+        <div class="main">
+          <el-form ref="form" :model="form" :rules="rules">
+            <el-form-item prop="principal">
+              <el-input
+                  v-model="form.principal"
+                  placeholder="手机号"
+                  class="cuborder-radius"
+                  @keyup.enter.native="onSubmit('form')"
+              />
+            </el-form-item>
+            <el-form-item prop="credential">
+              <el-input
+                  v-model="form.credential"
+                  placeholder="验证码"
+                  class="cuborder-radius"
+                  maxlength="6"
+                  style="width: 205px"
+                  @keyup.enter.native="onSubmit('form')"
+              />
+              <div v-if="smsLock" class="send-code-btn send-sms-disable">
+                正在发送 ...
+              </div>
+              <div
+                  v-else-if="smsLock === false && smsLockObj.time === null"
+                  class="send-code-btn"
+                  @click="sendSms"
+              >
+                获取验证码
+              </div>
+              <div v-else class="send-code-btn send-sms-disable">
+                重新发送({{ smsLockObj.time }}s)
+              </div>
+            </el-form-item>
+            <el-form-item prop="captchaCode">
+              <el-row>
+                <el-col :span=12>
+                  <el-input
+                      v-model="form.captchaCode"
+                      placeholder="图形验证码"
+                      class="cuborder-radius"
+                      maxlength="11"
+                      @keyup.enter.native="onSubmit('form')"
+                  />
+                </el-col>
+                <el-col :span=12>
+                  <img :src="captchaCode" alt="图形验证码" title="点击刷新" style="cursor:pointer;" @click="getCaptcha">
+                </el-col>
+              </el-row>
+            </el-form-item>
+            <el-form-item>
+              <el-button
+                  type="primary"
+                  class="submit-btn"
+                  :loading="loginLoading"
+                  @click="onSubmit('form')"
+              >立即登录
+              </el-button>
+            </el-form-item>
+            <el-form-item>
+              <div class="links">
+                <el-link
+                    type="primary"
+                    :underline="false"
+                    @click="toLink('/sso/forget')"
+                >找回密码
+                </el-link>
+                <el-link
+                    type="primary"
+                    :underline="false"
+                    @click="toLink('/sso/register')"
+                >没有账号?
+                </el-link>
+              </div>
+            </el-form-item>
+          </el-form>
+        </div>
+      </div>
+</template>
+
+<script>
+import {ServeCaptcha, ServeLogin, ServePubkey, ServeSendVerifyCode} from '@/api/auth'
+import { userMixin } from 'assets/js/mixin'
+import { isMobile } from '@/utils/validate'
+import SmsLock from "@/utils/sms-lock"
+import { JSEncrypt } from 'jsencrypt'
+import Cookies from 'js-cookie'
+
+export default {
+  mixins: [userMixin],
+  data() {
+    let validateMobile = (rule, value, callback) => {
+      if (value === '') {
+        callback(new Error('登录手机号不能为空!'))
+      } else {
+        // isMobile(value) ? callback() : callback(new Error('登录手机号格式不正确!'))
+        callback()
+      }
+    }
+    return {
+      loginLoading: false,
+      captchaUrl: '',
+      form: {
+        loginType: 1,
+        plat: 1,
+        principal: '',
+        credential: '',
+        captchaCode: ''
+      },
+      pubkey: {
+        key: '',
+        r: ''
+      },
+      captchaCode: '',
+      rules: {
+        principal: [
+          {
+            validator: validateMobile,
+            trigger: 'blur',
+          },
+          {
+            min: 1,
+            max: 100,
+            message: '手机号格式不正确!',
+            trigger: 'blur',
+          },
+        ],
+        credential: [
+          {
+            required: true,
+            message: '登录密码不能为空!',
+            trigger: 'blur',
+          },
+        ],
+      },
+      smsLock: false,
+      smsLockObj: null
+    }
+  },
+  mounted() {
+    // 此页面是作为iframe嵌入的,在加载时,监听父页面发来的信息
+    window.addEventListener('message', function(event) {
+      let data = event.data
+      console.log('收到父级窗口消息', data)
+      //父页面(也就是子系统),点击退出时,移除统一登录中心域名下的token
+      if (data.type === 'logOut') {
+        Cookies.remove('token', {
+          Secure: true,
+          SameSite: 'None',
+          Domain: 'reghao.cn' //这个域名填写统一登录中心的一级域名
+        })
+      }
+    })
+  },
+  created() {
+    this.getCaptcha()
+    this.getPubkey()
+    this.smsLockObj = new SmsLock('LOGIN_SMS', 60)
+  },
+  destroyed() {
+    this.smsLockObj.clearInterval()
+  },
+  methods: {
+    onSubmit(formName) {
+      if (this.loginLoading) return false
+
+      this.$refs[formName].validate(valid => {
+        if (!valid) return false
+        this.loginLoading = true
+        this.login()
+      })
+    },
+    getCaptcha() {
+      ServeCaptcha().then(res => {
+        if (res.code === 0) {
+          this.captchaCode = res.data
+        } else {
+          this.$notify.info({
+            title: '提示',
+            message: '获取 Captcha 失败, 请重新刷新页面...',
+          })
+        }
+      }).finally(() => {
+      })
+    },
+    getPubkey() {
+      ServePubkey().then(res => {
+        if (res.code === 0) {
+          this.pubkey.key = res.data.pubkey
+          this.pubkey.r = res.data.r
+        } else {
+          this.$notify.info({
+            title: '提示',
+            message: '获取公钥失败, 请重新刷新页面...',
+          })
+        }
+      }).finally(() => {
+      })
+    },
+    encryptPassword(password) {
+      var encryptor = new JSEncrypt()
+      encryptor.setPublicKey(this.pubkey.key)
+      return encryptor.encrypt(this.pubkey.r + password)
+    },
+    async login() {
+      ServeLogin({
+        loginType: this.form.loginType,
+        plat: this.form.plat,
+        principal: this.form.principal,
+        credential: this.encryptPassword(this.form.credential),
+        captchaCode: this.form.captchaCode
+      }).then(res => {
+          if (res.code === 0) {
+            let result = res.data
+            const token = result.userToken.accessToken
+            //取得登录接口的token后,在统一登录中心域名底下设置cookie
+            Cookies.set('token', token, {
+              Secure: true,
+              SameSite: 'None',
+              Domain: 'reghao.cn' //这个域名填写统一登录中心的一级域名
+            })
+
+            if (token) {
+              //向父窗口发送登录成功的消息
+              window.parent.postMessage({ type: 'loginSuccess' }, '*')
+            }
+          } else {
+            this.$notify.info({
+              title: '提示',
+              message: res.msg,
+            })
+          }
+        }).finally(() => {
+          this.loginLoading = false
+        })
+    },
+    sendSms() {
+      if (this.smsLock) return false
+      if (!isMobile(this.form.username)) {
+        this.$refs.form.validateField('username', (val) => {
+          return !val;
+        })
+      }
+
+      this.smsLock = true
+      ServeSendVerifyCode({
+        receiver: this.form.principal,
+        notifyType: 3,
+      }).then(res => {
+        if (res.code === 0) {
+          this.$notify({
+            title: '成功',
+            message: '验证码发送成功...',
+            type: 'success',
+          })
+          this.smsLockObj.start()
+        } else {
+          this.$notify({
+            title: '提示',
+            message: res.message,
+            customClass: 'cus-notifyclass',
+          })
+        }
+      }).finally(() => {
+            this.smsLock = false
+          })
+    },
+    toLink(url) {
+      this.$router.push({
+        path: url,
+      })
+    }
+  },
+}
+</script>
+<style lang="scss" scoped>
+@import '~@/assets/css/login-auth.scss';
+</style>

+ 314 - 0
src/views/sso/register.vue

@@ -0,0 +1,314 @@
+<template>
+  <div id="login-box">
+    <div class="header">注册</div>
+    <div class="main">
+      <el-form ref="form" :model="form" :rules="rules">
+        <el-form-item prop="username">
+          <el-input
+            v-model="form.username"
+            placeholder="用户名"
+            class="cuborder-radius"
+            maxlength="11"
+            @keyup.enter.native="onSubmit('form')"
+          />
+        </el-form-item>
+        <el-form-item prop="password">
+          <el-input
+              v-model="form.password"
+              type="password"
+              placeholder="密码"
+              class="cuborder-radius"
+              @keyup.enter.native="onSubmit('form')"
+          />
+        </el-form-item>
+        <el-form-item prop="password2">
+          <el-input
+              v-model="form.password2"
+              type="password"
+              placeholder="确认密码"
+              class="cuborder-radius"
+              @keyup.enter.native="onSubmit('form')"
+          />
+        </el-form-item>
+        <el-form-item prop="mobile">
+          <el-input
+            v-model="form.mobile"
+            placeholder="手机号"
+            class="cuborder-radius"
+            maxlength="11"
+            @keyup.enter.native="onSubmit('form')"
+          />
+        </el-form-item>
+        <el-form-item prop="smsCode">
+          <el-input
+            v-model="form.smsCode"
+            placeholder="验证码"
+            class="cuborder-radius"
+            maxlength="6"
+            style="width: 205px"
+            @keyup.enter.native="onSubmit('form')"
+          />
+
+          <div v-if="smsLock" class="send-code-btn send-sms-disable">
+            正在发送 ...
+          </div>
+          <div
+            v-else-if="smsLock === false && smsLockObj.time === null"
+            class="send-code-btn"
+            @click="sendSms"
+          >
+            获取验证码
+          </div>
+          <div v-else class="send-code-btn send-sms-disable">
+            重新发送({{ smsLockObj.time }}s)
+          </div>
+        </el-form-item>
+        <el-form-item prop="captchaCode">
+          <el-row>
+            <el-col :span=12>
+              <el-input
+                  v-model="form.captchaCode"
+                  placeholder="图形验证码"
+                  class="cuborder-radius"
+                  maxlength="11"
+                  @keyup.enter.native="onSubmit('form')"
+              />
+            </el-col>
+            <el-col :span=12>
+              <img :src="captchaCode" alt="图形验证码" title="点击刷新" style="cursor:pointer;" @click="getCaptcha">
+            </el-col>
+          </el-row>
+        </el-form-item>
+        <el-form-item>
+          <el-button
+            type="primary"
+            class="submit-btn"
+            :loading="registerLoading"
+            @click="onSubmit('form')"
+          >
+            立即注册
+          </el-button>
+        </el-form-item>
+        <el-form-item>
+          <div class="links">
+            <el-link
+              type="primary"
+              :underline="false"
+              @click="toLink('/sso/forget')"
+            >
+              找回密码
+            </el-link>
+            <el-link
+              type="primary"
+              :underline="false"
+              @click="toLink('/sso/login')"
+            >
+              已有账号?
+            </el-link>
+          </div>
+        </el-form-item>
+      </el-form>
+    </div>
+  </div>
+</template>
+
+<script>
+import { ServeCaptcha, ServeRegister, ServeSendVerifyCode } from '@/api/auth'
+import { isMobile } from '@/utils/validate'
+import SmsLock from '@/utils/sms-lock'
+
+export default {
+  data() {
+    let validateMobile = (rule, value, callback) => {
+      if (value === '') {
+        callback(new Error('手机号不能为空!'))
+        return
+      }
+
+      if (!isMobile(value)) {
+        callback(new Error('手机号格式不正确!'))
+      } else {
+        callback()
+      }
+    }
+
+    let validatePass2 = (rule, value, callback) => {
+      if (value === '') {
+        callback(new Error('请再次输入密码'))
+      } else if (value !== this.form.password) {
+        callback(new Error('两次输入密码不一致!'))
+      } else {
+        callback()
+      }
+    }
+
+    return {
+      captchaCode: '',
+      registerLoading: false,
+      form: {
+        username: '',
+        password: '',
+        mobile: '',
+        smsCode: '',
+        captchaCode: '',
+        plat: ''
+      },
+      rules: {
+        username: [
+          {
+            required: true,
+            message: '用户名不能为空!',
+            trigger: 'blur',
+          },
+        ],
+        mobile: [
+          {
+            validator: validateMobile,
+            trigger: 'blur',
+          },
+        ],
+        smsCode: [
+          {
+            required: true,
+            message: '短信验证码不能为空!',
+            trigger: 'blur',
+          },
+        ],
+      },
+      smsLock: false,
+      smsLockObj: null,
+    }
+  },
+  created() {
+    this.getCaptcha()
+    this.smsLockObj = new SmsLock('REGISTER_SMS', 60)
+  },
+  destroyed() {
+    this.smsLockObj.clearInterval()
+  },
+  methods: {
+    getCaptcha() {
+      ServeCaptcha().then(res => {
+        if (res.code === 0) {
+          this.captchaCode = res.data
+        } else {
+          this.$notify.info({
+            title: '提示',
+            message: '获取 Captcha 失败, 请重新刷新页面...',
+          })
+        }
+      }).finally(() => {
+      })
+    },
+    randomString(len) {
+      len = len || 16
+      var t = '012345678ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz'
+      var a = t.length
+      var res = ''
+      for (var i = 0; i < len; i++) res += t.charAt(Math.floor(Math.random() * a))
+      return res
+    },
+    resetFields() {
+      if (!this.model) {
+        console.warn('[Element Warn][Form]model is required for resetFields to work.');
+        return;
+      }
+      this.fields.forEach(field => {
+        field.resetField();
+      });
+    },
+    toLink(url) {
+      this.$router.push({
+        path: url,
+      })
+    },
+
+    onSubmit(formName) {
+      if (this.registerLoading) return false
+
+      this.$refs[formName].validate(valid => {
+        if (!valid) return false
+        this.registerLoading = true
+        this.register()
+      })
+    },
+    register() {
+      ServeRegister(
+          this.form
+      ).then(res => {
+          if (res.code === 0) {
+            this.$notify({
+              title: '成功',
+              message: '注册成功,快去登录吧...',
+              type: 'success',
+            })
+
+            this.$refs.form.resetFields()
+            setTimeout(() => {
+              this.toLink('/auth/login')
+            }, 1500)
+          } else {
+            this.$notify.info({
+              title: '提示',
+              message: res.message,
+            })
+          }
+        }).catch((e) => {
+          console.log(e)
+          this.$notify({
+            message: '网络错误,请稍后再试...',
+          })
+        }).finally(() => {
+          this.registerLoading = false
+        })
+    },
+    // 点击发送验证码
+    sendSms() {
+      if (this.smsLock) return false
+
+      if (!isMobile(this.form.mobile)) {
+        this.$refs.form.validateField('mobile', (val) => {
+          return !val;
+        })
+      }
+
+      this.smsLock = true
+      ServeSendVerifyCode({
+        receiver: this.form.mobile,
+        channel: 'register',
+      }).then(res => {
+          if (res.code === 0) {
+            this.$notify({
+              title: '成功',
+              message: '验证码发送成功...',
+              type: 'success',
+            })
+
+            this.smsLockObj.start()
+            /*if (res.data.is_debug) {
+              setTimeout(() => {
+                this.$notify({
+                  title: '提示',
+                  message: '已自动填充验证码',
+                })
+
+                this.form.sms_code = res.data.sms_code
+              }, 500)
+            }*/
+          } else {
+            this.$notify({
+              title: '提示',
+              message: res.message,
+              customClass: 'cus-notifyclass',
+            })
+          }
+        }).finally(() => {
+          this.smsLock = false
+        })
+    },
+  },
+}
+</script>
+<style lang="scss">
+@import '~@/assets/css/login-auth.scss';
+</style>

+ 15 - 0
vue.config.js

@@ -1,3 +1,9 @@
+const path = require('path')
+
+function resolve(dir) {
+  return path.join(__dirname, dir)
+}
+
 module.exports = {
   // https://github.com/vuejs/vue-docs-zh-cn/blob/master/vue-cli-plugin-eslint/README.md
   lintOnSave: process.env.NODE_ENV !== 'production',
@@ -42,5 +48,14 @@ module.exports = {
         limit: 10000
       })
   },
+  pluginOptions: {
+    'style-resources-loader': {
+      preProcessor: 'scss',
+      patterns: [
+        //全局加载 less 变量
+        path.resolve(__dirname, './src/assets/css/variable.scss'),
+      ],
+    },
+  },
   publicPath: '/'
 }

Some files were not shown because too many files changed in this diff