保誠-保戶業務員媒合平台
Tomas
2021-12-08 5730601f103ea285d129bf3d89acd649e86c114a
separate vue files
刪除10個檔案
修改15個檔案
新增43個檔案
修改4個檔案名稱
6030 ■■■■ 已變更過的檔案
PAMapp/assets/scss/utilities/_utilities.scss 12 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/ts/api/consultant.ts 8 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/ts/models/agentInfo.model.ts 19 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/ts/models/enum/gender.enum.ts 2 ●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/ts/models/enum/role.enum.ts 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/ts/models/fast-query-params.model.ts 6 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/ts/models/question-option.model.ts 12 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/ts/models/reviews-item.model.ts 5 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/ts/models/selected.model.ts 4 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/ts/models/strict-query-dto.model.ts 11 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/BackActionBar.vue 2 ●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/NavBar.vue 9 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/QuickFilter/QuickFilterSelector.vue 11 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/accountSetting/account-setting.component.scss 118 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/accountSetting/account-setting.component.ts 98 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/accountSetting/index.vue 255 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/agentInfo/_agentNo.vue 124 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/agentInfo/agent-info.component.scss 48 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/agentInfo/agent-info.component.ts 54 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/consultantLogin/consultant-login.component.scss 77 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/consultantLogin/consultant-login.component.ts 103 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/consultantLogin/index.vue 202 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/login/index.vue 474 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/login/login.component.scss 97 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/login/login.component.ts 373 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myAppointmentList.vue 133 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myAppointmentList/appointmentList/appointment-list.component.scss 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myAppointmentList/appointmentList/appointment-list.component.ts 26 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myAppointmentList/appointmentList/index.vue 29 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myAppointmentList/contactedList.vue 64 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myAppointmentList/contactedList/contacted-list.component.scss 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myAppointmentList/contactedList/contacted-list.component.ts 34 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myAppointmentList/contactedList/index.vue 33 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myAppointmentList/index.vue 45 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myAppointmentList/my-appointment.component.scss 26 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myAppointmentList/my-appointment.component.ts 63 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myConsultantList/consultantList.vue 30 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myConsultantList/consultantList/consultant-list.component.scss 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myConsultantList/consultantList/consultant-list.component.ts 16 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myConsultantList/consultantList/index.vue 18 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myConsultantList/contactedList.vue 28 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myConsultantList/contactedList/contacted-list.component.scss 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myConsultantList/contactedList/contacted-list.component.ts 13 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myConsultantList/contactedList/contactedList.vue 18 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myConsultantList/index.vue 41 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myConsultantList/my-consultant-list.component.scss 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myConsultantList/my-consultant-list.component.ts 28 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/questionnaire/_agentNo.vue 395 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/questionnaire/questionnaire.component.scss 159 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/questionnaire/questionnaire.component.ts 228 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/quickFilter/index.vue 213 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/quickFilter/quick-filter.component.scss 52 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/quickFilter/quick-filter.component.ts 144 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/recommendConsultant/criteria.vue 3 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/recommendConsultant/index.vue 493 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/recommendConsultant/recommend-consultant.component.scss 291 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/recommendConsultant/recommend-consultant.component.ts 183 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/recommendConsultant/result.vue 233 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/recommendConsultant/result/index.vue 88 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/recommendConsultant/result/recommend-consultant-result.component.scss 114 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/recommendConsultant/result/recommend-consultant-result.component.ts 32 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/record.vue 45 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/record/contactRecord.vue 3 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/record/index.vue 83 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/record/record.component.scss 44 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/record/record.component.ts 23 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/record/reviews.vue 3 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/userReviews/index.vue 171 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/userReviews/user-reviews.component.scss 103 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/userReviews/user-reviews.component.ts 43 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/userReviewsRecord/index.vue 113 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/store/localStorage.ts 2 ●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/scss/utilities/_utilities.scss
@@ -13,6 +13,10 @@
  margin-top: 20px;
}
.mt-25 {
  margin-top: 25px;
}
.mb-30 {
  margin-bottom: 30px;
}
@@ -89,8 +93,16 @@
  padding: 20px 30px 40px 30px;
}
.w-55 {
  width: 55% !important;
}
@for $fontSize from 12 through 45 {
  .fs-#{$fontSize} {
    font-size: #{$fontSize} + 'px';
  }
}
.position-r {
  position: relative;
}
PAMapp/assets/ts/api/consultant.ts
@@ -5,6 +5,7 @@
import _ from 'lodash';
import { UserSetting } from '../models/account.model';
import { Consultant } from '~/assets/ts/models/consultant.model';
import { FastQueryParams } from '../models/fast-query-params.model';
// é¡§å®¢ç™»å…¥(TODO: OTP認證開發前 æš«æ™‚使用)
export function login(user: any) {
@@ -131,13 +132,6 @@
        Authorization: 'Bearer ' + localStorage.getItem('id_token')
    }
    return service.post('/satisfaction/create', data ,{headers});
}
export interface FastQueryParams {
    gender             : string,
    communicationStyles: string[],
    avgScore           : number,
    status             : string
}
export interface AppointmentRequests {
PAMapp/assets/ts/models/agentInfo.model.ts
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,19 @@
export interface AgentInfo {
  name           : string;
  agentNo        : string;
  role           : string;
  image          : string;
  avgScore       : number;
  title          : string;
  phoneNumber    : string;
  serveArea      : string;
  companyAddress : string;
  latestLoginTime: Date | null;
  seniority      : string;
  suitability    : number;
  evaluation     : number;
  expertiseList  : string[];
  concept        : string;
  experiences    : string[];
  awards         : string;
}
PAMapp/assets/ts/models/enum/gender.enum.ts
File was renamed from PAMapp/assets/ts/models/enum/Gender.ts
@@ -1,4 +1,4 @@
export enum Gender{
  MALE="male",
  FEMALE="female",
}
}
PAMapp/assets/ts/models/enum/role.enum.ts
PAMapp/assets/ts/models/fast-query-params.model.ts
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,6 @@
export interface FastQueryParams {
  gender             : string,
  communicationStyles: string[],
  avgScore           : number,
  status             : string
}
PAMapp/assets/ts/models/question-option.model.ts
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,12 @@
export interface QuestionOption {
    title: string;
    detail: Detail[];
    type: string;
    name: string;
}
interface Detail {
    value: string;
    name?: string;
    className: string;
}
PAMapp/assets/ts/models/reviews-item.model.ts
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,5 @@
export interface ReviewsItem {
  avatar  : any;
  name    : string;
  avgScore: number;
}
PAMapp/assets/ts/models/selected.model.ts
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,4 @@
export interface Selected {
  option: string;
  value: any;
}
PAMapp/assets/ts/models/strict-query-dto.model.ts
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,11 @@
export interface StrictQueryDto {
  gender          : string,
  area            : string,
  status          : string,
  requirements    : string[],
  otherRequirement: string,
  seniority       : string,
  avgScore        : number,
  popularTags     : string[],
  otherPopularTags: string
}
PAMapp/components/BackActionBar.vue
@@ -9,7 +9,7 @@
<script lang="ts">
import { namespace, Watch } from 'nuxt-property-decorator';
import { Vue, Component,} from 'vue-property-decorator';
import { Role } from '~/assets/ts/models/enum/Role';
import { Role } from '~/assets/ts/models/enum/role.enum';
import * as _ from 'lodash';
import { isLogin } from '~/assets/ts/auth';
PAMapp/components/NavBar.vue
@@ -34,7 +34,7 @@
<script lang="ts">
  import { Vue, Component } from 'vue-property-decorator';
  import { namespace } from 'nuxt-property-decorator';
  import { Role } from '~/assets/ts/models/enum/Role';
  import { Role } from '~/assets/ts/models/enum/role.enum';
  import * as _ from 'lodash';
  const roleStorage = namespace('localStorage');
@@ -63,13 +63,8 @@
        title: '查看帳號資訊',
      },
      {
        authorityOfRoleList:[Role.ADMIN],
        authorityOfRoleList:[Role.ADMIN, Role.USER],
        routeUrl: '/record',
        title: '查看紀錄',
      },
      {
        authorityOfRoleList: [Role.USER],
        routeUrl: '/userReviewsRecord',
        title: '查看紀錄',
      },
      {
PAMapp/components/QuickFilter/QuickFilterSelector.vue
@@ -65,8 +65,9 @@
<script lang="ts">
import { Vue, Component, Prop, Watch, Emit } from 'nuxt-property-decorator';
import { FastQueryParams } from '~/assets/ts/api/consultant';
import { QuestionOption } from '~/pages/quickFilter/index.vue';
import { FastQueryParams } from '~/assets/ts/models/fast-query-params.model';
import { Selected } from '~/assets/ts/models/selected.model';
import { QuestionOption } from '~/assets/ts/models/question-option.model';
@Component
export default class QuickFilterDrawer extends Vue {
@@ -131,10 +132,6 @@
}
export interface Selected {
    option: string;
    value: any;
}
</script>
<style lang="scss" scoped>
@@ -146,4 +143,4 @@
        flex-wrap: wrap;
    }
</style>
</style>
PAMapp/pages/accountSetting/account-setting.component.scss
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,118 @@
.account-page{
  .block{
      display: flex;
  }
  .account-page-title{
      font-size: 20px;
      margin-bottom: 34px;
  }
  .account-card{
      display: flex;
      flex-direction: column;
      border-bottom: 1px solid gray;
      margin-bottom: 33px;
      .contact-type{
          width: 184px;
          margin-right: 16px;
          font-size: 20px;
          display: flex;
          flex-direction: column;
          align-items: flex-start;
      }
      &.edit {
          input {
              border: 1px solid lightgray;
              background-color: #fff;
          }
      }
  }
  .account-setting-btn{
      display: flex;
      justify-content: center;
  }
}
.error-txt{
  padding-bottom: 10px;
  .error {
      @extend .smTxt_bold;
      @extend .text--primary;
      height: 16px;
      }
}
.name-input{
          width: 184px;
          height:27px;
          margin-bottom: 20px;
          font-size: 20px;
          margin-top: -3px;
      }
.setting-title{
          margin-left: 28px;
          margin-bottom:10px;
          width: 58px;
          font-size: 20px;
      }
.header{
  display: flex;
  align-items: baseline;
}
.contact-input{
  font-size: 20px;
  margin-bottom: 10px;
  text-overflow: ellipsis;
  margin-top: 10px;
  width: 184px;
}
.input{
  border: 0;
  background-color: rgba(0,0,0,0) ;
  outline-color: gainsboro;
}
.input:focus{
  background-color: #fff;
}
.icon-color-change{
  color:$PRIMARY_RED;
  font-size: 20px;
}
.icon{
  font-size:20px;
  // color:#1B365D;
}
@include desktop{
  .header{
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  }
  .setting-title{
          margin-bottom:10px;
          width: 58px;
          font-size: 18px;
          font-weight: bold;
      }
  .account-page{
  .account-page-title{
      font-size: 20px;
      margin-bottom: 34px;
      font-weight: bold;
  }
  }
  .account-card{
      display: flex;
      flex-direction: column;
      border-bottom: 1px solid gray;
      margin-bottom: 33px;
      .contact-type{
          margin-left: 10px;
          font-size: 20px;
      }
  }
  .name-input{
      width: 550px;
  }
  .contact-input{
      width:550px
  }
  }
PAMapp/pages/accountSetting/account-setting.component.ts
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,98 @@
import { Vue,Component } from 'vue-property-decorator'
import { getUserAccountSetting, updateAccountSetting } from '~/assets/ts/api/consultant';
import { UserSetting } from '~/assets/ts/models/account.model';
@Component
export default class AccountSetting extends Vue {
        _userSetting!: UserSetting;
        userNameDisabled = true;
        userPhoneDisabled = true;
        userEmailDisabled = true ;
        userNameValue = '';
        phoneValue = '' ;
        emailValue = '' ;
        onEditMode = false;
        formValidStatus = {
            name: true,
            phone: true,
            email: true,
        };
        get nameValid(): boolean {
            this.formValidStatus.name = this.userNameValue ? true : false;
            return this.formValidStatus.name;
        }
        get phoneValid(): boolean {
            const rule = /^09[0-9]{8}$/;
            this.formValidStatus.phone = this.phoneValue ? rule.test(this.phoneValue) : true;
            return this.formValidStatus.phone;
        }
        get emailValid(): boolean {
            const rule = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/;
            this.formValidStatus.email = this.emailValue ? rule.test(this.emailValue) : true;;
            return this.formValidStatus.email;
        }
        editField(fieldName: string): void {
            this.onEditMode = true;
            const enablePromise = new Promise((resolve, reject) => { // æ­¤ç‚ºpromise語法
                resolve((this as any)[`${fieldName}Disabled`] = false);
            });
            const targetInput = this.$refs[fieldName] as any;
            enablePromise.then((_) => {
                targetInput.focus();
            });
        }
        get isSubmitBtnDisabled(): boolean {
            const isFormValid = this.formValidStatus.name && this.formValidStatus.phone && this.formValidStatus.email;
            return !isFormValid || !this.onEditMode
                || (!this.phoneValue && !this.emailValue);
        }
        updateAccountSetting(): void {
            // const dataChanged = (): boolean => {
            //     return this._userSetting.name !== this.userNameValue
            //         || this._userSetting.phone !== this.phoneValue
            //         || this._userSetting.email !== this.emailValue;
            // };
            // if (dataChanged) {
            // }
            if (!this.onEditMode) return;
            const editSettingInfo: UserSetting = {
                name: this.userNameValue,
                phone: this.phoneValue,
                email: this.emailValue
            }
            updateAccountSetting(editSettingInfo).then((res: any) => {
                this.resetSettingForm();
            });
        }
        private resetSettingForm(): void {
            this.onEditMode = false;
            this.userNameDisabled = true;
            this.userPhoneDisabled = true;
            this.userEmailDisabled = true ;
        }
        mounted(){
            getUserAccountSetting().then((userInfo: UserSetting)=>{
                this._userSetting = {
                    name: userInfo.name || '',
                    phone: userInfo.phone || '',
                    email: userInfo.email || '',
                };
                this.phoneValue = this._userSetting.phone!;
                this.userNameValue = this._userSetting.name!;
                this.emailValue = this._userSetting.email!;
            })
        }
}
PAMapp/pages/accountSetting/index.vue
@@ -1,14 +1,14 @@
<template>
<div class="account-page">
    <div class="account-page-title">個人帳號設定</div>
    <section class="account-card" :class="{'edit': !userNameDisabled }">
    <section class="account-card" :class="{'edit': !userNameDisabled }">
        <div class="header">
            <div class="block">
                <div class="setting-title">姓名</div>
                <div class="contact-type">
                    <input
                        :disabled="userNameDisabled"
                        v-model="userNameValue"
                        v-model="userNameValue"
                        ref="userName"
                        class="input name-input"
                        >
@@ -16,22 +16,22 @@
                        <span v-show="!nameValid" class="error">此欄位必填</span>
                    </div>
                </div>
            </div>
            <i class="icon-edit icon" @click="editField('userName')" :class="{'icon-color-change': !userNameDisabled}"></i>
        </div>
    </section>
    <section class="account-card" :class="{'edit': !userPhoneDisabled }" v-if="phoneValue">
    <section class="account-card" :class="{'edit': !userPhoneDisabled }" v-if="phoneValue">
        <div class="header">
            <div class="block">
            <div class="block">
            <div class="setting-title">綁定</div>
            <div class="contact-type">
                æ‰‹æ©Ÿè™Ÿç¢¼
                <input
                    :disabled="userPhoneDisabled"
                    v-model="phoneValue"
                    v-model="phoneValue"
                    :class="{
                    'is-invalid': !phoneValid
                    }"
@@ -44,19 +44,19 @@
                </div>
            </div>
            </div>
            <!-- <i class="icon-edit icon"
            <!-- <i class="icon-edit icon"
                @click="editField('userPhone')"
                :class="{'icon-color-change': !userPhoneDisabled}"></i> -->
        </div>
    </section>
    <section class="account-card" :class="{'edit': !userEmailDisabled }" v-if="emailValue">
    <section class="account-card" :class="{'edit': !userEmailDisabled }" v-if="emailValue">
        <div class="header">
            <div class="block">
            <div class="setting-title">綁定</div>
                <div class="contact-type">Email
                    <input
                    <input
                        :disabled="userEmailDisabled"
                        v-model="emailValue"
                        :class="{
@@ -71,15 +71,15 @@
                    </div>
                </div>
                </div>
                <!-- <i class="icon-edit icon" @click="editField('userEmail')"
                <!-- <i class="icon-edit icon" @click="editField('userEmail')"
                        :class="{'icon-color-change': !userEmailDisabled}"></i> -->
        </div>
    </section>
    <div class="account-setting-btn mb-30">
        <el-button
        <el-button
            :disabled="isSubmitBtnDisabled"
            @click.native="updateAccountSetting">送出</el-button>
    </div>
@@ -87,227 +87,8 @@
</div>
</template>
<script lang="ts">
import { Vue,Component } from 'vue-property-decorator'
import { getUserAccountSetting, updateAccountSetting } from '~/assets/ts/api/consultant';
import { UserSetting } from '~/assets/ts/models/account.model';
@Component
export default class AccountSetting extends Vue {
        _userSetting!: UserSetting;
        userNameDisabled = true;
        userPhoneDisabled = true;
        userEmailDisabled = true ;
        userNameValue = '';
        phoneValue = '' ;
        emailValue = '' ;
        onEditMode = false;
        formValidStatus = {
            name: true,
            phone: true,
            email: true,
        };
        get nameValid(): boolean {
            this.formValidStatus.name = this.userNameValue ? true : false;
            return this.formValidStatus.name;
        }
        get phoneValid(): boolean {
            const rule = /^09[0-9]{8}$/;
            this.formValidStatus.phone = this.phoneValue ? rule.test(this.phoneValue) : true;
            return this.formValidStatus.phone;
        }
        get emailValid(): boolean {
            const rule = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/;
            this.formValidStatus.email = this.emailValue ? rule.test(this.emailValue) : true;;
            return this.formValidStatus.email;
        }
        editField(fieldName: string): void {
            this.onEditMode = true;
            const enablePromise = new Promise((resolve, reject) => { // æ­¤ç‚ºpromise語法
                resolve((this as any)[`${fieldName}Disabled`] = false);
            });
            const targetInput = this.$refs[fieldName] as any;
            enablePromise.then((_) => {
                targetInput.focus();
            });
        }
        get isSubmitBtnDisabled(): boolean {
            const isFormValid = this.formValidStatus.name && this.formValidStatus.phone && this.formValidStatus.email;
            return !isFormValid || !this.onEditMode
                || (!this.phoneValue && !this.emailValue);
        }
        updateAccountSetting(): void {
            // const dataChanged = (): boolean => {
            //     return this._userSetting.name !== this.userNameValue
            //         || this._userSetting.phone !== this.phoneValue
            //         || this._userSetting.email !== this.emailValue;
            // };
            // if (dataChanged) {
            // }
            if (!this.onEditMode) return;
            const editSettingInfo: UserSetting = {
                name: this.userNameValue,
                phone: this.phoneValue,
                email: this.emailValue
            }
            updateAccountSetting(editSettingInfo).then((res: any) => {
                console.log('updateRes:', res);
                this.resetSettingForm();
            });
        }
        private resetSettingForm(): void {
            this.onEditMode = false;
            this.userNameDisabled = true;
            this.userPhoneDisabled = true;
            this.userEmailDisabled = true ;
        }
        mounted(){
            getUserAccountSetting().then((userInfo: UserSetting)=>{
                this._userSetting = {
                    name: userInfo.name || '',
                    phone: userInfo.phone || '',
                    email: userInfo.email || '',
                };
                this.phoneValue = this._userSetting.phone!;
                this.userNameValue = this._userSetting.name!;
                this.emailValue = this._userSetting.email!;
            })
        }
}
</script>
<script src="./account-setting.component.ts"></script>
<style lang="scss" scoped>
.account-page{
    .block{
        display: flex;
    }
    .account-page-title{
        font-size: 20px;
        margin-bottom: 34px;
    }
    .account-card{
        display: flex;
        flex-direction: column;
        border-bottom: 1px solid gray;
        margin-bottom: 33px;
        .contact-type{
            width: 184px;
            margin-right: 16px;
            font-size: 20px;
            display: flex;
            flex-direction: column;
            align-items: flex-start;
        }
        &.edit {
            input {
                border: 1px solid lightgray;
                background-color: #fff;
            }
        }
    }
    .account-setting-btn{
        display: flex;
        justify-content: center;
    }
}
.error-txt{
    padding-bottom: 10px;
    .error {
        @extend .smTxt_bold;
        @extend .text--primary;
        height: 16px;
        }
}
.name-input{
            width: 184px;
            height:27px;
            margin-bottom: 20px;
            font-size: 20px;
            margin-top: -3px;
        }
.setting-title{
            margin-left: 28px;
            margin-bottom:10px;
            width: 58px;
            font-size: 20px;
        }
.header{
    display: flex;
    align-items: baseline;
}
.contact-input{
    font-size: 20px;
    margin-bottom: 10px;
    text-overflow: ellipsis;
    margin-top: 10px;
    width: 184px;
}
.input{
    border: 0;
    background-color: rgba(0,0,0,0) ;
    outline-color: gainsboro;
}
.input:focus{
    background-color: #fff;
}
.icon-color-change{
    color:$PRIMARY_RED;
    font-size: 20px;
}
.icon{
    font-size:20px;
    // color:#1B365D;
}
@include desktop{
    .header{
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    }
    .setting-title{
            margin-bottom:10px;
            width: 58px;
            font-size: 18px;
            font-weight: bold;
        }
    .account-page{
    .account-page-title{
        font-size: 20px;
        margin-bottom: 34px;
        font-weight: bold;
    }
    }
    .account-card{
        display: flex;
        flex-direction: column;
        border-bottom: 1px solid gray;
        margin-bottom: 33px;
        .contact-type{
            margin-left: 10px;
            font-size: 20px;
        }
    }
    .name-input{
        width: 550px;
    }
    .contact-input{
        width:550px
    }
    }
</style>
  @import "./account-setting.component.scss";
</style>
PAMapp/pages/agentInfo/_agentNo.vue
@@ -130,8 +130,8 @@
        type="flex"
        class="pam-paragraph">
        <UiField icon="school" label="個人背景">
          <span v-for="(experience, index) in agentInfo.experiences" :key="index">
            {{ experience }}<span v-if="index !== agentInfo.experiences.length - 1">, </span>
          <span v-for="(experience, index) in agentInfo.expertiseList" :key="index">
            {{ experience }}<span v-if="index !== agentInfo.expertiseList.length - 1">, </span>
          </span>
        </UiField>
      </el-row>
@@ -182,124 +182,8 @@
    </div>
</template>
<script lang="ts">
import { Context } from '@nuxt/types';
import { namespace } from 'nuxt-property-decorator';
import { Vue, Component } from 'vue-property-decorator';
import { getConsultantDetail } from '~/assets/ts/api/consultant';
import { Role } from '~/assets/ts//models/enum/Role';
const roleStorage = namespace('localStorage');
@Component
export default class AgentInfoComponent extends Vue {
  @roleStorage.Getter currentRole!:string|null;
  role = Role;
  agentInfo!: AgentInfo;
  isAlertAddSuccess = false;
  isAlertFieldInfo = false;
  fieldInfoTitle = '';
  fieldInfoDesc = '';
  async asyncData(context: Context) {
    const agentNo = context.route.params.agentNo;
    let agentInfo = {};
    await getConsultantDetail(agentNo).then((res) => agentInfo = res.data )
    return {
      agentInfo
    }
  }
  get agentName(): string {
    return `${this.agentInfo.name}(${this.agentInfo.role})`;
  }
  alertAddSuccess() {
      this.isAlertAddSuccess = true;
  }
  alertFieldInfo(field: string): void {
    this.isAlertFieldInfo = true;
    switch(field) {
      case 'suitability':
        this.fieldInfoTitle = '匹配度';
        this.fieldInfoDesc = '匹配度是透過嚴選配對或快速篩選後,將每一位保險顧問資料進行比對後排序推薦給您的媒合數值,您可以作為選擇適合顧問的參考值。';
        break;
      case 'evaluation':
        this.fieldInfoTitle = '諮詢度表現';
        this.fieldInfoDesc = '諮詢度表現是將每一位保險顧問近一個月回覆諮詢數量進行比對後排序推薦給您的媒合數值。';
        break;
    }
  }
}
interface AgentInfo {
  name            : string;
  agentNo         : string;
  role            : string;
  image           : string;
  avgScore        : number;
  title           : string;
  phoneNumber     : string;
  serveArea       : string;
  companyAddress  : string;
  lastestLoginTime: Date | null;
  seniority       : string;
  suitability     : number;
  evaluation      : number;
  expertises      : string[];
  concept         : string;
  experiences     : string[];
  awards          : string;
}
</script>
<script src="./agent-info.component.ts"></script>
<style lang="scss">
.pam-icon {
  font-size: 15px;
  padding-right: 8px;
  color: $PRUDENTIAL_GREY;
  &.icon--primary {
    color: $PRIMARY_RED;
  }
}
.pam-field {
  display: flex;
  flex-direction: column;
  .pam-field__label {
    display: flex;
    align-items: center;
    .pam-icon {
      font-size: 12px;
    }
    .pam-field__title {
      font-size: 16px;
      font-weight: bold;
      display: flex;
      align-items: center;
    }
  }
}
.pam-field-suitability {
  .el-progress-bar__inner {
    background-color: $LIGHT_BLUE !important;
  }
}
.pam-field-evaluation {
  .el-progress-bar__inner {
    background-color: $TEAL_GREEN!important;
  }
}
.pam-field-experts {
  display: flex;
  flex-wrap: wrap;
}
.pam-progress__label {
  justify-content: space-between;
  flex-wrap: wrap;
  line-height: 24px;
}
  @import "./agent-info.component.scss";
</style>
PAMapp/pages/agentInfo/agent-info.component.scss
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,48 @@
.pam-icon {
  font-size: 15px;
  padding-right: 8px;
  color: $PRUDENTIAL_GREY;
  &.icon--primary {
    color: $PRIMARY_RED;
  }
}
.pam-field {
  display: flex;
  flex-direction: column;
  .pam-field__label {
    display: flex;
    align-items: center;
    .pam-icon {
      font-size: 12px;
    }
    .pam-field__title {
      font-size: 16px;
      font-weight: bold;
      display: flex;
      align-items: center;
    }
  }
}
.pam-field-suitability {
  .el-progress-bar__inner {
    background-color: $LIGHT_BLUE !important;
  }
}
.pam-field-evaluation {
  .el-progress-bar__inner {
    background-color: $TEAL_GREEN!important;
  }
}
.pam-field-experts {
  display: flex;
  flex-wrap: wrap;
}
.pam-progress__label {
  justify-content: space-between;
  flex-wrap: wrap;
  line-height: 24px;
}
PAMapp/pages/agentInfo/agent-info.component.ts
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,54 @@
import { Context } from '@nuxt/types';
import { namespace } from 'nuxt-property-decorator';
import { Vue, Component } from 'vue-property-decorator';
import { getConsultantDetail } from '~/assets/ts/api/consultant';
import { Role } from '~/assets/ts/models/enum/role.enum';
import { AgentInfo } from '~/assets/ts/models/agentInfo.model';
const roleStorage = namespace('localStorage');
@Component
export default class AgentInfoComponent extends Vue {
  @roleStorage.Getter currentRole!:string|null;
  role = Role;
  agentInfo!: AgentInfo;
  isAlertAddSuccess = false;
  isAlertFieldInfo = false;
  fieldInfoTitle = '';
  fieldInfoDesc = '';
  async asyncData(context: Context) {
    const agentNo = context.route.params.agentNo;
    let agentInfo = {};
    await getConsultantDetail(agentNo).then((res) => agentInfo = res.data )
    return {
      agentInfo
    }
  }
  get agentName(): string {
    return `${this.agentInfo.name}(${this.agentInfo.role})`;
  }
  alertAddSuccess() {
      this.isAlertAddSuccess = true;
  }
  alertFieldInfo(field: string): void {
    this.isAlertFieldInfo = true;
    switch(field) {
      case 'suitability':
        this.fieldInfoTitle = '匹配度';
        this.fieldInfoDesc = '匹配度是透過嚴選配對或快速篩選後,將每一位保險顧問資料進行比對後排序推薦給您的媒合數值,您可以作為選擇適合顧問的參考值。';
        break;
      case 'evaluation':
        this.fieldInfoTitle = '諮詢度表現';
        this.fieldInfoDesc = '諮詢度表現是將每一位保險顧問近一個月回覆諮詢數量進行比對後排序推薦給您的媒合數值。';
        break;
    }
  }
}
PAMapp/pages/consultantLogin/consultant-login.component.scss
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,77 @@
.pam-consultant-login {
  margin: auto;
  width: 336px;
  font-size: 20px;
  color: $PRIMARY_BLACK;
  &__header {
    text-align: center;
    font-size: 24px;
    font-weight: bold;
    letter-spacing: 1.2;
    color: $PRIMARY_BLACK;
  }
  &__title {
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  &__input {
    width: 100%;
    outline: 0;
    border: 1px solid #CCCCCC;
    border-radius: 10px;
    font-size: 20px;
    height: 50px;
    padding: 10px 90px 10px 15px;
    overflow: auto;
    box-sizing: border-box;
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    &Icon {
      position: absolute;
      display: flex;
      align-items: center;
      top: 15px;
      right: 15px;
    }
  }
  &__forgot-password {
    color: $PRIMARY_RED;
    text-decoration: none;
    font-size: 16px;
  }
  &__verifyBlock {
    display: flex;
    justify-content: space-between;
  }
  &__verifyImg {
    width: 126px;
    height: 50px;
    border:1px #cccccc solid;
    img {
      width: 100%;
      height: 100%;
    }
  }
  &__confirmBlock {
    display: flex;
    justify-content: center;
  }
  &__confirm {
    color: $PRIMARY_WHITE;
    width: 80px;
    height: 50px;
    border-radius: 30px;
    border: 1px solid $LIGHT_GREY;
    background-color: $PRIMARY_RED;
  }
}
PAMapp/pages/consultantLogin/consultant-login.component.ts
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,103 @@
import { Vue, Component , namespace } from 'nuxt-property-decorator';
import { AxiosError } from 'axios';
import { getImgOfVerification, logInToConsultant, getVerificationStatus } from '~/assets/ts/api/consultant';
import { Role } from '~/assets/ts/models/enum/role.enum';
import ErrorMessageBox from '~/assets/ts/errorService';
const roleStorage = namespace('localStorage');
@Component({
  layout: 'home'
})
export default class ConsultantLogin extends Vue {
  @roleStorage.Mutation storageIdToken!: (token: string) => void;
  @roleStorage.Mutation storageRole!: (role: string) => void;
  @roleStorage.Mutation storageConsultantId!:(id:string) => void;
  isRememberUserName = false;
  isShowPassword = false;
  imgSrc = '';
  verificationCode='';
  consultantDto = {
    username: '',
    password: '',
  }
  get isAlreadyDone():boolean{
    return !!(this.verificationCode && this.consultantDto.username && this.consultantDto.password);
  }
  mounted() {
    this.getInitUserName();
    this.regenerateImgOfVerification();
  };
  private getInitUserName(): void {
    const username = localStorage.getItem('consultantUserName')
    if (username) {
      this.consultantDto.username = username;
      this.isRememberUserName = true;
    }
  }
  public regenerateImgOfVerification(): void {
    getImgOfVerification().then( imgOfBase64 =>
      this.imgSrc = imgOfBase64
    );
  };
  public isRememberChange():void{
    this.isRememberUserName = !this.isRememberUserName;
    this.storeUserName();
  }
  public sendInfo():void{
    this.isAlreadyDone ? this.verify() : ErrorMessageBox('請確認帳號、密碼以及驗證碼是否填寫完畢');
  }
  private verify():void{
    getVerificationStatus(this.verificationCode).then( verifySuccess => {
      if(verifySuccess.data){
        this.loginWithConsultant()
      }else{
        this.clearValue();
        this.regenerateImgOfVerification();
        ErrorMessageBox('驗證碼輸入錯誤');
      }
    });
  }
  private loginWithConsultant(): void {
    logInToConsultant(this.consultantDto).then(res => {
      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;
        ErrorMessageBox(errorMsg);
        break;
      default:
        ErrorMessageBox('',error);
        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 = '';
  }
};
PAMapp/pages/consultantLogin/index.vue
@@ -55,206 +55,8 @@
  </div>
</template>
<script lang="ts">
  import { Vue, Component , namespace } from 'nuxt-property-decorator';
  import { AxiosError } from 'axios';
  import { getImgOfVerification, logInToConsultant, getVerificationStatus } from '~/assets/ts/api/consultant';
  import { Role } from '~/assets/ts/models/enum/Role';
  import ErrorMessageBox from '~/assets/ts/errorService';
  const roleStorage = namespace('localStorage');
  @Component({
    layout: 'home'
  })
  export default class ConsultantLogin extends Vue {
    @roleStorage.Mutation storageIdToken!: (token: string) => void;
    @roleStorage.Mutation storageRole!: (role: string) => void;
    @roleStorage.Mutation storageConsultantId!:(id:string) => void;
    isRememberUserName = false;
    isShowPassword = false;
    imgSrc = '';
    verificationCode='';
    consultantDto = {
      username: '',
      password: '',
    }
    get isAlreadyDone():boolean{
      return !!(this.verificationCode && this.consultantDto.username && this.consultantDto.password);
    }
    mounted() {
      this.getInitUserName();
      this.regenerateImgOfVerification();
    };
    private getInitUserName(): void {
      const username = localStorage.getItem('consultantUserName')
      if (username) {
        this.consultantDto.username = username;
        this.isRememberUserName = true;
      }
    }
    public regenerateImgOfVerification(): void {
      getImgOfVerification().then( imgOfBase64 =>
        this.imgSrc = imgOfBase64
      );
    };
    public isRememberChange():void{
      this.isRememberUserName = !this.isRememberUserName;
      this.storeUserName();
    }
    public sendInfo():void{
      this.isAlreadyDone ? this.verify() : ErrorMessageBox('請確認帳號、密碼以及驗證碼是否填寫完畢');
    }
    private verify():void{
      getVerificationStatus(this.verificationCode).then( verifySuccess => {
        if(verifySuccess.data){
          this.loginWithConsultant()
        }else{
          this.clearValue();
          this.regenerateImgOfVerification();
          ErrorMessageBox('驗證碼輸入錯誤');
        }
      });
    }
    private loginWithConsultant(): void {
      logInToConsultant(this.consultantDto).then(res => {
        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;
          ErrorMessageBox(errorMsg);
          break;
        default:
          ErrorMessageBox('',error);
          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 = '';
    }
  };
</script>
<script src="./consultant-login.component.ts"></script>
<style lang="scss" scoped>
  .mt-20 {
    margin-top: 20px;
  }
  .mt-25 {
    margin-top: 25px;
  }
  .w-55 {
    width: 55% !important;
  }
  .position-r {
    position: relative;
  }
  .pam-consultant-login {
    margin: auto;
    width: 336px;
    font-size: 20px;
    color: $PRIMARY_BLACK;
    &__header {
      text-align: center;
      font-size: 24px;
      font-weight: bold;
      letter-spacing: 1.2;
      color: $PRIMARY_BLACK;
    }
    &__title {
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    &__input {
      width: 100%;
      outline: 0;
      border: 1px solid #CCCCCC;
      border-radius: 10px;
      font-size: 20px;
      height: 50px;
      padding: 10px 90px 10px 15px;
      overflow: auto;
      box-sizing: border-box;
      -webkit-box-sizing: border-box;
      -moz-box-sizing: border-box;
      &Icon {
        position: absolute;
        display: flex;
        align-items: center;
        top: 15px;
        right: 15px;
      }
    }
    &__forgot-password {
      color: $PRIMARY_RED;
      text-decoration: none;
      font-size: 16px;
    }
    &__verifyBlock {
      display: flex;
      justify-content: space-between;
    }
    &__verifyImg {
      width: 126px;
      height: 50px;
      border:1px #cccccc solid;
      img {
        width: 100%;
        height: 100%;
      }
    }
    &__confirmBlock {
      display: flex;
      justify-content: center;
    }
    &__confirm {
      color: $PRIMARY_WHITE;
      width: 80px;
      height: 50px;
      border-radius: 30px;
      border: 1px solid $LIGHT_GREY;
      background-color: $PRIMARY_RED;
    }
  }
  @import "./consultant-login.component.scss";
</style>
PAMapp/pages/login/index.vue
@@ -339,476 +339,8 @@
    </div>
</template>
<script lang="ts">
import { namespace } from 'nuxt-property-decorator';
import { Vue, Component, Ref } from 'vue-property-decorator';
import { LoginRequest, LoginVerify, loginVerify, OtpInfo, register, RegisterInfo, sendOtp } from '~/assets/ts/api/consultant';
import ErrorMessageBox from '~/assets/ts/errorService';
import { OtpErrorCode } from '~/assets/ts/models/enum/otpErrorCode';
import { Role } from '~/assets/ts/models/enum/Role';
<script src="./login.component.ts"></script>
const roleStorage = namespace('localStorage');
@Component
export default class Login extends Vue {
  @roleStorage.Mutation storageIdToken!: (token:string) => void;
  @roleStorage.Mutation storageRole!: (role:string) => void;
  @Ref('contract') readonly contract!: any;
  connectDevice: 'MOBILE' | 'EMAIL' = 'MOBILE';
  phoneNumber = '';
  otpCode = '';
  onPhoneVerifyStep: 'APPLY_OTP' | 'INPUT_OTP' | 'SUBMIT_OTP' = 'APPLY_OTP';
  otpCounterSec = 900;
  otpResendCounter = 30;
  otpInterval: any;
  phoneOtpInfo!: OtpInfo;
  email = '';
  onEmailVerifyResendStatus: 'APPLY_OTP' | 'CAN_RESEND' = 'APPLY_OTP';
  emailCounterSec = 900;
  emailResendCounter = 30;
  emailOtpCode = '';
  emailResendInterval: any;
  emailOtpInfo!: OtpInfo;
  autoRedirectCounter = 3;
  autoRedirectInterval: any;
  name = '';
  agreeContract = false;
  isReadContract = false;
  phoneSuccessConfirmVisable = false;
  emailOtpConfirmVisable = false;
  registerDialogVisible = false;
  registerSuccessConfirmVisable = false;
  applyAccount_onAction = false;
  previousPath = '';
  mounted() {
    const phoneOtpTime = localStorage.getItem('phoneOtpTime');
    const emailOtpTime = localStorage.getItem('emailOtpTime');
    const parsePhoneOtpTime = phoneOtpTime ? JSON.parse(phoneOtpTime) : '';
    const parseEmailOtpTime = emailOtpTime ? JSON.parse(emailOtpTime) : '';
    if (parsePhoneOtpTime && parsePhoneOtpTime.contactType === 'SMS') {
      this.phoneDiffTime(parsePhoneOtpTime);
    }
    if (parseEmailOtpTime && parseEmailOtpTime.contactType === 'EMAIL') {
      this.emailDiffTime(parseEmailOtpTime);
    }
  }
  detectContractReadStatus(event: any): void {
    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 isSubmitBtnDisabled(): boolean {
    return this.connectDevice === 'MOBILE'
      ? (!this.otpCode || !this.phoneNumber || !this.phoneValid || !this.otpCounterSec)
      : (!this.emailOtpCode || !this.email || !this.emailValid || !this.emailCounterSec)
  }
  get phoneCounter() {
    let min = Math.floor(this.otpCounterSec / 60);
    let sec = Math.floor(this.otpCounterSec % 60);
    return `${min < 10 ? '0' + min : min}:${sec < 10 ? '0' + sec : sec}`;
  }
  get emailOtpCounter() {
    let min = Math.floor(this.emailCounterSec / 60);
    let sec = Math.floor(this.emailCounterSec % 60);
    return `${min < 10 ? '0' + min : min}:${sec < 10 ? '0' + sec : sec}`;
  }
  get showPhoneOtpCodeField(): boolean {
    return this.connectDevice === 'MOBILE' && this.onPhoneVerifyStep === 'INPUT_OTP';
  };
  get showEmailVerifyField(): boolean {
    return this.connectDevice === 'EMAIL' && this.onEmailVerifyResendStatus === 'CAN_RESEND';
  };
  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.storageOtpTime(type, otpInfo);
        this.startOtpSetting(type);
        this.startOtpCount(type);
      } else {
        const errorMsg = OtpErrorCode[otpInfo.failCode] ? OtpErrorCode[otpInfo.failCode]:'OTP系統錯誤';
        ErrorMessageBox(errorMsg);
      }
    });
  };
  resentOtp(type: string) {
    this.resetOtpSetting(type);
    this.applyOtpVerification(type);
  }
  deleteOtpInfo(type: string) {
    this.resetOtpSetting(type);
    if (type === 'MOBILE') {
      this.onPhoneVerifyStep = 'APPLY_OTP';
      this.phoneNumber = '';
      this.otpCode = '';
    } else {
      this.onEmailVerifyResendStatus = 'APPLY_OTP';
      this.email = '';
      this.emailOtpCode = '';
    }
    this.removeOtpTime();
  }
  applyAccount(): void {
    if (this.applyAccount_onAction) {
      return ;
    }
    this.applyAccount_onAction = true;
    const registerInfo = this.setRegisterInfo();
    register(registerInfo).then(res => {
      this.storageIdToken(res.data.id_token);
      this.storageRole(Role.USER);
      this.storagePhoneOrEmail(registerInfo);
      this.autoRedirect();
      this.registerSuccessConfirmVisable = true;
    }).catch(() => {
      this.applyAccount_onAction = false;
    });
  };
  confirmApplySuccess(): void {
    this.phoneSuccessConfirmVisable = false;
    this.registerSuccessConfirmVisable = false;
    this.redirect();
  }
  private autoRedirect() {
    this.autoRedirectInterval = setInterval(() => {
      this.autoRedirectCounter -= 1;
      if (this.autoRedirectCounter === 0) {
        clearInterval(this.autoRedirectInterval);
        this.redirect();
      }
    }, 1000)
  }
  private redirect() {
    const backToPrevious = ['questionnaire', 'myConsultantList'];
    const find = backToPrevious.findIndex(item => this.previousPath.includes(item));
    console.log(this.previousPath, find, 'redirect');
    find > -1 ? this.$router.go(-1) : this.$router.push('/');
  }
  beforeRouteEnter (to, from, next) {
      next(vm => {
        console.log(from.path, 'beforeRouteEnter');
        vm.previousPath = from.path;
      })
    }
  login() {
    const login: LoginVerify = this.setLoginInfo();
    this.removeOtpTime();
    loginVerify(login).then(res => {
      this.storageIdToken(res.data.id_token);
      this.storageRole(Role.USER);
      this.phoneSuccessConfirmVisable = true;
      this.autoRedirect();
      this.storagePhoneOrEmail(this.setRegisterInfo());
    }).catch(error => {
      this.checkHttpErrorStatus(error);
    });
  }
  private checkHttpErrorStatus(error:any):void{
    switch (error.response.status) {
        case 401:
          const errorMsg = OtpErrorCode[error.response?.data?.detail] ? OtpErrorCode[error.response?.data?.detail]:'OTP系統錯誤';
          ErrorMessageBox(errorMsg);
          break;
        case 403:
          this.registerDialogVisible = true;
          setTimeout(() => {
            const isScrollBarNeedless = this.contract.scrollHeight <= this.contract.clientHeight;
            if (isScrollBarNeedless) {
              this.isReadContract = true;
            }
          }, 1000);
          break;
        default:
          ErrorMessageBox('',error);
          break;
      }
  }
  destroyed() {
    this.removeOtpTime();
    clearInterval(this.otpInterval);
    clearInterval(this.emailResendInterval);
    clearInterval(this.autoRedirectInterval);
  }
  private phoneDiffTime(parseOtpTime: any) {
    const diffSecs = this.calcDiffSecs(parseOtpTime.time);
    if (diffSecs < this.otpCounterSec) {
      this.otpResendCounter = diffSecs < 30 ? 30 - diffSecs : 0;
        this.otpCounterSec -= diffSecs;
        this.phoneNumber = parseOtpTime.phone;
        this.onPhoneVerifyStep = 'INPUT_OTP';
        this.phoneOtpInfo = this.setOtpInfo(parseOtpTime);
        this.startOtpCount('MOBILE');
    } else {
      localStorage.removeItem('phoneOtpTime');
    }
  }
  private emailDiffTime(parseOtpTime: any) {
    const diffSecs = this.calcDiffSecs(parseOtpTime.time);
    if (diffSecs < this.emailCounterSec) {
      this.emailResendCounter =  diffSecs < 30 ? 30 - diffSecs : 0;
      this.emailCounterSec -= diffSecs;
      this.email = parseOtpTime.email;
      this.onEmailVerifyResendStatus = 'CAN_RESEND';
      this.emailOtpInfo = this.setOtpInfo(parseOtpTime);
      this.startOtpCount('EMAIL');
    } else {
      localStorage.removeItem('emailOtpTime');
    }
  }
  private calcDiffSecs(parseOtpTime) {
    const currentTime = new Date().getTime();
    const storageTime = new Date(parseOtpTime).getTime();
    return Math.floor((currentTime - storageTime) / 1000);
  }
  private resetOtpSetting(type: string) {
    if (type === 'MOBILE') {
      clearInterval(this.otpInterval);
      this.otpResendCounter = 30;
      this.otpCounterSec = 900;
    } else {
      clearInterval(this.emailResendInterval);
      this.emailResendCounter = 30;
      this.emailCounterSec = 900;
    }
  }
  private setOtpInfo(parseOtpTime) {
    return {
      indexKey: parseOtpTime.indexKey,
      success: true,
      failCode: '',
      failReason: '',
    }
  }
  private storageOtpTime(type: string, otpInfo: OtpInfo) {
    type === 'MOBILE' ? this.phoneOtpInfo = otpInfo : this.emailOtpInfo = otpInfo;
    const info = {...this.setRegisterInfo(), time: new Date()}
    type === 'MOBILE' ? localStorage.setItem('phoneOtpTime',JSON.stringify(info))
                      : localStorage.setItem('emailOtpTime',JSON.stringify(info));
  }
  private startOtpSetting(type: string) {
    if (type === 'MOBILE') {
      this.onPhoneVerifyStep = 'INPUT_OTP';
    } else {
      this.onEmailVerifyResendStatus = 'CAN_RESEND';
      this.emailOtpConfirmVisable = true;
    }
  }
  private startOtpCount(type: string) {
    type === 'MOBILE' ? this.startPhoneCounter() : this.startEmailCounter();;
  }
  private startEmailCounter() {
    this.emailResendInterval = setInterval(() => {
      this.emailCounterSec -= 1;
      if (this.emailResendCounter !== 0) {
        this.emailResendCounter -= 1;
      }
      if (this.emailCounterSec === 0) {
        clearInterval(this.emailResendInterval);
      }
    }, 1000)
  }
  private startPhoneCounter() {
    this.otpInterval = setInterval(() => {
      this.otpCounterSec -= 1;
      if (this.otpResendCounter !== 0) {
        this.otpResendCounter -= 1;
      }
      if (this.otpCounterSec === 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'
        }
  }
  private storagePhoneOrEmail(registerInfo:RegisterInfo):void{
    const info = {...registerInfo, time: new Date()}
    localStorage.setItem('userInfo',JSON.stringify(info));
  }
  private removeOtpTime() {
    localStorage.removeItem('emailOtpTime');
    localStorage.removeItem('phoneOtpTime');
  }
  private setLoginInfo() {
    const isMobile = this.connectDevice === 'MOBILE'
    return {
      account: isMobile ? this.phoneNumber : this.email,
      indexKey: isMobile ? this.phoneOtpInfo.indexKey : this.emailOtpInfo.indexKey,
      otpCode: isMobile ? this.otpCode : this.emailOtpCode
    }
  }
}
</script>
<style lang="scss">
.pam-login-page {
    font-size: 20px !important;
    display: flex;
    flex-direction: column;
    .pam-login-page__action-bar {
      display: flex;
      flex: 1;
      align-items: flex-end;
      @include desktop {
        margin-bottom: 30px;
      }
    }
  }
  .pam-input {
    height: 26px;
    width: calc(100% - 36px);
    border-radius: 10px !important;
    padding: 12px 18px !important;
    border:1px solid #CCCCCC;
    outline: 0;
    @extend .text--middle;
    &::placeholder {
      color: $PRUDENTIAL_GREY;
    }
    &.is-invalid {
      border: 1px solid $PRIMARY_RED !important;
      border-radius: 20px;
    }
  }
.pam-register-dialog__contract {
  $DEVICE_EXTRA_HEIGHT: 42px;
  $ALIGN_PADDING: 60px;
  $TOP_CONTENT_HEIGHT: 186px;
  $BOTTOM_CONTENT_HEIGHT: 131px;
  max-height: calc(100vh - $DEVICE_EXTRA_HEIGHT - $ALIGN_PADDING - $TOP_CONTENT_HEIGHT - $BOTTOM_CONTENT_HEIGHT);
  overflow-y: scroll;
  border-radius: 6px;
  border: 1px solid #707070;
  padding: 20px;
  @include desktop {
    height: 335px;
  }
}
  .pam-radio {
    color: $PRIMARY_RED;
    align-items: center;
    display: flex;
    font-size: 20px;
    font-weight: bold;
    input {
      display: none;
    }
    i {
      font-size: 27px;
      padding-right: 5px;
    }
  }
  .pam-field-title__hint {
    @extend .smTxt_bold;
    color: #68737A;
  }
  .error {
    @extend .smTxt_bold;
    @extend .text--primary;
    height: 16px;
  }
  .pam-popUp-title {
      font-size: 20px;
      line-height: 27px;
  }
  .pam-popUp-txt {
    font-size: 18px;
    color: $MID_GREY;
  }
  .disabled {
    color: #A7A8AA;
  }
  .pam-input-position {
    position: relative;
    .icon-close {
      cursor: pointer;
      position: absolute;
      right: 15px;
      top: 28px;
      font-size: 16px;
    }
  }
<style lang="scss" scoped>
  @import "./login.component.scss";
</style>
PAMapp/pages/login/login.component.scss
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,97 @@
.pam-login-page {
    font-size: 20px !important;
    display: flex;
    flex-direction: column;
    .pam-login-page__action-bar {
      display: flex;
      flex: 1;
      align-items: flex-end;
      @include desktop {
        margin-bottom: 30px;
      }
    }
  }
  .pam-input {
    height: 26px;
    width: calc(100% - 36px);
    border-radius: 10px !important;
    padding: 12px 18px !important;
    border:1px solid #CCCCCC;
    outline: 0;
    @extend .text--middle;
    &::placeholder {
      color: $PRUDENTIAL_GREY;
    }
    &.is-invalid {
      border: 1px solid $PRIMARY_RED !important;
      border-radius: 20px;
    }
  }
.pam-register-dialog__contract {
  $DEVICE_EXTRA_HEIGHT: 42px;
  $ALIGN_PADDING: 60px;
  $TOP_CONTENT_HEIGHT: 186px;
  $BOTTOM_CONTENT_HEIGHT: 131px;
  max-height: calc(100vh - $DEVICE_EXTRA_HEIGHT - $ALIGN_PADDING - $TOP_CONTENT_HEIGHT - $BOTTOM_CONTENT_HEIGHT);
  overflow-y: scroll;
  border-radius: 6px;
  border: 1px solid #707070;
  padding: 20px;
  @include desktop {
    height: 335px;
  }
}
  .pam-radio {
    color: $PRIMARY_RED;
    align-items: center;
    display: flex;
    font-size: 20px;
    font-weight: bold;
    input {
      display: none;
    }
    i {
      font-size: 27px;
      padding-right: 5px;
    }
  }
  .pam-field-title__hint {
    @extend .smTxt_bold;
    color: #68737A;
  }
  .error {
    @extend .smTxt_bold;
    @extend .text--primary;
    height: 16px;
  }
  .pam-popUp-title {
      font-size: 20px;
      line-height: 27px;
  }
  .pam-popUp-txt {
    font-size: 18px;
    color: $MID_GREY;
  }
  .disabled {
    color: #A7A8AA;
  }
  .pam-input-position {
    position: relative;
    .icon-close {
      cursor: pointer;
      position: absolute;
      right: 15px;
      top: 28px;
      font-size: 16px;
    }
  }
PAMapp/pages/login/login.component.ts
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,373 @@
import { namespace } from 'nuxt-property-decorator';
import { Vue, Component, Ref } from 'vue-property-decorator';
import { LoginRequest, LoginVerify, loginVerify, OtpInfo, register, RegisterInfo, sendOtp } from '~/assets/ts/api/consultant';
import ErrorMessageBox from '~/assets/ts/errorService';
import { OtpErrorCode } from '~/assets/ts/models/enum/otpErrorCode';
import { Role } from '~/assets/ts/models/enum/role.enum';
const roleStorage = namespace('localStorage');
@Component
export default class Login extends Vue {
  @roleStorage.Mutation storageIdToken!: (token:string) => void;
  @roleStorage.Mutation storageRole!: (role:string) => void;
  @Ref('contract') readonly contract!: any;
  connectDevice: 'MOBILE' | 'EMAIL' = 'MOBILE';
  phoneNumber = '';
  otpCode = '';
  onPhoneVerifyStep: 'APPLY_OTP' | 'INPUT_OTP' | 'SUBMIT_OTP' = 'APPLY_OTP';
  otpCounterSec = 900;
  otpResendCounter = 30;
  otpInterval: any;
  phoneOtpInfo!: OtpInfo;
  email = '';
  onEmailVerifyResendStatus: 'APPLY_OTP' | 'CAN_RESEND' = 'APPLY_OTP';
  emailCounterSec = 900;
  emailResendCounter = 30;
  emailOtpCode = '';
  emailResendInterval: any;
  emailOtpInfo!: OtpInfo;
  autoRedirectCounter = 3;
  autoRedirectInterval: any;
  name = '';
  agreeContract = false;
  isReadContract = false;
  phoneSuccessConfirmVisable = false;
  emailOtpConfirmVisable = false;
  registerDialogVisible = false;
  registerSuccessConfirmVisable = false;
  applyAccount_onAction = false;
  previousPath = '';
  mounted() {
    const phoneOtpTime = localStorage.getItem('phoneOtpTime');
    const emailOtpTime = localStorage.getItem('emailOtpTime');
    const parsePhoneOtpTime = phoneOtpTime ? JSON.parse(phoneOtpTime) : '';
    const parseEmailOtpTime = emailOtpTime ? JSON.parse(emailOtpTime) : '';
    if (parsePhoneOtpTime && parsePhoneOtpTime.contactType === 'SMS') {
      this.phoneDiffTime(parsePhoneOtpTime);
    }
    if (parseEmailOtpTime && parseEmailOtpTime.contactType === 'EMAIL') {
      this.emailDiffTime(parseEmailOtpTime);
    }
  }
  detectContractReadStatus(event: any): void {
    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 isSubmitBtnDisabled(): boolean {
    return this.connectDevice === 'MOBILE'
      ? (!this.otpCode || !this.phoneNumber || !this.phoneValid || !this.otpCounterSec)
      : (!this.emailOtpCode || !this.email || !this.emailValid || !this.emailCounterSec)
  }
  get phoneCounter() {
    let min = Math.floor(this.otpCounterSec / 60);
    let sec = Math.floor(this.otpCounterSec % 60);
    return `${min < 10 ? '0' + min : min}:${sec < 10 ? '0' + sec : sec}`;
  }
  get emailOtpCounter() {
    let min = Math.floor(this.emailCounterSec / 60);
    let sec = Math.floor(this.emailCounterSec % 60);
    return `${min < 10 ? '0' + min : min}:${sec < 10 ? '0' + sec : sec}`;
  }
  get showPhoneOtpCodeField(): boolean {
    return this.connectDevice === 'MOBILE' && this.onPhoneVerifyStep === 'INPUT_OTP';
  };
  get showEmailVerifyField(): boolean {
    return this.connectDevice === 'EMAIL' && this.onEmailVerifyResendStatus === 'CAN_RESEND';
  };
  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.storageOtpTime(type, otpInfo);
        this.startOtpSetting(type);
        this.startOtpCount(type);
      } else {
        const errorMsg = OtpErrorCode[otpInfo.failCode] ? OtpErrorCode[otpInfo.failCode]:'OTP系統錯誤';
        ErrorMessageBox(errorMsg);
      }
    });
  };
  resentOtp(type: string) {
    this.resetOtpSetting(type);
    this.applyOtpVerification(type);
  }
  deleteOtpInfo(type: string) {
    this.resetOtpSetting(type);
    if (type === 'MOBILE') {
      this.onPhoneVerifyStep = 'APPLY_OTP';
      this.phoneNumber = '';
      this.otpCode = '';
    } else {
      this.onEmailVerifyResendStatus = 'APPLY_OTP';
      this.email = '';
      this.emailOtpCode = '';
    }
    this.removeOtpTime();
  }
  applyAccount(): void {
    if (this.applyAccount_onAction) {
      return ;
    }
    this.applyAccount_onAction = true;
    const registerInfo = this.setRegisterInfo();
    register(registerInfo).then(res => {
      this.storageIdToken(res.data.id_token);
      this.storageRole(Role.USER);
      this.storagePhoneOrEmail(registerInfo);
      this.autoRedirect();
      this.registerSuccessConfirmVisable = true;
    }).catch(() => {
      this.applyAccount_onAction = false;
    });
  };
  confirmApplySuccess(): void {
    this.phoneSuccessConfirmVisable = false;
    this.registerSuccessConfirmVisable = false;
    this.redirect();
  }
  private autoRedirect() {
    this.autoRedirectInterval = setInterval(() => {
      this.autoRedirectCounter -= 1;
      if (this.autoRedirectCounter === 0) {
        clearInterval(this.autoRedirectInterval);
        this.redirect();
      }
    }, 1000)
  }
  private redirect() {
    const backToPrevious = ['questionnaire', 'myConsultantList'];
    const find = backToPrevious.findIndex(item => this.previousPath.includes(item));
    console.log(this.previousPath, find, 'redirect');
    find > -1 ? this.$router.go(-1) : this.$router.push('/');
  }
  beforeRouteEnter (to, from, next) {
      next(vm => {
        console.log(from.path, 'beforeRouteEnter');
        vm.previousPath = from.path;
      })
    }
  login() {
    const login: LoginVerify = this.setLoginInfo();
    this.removeOtpTime();
    loginVerify(login).then(res => {
      this.storageIdToken(res.data.id_token);
      this.storageRole(Role.USER);
      this.phoneSuccessConfirmVisable = true;
      this.autoRedirect();
      this.storagePhoneOrEmail(this.setRegisterInfo());
    }).catch(error => {
      this.checkHttpErrorStatus(error);
    });
  }
  private checkHttpErrorStatus(error:any):void{
    switch (error.response.status) {
        case 401:
          const errorMsg = OtpErrorCode[error.response?.data?.detail] ? OtpErrorCode[error.response?.data?.detail]:'OTP系統錯誤';
          ErrorMessageBox(errorMsg);
          break;
        case 403:
          this.registerDialogVisible = true;
          setTimeout(() => {
            const isScrollBarNeedless = this.contract.scrollHeight <= this.contract.clientHeight;
            if (isScrollBarNeedless) {
              this.isReadContract = true;
            }
          }, 1000);
          break;
        default:
          ErrorMessageBox('',error);
          break;
      }
  }
  destroyed() {
    this.removeOtpTime();
    clearInterval(this.otpInterval);
    clearInterval(this.emailResendInterval);
    clearInterval(this.autoRedirectInterval);
  }
  private phoneDiffTime(parseOtpTime: any) {
    const diffSecs = this.calcDiffSecs(parseOtpTime.time);
    if (diffSecs < this.otpCounterSec) {
      this.otpResendCounter = diffSecs < 30 ? 30 - diffSecs : 0;
        this.otpCounterSec -= diffSecs;
        this.phoneNumber = parseOtpTime.phone;
        this.onPhoneVerifyStep = 'INPUT_OTP';
        this.phoneOtpInfo = this.setOtpInfo(parseOtpTime);
        this.startOtpCount('MOBILE');
    } else {
      localStorage.removeItem('phoneOtpTime');
    }
  }
  private emailDiffTime(parseOtpTime: any) {
    const diffSecs = this.calcDiffSecs(parseOtpTime.time);
    if (diffSecs < this.emailCounterSec) {
      this.emailResendCounter =  diffSecs < 30 ? 30 - diffSecs : 0;
      this.emailCounterSec -= diffSecs;
      this.email = parseOtpTime.email;
      this.onEmailVerifyResendStatus = 'CAN_RESEND';
      this.emailOtpInfo = this.setOtpInfo(parseOtpTime);
      this.startOtpCount('EMAIL');
    } else {
      localStorage.removeItem('emailOtpTime');
    }
  }
  private calcDiffSecs(parseOtpTime) {
    const currentTime = new Date().getTime();
    const storageTime = new Date(parseOtpTime).getTime();
    return Math.floor((currentTime - storageTime) / 1000);
  }
  private resetOtpSetting(type: string) {
    if (type === 'MOBILE') {
      clearInterval(this.otpInterval);
      this.otpResendCounter = 30;
      this.otpCounterSec = 900;
    } else {
      clearInterval(this.emailResendInterval);
      this.emailResendCounter = 30;
      this.emailCounterSec = 900;
    }
  }
  private setOtpInfo(parseOtpTime) {
    return {
      indexKey: parseOtpTime.indexKey,
      success: true,
      failCode: '',
      failReason: '',
    }
  }
  private storageOtpTime(type: string, otpInfo: OtpInfo) {
    type === 'MOBILE' ? this.phoneOtpInfo = otpInfo : this.emailOtpInfo = otpInfo;
    const info = {...this.setRegisterInfo(), time: new Date()}
    type === 'MOBILE' ? localStorage.setItem('phoneOtpTime',JSON.stringify(info))
                      : localStorage.setItem('emailOtpTime',JSON.stringify(info));
  }
  private startOtpSetting(type: string) {
    if (type === 'MOBILE') {
      this.onPhoneVerifyStep = 'INPUT_OTP';
    } else {
      this.onEmailVerifyResendStatus = 'CAN_RESEND';
      this.emailOtpConfirmVisable = true;
    }
  }
  private startOtpCount(type: string) {
    type === 'MOBILE' ? this.startPhoneCounter() : this.startEmailCounter();;
  }
  private startEmailCounter() {
    this.emailResendInterval = setInterval(() => {
      this.emailCounterSec -= 1;
      if (this.emailResendCounter !== 0) {
        this.emailResendCounter -= 1;
      }
      if (this.emailCounterSec === 0) {
        clearInterval(this.emailResendInterval);
      }
    }, 1000)
  }
  private startPhoneCounter() {
    this.otpInterval = setInterval(() => {
      this.otpCounterSec -= 1;
      if (this.otpResendCounter !== 0) {
        this.otpResendCounter -= 1;
      }
      if (this.otpCounterSec === 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'
        }
  }
  private storagePhoneOrEmail(registerInfo:RegisterInfo):void{
    const info = {...registerInfo, time: new Date()}
    localStorage.setItem('userInfo',JSON.stringify(info));
  }
  private removeOtpTime() {
    localStorage.removeItem('emailOtpTime');
    localStorage.removeItem('phoneOtpTime');
  }
  private setLoginInfo() {
    const isMobile = this.connectDevice === 'MOBILE'
    return {
      account: isMobile ? this.phoneNumber : this.email,
      indexKey: isMobile ? this.phoneOtpInfo.indexKey : this.emailOtpInfo.indexKey,
      otpCode: isMobile ? this.otpCode : this.emailOtpCode
    }
  }
}
PAMapp/pages/myAppointmentList.vue
Àɮפw§R°£
PAMapp/pages/myAppointmentList/appointmentList/appointment-list.component.scss
PAMapp/pages/myAppointmentList/appointmentList/appointment-list.component.ts
File was renamed from PAMapp/pages/myAppointmentList/appointmentList.vue
@@ -1,28 +1,3 @@
<template>
    <div>
        <el-input
            type="text"
            placeholder="請輸入關鍵字"
            class="mb-30 pam-clientReserved-input"
            v-model="keyWord"
            @keyup.enter.native="search"
        >
            <i slot="suffix" class="icon-search search cursor--pointer" @click="search"></i>
        </el-input>
        <ClientList
            :clients="pageList"
            :title="'reservedList'"
        ></ClientList>
        <UiPagination
            :totalList="filterList"
            @changePage="changePage"
        ></UiPagination>
    </div>
</template>
<script lang="ts">
import { Vue, Component, State, Watch } from 'nuxt-property-decorator';
import { ClientInfo } from '~/assets/ts/api/appointment';
@@ -64,4 +39,3 @@
    }
}
</script>
PAMapp/pages/myAppointmentList/appointmentList/index.vue
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,29 @@
<template>
    <div>
        <el-input
            type="text"
            placeholder="請輸入關鍵字"
            class="mb-30 pam-clientReserved-input"
            v-model="keyWord"
            @keyup.enter.native="search"
        >
            <i slot="suffix" class="icon-search search cursor--pointer" @click="search"></i>
        </el-input>
        <ClientList
            :clients="pageList"
            :title="'reservedList'"
        ></ClientList>
        <UiPagination
            :totalList="filterList"
            @changePage="changePage"
        ></UiPagination>
    </div>
</template>
<script src="./appointment-list.component.ts"></script>
<style lang="scss" scoped>
  @import "./appointment-list.component.scss";
</style>
PAMapp/pages/myAppointmentList/contactedList.vue
Àɮפw§R°£
PAMapp/pages/myAppointmentList/contactedList/contacted-list.component.scss
PAMapp/pages/myAppointmentList/contactedList/contacted-list.component.ts
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,34 @@
import { Vue, Component, Watch, State } from 'nuxt-property-decorator';
import { ClientInfo } from '~/assets/ts/api/appointment';
@Component
export default class ClientContactedList extends Vue {
    @State('myAppointmentList') myAppointmentList!: ClientInfo[];
    contactedList: ClientInfo[] = [];
    pageList: ClientInfo[] = [];
    keyWord: string = '';
    filterList: ClientInfo[] = [];
    @Watch('myAppointmentList')
    onMyAppointmentListChange() {
        this.contactedList = (this.myAppointmentList || [])
            .filter(item => item.communicateStatus === 'contacted')
            .sort((a, b) => a.contactTime > b.contactTime ? -1 : 1);
        this.filterList = this.contactedList;
    }
    mounted() {
        this.onMyAppointmentListChange();
    }
    changePage(pageList: ClientInfo[]) {
        this.pageList = pageList;
    }
    search() {
        this.filterList = this.contactedList.filter(item => {
            return item.name.match(this.keyWord) || item.requirement.match(this.keyWord)
        })
    }
}
PAMapp/pages/myAppointmentList/contactedList/index.vue
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,33 @@
<template>
    <div>
        <el-input
            type="text"
            placeholder="請輸入關鍵字"
            class="mb-30 pam-clientReserved-input"
            v-model="keyWord"
            @keyup.enter.native="search"
        >
            <i
                slot="suffix"
                class="icon-search search cursor--pointer"
                @click="search"
            ></i>
        </el-input>
        <ClientList
            :clients="pageList"
            :title="'contactedList'"
        ></ClientList>
        <UiPagination
            :totalList="filterList"
            @changePage="changePage"
        ></UiPagination>
    </div>
</template>
<script src="./contacted-list.component.ts"></script>
<style lang="scss" scoped>
  @import "./contacted-list.component.scss";
</style>
PAMapp/pages/myAppointmentList/index.vue
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,45 @@
<template>
    <div>
        <div class="pam-myAppointment-banner"></div>
        <div class="pam-container">
            <div class="pam-cus-tabs mb-30">
                <div
                    class="cus-tab-item"
                    :class="{'is-active': activeTabName === 'appointmentList'}"
                    @click="tabClick('appointmentList')"
                >客戶預約
                    <span class="p">({{appointmentList.length}})</span>
                </div>
                <div
                    class="cus-tab-item"
                    :class="{'is-active': activeTabName === 'contactedList'}"
                    @click="tabClick('contactedList')"
                >已聯絡
                    <span class="p">({{contactedList.length}})</span>
                </div>
            </div>
            <NuxtChild></NuxtChild>
        </div>
        <PopUpFrame
             :isOpen.sync="showNewAppointmentNumber"
        >
            <div class="text--center mdTxt">
                <p class="mb-50">你有 <span class="text--primary">{{newAppointmentNumber}}</span> å‰‡æ–°çš„預約</p>
                <div class="text--center">
                    <el-button
                        type="primary"
                        @click="showNewAppointmentNumber = false"
                    >我知道了</el-button>
                </div>
            </div>
        </PopUpFrame>
    </div>
</template>
<script src="./my-appointment.component.ts"></script>
<style lang="scss" scoped>
  @import "./my-appointment.component.scss";
</style>
PAMapp/pages/myAppointmentList/my-appointment.component.scss
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,26 @@
.pam-myAppointment-banner {
    width: 100%;
    height: 120px;
    background-size: cover;
    background-repeat: no-repeat;
    background-position: center;
    position: relative;
    background-image: url('~/assets/images/myAppointmentList/agent_banner_mob.svg');
}
@media (min-width: 768px) {
    .pam-myAppointment-banner {
        background-image: url('~/assets/images/myAppointmentList/agent_banner_web.svg');
    }
}
.pam-container {
    margin: 30px 20px;
}
@include desktop {
    .pam-container {
        width: 700px;
        margin: 30px auto;
    }
}
PAMapp/pages/myAppointmentList/my-appointment.component.ts
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,63 @@
import { Vue, Component, State, Action, Watch } from 'nuxt-property-decorator';
import { allAppointmentsView, ClientInfo } from '~/assets/ts/api/appointment';
import * as _ from 'lodash';
@Component({
    layout: 'home'
})
export default class ClientReservedList extends Vue {
    activeTabName = 'appointmentList';
    appointmentList: ClientInfo[] = [];
    contactedList: ClientInfo[] = [];
    clients: ClientInfo[] = [];
    newAppointmentNumber: number = 0;
    showNewAppointmentNumber = false;
    @State('myAppointmentList') myAppointmentList!: ClientInfo[];
    @Action storeMyAppointmentList!: () => Promise<number>;
    mounted() {
     this.storeMyAppointmentList().then(newDataLength => {
         this.newAppointmentNumber = newDataLength;
         if (this.newAppointmentNumber > 0) {
             this.showNewAppointmentNumber = true;
             allAppointmentsView().then(res => res);
         }
    });
     if (this.$route.name) {
         this.activeTabName = this.$route.name.split('-')[1]
     }
    }
    @Watch('myAppointmentList')
    onMyAppointmentListChange() {
        this.contactedList = this.myAppointmentList
            .filter(item => item.communicateStatus === 'contacted');
        this.appointmentList = this.myAppointmentList
            .filter(item => item.communicateStatus !== 'contacted');
    }
    tabClick(path: string) {
        this.activeTabName = path;
        this.$router.push('/myAppointmentList/' + this.activeTabName)
    }
    get route(): string{
        const routeName = this.$route.name;
        return routeName ? routeName:'';
    };
    get bannerClassName() {
        return this.routeFormatBannerClass(this.route);
    };
    // format to {page}-banner or pam-no-banner tag
    private routeFormatBannerClass(route: string): string {
        const needBannerTags = ['myAppointmentList-appointmentList', 'myAppointmentList-contactedList'];
        return _.includes(needBannerTags, route) ? route + '-banner' : 'pam-no-banner';
    };
}
PAMapp/pages/myConsultantList/consultantList.vue
Àɮפw§R°£
PAMapp/pages/myConsultantList/consultantList/consultant-list.component.scss
PAMapp/pages/myConsultantList/consultantList/consultant-list.component.ts
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,16 @@
import { Vue, Component, Prop, Getter } from 'nuxt-property-decorator';
import { Consultant } from '~/assets/ts/models/consultant.model';
@Component
export default class ConsultantPage extends Vue {
    @Prop() consultantList!: Consultant[];
    @Getter isLogin!: boolean;
    pageList: Consultant[] = [];
    changePage(pageList: Consultant[]) {
        this.pageList = pageList;
    }
}
PAMapp/pages/myConsultantList/consultantList/index.vue
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,18 @@
<template>
    <div>
        <ConsultantList
            :agents="pageList"
        ></ConsultantList>
        <UiPagination
            :totalList="consultantList"
            @changePage="changePage"
        ></UiPagination>
    </div>
</template>
<script src="./consultant-list.component.ts"></script>
<style lang="scss" scoped>
  @import "./consultant-list.component.scss";
</style>
PAMapp/pages/myConsultantList/contactedList.vue
Àɮפw§R°£
PAMapp/pages/myConsultantList/contactedList/contacted-list.component.scss
PAMapp/pages/myConsultantList/contactedList/contacted-list.component.ts
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,13 @@
import { Vue, Component, Prop } from 'nuxt-property-decorator' ;
import { Consultant } from '~/assets/ts/models/consultant.model';
@Component
export default class ContactedList extends Vue {
    @Prop() contactedList!: Consultant[];
    pageList: Consultant[] = [];
    changePage(pageList: Consultant[]) {
        this.pageList = pageList;
    }
}
PAMapp/pages/myConsultantList/contactedList/contactedList.vue
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,18 @@
<template>
    <div>
        <ConsultantList
            :agents="pageList"
        ></ConsultantList>
        <UiPagination
            :totalList="contactedList"
            @changePage="changePage"
        ></UiPagination>
    </div>
</template>
<script src="./contacted-list.component.ts"></script>
<style lang="scss" scoped>
  @import "./contacted-list.component.scss";
</style>
PAMapp/pages/myConsultantList/index.vue
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,41 @@
<template>
    <div>
        <div class="pam-cus-tabs mb-30">
            <div
                class="cus-tab-item"
                :class="{'is-active': activeTabName === 'consultantList'}"
                @click="tabClick('consultantList')"
            >顧問清單
                <span class="p">({{consultantList.length}})</span>
            </div>
            <div
                class="cus-tab-item"
                :class="{'is-active': activeTabName === 'contactedList'}"
                @click="tabClick('contactedList')"
            >已聯絡
                <span class="p">({{contactedList.length}})</span>
            </div>
        </div>
        <NuxtChild
            :contactedList="contactedList"
            :consultantList="consultantList"
        ></NuxtChild>
        <!-- <ConsultantList
            :agents="pageList"
        ></ConsultantList>
        <UiPagination
            :totalList="consultantList"
            @changePage="changePage"
        ></UiPagination> -->
    </div>
</template>
<script src="./my-consultant-list.component.ts"></script>
<style lang="scss" scoped>
  @import "./my-consultant-list.component.scss";
</style>
PAMapp/pages/myConsultantList/my-consultant-list.component.scss
PAMapp/pages/myConsultantList/my-consultant-list.component.ts
File was renamed from PAMapp/pages/myConsultantList.vue
@@ -1,30 +1,3 @@
<template>
    <div>
        <div class="pam-cus-tabs mb-30">
            <div
                class="cus-tab-item"
                :class="{'is-active': activeTabName === 'consultantList'}"
                @click="tabClick('consultantList')"
            >顧問清單
                <span class="p">({{consultantList.length}})</span>
            </div>
            <div
                class="cus-tab-item"
                :class="{'is-active': activeTabName === 'contactedList'}"
                @click="tabClick('contactedList')"
            >已聯絡
                <span class="p">({{contactedList.length}})</span>
            </div>
        </div>
        <NuxtChild
            :contactedList="contactedList"
            :consultantList="consultantList"
        ></NuxtChild>
    </div>
</template>
<script lang='ts'>
import { Vue, Component, Watch, State, Action } from 'nuxt-property-decorator';
import { Consultant } from '~/assets/ts/models/consultant.model';
@@ -66,4 +39,3 @@
    }
}
</script>
PAMapp/pages/questionnaire/_agentNo.vue
@@ -127,397 +127,8 @@
  </div>
</template>
<script lang="ts">
  import { Vue, Component } from 'nuxt-property-decorator';
  import { addFavoriteConsultant, appointmentDemand, AppointmentParams, AppointmentRequests ,RegisterInfo } from '~/assets/ts/api/consultant';
  import { getRequestsFromStorage, setRequestsToStorage, getRequestQuestionFromStorage, removeRequestQuestionFromStorage  } from '~/assets/ts/storageRequests';
  import { Gender } from '~/assets/ts/models/enum/Gender';
  import { ContactType } from '~/assets/ts/models/enum/ContactType';
  import _ from 'lodash';
  import { isLogin } from '~/assets/ts/auth';
<script src="./questionnaire.component.ts"></script>
  @Component
  export default class Questionnaire extends Vue {
    genderOptions=[
      {
        title:'男性',
        label:Gender.MALE,
      },
      {
        title:'女性',
        label:Gender.FEMALE,
      }
    ];
    requirementOptions=[
      {
        title:'健康與保障',
        label:'健康與保障',
      },
      {
        title:'子女教育',
        label:'子女教育',
      },
      {
        title:'資產規劃',
        label:'資產規劃',
      },
      {
        title:'樂活退休',
        label:'樂活退休',
      },
      {
        title:'保單健檢/規劃',
        label:'保單健檢/規劃',
      },
      {
        title:'分紅保單',
        label:'分紅保單',
      },
    ];
    ageRangeOptions=[
      {
        title:'20歲以下',
        label:'under_20',
      },
      {
        title:'21-30 æ­²',
        label:'21-30'
      },
      {
        title:'31-40 æ­²',
        label:'31-40'
      },
      {
        title:'41-50 æ­²',
        label:'41-50'
      },
      {
        title:'46-55 æ­²',
        label:'46-55',
      },
      {
        title:'51-60 æ­²',
        label:'51-60',
      },
      {
        title:'61-70 æ­²',
        label:'61-70',
      },
      {
        title:'71 æ­²ä»¥ä¸Š',
        label:'over_71',
      }
    ];
    quesAboutList = [
                  {
                      title:'健康與保障',
                      content:'唯有把身體照顧好,才是保障幸福之本,不做盲目燃燒的蠟燭,只做綻開的陽光,陪孩子多走一哩路,人生的美正要開展。'
                  },
                  {
                      title:'子女教育',
                      content:'孩子,我們是雙方的導師也是學生,面對未來要並肩作戰,學會勇敢無畏、克服挫折、善於理財,這條路上我們一起學。'
                  },
                  {
                      title:'資產規劃',
                      content:'真正的財富來自嚴謹規劃資產傳承,為人生蓋一堵抵禦財務風險的牆,確保資產穩健成長,替全家族的未來做好萬全準備。'
                  },
                  {
                      title:'樂活退休',
                      content:'拼一輩子,退休後的日子要輕鬆快活,就得提早透過保險商品規劃退休財務,替自己創造穩定收入,為精彩的熟年人生揭開序幕。'
                  },
                  {
                      title:'保單健檢/規劃',
                      content:'全面檢視自己的保障結構是否符合現在或未來的風險移轉需求。'
                  },
                  {
                      title:'分紅保單',
                      content:'分紅保單 åˆ†ç´…保單是兼具「分攤風險」與「紅利共享」特色的保單,具有一定穩定度,讓你可以同時享有壽險保障及紅利!'
                  }
    ];
    myRequest: AppointmentRequests = {
      phone          : this.userInfo?.phone ? this.userInfo.phone                               : '',
      email          : this.userInfo?.email ? this.userInfo.email                               : '',
      contactType    : _.isEqual(this.userInfo?.contactType,ContactType.SMS) ? ContactType.PHONE: ContactType.EMAIL,
      gender         : '',
      age            : '',
      job            : '',
      requirement    : [],
      hopeContactTime: [{
        selectWeekOptions : [],
        selectTimesOptions: [],
      }],
      agentNo: '',
    };
    showDrawer= false;
    sendReserve = false;
    beforeRouteEnter(to: any, from: any, next: any) {
      next(vm => {
        if (from.name === 'login' && !isLogin()) {
          vm.$router.go(-1);
          return;
        }
        if (!isLogin()) {
          vm.$router.push('/login');
        }
      })
    }
    mounted(): void {
      this.setMyRequest();
    }
    private setMyRequest(): void {
      const storageMyRequest = getRequestsFromStorage();
      const storageMyQuestion = getRequestQuestionFromStorage();
      if (storageMyRequest) {
        this.myRequest = {
          ...storageMyRequest,
          hopeContactTime: storageMyRequest.hopeContactTime?.length
                            ? storageMyRequest.hopeContactTime
                            : [{
                                selectWeekOptions: [],
                                selectTimesOptions: [],
                              }],
        };
      }
      if (storageMyQuestion) {
        this.myRequest = {
          ...this.myRequest,
          requirement: storageMyQuestion
        }
        removeRequestQuestionFromStorage();
      }
    }
    get phoneValid(): boolean {
      const rule = /^09[0-9]{8}$/;
      return this.myRequest.phone
            ? rule.test(this.myRequest.phone) && _.isEqual(this.myRequest.phone.length,10)
            : true;
    }
    get userInfo(): RegisterInfo {
      const initUserInfo = JSON.parse(localStorage.getItem('userInfo')!);
      return initUserInfo;
    }
    get isDisabledSubmitBtn(): boolean {
           return _.includes(this.myRequest.contactType,ContactType.PHONE)
      ? !this.isHopeContactTimeDone()
      : !this.phoneValid;
    }
    get isLogin() {
      return isLogin();
    }
    private isHopeContactTimeDone():boolean{
      return this.myRequest.hopeContactTime[0]?.selectWeekOptions.length >0 && this.myRequest.hopeContactTime[0]?.selectTimesOptions.length >0;
    }
    sentDemand() {
      addFavoriteConsultant([this.$route.params.agentNo]).then(res => this.sentAppointmentDemand());
    }
    private sentAppointmentDemand() {
        const data: AppointmentParams = {
          ...this.myRequest,
          requirement: _.map(this.myRequest.requirement,o=>o).toString(),
          hopeContactTime: this.myRequest.phone && this.phoneValid ? this.getHopeContactTime() :'',
          agentNo: this.$route.params.agentNo
        };
        appointmentDemand(data).then(res => {
            this.sendReserve = true;
            this.myRequest.hopeContactTime = [];
            setRequestsToStorage(this.myRequest);
        });
    }
    getHopeContactTime() {
        const selectedHopeContactTime = this.myRequest.hopeContactTime.filter((i) => i.selectWeekOptions?.length && i.selectTimesOptions?.length);
        return selectedHopeContactTime.map(i => {
            return `'${i.selectWeekOptions}、${i.selectTimesOptions}'`}
        ).toString();
    }
    closeReservePopUp() {
        this.sendReserve = false;
        this.$router.push('/')
    }
  }
</script>
<style lang="scss" scoped>
.sendReserve-txt{
    display: flex;
    justify-content: center;
    margin-top: 10px;
    margin-bottom: 26px;
}
//drawer最底下文字樣式
.qa-dialog-footer{
    display: flex;
    justify-content: center;
    margin-bottom: 81px;
    color: #ED1B2E;
    cursor: pointer;
}
//送出按鈕樣式與排版
.ques-footer{
    justify-content: center;
    margin: 30px 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    .el-button {
    width: 120px;
    height:50px;
    background-color: #ED1B2E;
    color:#FFFFFF;
    font-weight: normal;
    @extend .text--middle ;
    &.el-button--default {
        color: $PRIMARY_RED;
        background-color: #FFFFFF;
        border-color: $PRIMARY_RED;
    }
    &.el-button--primary {
        background-color: $PRIMARY_RED;
        border-color: $PRIMARY_RED;
    }
    &.is-disabled {
    color: $PRIMARY_WHITE;
    background-color: $MID_GREY;
    border-color: $MID_GREY;
    border-style: solid;
    pointer-events: none;
    }
  }
}
//詳細問題drawer中間內容空間大小設置
.qa-dialog{
    overflow-y:auto;
    height: 500px;
    margin-top: 20px;
}
//詳細問題drawer主要標題
.qaTextTitle{
    margin-top:30px;
    display: flex;
    justify-content: center;
}
.el-button+.el-button{
    margin-left: 0;
}
.datepicker{
    display: flex;
    flex-direction: column;
}
.required {
    position: relative;
    &::before {
        content: '*';
        position: absolute;
        color: #FF0000;
        transform: translate(-12px, 0);
    }
}
.ques-page--reset.pam-page-container {
    margin: 0px auto;
}
.ques-header {
    position: relative;
}
.ques-header__mob-banner {
  width: 100%;
  min-height: 80px;
  background-color: #F8F9FA;
  background-image: url('~/assets/images/questionnaire/reserve_bg_mob.svg');
  background-repeat: no-repeat;
  background-size: cover;
  background-position: center;
}
.ques-header__info {
  position: relative;
  padding:30px 20px;
  margin: 0px 20px;
  background-color: #B3E7E3;
  border-radius: 10px;
}
.ques-header__input-block {
  display: flex;
  align-items: center;
  @extend .text--middle,.mt-10 ;
  .ques-header__input{
    &.is-invalid{
      border: 2px solid $PRIMARY_RED !important;
    }
    flex: 1;
    height: 50px;
    border-radius: 10px;
    border: 1px #CCCCCC solid;
    background-color: $PRIMARY_WHITE;
    padding: 15px 10px;
    box-sizing: border-box;
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
  }
}
.ques-container {
  position: relative;
  margin: 0px 20px;
}
@include desktop{
  .ques-header{
    display: flex;
    justify-content: flex-end;
    min-height: 460px;
    background-image: url('~/assets/images/questionnaire/reserve_bg_web.svg');
    background-repeat: no-repeat;
    background-size: contain;
    background-position: bottom;
  }
  .ques-header__mob-banner{
    display: none;
  }
  .ques-header__info{
    margin: 30px 20px;
    width:500px;
    min-height: 400px;
    -webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
    -moz-box-sizing: border-box;    /* Firefox, other Gecko */
    box-sizing: border-box;
  }
  .ques-container{
    margin: 0px;
  }
}
<style lang="scss">
  @import "./questionnaire.component.scss";
</style>
PAMapp/pages/questionnaire/questionnaire.component.scss
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,159 @@
.sendReserve-txt{
    display: flex;
    justify-content: center;
    margin-top: 10px;
    margin-bottom: 26px;
}
//drawer最底下文字樣式
.qa-dialog-footer{
    display: flex;
    justify-content: center;
    margin-bottom: 81px;
    color: #ED1B2E;
    cursor: pointer;
}
//送出按鈕樣式與排版
.ques-footer{
    justify-content: center;
    margin: 30px 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    .el-button {
    width: 120px;
    height:50px;
    background-color: #ED1B2E;
    color:#FFFFFF;
    font-weight: normal;
    @extend .text--middle ;
    &.el-button--default {
        color: $PRIMARY_RED;
        background-color: #FFFFFF;
        border-color: $PRIMARY_RED;
    }
    &.el-button--primary {
        background-color: $PRIMARY_RED;
        border-color: $PRIMARY_RED;
    }
    &.is-disabled {
    color: $PRIMARY_WHITE;
    background-color: $MID_GREY;
    border-color: $MID_GREY;
    border-style: solid;
    pointer-events: none;
    }
  }
}
//詳細問題drawer中間內容空間大小設置
.qa-dialog{
    overflow-y:auto;
    height: 500px;
    margin-top: 20px;
}
//詳細問題drawer主要標題
.qaTextTitle{
    margin-top:30px;
    display: flex;
    justify-content: center;
}
.el-button+.el-button{
    margin-left: 0;
}
.datepicker{
    display: flex;
    flex-direction: column;
}
.required {
    position: relative;
    &::before {
        content: '*';
        position: absolute;
        color: #FF0000;
        transform: translate(-12px, 0);
    }
}
.ques-page--reset.pam-page-container {
    margin: 0px auto;
}
.ques-header {
    position: relative;
}
.ques-header__mob-banner {
  width: 100%;
  min-height: 80px;
  background-color: #F8F9FA;
  background-image: url('~/assets/images/questionnaire/reserve_bg_mob.svg');
  background-repeat: no-repeat;
  background-size: cover;
  background-position: center;
}
.ques-header__info {
  position: relative;
  padding:30px 20px;
  margin: 0px 20px;
  background-color: #B3E7E3;
  border-radius: 10px;
}
.ques-header__input-block {
  display: flex;
  align-items: center;
  @extend .text--middle,.mt-10 ;
  .ques-header__input{
    &.is-invalid{
      border: 2px solid $PRIMARY_RED !important;
    }
    flex: 1;
    height: 50px;
    border-radius: 10px;
    border: 1px #CCCCCC solid;
    background-color: $PRIMARY_WHITE;
    padding: 15px 10px;
    box-sizing: border-box;
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
  }
}
.ques-container {
  position: relative;
  margin: 0px 20px;
}
@include desktop{
  .ques-header{
    display: flex;
    justify-content: flex-end;
    min-height: 460px;
    background-image: url('~/assets/images/questionnaire/reserve_bg_web.svg');
    background-repeat: no-repeat;
    background-size: contain;
    background-position: bottom;
  }
  .ques-header__mob-banner{
    display: none;
  }
  .ques-header__info{
    margin: 30px 20px;
    width:500px;
    min-height: 400px;
    -webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
    -moz-box-sizing: border-box;    /* Firefox, other Gecko */
    box-sizing: border-box;
  }
  .ques-container{
    margin: 0px;
  }
}
PAMapp/pages/questionnaire/questionnaire.component.ts
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,228 @@
import { Vue, Component } from 'nuxt-property-decorator';
import { addFavoriteConsultant, appointmentDemand, AppointmentParams, AppointmentRequests ,RegisterInfo } from '~/assets/ts/api/consultant';
import { getRequestsFromStorage, setRequestsToStorage, getRequestQuestionFromStorage, removeRequestQuestionFromStorage  } from '~/assets/ts/storageRequests';
import { Gender } from '~/assets/ts/models/enum/gender.enum';
import { ContactType } from '~/assets/ts/models/enum/ContactType';
import _ from 'lodash';
import { isLogin } from '~/assets/ts/auth';
@Component
export default class Questionnaire extends Vue {
  genderOptions=[
    {
      title:'男性',
      label:Gender.MALE,
    },
    {
      title:'女性',
      label:Gender.FEMALE,
    }
  ];
  requirementOptions=[
    {
      title:'健康與保障',
      label:'健康與保障',
    },
    {
      title:'子女教育',
      label:'子女教育',
    },
    {
      title:'資產規劃',
      label:'資產規劃',
    },
    {
      title:'樂活退休',
      label:'樂活退休',
    },
    {
      title:'保單健檢/規劃',
      label:'保單健檢/規劃',
    },
    {
      title:'分紅保單',
      label:'分紅保單',
    },
  ];
  ageRangeOptions=[
    {
      title:'20歲以下',
      label:'under_20',
    },
    {
      title:'21-30 æ­²',
      label:'21-30'
    },
    {
      title:'31-40 æ­²',
      label:'31-40'
    },
    {
      title:'41-50 æ­²',
      label:'41-50'
    },
    {
      title:'46-55 æ­²',
      label:'46-55',
    },
    {
      title:'51-60 æ­²',
      label:'51-60',
    },
    {
      title:'61-70 æ­²',
      label:'61-70',
    },
    {
      title:'71 æ­²ä»¥ä¸Š',
      label:'over_71',
    }
  ];
  quesAboutList = [
                {
                    title:'健康與保障',
                    content:'唯有把身體照顧好,才是保障幸福之本,不做盲目燃燒的蠟燭,只做綻開的陽光,陪孩子多走一哩路,人生的美正要開展。'
                },
                {
                    title:'子女教育',
                    content:'孩子,我們是雙方的導師也是學生,面對未來要並肩作戰,學會勇敢無畏、克服挫折、善於理財,這條路上我們一起學。'
                },
                {
                    title:'資產規劃',
                    content:'真正的財富來自嚴謹規劃資產傳承,為人生蓋一堵抵禦財務風險的牆,確保資產穩健成長,替全家族的未來做好萬全準備。'
                },
                {
                    title:'樂活退休',
                    content:'拼一輩子,退休後的日子要輕鬆快活,就得提早透過保險商品規劃退休財務,替自己創造穩定收入,為精彩的熟年人生揭開序幕。'
                },
                {
                    title:'保單健檢/規劃',
                    content:'全面檢視自己的保障結構是否符合現在或未來的風險移轉需求。'
                },
                {
                    title:'分紅保單',
                    content:'分紅保單 åˆ†ç´…保單是兼具「分攤風險」與「紅利共享」特色的保單,具有一定穩定度,讓你可以同時享有壽險保障及紅利!'
                }
  ];
  myRequest: AppointmentRequests = {
    phone          : this.userInfo?.phone ? this.userInfo.phone                               : '',
    email          : this.userInfo?.email ? this.userInfo.email                               : '',
    contactType    : _.isEqual(this.userInfo?.contactType,ContactType.SMS) ? ContactType.PHONE: ContactType.EMAIL,
    gender         : '',
    age            : '',
    job            : '',
    requirement    : [],
    hopeContactTime: [{
      selectWeekOptions : [],
      selectTimesOptions: [],
    }],
    agentNo: '',
  };
  showDrawer= false;
  sendReserve = false;
  beforeRouteEnter(to: any, from: any, next: any) {
    next(vm => {
      if (from.name === 'login' && !isLogin()) {
        vm.$router.go(-1);
        return;
      }
      if (!isLogin()) {
        vm.$router.push('/login');
      }
    })
  }
  mounted(): void {
    this.setMyRequest();
  }
  private setMyRequest(): void {
    const storageMyRequest = getRequestsFromStorage();
    const storageMyQuestion = getRequestQuestionFromStorage();
    if (storageMyRequest) {
      this.myRequest = {
        ...storageMyRequest,
        hopeContactTime: storageMyRequest.hopeContactTime?.length
                          ? storageMyRequest.hopeContactTime
                          : [{
                              selectWeekOptions: [],
                              selectTimesOptions: [],
                            }],
      };
    }
    if (storageMyQuestion) {
      this.myRequest = {
        ...this.myRequest,
        requirement: storageMyQuestion
      }
      removeRequestQuestionFromStorage();
    }
  }
  get phoneValid(): boolean {
    const rule = /^09[0-9]{8}$/;
    return this.myRequest.phone
          ? rule.test(this.myRequest.phone) && _.isEqual(this.myRequest.phone.length,10)
          : true;
  }
  get userInfo(): RegisterInfo {
    const initUserInfo = JSON.parse(localStorage.getItem('userInfo')!);
    return initUserInfo;
  }
  get isDisabledSubmitBtn(): boolean {
         return _.includes(this.myRequest.contactType,ContactType.PHONE)
    ? !this.isHopeContactTimeDone()
    : !this.phoneValid;
  }
  get isLogin() {
    return isLogin();
  }
  private isHopeContactTimeDone():boolean{
    return this.myRequest.hopeContactTime[0]?.selectWeekOptions.length >0 && this.myRequest.hopeContactTime[0]?.selectTimesOptions.length >0;
  }
  sentDemand() {
    addFavoriteConsultant([this.$route.params.agentNo]).then(res => this.sentAppointmentDemand());
  }
  private sentAppointmentDemand() {
      const data: AppointmentParams = {
        ...this.myRequest,
        requirement: _.map(this.myRequest.requirement,o=>o).toString(),
        hopeContactTime: this.myRequest.phone && this.phoneValid ? this.getHopeContactTime() :'',
        agentNo: this.$route.params.agentNo
      };
      appointmentDemand(data).then(res => {
          this.sendReserve = true;
          this.myRequest.hopeContactTime = [];
          setRequestsToStorage(this.myRequest);
      });
  }
  getHopeContactTime() {
      const selectedHopeContactTime = this.myRequest.hopeContactTime.filter((i) => i.selectWeekOptions?.length && i.selectTimesOptions?.length);
      return selectedHopeContactTime.map(i => {
          return `'${i.selectWeekOptions}、${i.selectTimesOptions}'`}
      ).toString();
  }
  closeReservePopUp() {
      this.sendReserve = false;
      this.$router.push('/')
  }
}
PAMapp/pages/quickFilter/index.vue
@@ -64,213 +64,8 @@
    </div>
</template>
<script lang="ts">
import { Vue, Component, namespace } from 'nuxt-property-decorator';
import { FastQueryParams } from '~/assets/ts/api/consultant';
import { Consultant } from '~/assets/ts/models/consultant.model';
import { Selected } from '~/components/QuickFilter/QuickFilterSelector.vue';
import { fastQuery } from '~/assets/ts/api/consultant';
<script src="./quick-filter.component.ts"></script>
const localStorage = namespace('localStorage');
@Component
export default class QuickFilter extends Vue {
    @localStorage.Mutation storageQuickFilter!: (token: string) => void;
    @localStorage.Getter quickFilterSelectedData!: Selected[];
    isOpenQuestionPopUp = false;
    consultantList: Consultant[] = [];
    questionOption = {};
    confirmItem: Selected[] = [];
    questionList: QuestionOption[] = [
        {
            name: 'gender',
            title: '顧問性別',
            detail: [
                { name: '男性', value: 'male', className: 'btn_man'},
                { name: '女性', value: 'female', className: 'btn_woman'}
            ],
            type: 'radio'
        },
        {
            name: 'avgScore',
            title: '顧問滿意度',
            detail: [],
            type: ''
        },
        {
            name: 'communicationStyles',
            title: '溝通風格',
            detail: [
                { value: '謹慎務實', className: 'btn_owl'},
                { value: '明快主動', className: 'btn_tiger'},
                { value: '耐心傾聽', className: 'btn_koala'},
                { value: '健談風趣', className: 'btn_peacock'}
            ],
            type: 'checkbox'
        },
        // {
        //     name: 'status',
        //     title: '上線狀態',
        //     detail: [],
        //     type: 'radio'
        // }
    ];
    mounted() {
        if (this.quickFilterSelectedData && this.quickFilterSelectedData.length > 0) {
            this.confirmItem = this.quickFilterSelectedData;
            this.getRecommendList();
        }
    }
    gender(): string {
        const filter = this.confirmItem.filter(item => item.option === 'gender').map(i => i.value);
        return filter.length === 0 ? '' : filter[0];
    }
    avgScore(): number {
        const filter = this.confirmItem.filter(item => item.option === 'avgScore').map(i => i.value);
        return filter.length === 0 ? '' : filter[0];
    }
    communicationStyles(): string[] {
        return this.confirmItem.filter(item => item.option === 'communicationStyles').map(i => i.value);
    }
    isActive(name: string) {
        return name === 'gender' && !!this.gender()
            || name === 'avgScore' && !!this.avgScore()
            || name === 'communicationStyles' && !!this.communicationStyles().length
    }
    openPopUp(question: QuestionOption) {
        this.questionOption = question;
        this.isOpenQuestionPopUp =true;
    }
    removeTag(value: string) {
        this.confirmItem = this.confirmItem.filter(item => item.value !== value);
        this.confirmItem.length > 0 ? this.getRecommendList() : this.consultantList = [];
    }
    confirm(event: Selected) {
        this.setConfirmData(event);
        this.confirmItem.length > 0 ? this.getRecommendList() : this.consultantList = [];
        this.isOpenQuestionPopUp = false;
    }
    setConfirmData(event: Selected) {
        if (event.option === 'communicationStyles') {
            this.filterCommunicationStyles(event);
        } else {
            const findIndex = this.confirmItem.findIndex(item => item.option === event.option);
            findIndex === -1 ? this.confirmItem.push(event) : this.confirmItem[findIndex] = event;
        }
    }
    filterCommunicationStyles(event: Selected) {
        const confirmValue = this.confirmItem
            .filter(item => item.option === 'communicationStyles')
            .map(i => i.value);
        const pickerValue = event.value;
        this.confirmItem = this.confirmItem
            .filter(item => pickerValue.includes(item.value) || item.option !== 'communicationStyles');
        const addValue = pickerValue.filter(item => !confirmValue.includes(item)).map(i => {
            return {
                option: 'communicationStyles',
                value: i
            }
        })
        if (addValue.length > 0) {
            this.confirmItem.push(...addValue);
        }
    }
    getRecommendList() {
        const data: FastQueryParams = {
            gender: this.gender(),
            communicationStyles: this.communicationStyles(),
            avgScore: this.avgScore(),
            status: ''
        }
        fastQuery(data).then((res) => {
            this.consultantList = res.data;
            this.storageQuickFilter(JSON.stringify(this.confirmItem))
        })
    }
}
export interface QuestionOption {
    title: string;
    detail: Detail[];
    type: string;
    name: string;
}
interface Detail {
    value: string;
    name?: string;
    className: string;
}
</script>
<style lang="scss" scoped>
    .emptyBox {
        width: 100%;
        height: 100px;
        border: solid 1px $LIGHT_GREY;
        text-align: center;
        border-radius: 10px;
        .smTxt {
            line-height: 100px;
        }
    }
    .recommendStyle {
        box-shadow: 0 0 6px #00000029;
        background-color: $PRIMARY_WHITE;
    }
    .quickBtnBlock {
        display: flex;
        width: 100%;
        height: 132px;
        flex-wrap: wrap;
        justify-content: space-between;
        .quickBtn {
            width: 48%;
            height: 56px;
            text-align: center;
            box-shadow: 0 0 6px #22222229;
            border-radius: 10px;
            border-color: $CORAL;
            &.isActive {
                background-color: $CORAL;
                color: $PRIMARY_WHITE;
            }
        }
        .quickBtn+.quickBtn {
            margin-left: 0;
        }
    }
    .recommend {
        position: relative;
        .img {
            position: absolute;
            top: -50px;
            right: 10px;
        }
    }
</style>
<style lang="scss">
  @import "./quick-filter.component.scss";
</style>
PAMapp/pages/quickFilter/quick-filter.component.scss
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,52 @@
.emptyBox {
    width: 100%;
    height: 100px;
    border: solid 1px $LIGHT_GREY;
    text-align: center;
    border-radius: 10px;
    .smTxt {
        line-height: 100px;
    }
}
.recommendStyle {
    box-shadow: 0 0 6px #00000029;
    background-color: $PRIMARY_WHITE;
}
.quickBtnBlock {
    display: flex;
    width: 100%;
    height: 132px;
    flex-wrap: wrap;
    justify-content: space-between;
    .quickBtn {
        width: 48%;
        height: 56px;
        text-align: center;
        box-shadow: 0 0 6px #22222229;
        border-radius: 10px;
        border-color: $CORAL;
        &.isActive {
            background-color: $CORAL;
            color: $PRIMARY_WHITE;
        }
    }
    .quickBtn+.quickBtn {
        margin-left: 0;
    }
}
.recommend {
    position: relative;
    .img {
        position: absolute;
        top: -50px;
        right: 10px;
    }
}
PAMapp/pages/quickFilter/quick-filter.component.ts
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,144 @@
import { Vue, Component, namespace } from 'nuxt-property-decorator';
import { fastQuery } from '~/assets/ts/api/consultant';
import { QuestionOption } from '~/assets/ts/models/question-option.model';
import { Consultant } from '~/assets/ts/models/consultant.model';
import { Selected } from '~/assets/ts/models/selected.model';
import { FastQueryParams } from '~/assets/ts/models/fast-query-params.model';
const localStorage = namespace('localStorage');
@Component
export default class QuickFilter extends Vue {
    @localStorage.Mutation storageQuickFilter!: (token: string) => void;
    @localStorage.Getter quickFilterSelectedData!: Selected[];
    isOpenQuestionPopUp = false;
    consultantList: Consultant[] = [];
    questionOption = {};
    confirmItem: Selected[] = [];
    questionList: QuestionOption[] = [
        {
            name: 'gender',
            title: '顧問性別',
            detail: [
                { name: '男性', value: 'male', className: 'btn_man'},
                { name: '女性', value: 'female', className: 'btn_woman'}
            ],
            type: 'radio'
        },
        {
            name: 'avgScore',
            title: '顧問滿意度',
            detail: [],
            type: ''
        },
        {
            name: 'communicationStyles',
            title: '溝通風格',
            detail: [
                { value: '謹慎務實', className: 'btn_owl'},
                { value: '明快主動', className: 'btn_tiger'},
                { value: '耐心傾聽', className: 'btn_koala'},
                { value: '健談風趣', className: 'btn_peacock'}
            ],
            type: 'checkbox'
        },
        // {
        //     name: 'status',
        //     title: '上線狀態',
        //     detail: [],
        //     type: 'radio'
        // }
    ];
    mounted() {
        if (this.quickFilterSelectedData && this.quickFilterSelectedData.length > 0) {
            this.confirmItem = this.quickFilterSelectedData;
            this.getRecommendList();
        }
    }
    gender(): string {
        const filter = this.confirmItem.filter(item => item.option === 'gender').map(i => i.value);
        return filter.length === 0 ? '' : filter[0];
    }
    avgScore(): number {
        const filter = this.confirmItem.filter(item => item.option === 'avgScore').map(i => i.value);
        return filter.length === 0 ? '' : filter[0];
    }
    communicationStyles(): string[] {
        return this.confirmItem.filter(item => item.option === 'communicationStyles').map(i => i.value);
    }
    isActive(name: string) {
        return name === 'gender' && !!this.gender()
            || name === 'avgScore' && !!this.avgScore()
            || name === 'communicationStyles' && !!this.communicationStyles().length
    }
    openPopUp(question: QuestionOption) {
        this.questionOption = question;
        this.isOpenQuestionPopUp =true;
    }
    removeTag(value: string) {
        this.confirmItem = this.confirmItem.filter(item => item.value !== value);
        this.confirmItem.length > 0 ? this.getRecommendList() : this.consultantList = [];
    }
    confirm(event: Selected) {
        this.setConfirmData(event);
        this.confirmItem.length > 0 ? this.getRecommendList() : this.consultantList = [];
        this.isOpenQuestionPopUp = false;
    }
    setConfirmData(event: Selected) {
        if (event.option === 'communicationStyles') {
            this.filterCommunicationStyles(event);
        } else {
            const findIndex = this.confirmItem.findIndex(item => item.option === event.option);
            findIndex === -1 ? this.confirmItem.push(event) : this.confirmItem[findIndex] = event;
        }
    }
    filterCommunicationStyles(event: Selected) {
        const confirmValue = this.confirmItem
            .filter(item => item.option === 'communicationStyles')
            .map(i => i.value);
        const pickerValue = event.value;
        this.confirmItem = this.confirmItem
            .filter(item => pickerValue.includes(item.value) || item.option !== 'communicationStyles');
        const addValue = pickerValue.filter(item => !confirmValue.includes(item)).map(i => {
            return {
                option: 'communicationStyles',
                value: i
            }
        })
        if (addValue.length > 0) {
            this.confirmItem.push(...addValue);
        }
    }
    getRecommendList() {
        const data: FastQueryParams = {
            gender: this.gender(),
            communicationStyles: this.communicationStyles(),
            avgScore: this.avgScore(),
            status: ''
        }
        fastQuery(data).then((res) => {
            this.consultantList = res.data;
            this.storageQuickFilter(JSON.stringify(this.confirmItem))
        })
    }
}
PAMapp/pages/recommendConsultant/criteria.vue
Àɮפw§R°£
PAMapp/pages/recommendConsultant/index.vue
@@ -1,5 +1,5 @@
<template>
  <div class="pam-rec-cosultant-page">
  <div class="pam-rec-consultant-page">
    <div class="pb-10 mdTxt">顧問性別</div>
    <SingleSelectBtn :singleSelected.sync="strictQueryDto.gender" :options="genderOptions"/>
    <div class="pam-paragraph">
@@ -83,495 +83,8 @@
  </div>
</template>
<script lang="ts">
  import {
    Vue,
    Component,
    Mutation,
    namespace,
    Action,
    State
  } from 'nuxt-property-decorator';
  import * as _ from 'lodash';
  import { Seniority } from '~/assets/ts/models/enum/seniority';
  import { setRequestQuestionToStorage } from '~/assets/ts/storageRequests';
  const localStorage = namespace('localStorage');
  @Component
  export default class RecommendConsultant extends Vue {
    isVisiblePopUp = false;
    strictQueryDto: StrictQueryDto ={
      gender:'',
      area:'',
      status:'',
      requirements: [],
      otherRequirement:'',
      seniority:'',
      avgScore:0,
      popularTags: [],
      otherPopularTags:'',
    };
    genderOptions=[
      {
        title:'男性',
        label:Gender.MALE,
      },
      {
        title:'女性',
        label:Gender.FEMALE,
      }
    ];
    requirementOptions=[
      {
        title:'健康與保障',
        label:'健康與保障',
      },
      {
        title:'子女教育',
        label:'子女教育',
      },
      {
        title:'資產規劃',
        label:'資產規劃',
      },
      {
        title:'樂活退休',
        label:'樂活退休',
      },
      {
        title:'保單健檢/規劃',
        label:'保單健檢/規劃',
      },
      {
        title:'分紅保單',
        label:'分紅保單',
      },
    ];
    seniorityOptions=[
      {
        title:'不限',
        subTitle:'年齡不是問題',
        label:Seniority.UNLIMITED,
      },
      {
        title:'年輕',
        subTitle:'給年輕人一個機會',
        label:Seniority.YOUNG,
      },
      {
        title:'資深',
        subTitle:'薑是老的辣',
        label:Seniority.SENIOR,
      }
    ];
    popularOptions=[
      {
        title: '#防疫',
        label:'防疫'
      },
      {
        title: '#失能',
        label:'失能'
      },
      {
        title: '#防癌',
        label:'防癌'
      },
      {
        title: '#醫療',
        label:'醫療'
      },
      {
        title: '#壽險',
        label: '壽險'
      },
      {
        title: '#儲蓄',
        label:'儲蓄'
      },
      {
        title: '#投資',
        label:'投資'
      },
      {
        title: '#意外',
        label:'意外'
      }
    ];
    queaAboutList = [
      {
        title: '健康與保障',
        content: '唯有把身體照顧好,才是保障幸福之本,不做盲目燃燒的蠟燭,只做綻開的陽光,陪孩子多走一哩路,人生的美正要開展。'
      },
      {
        title: '子女教育',
        content: '孩子,我們是雙方的導師也是學生,面對未來要並肩作戰,學會勇敢無畏、克服挫折、善於理財,這條路上我們一起學。'
      },
      {
        title: '資產規劃',
        content: '真正的財富來自嚴謹規劃資產傳承,為人生蓋一堵抵禦財務風險的牆,確保資產穩健成長,替全家族的未來做好萬全準備。'
      },
      {
        title: '樂活退休',
        content: '拼一輩子,退休後的日子要輕鬆快活,就得提早透過保險商品規劃退休財務,替自己創造穩定收入,為精彩的熟年人生揭開序幕。'
      },
      {
        title: '保單健檢/規劃',
        content: '全面檢視自己的保障結構是否符合現在或未來的風險移轉需求。'
      },
      {
        title: '分紅保單',
        content: '分紅保單 åˆ†ç´…保單是兼具「分攤風險」與「紅利共享」特色的保單,具有一定穩定度,讓你可以同時享有壽險保障及紅利!'
      }
    ];
    showDialog = false;
    showAddress = false;
    @Mutation updateStrictQueryList!: (data: any) => void;
    @Action storeStrictQueryList!: (data: any) => Promise<number>;
    @State strictQueryList!: any;
    @localStorage.State recommendConsultantItem!: string;
    mounted() {
      if (!!this.recommendConsultantItem) {
        this.strictQueryDto = JSON.parse(this.recommendConsultantItem);
      }
    }
    async makePair() {
      await this.storeStrictQueryList(this.strictQueryDto).then(dataLength => {
        const questions = this.strictQueryDto.requirements.length ? this.strictQueryDto.requirements : [];
        setRequestQuestionToStorage(questions);
        if (dataLength === 0) {
          this.isVisiblePopUp = true;
          return;
        }
        this.$router.push('/recommendConsultant/result');
      });
    }
    get notFinishByRequireRules():boolean{
      const area = this.strictQueryDto.area;
      const requirementLength = this.strictQueryDto.requirements
        ? this.strictQueryDto.requirements.length
        : 0;
      return !(area && requirementLength >0)
    }
    confirmAddress(area: string) {
      this.strictQueryDto.area = area;
      this.showAddress = false;
    }
  }
  enum Gender{
    MALE="male",
    FEMALE="female",
  }
  export interface StrictQueryDto {
    gender: string,
    area: string,
    status: string,
    requirements: string[],
    otherRequirement: string,
    seniority: string,
    avgScore: number,
    popularTags: string[],
    otherPopularTags: string
  }
</script>
<script src="./recommend-consultant.component.ts"></script>
<style lang="scss">
.pam-rec-cosultant-page {
  .rec-pop-container{
    width:310px;
    .rec-pop-options{
      .el-checkbox-group{
        display: flex;
        flex-wrap: wrap;
        flex-direction: row;
        .el-checkbox{
          width:90px;
          height: 50px;
          padding:0;
          .el-checkbox__label{
            justify-content: center;
            align-items: center;
            display: flex;
            padding:15px 20px;
            text-align: center;
          }
        }
        .pam-selectAll-btn{
          margin-top: 60px;
          margin-left:-203px;
          height: 50px;
          width: 90px;
          padding: 10px;
        }
      }
    }
  }
  .rec-multi-select{
    .el-checkbox-group {
      display: flex;
      flex-direction: column;
      align-items: flex-start;
    }
  }
  input:focus,
  textarea:focus {
    outline: none;
  }
  .input {
    border: none;
    width: 90%;
    border-radius: 10px;
  }
  .job-pick {
    height: 50px;
    border-radius: 10px;
    border: 1px solid #D0D0CE;
    display: flex;
    justify-content: space-between;
    background-color: #FFFFFF;
  }
  .down-icon {
    color: #ED1B2E;
    font-size: 25px;
    align-self: center;
    margin-right: 15px;
  }
  .popOtherBtn {
    margin-left: -190px;
    margin-top: 45px;
  }
  .genderBtn {
    width: 80px;
    height: 47px;
    display: contents;
  }
  .qa-dialog {
    overflow-y: auto;
    height: 500px;
    margin-top: 20px;
    text-align: justify;
  }
  .qaTextTitle {
    margin-top: 30px
  }
  .qa-dialog-footer {
    display: flex;
    justify-content: center;
    margin-bottom: 81px;
    color: #ED1B2E;
    cursor: pointer;
  }
  .el-drawer__container ::-webkit-scrollbar {
    display: none;
  }
  .el-button+.el-button {
    margin-left: 0;
  }
  .seniority-choice {
    display: flex;
    flex-wrap: wrap;
  }
  .area-choice {
    height: 50px;
    border-radius: 10px;
    border: 1px solid #D0D0CE;
    display: flex;
    justify-content: space-between;
    background-color: #FFFFFF;
  }
  .area-icon {
    color: #ED1B2E;
    font-size: 25px;
    display: flex;
    justify-content: flex-end;
    padding-right: 16px;
    padding-top: 11px;
  }
  input::-webkit-input-placeholder {
    font-size: 20px;
    padding-left: 10px;
  }
  .el-button.is-disabled {
    font-size: 20px;
    border-radius: 20px;
    color: #FFFFFF;
    background-color: #A7A8AA;
    border: 1px solid #A7A8AA;
  }
  .rec-footer {
    height: 70px;
    display: flex;
    justify-content: center;
    align-items: center;
  }
  .other-PopBtn {
    width: 90px;
    height: 47px;
    margin-top: 10px;
  }
  .rec-ques-location {
    display: flex;
    align-items: center;
  }
  .pop-tag {
    display: flex;
    flex-wrap: wrap;
    margin: -10px;
  }
  .rec-popular {
    display: flex;
    align-items: baseline;
    padding-top: 10px;
    margin-bottom: 10px;
  }
  .rec-btn-type {
    padding-bottom: 10px;
  }
  .rec-question {
    display: flex;
    flex-direction: column;
  }
  .rec-banner {
    height: 120px;
    background-color: #D0D0CE;
  }
  .rec-btn {
    font-size: 20px;
    border-radius: 20px;
    color: black;
    border: 1px solid #D0D0CE;
  }
  .rec-pop-btn {
    font-size: 20px;
    border-radius: 20px;
    color: black;
    margin: 0px 0px 10px 10px;
    border: 1px solid #D0D0CE;
    width: 90px;
    height: 47px;
  }
  .el-progress__text {
    display: none;
  }
  .el-progress-bar {
    padding-right: 0;
  }
  .el-progress-bar__inner {
    background-color: #ED1B2E;
  }
  .required {
    position: relative;
    &::before {
      content: '*';
      position: absolute;
      color: #FF0000;
      transform: translate(-12px, 0);
      z-index: 5;
    }
  }
  .area-txt {
    display: flex;
    align-items: center;
    margin-left: 18px;
  }
  @include desktop {
    .desktopBtn {
      margin-right: 10px;
      height: 47px
    }
    .popOtherBtn {
      margin-left: 10px;
      margin-top: -10px;
    }
    .rec-pop-container{
      width:auto;
    .rec-pop-options{
      .el-checkbox-group{
        display: flex;
        flex-wrap:wrap;
        flex-direction: none;
        .el-checkbox{
          width:90px;
          height: 50px;
          padding:0;
          .el-checkbox__label{
            justify-content: center;
            align-items: center;
            display: flex;
            padding:15px 20px;
            text-align: center;
          }
        }
        .pam-selectAll-btn{
          margin-top:0px;
          margin-left:0px;
          height: 50px;
          width: 90px;
          padding: 10px;
        }
      }
    }
  }
    .rec-multi-select{
    .el-checkbox-group {
      display: flex;
      flex-direction: row;
      align-items: flex-start;
      flex-wrap: wrap;
    }
  }
  }
}
  @import "./recommend-consultant.component.scss";
</style>
PAMapp/pages/recommendConsultant/recommend-consultant.component.scss
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,291 @@
.pam-rec-consultant-page {
  .rec-pop-container{
    width:310px;
    .rec-pop-options{
      .el-checkbox-group{
        display: flex;
        flex-wrap: wrap;
        flex-direction: row;
        .el-checkbox{
          width:90px;
          height: 50px;
          padding:0;
          .el-checkbox__label{
            justify-content: center;
            align-items: center;
            display: flex;
            padding:15px 20px;
            text-align: center;
          }
        }
        .pam-selectAll-btn{
          margin-top: 60px;
          margin-left:-203px;
          height: 50px;
          width: 90px;
          padding: 10px;
        }
      }
    }
  }
  .rec-multi-select{
    .el-checkbox-group {
      display: flex;
      flex-direction: column;
      align-items: flex-start;
    }
  }
  input:focus,
  textarea:focus {
    outline: none;
  }
  .input {
    border: none;
    width: 90%;
    border-radius: 10px;
  }
  .job-pick {
    height: 50px;
    border-radius: 10px;
    border: 1px solid #D0D0CE;
    display: flex;
    justify-content: space-between;
    background-color: #FFFFFF;
  }
  .down-icon {
    color: #ED1B2E;
    font-size: 25px;
    align-self: center;
    margin-right: 15px;
  }
  .popOtherBtn {
    margin-left: -190px;
    margin-top: 45px;
  }
  .genderBtn {
    width: 80px;
    height: 47px;
    display: contents;
  }
  .qa-dialog {
    overflow-y: auto;
    height: 500px;
    margin-top: 20px;
    text-align: justify;
  }
  .qaTextTitle {
    margin-top: 30px
  }
  .qa-dialog-footer {
    display: flex;
    justify-content: center;
    margin-bottom: 81px;
    color: #ED1B2E;
    cursor: pointer;
  }
  .el-drawer__container ::-webkit-scrollbar {
    display: none;
  }
  .el-button+.el-button {
    margin-left: 0;
  }
  .seniority-choice {
    display: flex;
    flex-wrap: wrap;
  }
  .area-choice {
    height: 50px;
    border-radius: 10px;
    border: 1px solid #D0D0CE;
    display: flex;
    justify-content: space-between;
    background-color: #FFFFFF;
  }
  .area-icon {
    color: #ED1B2E;
    font-size: 25px;
    display: flex;
    justify-content: flex-end;
    padding-right: 16px;
    padding-top: 11px;
  }
  input::-webkit-input-placeholder {
    font-size: 20px;
    padding-left: 10px;
  }
  .el-button.is-disabled {
    font-size: 20px;
    border-radius: 20px;
    color: #FFFFFF;
    background-color: #A7A8AA;
    border: 1px solid #A7A8AA;
  }
  .rec-footer {
    height: 70px;
    display: flex;
    justify-content: center;
    align-items: center;
  }
  .other-PopBtn {
    width: 90px;
    height: 47px;
    margin-top: 10px;
  }
  .rec-ques-location {
    display: flex;
    align-items: center;
  }
  .pop-tag {
    display: flex;
    flex-wrap: wrap;
    margin: -10px;
  }
  .rec-popular {
    display: flex;
    align-items: baseline;
    padding-top: 10px;
    margin-bottom: 10px;
  }
  .rec-btn-type {
    padding-bottom: 10px;
  }
  .rec-question {
    display: flex;
    flex-direction: column;
  }
  .rec-banner {
    height: 120px;
    background-color: #D0D0CE;
  }
  .rec-btn {
    font-size: 20px;
    border-radius: 20px;
    color: black;
    border: 1px solid #D0D0CE;
  }
  .rec-pop-btn {
    font-size: 20px;
    border-radius: 20px;
    color: black;
    margin: 0px 0px 10px 10px;
    border: 1px solid #D0D0CE;
    width: 90px;
    height: 47px;
  }
  .el-progress__text {
    display: none;
  }
  .el-progress-bar {
    padding-right: 0;
  }
  .el-progress-bar__inner {
    background-color: #ED1B2E;
  }
  .required {
    position: relative;
    &::before {
      content: '*';
      position: absolute;
      color: #FF0000;
      transform: translate(-12px, 0);
      z-index: 5;
    }
  }
  .area-txt {
    display: flex;
    align-items: center;
    margin-left: 18px;
  }
  @include desktop {
    .desktopBtn {
      margin-right: 10px;
      height: 47px
    }
    .popOtherBtn {
      margin-left: 10px;
      margin-top: -10px;
    }
    .rec-pop-container{
      width:auto;
    .rec-pop-options{
      .el-checkbox-group{
        display: flex;
        flex-wrap:wrap;
        flex-direction: none;
        .el-checkbox{
          width:90px;
          height: 50px;
          padding:0;
          .el-checkbox__label{
            justify-content: center;
            align-items: center;
            display: flex;
            padding:15px 20px;
            text-align: center;
          }
        }
        .pam-selectAll-btn{
          margin-top:0px;
          margin-left:0px;
          height: 50px;
          width: 90px;
          padding: 10px;
        }
      }
    }
  }
    .rec-multi-select{
    .el-checkbox-group {
      display: flex;
      flex-direction: row;
      align-items: flex-start;
      flex-wrap: wrap;
    }
  }
  }
}
PAMapp/pages/recommendConsultant/recommend-consultant.component.ts
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,183 @@
import {
  Vue,
  Component,
  Mutation,
  namespace,
  Action,
  State
} from 'nuxt-property-decorator';
import * as _ from 'lodash';
import { Seniority } from '~/assets/ts/models/enum/seniority';
import { setRequestQuestionToStorage } from '~/assets/ts/storageRequests';
import { Gender } from '~/assets/ts/models/enum/gender.enum';
import { StrictQueryDto } from '~/assets/ts/models/strict-query-dto.model';
const localStorage = namespace('localStorage');
@Component
export default class RecommendConsultant extends Vue {
  isVisiblePopUp = false;
  strictQueryDto: StrictQueryDto ={
    gender:'',
    area:'',
    status:'',
    requirements: [],
    otherRequirement:'',
    seniority:'',
    avgScore:0,
    popularTags: [],
    otherPopularTags:'',
  };
  genderOptions=[
    {
      title:'男性',
      label:Gender.MALE,
    },
    {
      title:'女性',
      label:Gender.FEMALE,
    }
  ];
  requirementOptions=[
    {
      title:'健康與保障',
      label:'健康與保障',
    },
    {
      title:'子女教育',
      label:'子女教育',
    },
    {
      title:'資產規劃',
      label:'資產規劃',
    },
    {
      title:'樂活退休',
      label:'樂活退休',
    },
    {
      title:'保單健檢/規劃',
      label:'保單健檢/規劃',
    },
    {
      title:'分紅保單',
      label:'分紅保單',
    },
  ];
  seniorityOptions=[
    {
      title:'不限',
      subTitle:'年齡不是問題',
      label:Seniority.UNLIMITED,
    },
    {
      title:'年輕',
      subTitle:'給年輕人一個機會',
      label:Seniority.YOUNG,
    },
    {
      title:'資深',
      subTitle:'薑是老的辣',
      label:Seniority.SENIOR,
    }
  ];
  popularOptions=[
    {
      title: '#防疫',
      label:'防疫'
    },
    {
      title: '#失能',
      label:'失能'
    },
    {
      title: '#防癌',
      label:'防癌'
    },
    {
      title: '#醫療',
      label:'醫療'
    },
    {
      title: '#壽險',
      label: '壽險'
    },
    {
      title: '#儲蓄',
      label:'儲蓄'
    },
    {
      title: '#投資',
      label:'投資'
    },
    {
      title: '#意外',
      label:'意外'
    }
  ];
  queaAboutList = [
    {
      title: '健康與保障',
      content: '唯有把身體照顧好,才是保障幸福之本,不做盲目燃燒的蠟燭,只做綻開的陽光,陪孩子多走一哩路,人生的美正要開展。'
    },
    {
      title: '子女教育',
      content: '孩子,我們是雙方的導師也是學生,面對未來要並肩作戰,學會勇敢無畏、克服挫折、善於理財,這條路上我們一起學。'
    },
    {
      title: '資產規劃',
      content: '真正的財富來自嚴謹規劃資產傳承,為人生蓋一堵抵禦財務風險的牆,確保資產穩健成長,替全家族的未來做好萬全準備。'
    },
    {
      title: '樂活退休',
      content: '拼一輩子,退休後的日子要輕鬆快活,就得提早透過保險商品規劃退休財務,替自己創造穩定收入,為精彩的熟年人生揭開序幕。'
    },
    {
      title: '保單健檢/規劃',
      content: '全面檢視自己的保障結構是否符合現在或未來的風險移轉需求。'
    },
    {
      title: '分紅保單',
      content: '分紅保單 åˆ†ç´…保單是兼具「分攤風險」與「紅利共享」特色的保單,具有一定穩定度,讓你可以同時享有壽險保障及紅利!'
    }
  ];
  showDialog = false;
  showAddress = false;
  @Mutation updateStrictQueryList!: (data: any) => void;
  @Action storeStrictQueryList!: (data: any) => Promise<number>;
  @State strictQueryList!: any;
  @localStorage.State recommendConsultantItem!: string;
  mounted() {
    if (!!this.recommendConsultantItem) {
      this.strictQueryDto = JSON.parse(this.recommendConsultantItem);
    }
  }
  async makePair() {
    await this.storeStrictQueryList(this.strictQueryDto).then(dataLength => {
      const questions = this.strictQueryDto.requirements.length ? this.strictQueryDto.requirements : [];
      setRequestQuestionToStorage(questions);
      if (dataLength === 0) {
        this.isVisiblePopUp = true;
        return;
      }
      this.$router.push('/recommendConsultant/result');
    });
  }
  get notFinishByRequireRules():boolean{
    const area = this.strictQueryDto.area;
    const requirementLength = this.strictQueryDto.requirements
      ? this.strictQueryDto.requirements.length
      : 0;
    return !(area && requirementLength >0)
  }
  confirmAddress(area: string) {
    this.strictQueryDto.area = area;
    this.showAddress = false;
  }
}
PAMapp/pages/recommendConsultant/result.vue
Àɮפw§R°£
PAMapp/pages/recommendConsultant/result/index.vue
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,88 @@
<template>
<div>
    <div class="mdTxt pb-10">嚴選顧問推薦</div>
    <ul class="pam-rec-agent__list">
        <li class="pam-rec-agent-card" v-for="(info,index) in pageList" :key="index">
            <div class="pam-rec-agent-card__content">
                <div class="pam-rec-agent-card__content-header">
                    <div class="pam-rec-agent-card__avatar">
                        <UiAvatar :fileName="info.img" ></UiAvatar>
                    </div>
                    <div class="pam-rec-agent-card__main-info">
                        <div class="text--middle  pt-10 rec-desktop-name">{{ info.name }}</div>
                        <div class="rec-role">{{ info.role }}</div>
                        <span class="rec-detail fix-chrome-click--issue"  @click="showAgentDetail(info.agentNo)">詳細資料</span>
                    </div>
                </div>
                <div class="pam-rec-agent-card__content-body">
                    <el-row type="flex" class="pam-paragraph">
                        <div class="field">
                            <div class="field__label">專長領域</div>
                            <div class="field__content expertieses-container">
                                <div class="pr-10 pb-10" v-for="(expert, index) in info.expertise" :key="index">
                                    #{{ expert }}
                                </div>
                            </div>
                        </div>
                    </el-row>
                    <el-row type="flex" class="pam-paragraph">
                        <el-col :span="12">
                            <div class="field__label">
                            æœå‹™è³‡æ­·
                            </div>
                            <div class="field__content">
                            {{ info.seniority }}
                            </div>
                        </el-col>
                        <el-col :span="12">
                            <div class="field__label">
                            å®¢æˆ¶æ»¿æ„åº¦
                            </div>
                            <div class="field__content">
                                <i class="icon-star" style="color:#F2C75C"></i>
                            {{ info.avgScore }}
                            </div>
                        </el-col>
                    </el-row>
                </div>
                <div class="pam-rec-agent-card__content-footer">
                    <AddAndReservedBtns
                        :cusClass="'pam-rec-btns'"
                        :agentInfo="info"
                        @openPopUp="openPopUp"
                    ></AddAndReservedBtns>
                </div>
            </div>
        </li>
    </ul>
    <UiPagination
        class="mb-30"
        :totalList="strictQueryList"
        @changePage="changePage"
        :pageSize = 6
    ></UiPagination>
    <PopUpFrame :isOpen.sync="isVisiblePopUp"
      >
        <div class="text--center mdTxt">
            <p class="mb-50">{{popUpTxt}}</p>
            <div class="text--center">
                <el-button
                    type="primary"
                    @click="isVisiblePopUp = false"
                >我知道了</el-button>
            </div>
        </div>
    </PopUpFrame>
</div>
</template>
<script src="./recommend-consultant-result.component.ts"></script>
<style lang="scss" scoped>
  @import "./recommend-consultant-result.component.scss";
</style>
PAMapp/pages/recommendConsultant/result/recommend-consultant-result.component.scss
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,114 @@
.pam-rec-agent-card {
  margin-bottom: 10px;
  border-radius: 10px;
  border: 1px solid $LIGHT_GREY;
  padding: 20px 33px;
  .pam-rec-agent-card__content {
      .pam-rec-agent-card__content-header {
          display: flex;
          .pam-rec-agent-card__avatar {
              display: flex;
              flex-direction: row;
              margin-right: 20px;
          }
          .pam-rec-agent-card__main-info {
              display: flex;
              flex-direction: column;
              justify-content: flex-end;
              .rec-role {
                  font-size: 16px;
                  color: $PRUDENTIAL_GREY;
                  font-weight: bold;
                  margin-top: 4px;
              }
              .rec-detail{
                  font-size: 20px;
                  color:$PRIMARY_RED;
                  font-weight: bold;
                  padding-top: 30px;
                  cursor: pointer;
              }
          }
      }
      .pam-rec-agent-card__content-body {
          height: 200px;
      }
  }
}
.field__label {
  font-size: 16px;
  color: $PRUDENTIAL_GREY;
  font-weight:bold;
  margin-bottom: 7px;
}
.field__content{
  font-size: 18px;
}
.expertieses-container {
  display: flex;
  flex-wrap: wrap;
}
@include desktop{
  .pam-rec-agent__list{
      display: flex;
      flex-wrap: wrap;
      flex-direction:row;
      width: 100%;
  }
  .pam-paragraph{
      margin-top: 10px;
  }
  .pam-rec-agent-card {
      border-radius: 10px;
      border: 1px solid $LIGHT_GREY;
      padding: 15px 20px 15px 20px;
      width: 170px;
      margin: 0 10px 10px 10px;
      .pam-rec-agent-card__content {
          .pam-rec-agent-card__content-header {
              display: flex;
              .pam-rec-agent-card__avatar {
                  display: flex;
                  flex-direction: row;
                  margin-right: 20px;
              }
              .pam-rec-agent-card__main-info {
                  display: flex;
                  flex-direction: column;
                  justify-content: center;
                  .rec-desktop-name{
                      font-size: 12px;
                      font-weight: bold;
                  }
                  .rec-role {
                      font-size: 12px;
                      color:$PRUDENTIAL_GREY;
                  }
                  .rec-detail{
                      font-size: 12px;
                      color:$PRIMARY_RED;
                      font-weight: bold;
                      padding-top: 10px;
                  }
              }
          }
      }
  }
  .field__label {
      font-size: 12px;
      color: $PRUDENTIAL_GREY;
      font-weight:bold;
      margin-bottom: 7px;
  }
  .field__content{
      font-size: 12px;
  }
  .expertieses-container {
      display: flex;
      flex-wrap: wrap;
  }
}
PAMapp/pages/recommendConsultant/result/recommend-consultant-result.component.ts
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,32 @@
import {Vue,Component, State, namespace, Action} from 'nuxt-property-decorator';
import { AgentOfStrictQuery } from '~/assets/ts/api/consultant';
const localStorage = namespace('localStorage');
@Component
export default class RecommendConsultantResult extends Vue{
    @State('strictQueryList') strictQueryList!: AgentOfStrictQuery[];
    @Action storeStrictQueryList!: (data: any) => Promise<number>;
    @localStorage.State recommendConsultantItem!: string;
    pageList: any[] = [];
    isVisiblePopUp = false;
    popUpTxt = '';
    mounted() {
        if (this.recommendConsultantItem && this.strictQueryList.length === 0) {
            const strictQueryDto = JSON.parse(this.recommendConsultantItem);
            this.storeStrictQueryList(strictQueryDto);
        }
    }
    changePage(pageList: any[]) {
        this.pageList = pageList;
    }
    showAgentDetail(agentNo: string): void {
        this.$router.push(`/agentInfo/${agentNo}`);
    }
    openPopUp(txt: string) {
        this.popUpTxt = txt;
        this.isVisiblePopUp = true;
    }
}
PAMapp/pages/record.vue
Àɮפw§R°£
PAMapp/pages/record/contactRecord.vue
Àɮפw§R°£
PAMapp/pages/record/index.vue
@@ -12,7 +12,7 @@
    </section>
    <section class="user-reviews-content">
        <div
        <div
            class="user-reviews-card"
            v-for="(appointmentLog, index) in myAppointmentReviewLogList"
            :key="index">
@@ -24,92 +24,25 @@
            </div>
            <div class="user-reviews-card-date">
                <div class="date">
                    <UiDateFormat
                    <UiDateFormat
                        :date="appointmentLog.lastModifiedDate"
                        onlyShowSection="DAY" />
                </div>
                <div class="time">
                    <UiDateFormat
                    <UiDateFormat
                        :date="appointmentLog.lastModifiedDate"
                        onlyShowSection="TIME" />
                </div>
            </div>
        </div>
    </section>
</div>
</template>
<script lang="ts">
import { Vue, Component, Action, State, namespace } from 'nuxt-property-decorator';
import { AppointmentLog } from '~/assets/ts/models/appointment.model';
const roleStorage = namespace('localStorage');
<script src="./record.component.ts"></script>
@Component
export default  class Reviews extends Vue{
    today = new Date();
    @roleStorage.Getter currentRole!:string;
    @State('myAppointmentReviewLogList') myAppointmentReviewLogList!: AppointmentLog[];
    @Action storeMyAppointmentReviewLog!: any;
    appointmentLogList: AppointmentLog[] = [];
    mounted() {
        this.storeMyAppointmentReviewLog();
    }
}
</script>
<style lang="scss" scoped>
.user-reviews-page{
    margin-bottom:155px;
    .user-reviews-header{
        height: 43px;
        margin-top: 28px;
        display: flex;
        justify-content: center;
        border-bottom: 2px solid black;
    }
    .user-reviews-content{
        .user-reviews-card{
            display: flex;
            justify-content: space-between;
            margin-top: 26px;
            border-bottom: 1px solid #707070;
            height: 54px;
            padding-bottom: 15px;
            .user-reviews-card-content{
                width: 242px;
                padding-right:50px;
                line-height: 1.2;
                font-size: 20px;
                margin-left: 15px;
            }
            .user-reviews-card-date{
                font-size: 12px;
                display: flex;
                flex-direction: column;
                align-items: flex-end;
                margin-right: 15px;
                width:52px;
                .date{
                    margin-bottom: 2px;
                }
            }
        }
    }
}
@include desktop{
    .user-reviews-card-content{
        flex: 1;
    }
}
</style>
<style lang="scss">
  @import "./record.component.scss";
</style>
PAMapp/pages/record/record.component.scss
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,44 @@
.user-reviews-page{
  margin-bottom:155px;
  .user-reviews-header{
      height: 43px;
      margin-top: 28px;
      display: flex;
      justify-content: center;
      border-bottom: 2px solid black;
  }
  .user-reviews-content{
      .user-reviews-card{
          display: flex;
          justify-content: space-between;
          margin-top: 26px;
          border-bottom: 1px solid #707070;
          height: 54px;
          padding-bottom: 15px;
          .user-reviews-card-content{
              width: 242px;
              padding-right:50px;
              line-height: 1.2;
              font-size: 20px;
              margin-left: 15px;
          }
          .user-reviews-card-date{
              font-size: 12px;
              display: flex;
              flex-direction: column;
              align-items: flex-end;
              margin-right: 15px;
              width:52px;
              .date{
                  margin-bottom: 2px;
              }
          }
      }
  }
}
@include desktop{
  .user-reviews-card-content{
      flex: 1;
  }
}
PAMapp/pages/record/record.component.ts
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,23 @@
import { Vue, Component, Action, State, namespace } from 'nuxt-property-decorator';
import { AppointmentLog } from '~/assets/ts/models/appointment.model';
const roleStorage = namespace('localStorage');
@Component
export default  class Reviews extends Vue{
    today = new Date();
    @roleStorage.Getter currentRole!:string;
    @State('myAppointmentReviewLogList') myAppointmentReviewLogList!: AppointmentLog[];
    @Action storeMyAppointmentReviewLog!: any;
    appointmentLogList: AppointmentLog[] = [];
    mounted() {
        this.storeMyAppointmentReviewLog();
    }
}
PAMapp/pages/record/reviews.vue
Àɮפw§R°£
PAMapp/pages/userReviews/index.vue
@@ -1,9 +1,9 @@
<template>
<template>
<div class="reviews-page">
    <!-- é¡§å®¢ç™¼é€æ»¿æ„åº¦çµ¦é¡§å• -->
    <div class="reviews-banner"></div>
    <section class="reviews-container">
    <section class="reviews-container">
        <section class="reviews-header">
            <div class="reviews-header-container">
                <div class="reviews-header-title">滿意度調查</div>
@@ -20,17 +20,17 @@
                    <div class="card-txt">
                        å°æ–¼é¡§å•
                        <span class="p">{{item.name}}</span>的整體服務,您給予幾顆星的評價?
                        <div
                        <div
                            class="card-score"
                            v-if="!isMobileDevice">
                            <el-rate class="user-reviews-rate" v-model="item.avgScore"></el-rate>
                        </div>
                    </div>
                </div>
                <div
                <div
                    class="card-score"
                    v-if="isMobileDevice">
                    <el-rate
                    <el-rate
                        class="user-reviews-rate"
                        v-model="item.avgScore"></el-rate>
                </div>
@@ -49,164 +49,13 @@
            <el-button type="primary" class="reviews-dialog-btn" @click.native="reviewsDialogCheck">我知道了</el-button>
        </div>
    </PopUpFrame>
</div>
</template>
<script lang="ts">
import { Vue,Component } from 'vue-property-decorator'
import { isMobileDevice } from '~/assets/ts/device';
<script src="./user-reviews.component.ts"></script>
@Component({
    layout: 'home'
})
export default class UserReviews extends Vue{
    isMobileDevice = true;
    showReviews = false;
    reviewsList:ReviewsList[] = [
        {
            avatar:'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
            name:'蔡美眉',
            avgScore: 0
        },
        {
            avatar:'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
            name:'賈斯町',
            avgScore: 0
        },
        {
            avatar:'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
            name:'服務媒合',
            avgScore: 0
        }
    ];
    reviewsDialogCheck(): void {
        this.reviewsList = this.reviewsList.filter((reviewItem) => !reviewItem.avgScore);
        this.showReviews = false;
    };
    mounted() {
        this.isMobileDevice = isMobileDevice();
    };
    sendReviews() {
        this.showReviews = true;
    };
}
export interface ReviewsList{
    avatar:any;
    name:string;
    avgScore:number;
}
</script>
<style lang="scss" scoped>
.reviews-page{
    background-color: #F8F9FA;
    .reviews-banner{
        background-image: url('~/assets/images/satisfaction/banner_mob.svg');
        height: 120px;
        margin-bottom: 10px;
    }
    .reviews-container{
        padding-right: 10px;
        padding-left: 10px;
        padding-bottom: 10px;
        .reviews-header{
            margin-top: 10px;
            .reviews-header-container{
                display: flex;
                margin-bottom:38px;
                align-items: baseline;
                .reviews-header-title{
                    margin-right: 17.5px;
                    font-size: 20px;
                }
                .reviews-header-subTitle{
                    font-size: 16px;
                    color: #68737A;
                }
            }
        }
        .reviews-content{
            .reviews-content-card{
                .card-body{
                    display: flex;
                    .card-avatar{
                        .img{
                            height: 80px;
                            width: 80px;
                        }
                    }
                    .card-txt{
                        font-size: 20px;
                        padding-top: 20px;
                        .p{
                            font-size: 23px;
                            color:#ED1B2E;
                            font-weight: bold;
                        }
                    }
                }
                .card-score{
                    margin-top: 10px;
                    margin-bottom: 30px;
                    display: flex;
                    justify-content: center;
                }
            }
        }
    }
    .reviews-footer{
        height: 70px;
        display: flex;
        justify-content: center;
        margin-top: 45px;
        background-color: #fff;
        .reviews-footer-btn{
            width: 120px;
            height: 50px;
            margin-top: 10px;
        }
    }
    .reviews-dialog{
        display: flex;
        justify-content: center;
        margin-bottom: 56px;
        .reviews-dialog-title{
            font-size: 18px;
        }
    }
    .reviews-btn-block{
        display: flex;
        justify-content: center;
    }
}
@include desktop{
    .reviews-page{
        .reviews-banner{
            height: 147px;
            background-image: url('~/assets/images/satisfaction/banner_web.svg');
        }
        .reviews-container{
            width: 700px;
            margin: 30px auto 0px auto;
            .reviews-content{
                display: flex;
                flex-direction: column;
                align-items: flex-start;
            }
        }
        .reviews-footer{
            background-color:#F8F9FA;
        }
    }
}
</style>
<style lang="scss">
  @import "./user-reviews.component.scss";
</style>
PAMapp/pages/userReviews/user-reviews.component.scss
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,103 @@
.reviews-page{
    background-color: #F8F9FA;
    .reviews-banner{
        background-image: url('~/assets/images/satisfaction/banner_mob.svg');
        height: 120px;
        margin-bottom: 10px;
    }
    .reviews-container{
        padding-right: 10px;
        padding-left: 10px;
        padding-bottom: 10px;
        .reviews-header{
            margin-top: 10px;
            .reviews-header-container{
                display: flex;
                margin-bottom:38px;
                align-items: baseline;
                .reviews-header-title{
                    margin-right: 17.5px;
                    font-size: 20px;
                }
                .reviews-header-subTitle{
                    font-size: 16px;
                    color: #68737A;
                }
            }
        }
        .reviews-content{
            .reviews-content-card{
                .card-body{
                    display: flex;
                    .card-avatar{
                        .img{
                            height: 80px;
                            width: 80px;
                        }
                    }
                    .card-txt{
                        font-size: 20px;
                        padding-top: 20px;
                        .p{
                            font-size: 23px;
                            color:#ED1B2E;
                            font-weight: bold;
                        }
                    }
                }
                .card-score{
                    margin-top: 10px;
                    margin-bottom: 30px;
                    display: flex;
                    justify-content: center;
                }
            }
        }
    }
    .reviews-footer{
        height: 70px;
        display: flex;
        justify-content: center;
        margin-top: 45px;
        background-color: #fff;
        .reviews-footer-btn{
            width: 120px;
            height: 50px;
            margin-top: 10px;
        }
    }
    .reviews-dialog{
        display: flex;
        justify-content: center;
        margin-bottom: 56px;
        .reviews-dialog-title{
            font-size: 18px;
        }
    }
    .reviews-btn-block{
        display: flex;
        justify-content: center;
    }
}
@include desktop{
    .reviews-page{
        .reviews-banner{
            height: 147px;
            background-image: url('~/assets/images/satisfaction/banner_web.svg');
        }
        .reviews-container{
            width: 700px;
            margin: 30px auto 0px auto;
            .reviews-content{
                display: flex;
                flex-direction: column;
                align-items: flex-start;
            }
        }
        .reviews-footer{
            background-color:#F8F9FA;
        }
    }
}
PAMapp/pages/userReviews/user-reviews.component.ts
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,43 @@
import { Vue,Component } from 'vue-property-decorator'
import { isMobileDevice } from '~/assets/ts/device';
import { ReviewsItem } from '~/assets/ts/models/reviews-item.model';
@Component({
    layout: 'home'
})
export default class UserReviews extends Vue {
    isMobileDevice = true;
    showReviews = false;
    reviewsList: ReviewsItem[] = [
        {
            avatar:'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
            name:'蔡美眉',
            avgScore: 0
        },
        {
            avatar:'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
            name:'賈斯町',
            avgScore: 0
        },
        {
            avatar:'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
            name:'服務媒合',
            avgScore: 0
        }
    ];
    reviewsDialogCheck(): void {
        this.reviewsList = this.reviewsList.filter((reviewItem) => !reviewItem.avgScore);
        this.showReviews = false;
    };
    mounted() {
        this.isMobileDevice = isMobileDevice();
    };
    sendReviews() {
        this.showReviews = true;
    };
}
PAMapp/pages/userReviewsRecord/index.vue
Àɮפw§R°£
PAMapp/store/localStorage.ts
@@ -1,5 +1,5 @@
import { Selected } from '~/components/QuickFilter/QuickFilterSelector.vue';
import { Module, Mutation, VuexModule ,Action } from 'vuex-module-decorators';
import { Selected } from '~/assets/ts/models/selected.model';
@Module
export default class LocalStorage extends VuexModule {
  id_token = localStorage.getItem('id_token');