Vue\vue3_admin\src\router\index.ts
// import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
import { constantRoutes } from './routes'
const router = createRouter({
// 路由器的工作模式 `history`模式 `hash`模式
history: createWebHistory(),
// history: createWebHashHistory(import.meta.env.BASE_URL),
// 路由配置
// routes: allRoutes,
routes: constantRoutes,
// 滚动行为
scrollBehavior() {
return {
left: 0,
top: 0,
}
}
})
export default router
Vue\vue3_admin\src\router\routes.ts
// 对外暴露配置路由 常量路由
export const constantRoutes = [
{ // 首页
path: '/',
component: () => import('@/layout/index.vue'),
name: 'Layout',
redirect: '/home',
meta: {
title: '', // 菜单标题
isShow: false,
icon: '', // 菜单文字左侧的图标,支持element-plus全部图标
},
children: [
{
path: '/home',
name: 'Home',
component: () => import('@/views/home/index.vue'),
meta: {
title: '首页', // 菜单标题
isShow: true, // 代表路由标题在菜单中是否显示,true 显示 false 不显示
icon: 'HomeFilled', // 菜单文字左侧的图标,支持element-plus全部图标
}
},
]
},
// { // 登录后显示的首页
// path: '/home',
// name: 'home',
// component: () => import('@/layout/index.vue')
// },
{ // 登录页
path: '/login',
name: 'Login',
component: () => import('@/views/login/index.vue'),
meta: {
title: '登录', // 菜单标题
isShow: false, // 代表路由标题在菜单中是否显示,true 显示 false 不显示
icon: 'Promotion', // 菜单文字左侧的图标,支持element-plus全部图标
},
},
{ // 404
path: '/404',
name: '404',
component: () => import('@/views/404/index.vue'),
meta: {
title: '404', // 菜单标题
isShow: false, // 代表路由标题在菜单中是否显示,true 显示 false 不显示
icon: 'Failed',
}
},
{ // 数据大屏
path: '/screen',
name: 'Screen',
component: () => import('@/views/screen/index.vue'),
meta: {
title: '数据大屏', // 菜单标题
isShow: true, // 代表路由标题在菜单中是否显示,true 显示 false 不显示
icon: 'DataLine', // 菜单文字左侧的图标,支持element-plus全部图标
}
},
]
// 异步路由
export const asyncRoutes = [
{ // 权限管理
path: '/acl',
component: () => import('@/layout/index.vue'),
name: 'Acl',
redirect: '/acl/user',
meta: {
title: '权限管理', // 菜单标题
isShow: true,
icon: 'Lock', // 菜单文字左侧的图标,支持element-plus全部图标
},
children: [
{
path: '/acl/user',
name: 'AclUser',
component: () => import('@/views/acl/user/index.vue'),
meta: {
title: '用户管理', // 菜单标题
isShow: true, // 代表路由标题在菜单中是否显示,true 显示 false 不显示
icon: 'User', // 菜单文字左侧的图标,支持element-plus全部图标
}
},
{
path: '/acl/role',
name: 'AclRole',
component: () => import('@/views/acl/role/index.vue'),
meta: {
title: '角色管理', // 菜单标题
isShow: true, // 代表路由标题在菜单中是否显示,true 显示 false 不显示
icon: 'UserFilled', // 菜单文字左侧的图标,支持element-plus全部图标
}
},
{
path: '/acl/permission',
name: 'AclPermission',
component: () => import('@/views/acl/permission/index.vue'),
meta: {
title: '菜单管理',
isShow: true,
icon: 'Management',
}
},
]
},
{ // 商品管理
path: '/product',
component: () => import('@/layout/index.vue'),
name: 'Product',
redirect: '/product/trademark',
meta: {
title: '商品管理',
isShow: true,
icon: 'Shop',
},
children: [
{ // 品牌管理
path: '/product/trademark',
name: 'ProductTrademark',
component: () => import('@/views/product/trademark/index.vue'),
meta: {
title: '品牌管理',
isShow: true,
icon: 'ShoppingCart',
}
},
{ // 属性管理
path: '/product/attr',
name: 'ProductAttr',
component: () => import('@/views/product/attr/index.vue'),
meta: {
title: '属性管理',
isShow: true,
icon: 'Goods',
}
},
{ // spu管理 标准化产品单元
path: '/product/spu',
name: 'ProductSpu',
component: () => import('@/views/product/spu/index.vue'),
meta: {
title: 'SPU管理',
isShow: true,
icon: 'Present',
}
},
{ // sku管理 库存量单位
path: '/product/sku',
name: 'ProductSku',
component: () => import('@/views/product/sku/index.vue'),
meta: {
title: 'SKU管理',
isShow: true,
icon: 'ShoppingCartFull',
}
},
]
},
]
// 任意路由
export const anyRoutes = [
{ // 任意未存在页面转404
path: '/:pathMatch(.*)*',
redirect: '/404',
name: 'Any',
meta: {
title: '任意路由', // 菜单标题
isShow: false, // 代表路由标题在菜单中是否显示,true 显示 false 不显示
icon: 'Checked',
}
}
]
// 所有路由
export const allRoutes = [
{ // 首页
path: '/',
component: () => import('@/layout/index.vue'),
name: 'Layout',
redirect: '/home',
meta: {
title: '', // 菜单标题
isShow: false,
icon: '', // 菜单文字左侧的图标,支持element-plus全部图标
},
children: [
{
path: '/home',
name: 'Home',
component: () => import('@/views/home/index.vue'),
meta: {
title: '首页', // 菜单标题
isShow: true, // 代表路由标题在菜单中是否显示,true 显示 false 不显示
icon: 'HomeFilled', // 菜单文字左侧的图标,支持element-plus全部图标
}
},
]
},
// { // 登录后显示的首页
// path: '/home',
// name: 'home',
// component: () => import('@/layout/index.vue')
// },
{ // 登录页
path: '/login',
name: 'Login',
component: () => import('@/views/login/index.vue'),
meta: {
title: '登录', // 菜单标题
isShow: false, // 代表路由标题在菜单中是否显示,true 显示 false 不显示
icon: 'Promotion', // 菜单文字左侧的图标,支持element-plus全部图标
},
},
{ // 404
path: '/404',
name: '404',
component: () => import('@/views/404/index.vue'),
meta: {
title: '404', // 菜单标题
isShow: false, // 代表路由标题在菜单中是否显示,true 显示 false 不显示
icon: 'Failed',
}
},
{ // 数据大屏
path: '/screen',
name: 'Screen',
component: () => import('@/views/screen/index.vue'),
meta: {
title: '数据大屏', // 菜单标题
isShow: true, // 代表路由标题在菜单中是否显示,true 显示 false 不显示
icon: 'DataLine', // 菜单文字左侧的图标,支持element-plus全部图标
}
},
{ // 权限管理
path: '/acl',
component: () => import('@/layout/index.vue'),
name: 'Acl',
redirect: '/acl/user',
meta: {
title: '权限管理', // 菜单标题
isShow: true,
icon: 'Lock', // 菜单文字左侧的图标,支持element-plus全部图标
},
children: [
{
path: '/acl/user',
name: 'AclUser',
component: () => import('@/views/acl/user/index.vue'),
meta: {
title: '用户管理', // 菜单标题
isShow: true, // 代表路由标题在菜单中是否显示,true 显示 false 不显示
icon: 'User', // 菜单文字左侧的图标,支持element-plus全部图标
}
},
{
path: '/acl/role',
name: 'AclRole',
component: () => import('@/views/acl/role/index.vue'),
meta: {
title: '角色管理', // 菜单标题
isShow: true, // 代表路由标题在菜单中是否显示,true 显示 false 不显示
icon: 'UserFilled', // 菜单文字左侧的图标,支持element-plus全部图标
}
},
{
path: '/acl/permission',
name: 'AclPermission',
component: () => import('@/views/acl/permission/index.vue'),
meta: {
title: '菜单管理',
isShow: true,
icon: 'Management',
}
},
]
},
{ // 商品管理
path: '/product',
component: () => import('@/layout/index.vue'),
name: 'Product',
redirect: '/product/trademark',
meta: {
title: '商品管理',
isShow: true,
icon: 'Shop',
},
children: [
{ // 品牌管理
path: '/product/trademark',
name: 'ProductTrademark',
component: () => import('@/views/product/trademark/index.vue'),
meta: {
title: '品牌管理',
isShow: true,
icon: 'ShoppingCart',
}
},
{ // 属性管理
path: '/product/attr',
name: 'ProductAttr',
component: () => import('@/views/product/attr/index.vue'),
meta: {
title: '属性管理',
isShow: true,
icon: 'Goods',
}
},
{ // spu管理 标准化产品单元
path: '/product/spu',
name: 'ProductSpu',
component: () => import('@/views/product/spu/index.vue'),
meta: {
title: 'SPU管理',
isShow: true,
icon: 'Present',
}
},
{ // sku管理 库存量单位
path: '/product/sku',
name: 'ProductSku',
component: () => import('@/views/product/sku/index.vue'),
meta: {
title: 'SKU管理',
isShow: true,
icon: 'ShoppingCartFull',
}
},
]
},
{ // 任意未存在页面转404
path: '/:pathMatch(.*)*',
redirect: '/404',
name: 'Any',
meta: {
title: '任意路由', // 菜单标题
isShow: false, // 代表路由标题在菜单中是否显示,true 显示 false 不显示
icon: 'Checked',
}
},
]
Vue\vue3_admin\src\layout\menu\index.vue
<template>
<!-- <h1>{{ menuList }}</h1> -->
<template v-for="(item) in menuList" :key="item.path">
<!-- 没有子路由 -->
<template v-if="!item.children">
<el-menu-item v-if="item.meta.isShow" :index="item.path" @click="goRoute"
:class="{ fold: layOutSettingStore.fold ? true : false }">
<el-icon>
<component :is="item.meta.icon"></component>
</el-icon>
<template #title>
<span>{{ item.meta.title }}</span>
</template>
</el-menu-item>
</template>
<!-- 有子路由且只有一个子路由 -->
<template v-if="item.children && item.children.length == 1">
<el-menu-item v-if="item.children[0].meta.isShow" :index="item.children[0].path" @click="goRoute"
:class="{ fold: layOutSettingStore.fold ? true : false }">
<el-icon>
<component :is="item.children[0].meta.icon"></component>
</el-icon>
<template #title>
<span>{{ item.children[0].meta.title }}</span>
</template>
</el-menu-item>
</template>
<!-- 有子路由且有多个子路由 -->
<el-sub-menu v-if="item.children && item.children.length > 1" :index="item.path"
:class="{ fold: layOutSettingStore.fold ? true : false }">
<template #title>
<el-icon>
<component :is="item.meta.icon"></component>
</el-icon>
<span>{{ item.meta.title }}</span>
</template>
<LayoutMenu :menuList="item.children"></LayoutMenu>
</el-sub-menu>
</template>
</template>
<script lang="ts" setup name="LayoutMenu">
import useLayOutSettingStore from '@/stores/modules/setting'
import { useRouter } from 'vue-router';
// 获取父组件传递过来的全部路由数组
defineProps(['menuList']);
// 获取路由器对象
let $router = useRouter();
// 点击菜单的回调
const goRoute = (vc: any) => {
// console.log(vc.index);
// console.log($router);
$router.push(vc.index)
}
// 获取layout配置仓库
let layOutSettingStore = useLayOutSettingStore()
</script>
<!-- 暴露一下自己 -->
<script lang="ts">
export default {
name: 'LayoutMenu'
}
</script>
Vue\vue3_admin\src\stores\modules\user.ts
// 引入登录接口
import { reqLogin, reqLogout, reqUserInfo } from '@/api/user';
// 引入数据类型
import type { loginFormData, loginResponseData, responseData, tokenType } from '@/api/user/type';
import type { userState } from './types/types'
// 引入操作本地存储工具方法
import { SET_TOKEN, GET_TOKEN, REMOVE_TOKEN, GET_USERNAME, GET_AVATAR, SET_USERNAME, SET_AVATAR, REMOVE_USERINFO } from '@/utils/token';
// 引入大仓库
import { defineStore } from 'pinia';
// 引入路由(常量路由)
import { constantRoutes, asyncRoutes, anyRoutes } from '@/router/routes';
// 引入深拷贝方法
import cloneDeep from 'lodash/cloneDeep'
// 引入路由器
import router from '@/router';
// 用于过滤当前用户需要展示的异步路由
function filterAsyncRoute(asyncRoutes: any, routes: any) {
return asyncRoutes.filter((item: any) => {
if (routes.includes(item.name)) {
if (item.children && item.children.length > 0) {
// 切换账号时路由数据不匹配问题
item.children = filterAsyncRoute(item.children, routes)
}
return true
}
})
}
// 创建用户相关的小仓库
const useUserStore = defineStore('User', {
// 仓库存储数据的地方
state: (): userState => {
return {
// token: localStorage.getItem("TOKEN") || '', // 用户的唯一标识token
token: GET_TOKEN() as string,
menuRoutes: JSON.parse(localStorage.getItem('menuRoutes') as any), // 仓库存储生成菜单需要的数组(路由)
username: GET_USERNAME() as string,
avatar: GET_AVATAR() as string,
nickName: '',
// userAsyncRoute: JSON.parse(localStorage.getItem('userAsyncRoute') as any)
userAsyncRoute: [],
userButtons: []
}
},
// 异步|逻辑的地方
actions: {
// 用户登录的方法
async userLogin(data: loginFormData) {
// console.log(data);
const request: loginResponseData = await reqLogin(data);
// console.log('登录',request);
// 登录请求成功 200 --> token
if (request.code == 200) {
// pinia仓库存储token
// 由于pinia|vuex存储数据其实是利用js对象,非持久化存储
// console.log(request.data);
this.token = (request.data.token as string);
this.username = (request.data.username as string);
this.nickName = (request.data.nickName as string);
this.avatar = (request.data.avatar as string);
// 本地存储持久化
// localStorage.setItem("TOKEN", (request.data.token as string))
SET_TOKEN((request.data.token as string))
SET_USERNAME((request.data.username as string))
SET_AVATAR((request.data.avatar as string))
return 'ok' // 能保证当前async函数能返回一个成功的promise
} else {
// 登录请求失败 201 --> 登录失败错误的信息
return Promise.reject(new Error(request.data.message))
}
},
// 获取用户信息的方法
async userInfo() {
// 获取用户的信息进行存储在仓库中[用户头像,用户名]
const result = await reqUserInfo();
// console.log(result);
if (result.code == 200) {
this.username = (result.data.user_info.username as string);
this.nickName = (result.data.user_info.nickName as string);
this.avatar = (result.data.user_info.avatar as string);
// 计算当前用户需要展示的异步路由,使用深拷贝
// const userAsyncRoute = filterAsyncRoute(asyncRoutes, result.data.user_info.routes)
const userAsyncRoute = filterAsyncRoute(cloneDeep(asyncRoutes), result.data.user_info.routes)
// console.log(userAsyncRoute);
// 存储用户动态路由
localStorage.setItem('userAsyncRoute', JSON.stringify(userAsyncRoute));
this.userAsyncRoute = userAsyncRoute
// 菜单的路由数据整理
this.menuRoutes = [...constantRoutes, ...userAsyncRoute, ...anyRoutes]
localStorage.setItem('menuRoutes', JSON.stringify(this.menuRoutes));
// 用户按钮权限
this.userButtons = result.data.user_info.buttons as string[]
localStorage.setItem('userButtons', JSON.stringify(this.userButtons));
// 目前路由器管理的只有常量路由:用户计算完毕异步路由、任意路由动态追加
// console.log(router.getRoutes());
userAsyncRoute.forEach((route: any) => {
router.addRoute(route)
});
anyRoutes.forEach((route: any) => {
router.addRoute(route)
});
// console.log(router.getRoutes());
// 持久化
SET_USERNAME((result.data.user_info.username as string))
SET_AVATAR((result.data.user_info.avatar as string))
// console.log("用户名",this.username,"用户头像",this.avatar);
return '获取用户信息成功!'
} else {
return Promise.reject('获取用户信息失败!')
}
},
// 用户退出登录的方法
async userLogout(token: tokenType) {
const request: responseData = await reqLogout(token);
if (request.code == 200) {
// 清理本地token
REMOVE_TOKEN();
REMOVE_USERINFO();
this.token = '';
this.username = '';
this.nickName = '';
this.avatar = '';
// 清空当前动态路由
// localStorage.removeItem('menuRoutes');
// console.log(this.userAsyncRoute);
// this.userAsyncRoute.forEach((route: any) => {
// router.removeRoute(route)
// });
this.userAsyncRoute = []
this.menuRoutes = [...constantRoutes, ...anyRoutes]
localStorage.setItem('menuRoutes', JSON.stringify(this.menuRoutes));
// this.menuRoutes = [];
// console.log("当前Token:",GET_TOKEN());
// console.log("用户登出,清理成功");
// 刷新页面数据,防止数据残留
window.location.reload();
return '用户已登出'
} else {
// 登出失败错误的信息
console.log("用户登出错误");
// 清理本地token
REMOVE_TOKEN();
REMOVE_USERINFO();
this.token = '';
this.username = '';
this.avatar = '';
// 清空当前动态路由及按钮权限
localStorage.removeItem('menuRoutes');
localStorage.removeItem('userAsyncRoute');
this.userAsyncRoute = []
this.userButtons = []
router.replace({ path: '/login' });
return Promise.reject(new Error(request.data.message))
}
}
},
getters: {
},
// 开户持久化 默认存储到 sessionStorage
persist: true,
})
// 对外暴露获取小仓库的方法
export default useUserStore
Vue\vue3_admin\src\directive\has.ts
// 自定义指令控制按钮权限
import pinia from '@/stores'
import useUserStore from '@/stores/modules/user';
const userStore = useUserStore(pinia)
export const isHasButton = (app: any) => {
//获取对应的用户仓库
//全局自定义指令:实现按钮的权限
app.directive('has', {
//代表使用这个全局自定义指令的DOM|组件挂载完毕的时候会执行一次
mounted(el: any, options: any) {
//自定义指令右侧的数值:如果在用户信息buttons数组当中没有
//从DOM树上干掉或禁用
if (userStore.userButtons.includes('btn.all')) {
// el.parentNode.removeChild(el)
// console.log(el);
el.disabled = false
// el.parentNode.style.opacity = 0.3
} else if (userStore.userButtons.includes(options.value)) {
// el.parentNode.removeChild(el)
el.disabled = false
} else {
// el.parentNode.removeChild(el)
// el.disabled = true
el.disabled = true
el.parentNode.style.opacity = 0.3
}
},
})
}