From 68a01779c9df1c8b8d68a642204e80c1b3ff896e Mon Sep 17 00:00:00 2001
From: Mila <Mila@pollex.com.tw>
Date: 星期三, 25 十二月 2024 15:05:40 +0800
Subject: [PATCH] feat(顧問登入): 顧問登入頁增加otp驗證流程

---
 PAMapp/pages/consultantLogin/index.vue |  407 +++++++++++++++++++++++++++++++++++++++++++++++-----------
 1 files changed, 329 insertions(+), 78 deletions(-)

diff --git a/PAMapp/pages/consultantLogin/index.vue b/PAMapp/pages/consultantLogin/index.vue
index bda41c9..34f930e 100644
--- a/PAMapp/pages/consultantLogin/index.vue
+++ b/PAMapp/pages/consultantLogin/index.vue
@@ -6,125 +6,343 @@
         <div class="pam-consultant-login__title">撣唾��</div>
         <div class="position-r mt-10">
           <input type="text"
-            v-model="consultantDto.account"
+            v-model="consultantDto.username"
+            :disabled="onOTPVerifyStep === 'CAN_RESEND'"
             class="pam-consultant-login__input"
             placeholder="頛詨eService撣唾��">
-          <div class="pam-consultant-login__inputIcon text--primary cursor--pointer"
-            @click="recordAccount">
-            <i :class="[isRemember ? 'icon-checkbox-1' : 'icon-checkbox','pr-5']"></i>
+          <div v-if="onOTPVerifyStep === 'CAN_RESEND'" class="pam-consultant-login__inputIcon cursor--pointer fix-chrome-click--issue"
+            @click="deleteOtpInfo()">
+            <i class="icon-close"></i>
+          </div>
+          <div v-if="onOTPVerifyStep !== 'CAN_RESEND'" class="pam-consultant-login__inputIcon text--primary cursor--pointer fix-chrome-click--issue"
+            @click="isRememberChange">
+            <i :class="[isRememberUserName ? 'icon-checkbox-1' : 'icon-checkbox','pr-5']"></i>
             閮��
           </div>
         </div>
       </div>
       <div class="pam-paragraph">
-        <div class="pam-consultant-login__title ">
-          <div>撖Ⅳ</div>
-          <a class="pam-consultant-login__forgot-password cursor--pointer" 
-            :href="forgotPasswordLink" 
-            target="_blank" 
-            rel="靽�犖憯�">
-            敹��Ⅳ嚗�
-          </a>
+        <div class="password-Txt">
+          <div class="pam-consultant-login__title ">撖Ⅳ</div>
+          <div class="password-reset" @click="resetPassword">敹��Ⅳ</div>
         </div>
         <div class="position-r mt-10">
-          <input :type="[isShowPassword ? 'text' : 'password']"
+          <input :type="checkPasswordType ? 'text' : 'password'"
             v-model="consultantDto.password"
+            :disabled="onOTPVerifyStep === 'CAN_RESEND'"
             class="pam-consultant-login__input"
             placeholder="頛詨eService撖Ⅳ">
-          <div class="pam-consultant-login__inputIcon cursor--pointer"
+          <div v-if="onOTPVerifyStep !== 'CAN_RESEND'" class="pam-consultant-login__inputIcon cursor--pointer fix-chrome-click--issue"
             @click="isShowPassword = !isShowPassword">
-            <i :class="[isShowPassword ? 'icon-eye-1 fs-25' : 'icon-eye' , 'text--primary']"></i>
+            <i :class="[isShowPassword ? 'icon-eye':'icon-eye-1 fs-25', 'text--primary']"></i>
           </div>
         </div>
       </div>
-      <div class="pam-paragraph">
+      <div class="pam-paragraph" v-if="onOTPVerifyStep === 'APPLY_OTP'">
         <div class="pam-consultant-login__title">
-          <div>撽�Ⅳ</div>
-          <div class="text--dark-blue fs-16 cursor--pointer"
-            @click="regenerateCode">������</div>
+          <div>撽�Ⅳ <span class="text--dark-blue fs-16">(����之撠神)</span></div>
+          <div class="text--primary fs-16 cursor--pointer fix-chrome-click--issue"
+            @click="regenerateImgOfVerification">������</div>
         </div>
         <div class="pam-consultant-login__verifyBlock mt-10">
           <div class="w-55">
             <input type="text"
-              v-model="consultantDto.verificationCode"
+              v-model="verificationCode"
+              maxlength="4"
               class="pam-consultant-login__input">
           </div>
           <div class="pam-consultant-login__verifyImg">
-            <img src="~/assets/images/logo.png" alt="撽�Ⅳ">
+            <img :src="imgSrc" alt="撽�Ⅳ">
           </div>
         </div>
       </div>
-      <div class="pam-consultant-login__confirmBlock pam-paragraph">
-        <button class="pam-consultant-login__confirm cursor--pointer"
-          @click="fakeLogin">�</button>
+
+      <!--  -->
+      <div v-show="onOTPVerifyStep === 'CAN_RESEND'">
+        <el-row type="flex" justify="space-between">
+            <div class="mdTxt">頛詨 OTP 撽�Ⅳ</div>
+            <div class="otp-count-timer">
+              {{counterTime(otpCounterSec)}}
+            </div>
+        </el-row>
+
+        <el-row>
+          <input
+            class="pam-consultant-login__input mt-10"
+            :class="{
+              'is-invalid': !otpCode
+            }"
+            v-model="otpCode"
+            placeholder="隢撓� OTP 撽�Ⅳ"
+            >
+        </el-row>
+        <div class="error mt-5 mb-10">
+            <span v-show="otpCounterSec === 0">OTP 撽�Ⅳ撌脤����� OTP 撽�Ⅳ</span>
+        </div>
+
+        <el-row>
+          <el-button
+            :disabled="!consultantDto.password || otpResendCounter !== 0 || !consultantDto.username"
+            @click="resetOtpSetting()"
+            icon="icon-arrow"
+          >
+            �� OTP 撽�Ⅳ<span
+              class="pam-field-title__hint pl-5"
+              v-if="otpResendCounter !== 0"
+            >({{ otpResendCounter }})</span>
+          </el-button>
+
+        </el-row>
       </div>
+      <div class="pam-paragraph">
+        <el-button v-if="onOTPVerifyStep === 'APPLY_OTP'" icon="icon-arrow"
+          :disabled="!consultantDto.username || !consultantDto.password || verificationCode.length !== 4"
+            @click="applyOtpVerification">
+            ��� OTP 撽�Ⅳ</el-button>
+      </div>
+       <div class="pam-consultant-login__confirmBlock pam-paragraph"  v-if="onOTPVerifyStep === 'CAN_RESEND'">
+        <button class="pam-consultant-login__confirm cursor--pointer fix-chrome-click--issue"
+          :disabled="!consultantDto.username || !consultantDto.password || !otpCode || !otpCounterSec"
+          @click="sendInfo">�</button>
+        </div>
     </div>
+
+    <PopUpFrame class="pam-popUpFrame"
+        :isOpen.sync="otpConfirmVisible"
+      >
+        <div class="pam-popUp-title text--center">撌脣������</div>
+        <div class="pam-popUp-title text--center">{{'email'}}</div>
+        <div class="pam-popUp-title text--center">隢��摮隞嗡蒂摰������</div>
+        <div class="pam-popUp-confirm-bolck mt-30">
+          <div class="text--center">
+            <el-button
+                type="primary"
+                @click="otpConfirmVisible = false"
+            >������</el-button>
+          </div>
+        </div>
+      </PopUpFrame>
   </div>
 </template>
 
 <script lang="ts">
-  import { Vue, Component} from 'vue-property-decorator';
-  import { getForgotPasswordLink , getVerificationCodeImg , login } from '~/assets/ts/api/consultant';
-  import { Role } from '../../components/NavBar.vue';
+  import { Vue, Component , namespace, Watch } from 'nuxt-property-decorator';
+  import { AxiosError } from 'axios';
+  import { Role } from '~/shared/models/enum/Role';
+  import messageBoxService from '~/shared/services/message-box.service';
+  import loginService from '~/shared/services/login.service'
+  import { AgentInfo } from '~/shared/models/agent-info.model';
+
+  const loginStore  = namespace('login.store');
+  const roleStorage = namespace('localStorage');
+
   @Component({
-    layout: 'default'
+    layout: 'home'
   })
   export default class ConsultantLogin extends Vue {
-    isRemember = false;
-    isShowPassword = false;
+    @roleStorage.Mutation
+    storageIdToken!: (token: string) => void;
+
+    @roleStorage.Mutation
+    storageRole!: (role: string) => void;
+
+    @roleStorage.Mutation
+    storageConsultantId!:(id:string) => void;
+
+    @loginStore.Action
+    getLoginConsultantDetail!: (agentNo: string) => Promise<AgentInfo>;
+
     consultantDto = {
-      account: '',
       password: '',
-      verificationCode: '',
-    }
-    forgotPasswordLink = '';
-    imgOfVerificationCode='';
-    mounted() {
-      this.regenerateCode();
-      this.getLinkOfForgotPassword();
+      username: '',
     };
-    private getLinkOfForgotPassword():void{
-      getForgotPasswordLink().then(link=>{
-        console.log('link',link);
-        this.forgotPasswordLink = link;
-      });
-    };
-    recordAccount(): void {
-      this.isRemember = !this.isRemember;
-      if (this.isRemember) {
-        console.log('sotre account');
+    imgSrc = '';
+    isRememberUserName = false;
+    isShowPassword = false;
+    verificationCode='';
+
+    otpConfirmVisible = false;
+    otpCode = '';
+    onOTPVerifyStep: 'APPLY_OTP' | 'CAN_RESEND' = 'APPLY_OTP';
+    otpCounterSec = 50;
+    otpResendCounter = 30;
+    otpInterval: NodeJS.Timeout | null = null;
+    otpIndexKey!: string;
+
+    @Watch('onOTPVerifyStep')
+    onOTPVerifyStepChange() {
+      if (this.onOTPVerifyStep === 'APPLY_OTP') {
+        this.regenerateImgOfVerification();
       }
+    }
+
+    ////////////////////////////////////////////////////////////////////
+
+    mounted() {
+      this.getInitUserName();
+      this.regenerateImgOfVerification();
     };
-    regenerateCode(): void {
-      getVerificationCodeImg().then((imgOfbase64:any)=>{
-        this.imgOfVerificationCode = imgOfbase64;
+
+    destroyed() {
+      clearInterval(this.otpInterval ?? undefined);
+    }
+
+    private getInitUserName(): void {
+      const username = localStorage.getItem('consultantUserName')
+      if (username) {
+        this.consultantDto.username = username;
+        this.isRememberUserName = true;
+      }
+    }
+
+    ////////////////////////////////////////////////////////////////////
+
+
+    public regenerateImgOfVerification(): void {
+      loginService.getImgOfVerification().then( imgOfBase64 =>{
+        this.imgSrc = imgOfBase64;
+        this.verificationCode = '';
       });
     };
-    // loginWithConsultant():void{
-    //   console.log('consultantDto',this.consultantDto);
-    //   logInToConsultant(this.consultantDto).then(res=>{
-    //     localStorage.setItem('roleOfState',Role.CONSULTANT);
-    //     this.$router.push('/myAppointmentList/appointmentList');
-    //   });
-    // }
 
-    fakeLogin(): void {
-        const user = {
-            username: 'admin',
-            password: 'admin'
-        }
-        console.log('user',user);
-        login(user).then((res) => {
-            localStorage.setItem('id_token', res.data.id_token);
-            localStorage.setItem('roleOfState',Role.ADMIN)
-            this.$router.push('/myAppointmentList/appointmentList');
-        })
+    public isRememberChange():void{
+      this.isRememberUserName = !this.isRememberUserName;
+      this.storeUserName();
     }
-  };
 
+    public applyOtpVerification(type: string): void {
+    // TODO: sendOTP
+        const otpInfo = {
+          indexKey: "string", /** ��撣嗅otp隤��� */
+          success: true, /** Otp�������� */
+          failCode: "string",
+          failReason: "string",
+        }
+        if (otpInfo.success) {
+          // this.storageOtpTime(type, otpInfo);
+          this.startOtpSetting(type);
+          this.startOtpCount();
+        } else {
+          // TODO: otp error
+          // const errorMsg = OtpErrorCode[otpInfo.failCode] ? OtpErrorCode[otpInfo.failCode]:'OTP蝟餌絞�隤�';
+          // messageBoxService.showErrorMessage(errorMsg);
+        }
+    }
+
+    public sendInfo():void{
+      this.isAlreadyDone
+        ? this.verify()
+        : messageBoxService.showErrorMessage('隢Ⅱ隤董����Ⅳ隞亙���Ⅳ��憛怠神摰');
+    }
+
+    get isAlreadyDone():boolean{
+      return !!(this.verificationCode && this.consultantDto.username && this.consultantDto.password);
+    }
+
+    resetPassword() {
+        window.open(process.env.CONSULTANT_FORGET_PASSWORD_URL);
+    }
+
+    resetOtpSetting() {
+      clearInterval(this.otpInterval ?? undefined);
+      this.otpResendCounter = 30;
+      this.otpCounterSec = 50;
+      this.onOTPVerifyStep = 'APPLY_OTP';
+      this.otpCode = '';
+  }
+
+    counterTime(counterSec) {
+      let min = Math.floor(counterSec / 60);
+      let sec = Math.floor(counterSec % 60);
+      return `${min < 10 ? '0' + min : min}:${sec < 10 ? '0' + sec : sec}`;
+    }
+
+    deleteOtpInfo() {
+      this.resetOtpSetting();
+      this.onOTPVerifyStep = 'APPLY_OTP';
+      this.consultantDto.password = '';
+      this.consultantDto.username = '';
+      this.otpCode = '';
+    }
+
+    get checkPasswordType(): boolean {
+      return this.onOTPVerifyStep === 'CAN_RESEND' ? false : this.isShowPassword;
+    }
+    ////////////////////////////////////////////////////////////////////
+
+    private verify():void{
+      loginService.getVerificationStatus(this.verificationCode).then( verifySuccess => {
+        if(verifySuccess.data){
+          this.loginWithConsultant();
+        }else{
+          this.clearValue();
+          this.regenerateImgOfVerification();
+          messageBoxService.showErrorMessage('撽�Ⅳ頛詨�隤�');
+        }
+      });
+    }
+
+    private loginWithConsultant(): void {
+      loginService.logInToConsultant(this.consultantDto, this.verificationCode).then(res => {
+        this.getLoginConsultantDetail(this.consultantDto.username);
+        this.storageIdToken(res.data.id_token);
+        this.storageRole(Role.ADMIN);
+        this.storageConsultantId(this.consultantDto.username)
+        this.storeUserName();
+        this.$router.push('/myAppointmentList/appointmentList');
+      }).catch((error:AxiosError)=>{
+        this.checkHttpErrorStatus(error);
+      });
+    }
+    private checkHttpErrorStatus(error:any):void{
+      this.clearValue();
+      this.regenerateImgOfVerification();
+      switch (error.response.status) {
+        case 401:
+          const errorMsg = error.response.data.detail;
+          messageBoxService.showErrorMessage(errorMsg);
+          break;
+
+        default:
+          const defaultErrorMsg = error.response.data.title
+          messageBoxService.showErrorMessage('',defaultErrorMsg);
+          break;
+      }
+    }
+
+    private storeUserName(): void {
+      localStorage.setItem('consultantUserName', this.isRememberUserName ? this.consultantDto.username : '');
+    };
+
+    private clearValue():void{
+      if (!this.isRememberUserName) this.consultantDto.username='';
+      this.consultantDto.password = '';
+      this.verificationCode = '';
+    }
+
+  private startOtpSetting(type: string) {
+    this.onOTPVerifyStep = 'CAN_RESEND';
+    this.otpConfirmVisible = true;
+  }
+
+  private startOtpCount() {
+    this.otpInterval = setInterval(() => {
+      this.otpCounterSec -= 1;
+      if (this.otpResendCounter !== 0) {
+        this.otpResendCounter -= 1;
+        if (this.otpResendCounter === 0) {
+          // this.regenerateImgOfVerification();
+        }
+      }
+      if (this.otpCounterSec === 0 && this.otpInterval) {
+        clearInterval(this.otpInterval);
+      }
+    }, 1000)
+  }
+};
 </script>
-<style lang="scss"
-  scoped>
+
+<style lang="scss" scoped>
   .mt-20 {
     margin-top: 20px;
   }
@@ -139,6 +357,20 @@
 
   .position-r {
     position: relative;
+  }
+  .password-Txt {
+    display: flex;
+    justify-content: space-between;
+    align-items: flex-end;
+  }
+  .password-reset {
+    font-size: 16px ;
+    cursor: pointer;
+  }
+
+  .pam-popUp-title {
+      font-size: 20px;
+      line-height: 27px;
   }
 
   .pam-consultant-login {
@@ -182,11 +414,13 @@
         right: 15px;
       }
     }
-    &__forgot-password{
-        color: $PRIMARY_RED;
-        text-decoration:none;
-        font-size: 16px; 
+
+    &__forgot-password {
+      color: $PRIMARY_RED;
+      text-decoration: none;
+      font-size: 16px;
     }
+
     &__verifyBlock {
       display: flex;
       justify-content: space-between;
@@ -194,9 +428,9 @@
 
     &__verifyImg {
       width: 126px;
-      border: 1px black solid;
       height: 50px;
-      img{
+      border:1px #cccccc solid;
+      img {
         width: 100%;
         height: 100%;
       }
@@ -214,7 +448,24 @@
       border-radius: 30px;
       border: 1px solid $LIGHT_GREY;
       background-color: $PRIMARY_RED;
+      font-weight: 700;
+
+      &:disabled {
+        color: $PRIMARY_WHITE;
+        background-color: $MID_GREY;
+        border-color: $MID_GREY;
+      }
     }
   }
 
+  .pam-field-title__hint {
+    @extend .smTxt_bold;
+    color: #68737A;
+  }
+
+  .error {
+    @extend .smTxt_bold;
+    @extend .text--primary;
+    height: 16px;
+  }
 </style>

--
Gitblit v1.8.0