项目一共需要4个一级路由:登录(login)、主页(home)、404、任意路由(重定向到404)。
pnpm install vue-router
在src目录下新建views文件夹,在views中创建login、home、404路由组件。
在src目录下新建router文件夹,书写路由配置(包含index.ts和routes.ts)。
src/router/routes.ts
// 对外暴露配置路由(常量路由)
export const constantRoute = [
{
// 登录
path: '/login',
component: () => import('@/views/login/index.vue'),
name: 'login'
},
{
// 登录成功以后展示数据的路由
path: '/',
component: () => import('@/views/home/index.vue'),
name: 'layout'
},
{
// 404
path: '/404',
component: () => import('@/views/404/index.vue'),
name: '404'
},
{
// 任意路由
path: '/:pathMatch(.*)*',
redirect: '/404',
name: 'Any'
}
]
src/router/index.ts
// 通过vue-router插件实现路由配置
import { createRouter, createWebHashHistory } from 'vue-router';
// 引入routes配置项
import { constantRoute } from './routes';
// 创建路由
let router = createRouter({
// 路由模式hash
history: createWebHashHistory(),
routes: constantRoute,
// 滚动行为
scrollBehavior() {
return {
left: 0,
top: 0
}
}
})
export default router;
在入口文件(main.js)引入路由:
// 引入路由
import router from '@/router'
// 注册模板路由
app.use(router)
最后,在模板中通过
采用element-plus中的Layout布局(栅格布局)、From表单组件、input组件、button组件。
Layout布局:一共是24 分栏,:span代表栅格占据的列数,:xs代表屏幕宽度<768px时栅格占据的列数。
input组件::prefix-icon代表前缀图标,show-password代表是否显示切换密码图标
src/views/login/index.vue
Hello
欢迎来到唧唧bong甄选
登录
点击登录时,会携带用户名和密码向服务器发请求获取token,此时我们需要把token存储起来,用于后续向服务端发请求获取信息的身份验证,这里我们用pinia和loacalStroage进行存储。
安装pinia
pnpm i pinia
创建大仓库:src/store/index.ts
import { createPinia } from 'pinia'
//创建大仓库
const pinia = createPinia()
//对外暴露:入口文件需要安装仓库
export default pinia
在入口文件(main.ts)中引入并安装:src/main.ts
// 引入大仓库
import pinia from './store'
// 安装仓库
app.use(pinia)
创建小仓库:src/store/modules/user.ts
import { defineStore } from 'pinia'
// 引入接口
import { reqLogin } from '@/api/user'
// 引入类型
import type { loginForm, loginResponseData } from '@/api/user/type'
import type { UserState } from './types/type'
// 引入操作本地存储的工具方法
import { SET_TOKEN, GET_TOKEN } from '@/utils/token'
// 创建用户小仓库
const useUserStore = defineStore('User', {
// 小仓库存储数据的地方
state: (): UserState => {
return {
token: GET_TOKEN(), //用户唯一的标识token
}
},
// 异步|逻辑的地方
actions: {
// 用户登录的方法
async userLogin(data: loginForm) {
// 登录请求
let result: loginResponseData = await reqLogin(data)
// 登录请求:成功200->token
// 登录请求:失败201->登录失败错误的信息
if (result.code === 200) {
// pinia仓库存储一下token
// 由于pinia|vuex存储数据其实利用js对象(非持久化存储)
this.token = (result.data.token as string)
// 本地化持久存储一份
SET_TOKEN((result.data.token as string))
// 能保证当前async函数返回一个成功的promise
return 'ok'
}
else {
return Promise.reject(new Error(result.data.message))
}
}
},
getters: {}
})
// 对外暴露用户小仓库
export default useUserStore
在登录页面中引入小仓库,点击登录时通知user小仓库发请求,存储token:src/views/login/index.vue
- userLogin会返回一个Promise,此处可以使用try...catch...或.then来进行下一步结果处理。
- 不管成功或失败,都需要使登录加载效果消失,因此也可以统一写在finally里面:
try { // 保证登录成功 await useStore.userLogin(loginFrom) // 编程式导航跳转到展示数据首页 $router.push('/') // 登录成功信息提示 ElNotification({ type: 'success', message: '登录成功', }) } catch (error) { // 登录失败的提示信息 ElNotification({ type: 'error', message: (error as Error).message }) } finally{ // 登录成功/失败加载效果消失 loading.value = false }
定义小仓库数据state类型:src\store\modules\types\type.ts
// 定义小仓库数据state类型
export interface UserState {
token: string | null
}
登录接口返回的数据类型:src\api\user\type.ts
登录请求可能返回成功/失败的数据,因此类型需要dataType需要包括成功的数据token和失败的数据message,且是可选的,要加上"?"。
interface dataType {
token?: string,
message?:string
}
// 登录接口返回的数据类型
export interface loginResponseData {
code: number,
data: dataType
}
封装本地存储数据和读取方法:src/utils/token.js
// 存储数据
export const SET_TOKEN = (token: string) => {
localStorage.setItem('TOKEN', token)
}
// 本地存储获取数据
export const GET_TOKEN = () => {
return localStorage.getItem('TOKEN')
}
在utils中封装一个函数:src/utils/time.js
// 封装一个函数:获取一个结果:当前早上|上午|中午|下午|晚上
export const getTime = () => {
let time = ''
// 通过内置的构造函数Date
let hour = new Date().getHours()
if (hour < 9) {
time = '早上'
}
else if (hour <= 12) {
time = '上午'
}
else if (hour <= 14) {
time = '中午'
}
else if (hour <= 18) {
time = '下午'
}
else {
time = '晚上'
}
return time
}
在login组件中引入并使用
// 引入当前时间的函数
import { getTime } from '@/utils/time'
......
// 登录成功信息提示
ElNotification({
type: 'success',
message: '欢迎回来',
title: `HI,${getTime()}好`
})
使用element-plus的表单验证功能 ,步骤如下:
- :model:要验证的表单数据对象
- :rules="rules":表单验证规则
- prop:要校验字段的属性名
// 第一步:给el-form添加 :model="loginFrom"和:rules="rules"
// 第二步:给需要验证的每个el-form-item添加prop属性,如 prop="username"、prop="password"
// 第三步:定义表单校验需要配置对象rules
// 规则对象属性:
// required:代表这个字段必须校验
// min:文本长度至少多少位
// max:文本长度最多多少位
// message:错误的提示信息
// trigger:触发校验表单的时机,change:文本发生变化时触发校验,blur:失去焦点时触发校验
const rules = {
username: [
{ required: true, min: 5, max: 10, message: '用户名长度应为5-10位', trigger: 'change' },
],
password: [
{ required: true, min: 6, max: 10, message: '密码长度应为6-10位', trigger: 'change' },
]
}
第四步:请求前使用 loginFroms.value.validate()触发表单中所有表单项的校验,保证全部的表单项校验通过再发请求
// 通过ref属性获取el-form组件
let loginFroms = ref()
const login = async () => {
// 保证全部的表单项校验通过再发请求
await loginForms.value.validate()
......
}
PS:在
el-form
组件中,可以使用ref
属性来获取表单的引用,然后调用该引用上的validate
方法。这个方法会触发表单中所有表单项的校验,并返回一个Promise
对象,该对象的resolve
回调函数会在校验通过时被调用,而reject
回调函数会在校验失败时被调用。
上面的验证比较简单,公司的开发项目中表单验证会更复杂,这个时候就要用到element-plus的自定义校验规则了 。
自定义校验表单的配置项中需要一个validator属性,值是一个方法,用于书写自定义规则。
// 自定义校验规则函数
const validateUsername = (rule: any, value: any, callback: any) => {
//rule:即为校验规则对象
//value:即为表单元素文本内容
//函数:如果符合条件callback放行通过即为
//如果不符合条件callback方法,注入错误提示信息
if(value.length >= 5){
callback()
}
else{
callback(new Error('用户名不少于5位'))
}
}
const validatePassword = (rule: any, value: any, callback: any) => {
if(value.length >= 6){
callback()
}
else{
callback(new Error('用户名不少于6位'))
}
}
// 定义表单校验需要配置对象
const rules = {
username: [
{ validator: validateUsername, trigger: 'change' },
],
password: [
{ validator: validatePassword, trigger: 'change' },
]
}
PS:这里只是简单的示范,正式开发中大多场景的校验规则会更复杂,需要用到正则表达式来书写。
layout组件主页分为三部分:左侧菜单、顶部导航、内容展示区域。
在src目录下创建layout组件:src/layout/index.vue
内容
配置layout相关的样式的全局变量:src/styles/variable.scss
// 左侧菜单的宽度
$base-menu-width: 260px;
// 左侧菜单的背景颜色
$base-menu-background: #001529;
// 顶部导航的高度
$base-tabbar-height: 50px;
// 顶部导航的背景颜色
$base-tabbar-background: #ffffff;
// 内容展示区域的背景颜色
$base-main-background: #ccc8cc;
设置滚动条样式:src/styles/index.scss
// 滚动条外观设置
::-webkit-scrollbar{
width: 10px;
}
::-webkit-scrollbar-track{
background: $base-menu-background;
}
::-webkit-scrollbar-thumb{
width: 10px;
background: yellowgreen;
border-radius: 10px;
}
创建logo组件:src/layout/logo/index.vue
{{ setting.title }}
配置logo相关的样式的全局变量:src/styles/variable.scss
//左侧菜单logo高度设置
$base-menu-logo-height:50px;
//左侧菜单logo右侧文字大小
$base-logo-title-fontSize:16px;
项目logo/标题配置文件:src/setting.ts
// 用于项目logo|标题配置
export default {
title:'唧唧bong甄选运营平台', // 项目标题
logo:'/logo.png', // 项目logo设置
logoHidden: true // logo组件是否隐藏设置
}
创建menu组件:src/layout/menu/index.vue,并在layout中引入并使用menu组件
添加二级路由:src/router/routes.ts
{
// 登录成功以后展示数据的路由
path: '/',
component: () => import('@/layout/index.vue'),
name: 'layout',
meta: {
title: 'layout',
hidden: true
},
children: [
{
path: '/home',
component: () => import('@/views/home/index.vue'),
name: 'home',
meta: {
title: '首页',
hidden: false
}
}
]
},
将路由数组存储到store中(方便组件访问路由数据):src/store/modules/user.ts
// 引入路由(常量路由)
import { constantRoute } from '@/router/routes';
// 创建用户小仓库
const useUserStore = defineStore('User', {
// 小仓库存储数据的地方
state: (): UserState => {
return {
token: GET_TOKEN(), //用户唯一的标识token
//路由配置数据
menuRoutes: constantRoute
}
},
......
})
UserState中添加路由的类型定义:src/store/modules/types/type.ts
// 引入描述路由配置信息的类型(这个类型包含了路由的路径、组件、子路由等信息)
import type { RouteRecordRaw } from 'vue-router'
// 定义小仓库数据state类型
export interface UserState {
token: string | null,
menuRoutes: RouteRecordRaw[]
}
layout组件中引入小仓库获取路由数据,通过props传递给menu组件:src/layout/index.vue
......
给每个路由添加meta元信息:src/router/routes.ts
meta: {
title: '登录', // 菜单标题
hidden: true // 代表路由标题在菜单中是否隐藏 true:隐藏 false:显示
}
书写menu组件:src/layout/menu/index.vue
1. menu组件分三种情况:
- 没有子路由
- 有且只有一个子路由
- 有一个以上的子路由
2. 点击菜单item跳转路由(@click="goRoute"):
标签有click事件(菜单点击时的回调函数,回调参数是el-menu-item实例。
{{ item.meta.title }}
{{ item.children[0].meta.title }}
{{ item.meta.title }}
PS:递归组件必须有一个名字,因为在vue中,组件是通过其名字进行注册和引用的。递归组件需要在自身的模板中引用自身,但如果组件没有名字,Vue无法在模板中正确地引用它,从而导致递归出现问题。
将element-plus图标 注册成全局组件:src/components/index.ts
具体可参考官网:Icon 图标 | Element Plus (gitee.io)
// 引入elemnet-plus提供全部图标组件
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
// 对外暴露一个插件对象
export default {
install(app: any) {
// 将element-plus提供图标注册为全局组件
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
}
}
菜单图标由路由配置决定,meta中添加 icon 字段:src/router/routes.ts
meta: {
......
icon: 'Promotion', // 菜单文字左侧的图标,支持element-plus全部图标
}
在menu中使用element-plus图标:src/layout/menu/index.vue
- 首页重定向到home
- 权限管理和商品管理的一级路由用的还是组件 layout
src/router/routes.ts
// 对外暴露配置路由(常量路由)
export const constantRoute = [
{
// 登录
path: '/login',
component: () => import('@/views/login/index.vue'),
name: 'login',
meta: {
title: '登录', // 菜单标题
hidden: true, // 代表路由标题在菜单中是否隐藏 true:隐藏 false:显示
icon: 'Promotion', // 菜单文字左侧的图标,支持element-plus全部图标
}
},
{
// 登录成功以后展示数据的路由
path: '/',
component: () => import('@/layout/index.vue'),
name: 'layout',
meta: {
title: 'layout',
hidden: true,
icon: 'Avatar',
},
redirect: '/home',
children: [
{
path: '/home',
component: () => import('@/views/home/index.vue'),
name: 'home',
meta: {
title: '首页',
hidden: false,
icon: 'HomeFilled',
}
}
]
},
{
// 404
path: '/404',
component: () => import('@/views/404/index.vue'),
name: '404',
meta: {
title: '404',
hidden: true,
icon: 'BrushFilled',
}
},
{
path: '/screen',
component: () => import('@/views/screen/index.vue'),
name: 'Screen',
meta: {
title: '数据大屏',
hidden: false,
icon: 'Platform',
}
},
{
path: '/acl',
component: () => import('@/layout/index.vue'),
name: 'Acl',
meta: {
title: '权限管理',
icon: 'Lock',
},
children: [
{
path: '/acl/user',
component: () => import('@/views/acl/user/index.vue'),
name: 'User',
meta: {
title: '用户管理',
icon: 'User',
}
},
{
path: '/acl/role',
component: () => import('@/views/acl/role/index.vue'),
name: 'Role',
meta: {
title: '角色管理',
icon: 'UserFilled',
}
},
{
path: '/acl/permission',
component: () => import('@/views/acl/permission/index.vue'),
name: 'Permission',
meta: {
title: '菜单管理',
icon: 'Monitor',
}
},
]
},
{
path: '/product',
component: () => import('@/layout/index.vue'),
name: 'Product',
meta: {
title: '商品管理',
icon: 'Goods',
},
children: [
{
path: '/product/trademark',
component: () => import('@/views/product/trademark/index.vue'),
name: 'Trademark',
meta: {
title: '品牌管理',
icon: 'ShoppingCartFull',
}
},
{
path: '/product/attr',
component: () => import('@/views/product/attr/index.vue'),
name: 'Attr',
meta: {
title: '属性管理',
icon: 'ChromeFilled',
}
},
{
path: '/product/spu',
component: () => import('@/views/product/spu/index.vue'),
name: 'Spu',
meta: {
title: 'SPU管理',
icon: 'Calendar',
}
},
{
path: '/product/sku',
component: () => import('@/views/product/sku/index.vue'),
name: 'Sku',
meta: {
title: 'SKU管理',
icon: 'Orange',
}
},
]
},
{
// 任意路由
path: '/:pathMatch(.*)*',
redirect: '/404',
name: 'Any',
meta: {
title: '任意路由',
hidden: true,
icon: 'Wallet',
}
}
]
layout右侧展示区域封装成一个组件 main(为了实现一些动画效果):src/layout/main/main.vue
关于路由过度可参考官网:过渡动效 | Vue Router (vuejs.org)
在layout组件中引入main:src/layout/index.vue
// 右侧内容展示组件
import Main from '@/layout/main/index.vue'
左侧菜单刷新折叠问题解决:src/layout/index.vue
// el-menu中新增default-active属性
// 获取路由对象
import { useRoute } from 'vue-router'
let $route = useRoute()
tabbar组件封装:拆分成左侧面包屑组件(breadcrumb)和右侧设置组件(setting)
面包屑组件:scr/layout/tabbar/breadcrumb/index.vue
权限管理
用户管理
设置组件:scr/layout/tabbar/setting/index.vue
admin
退出登录
tabbar组件:scr/layout/tabbar/index.vue
定义控制折叠/展开响应式数据fold:src/store/modules/setting.ts
因为layout组件和breadcrumb组件都需要用到fold,说定义在仓库比较合适。
// 小仓库:layout组件相关配置仓库
import { defineStore } from 'pinia'
const useLayoutSettingStore = defineStore('SettingStore', {
state: () => {
return {
fold: false, // 用户控制菜单折叠还是收起
}
}
})
export default useLayoutSettingStore
面包屑组件折叠图标切换实现:src/layout/tabbar/breadcrumb/index.vue
......
layout组件菜单折叠效果实现:src/layout/index.vue
步骤:
- 通过el-menu标签的collapse属性配合fold实现菜单折叠/展开效果
- 给左侧菜单、顶部导航、右侧内容展示区域添加动态类fold实现折叠/展开的布局改变
PS:折叠之后图标不见的问题:将icon标签放在title插槽外面
- 通过$route.matched获取匹配的路由信息实现动态展示。
- 点击首页不需要展示layout路由,所以删除router.ts文件中layout路由中的元信息title和icon的值,并通过v-show判断是否展示。
- 通过 :to 可使点击面包屑跳转匹配路由。
src/layout/tabbar/breadcrumb/index.vue
{{ item.meta.title }}
// 获取路由对象
import { useRoute } from 'vue-router'
let $route = useRoute()
PS:点击商品管理、权限管理等一级路由的面包屑时,默认跳转到它的首个二级路由,因此需要在router.ts文件中给商品管理、权限管理的路由添加重定向。
redirect: '/acl/user',
redirect: '/product/trademark',
刷新业务就是路由组件销毁和重建的过程。涉及顶部导航组件和内容区域组件通信,因此可以使用store存储刷新业务相关标识。
小仓库中添加刷新标识数据:src/store/modules/setting.ts
refresh: false,// 用于控制刷新效果
顶部导航setting组件实现控制下仓库refresh变化 :src/layout/tabbar/setting/index.vue
// 给刷新按钮绑定点击事件
// 获取仓库中刷新标识
import useLayoutSettingStore from '@/store/modules/setting'
let layoutSettingStore = useLayoutSettingStore()
// 刷新按钮点击回调
const updateRefresh = () => {
// 更新刷新标识
layoutSettingStore.refresh = !layoutSettingStore.refresh
}
main组件中监听小仓库refresh是否变化,控制路由销毁与重建:src/layout/main/index.vue
import { watch, ref, nextTick } from 'vue'
import useLayoutSettingStore from '@/store/modules/setting'
let layoutSettingStore = useLayoutSettingStore()
// 控制当前组件是否销毁重建
let flag = ref(true)
// 监听仓库内部数据是否发生变化,如果发生变化,说明用户点击过刷新按钮
watch(() => layoutSettingStore.refresh, () => {
// 点击刷新按钮:路由组件销毁
flag.value = false
nextTick(() => {
flag.value = true
})
})
这里利用DOM实现全屏切换(不同浏览器可能会有兼容问题),也可以使用插件实现。
src/layout/tabbar/setting/index.vue
// 给全屏按钮绑定点击事件
// 全屏按钮点击回调
const fullScreen = () => {
// DOM对象的一个属性:可以用来判断当前是不是全屏模式(全屏:true,不是全屏:false)
let full = document.fullscreenElement
// 切换为全屏模式
if (!full) {
// 文档根节点的方法requestFullscreen,实现全屏模式
document.documentElement.requestFullscreen()
} else {
// 变为不是全屏模式 -> 退出全屏模式
document.exitFullscreen()
}
}
发生登录请求时由后端返回的唯一标识,后续向后端发送各种请求都需要携带token,因此token作为每次请求都需带的公共参数,放在请求拦截器里,通过config配置项hearders携带最合适。
src/utils/request.ts
// 引入用户相关的小仓库
import useUserStore from '@/store/modules/user';
request.interceptors.request.use((config) => {
// config配置对象,包括hearders属性请求头,经常给服务端携带公共参数
let useStore = useUserStore()
if(useStore.token){
config.headers.token = useStore.token
}
// 返回配置对象
return config;
});
home首页挂载完毕发请求获取用户信息:src/views/home/index.vue
import {onMounted} from 'vue'
// 获取仓库
import useUserStore from '@/store/modules/user';
let useStore = useUserStore()
// 目前首页挂载完毕发请求获取用户信息
onMounted(() => {
useStore.userInfo()
})
用户小仓库:src/store/modules/user.ts
在type.ts文件中定义username、avatar类型:
username: string, avatar: string
// 小仓库存储数据的地方
state: (): UserState => {
return {
......
username:'',
avatar:''
}
},
// 异步|逻辑的地方
actions: {
......
// 获取用户信息
async userInfo(){
// 获取用户信息进行存储仓库当中(用户头像、名字)
let result = await reqUserInfo()
// 如果获取信息成功,存储下用户信息
if(result.code === 200){
this.username = result.data.checkUser.username
this.avatar = result.data.checkUser.avatar
}
}
},
在setting组件中,通过user小仓库获取用户信息进行展示:src/layout/tabbar/setting/index.vue
......
{{ useStore.username }}
......
// 获取用户相关的小仓库
import useUserStore from '@/store/modules/user';
let useStore = useUserStore()
退出登录时,需要做的事情 :
- 需要向服务器发请求(退出登录接口)
- 仓库中关于用户相关的数据清空(token|username|avatar)
- 跳转到登录页面
src/layout/tabbar/setting/index.vue
退出登录
// 退出登录点击回调
const logout = () => {
// 第一件事情:需要向服务器发请求(退出登录接口)----目前还没有
// 第二件事情:仓库中关于用户相关的数据清空(token|username|avatar)
useStore.userLogout()
// 第三件事情:跳转到登录页面,通过query参数传递退出登录前的路径
$router.push({ path: '/login', query: { redirect: $route.path } })
}
封装删除token本地存储的方法:src/utils/token.ts
// 本地存储删除数据方法
export const REMOVE_TOKEN = () => {
localStorage.removeItem('TOKEN')
}
用户小仓库:src/store/modules/user.ts
// 引入操作本地存储的工具方法
import { SET_TOKEN, GET_TOKEN, REMOVE_TOKEN } from '@/utils/token'
// 退出登录
userLogout() {
// 目前没有mock接口:退出登录接口(通知服务器本地用户唯一标识失败)
this.token = ''
this.username = ''
this.avatar = ''
REMOVE_TOKEN()
}
login组件添加登录前判断跳转路由的逻辑:src/views/login/index.vue
import { useRouter, useRoute } from 'vue-router'
// 获取路由对象
let $route = useRoute()
......
// 判断登录的时候,路由的路径当中是否有query参数,如果有就往query参数跳转,没有就跳转到首页
let redirect: any = $route.query.redirect
$router.push({ path: redirect || '/' })
暗黑模式需要在main.js文件中引入所需的样式
import 'element-plus/theme-chalk/dark/css-vars.css'
src/layout/tabbar/setting/index.vue
PS: 遇到ColorPicker弹出框但是还没有选择颜色Popover就关闭的问题
解决方法:可以在el-color-picker组件加上:teleported="false"防止外部Popover关闭
import { ref } from 'vue'
//收集开关的数据
let dark = ref(false);
//颜色组件组件的数据
const color = ref('rgba(255, 69, 0, 0.68)')
const predefineColors = ref([
'#ff4500',
'#ff8c00',
'#ffd700',
'#90ee90',
'#00ced1',
'#1e90ff',
'#c71585',
'rgba(255, 69, 0, 0.68)',
'rgb(255, 120, 0)',
'hsv(51, 100, 98)',
'hsva(120, 40, 94, 0.5)',
'hsl(181, 100%, 37%)',
'hsla(209, 100%, 56%, 0.73)',
'#c7158577',
])
// switch开关的chang事件进行暗黑模式的切换
const changeDark = () => {
//获取HTML根节点
let html = document.documentElement
//判断HTML标签是否有类名dark
dark.value ? html.className = 'dark' : html.className = ''
}
// 主题颜色的设置
const setColor = () => {
//通知js修改根节点的样式对象的属性与属性值
let html = document.documentElement
html.style.setProperty('--el-color-primary', color.value)
}
暗黑模式使用可参考:暗黑模式 | Element Plus (element-plus.org)
主题颜色使用可参考:主题 | Element Plus (element-plus.org)
路由鉴权: 项目中能不能被访问的权限设置(某一个路由什么条件下可以访问,什么条件下不可以访问)。
安装nprogress插件:pnpm i nprogress
src/permission.ts
// 路由鉴权:项目中能不能被访问的权限设置(某一个路由什么条件下可以访问,什么条件下不可以访问)
import router from '@/router'
import setting from '@/setting'
import { SET_TOKEN, GET_TOKEN, REMOVE_TOKEN } from '@/utils/token'
// @ts-ignore
import nprogress from 'nprogress'
// 引入进度条样式
import "nprogress/nprogress.css"
nprogress.configure({ showSpinner: false })
// 获取用户相关的小仓库内部token数据,去判断用户是否登录成功
import useUserStore from './store/modules/user'
import pinia from './store'
let useStore = useUserStore(pinia)
// 全局守卫:项目中任意路由切换都会触发的钩子
// 全局前置守卫
router.beforeEach(async (to: any, from: any, next: any) => {
// to:你将要访问哪个路由
// from:你从哪个路由而来
// next:路由的放行函数
// 进度条开始
nprogress.start()
// 获取token,去判断用户登录,还是未登录
let token = useStore.token
// 获取用户名字
let username = useStore.username
// 用户登录判断
if (token) {
// 登录成功,不能访问login,指向home
if (to.path == '/login') {
next({ path: '/' })
} else {
// 登录成功访问其余六个路由(登录排除)
// 有用户信息
if (username) {
// 放行
next()
} else {
// 如果没有用户信息,在守卫这里发请求获取到了用户信息再放行
try {
// 获取用户信息
await useStore.userInfo()
// 放行
next()
} catch (error) {
// token过期:获取不到用户信息了
// 用户手动修改本地存储token
// 退出登录->用户相关的数据清空
useStore.userLogout()
next({ path: '/login' })
}
}
}
} else {
// 用户未登录判断
if (to.path == '/login') {
next()
} else {
next({ path: '/login', query: { redirect: to.path } })
}
}
})
// 全局后置守卫
router.afterEach((to: any, from: any) => {
document.title = `${setting.title} - ${to.meta.title}`
// 进度条结束
nprogress.done()
})
// 第一个问题:任意路由切换实现进度条业务 ---nprogress
// 第二个问题:路由鉴权(路由组件访问权限的设置)
// 全部路由组件:登录|404|任意路由|首页|数据大屏|权限管理(三个子路由)|商品管理(四个子路由)
// 用户未登录:可以访问login,其余六个路由不能访问(指向login)
// 用户登录成功:不可以访问login(指向首页)
PS:在组件的外部通过同步的语句获取仓库的数据是拿不到的。如果想获取小仓库的数据,必须先得有大仓库(pinia) 。
在入口文件(main.ts)引入鉴权文件
// 引入路由鉴权文件
import './permission'
1. 替换各个环境下的服务器地址( .env.development、.env.production、.env.test )
2. 配饰代理跨域:vite.config.ts(具体配置参数可参考官网:开发服务器选项 | Vite 官方中文文档)
export default defineConfig(({ command, mode }) => {
// 获取各种环境下对应的变量
let env = loadEnv(mode, process.cwd())
return {
......
// 代理跨域
server: {
proxy: {
[env.VITE_APP_BASE_API]: {
// 获取数据的服务器地址设置
target: env.VITE_SERVE,
// 是否代理跨域
changeOrigin: true,
// 路径重写
rewrite: (path) => path.replace(/^\/api/, ''),
}
}
}
}
})
3. 重新书写API接口文件及接口类型文件
src/api/user/index.ts
// 统一管理项目用户相关的接口
import request from "@/utils/request";
import type { loginFormData, loginResponseData, userInfoResponeData } from "./type"
// 项目用户相关的请求地址
enum API {
LOGIN_URL = '/admin/acl/index/login',
USERINFO_URL = '/admin/acl/index/info',
LOGOUT_URL = '/admin/acl/index/logout',
}
// 暴露请求函数
// 登录接口
export const reqLogin = (data: loginFormData) => request.post(API.LOGIN_URL, data)
// 获取用户信息
export const reqUserInfo = () => request.get(API.USERINFO_URL)
// 退出登录
export const reqLogout = () => request.post(API.LOGOUT_URL)
src/api/user/index.ts
// 定义用户相关数据的ts类型
// 用户登录接口携带参数的ts类型
export interface loginFormData {
username: string,
password: string
}
// 定义全部接口返回数据都拥有的ts类型
export interface ResponseData {
code: number,
message: string,
ok: boolean
}
// 定义登录接口返回数据类型
export interface loginResponseData extends ResponseData {
data: string
}
// 定义获取用户信息返回的数据类型
export interface userInfoResponeData extends ResponseData {
data: {
routes: string[],
buttons: string[],
roles: string[],
name: string,
avatar: string
}
}
4. 修改接口相关的代码(src/store/modules/user.ts、permission.ts等文件)