保誠-保戶業務員媒合平台
劉鈞霖
2021-11-24 cfd8c1b9f5acce841d118d951458565d16ba5719
Merge branch 'master' of ssh://dev.pollex.com.tw:29418/pcalife/PAM
修改6個檔案
486 ■■■■ 已變更過的檔案
PAMapp/assets/scss/vendors/elementUI/_button.scss 3 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/ts/api/consultant.ts 49 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/ts/api/share.ts 4 ●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/NavBar.vue 8 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/login/index.vue 421 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myAppointmentList.vue 1 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/scss/vendors/elementUI/_button.scss
@@ -24,6 +24,7 @@
    &:hover{
      background-color: $MID_GREY;
      border-color: $MID_GREY;
      color: $PRIMARY_WHITE;
    }
  }
}
@@ -106,7 +107,7 @@
          border:none;
          font-size: 20px;
      }
  }
  .radio-btn{
      display: flex;
PAMapp/assets/ts/api/consultant.ts
@@ -8,6 +8,21 @@
    return service.post('/authenticate', user)
}
// 顧客登入-發送OTP
export function sendOtp(loginInfo: LoginRequest) {
    return service.post<OtpInfo>('/otp/sendOtp', loginInfo).then(res => res.data)
}
// 顧客登入-驗證otp並登入
export function loginVerify(loginVerify: LoginVerify) {
    return service.post('/otp/verify', loginVerify)
}
// 顧客註冊
export function register(registerInfo: RegisterInfo) {
    return service.post('/otp/register', registerInfo)
}
// 推薦保險顧問
export function recommend() {
    return service.get<Consultants[]>('/consultant/recommend')
@@ -158,3 +173,37 @@
    id_token:string;
}
export interface LoginRequest {
    /** "SMS"=手機,"EMAIL"=email */
    loginType: string,
    /** 若loginType填SMS則該欄帶入手機、EMAIL則帶入郵件信箱 */
    account: string,
}
export interface OtpInfo {
    /** 用於帶入otp認證時 */
    indexKey: string,
    /** Otp是否有成功發送 */
    success: boolean,
    failCode: string,
    failReason: string,
}
export interface LoginVerify {
    /** 可帶入手機或email */
    account: string,
    /** 由otp的api回的index key */
    indexKey: string,
    /** 由手機或信箱收到的認證碼 */
    otpCode: string
}
export interface RegisterInfo {
    phone?: string,
    email?: string,
    indexKey: string,
    otpCode: string,
    name: string,
    /** "SMS":Otp發送手機,"EMAIL":Otp發email */
    contactType: string
}
PAMapp/assets/ts/api/share.ts
@@ -20,7 +20,9 @@
  },
  (error:AxiosError)=>{
    loadingFinish();
    openErrorMessage();
    if (error.config.url !== '/otp/verify') {
      openErrorMessage();
    }
    return Promise.reject(error);
  }
);
PAMapp/components/NavBar.vue
@@ -9,7 +9,7 @@
    <div class="pam-header__action-bar">
      <i class="icon-bell text--dark-blue cursor--pointer fix-chrome-click--issue"
        @click="$router.push('/notification')"></i>
        <el-dropdown :class="{'is-open':isOpenDropdown}"
        <el-dropdown :class="{'is-open':isOpenDropdown}"
          ref="dropdown"
          trigger="click"
          @command="routerNavigateTo">
@@ -36,7 +36,7 @@
  import { namespace } from 'nuxt-property-decorator';
  import { Role } from '~/assets/ts/models/enum/Role';
  import * as _ from 'lodash';
  const roleStorage = namespace('localStorage');
  @Component
  export default class NavBar extends Vue {
@@ -184,7 +184,7 @@
        color: $PRIMARY_WHITE;
      }
    }
  }
  }
  @include desktop {
    .pam-header {
@@ -202,7 +202,7 @@
      .pam-header__title {
        display: flex;
        justify-content: start;
        justify-content: flex-start;
        align-items: center;
        border: none;
        padding-left: 30px;
PAMapp/pages/login/index.vue
@@ -1,104 +1,129 @@
<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
@@ -213,7 +238,22 @@
      </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>
@@ -227,14 +267,27 @@
          </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');
@@ -249,20 +302,33 @@
  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 {
@@ -270,34 +336,141 @@
  };
  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>
@@ -319,8 +492,7 @@
  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;
@@ -328,19 +500,6 @@
  &.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;
  }
}
@@ -396,4 +555,18 @@
  }
}
.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>
PAMapp/pages/myAppointmentList.vue
@@ -45,7 +45,6 @@
     this.storeMyAppointmentList();
     if (this.$route.name) {
         console.log('mounted route')
         this.activeTabName = this.$route.name.split('-')[1]
     }
    }