黑马程序员前端 Vue3 小兔鲜电商项目——(八)登录页面

image-20230624165512735

登录页面的主要功能就是表单校验和登录登出业务。

账号密码

account password
cdshi0080 123456
cdshi0081 123456
cdshi0082 123456
cdshi0083 123456
cdshi0084 123456
cdshi0085 123456
cdshi0086 123456
cdshi0087 123456
cdshi0088 123456

路由配置

模版代码

在 src\views\Login\index.vue 中添加登录页代码:

<script setup>

</script>

<template>
  <div>
    <header class="login-header">
      <div class="container m-top-20">
        <h1 class="logo">
          <RouterLink to="/">小兔鲜</RouterLink>
        </h1>
        <RouterLink class="entry" to="/">
          进入网站首页
          <i class="iconfont icon-angle-right"></i>
          <i class="iconfont icon-angle-right"></i>
        </RouterLink>
      </div>
    </header>
    <section class="login-section">
      <div class="wrapper">
        <nav>
          <a href="javascript:;">账户登录</a>
        </nav>
        <div class="account-box">
          <div class="form">
            <el-form label-position="right" label-width="60px"
              status-icon>
              <el-form-item  label="账户">
                <el-input/>
              </el-form-item>
              <el-form-item label="密码">
                <el-input/>
              </el-form-item>
              <el-form-item label-width="22px">
                <el-checkbox  size="large">
                  我已同意隐私条款和服务条款
                </el-checkbox>
              </el-form-item>
              <el-button size="large" class="subBtn">点击登录</el-button>
            </el-form>
          </div>
        </div>
      </div>
    </section>

    <footer class="login-footer">
      <div class="container">
        <p>
          <a href="javascript:;">关于我们</a>
          <a href="javascript:;">帮助中心</a>
          <a href="javascript:;">售后服务</a>
          <a href="javascript:;">配送与验收</a>
          <a href="javascript:;">商务合作</a>
          <a href="javascript:;">搜索推荐</a>
          <a href="javascript:;">友情链接</a>
        </p>
        <p>CopyRight &copy; 小兔鲜儿</p>
      </div>
    </footer>
  </div>
</template>

<style scoped lang='scss'>
.login-header {
  background: #fff;
  border-bottom: 1px solid #e4e4e4;

  .container {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
  }

  .logo {
    width: 200px;

    a {
      display: block;
      height: 132px;
      width: 100%;
      text-indent: -9999px;
      background: url("@/assets/images/logo.png") no-repeat center 18px / contain;
    }
  }

  .sub {
    flex: 1;
    font-size: 24px;
    font-weight: normal;
    margin-bottom: 38px;
    margin-left: 20px;
    color: #666;
  }

  .entry {
    width: 120px;
    margin-bottom: 38px;
    font-size: 16px;

    i {
      font-size: 14px;
      color: $xtxColor;
      letter-spacing: -5px;
    }
  }
}

.login-section {
  background: url('@/assets/images/login-bg.png') no-repeat center / cover;
  height: 488px;
  position: relative;

  .wrapper {
    width: 380px;
    background: #fff;
    position: absolute;
    left: 50%;
    top: 54px;
    transform: translate3d(100px, 0, 0);
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.15);

    nav {
      font-size: 14px;
      height: 55px;
      margin-bottom: 20px;
      border-bottom: 1px solid #f5f5f5;
      display: flex;
      padding: 0 40px;
      text-align: right;
      align-items: center;

      a {
        flex: 1;
        line-height: 1;
        display: inline-block;
        font-size: 18px;
        position: relative;
        text-align: center;
      }
    }
  }
}

.login-footer {
  padding: 30px 0 50px;
  background: #fff;

  p {
    text-align: center;
    color: #999;
    padding-top: 20px;

    a {
      line-height: 1;
      padding: 0 10px;
      color: #999;
      display: inline-block;

      ~a {
        border-left: 1px solid #ccc;
      }
    }
  }
}

.account-box {
  .toggle {
    padding: 15px 40px;
    text-align: right;

    a {
      color: $xtxColor;

      i {
        font-size: 14px;
      }
    }
  }

  .form {
    padding: 0 20px 20px 20px;

    &-item {
      margin-bottom: 28px;

      .input {
        position: relative;
        height: 36px;

        >i {
          width: 34px;
          height: 34px;
          background: #cfcdcd;
          color: #fff;
          position: absolute;
          left: 1px;
          top: 1px;
          text-align: center;
          line-height: 34px;
          font-size: 18px;
        }

        input {
          padding-left: 44px;
          border: 1px solid #cfcdcd;
          height: 36px;
          line-height: 36px;
          width: 100%;

          &.error {
            border-color: $priceColor;
          }

          &.active,
          &:focus {
            border-color: $xtxColor;
          }
        }

        .code {
          position: absolute;
          right: 1px;
          top: 1px;
          text-align: center;
          line-height: 34px;
          font-size: 14px;
          background: #f5f5f5;
          color: #666;
          width: 90px;
          height: 34px;
          cursor: pointer;
        }
      }

      >.error {
        position: absolute;
        font-size: 12px;
        line-height: 28px;
        color: $priceColor;

        i {
          font-size: 14px;
          margin-right: 2px;
        }
      }
    }

    .agree {
      a {
        color: #069;
      }
    }

    .btn {
      display: block;
      width: 100%;
      height: 40px;
      color: #fff;
      text-align: center;
      line-height: 40px;
      background: $xtxColor;

      &.disabled {
        background: #cfcdcd;
      }
    }
  }

  .action {
    padding: 20px 40px;
    display: flex;
    justify-content: space-between;
    align-items: center;

    .url {
      a {
        color: #999;
        margin-left: 10px;
      }
    }
  }
}

.subBtn {
  background: $xtxColor;
  width: 100%;
  color: #fff;
}
</style>

配置路由跳转

修改 src\views\Layout\components\LayoutNav.vue 文件中【请先登录】的 a 标签:

 <li><a href="javascript:;" @click="router.push('/login')">请先登录</a></li>

表单校验实现

作用:前端提前校验可以省去一些错误的请求提交,为后端节省接口压力。

ElementPlus 表单组件内置了表单校验功能,只需要按照组件要求配置必要参数即可(直接看文档)。ElementPlus表单组件内置了初始的校验配置,应付简单的校验只需要通过配置即可,如果想要定制一些特殊的校验需求,可以使用自定义校验规则。

校验要求

用户名:不能为空,字段名为 account
密码:不能为空且为6-14个字符,字段名为 password
同意协议:必选,字段名为 agree

代码实现

  1. 按照字段准备表单对象:

    // 1.准备表单对象
    const form = ref({
          
          
      account: '',
      password: '',
      agree: true
    })
    
  2. 按照产品要求准备规则对象:

    // 2. 校验规则对象
    const rules = {
          
          
      account: [
        {
          
           required: true, message: '用户名不能为空', trigger: 'blur' }
      ],
      password: [
        {
          
           required: true, message: '密码不能为空', trigger: 'blur' },
        {
          
           min: 6, max: 24, message: '密码长度要求6-14个字符', trigger: 'blur' }
      ],
      agree: [
        {
          
          
          validator: (rule, value, callBack) => {
          
          
            console.log(value)
            //自定义校验逻辑
            // 勾选协议通过,不勾选不通过
            if (value) {
          
          
              callBack()
            } else {
          
          
              callBack(new Error('请勾选协议'))
            }
          }
        }
      ]
    }
    
  3. 给表单绑定用户表单对象和校验规则:

    <el-form label-position="right" :model="form" :rules="rules" label-width="60px" status-icon>
      ...
    </el-form>
    
  4. 指定表单域的校验字段名:

    <el-form-item prop="account" label="账户">
      <el-input />
    </el-form-item>
    <el-form-item prop="password" label="密码">
      <el-input />
    </el-form-item>
    <el-form-item prop="agree" label-width="22px">
      <el-checkbox size="large">
        我已同意隐私条款和服务条款
      </el-checkbox>
    </el-form-item>
    
  5. 把表单对象进行双向绑定:

<el-form-item prop="account" label="账户">
  <el-input v-model="form.account"/>
</el-form-item>
<el-form-item prop="password" label="密码">
  <el-input v-model="form.password"/>
</el-form-item>
<el-form-item prop="agree" label-width="22px">
  <el-checkbox size="large" v-model="form.agree">
    我已同意隐私条款和服务条款
  </el-checkbox>
</el-form-item>

统一校验

在点击登录时需要对所有需要校验的表单进行统一校验。

  1. 获取表单实例:

    // 3.获取 form 实例做统一校验
    const formRef = ref(null)
    

    与表单进行绑定:

    <el-form ref="formRef" label-position="right" :model="form" :rules="rules" label-width="60px" status-icon>
      ...
    </el-form>
    
  2. 编写登录逻辑:

    const doLogin = () => {
          
          
      // 调用实例方法
      formRef.value.validate(async (valid) => {
          
          
        // valid: 所有表单都通过校验  才为true
        console.log(valid)
        // 以valid做为判断条件 如果通过校验才执行登录逻辑
        if (valid) {
          
          
          // TODO LOGIN
        }
      })
    }
    
  3. 与登录按钮进行绑定:

     <el-button size="large" class="subBtn" @click="doLogin">点击登录</el-button>
    

登录基础业务实现

基础思想

  1. 调用登录接口获取用户信息
  2. 提示用户当前是否成功
  3. 跳转到首页
  1. 新建 src\apis\user.js 文件,编写登录 api:

    //封装所有和用户相关的接口函数
    import http from '@/utils/http'
    
    export const loginApi = ({ account, password }) => {
        return http({
            url: '/login',
            method: 'POST',
            data: {
                account, password
            }
        })
    }
    
  2. src\views\Login\index.vue 中完善登录逻辑:

    // 3.获取 form 实例做统一校验
    const router = useRouter()
    const formRef = ref(null)
    const doLogin = () => {
          
          
      const {
          
           account, password } = form.value
      // 调用实例方法
      formRef.value.validate(async (valid) => {
          
          
        // valid: 所有表单都通过校验  才为true
        console.log(valid)
        // 以valid做为判断条件 如果通过校验才执行登录逻辑
        if (valid) {
          
          
          // TODO LOGIN
          await loginAPI({
          
           account, password })
          // 1. 提示用户
          ElMessage({
          
           type: 'success', message: '登录成功' })
          // 2. 跳转首页
          router.replace({
          
           path: '/' })
        }
      })
    }
    

统一错误信息提示

在 src\utils\http.js 中的响应拦截器中进行处理:

// axios响应式拦截器
http.interceptors.response.use(res => res.data, e => {
    
    
  // 统一错误提示
  ElMessage({
    
    
    type: 'warning',
    message: e.response.data.message
  })
  return Promise.reject(e)
})

Pinia 管理用户数据

由于用户数据的特殊性,在很多组件中都有可能进行共享,共享的数据使用 Pinia 管理会更加方便。Pinia 负责用户数据相关的 state 和 action,组件中只负责触发 action 函数并传递参数。

  1. 添加 src\stores\user.js 文件,在其中添加保存用户数据的方法:

    // 管理用户数据相关
    import {
          
           defineStore } from 'pinia'
    import {
          
           ref } from 'vue'
    import {
          
           loginAPI } from '@/apis/user'
    
    export const useUserStore = defineStore('user', () => {
          
          
        // 1. 定义管理用户数据的state
        const userInfo = ref({
          
          })
        // 2. 定义获取接口数据的action函数
        const getUserInfo = async ({
           
            account, password }) => {
          
          
            const res = await loginAPI({
          
           account, password })
            userInfo.value = res.result
        }
        // 3. 以对象的格式把state和action return
        return {
          
          
            userInfo,
            getUserInfo
        }
    })
    
  2. 替换 src\views\Login\index.vue 中登录逻辑的原代码:

    import {
          
          useUserStore} from '@/stores/user'
    ...
    const userStore =  useUserStore()
    // TODO LOGIN
    await userStore.getUserInfo({
          
           account, password })
    

Pinia 用户数据持久化

用户数据中有一个关键的数据叫做 Token(用来标识当前用户是否登录),而 Token 持续一段时间才会过期。Pinia 的存储是基于内存的,刷新就丢失,为了保持登录状态就要做到刷新不丢失,需要配合持久化进行存储。

最终效果:操作 state 时会自动把用户数据在本地的 localStorage 也存一份,刷新的时候会从 localStorage 中先取。

  1. 安装 pinia 持久化插件 persistedstate:

    npm i pinia-plugin-persistedstate  
    
  2. 在 main.js 中注册 pinia 持久化插件:

    import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
    
    const app = createApp(App)
    const pinia = createPinia()
    //注册持久化插件
    pinia.use(piniaPluginPersistedstate)
    app.use(pinia)
    
  3. 对 store 持久化配置:

    export const useUserStore = defineStore('user', () => {
          
          
      ...
    }, {
          
          
      persist: true
    })
    

用户登录状态

在首页根据用户登录状态区分显示的模块。

在 src\views\Layout\components\LayoutNav.vue 中获取 pinia 中存储的用户数据:

import {
    
     useUserStore } from '@/stores/user'
const userStore = useUserStore()

修改模板的 v-if 判断逻辑:

<!--多模版渲染区分登录状态和非登录状态-->
<!--适配思路:登录时显示第一块非登录时显示第二块是否有token-->
<template v-if="userStore.userInfo.token">
  <li><a href="javascript:;"><i class=" iconfont icon-user"></i>{
   
   {userStore.userInfo.account}}</a></li>
  ...
</template>
<template v-else>
  ...
</template>

请求拦截器携带 token

Token作为用户标识,在很多个接口中都需要携带Token才可以正确获取数据,所以需要在接口调用时携带Token。另外,为了统一控制采取请求拦截器携带的方案。

Axiosi请求拦截器可以在接口正式发起之前对请求参数做一些事情,通常Token数据会被注入到请求header中,格式按
照后端要求的格式进行拼接处理。格式如图:

image-20230624164604499

修改 src\utils\http.js 文件中的请求拦截器,从 pinia 获取token数据,将 token 存储到请求的请求头中:

// axios请求拦截器
htpp.interceptors.request.use(config => {
    
    
  // 1. 从pinia获取token数据
  const userStore = useUserStore()
  // 2. 按照后端的要求拼接token数据
  const token = userStore.userInfo.token
  if (token) {
    
    
    config.headers.Authorization = `Bearer ${
      
      token}`
  }
  return config
}, e => Promise.reject(e))

退出登录实现

基础思想:

  1. 清除用户信息
  2. 跳转到登录页
  1. src\stores\user.js 中新增清除用户信息方法:

    export const useUserStore = defineStore('user', () => {
          
          
      ...
      // 退出时清除用户信息
      const clearUserInfo = () => {
          
          
        userInfo.value = {
          
          }
      }
      // 3. 以对象的格式把state和action return
      return {
          
          
        userInfo,
        getUserInfo,
        clearUserInfo
      }
    }, {
          
          
      persist: true
    })
    
  2. 执行退出逻辑,清除用户信息:

    <script setup>
    import {
          
           useUserStore } from '@/stores/userStore'
    import {
          
           useRouter } from 'vue-router'
    const userStore = useUserStore()
    const router = useRouter()
    const confirm = () => {
          
          
      console.log('用户要退出登录了')
      // 退出登录业务逻辑实现
      // 1.清除用户信息 触发action
      userStore.clearUserInfo()
      // 2.跳转到登录页
      router.push('/login')
    }
    </script>
    

Token 失效拦截处理

Token的有效性可以保持一定时间,如果用户一段时间不做任何操作,Token.就会失效,使用失效的Token再去请求一
些接口,接口就会报401状态码错误,需要我们做额外处理。

在 src\utils\http.js 中进行处理:

// axios响应式拦截器
http.interceptors.response.use(res => res.data, e => {
    
    
  // 从pinia获取token数据
  const userStore = useUserStore()
  // 统一错误提示
  ElMessage({
    
    
    type: 'warning',
    message: e.response.data.message
  })
  // 401 token 失效处理
  // 1.清楚本地用户数据
  // 2.跳转登录页
  if(e.response.status===401){
    
    
    userStore.clearUserInfo()
    router.push('/login')
  }
  return Promise.reject(e)
})

猜你喜欢

转载自blog.csdn.net/qq_20185737/article/details/131363742
今日推荐