5.3 开发登录页面
前面已经完成项目基础结构的搭建,接下来正式进入业务开发环节。本节开发登录页面。
5.3.1 编写登录页组件
上节已经创建了登录页组件 Login.vue。登录页的功能包括登录和注册,若没有账号则先注册,若已有账号则登录。
我们需要使用表单功能来接收用户的输入,并且可以切换登录模式和注册模式。
(1)编写登录页面组件模板代码:
<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 语法::
<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 表示按钮是否有加载中动画,代码如下:
<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()方法,执行登录或注册的逻辑。
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 中则不会。因此在上面代码中,获取真实的状态值是这样的:
let { value: user } = form;
// 等同于
let user = form.value;
验证提交内容后,就要进入真实的登录/注册步骤了。正常情况下这里肯定要通过接口完成用户的登录与注册,但我们没有接口,所以这里通过全局状态管理来模拟实现。
假设现在定义了一个 user 仓库用于全局存储用户信息,并定义了登录和注册等方法,那么我们可以继续编写登录和注册逻辑:
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 文件编写用户数据类型:
export interface UserType {
user_id?: number
user_name: string
phone: string
password?: string
}
上面的代码定义并导出了 UserType 类型,该类型会在创建状态时使用。
(2)在 stores/user 文件夹下创建 index.ts 文件,并在该文件中创建 Store:
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()方法:
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 请求,代码如下:
// 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,代码如下:
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 赋值,表示登录成功。代码如下:
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 仓库导出,在外部使用时就可以直接导入,代码如下:
// stores/user/index.ts
import { defineStore } from "pinia";
const userStore = defineStore("user", {...})
export default userStore;
但是有的页面可能会导入多个仓库,为了减少多次导入,创建了 stores/index.ts 文件,在这里将仓库全局导出:
// stores/index.ts
export { default as userStore } from "./user";
之后便可以在任意页面中快速导入该仓库并使用:
import { userStore } from "@/stores";
const store = userStore();