Vue3 H5 项目尝试(四)

189 阅读2分钟

实现一个登录

以pc端为例,实现一个验证码登录

实现一个模板

pc/views/sms

<template>
  <div class="view-account">
    <div class="view-account-header"></div>
    <div class="view-account-container">
      <div class="view-account-top">
        <h2>请先验证手机:</h2>
      </div>
      <div class="view-account-form">
        <el-form
          ref="formRef"
          label-placement="left"
          size="large"
          :model="formInline"
          :rules="rules"
          :disabled="loading.confirm"
        >
          <el-form-item prop="phoneNum">
            <el-input v-model.trim="formInline.phoneNum" placeholder="手机号">
              <template #prefix>+86</template>
            </el-input>
          </el-form-item>
          <el-form-item prop="sms">
            <el-input v-model.trim="formInline.sms" placeholder="验证码" clearable>
              <template #append>
                <el-button
                  @click="validateForm('phoneNum', getIdCode)"
                  :disabled="lock || !formInline.phoneNum || loading.confirm"
                  :loading="loading.id"
                  >{{ smsMsg }}
                </el-button>
              </template>
            </el-input>
          </el-form-item>
          <el-form-item>
            <div
              style="display: flex; justify-content: center; width: 100%"
              @click="submitForm(formRef)"
            >
              <el-button
                type="primary"
                size="large"
                style="flex: 1"
                :loading="loading.confirm"
                block
                :disabled="!formInline.phoneNum || !formInline.sms"
              >
                确 定
              </el-button>
            </div>
          </el-form-item>
        </el-form>
      </div>
    </div>
  </div>
</template>

<script lang="ts" setup>
  import useSMS from '@/hooks/useSMS';
  import { onBeforeUnmount } from 'vue';

  const {
    formRef,
    loading,
    formInline,
    rules,
    lock,
    smsMsg,
    clearCounting,
    getIdCode,
    submitForm,
  } = useSMS();

  const validateForm = async (name: string, callback?) => {
    if (!formRef.value) return;
    formRef.value.validateField([name], async (valid: boolean) => {
      if (valid) return;
      if (callback) callback();
    });
  };

  onBeforeUnmount(() => {
    clearCounting();
  });
</script>

<style lang="less" scoped>
  .view-account {
    display: flex;
    flex-direction: column;
    height: 100vh;
    overflow: auto;

    &-container {
      flex: 1;
      padding: 64px 0;
      width: 384px;
      margin: auto;
    }

    &-top {
      padding: 16px 0;
      text-align: left;

      &-desc {
        font-size: 14px;
        color: #808695;
      }
    }

    &-form {
      :deep(.el-input--prefix .el-input__inner) {
        padding-left: 48px;
      }
    }

    &-other {
      width: 100%;
    }
  }

  @media (min-width: 768px) {
    .view-account {
      background-image: url('../../../assets/images/login.svg');
      background-repeat: no-repeat;
      background-position: 50%;
      background-size: 100%;
    }

    .page-account-container {
      padding: 32px 0 24px 0;
    }
  }
</style>

效果:

image.png

逻辑代码

注意:由于项目分为pc端和移动端,大部分数据定义及逻辑代码都写在了useSMS这个hook中,方便复用,我们看下@/hooks/useSMS

import { useRoute, useRouter } from 'vue-router';
import { sendSMSCode, verifySMSCode } from '@/api/sms';
import { useUserStore } from '@/store/modules/user';
interface FormState {
  phoneNum: string;
  sms: string;
}

export default function () {
  const router = useRouter();
  const route = useRoute();
  const userStore = useUserStore();

  const lockTime = ref(60);
  const lock = ref(false);
  const formRef = ref();
  const loading = reactive({
    id: false,
    confirm: false,
  });

  const formInline = reactive<FormState>({
    phoneNum: '',
    sms: '',
  });

  const rules = {
    phoneNum: [
      { required: true, message: '请输入手机号' },
      { required: true, pattern: /^(?:(?:\+|00)86)?1[3-9]\d{9}$/, message: '请输入正确手机号' },
    ],
    sms: [{ required: true, message: '请输入验证码' }],
  };

  const smsMsg = computed(() => {
    return lock.value ? `${lockTime.value}秒后重新获取` : '获取验证码';
  });

  const setLockTime = (initTime = 60) => {
    lockTime.value = initTime;
  };

  const setLock = (is = true) => {
    lock.value = is;
  };

  let timer;
  
  const timekeeping = () => {
    clearInterval(timer);
    setLock();
    // 重置时间
    setLockTime();
    timer = setInterval(() => {
      // 倒计时递减
      setLockTime(lockTime.value - 1);
      if (lockTime.value <= 0) {
        setLock(false);
        return clearInterval(timer);
      }
    }, 1000);
  };

  const clearCounting = () => {
    clearInterval(timer);
  };

  const getIdCode = async () => {
    try {
      loading.id = true;
      const { data } = await sendSMSCode({ tel: formInline.phoneNum });
      if (data) {
        timekeeping();
      }
    } catch (error) {
    } finally {
      loading.id = false;
    }
  };

  const submitForm = (formEl) => {
    if (!formEl) return;
    formEl.validate((valid: boolean) => {
      onConfirm(valid);
    });
  };

  const onConfirm = async (valid: boolean) => {
    if (!valid) return;
    try {
      loading.confirm = true;
      const { data } = await verifySMSCode({
        tel: formInline.phoneNum,
        code: formInline.sms,
        foreignUserId: userStore.getForeignUserId,
        productCode: userStore.getProductCode,
        userName: userStore.getUserFaultUserName,
      });
      if (data) {
        userStore.setTel(formInline.phoneNum);
        userStore.setId(data);
        const toPath = decodeURIComponent((route.query?.redirect || '/') as string);
        window['$message'].success('验证通过');
        router.replace({
          path: toPath,
          query: { ...route.query, redirect: undefined, page: undefined },
        });
      } else {
        window['$message'].warning('验证失败');
      }
    } finally {
      loading.confirm = false;
    }
  };

  return {
    formRef,
    loading,
    formInline,
    rules,
    lockTime,
    lock,
    smsMsg,
    timekeeping,
    clearCounting,
    getIdCode,
    submitForm,
    onConfirm,
  };
}

结合模板和逻辑代码

  1. 输手机号;
  2. 校验格式;
  3. 获取验证码;
  4. 60秒倒计时;
  5. 请求验证码;
  6. 输入验证码;
  7. 验证验证码;
  8. 路由跳转;

基本的功能这样就实现了。

路由拦截

import { useUserStoreWidthOut } from '@/store/modules/user';

const LOGIN_PATH = '/sms';

router.beforeEach(async (to, from, next) => {
  const userStore = useUserStoreWidthOut();
  if (!userStore.getUserId || !userStore.getUserTel) {
    // redirect login page
    const redirectData: { path: string; replace: boolean; query?: Recordable<string> } = {
      path: LOGIN_PATH,
      replace: true,
    };
    if (to.path) {
      redirectData.query = {
        ...to.query,
        redirect: to.path,
      };
    }
    next(redirectData);
    return;
  }
}

当缺少用户信息时进行路由拦截,跳转验证码登录,并讲原本的路由保存到query上,以便登录成功后跳回原本的页面。

这样一个验证码登录就做好了。