在 views
中 login
文件夹,创建 index.vue
文件,书写登录界面,
都是一些基本的UI操作,我下边直接粘贴出代码,
<template>
<div class="login-container">
<el-form ref="loginFromRef" class="login-form" :model="loginForm" :rules="loginRules">
<div class="title-container">
<h3 class="title">后台管理系统</h3>
</div>
<el-form-item prop="username">
<span class="icon-container">
<i class="el-icon-user"></i>
</span>
<el-input placeholder="请输入用户名" name="username" type="text" v-model="loginForm.username" />
</el-form-item>
<el-form-item prop="password">
<span class="icon-container">
<i class="el-icon-lock"></i>
</span>
<el-input placeholder="请输入密码" name="password" :type="passwordType" v-model="loginForm.password" />
<span class="show-pwd">
<svg-icon
:icon="passwordType === 'password' ? 'eye' : 'eye-open'"
@click="onChangePwdType"
/>
</span>
</el-form-item>
<el-form-item class="code-box">
<span class="icon-container">
<i class="el-icon-tickets"></i>
</span>
<el-input
placeholder="图形验证码"
v-model="loginForm.captcha_code" class="code-input" maxlength="4">
</el-input>
<div class="code-img" @click="getCodeImg">{{loginForm.captcha_code}}</div>
</el-form-item>
<el-button type="primary" style="width: 100%; margin-bottom: 30px;" :loading="loading"
@click="handleLogin">登录</el-button>
</el-form>
</div>
</template>
<script setup>
import {} from 'vue'
</script>
<style lang="scss" scoped></style>
在 router/index.js
中增加以下路由配置
/**
* 公开路由表
*/
const publicRoutes = [
{
path: '/login',
component: () => import('@/views/login/index')
}
]
const router = createRouter({
history: createWebHashHistory(),
routes: publicRoutes
})
其中的一些icon图标用的是自己封装的SvgIcon组件(包含element-plus
的图标和自定义的 svg
图标),我们这节主要讲的是后台系统整个登录的方案实现,一些设计的简单UI及其UI组件,相信只要有过 element-ui
使用经验的同学,应该对这里都不陌生,所以这里就不对这块内容进行过多赘述了,如果有想要代码的小伙伴,下方留言,我可以私发你具体的代码。
整个登录界面的UI搭建完成后,效果就是这样的
image.png处理完了基本UI表单操作之后,接下来就是登录操作的实现了。
对于登录操作在后台项目中是一个通用的解决方案,具体可以分为以下几点:
- 封装
axios
模块 - 封装 接口请求 模块
- 封装登录请求动作
- 保存服务端返回的
token
- 登录鉴权
这些内容就共同的组成了一套 后台登录解决方案 。接下来,我们就分别来去处理介绍这些内容。
封装 axios
模块
在当前这个场景下,我们希望封装出来的 axios
模块,至少需要具备一种能力,那就是:根据当前模式的不同,设定不同的 BaseUrl
,因为通常情况下企业级项目在 开发状态 和 生产状态 下它的 baseUrl
是不同的。
对于 @vue/cli
来说,它具备三种不同的模式:
development
test
production
根据我们前面所提到的 开发状态和生产状态 那么此时我们的 axios
必须要满足:在 开发 || 生产 状态下,可以设定不同 BaseUrl
的能力
那么想要解决这个问题,就必须要使用到 @vue/cli
所提供的 环境变量 来去进行实现。
我们可以在项目中创建两个文件:
-
.env.development
-
.env.production
它们分别对应 开发状态 和 生产状态。
我们可以在上面两个文件中分别写入以下代码:
.env.development
:
# 标志
ENV = 'development'
# base api
VUE_APP_BASE_API = '/api'
.env.production
:
# 标志
ENV = 'production'
# base api
VUE_APP_BASE_API = '/prod-api'
有了这两个文件之后,我们就可以创建对应的 axios
模块
创建 utils/request.js
,写入如下代码:
import axios from 'axios'
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 5000
})
export default service
封装请求动作
创建 api
文件夹,创建 sys.js
:
import request from '@/utils/request'
/**
* 登录
*/
export const login = data => {
return request({
url: '/sys/login',
method: 'POST',
data
})
}
封装登录请求动作:
该动作我们期望把它封装到 vuex
的 action
中
在 store
下创建 modules
文件夹,创建 user.js
模块,用于处理所有和 用户相关 的内容:
import { login } from '@/api/sys'
export default {
namespaced: true,
state: () => ({}),
mutations: {},
actions: {
login(context, userInfo) {
const { username, password } = userInfo
return new Promise((resolve, reject) => {
login({
username,
password: password
})
.then(data => {
resolve()
})
.catch(err => {
reject(err)
})
})
}
}
}
在 store/index
中完成注册:
import { createStore } from 'vuex'
import user from './modules/user.js'
export default createStore({
modules: {
user
}
})
登录触发动作
<script setup>
import { ref, onMounted } from 'vue'
import { validatePassword } from './rules'
import { getCode } from '@/api/user'
import { useStore } from 'vuex'
import { useRouter } from 'vue-router'
onMounted(() => {
getCodeImg()
})
//数据源
const loginForm = ref({
username: 'test',
password: '123456',
captcha_code: '',
code_key: ''
})
//验证规则
const loginRules = ref({
username: [
{
required: true,
trigger: 'blur',
message: '请输入用户名'
},
],
password: [
{
required: true,
trigger: 'blur',
validator: validatePassword()
},
],
})
// 处理密码框文本显示状态
const passwordType = ref('password')
const onChangePwdType = () => {
if (passwordType.value === 'password') {
passwordType.value = 'text'
} else {
passwordType.value = 'password'
}
}
// 登录动作处理
const loading = ref(false)
const loginFromRef = ref(null)
const store = useStore()
const router = useRouter()
/**
* 登录
*/
const handleLogin = () => {
loginFromRef.value.validate(valid => {
if (!valid) return
loading.value = true
store.dispatch('user/login', loginForm.value)
.then(() => {
loading.value = false
// TODO: 登录后操作
router.push('/')
})
.catch(err => {
getCodeImg()
loading.value = false
})
})
}
/**
* 获取图形验证码
*/
const getCodeImg = () => {
getCode({})
.then(data => {
let obj = data.bizobj
loginForm.value.code_key = obj.code_key
loginForm.value.captcha_code = obj.code
})
.catch(err => {
})
}
</script>
当然这时候你如果,去触发登录操作,可能会报错,因为我们还没有设置代理
在 vue.config.js
中,加入以下代码:
devServer: {
open: true,//启动自动打开浏览器
// 当地址中有/api的时候会触发代理机制
proxy: {
'/': {
target: '填写你们自己公司的测试URL', //测试环境
changeOrigin: true,
ws: false,
pathRewrite: {
// '^/api': '' //需要rewrite重写的,
}
}
}
},
重新启动服务,再次进行请求,就可以得到返回数据了
本地缓存处理方案
通常情况下,在获取到 token
之后,我们会把 token
进行缓存,而缓存的方式将会分为两种:
- 本地缓存:
LocalStorage
- 全局状态管理:
Vuex
保存在 LocalStorage
是为了方便实现 自动登录功能
保存在 vuex
中是为了后面在其他位置进行使用
那么下面我们就分别来实现对应的缓存方案:
LocalStorage:
-
创建
utils/storage.js
文件,封装三个对应方法:/** * 存储数据 */ export const setItem = (key, value) => { // 将数组、对象类型的数据转化为 JSON 字符串进行存储 if (typeof value === 'object') { value = JSON.stringify(value) } window.localStorage.setItem(key, value) } /** * 获取数据 */ export const getItem = key => { const data = window.localStorage.getItem(key) try { return JSON.parse(data) } catch (err) { return data } } /** * 删除数据 */ export const removeItem = key => { window.localStorage.removeItem(key) } /** * 删除所有数据 */ export const removeAllItem = key => { window.localStorage.clear() }
-
在
vuex
的user
模块下,处理token
的保存import { login } from '@/api/sys' import { setItem, getItem } from '@/utils/storage' import { TOKEN } from '@/constant' export default { namespaced: true, state: () => ({ token: getItem(TOKEN) || '' }), mutations: { setToken(state, token) { state.token = token setItem(TOKEN, token) } }, actions: { login(context, userInfo) { ... .then(data => { this.commit('user/setToken', data.data.data.token) resolve() }) ... }) } } }
-
处理保存的过程中,需要创建
constant
常量目录constant/index.js
export const TOKEN = 'token'
此时,当点击登陆时,即可把 token
保存至 vuex
与 localStorage
中
响应数据的统一处理
在 utils/request.js
中实现以下代码:
import axios from 'axios'
import { ElMessage } from 'element-plus'
...
// 响应拦截器
service.interceptors.response.use(
response => {
const { success, message, data } = response.data
// 要根据success的成功与否决定下面的操作
if (success) {
return data
} else {
// 业务错误
ElMessage.error(message) // 提示错误消息
return Promise.reject(new Error(message))
}
},
error => {
ElMessage.error(error.message) // 提示错误信息
return Promise.reject(error)
}
)
export default service
此时,对于 vuex 中的 user 模块
就可以进行以下修改了:
this.commit('user/setToken', data.token)
登录后操作
那么截止到此时,我们距离登录操作还差最后一个功能就是 登录鉴权 。
只不过在进行 登录鉴权 之前我们得先去创建一个登录后的页面,也就是我们所说的登录后操作。
-
创建
layout/index.vue
,写入以下代码:<template> <div class="">Layout 页面</div> </template> <script setup> import {} from 'vue' </script> <style lang="scss" scoped></style>
-
在
router/index
中,指定对应路由表:const publicRoutes = [ ... { path: '/', component: () => import('@/layout/index') } ]
-
在登录成功后,完成跳转
// 登录后操作 router.push('/')
登录鉴权解决方案
在处理了登陆后操作之后,接下来我们就来看一下最后的一个功能,也就是 登录鉴权
首先我们先去对 登录鉴权 进行一个定义,什么是 登录鉴权 呢?
当用户未登陆时,不允许进入除
login
之外的其他页面。用户登录后,
token
未过期之前,不允许进入login
页面
而想要实现这个功能,那么最好的方式就是通过 路由守卫 来进行实现。
那么明确好了 登录鉴权 的概念之后,接下来就可以去实现一下
在 main.js
平级,创建 permission
文件
import router from './router'
import store from './store'
// 白名单
const whiteList = ['/login']
/**
* 路由前置守卫
*/
router.beforeEach(async (to, from, next) => {
// 存在 token ,进入主页
// if (store.state.user.token) {
// 快捷访问
if (store.getters.token) {
if (to.path === '/login') {
next('/')
} else {
next()
}
} else {
// 没有token的情况下,可以进入白名单
if (whiteList.indexOf(to.path) > -1) {
next()
} else {
next('/login')
}
}
})
在此处我们使用到了 vuex 中的 getters
,此时的 getters
被当作 快捷访问 的形式进行访问
所以我们需要声明对应的模块,创建 store/getters
const getters = {
token: state => state.user.token
}
export default getters
在 store/index
中进行导入:
import getters from './getters'
export default createStore({
getters,
})
那么到这里我们整个"通用后台登录方案解析"就算是全部讲解完成了。
当然,里面的一些代码,例如网络请求的解析,这些就需要你根据自己公司的接口自行去调整了,还有登录鉴权的一些返回码,都需要具体问题具体分析,我们这边只是给出一个通用的方式,及其思路,但相信你看完整篇文章,这些也不是什么大问题的。加油吧!
-
下一次,我们继续介绍 “# 项目架构之搭建Layout架构 解决方案与实现”,下面给个预告图,下次再见吧
image.png
网友评论