1.获取图标文件里的所有图标
<template> <div class="icon-select"> <el-input v-model="iconName" clearable placeholder="请输入图标名称" @clear="filterIcons" @input="filterIcons"> </el-input> <div class="icon-select__list"> <div v-for="(item, index) in iconList" :key="index" @click="selectedIcon(item)"> <svg-icon color="#999" :icon-class="item" style="height: 30px; width: 16px; margin-right: 5px" /> <span>{{ item }}</span> </div> </div> </div> </template>
<script setup lang="ts"> import { ref } from 'vue' import SvgIcon from '@/components/SvgIcon/index.vue' const icons = [] as string[] //获取图标文件 const modules = import.meta.glob('../../assets/icons/*.svg') for (const path in modules) { const p = path.split('assets/icons/')[1].split('.svg')[0] //icons为图标文件名 数组 icons.push(p) } const iconList = ref(icons) const iconName = ref('') const emit = defineEmits(['selected']) function filterIcons() { iconList.value = icons if (iconName.value) { iconList.value = icons.filter((item) => item.indexOf(iconName.value) !== -1) } } function selectedIcon(name: string) { emit('selected', name) document.body.click() } function reset() { iconName.value = '' iconList.value = icons } defineExpose({ reset, }) </script> <style lang="scss" scoped> .icon-select { width: 100%; padding: 10px; &__list { height: 200px; overflow-y: scroll; div { height: 30px; line-height: 30px; margin-bottom: -5px; cursor: pointer; width: 33%; float: left; } span { display: inline-block; vertical-align: -0.15em; fill: currentColor; overflow: hidden; } } } </style>
modules
path.split('assets/icons/')
p
2.动态路由
store/modules/permission.ts
import { PermissionState } from '@/types/store/permission' import { RouteRecordRaw } from 'vue-router' import { defineStore } from 'pinia' import { constantRoutes } from '@/router' import { listRoutes } from '@/api/system/menu' //获取view下所有的vue文件 const modules = import.meta.glob('../../views/**/*.vue') export const Layout = () => import('@/layout/index.vue') // 递归拼接组成路由的component export const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]) => { const res: RouteRecordRaw[] = [] routes.forEach( (route) => { const tmp = { ...route } as any //if (hasPermission(roles, tmp)) { if (tmp.component == 'Layout') { tmp.component = Layout } else { const component = modules[`../../views/${tmp.component}.vue`] as any if (component) { tmp.component = modules[`../../views/${tmp.component}.vue`] } else { tmp.component = modules[`../../views/error-page/404.vue`] } } tmp.name = tmp.path res.push(tmp) if (tmp.children) { tmp.children = filterAsyncRoutes(tmp.children, roles) } } //} ) return res } const usePermissionStore = defineStore({ id: 'permission', state: (): PermissionState => ({ routes: [], addRoutes: [], }), actions: { setRoutes(routes: RouteRecordRaw[]) { this.addRoutes = routes // this.routes供左边菜单栏使用,constantRoutes为路由文件原有的登陆、首页等 this.routes = constantRoutes.concat(routes) }, generateRoutes(roles: string[]) { return new Promise((resolve, reject) => { listRoutes() .then((response) => { // asyncRoutes:获取后端返回的路由 const asyncRoutes = response.data // accessedRoutes:拼接成功后想要的路由 const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles) this.setRoutes(accessedRoutes) resolve(accessedRoutes) }) .catch((error) => { reject(error) }) }) }, }, }) export default usePermissionStore
modules
asyncRoutes:获取后端返回的路由
accessedRoutes:拼接成功后想要的路由
router/index.ts
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'; import useStore from '@/store'; export const Layout = () => import('@/layout/index.vue'); // 参数说明: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html // 静态路由 export const constantRoutes: Array<RouteRecordRaw> = [ { path: '/redirect', component: Layout, meta: { hidden: true }, children: [ { path: '/redirect/:path(.*)', component: () => import('@/views/redirect/index.vue') } ] }, { path: '/login', component: () => import('@/views/login/index.vue'), meta: { hidden: true } }, { path: '/404', component: () => import('@/views/error-page/404.vue'), meta: { hidden: true } }, { path: '/', component: Layout, redirect: '/dashboard', children: [ { path: 'dashboard', component: () => import('@/views/dashboard/index.vue'), name: 'Dashboard', meta: { title: '首页', icon: 'homepage', affix: true } }, { path: '401', component: () => import('@/views/error-page/401.vue'), meta: { hidden: true } }, ] } // 外部链接 /*{ path: '/external-link', component: Layout, children: [ { path: 'https://www.cnblogs.com/haoxianrui/', meta: { title: '外部链接', icon: 'link' } } ] }*/ // 多级嵌套路由 /* { path: '/nested', component: Layout, redirect: '/nested/level1/level2', name: 'Nested', meta: {title: '多级菜单', icon: 'nested'}, children: [ { path: 'level1', component: () => import('@/views/nested/level1/index.vue'), name: 'Level1', meta: {title: '菜单一级'}, redirect: '/nested/level1/level2', children: [ { path: 'level2', component: () => import('@/views/nested/level1/level2/index.vue'), name: 'Level2', meta: {title: '菜单二级'}, redirect: '/nested/level1/level2/level3', children: [ { path: 'level3-1', component: () => import('@/views/nested/level1/level2/level3/index1.vue'), name: 'Level3-1', meta: {title: '菜单三级-1'} }, { path: 'level3-2', component: () => import('@/views/nested/level1/level2/level3/index2.vue'), name: 'Level3-2', meta: {title: '菜单三级-2'} } ] } ] }, ] }*/ ]; // 创建路由 const router = createRouter({ history: createWebHashHistory(), routes: constantRoutes as RouteRecordRaw[], // 刷新时,滚动条位置还原 scrollBehavior: () => ({ left: 0, top: 0 }) }); // 重置路由 export function resetRouter() { const { permission } = useStore(); permission.routes.forEach(route => { const name = route.name; if (name && router.hasRoute(name)) { router.removeRoute(name); } }); } export default router;
与App.vue同级 permission.ts
import router from '@/router' import useStore from '@/store' import NProgress from 'nprogress' import 'nprogress/nprogress.css' NProgress.configure({ showSpinner: false }) // 进度环显示/隐藏 // 白名单路由 const whiteList = ['/login'] router.beforeEach(async (to, from, next) => { if (to.meta.title) { //判断是否有标题 document.title = `智-admin-${to.meta.title} ` } else { document.title = `智-admin` } NProgress.start() const { user, permission } = useStore() const hasToken = user.token if (hasToken) { // 登录成功,跳转到首页 if (to.path == '/login') { next({ path: '/' }) NProgress.done() } else { const hasGetUserInfo = user.roles.length > 0 // 第一步.hasGetUserInfo一开始为false if (hasGetUserInfo) { if (to.matched.length == 0) { from.name ? next({ name: from.name as any }) : next('/401') } else { // 第四步 next() } } else { try { // 第二步 await user.getUserInfo() const roles = user.roles const accessRoutes: any = await permission.generateRoutes(roles) accessRoutes.forEach((route: any) => { router.addRoute(route) }) // 第三步 // 如果 addRoutes 并未完成,路由守卫会一层一层的执行执行,直到 addRoutes 完成,找到对应的路由 next({ ...to, replace: true }) } catch (error) { // 移除 token 并跳转登录页 await user.resetToken() next(`/login?redirect=${to.path}`) NProgress.done() } } } } else { // 未登录可以访问白名单页面(登录页面) if (whiteList.indexOf(to.path) !== -1) { next() } else { next(`/login?redirect=${to.path}`) NProgress.done() } } }) router.afterEach(() => { NProgress.done() })
最后在main.ts导入permission.ts
next({ ...to, replace: true }) 意思:VUE 路由守卫 next() / next({ ...to, replace: true }) / next(‘/‘) 说明_路由守卫next_anne都的博客-CSDN博客
3.动态递归组件侧边菜单栏
1.src/layout/components/sidebar/index.vue
<template> <div :class="{ 'has-logo': showLogo }"> <logo v-if="showLogo" :collapse="isCollapse" /> <el-scrollbar> <el-menu :default-active="activeMenu" :collapse="isCollapse" :background-color="variables.menuBg" :text-color="variables.menuText" :active-text-color="variables.menuActiveText" :unique-opened="true" :collapse-transition="false" mode="vertical" @open="handleOpen"> <sidebar-item v-for="route in routes" :item="route" :key="route.path" :base-path="route.path" :is-collapse="isCollapse" /> </el-menu> </el-scrollbar> </div> </template> <script setup lang="ts"> import { computed } from 'vue' import { useRoute, useRouter } from 'vue-router' import SidebarItem from './SidebarItem.vue' import Logo from './Logo.vue' import variables from '@/styles/variables.module.scss' import useStore from '@/store' const { permission, setting, app } = useStore() const route = useRoute() const router = useRouter() const routes = computed(() => permission.routes) const showLogo = computed(() => setting.sidebarLogo) const isCollapse = computed(() => !app.sidebar.opened) const activeMenu = computed(() => { const { meta, path } = route if (meta.activeMenu) { return meta.activeMenu as string } return path }) // 默认选中第一个 const handleOpen = (key: string, keyPath: string[]) => { router.push(key) } </script>
routes
sidebarItem组件
<template> <div v-if="!item.meta || !item.meta.hidden"> <!-- 没有子路由,子菜单,没有展开项 --> <template v-if=" hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && (!item.meta || !item.meta.alwaysShow) "> <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)"> <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }"> <svg-icon v-if="onlyOneChild.meta && onlyOneChild.meta.icon" :icon-class="onlyOneChild.meta.icon" /> <template #title> {{ generateTitle(onlyOneChild.meta.title) }} </template> </el-menu-item> </app-link> </template> <!-- 有子路由,有展开项 --> <el-sub-menu v-else :index="resolvePath(item.path)" teleported> <template #title> <svg-icon v-if="item.meta && item.meta.icon" :icon-class="item.meta.icon"></svg-icon> <span v-if="item.meta && item.meta.title">{{ generateTitle(item.meta.title) }}</span> </template> <!-- 递归组件 --> <sidebar-item v-for="child in item.children" :key="child.path" :item="child" :is-nest="true" :base-path="resolvePath(child.path)" class="nest-menu" /> </el-sub-menu> </div> </template> <script setup lang="ts"> import { ref } from 'vue' import path from 'path-browserify' import { isExternal } from '@/utils/validate' import AppLink from './Link.vue' import { generateTitle } from '@/utils/i18n' import SvgIcon from '@/components/SvgIcon/index.vue' const props = defineProps({ item: { type: Object, required: true, }, isNest: { type: Boolean, required: false, }, basePath: { type: String, required: true, }, }) const onlyOneChild = ref() function hasOneShowingChild(children = [] as any, parent: any) { if (!children) { children = [] } const showingChildren = children.filter((item: any) => { if (item.meta && item.meta.hidden) { return false } else { // 过滤出子元素 onlyOneChild.value = item return true } }) // 当只有一个子路由,该子路由显示子菜单,没有用展开项 if (showingChildren.length == 1) { return true } // 没有子路由则显示父路由 if (showingChildren.length == 0) { onlyOneChild.value = { ...parent, path: '', noShowingChildren: true } return true } return false } // 解析路径 function resolvePath(routePath: string) { // isExternal 判断是否为网址 if (isExternal(routePath)) { return routePath } if (isExternal(props.basePath)) { return props.basePath } // path.resolve('/partner', '/business') 为 /partner/business return path.resolve(props.basePath, routePath) } </script> <style lang="scss" scoped></style>
appLink组件
<template> <a v-if="isExternal(to)" :href="to" target="_blank" rel="noopener"> <slot /> </a> <div v-else @click="push"> <slot /> </div> </template> <script lang="ts"> import { computed, defineComponent } from 'vue'; import { isExternal } from '@/utils/validate'; import { useRouter } from 'vue-router'; import useStore from '@/store'; const { app } = useStore(); const sidebar = computed(() => app.sidebar); const device = computed(() => app.device); export default defineComponent({ props: { to: { type: String, required: true } }, setup(props) { const router = useRouter(); const push = () => { if (device.value == 'mobile' && sidebar.value.opened == true) { app.closeSideBar(false); } router.push(props.to).catch(err => { console.log(err); }); }; return { push, isExternal }; } }); </script>