登录界面开发

配置

组件

分别注册了两个组件,一个是login页面(src/views/login/index.vue)、另外一个是登陆后展示的layout页面(src/views/layout/index.vue)

路由配置

配置路由(src/router/index.ts)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//引入createRouter和createWebHashHistory方法
import { createRouter, createWebHashHistory } from "vue-router";
//创建router实例
const router = createRouter({
history: createWebHashHistory(),
routes:[
{
path:'/login',
component:()=>import('@/view/login/index.vue'), //导入组件
name:'login',
meta:{
title:'登录',
hidden:true,
icon:'el-icon-s-home'
}
},
{
path:'/',
name:'layout',
component:()=>import('@/view/layout/index.vue'),
meta: {
title: '', //菜单需要的标题
hidden: false,
icon: '',
}
}
],
//滚动行为,只有在history模式下才有用
// scrollBehavior(){
// return{
// left:0,
// top:0
// }
// }
})
export default router

在 Vue Router 中,router 和 route 是两个不同的概念。

  • router 是 Vue Router 的实例,它包含了一些方法和属性,如 push、> replace、go 等,用于控制路由的跳转和行为。你可以通过 this.$router 在 Vue 组件中访问到 router 实例。
  • route 是当前激活的路由信息对象,它包含了当前路由的路径、参数、查询字符串等信息。你可以通过 this.$route 在 Vue 组件中访问到当前的 route 对象。
  • router 和 route 的主要区别在于,router 是用于控制路由的跳转和行为的,而 route 是表示当前路由状态的信息对象。
  • 在大多数情况下,你可能需要使用 this.$router 来进行路由跳转,如 this.$router.push('/path')。而当你需要获取当前路由的信息,如路径、参数等,你可以使用 this.$route,如 this.$route.params.id。

在app.vue中使用router-view

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div class="outer">
<router-view></router-view>
</div>
</template>
<script setup lang="ts">

</script>
<style scoped lang="scss">
.outer{
width: 100vw;
height:100vh;
margin: 0px;
padding: 0px;
}
</style>

静态页面

el-row/el-col

官网介绍:https://element-plus.org/zh-CN/component/layout.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<template>
<div class="login_box">
<el-row>
{/* :span总和是24,各设置为12,那就是两列对半分。 */}
<el-col :span="12" :xs="24">
<el-form class="login_form">
<h2>地表异常预警平台</h2>
<el-form-item prop="username">
<el-input prefix-icon="User"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input type="password" prefix-icon="Lock"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" class="login_but">登录</el-button>
</el-form-item>
</el-form>
</el-col>
<el-col :span="12" :xs="0"></el-col>
</el-row>
</div>
</template>

<script setup lang="ts"></script>

<style lang="scss" scoped>
.login_box {
width: 100%;
height: 100%;
background-image: url('../../assets/login/bg.gif');
background-size: cover;
.login_form {
position: relative;
padding: 40px;
width: 60%;
top: 30vh;
.login_but{
width: 100%;
}
}
}
</style>


css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<style lang="scss" scoped>
.login_box {
//能用100%是因为父盒子有定义width和height。这里的百分比是相对于父盒子的
//如果父盒子没有明确的width和height,那么百分比会失效
width: 100%;
height: 100%;
background-image: url('../../assets/login/bg.gif');
//cover和contain都会保持图片的宽高比,区别在于contain会留白,cover会完全覆盖,
//也就是cover会导致,图片某部分被裁剪
background-size: cover;
.login_form {
//position属性可能有static、realative、sbsolute、sticky和fixed
position: relative;
padding: 40px;
width: 60%;
top: 30vh;
.login_but{
width: 100%;
}
}
}
</style>

positon的值

  • static:这是元素的默认值。静态定位的元素不会受到 topbottomleftright 的影响。
  • relative:元素按照正常的文档流进行定位,然后相对于它的正常位置进行偏移。偏移不会影响其他元素的位置。
  • absolute:元素脱离正常的文档流,并相对于最近的已定位父元素(position 属性为 relativeabsolutefixedsticky 的元素)进行定位。如果没有已定位的父元素,那么它会相对于 body 元素进行定位。
  • fixed:元素脱离正常的文档流,并相对于浏览器窗口进行定位。即使页面滚动,它仍然会停留在同一的位置。
  • sticky:元素在滚动范围内表现为 relative,而在滚动范围外表现为 fixed。也就是说,元素会“粘”在特定位置,然后随着页面滚动。

动态绑定

mock模拟服务器

使用mock模拟后端

  • 安装mock
    pnpm i mockjs --save
  • 模拟数据和服务
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    //导入mock库
    import Mock from 'mockjs'
    //模拟数据
    const user = Mock.mock({
    fixedUser: [
    {
    username: 'admin',
    password: 'admin111',
    avatar:
    ' https://tse4-mm.cn.bing.net/th/id/OIP-C.NNF59vntIv7760rOlX1zMgAAAA?w=187&h=188&c=7&r=0&o=5&cb=10&dpr=1.2&pid=1.7'
    },
    {
    username: 'user',
    password: 'user111',
    avatar:
    'https://tse1-mm.cn.bing.net/th/id/OIP-C.D34PjxR7ud-vxeTDvs5Z8gAAAA?w=135&h=150&c=7&r=0&o=5&cb=10&dpr=1.2&pid=1.7'
    }
    ]
    })

    export default()=>{
    Mock.mock('/api/login', 'post', (req: any) => {
    const { username, password } = JSON.parse(req.body)
    //从模拟数据中找
    const userItem = user.fixedUser.find(
    (item: any) => item.username === username && item.password === password
    )
    //如果用户数据在模拟数据中,return200,和token
    if (userItem) {
    return {
    code: 200,
    data: {
    //返回用户名,用户头像和token
    username: userItem.username,
    avatar: userItem.avatar,
    token: 'admin-token'
    }
    }
    } else {
    return {
    code: 400,
    message: '用户名或密码错误'
    }
    }
    })
    }
  • 在main.ts中引入mock
    1
    2
    import configureMockServer from './mock/user.ts';
    configureMockServer()

新建仓库

新建仓库用来存放用户数据,发起登录请求等

  • 安装pinia
    pnpm i pinia

  • 新建一个大仓库(src/store/index.ts)

    1
    2
    3
    import {createPinia} from 'pinia'
    const pinia = createPinia()
    export default pinia
  • 封装本地存储、获取和移除token的功能(src/utils/token)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //封装本地存储和读取数据的方法
    //本地存储数据
    export const SET_TOKEN = (token: string) => {
    localStorage.setItem('TOKEN', token)
    }
    //本地存储获取数据
    export const GET_TOKEN = () => {
    return localStorage.getItem('TOKEN')
    }
    export const REMOVE_TOKEN = () => {
    return localStorage.removeItem('TOKEN')
    }
  • 在main.ts中注册pinia

    1
    2
    import pinia from './store'
    app.use(pinia)
  • 创建用户仓库

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    //存放用户数据的仓库
    import { defineStore } from 'pinia'
    import { reqLogin } from '@/api/user'
    import { SET_TOKEN, GET_TOKEN, } from '@/utils/token'
    export const userStore = defineStore({
    id: 'user',
    state: () => {
    return {
    token: GET_TOKEN(),
    username: '',
    avatar: ''
    }
    },
    //异步操作
    actions: {
    //登录
    async userLogin(data: any) {
    const res = await reqLogin(data)
    if (res.code === 200) {
    //将用户名、头像存入仓库
    this.username = res.data.username
    this.avatar = res.data.avatar
    //将token存入localStorage
    SET_TOKEN(res.data.token)
    } else {
    //登录失败
    return Promise.reject(new Error(res.message))
    }
    }
    }
    })

创建并暴露用户登录api

在(src/api/user/index.ts)中书写相关代码

1
2
3
4
5
6
7
8
9
//导入二次封装的axios
import request from '@/utils/request'

// 枚举接口
enum API{
LOGIN_URL = '/login'
}
//暴露登录接口
export const reqLogin = (data:any)=> request.post<any, any>(API.LOGIN_URL,data)

v-model绑定

  • 双向绑定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <template>
    <div class="login_box">
    <el-row>
    <el-col :span="12" :xs="24">
    {/* 使用ref获取el-form组件 */}
    <el-form class="login_form" :model="LoginForm" :rules="rules" ref="loginFormRef">
    <h2>我的平台</h2>
    <el-form-item prop="username">
    {/* 使用v-model双向绑定 */}
    <el-input prefix-icon="User" v-model="LoginForm.username"></el-input>
    </el-form-item>
    <el-form-item prop="password">
    <el-input type="password" prefix-icon="Lock" v-model="LoginForm.password"></el-input>
    </el-form-item>
    <el-form-item>
    {/* 绑定点击事件发起登录请求 */}
    <el-button type="primary" class="login_but" @click="login">登录</el-button>
    </el-form-item>
    </el-form>
    </el-col>
    <el-col :span="12" :xs="0"></el-col>
    </el-row>
    </div>
    </template>
  • 点击事件触发login

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    <script setup lang="ts">
    import { ref } from 'vue'
    import { userStore } from '@/store/modules/user'
    import {useRouter, useRoute} from 'vue-router'
    import { ElNotification } from 'element-plus';
    //新建一个loginForm用于收集表单数据
    let LoginForm = ref({
    username: '',
    password: ''
    })
    //获取用户仓库
    let ustore = userStore()
    //获取路由器
    let $router = useRouter()
    let $route = useRoute()
    //获取表单组件
    let loginFormRef = ref()
    const login = async()=>{
    await loginFormRef.value.validate()
    //登录逻辑
    //首先,发起请求,将用户名和密码发送给后端
    //然后,后端返回一个token,将token存储在本地
    //最后,跳转到首页
    try{
    await ustore.userLogin(LoginForm.value)
    //RU: 如果有redirect参数,就跳转到redirect,否则跳转到首页
    let redirect: any = $route.query.redirect
    $router.push({ path: redirect || '/' })
    // 提示登录成功
    ElNotification({
    title: `欢迎回来${ustore.username}`,
    message: '欢迎回来',
    type: 'success'
    });
    } catch(error){

    // 提示登录失败
    ElNotification({
    type: 'error',
    message: (error as Error).message,
    });
    }
    }

    const validatorUsername = (rule:any , value:string, callback:any)=>{
    if(value== undefined){
    callback(new Error('用户名不能为空'))
    }
    if(value.length>=5 && value.length<=20){
    callback()
    }else{
    callback(new Error('用户名必须是5-20位的字符串'))
    }
    }

    const validatorPassword = (rule:any , value:string, callback:any)=>{
    if(value== undefined){
    callback(new Error('密码不能为空'))
    }
    if(value.length > 5){
    callback()
    }else{
    callback(new Error('密码长度必须大于5位'))
    }
    }
    //定义表达验证需要配置的对象
    const rules = {
    username: [
    //validatorUsername在上面定义
    {trigger:'blur', validator: validatorUsername}
    ],
    password: [
    {trigger:'blur', validator:validatorPassword}
    ]
    }
    </script>

规则校验

使用elementPlus规定好的关键字,实现进入规则校验

  • 在表单中绑定rules属性为rules对对象
    1
    <el-form class="login_form" :model="LoginForm" :rules="rules" ref="loginFormRef">
  • 在script中,定义表单验证的配置对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    //这点代码中定义的前两个对象用于rules对象中
    const validatorUsername = (rule:any , value:string, callback:any)=>{
    if(value== undefined){
    callback(new Error('用户名不能为空'))
    }
    if(value.length>=5 && value.length<=20){
    callback()
    }else{
    callback(new Error('用户名必须是5-20位的字符串'))
    }
    }

    const validatorPassword = (rule:any , value:string, callback:any)=>{
    if(value== undefined){
    callback(new Error('密码不能为空'))
    }
    if(value.length > 5){
    callback()
    }else{
    callback(new Error('密码长度必须大于5位'))
    }
    }
    //定义表达验证需要配置的对象
    const rules = {
    username: [
    //validatorUsername在上面定义
    //失去焦点后触发
    {trigger:'blur', validator: validatorUsername}
    ],
    password: [
    {trigger:'blur', validator:validatorPassword}
    ]
    }
  • 在登录点击事件中,发起请求之前先进行规则校验,校验通过后才能发起请求

路由跳转

登录成功后跳转到页面

1
2
3
4
5
6
7
8
let $router = useRouter()
let $route = useRoute()

登录函数中:
登录成功后:
//如果有redirect参数,就跳转到redirect,否则跳转到首页
let redirect: any = $route.query.redirect
$router.push({ path: redirect || '/' })