Просмотр исходного кода

#13165 Email OTP authentication and login process
#202306 优化编译后分包策略,加快访问速度

vbea 3 лет назад
Родитель
Сommit
67b5c34105

+ 16 - 3
Strides-Admin/src/http/api/user.js

@@ -2,6 +2,19 @@ import {get, post} from '../http'
 
 const MODULE_NAME = "user/"
 
-export function login(data) {
-  return post(`${MODULE_NAME}/login`, data)
-}
+const user = {
+  login(data) {
+    return post(MODULE_NAME + "login", data)
+  },
+  sendOTP(data) {
+    return get(MODULE_NAME + "otp", data)
+  },
+  preLogin(data) {
+    return post(MODULE_NAME + "pre-login", data)
+  },
+  updatePassword(data) {
+    return post(MODULE_NAME + "update-pwd", data)
+  }
+}
+
+export default user;

+ 1 - 1
Strides-Admin/src/layout/components/Navbar.vue

@@ -104,7 +104,7 @@ export default {
     },
     logout() {
       this.$confirm('Are you sure you want to logout ?', 'Logout', {
-        confirmButtonText: 'Ok',
+        confirmButtonText: 'OK',
         cancelButtonText: 'Cancel',
         type: 'warning',
       }).then(async () => {

+ 4 - 4
Strides-Admin/src/store/modules/user.js

@@ -40,12 +40,12 @@ const mutations = {
 const actions = {
   // user login
   login({ commit }, userInfo) {
-    const { username, password, email } = userInfo
     return new Promise((resolve, reject) => {
       login({
-        email,
-        //userName: username.trim(),
-        password: password
+        email: userInfo.email,
+        //userName: userInfo.username.trim(),
+        password: userInfo.password,
+        otp: userInfo.otp
       }).then(response => {
         const {
           data: {

+ 4 - 0
Strides-Admin/src/styles/btn.scss

@@ -97,3 +97,7 @@
   font-size: 14px;
   border-radius: 4px;
 }
+
+.el-button.el-button--content {
+  color: #232323;
+}

+ 2 - 2
Strides-Admin/src/styles/element-variables.scss

@@ -2,9 +2,9 @@
 * I think element-ui's default theme color is too light for long-term use.
 * So I modified the default color and you can modify it to your liking.
 **/
-
+@import "./element-ui.scss";
 /* theme color */
-$--color-primary: #1890ff;
+//$--color-primary: #1890ff;
 $--color-success: #13ce66;
 $--color-warning: #ffba00;
 $--color-danger: #ff4949;

+ 4 - 4
Strides-Admin/src/views/access/DialogDetail.vue

@@ -61,7 +61,7 @@
             v-model="form.password"
             class="flex-item"
             type="password"
-            maxlength="16"/>
+            maxlength="32"/>
         </el-form-item>
         <el-form-item
           class="form-item"
@@ -72,7 +72,7 @@
             v-model="form.password"
             class="flex-item"
             type="password"
-            maxlength="16"/>
+            maxlength="32"/>
         </el-form-item>
         <el-form-item
           class="form-item"
@@ -214,11 +214,11 @@ export default {
           trigger: 'blur',
           message: 'Please type a correct email'
         }],
-        password: {
+        password: [{
           required: true,
           message: "Passwrod is required",
           trigger: "blur"
-        },
+        }],
         providerPk: {
           required: true,
           message: "Provider is required",

+ 2 - 2
Strides-Admin/src/views/dashboard/index.vue

@@ -106,11 +106,11 @@ export default {
 </script>
 
 <style scoped lang='scss'>
-  @import '../../styles/variables.scss';
+  /*@import '../../styles/variables.scss';*/
   @import '../../styles/element-ui.scss';
   .dashboard-container {
     padding: 10px 20px;
-    min-height: $mainAppMinHeight;
+    /*min-height: $mainAppMinHeight;*/
     background-color: #F0F5FC;
     .sp-filter {
       flex: 1;

+ 321 - 0
Strides-Admin/src/views/login/Password.vue

@@ -0,0 +1,321 @@
+<template>
+  <div class="container">
+    <div class="close" @click="onClose" v-if="!email">
+      <i class="el-icon-back"></i>
+    </div>
+    <el-form
+      ref="loginForm"
+      :model="form"
+      :rules="rules"
+      label-position="left">
+      <el-form-item v-if="email">
+        <div class="form-login-input no-border">
+          <span class="svg-container">
+            <svg-icon icon-class="user" />
+          </span>
+          <div class="input">&nbsp; {{email}}</div>
+        </div>
+      </el-form-item>
+      <el-form-item prop="email" v-else>
+        <div class="form-login-input">
+          <span class="svg-container">
+            <svg-icon icon-class="email" />
+          </span>
+          <el-input
+            ref="username"
+            v-model="form.email"
+            name="email"
+            type="text"
+            tabindex="1"
+            autocomplete="on"
+            clearable
+            maxlength="50"/>
+        </div>
+      </el-form-item>
+      <el-form-item prop="password" label="Create Password">
+        <div class="form-login-input">
+          <span class="svg-container">
+            <svg-icon icon-class="password" />
+          </span>
+          <el-input
+            ref="password"
+            v-model="form.password"
+            name="password"
+            :type="passwordType"
+            tabindex="1"
+            autocomplete="on"
+            clearable
+            maxlength="32"
+            @input="applyStrength"/>
+          <span class="show-pwd" @click="showPwd">
+            <svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />
+          </span>
+        </div>
+      </el-form-item>
+      <div class="layout-strength">
+        <div class="flexc">
+          <span class="title-strength">Password Strength</span>
+          <template v-for="item in 5">
+            <span :class='strength >= item ? "strength" : "no-strength"'></span>
+          </template>
+        </div>
+        <div class="strength-desc">
+          You Password Must Have: <br/>
+          - 8 or more characters<br/>
+          - upper and lower case letters<br/>
+          - at least one number
+        </div>
+      </div>
+      <el-form-item prop="password2" label="Confirm Password">
+        <div class="form-login-input">
+          <span class="svg-container">
+            <svg-icon icon-class="password" />
+          </span>
+          <el-input
+            ref="password2"
+            v-model="form.password2"
+            name="password2"
+             :type="passwordType"
+            tabindex="1"
+            autocomplete="on"
+            clearable
+            maxlength="32"/>
+        </div>
+      </el-form-item>
+      <el-button
+        :loading="loading"
+        type="primary"
+        class="login-button"
+        @click="onSubmit">
+        {{email ? "CREATE PASSWORD" : "RESET PASSWORD" }}
+      </el-button>
+    </el-form>
+  </div>
+</template>
+
+<script>
+  export default {
+    name: "Password",
+    props: {
+      email: String
+    },
+    data() {
+      return {
+        loading: false,
+        form: {
+          email: "",
+          password: "",
+          password2: ""
+        },
+        rules: {
+          email: [{
+            required: true,
+            trigger: 'blur',
+            message: "Please type email"
+          }, {
+            pattern: /^[a-zA-Z0-9]+[\S]+@[a-zA-Z0-9_-]+[\.][\Sa-zA-Z]+$/,
+            trigger: 'blur',
+            message: 'Please type a correct email'
+          }],
+          password: [{
+            required: true,
+            trigger: 'blur',
+            message: "Please type password"
+          }],
+          password2: [{
+            required: true,
+            trigger: 'blur',
+            message: "Please type confirm password"
+          }, {
+            trigger: 'blur',
+            validator: this.validatePassword
+          }]
+        },
+        passwordType: "password",
+        strength: 0
+      };
+    },
+    mounted() {
+      
+    },
+    methods: {
+      onClose() {
+        this.$emit("back")
+      },
+      showPwd() {
+        if (this.passwordType === 'password') {
+          this.passwordType = ''
+        } else {
+          this.passwordType = 'password'
+        }
+        this.$nextTick(() => {
+          this.$refs.password.focus()
+        })
+      },
+      applyStrength(text) {
+        var streng = 0;
+        if (text) {
+          if (text.length >= 8) {
+            streng += 1;
+          }
+          if (/[a-z]{1,}/.test(text)) {
+            streng += 1;
+          }
+          if (/[A-Z]{1,}/.test(text)) {
+            streng += 1;
+          }
+          if (/\d{1,}/.test(text)) {
+            streng += 1;
+          }
+          if (/\W{1,}/.test(text)) {
+            streng += 1;
+          }
+        }
+        this.strength = streng;
+      },
+      validatePassword(rule, value, callback) {
+        if (value !== this.form.password) {
+          callback(new Error('The twice passwords are inconsistent'))
+        } else {
+          callback()
+        }
+      },
+      onSubmit() {
+        this.$refs.loginForm.validate(valid => {
+          if (valid) {
+            if (this.strength >= 4) {
+              if (this.email) {
+                this.$emit("confirm", {
+                  type: "create",
+                  password: this.form.password
+                });
+              } else {
+                this.$emit("confirm", {
+                  type: "reset",
+                  password: this.form.password
+                });
+              }
+            } else {
+              this.$message({
+                type: 'error',
+                message: "Your password is not strong"
+              })
+            }
+          }
+        })
+      }
+    }
+  }
+</script>
+
+<style scoped lang="scss">
+  @import '../../styles/element-ui.scss';
+  .container {
+    position: relative;
+  }
+  ::v-deep label.el-form-item__label {
+    color: #333;
+    &::before {
+      display: none;
+    }
+  }
+  .form-login-input {
+    width: 100%;
+    display: flex;
+    padding: 0 10px;
+    position: relative;
+    align-items: center;
+    border: 1px solid #eee;
+    border-radius: 4px;
+    &.no-border {
+      border: none;
+    }
+    label {
+      top: 0;
+      left: 0;
+      color: #aaa;
+      font-size: 14px;
+      position:absolute;
+      padding-top: 7px;
+      padding-left: 45px;
+      font-weight: normal;
+      pointer-events: none;
+      transition: all 0.3s;
+    }
+    label.focus {
+    	top: -30px;
+    	color: #333;
+    	font-size: 15px;
+      font-weight: bold;
+      padding-left: 5px;
+    }
+    .svg-container {
+      width: 30px;
+      color: #333;
+      font-size: 16px;
+      padding: 0px 6px;
+    }
+    .el-input,.input {
+      flex: 1;
+      color: #333;
+      height: 42px;
+      font-size: 14px;
+    
+      &::v-deep input {
+        color: #333;
+        height: 42px;
+        border: none;
+        font-size: 14px;
+        padding: 0 5px;
+        border-radius: 0px;
+        -webkit-appearance: none;
+        background: transparent;
+      }
+    }
+  }
+  .layout-strength {
+    padding-left: 16px;
+    padding-bottom: 2px;
+  }
+  .title-strength {
+    color: #333;
+    font-size: 12px;
+    padding-right: 10px;
+  }
+  .strength {
+    width: 14px;
+    height: 3px;
+    margin: 3px;
+    border-radius: 3px;
+    background-color: $--color-accent;
+  }
+  .no-strength {
+    width: 14px;
+    height: 3px;
+    margin: 3px;
+    border-radius: 3px;
+    background-color: #ccc;
+  }
+  .strength-desc {
+    color: #999;
+    font-size: 12px;
+    padding-top: 5px;
+  }
+  .login-button {
+    width:100%;
+    color: #fff;
+    padding: 15px;
+    margin-top: 10px;
+    margin-bottom: 10px;
+    font-weight: bold;
+  }
+  .close {
+    top: -43px;
+    left: -5px;
+    z-index: 10;
+    padding: 5px;
+    cursor: pointer;
+    font-size: 16px;
+    position: absolute;
+  }
+</style>

+ 222 - 93
Strides-Admin/src/views/login/login.vue

@@ -1,72 +1,105 @@
 <template>
   <div class="login-container" id="particles-js">
     <div class="bg-logo anim"></div>
-    <el-form
-      ref="loginForm"
-      :model="loginForm"
-      :rules="loginRules"
-      class="login-form"
-      autocomplete="on"
-      label-position="left">
-
+    <div class="login-form">
       <div class="title-container">
         <!-- <h3 class="title">{{appName}}</h3> -->
         <img class="csms-logo" src="../../icons/logo.png"/>
-        <p class="title">CSMS LOGIN</p>
+        <p class="title" v-if="showOTP">Please enter 6-digit OTP sent to your email</p>
+        <p class="title" v-else-if="passwordTitle">{{passwordTitle}}</p>
+        <p class="title" v-else>{{showSetPassword ? "Reset Password" : "CSMS LOGIN"}}</p>
+        <p class="sub-title" v-if="passwordSubTitle">{{passwordSubTitle}}</p>
       </div>
-
-      <el-form-item prop="email">
-        <div class="form-login-input">
-          <span class="svg-container">
-            <svg-icon icon-class="email" />
-          </span>
-          <el-input
-            ref="username"
-            v-model="loginForm.email"
-            name="email"
-            type="text"
-            tabindex="1"
-            autocomplete="on"
-            clearable
-            maxlength="50"
-            @focus="() => focusFiled.email = true"
-            @blur="() => focusFiled.email = false"/>
-          <label :class='isNameValid ? "focus" : ""'>Email</label>
-        </div>
-      </el-form-item>
-      <el-form-item prop="password">
-        <div class="form-login-input">
-          <span class="svg-container">
-            <svg-icon icon-class="password" />
-          </span>
-          <el-input
-            ref="password"
-            v-model="loginForm.password"
-            :type="passwordType"
-            name="password"
-            tabindex="2"
-            autocomplete="off"
-            clearable
-            maxlength="20"
-            @keyup.enter.native="handleLogin"
-            @focus="() => focusFiled.password = true"
-            @blur="() => focusFiled.password = false"
-          />
-          <span class="show-pwd" @click="showPwd">
-            <svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />
-          </span>
-          <label :class='isPsdValid ? "focus" : ""'>Password</label>
+      <div v-if="showResetSuccess">
+        <div style="height: 200px;"></div>
+        <el-button
+          :loading="loading"
+          type="primary"
+          class="login-button"
+          @click="handleShowLogin">
+          GO TO LOGIN
+        </el-button>
+      </div>
+      <OtpView
+        v-else-if="showOTP"
+        :loginInfo="loginForm"
+        :resetInfo="newPasswordInfo"
+        :isReset="showSetPassword"
+        @onLogin="handleLogin"
+        @onReset="handleResetSuccess"/>
+      <el-form
+        v-else-if="!showSetPassword"
+        ref="loginForm"
+        :model="loginForm"
+        :rules="loginRules"
+        autocomplete="on"
+        label-position="left">
+        <el-form-item prop="email">
+          <div class="form-login-input">
+            <span class="svg-container">
+              <svg-icon icon-class="email" />
+            </span>
+            <el-input
+              ref="username"
+              v-model="loginForm.email"
+              name="email"
+              type="text"
+              tabindex="1"
+              autocomplete="on"
+              clearable
+              maxlength="50"
+              @focus="() => focusFiled.email = true"
+              @blur="() => focusFiled.email = false"/>
+            <label :class='isNameValid ? "focus" : ""'>Email</label>
+          </div>
+        </el-form-item>
+        <el-form-item prop="password">
+          <div class="form-login-input">
+            <span class="svg-container">
+              <svg-icon icon-class="password" />
+            </span>
+            <el-input
+              ref="password"
+              v-model="loginForm.password"
+              :type="passwordType"
+              name="password"
+              tabindex="2"
+              autocomplete="off"
+              clearable
+              maxlength="32"
+              @keyup.enter.native="onPreLogin"
+              @focus="() => focusFiled.password = true"
+              @blur="() => focusFiled.password = false"
+            />
+            <span class="show-pwd" @click="showPwd">
+              <svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />
+            </span>
+            <label :class='isPsdValid ? "focus" : ""'>Password</label>
+          </div>
+        </el-form-item>
+        
+        <div>
+          <el-button
+            :loading="loading"
+            type="primary"
+            class="login-button"
+            @click.native.prevent="onPreLogin">
+            LOGIN
+          </el-button>
+          <el-button
+            type="content"
+            class="forgot-button"
+            @click="handleForgotPassword">
+            FORGET PASSWORD
+          </el-button>
         </div>
-      </el-form-item>
-
-      <el-button
-        :loading="loading"
-        type="primary"
-        class="login-button"
-        @click.native.prevent="handleLogin">
-        Login
-      </el-button>
-    </el-form>
+      </el-form>
+      <Password
+        v-else
+        :email="expireEmail"
+        @back="handleShowLogin"
+        @confirm="onResetPasswordConfirm"/>
+    </div>
     <div class="copyinfo">©{{copyYear}}&nbsp; {{company}}</div>
   </div>
 </template>
@@ -78,6 +111,10 @@ import settings from '../../settings.js'
 import {getEmail} from '../../utils/auth.js'
 import particles from 'particles.js'
 import animateJson from './animate.json'
+import Password from './Password.vue'
+import OtpView from './otp.vue'
+import api from '@/http/api/user'
+import {parseTime} from '@/utils/index'
 const { isNavigationFailure, NavigationFailureType } = VueRouter
 
 export default {
@@ -95,6 +132,7 @@ export default {
       appName: settings.title,
       company: settings.company,
       loginForm: {
+        otp: '',
         email: '',
         password: ''
       },
@@ -122,7 +160,14 @@ export default {
       focusFiled: {
         email: false,
         password: false
-      }
+      },
+      showOTP: false,
+      expireEmail: "",
+      showSetPassword: false,
+      showResetSuccess: false,
+      passwordTitle: "",
+      passwordSubTitle: "",
+      newPasswordInfo: {}
     }
   },
   watch: {
@@ -153,6 +198,7 @@ export default {
       return (this.loginForm.password !== "" || this.focusFiled.password)
     }
   },
+  components: {Password, OtpView},
   created() {
     // window.addEventListener('storage', this.afterQRScan)
     const user = getEmail()
@@ -189,45 +235,111 @@ export default {
         this.$refs.password.focus()
       })
     },
-    handleLogin() {
+    handleShowLogin() {
+      this.showOTP = false;
+      this.showSetPassword = false;
+      this.showResetSuccess = false;
+      this.expireEmail = "";
+      this.passwordTitle = "";
+      this.passwordSubTitle = "";
+      this.newPasswordInfo = {};
+    },
+    handleResetSuccess() {
+      this.handleShowLogin();
+      this.showResetSuccess = true;
+      this.passwordTitle = "Successful. Your password has been changed.";
+      this.passwordSubTitle = parseTime(new Date(), "{d}/{m}/{y}");
+    },
+    handleForgotPassword() {
+      this.handleShowLogin();
+      this.showSetPassword = true;
+    },
+    handleResetPassword(isFirst) {
+      this.handleShowLogin();
+      this.passwordTitle = isFirst ? "Please set your password." : "Password Expired.";
+      this.passwordSubTitle = isFirst ? "" : "Please create new password";
+      this.expireEmail = this.loginForm.email;
+      this.showSetPassword = true;
+    },
+    onResetPasswordConfirm(info) {
+      //reset 忘记密码
+      //create 首次登录、密码过期
+      this.loginForm.password = "";
+      this.newPasswordInfo = info;
+      this.showOTP = true;
+    },
+    onPreLogin() {
       this.$refs.loginForm.validate(valid => {
         if (valid) {
           this.loading = true
-          this.$store.dispatch('user/login', this.loginForm).then((response) => {
-            let path = this.redirect || '/'
-            if (response.data.roleName === "MCST") {
-              path = '/site-management'
-            } else if (response.data.resources.length > 0) {
-              let validPath = false;
-              for (let _route of response.data.resources) {
-                if (path.indexOf(_route.resourcePath) >= 0) {
-                  validPath = true;
-                  break
-                }
-              }
-              if (!validPath) {
-                path =  response.data.resources[0].resourcePath;
+          api.preLogin(this.loginForm).then(res => {
+            if (res.code) {
+              switch(res.code) {
+                case 2016: //未启用二次登录验证
+                  this.handleLogin("");
+                  break;
+                case 2017: //首次登录流程
+                  this.handleResetPassword(true);
+                  break;
+                case 2018: //密码过期流程
+                  this.handleResetPassword(false);
+                  break;
+                case 2019: //OTP 登录流程
+                  this.showOTP = true;
+                  break;
+                default:
+                  this.handleLogin("");
+                  break;
               }
             }
-            this.$router.push({
-              path: path,
-              query: this.otherQuery,
-            }).catch((failure) => {
-              if (!isNavigationFailure(failure, NavigationFailureType.redirected)) {
-                throw failure
-              }
+          }).catch(err => {
+            this.$message({
+              type: 'error',
+              message: err
             })
-            this.loading = false
-          }).catch((error) => {
-            Message.error(error.message)
-            this.loading = false
-          });
+          }).finally(() => {
+            this.loading = false;
+          })
         } else {
           console.log('error submit!!')
-          return false
         }
       })
     },
+    handleLogin(otp) {
+      if (otp) {
+        this.loginForm.otp = otp;
+      }
+      this.$store.dispatch('user/login', this.loginForm).then((response) => {
+        let path = this.redirect || '/'
+        if (response.data.roleName === "MCST") {
+          path = '/site-management'
+        } else if (response.data.resources.length > 0) {
+          let validPath = false;
+          for (let _route of response.data.resources) {
+            if (path.indexOf(_route.resourcePath) >= 0) {
+              validPath = true;
+              break
+            }
+          }
+          if (!validPath) {
+            path =  response.data.resources[0].resourcePath;
+          }
+        }
+        this.$router.push({
+          path: path,
+          query: this.otherQuery,
+        }).catch((failure) => {
+          if (!isNavigationFailure(failure, NavigationFailureType.redirected)) {
+            throw failure
+          }
+        })
+        this.loading = false
+      }).catch((error) => {
+        Message.error(error.message)
+        this.loading = false
+        this.handleShowLogin();
+      });
+    },
     getOtherQuery(query) {
       return Object.keys(query).reduce((acc, cur) => {
         if (cur !== 'redirect') {
@@ -256,12 +368,13 @@ export default {
     width: 100%;
     z-index: 2;
     margin: 0 auto;
-    max-width: 420px;
+    max-width: 450px;
     overflow: hidden;
     background: #fff;
     position: relative;
     border-radius: 3px;
     padding: 30px 40px 50px;
+    transition: all .3s;
     box-shadow: 1px 1px 5px 3px rgba(0,0,0,.2);
   }
   .title-container {
@@ -270,14 +383,23 @@ export default {
     padding: 20px 0 0;
   
     .title {
-      font-size: 18px;
+      font-size: 16px;
       color: #333;
       margin: 20px 0;
       text-align: center;
       font-weight: bold;
     }
+    .sub-title {
+      font-size: 16px;
+      color: #333;
+      margin-top: -10px;
+      padding-bottom: 5px;
+      text-align: center;
+    }
     .csms-logo {
       max-width: 200px;
+      height: 70px;
+      object-fit: contain;
     }
   }
   
@@ -343,7 +465,14 @@ export default {
     color: #fff;
     padding: 15px;
     margin-top: 40px;
-    margin-bottom: 30px;
+    margin-bottom: 20px;
+    font-weight: bold;
+  }
+  
+  .forgot-button {
+    width:100%;
+    padding: 15px;
+    margin: 0 0 20px;
     font-weight: bold;
   }
   

+ 131 - 0
Strides-Admin/src/views/login/otp.vue

@@ -0,0 +1,131 @@
+<template>
+  <div style="padding: 20px 20px 50px;">
+    <el-input
+      v-model="otp"
+      maxlength="6"
+      class="center"/>
+    <div>
+      <el-button
+        :loading="loadingV"
+        type="primary"
+        class="login-button"
+        @click="onVerify">
+        VERIFY
+      </el-button>
+      <el-button
+        :loading="loading"
+        type="content"
+        class="forgot-button"
+        @click="sendOTP">
+        RESEND OTP
+      </el-button>
+    </div>
+  </div>
+</template>
+
+<script>
+  import api from '@/http/api/user'
+  export default {
+    name: "",
+    props: {
+      isReset: {
+        type: Boolean,
+        default: false
+      },
+      loginInfo: {
+        type: Object,
+        default: {}
+      },
+      resetInfo: {
+        type: Object,
+        default: {}
+      }
+    },
+    data() {
+      return {
+        otp: "",
+        loading: false,
+        loadingV: false
+      };
+    },
+    mounted() {
+      
+    },
+    methods: {
+      sendOTP() {
+        this.loading = true;
+        api.sendOTP({
+          email: this.loginInfo.email,
+          type: this.isReset ? "reset" : "login"
+        }).then(res => {
+          /*if (res.data.resendTime) {
+            
+          }*/
+          this.$message({
+            type: 'success',
+            message: "Send OTP successfully!"
+          })
+        }).catch(err => {
+          this.$message({
+            type: 'error',
+            message: err
+          })
+        }).finally(() => {
+          this.loading = false;
+        })
+      },
+      onVerify() {
+        if (this.otp) {
+          this.loadingV = true;
+          if (this.isReset) {
+            this.onUpdatePassword();
+          } else {
+            this.$emit("onLogin", this.otp)
+          }
+        } else {
+          this.$message({
+            type: 'error',
+            message: "Please input otp"
+          })
+        }
+      },
+      onUpdatePassword() {
+        api.updatePassword({
+          email: this.loginInfo.email,
+          password: this.resetInfo.password,
+          otp: this.otp
+        }).then(res => {
+          this.$emit("onReset", true)
+        }).catch(err => {
+          this.$message({
+            type: 'error',
+            message: err
+          })
+        }).finally(() => {
+          this.loadingV = false;
+        })
+      }
+    }
+  }
+</script>
+
+<style scoped>
+  .el-input.center >>> .el-input__inner {
+    text-align: center;
+  }
+  .login-button {
+    width:100%;
+    color: #fff;
+    padding: 15px;
+    margin-top: 40px;
+    margin-bottom: 20px;
+    font-weight: bold;
+  }
+  
+  .forgot-button {
+    width:100%;
+    padding: 15px;
+    margin: 0 0 20px;
+    font-weight: bold;
+  }
+</style>

+ 44 - 4
Strides-Admin/vue.config.js

@@ -105,6 +105,8 @@ module.exports = {
           config
             .optimization.splitChunks({
               chunks: 'all',
+              minSize: 20000,
+              maxInitialRequests: Infinity,
               cacheGroups: {
                 libs: {
                   name: 'chunk-libs',
@@ -117,12 +119,50 @@ module.exports = {
                   priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
                   test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
                 },
+                vue: {
+                  name: 'chunk-vue',
+                  test: /[\\/]node_modules[\\/](vue|vuex|vue-router|veu-loader|axios|screenfull|qrcodejs2)/,
+                  priority: 30
+                },
+                echarts: {
+                  name: 'chunk-echarts',// split echarts libs
+                  test: /[\\/]node_modules[\\/](echarts)/,
+                  priority: 35
+                },
+                googlemaps: {
+                  name: 'chunk-googlemaps',// split others libs
+                  test: /[\\/]node_modules[\\/](clipboardy|core-js|js-base64|js-cookie|nprogress|@googlemaps)/,
+                  priority: 39
+                },
+                router: {
+                  name: 'chunk-router',
+                  test: /[\\/]src[\\/]views[\\/](charging|transaction|user|driver)/,
+                  priority: 40
+                },
                 commons: {
                   name: 'chunk-commons',
-                  test: resolve('src/components'), // can customize your rules
-                  minChunks: 3, //  minimum common number
-                  priority: 5,
-                  reuseExistingChunk: true
+                  test: /[\\/]src[\\/]views[\\/](site|charge|ocpp|login)/, // can customize your rules
+                  priority: 41
+                },
+                vbea: {
+                  name: 'chunk-vbea',
+                  test: /[\\/]src[\\/]views[\\/](limit|feedback|financial|provider)/,
+                  priority: 42
+                },
+                index: {
+                  name: 'chunk-index',
+                  test: /[\\/]src[\\/]views[\\/](dashboard)/,
+                  priority: 44
+                },
+                assets: {
+                  name: 'chunk-assets',
+                  test: /[\\/]src[\\/](assets|icons)/,
+                  priority: 50
+                },
+                utils: {
+                  name: 'chunk-utils',
+                  test: /[\\/]src[\\/](utils|layout|store|router|components|http)/,
+                  priority: 51
                 }
               }
             })