Bootstrap

Vite4 + Vue3 + vue-router4 动态路由

动态路由,基本上每一个项目都能接触到这个东西,通俗一点就是我们的菜单是根据后端接口返回的数据进行动态生成的。表面上是对菜单的一个展现处理,其实内部就是对router的一个数据处理。当然你只对菜单做处理也是可以的,但是没有任何意义,熟悉router的小伙伴都知道,如果你的一个路由存在,即使没有这个菜单,我只要改变浏览器的地址一样能访问到。所以你还是省不了修改router的步骤。通过接口获取数据的话就可以根据角色权限或者一些业务上的需求,根据不同属性实现路由的划分。达到不同的页面渲染效果。

本文只是讲解菜单的权限控制,不到按钮级别。其实按钮也是差不多的。可以设置一个属性表示菜单,一个属性表示按钮,每一个菜单的叶子节点上都包含根据权限返回的按钮数组。接着可以通过组件的形式去输出相应的按钮就可以。

一、搭建项目 😛😛😛

这里我已经提前搭建好了 Vite4+Pinia2+vue-router4+ElmentPlus搭建Vue3项目(组件、图标等按需引入)

二、根据上面链接搭建好项目,修改src/router/index.ts 😁 😉

asyncRoutes里面可以存放默认的一些路由,比如登录、404、403这些。由于我只是演示,所以就啥都不放了。清晰明朗一点。

import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'

export const asyncRoutes: RouteRecordRaw[] = []

const router = createRouter({
    history: createWebHashHistory(),
    routes: asyncRoutes,
    scrollBehavior: () => ({ left: 0, top: 0 })
})

export default router

三、创建 'src/layout/index.vue' 文件 😁 😉

这个文件就是整个项目的布局,一般我们常见的项目都分为上下结构,就如下图。导航和菜单部分基本上是用户登录以后就已经确定好了,点击菜单的时候去切换路由。我这里由于这部分不是重点,所以我就很潦草的画了一个很简单的页面。

<template>
  <div style="padding: 100px;">
    <div>
      <div v-for="(item, index) in menus[0].children" :key="index" style="margin-bottom: 20px;">
        <router-link :to="item.path">{{item.title}}</router-link>
      </div>
    </div>
    <div>
      <router-view #default="{route, Component}">
        <transition leave-from-class="ts-web-fade--leave-to" enter-active-class="animate__animated animate__bounceInRight">
          <component :is="Component"></component>
        </transition>
      </router-view>
    </div>
  </div>
</template>


<script lang="ts">
import appStore from '@/pinia';

export default defineComponent({
  setup() {

    const { menus } = appStore.permissionModule

    console.log(menus, 'menus')

    return {
      menus,
    }
  }
})
</script>

四、创建 'src/pinia/modules/permission.ts' 文件 😁 😉

由于我这里是一个demo,没有真正的去接入后端。所以我暂时放入的静态数据。自己替换成接口返回就好。

import { defineStore } from 'pinia';
import router from '@/router'
// 这是整个项目的布局页面。根据自己的项目替换就好
import Layout from "@/layout/index.vue";
import {RouteRecordRaw} from "vue-router";

export type MenuType = {
    path: string,
    title: string,
    component: string,
    redirect?: string,
    children?: Array<MenuType>
}

type RouterType = RouteRecordRaw & {
    hidden?: boolean;
    alwaysShow?: boolean;
}

export interface IPermissionState {
    routes: RouterType[]
    dynamicRoutes: RouterType[]
    menus: Array<MenuType>
}

function hasPermission<T>(roles: T[], route: RouterType) {
    if (route.meta && route.meta.roles) {
        return roles.some((role) => (route.meta?.roles as T[]).includes(role));
    }
    return true
}

const modules = import.meta.glob('../../views/**/*.vue')
const _import = (path: string) => () => import(`../../views/${path}.vue`)

const assembleRouter = (routers: any) => {
    const addRouter = routers.filter((router: any) => {
        (router.title && router.icon) && (router.meta = {
            title: router.title,
            // icon: router.icon,
            // alwaysShow: router.alwaysShow || false,
            // affix: router.affix || false,
        })
        if (router.component === 'Layout') {
            router.component = shallowRef(Layout)
        } else {
            if (import.meta.env.MODE === 'development') {
                router.component = _import(router.component)
            } else {
                router.component = modules[`../../views/${router.component}.vue`]
            }
        }
        if (router.children && router.children.length) {
            router.children = assembleRouter(router.children)
        }
        return true

    })
    return addRouter
}

export function filterAsyncRoutes(routes: RouterType[], roles: string[]) {
    const res: RouterType[] = []

    routes.forEach((route) => {
        const tmp = { ...route }
        if (hasPermission<string>(roles, tmp)) {
            if (tmp.children) {
                tmp.children = filterAsyncRoutes(tmp.children, roles)
            }
            res.push(tmp)
        }
    })
    return res
}

export const permissionModule = defineStore({
    id: 'permission',
    state(): IPermissionState{
        return {
            routes: [],
            dynamicRoutes: [],
            menus: []
        }
    },
    actions:{
        async getMenus() {
            try {
                // 这里由于不方便演示,所以我写的静态数据。换着自己对于的接口就好
                const list:MenuType[] = [
                    {
                        path: '/',
                        title: 'ts-super-web',
                        component: 'Layout',
                        redirect: '/home',
                        children: [
                            {
                                title: 'home',
                                path: 'home',
                                component: 'home'
                            },
                            {
                                title: 'home1',
                                path: 'home1',
                                component: 'home1'
                            }
                        ]
                    }
                ]
                this.menus = list
                // 组件路由
                let addRouter = assembleRouter(this.menus)
                // addRouter = assembleRouterDelete(addRouter)
                // 动态添加菜单
                addRouter.forEach((ts: any) => {
                    router.addRoute(ts)
                })
            } catch (err) {
                return Promise.reject(err);
            }
        }
    }
})

五、路由拦截器 😁 😉 😜

因为上面说到我没有真正的接入后端,所以这里我也没有进行token判断。自行增加一下就好,比较简单。除了token还可以在拦截器里面放置一个白名单列表,对于白名单里面的路由我们不做拦截。比如login登录页面、404等等。根据自己需求配置就好。

import router from '@/router'
// @ts-ignore
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import appStore from "@/pinia";

NProgress.configure({
    easing: 'ease', // 动画方式
    showSpinner: true, // 是否显示加载ico
    trickleSpeed: 200, // 自动递增间隔
    minimum: 0.4, // 更改启动时使用的最小百分比
})

router.beforeEach(async (to, form, next) => {
    // 这里处理自己的逻辑,比如需要登录以后才能访问其他页面等等
    NProgress.start()
    const { menus, getMenus } = appStore.permissionModule
    if (menus.length === 0) {
        try {
            // 调用接口获取菜单 进行跳转
            await getMenus()
            next({ ...to, replace: true })
        } catch (err) {
            next()
        }
    } else {
        next()
    }
    NProgress.done()
})

六、修改app.vue

<template>
  <router-view />
</template>

<style>
html,body,#app {
  height: 100%;
  width: 100%;
  margin: 0;
  padding: 0;
}
</style>

七、src/views下的两个home文件进行一下修改

home 这两个文件不修改也不影响。

<template>
  <p style="font-size: 32px;">你好,我是home</p>
</template>

home1 这两个文件不修改也不影响。

<template>
  <p style="font-size: 32px;">你好,我是home1</p>
</template>

八、效果浏览

我是Etc.End。如果文章对你有所帮助,能否帮我点个免费的赞和收藏😍。

👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇

;