Skip to content

5.3 开发登录页面

前面已经完成项目基础结构的搭建,接下来正式进入业务开发环节。本节开发登录页面。

5.3.1 编写登录页组件

上节已经创建了登录页组件 Login.vue。登录页的功能包括登录和注册,若没有账号则先注册,若已有账号则登录。

我们需要使用表单功能来接收用户的输入,并且可以切换登录模式和注册模式。

(1)编写登录页面组件模板代码:

vue
<template>
  <div class="login-page">
    <el-form class="login-form">
      <h2 class="title">{{ is_login ? "登录" : "注册" }}</h2>
      <el-form-item v-if="!is_login">
        <el-input placeholder="用户名" size="large" v-model="form.user_name" />
      </el-form-item>
      <el-form-item>
        <el-input placeholder="手机号" size="large" v-model="form.phone" />
      </el-form-item>
      <el-form-item>
        <el-input placeholder="密码" size="large" v-model="form.password" />
      </el-form-item>
      <el-form-item>
        <el-button @click="submitForm">{{
          is_login ? "提交" : "注册"
        }}</el-button>
        <div class="text-row">
          <span class="text-wrap" @click="is_login = !is_login">
            <span>{{
              is_login ? "没有账号?去注册" : "已有账号?去登录"
            }}</span>
          </span>
        </div>
      </el-form-item>
    </el-form>
  </div>
</template>

上面的代码用到了表单组件 el-form 和 el-form-item,这两个组件由 Element Plus 提供。表单中一共包括 3 个元素,分别为用户名文本框、手机号文本框和密码文本框,并且与对应的状态双向绑定。此外,状态 is_login 用来判断当前是登录还是注册,并展示不同的内容。

表单底部可以切换登录模式与注册模式。当切换到注册模式时,表单中会多出用户名文本框和确认密码文本框,在不同的模式下单击“提交”按钮会执行不同的操作。

(2)添加基础样式代码,使用 Less 语法::

vue
<style lang="less">
.login-form {
  width: 360px;
  background: #fff;
  padding: 30px 50px 10px 50px;
  border-radius: 7px;
  margin: 20px auto;
  box-shadow: var(--el-box-shadow);
  .title {
    text-align: center;
    margin-bottom: 18px;
  }
  .login-btn {
    margin-top: 14px;
    width: 100%;
  }
  .text-row {
    text-align: right;
    width: 100%;
    padding-top: 5px;
    .text-wrap {
      display: inline-flex;
      cursor: pointer;
      align-items: center;
    }
  }
}
</style>

上面的代码修饰了表单区域的样式,并且使用了 Element Plus 提供的 CSS 变量,即--el-box-shadow。该变量定义了一个阴影效果,可以直接用val()来加载这个变量。

最终的登录和注册的页面效果分别如图 5-3 和图 5-4 所示。

(3)定义组件状态。在 script 部分分别定义状态 is_login、form 和 loading,loading 表示按钮是否有加载中动画,代码如下:

vue
<script setup lang="ts">
import { ref } from "vue";
const is_login = ref(true);
const loading = ref(false);
const form = ref({
  phone: "",
  user_name: "",
  password: "",
});
</script>

(4)单击“提交”按钮会触发 submitForm()方法,执行登录或注册的逻辑。

js
import { ElMessage } from "element-plus";
const submitForm = async () => {
  let { value: user } = form;
  if (!user.phone || !user.password) {
    return ElMessage({
      type: "warning",
      message: "手机号和密码不能为空",
    });
  }
  if (user.phone.length != 11 || isNaN(Number(user.phone))) {
    return ElMessage({
      type: "warning",
      message: "请输入正确的手机号",
    });
  }
  if (!is_login.value && !user.user_name) {
    return ElMessage({
      type: "warning",
      message: "请输入用户名",
    });
  }
};

在函数中使用定义的状态 form 时,实际的值必须通过 form.value 访问。但在模版中可以直接使用 from,这是因为在模版中会自动解构,而 script 中则不会。因此在上面代码中,获取真实的状态值是这样的:

js
let { value: user } = form;
// 等同于
let user = form.value;

验证提交内容后,就要进入真实的登录/注册步骤了。正常情况下这里肯定要通过接口完成用户的登录与注册,但我们没有接口,所以这里通过全局状态管理来模拟实现。

假设现在定义了一个 user 仓库用于全局存储用户信息,并定义了登录和注册等方法,那么我们可以继续编写登录和注册逻辑:

js
import { ElMessage } from "element-plus";
import { useRouter } from "vue-router";
import { userStore } from "@/stores";
const store = userStore();
const router = useRouter();

const submitForm = async () => {
  // ... 验证代码
  try {
    loading.value = true;
    if (is_login.value) {
      await store.login(user);
      setTimeout(() => {
        router.push("/");
      }, 500);
    } else {
      await store.register(user);
      is_login.value = true
    }
    loading.value = false;
    ElMessage({
      type: "success",
      message: is_login.value ? "登录成功" : "注册成功",
    });
  } catch (errer: any) {
    loading.value = false;
    ElMessage({
      type: "error",
      message: errer || "错误",
    });
  }
}

代码中导入的 userStore 表示 user 仓库,并调用了 login() 和 register() 方法分别表示执行登录和注册请求。由代码可见,两个方法均返回了 Promise。当登录成功后,使用路由对象跳转页面到首页。

具体的状态定义,以及登录和注册的代码实现细节,都定义在 user 仓库下,现在我们来编写这个仓库的代码。

5.3.2 编写用户 Store

用户 Store 主要用于定义用户状态和修改用户状态。下面创建用户仓库并实现状态的定义和操作。

(1)新建 stores/user 文件夹,创建 types.ts 文件编写用户数据类型:

js
export interface UserType {
  user_id?: number
  user_name: string
  phone: string
  password?: string
}

上面的代码定义并导出了 UserType 类型,该类型会在创建状态时使用。

(2)在 stores/user 文件夹下创建 index.ts 文件,并在该文件中创建 Store:

ts
import { defineStore } from "pinia";
import type { UserType } from "./types";
const userStore = defineStore("user", {
  state: () => ({
    userInfo: null as UserType | null,
  }),
});
export default userStore;

上面的代码创建了 userStore 仓库,并定义了状态 userInfo 表示当前已登录的用户。userInfo 使用 as 关键字为其指定 UserType 类型。

(3)创建修改用户的方法。在 actions 选项下定义 setUser()方法:

ts
actions: {
  setUser(user: UserType) {
    this.userInfo = user;
    localStorage.setItem("login_user", JSON.stringify(user));
  }
}

在上面的代码中,在为 userInfo 赋值后,还将用户数据存储在 localStorage 中,这是为了实现数据的持久化。

因为在 Pinia 仓库中存储的所有状态在刷新浏览器时都会被销毁重置,所以需要将这些状态存储在 localStorage 中避免数据丢失。同时,因为本项目使用 localStorage 代替数据库存储数据,所以要保持 Pinia 和 localStorage 中的数据同步。

(4)在 utils/index.ts 文件中定义 ImitateHttp() 方法,其含义为模拟 HTTP 请求,代码如下:

js
// utils/index.ts
export const ImitateHttp = (
  fun: (s: Function, f: Function) => void,
  timer = 1000
) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => fun(resolve, reject), timer);
  });
};

(5)创建注册方法 register(),该方法用来接收用户信息并生成 user_id,代码如下:

ts
actions: {
  register(form: UserType) {
    return ImitateHttp((s, f) => {
      form.user_id = parseInt(form.phone.slice(-4));
      localStorage.setItem("regis_user", JSON.stringify(form));
      s('ok');
    })
  }
}

register()方法使用工具函数 ImitateHttp()模拟请求,在延迟 1 秒后执行注册逻辑。延迟的目的是模拟调用注册接口时的耗时,使用户体验更逼真。

(6)创建登录方法 login()。登录方法有两重验证,一是是否已注册,二是账号密码是否正确。两重验证通过后即可为 userInfo 赋值,表示登录成功。代码如下:

ts
actions: {
  login(form: UserType) {
    let regis = localStorage.getItem("regis_user");
    return ImitateHttp((s, f) => {
      if (!regis) {
        f("用户未注册");
      } else {
        let user: UserType = JSON.parse(regis);
        if (user.phone == form.phone && user.password == form.password) {
          this.setUser(user);
          s("登录成功");
        } else {
          s("手机号或密码错误");
        }
      }
    });
  }
}

(7)在文件末尾将 userStore 仓库导出,在外部使用时就可以直接导入,代码如下:

js
// stores/user/index.ts
import { defineStore } from "pinia";
const userStore = defineStore("user", {...})
export default userStore;

但是有的页面可能会导入多个仓库,为了减少多次导入,创建了 stores/index.ts 文件,在这里将仓库全局导出:

ts
// stores/index.ts
export { default as userStore } from "./user";

之后便可以在任意页面中快速导入该仓库并使用:

js
import { userStore } from "@/stores";
const store = userStore();