Vue3+Vite+TS后台项目 ~ 6.用户登录校验


一、登录

1. 登录静态页

编辑 src / views / login / index.vue 文件

<template>
  <div class="login-container">
    <el-form
      class="login-form"
      :rules="rules"
      ref="form"
      :model="user"
      size="medium"
      @submit.prevent="handleSubmit"
    >
      <div class="login-form__header">
        <img
          class="login-logo"
          src="@/assets/login_logo.png"
          alt="拉勾心选"
        >
      </div>
      <el-form-item prop="account">
        <el-input
          v-model="user.account"
          placeholder="请输入用户名"
        >
          <template #prefix>
            <i class="el-input__icon el-icon-user" />
          </template>
        </el-input>
      </el-form-item>
      <el-form-item prop="pwd">
        <el-input
          v-model="user.pwd"
          type="password"
          placeholder="请输入密码"
        >
          <template #prefix>
            <i class="el-input__icon el-icon-lock" />
          </template>
        </el-input>
      </el-form-item>
      <el-form-item prop="imgcode">
        <div class="imgcode-wrap">
          <el-input
            v-model="user.imgcode"
            placeholder="请输入验证码"
          >
            <template #prefix>
              <i class="el-input__icon el-icon-key" />
            </template>
          </el-input>
          <img
            class="imgcode"
            alt="验证码"
            src="https://shop.fed.lagou.com/api/admin/captcha_pro"
          >
        </div>
      </el-form-item>
      <el-form-item>
        <el-button
          class="submit-button"
          type="primary"
          :loading="loading"
          native-type="submit"
        >
          登录
        </el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script lang="ts" setup>
import { reactive, ref } from 'vue'

const user = reactive({
  account: 'admin',
  pwd: '123456',
  imgcode: ''
})
const loading = ref(false)
const rules = ref({
  account: [
    { required: true, message: '请输入账号', trigger: 'change' }
  ],
  pwd: [
    { required: true, message: '请输入密码', trigger: 'change' }
  ],
  imgcode: [
    { required: true, message: '请输入验证码', trigger: 'change' }
  ]
})

const handleSubmit = async () => {
  console.log('handleSubmit')
}

</script>

<style lang="scss" scoped>
.login-container {
  min-width: 400px;
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: #2d3a4b;
}

.login-form {
  padding: 30px;
  border-radius: 6px;
  background: #fff;
  min-width: 350px;
  .login-form__header {
    display: flex;
    justify-content: center;
    align-items: center;
    padding-bottom: 30px;
  }

  .el-form-item:last-child {
    margin-bottom: 0;
  }

  .login__form-title {
    display: flex;
    justify-content: center;
    color: #fff;
  }

  .submit-button {
    width: 100%;
  }

  .login-logo {
    width: 271px;
    height: 74px;
  }
  .imgcode-wrap {
    display: flex;
    align-items: center;
    .imgcode {
      height: 37px;
    }
  }
}
</style>

2. 动态验证码

⑴、 封装请求接口

编辑 src / api / common.ts 文件

...
export const getCaptcha = () => {
  return request<Blob>({
    method: 'GET',
    url: '/admin//captcha_pro',
    params: {
      stamp: Date.now()
    },
    responseType: 'blob' // 请求获取图片数据
  })
}

⑵、 登录页调用
<template>
...
          <img
            class="imgcode"
            alt="验证码"
            :src="captchaSrc"
            @click="loadCaptcha"
          >
        </div>
      </el-form-item>
...
</template>

<script lang="ts" setup>
import { onMounted, reactive, ref } from 'vue'
import { getCaptcha } from '@/api/common'

...
const captchaSrc = ref('')

onMounted(() => {
  loadCaptcha()
})

const loadCaptcha = async () => { // 获取图片验证码
  const data = await getCaptcha()
  captchaSrc.value = URL.createObjectURL(data)
}
...
</script>

⑶、 请求优化

编辑 src / utils / request.ts 文件

...
// 接口返回数据处理
export default <T = any>(config: AxiosRequestConfig) => {
  return request(config).then(res => {
    return (res.data.data || res.data) as T
  })
}

3. 登录基础流程

⑴、 封装请求接口

编辑 src / api / common.ts 文件

...
export const login = (data: {
  account: string,
  pwd: string,
  imgcode: string
}) => { // 用户登录请求
  return request<ILoginResponse>({
    method: 'POST',
    url: '/admin/login',
    data
  })
}

编辑 src / api / types / common.ts 文件

// 登录请求
export interface IUserInfo {
  id: number
  account: string
  head_pic: string
}

export interface IMenu {
  path: string
  title: string
  icon: string
  header: string
  is_header: number
  children?: IMenu[]
}

export interface ILoginResponse {
  token: string
  expires_time: number
  menus: IMenu[]
  unique_auth: string[]
  user_info: IUserInfo
  logo: string
  logo_square: string
  version: string
  newOrderAudioLink: string
}

⑵、 登录页调用

编辑 src / views / login / index.vue 文件

...
<script lang="ts" setup>
import { onMounted, reactive, ref } from 'vue'
import { getCaptcha, login } from '@/api/common'
import { ElForm } from 'element-plus'
import { useRouter } from 'vue-router'

const router = useRouter()

const user = reactive({
  account: 'admin',
  pwd: '123456',
  imgcode: ''
})
const loading = ref(false)
const rules = ref({
  account: [
    { required: true, message: '请输入账号', trigger: 'change' }
  ],
  pwd: [
    { required: true, message: '请输入密码', trigger: 'change' }
  ],
  imgcode: [
    { required: true, message: '请输入验证码', trigger: 'change' }
  ]
})
const captchaSrc = ref('')
const form = ref<InstanceType<typeof ElForm> | null>(null)

onMounted(() => {
  loadCaptcha()
})

const loadCaptcha = async () => { // 获取图片验证码
  const data = await getCaptcha()
  captchaSrc.value = URL.createObjectURL(data)
}

const handleSubmit = async () => { // 登录请求
  // 表单验证
  const valid = await form.value?.validate()
  if (!valid) {
    return false
  }
  // 验证通过, 展示loading
  loading.value = true
  // 请求提交
  const data = await login(user).finally(() => {
    loading.value = false
  })
  console.log('data=>', data)
  router.replace({
    name: 'home'
  })
}
</script>
...

⑶、 错误请求统一处理

编辑 src / utils / request.ts 文件

// 响应拦截器
request.interceptors.response.use(function (response) {
  // 统一设置接口相应错误, 比如 token 过期失效, 服务端异常
  if (response.data.status && response.data.status !== 200) { // 后端返回访问失败
    ElMessage.error(response.data.msg || '请求失败, 请刷新后重试')
    // 手动返回一个 Promise 异常
    return Promise.reject(response.data)
  }
  return response
}, function (error) {
  // Do something with response error
  return Promise.reject(error)
})

⑷、 页面样式

在这里插入图片描述

4. elementPlus 类型封装

⑴、 类型封装

新建 src / types / element-plus.ts 文件

import { ElForm } from 'element-plus'
import type { FormItemRule } from 'element-plus/es/components/form/src/form.type'

export type IElForm = InstanceType<typeof ElForm>
export type IFormRule = Record<string, FormItemRule[]>

⑵、 页面调用

编辑 src / views / login / index.vue 文件

...
import type { IElForm, IFormRule } from '@/types/element-plus'

// const rules = ref({
const rules = ref<IFormRule>({

// const form = ref<InstanceType<typeof ElForm> | null>(null)
const form = ref<IElForm | null>(null)
...


二、用户信息展示

1. 获取用户信息

编辑 src / views / login / index.vue 文件

<script lang="ts" setup>
...
  // 请求提交
  const data = await login(user).finally(() => {
    loading.value = false
  })
  store.commit('setUser', data.user_info)
  console.log('data =>', data)
...
</script>

2. 存储用户信息

编辑 src / store / index.vue 文件

import { IUserInfo } from '@/api/types/common'

const state = {
  count: 1,
  isCollapse: false,
  user: JSON.parse(window.localStorage.getItem('user') || 'null') as IUserInfo | null
}
...
  mutations: {
...
    setUser (state, payload) {
      state.user = payload
      // 本地存储
      window.localStorage.setItem('user', JSON.stringify(state.user))
...

3. 页面调用

新建 src / layout / components / AppHeader / components / UserInfo.vue 文件

<template>
  <el-dropdown>
    <span class="el-dropdown-link">
      {{ $store.state.user?.account }}
      <el-icon class="el-icon--right">
        <arrow-down />
      </el-icon>
    </span>
    <template #dropdown>
      <el-dropdown-menu>
        <el-dropdown-item>个人中心</el-dropdown-item>
        <el-dropdown-item>退出登录</el-dropdown-item>
      </el-dropdown-menu>
    </template>
  </el-dropdown>
</template>

<script lang="ts" setup>
import { ArrowDown } from '@element-plus/icons-vue'
</script>
<style lang="scss" scoped></style>

编辑 src / layout / components / AppHeader / index.vue 文件

<template>
  <el-space>
    <ToggleSidebar />
    <BreadcrumbVue />
  </el-space>
  <el-space>
    <FullScreen />
    <UserInfoVue />
  </el-space>
</template>
<script lang="ts" setup>
import ToggleSidebar from './components/ToggleSidebar.vue'
import BreadcrumbVue from './components/Breadcrumb.vue'
import UserInfoVue from './components/UserInfo.vue'
import FullScreen from './components/FullScreen.vue'
</script>
<style lang="scss" scoped></style>

4. 页面展示

在这里插入图片描述

5. 封装 localstorage 方法

⑴、 自定义方法

新建 src / utils / storage.ts 文件

export const getItem = <T>(key: string) => {
  const data = window.localStorage.getItem(key)
  if (!data) return null
  try {
    return JSON.parse(data) as T
  } catch (err) {
    return null
  }
}

export const setItem = (key: string, value: object | string | null) => {
  if (typeof value === 'object') {
    value = JSON.stringify(value)
  }
  window.localStorage.setItem(key, value)
}

export const removeItem = (key: string) => {
  window.localStorage.removeItem(key)
}

⑵、 组件调用

编辑 src / store / index.ts 文件

  import { getItem, setItem } from '@/utils/storage'

  // user: JSON.parse(window.localStorage.getItem('user') || 'null') as IUserInfo | null
  user: getItem<IUserInfo>('user')
  
  // window.localStorage.setItem('user', JSON.stringify(state.user))
  setItem('user', state.user)

6. 固话常量

⑴、 自定义常量

新建 src / utils / constants.ts 文件

export const USER = 'USER'

⑵、 组件调用

编辑 src / store / index.ts 文件

import { USER } from '@/utils/constants'

// user: getItem<IUserInfo>('user')
user: getItem<IUserInfo>(USER)

// setItem('user', state.user)
setItem(USER, state.user)


三、退出登录

1. 封装请求

编辑 src / api / common.ts 文件

...
export const logout = () => { // 管理员退出
  return request<ILoginResponse>({
    method: 'GET',
    url: '/setting/admin/logout'
  })
}

2. 页面调用

编辑 src / layout / components / AppHeader / components / UserInfo.vue 文件

<template>
  <el-dropdown>
    <span class="el-dropdown-link">
      {{ $store.state.user?.account }}
      <el-icon class="el-icon--right">
        <arrow-down />
      </el-icon>
    </span>
    <template #dropdown>
      <el-dropdown-menu>
        <el-dropdown-item>个人中心</el-dropdown-item>
        <el-dropdown-item @click="handleLogout">
          退出登录
        </el-dropdown-item>
      </el-dropdown-menu>
    </template>
  </el-dropdown>
</template>

<script lang="ts" setup>
import { logout } from '@/api/common'
import { ArrowDown } from '@element-plus/icons-vue'
import { ElMessageBox, ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
import { store } from '@/store'

const router = useRouter()

const handleLogout = () => {
  // 确认提示
  ElMessageBox.confirm(
    '是否确认退出?',
    '提示',
    {
      confirmButtonText: '确认',
      cancelButtonText: '取消',
      type: 'warning'
    }
  )
    .then(async () => {
      // 退出请求
      await logout()
      // 跳转登录页
      router.push({
        name: 'login'
      })
      // 清除用户信息
      store.commit('setUser', null)
      ElMessage({
        type: 'success',
        message: '退出成功'
      })
    })
    .catch(() => {
      ElMessage({
        type: 'info',
        message: '已取消退出'
      })
    })
}
</script>
<style lang="scss" scoped></style>

3. token 存储

编辑 src / views / login / index.vue 文件

const handleSubmit = async () => { // 登录请求
...
  store.commit('setUser', {
    ...data.user_info,
    token: data.token
  })
...

编辑 src / utils / request.ts 文件

// 根据不同环境 切换不同路径
const request = axios.create({
  baseURL: import.meta.env.VITE_API_BASEURL
})

// 请求拦截器
request.interceptors.request.use(function (config: any) {
  // 统一设置用户身份 token
  const user = store.state.user
  if (user && user.token) {
    config.headers.Authorization = `Bearer ${user.token}`
  }
  return config
}, function (error) {
  return Promise.reject(error)
})

4. 页面展示

在这里插入图片描述

这里退出并没有成功, 因为退出需要传递用户 token 作为标识



四、登录状态

1. 路由校验

路由元信息

编辑 router.d.ts 文件

import 'vue-router'

declare module 'vue-router' {
  // eslint-disable-next-line no-unused-vars
  interface RouteMeta {
    title?: string,
    requiresAuth?: boolean
  }
}

2. 路由配置

编辑 src / router / index.ts 文件

...
        path: '', // 默认子路由
        name: 'home',
        component: () => import('../views/home/index.vue'),
        meta: { title: '首页', requiresAuth: true }
      },
...

// 全局前置守卫
router.beforeEach((to, form) => {
  nprogress.start() // 开始加载进度条
  if (to.meta.requiresAuth && !store.state.user) {
    // 此路由需要授权,请检查是否已登录
    // 如果没有,则重定向到登录页面
    return {
      path: '/login',
      // 保存我们所在的位置,以便以后再来
      query: { redirect: to.fullPath }
    }
  }
})
...

编辑 src / router / modules /product.ts 文件

...
  path: 'product',
  component: RouterView,
  meta: {
    title: '商品',
    requiresAuth: true
  },
...

3. 登录跳转

编辑 src / views / login / index.vue 文件

...
  console.log('data =>', data)
  // 获取当前路由对象
  let redirect = route.query.redirect || '/'
  if (typeof redirect !== 'string') {
    redirect = '/'
  }
  router.replace(redirect)
...

4. 登录过期

编辑 src / utils / request.ts 文件

import axios, { AxiosRequestConfig } from 'axios'
import { ElMessage, ElMessageBox } from 'element-plus'
import { store } from '@/store'
import router from '@/router/'

// 根据不同环境 切换不同路径
const request = axios.create({
  baseURL: import.meta.env.VITE_API_BASEURL
})

// 请求拦截器
request.interceptors.request.use(function (config: any) {
  // 统一设置用户身份 token
  const user = store.state.user
  if (user && user.token) {
    config.headers.Authorization = `Bearer ${user.token}`
  }
  return config
}, function (error) {
  return Promise.reject(error)
})

// 控制登录过期的锁
let isRefreshing = false

// 响应拦截器
request.interceptors.response.use(function (response) {
  // 统一设置接口相应错误, 比如 token 过期失效, 服务端异常
  // if (response.data.status && response.data.status !== 200) { // 后端返回访问失败
  //   ElMessage.error(response.data.msg || '请求失败, 请刷新后重试')
  //   // 手动返回一个 Promise 异常
  //   return Promise.reject(response.data)
  // }
  // return response

  const status = response.data.status
  if (!status || status === 200) { // 正常情况
    return response
  }
  if (status === 410000) { // 异常情况: token 过期...
    if (isRefreshing) return Promise.reject(response)
    isRefreshing = true
    // 提示是否跳转登录页
    ElMessageBox.confirm('你的登录状态已经过期, 是否前往登录页面?', '提示', {
      confirmButtonText: '确认',
      cancelButtonText: '取消'
    })
      .then(() => {
        // 清除本地过期登录状态
        store.commit('setUser', null)
        // 跳转登录页面
        router.push({
          name: 'login',
          query: {
            redirect: router.currentRoute.value.fullPath
          }
        })
        // 抛出异常
      })
      .finally(() => {
        isRefreshing = false
      })
      // 内部这个消化业务异常
    return Promise.reject(response)
  }
  // 其他情况
  ElMessage.error(response.data.msg || '请求失败, 请刷新后重试')
  // 手动返回一个 Promise 异常
  return Promise.reject(response.data)
}, function (error) {
  return Promise.reject(error)
})

// 接口返回数据处理
export default <T = any>(config: AxiosRequestConfig) => {
  return request(config).then(res => {
    return (res.data.data || res.data) as T
  })
}

5. 实现功能

  • 跳转路由时,会校验用户 token,如果为空则进入登录页
  • 会记录用户上一次登录页, 登录后,跳转相应页面
  • token 过期时,进行提示


  • 2
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

后海 0_o

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值