From 078cdb2b41d1dec47e2d981c2d2e618d12beddb4 Mon Sep 17 00:00:00 2001 From: Mila <Mila@pollex.com.tw> Date: 星期四, 26 十二月 2024 09:43:23 +0800 Subject: [PATCH] feat(顧問登入): 串接 otp 發送/驗證 api --- PAMapp/pages/consultantLogin/index.vue | 465 +++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 files changed, 394 insertions(+), 71 deletions(-) diff --git a/PAMapp/pages/consultantLogin/index.vue b/PAMapp/pages/consultantLogin/index.vue index c131841..e7ee471 100644 --- a/PAMapp/pages/consultantLogin/index.vue +++ b/PAMapp/pages/consultantLogin/index.vue @@ -1,118 +1,409 @@ <template> - <div> - <div class="pam-consultant-login"> - <div class="pam-consultant-login__header mt-30">憿批��</div> - <div class="pam-consultant-login__title mt-30">撣唾��</div> + <div> + <div class="pam-consultant-login"> + <div class="pam-consultant-login__header pam-paragraph">憿批��</div> + <div class="pam-paragraph"> + <div class="pam-consultant-login__title">撣唾��</div> <div class="position-r mt-10"> <input type="text" - :model="loginDto.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="cookieAccount"> - <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 class="pam-consultant-login__title mt-30"> - <div>撖Ⅳ</div> - <div class="text--primary fs-16 cursor--pointer" @click="forgetPassword">敹��Ⅳ嚗�</div> - </div> + </div> + <div class="pam-paragraph"> + <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']" - :model="loginDto.password" + <input :type="checkInputType ? '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" @click="isShowPassword = !isShowPassword"> - <i :class="[isShowPassword ? 'icon-eye-1 fs-25' : 'icon-eye' , 'text--primary']"></i> + <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':'icon-eye-1 fs-25', 'text--primary']"></i> </div> </div> - <div class="pam-consultant-login__title mt-30"> - <div>撽�Ⅳ</div> - <div class="text--dark-blue fs-16 cursor--pointer" @click="regenerateCode">������</div> - </div> + </div> + <div class="pam-paragraph" v-if="onOTPVerifyStep === 'APPLY_OTP'"> + <div class="pam-consultant-login__title"> + <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" - :modal="loginDto.verificationCode" - class="pam-consultant-login__input" > + v-model="verificationCode" + maxlength="4" + class="pam-consultant-login__input"> </div> - <div class="pam-consultant-login__verifyImg"></div> - </div> - <div class="pam-consultant-login__confirmBlock mt-30"> - <button class="pam-consultant-login__confirm cursor--pointer" @click="login">�</button> + <div class="pam-consultant-login__verifyImg"> + <img :src="imgSrc" alt="撽�Ⅳ"> + </div> </div> </div> + + <!-- OTP 撽�Ⅳ --> + <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-consultant-login__confirmBlock pam-paragraph"> + <button class="pam-consultant-login__confirm cursor--pointer fix-chrome-click--issue" + :disabled="isSentBtnDisabled" + @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">蝪∟��mail</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 { 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'; +import { OtpErrorCode } from '~/shared/models/enum/otpErrorCode'; -@Component({ - layout:'home' -}) -export default class ConsultantLogin extends Vue { - isRemember=false; - isShowPassword=false; - loginDto={ - account:'', - password:'', - verificationCode:'', - } + const loginStore = namespace('login.store'); + const roleStorage = namespace('localStorage'); + @Component({ + layout: 'home' + }) + export default class ConsultantLogin extends Vue { + @roleStorage.Mutation + storageIdToken!: (token: string) => void; - login():void{ - console.log('login'); - } + @roleStorage.Mutation + storageRole!: (role: string) => void; - cookieAccount():void{ - this.isRemember = !this.isRemember; - if(this.isRemember){ - console.log('sotre account'); + @roleStorage.Mutation + storageConsultantId!:(id:string) => void; + + @loginStore.Action + getLoginConsultantDetail!: (agentNo: string) => Promise<AgentInfo>; + + consultantDto = { + password: '', + username: '', + }; + imgSrc = ''; + isRememberUserName = false; + isShowPassword = false; + verificationCode=''; + + otpConfirmVisible = false; + otpCode = ''; + onOTPVerifyStep: 'APPLY_OTP' | 'CAN_RESEND' = 'APPLY_OTP'; + otpCounterSec = 300; + otpResendCounter = 30; + otpInterval: NodeJS.Timeout | null = null; + otpIndexKey!: string; + + @Watch('onOTPVerifyStep') + onOTPVerifyStepChange() { + if (this.onOTPVerifyStep === 'APPLY_OTP') { + this.regenerateImgOfVerification(); + } } + + //////////////////////////////////////////////////////////////////// + + mounted() { + this.getInitUserName(); + this.regenerateImgOfVerification(); + }; + + 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 = ''; + }); + }; + + public isRememberChange():void{ + this.isRememberUserName = !this.isRememberUserName; + this.storeUserName(); + } + + public sendInfo():void{ + if (this.onOTPVerifyStep === 'APPLY_OTP') { + this.isAlreadyDone + ? this.verify() + : messageBoxService.showErrorMessage('隢Ⅱ隤董����Ⅳ隞亙���Ⅳ��憛怠神摰'); + } else { + this.login(); + } + } + + resetPassword() { + window.open(process.env.CONSULTANT_FORGET_PASSWORD_URL); + } + + resetOtpSetting() { + clearInterval(this.otpInterval ?? undefined); + this.otpResendCounter = 30; + this.otpCounterSec = 300; + this.onOTPVerifyStep = 'APPLY_OTP'; + this.otpCode = ''; } - forgetPassword():void{ - console.log('forget password'); - } + 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}`; + } - regenerateCode():void{ - console.log('call api regenerate verificationCode') - } -} + deleteOtpInfo() { + this.resetOtpSetting(); + this.onOTPVerifyStep = 'APPLY_OTP'; + this.consultantDto.password = ''; + this.consultantDto.username = ''; + this.otpCode = ''; + } + + get isAlreadyDone():boolean{ + return !!(this.verificationCode && this.consultantDto.username && this.consultantDto.password); + } + + get checkInputType(): boolean { + return this.onOTPVerifyStep === 'CAN_RESEND' ? false : this.isShowPassword; + } + + get isSentBtnDisabled(): boolean { + if (this.onOTPVerifyStep === 'APPLY_OTP') { + return !this.consultantDto.username || !this.consultantDto.password || this.verificationCode.length !== 4; + } else { + return !this.consultantDto.username || !this.consultantDto.password || !this.otpCode || !this.otpCounterSec; + } + } + + //////////////////////////////////////////////////////////////////// + + 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.applyOtpVerification(); + }).catch((error:AxiosError)=>{ + this.checkHttpErrorStatus(error); + }); + } + + private checkHttpErrorStatus(error:any):void{ + this.clearValue(); + this.onOTPVerifyStep = 'APPLY_OTP'; + 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 applyOtpVerification(): void { + loginService.sentOtpWithConsultant(this.consultantDto.username).then(otpInfo => { + if (otpInfo.success) { + this.otpIndexKey = otpInfo.indexKey; + this.startOtpSetting(); + this.startOtpCount(); + } else { + const errorMsg = OtpErrorCode[otpInfo.failCode] ? OtpErrorCode[otpInfo.failCode]:'OTP蝟餌絞�隤�'; + messageBoxService.showErrorMessage(errorMsg); + } + }) + } + + private startOtpSetting() { + this.onOTPVerifyStep = 'CAN_RESEND'; + this.otpConfirmVisible = true; + } + + private startOtpCount() { + this.otpInterval = setInterval(() => { + this.otpCounterSec -= 1; + if (this.otpResendCounter !== 0) { + this.otpResendCounter -= 1; + } + if (this.otpCounterSec === 0 && this.otpInterval) { + clearInterval(this.otpInterval); + } + }, 1000) + } + + private login() { + const loginVerify = { + account: this.consultantDto.username, + indexKey: this.otpIndexKey, + otpCode: this.otpCode + } + loginService.loginVerifyWithConsultant(loginVerify).then(res => { + this.getLoginConsultantDetail(this.consultantDto.username); + this.storageIdToken(res.id_token); + this.storageRole(Role.ADMIN); + this.storageConsultantId(this.consultantDto.username) + this.storeUserName(); + this.$router.push('/myAppointmentList/appointmentList'); + }) + } + }; </script> + <style lang="scss" scoped> - .mt-20{ + .mt-20 { margin-top: 20px; } - .mt-25{ + + .mt-25 { margin-top: 25px; } - .w-55{ + + .w-55 { width: 55% !important; } - .position-r{ + + .position-r { position: relative; } - .pam-consultant-login{ + .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 { + margin: auto; width: 336px; - margin: 40px auto 30px auto; font-size: 20px; color: $PRIMARY_BLACK; - &__header{ + + &__header { text-align: center; font-size: 24px; font-weight: bold; letter-spacing: 1.2; color: $PRIMARY_BLACK; } - &__title{ - display:flex; + + &__title { + display: flex; justify-content: space-between; align-items: center; - padding: 0px 10px; } - &__input{ + + &__input { width: 100%; outline: 0; border: 1px solid #CCCCCC; @@ -125,7 +416,7 @@ -webkit-box-sizing: border-box; -moz-box-sizing: border-box; - &Icon{ + &Icon { position: absolute; display: flex; align-items: center; @@ -133,26 +424,58 @@ right: 15px; } } - &__verifyBlock{ + + &__forgot-password { + color: $PRIMARY_RED; + text-decoration: none; + font-size: 16px; + } + + &__verifyBlock { display: flex; justify-content: space-between; } - &__verifyImg{ - width:126px; - border:1px black solid; + + &__verifyImg { + width: 126px; + height: 50px; + border:1px #cccccc solid; + img { + width: 100%; + height: 100%; + } } - &__confirmBlock{ + + &__confirmBlock { display: flex; justify-content: center; } - &__confirm{ + + &__confirm { color: $PRIMARY_WHITE; width: 80px; height: 50px; border-radius: 30px; border: 1px solid $LIGHT_GREY; - background-color:$PRIMARY_RED; + 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