| | |
| | | <template> |
| | | <div class="pam-login-page"> |
| | | <div class="text--middle">登入</div> |
| | | <div class='mb-30'> |
| | | <div class="mdTxt">登入方式</div> |
| | | <div class="pam-field-title__hint mt-5 mb-10" |
| | | >顧問將會以此{{connectDevice === 'MOBILE' ? '手機號碼' : 'Email'}}為主要預約諮詢聯繫方式</div> |
| | | |
| | | <div class="pam-paragraph"> |
| | | <div class="mdTxt"> |
| | | 驗證方式<small class="pam-field-title__hint pl-10">(顧問會以您指定的方式與您聯繫)</small> |
| | | </div> |
| | | <div class="pam-tags"> |
| | | <el-row type="flex" class="pt-10"> |
| | | <el-button |
| | | :class="{ 'active': connectDevice === 'MOBILE'}" |
| | | @click="connectDevice = 'MOBILE'">手機號碼</el-button> |
| | | <el-button |
| | | :class="{ 'active': connectDevice === 'EMAIL'}" |
| | | @click="connectDevice = 'EMAIL'">Email</el-button> |
| | | </el-row> |
| | | |
| | | </div> |
| | | <el-row type="flex" class="pt-10" v-show="connectDevice === 'MOBILE'"> |
| | | <input |
| | | class="pam-input" |
| | | :class="{ |
| | | 'is-invalid': !phoneNumber |
| | | }" |
| | | v-model="phoneNumber" |
| | | placeholder="請輸入手機號碼" |
| | | > |
| | | </el-row> |
| | | |
| | | <el-row class="pt-10" v-show="connectDevice === 'EMAIL'"> |
| | | <input |
| | | class="pam-input" |
| | | :class="{ |
| | | 'is-invalid': !phoneNumber |
| | | }" |
| | | v-model="email" |
| | | placeholder="請輸入 Email 地址" |
| | | > |
| | | </el-row> |
| | | |
| | | <div class="pt-30" v-show="showPhoneOtpCodeField"> |
| | | <el-row type="flex" justify="space-between"> |
| | | <div class="mdTxt">輸入驗證碼</div> |
| | | <div class="otp-count-timer"> |
| | | 13:50 |
| | | </div> |
| | | <div class="pam-tags"> |
| | | <el-row type="flex" class="pt-30"> |
| | | <el-button |
| | | :class="{ 'active': connectDevice === 'MOBILE'}" |
| | | @click="connectDevice = 'MOBILE'">手機號碼</el-button> |
| | | <el-button |
| | | :class="{ 'active': connectDevice === 'EMAIL'}" |
| | | @click="connectDevice = 'EMAIL'">Email</el-button> |
| | | </el-row> |
| | | </div> |
| | | |
| | | <el-row class="pt-10"> |
| | | <div class="pam-inputs mb-10"> |
| | | <div class="pt-10" v-show="connectDevice === 'MOBILE'"> |
| | | <input |
| | | class="pam-input" |
| | | :class="{ |
| | | 'is-invalid': !phoneValid |
| | | }" |
| | | v-model="phoneNumber" |
| | | placeholder="請輸入手機號碼"> |
| | | <div class="error mt-5 mb-5"> |
| | | <span v-show="!phoneValid">手機號碼格式有誤</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="pt-10" v-show="connectDevice === 'EMAIL'"> |
| | | <input |
| | | class="pam-input" |
| | | :class="{ |
| | | 'is-invalid': !otpCode |
| | | 'is-invalid': !emailValid |
| | | }" |
| | | v-model="otpCode" |
| | | placeholder="請輸入驗證碼" |
| | | v-model="email" |
| | | placeholder="請輸入 Email 地址" |
| | | > |
| | | <div class="error mt-5 mb-5"> |
| | | <span v-show="!emailValid">Email格式有誤</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- mobile 驗證碼 --> |
| | | <template v-if="connectDevice === 'MOBILE'"> |
| | | <div v-show="showPhoneOtpCodeField"> |
| | | <el-row type="flex" justify="space-between"> |
| | | <div class="mdTxt">輸入驗證碼</div> |
| | | <div class="otp-count-timer"> |
| | | {{otpCounter}} |
| | | </div> |
| | | </el-row> |
| | | |
| | | <el-row class="mb-30"> |
| | | <input |
| | | class="pam-input mt-10" |
| | | :class="{ |
| | | 'is-invalid': !otpCode |
| | | }" |
| | | v-model="otpCode" |
| | | placeholder="請輸入驗證碼" |
| | | > |
| | | </el-row> |
| | | |
| | | <el-row> |
| | | <el-button |
| | | :disabled="!phoneNumber || otpResendCounter !== 0 || !phoneValid" |
| | | @click="resentOtp('MOBILE')" |
| | | icon="icon-arrow" |
| | | > |
| | | 重發驗證碼<span class="pam-field-title__hint pl-5">({{ otpResendCounter }})</span> |
| | | </el-button> |
| | | </el-row> |
| | | </div> |
| | | |
| | | <el-row> |
| | | <el-button |
| | | v-if="onPhoneVerifyStep === 'APPLY_OTP'" |
| | | :disabled="!phoneNumber || !phoneValid" |
| | | @click="applyOtpVerification('MOBILE')" |
| | | icon="icon-arrow" |
| | | > |
| | | 發送驗證碼 |
| | | </el-button> |
| | | </el-row> |
| | | |
| | | <el-row class="pt-10"> |
| | | <button |
| | | class="pam-otp-resend-btn" |
| | | :class="{'disabled': true}"> |
| | | <i class="icon-arrow"></i> |
| | | 重發驗證碼({{ otpResendCounter }}) |
| | | </button> |
| | | </el-row> |
| | | </div> |
| | | </template> |
| | | |
| | | <div v-show="showEmailVerifyField"> |
| | | <el-row class="pt-10"> |
| | | <button |
| | | class="pam-otp-resend-btn" |
| | | :class="{'disabled': onEmailVerifyResendStatus === 'CANNOT_RESEND'}"> |
| | | <i class="icon-arrow"></i> |
| | | 重發驗證碼({{ emailResendCounter }}) |
| | | </button> |
| | | <!-- email 驗證碼 --> |
| | | <template v-if="connectDevice === 'EMAIL'"> |
| | | <el-row v-show="showEmailVerifyField"> |
| | | <el-button |
| | | :disabled="!email || emailResendCounter !== 0 || !emailValid" |
| | | icon="icon-arrow" |
| | | @click="resentOtp('EMAIL')" |
| | | > |
| | | 重發驗證碼<span class="pam-field-title__hint pl-5">({{ emailResendCounter }})</span> |
| | | </el-button> |
| | | <div class="mt-10 smTxt_bold text--primary">! 請稍等,新的Email待啟用</div> |
| | | </el-row> |
| | | </div> |
| | | |
| | | <el-row v-show="!showEmailVerifyField"> |
| | | <el-button |
| | | :disabled="!email || !emailValid" |
| | | @click="applyOtpVerification('EMAIL')" |
| | | icon="icon-arrow" |
| | | > |
| | | 發送驗證碼 |
| | | </el-button> |
| | | </el-row> |
| | | </template> |
| | | </div> |
| | | |
| | | <el-row type="flex" justify="center" class="pam-login-page__action-bar"> |
| | | <div v-if="connectDevice === 'MOBILE'"> |
| | | <el-button |
| | | type="primary" |
| | | v-if="onPhoneVerifyStep === 'APPLY_OTP'" |
| | | :disabled="!phoneNumber" |
| | | @click="applyOtpVerification"> |
| | | 發送驗證碼 |
| | | </el-button> |
| | | |
| | | <el-button |
| | | type="primary" |
| | | v-if="connectDevice === 'MOBILE' && onPhoneVerifyStep === 'INPUT_OTP'" |
| | | :disabled="!otpCode" |
| | | @click="registerDialogVisable = true"> |
| | | 送出 |
| | | </el-button> |
| | | </div> |
| | | |
| | | <el-row type="flex" justify="center" class="pam-login-page__action-bar mt-30"> |
| | | <el-button |
| | | type="primary" |
| | | v-if="connectDevice === 'MOBILE' && onPhoneVerifyStep === 'INPUT_OTP'" |
| | | :disabled="!otpCode || !phoneNumber || !phoneValid" |
| | | @click="phoneLogin"> |
| | | 送出 |
| | | </el-button> |
| | | </el-row> |
| | | |
| | | <el-dialog |
| | |
| | | </el-dialog> |
| | | |
| | | <PopUpFrame class="pam-popUpFrame" |
| | | :isOpen.sync="applySuccessConfirmVisable"> |
| | | :isOpen.sync="emailOtpConfirmVisable"> |
| | | <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 pam-paragraph"> |
| | | <div class="text--center"> |
| | | <el-button |
| | | type="primary" |
| | | @click="emailOtpConfirmVisable = false" |
| | | >我知道了</el-button> |
| | | </div> |
| | | </div> |
| | | </PopUpFrame> |
| | | |
| | | <PopUpFrame class="pam-popUpFrame" |
| | | :isOpen.sync="registerSuccessConfirmVisable"> |
| | | <div class="pam-popUp-title text--center"> |
| | | 歡迎您登入成功,如您預約諮詢,顧問會以您留下的{{ connectDevice === 'MOBILE' ? '手機號碼' : 'Email'}}與您聯繫 |
| | | </div> |
| | |
| | | </div> |
| | | </PopUpFrame> |
| | | |
| | | <el-button class="mt-30" @click="fakeLogin">客戶登入</el-button> |
| | | <PopUpFrame class="pam-popUpFrame" |
| | | :isOpen.sync="phoneSuccessConfirmVisable"> |
| | | <div class="pam-popUp-title text--center mb-50" |
| | | >歡迎您登入成功</div> |
| | | <div class="pam-popUp-confirm-bolck pam-paragraph"> |
| | | <div class="text--center"> |
| | | <el-button |
| | | type="primary" |
| | | @click="confirmApplySuccess" |
| | | >我知道了</el-button> |
| | | </div> |
| | | </div> |
| | | </PopUpFrame> |
| | | |
| | | </div> |
| | | </template> |
| | | |
| | | <script lang="ts"> |
| | | import { namespace } from 'nuxt-property-decorator'; |
| | | import { Vue, Component } from 'vue-property-decorator'; |
| | | import { login } from '~/assets/ts/api/consultant'; |
| | | import { LoginRequest, loginVerify, OtpInfo, register, RegisterInfo, sendOtp } from '~/assets/ts/api/consultant'; |
| | | import { Role } from '~/assets/ts/models/enum/Role'; |
| | | |
| | | const roleStorage = namespace('localStorage'); |
| | |
| | | phoneNumber = ''; |
| | | otpCode = ''; |
| | | onPhoneVerifyStep: 'APPLY_OTP' | 'INPUT_OTP' | 'SUBMIT_OTP' = 'APPLY_OTP'; |
| | | otpCounter = '15:00'; |
| | | otpResendCounter = 30; |
| | | otpInterval: any; |
| | | phoneOtpInfo!: OtpInfo; |
| | | |
| | | email = ''; |
| | | onEmailVerifyResendStatus: 'CAN_RESEND' | 'CANNOT_RESEND' = 'CANNOT_RESEND'; |
| | | onEmailVerifyResendStatus: 'APPLY_OTP' | 'CAN_RESEND' = 'APPLY_OTP'; |
| | | emailResendCounter = 30; |
| | | emailResendInterval: any; |
| | | emailOtpInfo!: OtpInfo; |
| | | |
| | | registerDialogVisable = false; |
| | | applySuccessConfirmVisable = false; |
| | | name = ''; |
| | | agreeControct = false; |
| | | isReadContract = false; |
| | | |
| | | phoneSuccessConfirmVisable = false; |
| | | emailOtpConfirmVisable = false; |
| | | |
| | | registerDialogVisable = false; |
| | | registerSuccessConfirmVisable = false; |
| | | |
| | | detectContructReadStatus(event: any): void { |
| | | this.isReadContract = Math.round(event.target.scrollTop) === (event.target.scrollHeight - event.target.clientHeight); |
| | | const scrollTop = Math.round(event.target.scrollTop); |
| | | const height = event.target.scrollHeight - event.target.clientHeight; |
| | | if (Math.floor(scrollTop/10) === (Math.floor(height/10))) { |
| | | this.isReadContract = true; |
| | | } |
| | | }; |
| | | |
| | | get showPhoneOtpCodeField(): boolean { |
| | |
| | | }; |
| | | |
| | | get showEmailVerifyField(): boolean { |
| | | return this.connectDevice === 'EMAIL'; |
| | | return this.connectDevice === 'EMAIL' && this.onEmailVerifyResendStatus !== 'APPLY_OTP'; |
| | | }; |
| | | |
| | | applyOtpVerification(): void { |
| | | this.onPhoneVerifyStep = 'INPUT_OTP'; |
| | | get phoneValid() { |
| | | const rule = /^09[0-9]{8}$/; |
| | | return this.phoneNumber ? rule.test(this.phoneNumber) : true; |
| | | } |
| | | |
| | | get emailValid() { |
| | | const rule = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/; |
| | | return this.email ? rule.test(this.email) : true; |
| | | } |
| | | |
| | | applyOtpVerification(type: string): void { |
| | | const isMobile = this.connectDevice === 'MOBILE'; |
| | | const loginInfo: LoginRequest = { |
| | | loginType: isMobile ? 'SMS' : 'EMAIL', |
| | | account: isMobile ? this.phoneNumber : this.email, |
| | | } |
| | | sendOtp(loginInfo).then(otpInfo => { |
| | | if (otpInfo.success) { |
| | | this.startOtpCount(type, otpInfo); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | resentOtp(type: string) { |
| | | if (type === 'MOBILE') { |
| | | clearInterval(this.otpInterval); |
| | | this.otpResendCounter = 30; |
| | | this.otpCounter = '15:00'; |
| | | this.startPhoneCounter(); |
| | | } else { |
| | | this.emailResendCounter = 30; |
| | | this.startEmailCounter(); |
| | | this.emailOtpConfirmVisable = true; |
| | | } |
| | | } |
| | | |
| | | private startOtpCount(type: string, otpInfo) { |
| | | type === 'MOBILE' ? this.phoneOtpInfo = otpInfo : this.emailOtpInfo = otpInfo; |
| | | if (type === 'MOBILE') { |
| | | this.onPhoneVerifyStep = 'INPUT_OTP'; |
| | | this.startPhoneCounter(); |
| | | } else { |
| | | this.onEmailVerifyResendStatus = 'CAN_RESEND'; |
| | | this.startEmailCounter(); |
| | | this.emailOtpConfirmVisable = true; |
| | | } |
| | | } |
| | | |
| | | private startEmailCounter() { |
| | | this.emailResendInterval = setInterval(() => { |
| | | this.emailResendCounter -= 1; |
| | | if (this.emailResendCounter === 0) { |
| | | clearInterval(this.emailResendInterval) |
| | | } |
| | | }, 1000) |
| | | } |
| | | |
| | | private startPhoneCounter() { |
| | | const minCount = this.otpCounter.split(':'); |
| | | let secCount = (+minCount[0] * 60) + (+minCount[1]); |
| | | let min = 0; |
| | | let sec = 0; |
| | | this.otpInterval = setInterval(() => { |
| | | secCount -= 1; |
| | | min = Math.floor(secCount/60); |
| | | sec = Math.floor(secCount%60); |
| | | this.otpCounter = `${min < 10 ? '0' + min : min}:${sec < 10 ? '0' + sec : sec}`; |
| | | |
| | | if (this.otpResendCounter !== 0) { |
| | | this.otpResendCounter -= 1; |
| | | } |
| | | |
| | | if (secCount === 0) { |
| | | clearInterval(this.otpInterval) |
| | | } |
| | | }, 1000) |
| | | } |
| | | |
| | | private setRegisterInfo(): RegisterInfo { |
| | | return this.connectDevice === 'MOBILE' |
| | | ? { |
| | | phone: this.phoneNumber, |
| | | indexKey: this.phoneOtpInfo.indexKey, |
| | | otpCode: this.otpCode, |
| | | name: this.name, |
| | | contactType: 'SMS' |
| | | } |
| | | : { |
| | | email: this.email, |
| | | indexKey: this.emailOtpInfo.indexKey, |
| | | otpCode: this.otpCode, |
| | | name: this.name, |
| | | contactType: 'EMAIL' |
| | | } |
| | | } |
| | | |
| | | applyAccount(): void { |
| | | this.applySuccessConfirmVisable = true; |
| | | console.log('apply new account!') |
| | | const registerInfo = this.setRegisterInfo(); |
| | | register(registerInfo).then(res => { |
| | | this.storageIdToken(res.data.id_token); |
| | | this.storageRole(Role.USER); |
| | | this.registerSuccessConfirmVisable = true; |
| | | }) |
| | | }; |
| | | |
| | | confirmApplySuccess(): void { |
| | | this.applySuccessConfirmVisable = false |
| | | this.phoneSuccessConfirmVisable = false; |
| | | this.registerSuccessConfirmVisable = false; |
| | | this.$router.go(-1); |
| | | } |
| | | |
| | | // TODO: 僅OTP認證開發前 暫時使用 |
| | | fakeLogin() { |
| | | const user = { |
| | | username: 'user', |
| | | password: 'user', |
| | | } |
| | | login(user).then((res) => { |
| | | this.storageIdToken(res.data.id_token); |
| | | this.storageRole(Role.USER); |
| | | this.$router.go(-1); |
| | | }) |
| | | }; |
| | | phoneLogin() { |
| | | const login = { |
| | | account: this.phoneNumber, |
| | | indexKey: this.phoneOtpInfo.indexKey, |
| | | otpCode: this.otpCode |
| | | } |
| | | loginVerify(login).then(res => { |
| | | this.storageIdToken(res.data.id_token); |
| | | this.storageRole(Role.USER); |
| | | this.phoneSuccessConfirmVisable = true; |
| | | }).catch(error => { |
| | | if (error.response.status === 401) { |
| | | this.registerDialogVisable = true; |
| | | } |
| | | }) |
| | | } |
| | | |
| | | destroyed() { |
| | | clearInterval(this.otpInterval); |
| | | clearInterval(this.emailResendInterval); |
| | | } |
| | | |
| | | } |
| | | </script> |
| | |
| | | width: calc(100% - 36px); |
| | | border-radius: 10px !important; |
| | | padding: 12px 18px !important; |
| | | border-width: 1px; |
| | | outline: none; |
| | | border:1px solid #CCCCCC; |
| | | @extend .text--middle; |
| | | &::placeholder { |
| | | color: $PRUDENTIAL_GREY; |
| | |
| | | &.is-invalid { |
| | | border: 1px solid $PRIMARY_RED !important; |
| | | border-radius: 20px; |
| | | } |
| | | } |
| | | |
| | | .pam-otp-resend-btn { |
| | | background: transparent; |
| | | border: none; |
| | | color: $PRIMARY_RED; |
| | | font-weight: bold; |
| | | i { |
| | | margin-right: 10px; |
| | | } |
| | | &.disabled { |
| | | color: $LIGHT_GREY; |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | .pam-field-title__hint { |
| | | @extend .smTxt_bold; |
| | | color: #68737A; |
| | | } |
| | | |
| | | .error { |
| | | @extend .smTxt_bold; |
| | | @extend .text--primary; |
| | | height: 16px; |
| | | } |
| | | |
| | | .pam-popUp-title { |
| | | line-height: 24px; |
| | | } |
| | | </style> |