优化全局状态,将导航和路由拆分成menu和route分别管理

This commit is contained in:
hooray 2022-02-01 02:31:55 +08:00
parent c8e2d48f3a
commit 151e510e2c
9 changed files with 298 additions and 314 deletions

View File

@ -6,7 +6,7 @@
<Logo />
<!-- 顶部模式 -->
<div class="nav">
<template v-for="(item, index) in menuStore.transformRoutes">
<template v-for="(item, index) in menuStore.allMenus">
<div v-if="item.children && item.children.length !== 0" :key="index" class="item" :class="{'active': index == menuStore.headerActived}" @click="switchMenu(index)">
<svg-icon v-if="item.meta.icon" :name="item.meta.icon" />
<span v-if="item.meta.title">{{ item.meta.title }}</span>

View File

@ -4,7 +4,7 @@
<Logo :show-title="false" class="sidebar-logo" />
<!-- 侧边栏模式含主导航 -->
<div class="nav">
<template v-for="(item, index) in menuStore.transformRoutes">
<template v-for="(item, index) in menuStore.allMenus">
<div
v-if="item.children && item.children.length !== 0" :key="index" :class="{
'item': true,

View File

@ -37,7 +37,7 @@
</div>
<div class="breadcrumb">
<span v-for="(bc, bcIndex) in item.breadcrumb" :key="bcIndex">
{{ bc }}
{{ bc.title }}
<svg-icon name="el-icon-arrow-right" />
</span>
</div>
@ -52,20 +52,50 @@
</template>
<script setup>
import { deepClone, isExternalLink } from '@/util'
import { isExternalLink } from '@/util'
const { proxy } = getCurrentInstance()
import { useSettingsStore } from '@/store/modules/settings'
const settingsStore = useSettingsStore()
import { useMenuStore } from '@/store/modules/menu'
const menuStore = useMenuStore()
import { useRouteStore } from '@/store/modules/route'
const routeStore = useRouteStore()
const isShow = ref(false)
const searchInput = ref('')
const sourceList = ref([])
const actived = ref(-1)
const sourceList = computed(() => {
let list = []
routeStore.flatRoutes.map(item => {
if (item.children) {
item.children.map(child => {
list.push({
icon: child.meta.icon || item.meta.icon,
title: child.meta.title,
i18n: child.meta.i18n,
breadcrumb: child.meta.breadcrumbNeste,
path: child.path,
isExternalLink: isExternalLink(child.path)
})
})
} else {
list.push({
icon: item.meta.icon,
title: item.meta.title,
i18n: item.meta.i18n,
breadcrumb: [{
title: item.meta.title,
path: item.path
}],
path: item.path,
isExternalLink: isExternalLink(item.path)
})
}
})
return list
})
const resultList = computed(() => {
let result = []
result = sourceList.value.filter(item => {
@ -76,7 +106,7 @@ const resultList = computed(() => {
if (item.path.indexOf(searchInput.value) >= 0) {
flag = true
}
if (item.breadcrumb.some(b => b.indexOf(searchInput.value) >= 0)) {
if (item.breadcrumb.some(b => b.title.indexOf(searchInput.value) >= 0)) {
flag = true
}
return flag
@ -121,59 +151,8 @@ onMounted(() => {
isShow.value = true
}
})
menuStore.routes.map(item => {
getSourceList(item.children)
})
})
function hasChildren(item) {
let flag = true
if (item.children) {
if (item.children.every(i => i.meta.sidebar === false)) {
flag = false
}
} else {
flag = false
}
return flag
}
function getSourceList(arr) {
arr.map(item => {
if (item.meta.sidebar !== false) {
if (hasChildren(item)) {
let baseBreadcrumb = item.meta.baseBreadcrumb ? deepClone(item.meta.baseBreadcrumb) : []
baseBreadcrumb.push(item.meta.title)
let child = deepClone(item.children)
child.map(c => {
c.meta.baseIcon = item.meta.icon || item.meta.baseIcon
c.meta.baseBreadcrumb = baseBreadcrumb
c.meta.basePath = item.meta.basePath ? [item.meta.basePath, item.path].join('/') : item.path
})
getSourceList(child)
} else {
let breadcrumb = []
if (item.meta.baseBreadcrumb) {
breadcrumb = deepClone(item.meta.baseBreadcrumb)
}
breadcrumb.push(item.meta.title)
let path = ''
if (isExternalLink(item.path)) {
path = item.path
} else {
path = item.meta.basePath ? [item.meta.basePath, item.path].join('/') : item.path
}
sourceList.value.push({
icon: item.meta.icon || item.meta.baseIcon,
title: item.meta.title,
i18n: item.meta.i18n,
breadcrumb: breadcrumb,
path: path,
isExternalLink: isExternalLink(item.path)
})
}
}
})
}
function keyUp() {
if (resultList.value.length) {
actived.value -= 1

View File

@ -14,7 +14,7 @@
}"
>
<transition-group name="sub-sidebar">
<template v-for="route in menuStore.sidebarRoutes">
<template v-for="route in menuStore.sidebarMenus">
<SidebarItem v-if="route.meta.sidebar !== false" :key="route.path" :item="route" :base-path="route.path" />
</template>
</transition-group>

View File

@ -92,10 +92,10 @@ provide('switchMenu', switchMenu)
function switchMenu(index) {
menuStore.switchHeaderActived(index)
if (settingsStore.menu.switchMainMenuAndPageJump) {
if (isExternalLink(menuStore.sidebarRoutesFirstDeepestPath)) {
window.open(menuStore.sidebarRoutesFirstDeepestPath, '_blank')
if (isExternalLink(menuStore.sidebarMenusFirstDeepestPath)) {
window.open(menuStore.sidebarMenusFirstDeepestPath, '_blank')
} else {
router.push(menuStore.sidebarRoutesFirstDeepestPath)
router.push(menuStore.sidebarMenusFirstDeepestPath)
}
}
}

View File

@ -3,6 +3,7 @@ import { useSettingsOutsideStore } from '@/store/modules/settings'
import { useKeepAliveOutsideStore } from '@/store/modules/keepAlive'
import { useUserOutsideStore } from '@/store/modules/user'
import { useMenuOutsideStore } from '@/store/modules/menu'
import { useRouteOutsideStore } from '@/store/modules/route'
import '@/assets/styles/nprogress.scss'
import { useNProgress } from '@vueuse/integrations/useNProgress'
@ -144,11 +145,12 @@ router.beforeEach(async(to, from, next) => {
const settingsOutsideStore = useSettingsOutsideStore()
const userOutsideStore = useUserOutsideStore()
const menuOutsideStore = useMenuOutsideStore()
const routeOutsideStore = useRouteOutsideStore()
settingsOutsideStore.app.enableProgress && (isLoading.value = true)
// 是否已登录
if (userOutsideStore.isLogin) {
// 是否已根据权限动态生成并挂载路由
if (menuOutsideStore.isGenerate) {
if (routeOutsideStore.isGenerate) {
// 导航栏如果不是 single 模式,则需要根据 path 定位主导航的选中状态
settingsOutsideStore.menu.menuMode !== 'single' && menuOutsideStore.setHeaderActived(to.path)
if (to.name) {
@ -161,9 +163,9 @@ router.beforeEach(async(to, from, next) => {
})
} else if (!settingsOutsideStore.dashboard.enable && to.name == 'dashboard') {
// 如果未开启控制台页面,则默认进入侧边栏导航第一个模块
if (menuOutsideStore.sidebarRoutes.length > 0) {
if (menuOutsideStore.sidebarMenus.length > 0) {
next({
path: menuOutsideStore.sidebarRoutesFirstDeepestPath,
path: menuOutsideStore.sidebarMenusFirstDeepestPath,
replace: true
})
} else {
@ -182,21 +184,20 @@ router.beforeEach(async(to, from, next) => {
next()
}
} else {
let accessRoutes = []
if (!settingsOutsideStore.app.enableBackendReturnRoute) {
accessRoutes = await menuOutsideStore.generateRoutesAtFront(asyncRoutes)
await routeOutsideStore.generateRoutesAtFront(asyncRoutes)
} else {
accessRoutes = await menuOutsideStore.generateRoutesAtBack()
await routeOutsideStore.generateRoutesAtBack()
}
accessRoutes.push(lastRoute)
let removeRoutes = []
accessRoutes.forEach(route => {
routeOutsideStore.flatRoutes.forEach(route => {
if (!/^(https?:|mailto:|tel:)/.test(route.path)) {
removeRoutes.push(router.addRoute(route))
}
})
removeRoutes.push(router.addRoute(lastRoute))
// 记录的 accessRoutes 路由数据,在登出时会使用到,不使用 router.removeRoute 是考虑配置的路由可能不一定有设置 name ,则通过调用 router.addRoute() 返回的回调进行删除
menuOutsideStore.setCurrentRemoveRoutes(removeRoutes)
routeOutsideStore.setCurrentRemoveRoutes(removeRoutes)
next({ ...to, replace: true })
}
} else {

View File

@ -1,123 +1,10 @@
import { defineStore } from 'pinia'
import { piniaStore } from '@/store'
import { resolveRoutePath } from '@/util'
import path from 'path-browserify'
import { deepClone, resolveRoutePath } from '@/util'
import api from '@/api'
import { useSettingsStore } from './settings'
import { useUserStore } from './user'
function hasPermission(permissions, route) {
let isAuth = false
if (route.meta && route.meta.auth) {
isAuth = permissions.some(auth => {
if (typeof route.meta.auth == 'string') {
return route.meta.auth === auth
} else {
return route.meta.auth.some(routeAuth => {
return routeAuth === auth
})
}
})
} else {
isAuth = true
}
return isAuth
}
function filterAsyncRoutes(routes, permissions) {
const res = []
routes.forEach(route => {
let tmpRoute = deepClone(route)
if (hasPermission(permissions, tmpRoute)) {
if (tmpRoute.children) {
tmpRoute.children = filterAsyncRoutes(tmpRoute.children, permissions)
tmpRoute.children.length && res.push(tmpRoute)
} else {
res.push(tmpRoute)
}
}
})
return res
}
function formatBackRoutes(routes, views = import.meta.glob('../../views/**/*.vue')) {
return routes.map(route => {
switch (route.component) {
case 'Layout':
route.component = () => import('@/layout/index.vue')
break
default:
if (route.component) {
route.component = views[`../../views/${route.component}`]
}
}
if (route.children) {
route.children = formatBackRoutes(route.children, views)
}
return route
})
}
// 将多层嵌套路由处理成平级
function flatAsyncRoutes(routes, breadcrumb, baseUrl = '') {
let res = []
routes.forEach(route => {
if (route.children) {
let childrenBaseUrl = ''
if (baseUrl == '') {
childrenBaseUrl = route.path
} else if (route.path != '') {
childrenBaseUrl = `${baseUrl}/${route.path}`
}
let childrenBreadcrumb = deepClone(breadcrumb)
if (route.meta.breadcrumb !== false) {
childrenBreadcrumb.push({
path: childrenBaseUrl,
title: route.meta.title
})
}
let tmpRoute = deepClone(route)
tmpRoute.path = childrenBaseUrl
tmpRoute.meta.breadcrumbNeste = childrenBreadcrumb
delete tmpRoute.children
res.push(tmpRoute)
let childrenRoutes = flatAsyncRoutes(route.children, childrenBreadcrumb, childrenBaseUrl)
childrenRoutes.map(item => {
// 如果 path 一样则覆盖,因为子路由的 path 可能设置为空,导致和父路由一样,直接注册会提示路由重复
if (res.some(v => v.path == item.path)) {
res.forEach((v, i) => {
if (v.path == item.path) {
res[i] = item
}
})
} else {
res.push(item)
}
})
} else {
let tmpRoute = deepClone(route)
if (baseUrl != '') {
if (tmpRoute.path != '') {
tmpRoute.path = `${baseUrl}/${tmpRoute.path}`
} else {
tmpRoute.path = baseUrl
}
}
// 处理面包屑导航
let tmpBreadcrumb = deepClone(breadcrumb)
if (tmpRoute.meta.breadcrumb !== false) {
tmpBreadcrumb.push({
path: tmpRoute.path,
title: tmpRoute.meta.title
})
}
tmpRoute.meta.breadcrumbNeste = tmpBreadcrumb
res.push(tmpRoute)
}
})
return res
}
import { useRouteStore } from './route'
function getDeepestPath(routes, rootPath = '') {
let retnPath
@ -147,137 +34,49 @@ export const useMenuStore = defineStore(
'menu',
{
state: () => ({
isGenerate: false,
routes: [],
defaultOpenedPaths: [],
headerActived: 0,
currentRemoveRoutes: []
headerActived: 0
}),
getters: {
transformRoutes: state => {
let routes
// 完整导航数据
allMenus() {
const settingsStore = useSettingsStore()
const routeStore = useRouteStore()
let routes
if (settingsStore.menu.menuMode === 'single') {
routes = [{ children: [] }]
state.routes.map(item => {
routeStore.routes.map(item => {
routes[0].children.push(...item.children)
})
} else {
routes = state.routes
routes = routeStore.routes
}
return routes
},
sidebarRoutes() {
return this.transformRoutes.length > 0 ? this.transformRoutes[this.headerActived].children : []
// 次导航数据
sidebarMenus() {
return this.allMenus.length > 0 ? this.allMenus[this.headerActived].children : []
},
sidebarRoutesFirstDeepestPath() {
return this.transformRoutes.length > 0 ? getDeepestPath(this.sidebarRoutes[0]) : '/'
// 次导航里第一个导航的路径
sidebarMenusFirstDeepestPath() {
return this.allMenus.length > 0 ? getDeepestPath(this.sidebarMenus[0]) : '/'
},
defaultOpenedPaths() {
const routeStore = useRouteStore()
let defaultOpenedPaths = []
routeStore.routes.map(item => {
item.meta.defaultOpened && defaultOpenedPaths.push(item.path)
item.children && item.children.map(child => {
child.meta.defaultOpened && defaultOpenedPaths.push(path.resolve(item.path, child.path))
})
})
return defaultOpenedPaths
}
},
actions: {
// 根据权限动态生成路由(前端生成)
generateRoutesAtFront(asyncRoutes) {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async resolve => {
const settingsStore = useSettingsStore()
const userStore = useUserStore()
let accessedRoutes
// 如果权限功能开启,则需要对路由数据进行筛选过滤
if (settingsStore.app.enablePermission) {
const permissions = await userStore.getPermissions()
accessedRoutes = filterAsyncRoutes(asyncRoutes, permissions)
} else {
accessedRoutes = deepClone(asyncRoutes)
}
// 设置 routes 数据
this.isGenerate = true
let newRoutes = deepClone(accessedRoutes)
this.routes = newRoutes.filter(item => {
return item.children.length != 0
})
// 将三级及以上路由数据拍平成二级
let routes = []
accessedRoutes.map(item => {
routes.push(...item.children)
})
routes.map(item => {
if (item.children) {
item.children = flatAsyncRoutes(item.children, [{
path: item.path,
title: item.meta.title
}], item.path)
}
})
// 设置 defaultOpenedPaths 数据
let defaultOpenedPaths = []
routes.map(item => {
item.meta.defaultOpened && defaultOpenedPaths.push(item.path)
item.children && item.children.map(child => {
child.meta.defaultOpened && defaultOpenedPaths.push(path.resolve(item.path, child.path))
})
})
this.defaultOpenedPaths = defaultOpenedPaths
resolve(routes)
})
},
// 生成路由(后端获取)
generateRoutesAtBack() {
return new Promise(resolve => {
api.get('route/list', {
baseURL: '/mock/'
}).then(async res => {
const settingsStore = useSettingsStore()
const userStore = useUserStore()
let asyncRoutes = formatBackRoutes(res.data)
let accessedRoutes
// 如果权限功能开启,则需要对路由数据进行筛选过滤
if (settingsStore.app.enablePermission) {
const permissions = await userStore.getPermissions()
accessedRoutes = filterAsyncRoutes(asyncRoutes, permissions)
} else {
accessedRoutes = deepClone(asyncRoutes)
}
// 设置 routes 数据
this.isGenerate = true
let newRoutes = deepClone(accessedRoutes)
this.routes = newRoutes.filter(item => {
return item.children.length != 0
})
// 将三级及以上路由数据拍平成二级
let routes = []
accessedRoutes.map(item => {
routes.push(...item.children)
})
routes.map(item => {
if (item.children) {
item.children = flatAsyncRoutes(item.children, [{
path: item.path,
title: item.meta.title
}], item.path)
}
})
// 设置 defaultOpenedPaths 数据
let defaultOpenedPaths = []
routes.map(item => {
item.meta.defaultOpened && defaultOpenedPaths.push(item.path)
item.children && item.children.map(child => {
child.meta.defaultOpened && defaultOpenedPaths.push(path.resolve(item.path, child.path))
})
})
this.defaultOpenedPaths = defaultOpenedPaths
resolve(routes)
})
})
},
invalidRoutes() {
this.isGenerate = false
this.routes = []
this.defaultOpenedPaths = []
this.headerActived = 0
},
// 根据路由判断属于哪个头部导航
setHeaderActived(path) {
this.routes.map((item, index) => {
const routeStore = useRouteStore()
routeStore.routes.map((item, index) => {
if (
item.children.some(r => {
return path.indexOf(r.path + '/') === 0 || path == r.path
@ -290,17 +89,6 @@ export const useMenuStore = defineStore(
// 切换头部导航
switchHeaderActived(index) {
this.headerActived = index
},
// 记录 accessRoutes 路由,用于登出时删除路由
setCurrentRemoveRoutes(routes) {
this.currentRemoveRoutes = routes
},
// 清空动态路由
removeRoutes() {
this.currentRemoveRoutes.forEach(removeRoute => {
removeRoute()
})
this.currentRemoveRoutes = []
}
}
}

214
src/store/modules/route.js Normal file
View File

@ -0,0 +1,214 @@
import { defineStore } from 'pinia'
import { piniaStore } from '@/store'
import { deepClone, isExternalLink } from '@/util'
import api from '@/api'
import { useSettingsStore } from './settings'
import { useUserStore } from './user'
function hasPermission(permissions, route) {
let isAuth = false
if (route.meta && route.meta.auth) {
isAuth = permissions.some(auth => {
if (typeof route.meta.auth == 'string') {
return route.meta.auth === auth
} else {
return route.meta.auth.some(routeAuth => {
return routeAuth === auth
})
}
})
} else {
isAuth = true
}
return isAuth
}
function filterAsyncRoutes(routes, permissions) {
const res = []
routes.forEach(route => {
let tmpRoute = deepClone(route)
if (hasPermission(permissions, tmpRoute)) {
if (tmpRoute.children) {
tmpRoute.children = filterAsyncRoutes(tmpRoute.children, permissions)
tmpRoute.children.length && res.push(tmpRoute)
} else {
res.push(tmpRoute)
}
}
})
return res
}
function formatBackRoutes(routes, views = import.meta.glob('../../views/**/*.vue')) {
return routes.map(route => {
switch (route.component) {
case 'Layout':
route.component = () => import('@/layout/index.vue')
break
default:
if (route.component) {
route.component = views[`../../views/${route.component}`]
}
}
if (route.children) {
route.children = formatBackRoutes(route.children, views)
}
return route
})
}
// 将多层嵌套路由处理成平级
function flatAsyncRoutes(routes, breadcrumb, baseUrl = '') {
let res = []
routes.forEach(route => {
if (route.children) {
let childrenBaseUrl = ''
if (baseUrl == '') {
childrenBaseUrl = route.path
} else if (route.path != '') {
childrenBaseUrl = `${baseUrl}/${route.path}`
}
let childrenBreadcrumb = deepClone(breadcrumb)
if (route.meta.breadcrumb !== false) {
childrenBreadcrumb.push({
path: childrenBaseUrl,
title: route.meta.title
})
}
let tmpRoute = deepClone(route)
tmpRoute.path = childrenBaseUrl
tmpRoute.meta.breadcrumbNeste = childrenBreadcrumb
delete tmpRoute.children
res.push(tmpRoute)
let childrenRoutes = flatAsyncRoutes(route.children, childrenBreadcrumb, childrenBaseUrl)
childrenRoutes.map(item => {
// 如果 path 一样则覆盖,因为子路由的 path 可能设置为空,导致和父路由一样,直接注册会提示路由重复
if (res.some(v => v.path == item.path)) {
res.forEach((v, i) => {
if (v.path == item.path) {
res[i] = item
}
})
} else {
res.push(item)
}
})
} else {
let tmpRoute = deepClone(route)
if (baseUrl != '' && !isExternalLink(tmpRoute.path)) {
if (tmpRoute.path != '') {
tmpRoute.path = `${baseUrl}/${tmpRoute.path}`
} else {
tmpRoute.path = baseUrl
}
}
// 处理面包屑导航
let tmpBreadcrumb = deepClone(breadcrumb)
if (tmpRoute.meta.breadcrumb !== false) {
tmpBreadcrumb.push({
path: tmpRoute.path,
title: tmpRoute.meta.title
})
}
tmpRoute.meta.breadcrumbNeste = tmpBreadcrumb
res.push(tmpRoute)
}
})
return res
}
export const useRouteStore = defineStore(
// 唯一ID
'route',
{
state: () => ({
isGenerate: false,
routes: [],
currentRemoveRoutes: []
}),
getters: {
// 扁平化路由(将三级及以上路由数据拍平成二级)
flatRoutes: state => {
let routes = []
if (state.routes) {
state.routes.map(item => {
routes.push(...deepClone(item.children))
})
routes.map(item => {
if (item.children) {
item.children = flatAsyncRoutes(item.children, [{
path: item.path,
title: item.meta.title
}], item.path)
}
})
}
return routes
}
},
actions: {
// 根据权限动态生成路由(前端生成)
generateRoutesAtFront(asyncRoutes) {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async resolve => {
const settingsStore = useSettingsStore()
const userStore = useUserStore()
let accessedRoutes
// 如果权限功能开启,则需要对路由数据进行筛选过滤
if (settingsStore.app.enablePermission) {
const permissions = await userStore.getPermissions()
accessedRoutes = filterAsyncRoutes(asyncRoutes, permissions)
} else {
accessedRoutes = deepClone(asyncRoutes)
}
// 设置 routes 数据
this.isGenerate = true
this.routes = accessedRoutes.filter(item => item.children.length != 0)
resolve()
})
},
// 根据权限动态生成路由(后端获取)
generateRoutesAtBack() {
return new Promise(resolve => {
api.get('route/list', {
baseURL: '/mock/'
}).then(async res => {
const settingsStore = useSettingsStore()
const userStore = useUserStore()
let asyncRoutes = formatBackRoutes(res.data)
let accessedRoutes
// 如果权限功能开启,则需要对路由数据进行筛选过滤
if (settingsStore.app.enablePermission) {
const permissions = await userStore.getPermissions()
accessedRoutes = filterAsyncRoutes(asyncRoutes, permissions)
} else {
accessedRoutes = deepClone(asyncRoutes)
}
// 设置 routes 数据
this.isGenerate = true
this.routes = accessedRoutes.filter(item => item.children.length != 0)
resolve()
})
})
},
// 记录 accessRoutes 路由,用于登出时删除路由
setCurrentRemoveRoutes(routes) {
this.currentRemoveRoutes = routes
},
// 清空路由
removeRoutes() {
this.isGenerate = false
this.routes = []
this.currentRemoveRoutes.forEach(removeRoute => {
removeRoute()
})
this.currentRemoveRoutes = []
}
}
}
)
export function useRouteOutsideStore() {
return useRouteStore(piniaStore)
}

View File

@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
import { piniaStore } from '@/store'
import api from '@/api'
import { useRouteStore } from './route'
import { useMenuStore } from './menu'
export const useUserStore = defineStore(
@ -46,6 +47,7 @@ export const useUserStore = defineStore(
},
logout() {
return new Promise(resolve => {
const routeStore = useRouteStore()
const menuStore = useMenuStore()
localStorage.removeItem('account')
localStorage.removeItem('token')
@ -53,8 +55,8 @@ export const useUserStore = defineStore(
this.account = ''
this.token = ''
this.failure_time = ''
menuStore.invalidRoutes()
menuStore.removeRoutes()
routeStore.removeRoutes()
menuStore.switchHeaderActived(0)
resolve()
})
},