Vue3 & TypeScript組件嵌套:動態(tài)菜單兒(使用el-menu、pinia)

效果圖:
展開
點:
  1. 首先我們的項目中需要安裝unplugin-vue-components/vite和unplugin-auto-import/vite,可以自動引用組件(我看另外一個開源項目中直接在頁面中引用、使用,在組件中就不需要引用了。。。?;仡^可以試一下子);或者將所有的組件的引用封裝在一起,可以查看 vue ---- 全局組件統(tǒng)一管理(這篇是用vue2寫的,用于vue3得稍作改動);

  2. 組件思路是:el-menu,里面是一會兒將要嵌套的小組件~小組件循環(huán)每個路由單項;

  3. 組件單項思路(將menu-item抽出來):看el-menu中從有沒有子項就分為el-sub-menu和el-menu-item兩種:el-sub-menu是右側(cè)有小箭頭,代表該項存在子菜單,可展開;el-menu-item沒有小箭頭,代表沒有子菜單;

  4. 在menu-item中進行判斷,以有無子項為準,選擇采用以上哪種子項;

  5. 渲染el-menu時需要設(shè)置defaultActive默認項,一般為首頁唯一值dashboard;但如果因為頁面刷新,需要實時監(jiān)聽當(dāng)前路由,再將唯一值賦值給defaultActive;

  6. 圖中菜單右側(cè)屬于el-menu的部分明顯有一道透明的邊邊。因為el-menu有一個1px透明的邊邊。。。。我們在組件中設(shè)置以下代碼即可;

  7. 動態(tài)路由的過濾方法全部放在store的permission模塊中;在每個頁面渲染之前需要先用守衛(wèi)攔一下子,一般在src下的permission.ts中完成:

    • 需要給出一個完整的側(cè)邊路由和固定路由,方便獲取權(quán)限后進行過濾(pinia);
    • 路由守衛(wèi)beforeEach中需要做出的操作內(nèi)容:
      1. 先判斷是否為登錄態(tài),不是直接去LoginPage,是就接下來判斷是否有角色分配;
      2. 有角色分配直接next放行,無角色分配就先獲取角色;
      3. 根據(jù)角色(代碼中的modulePermission)對完整路由進行過濾(分別對路由進行添加、給側(cè)邊菜單賦值);

思路就是這,下面是代碼

sidebar代碼:
<script lang="ts" setup>
import Router from "@/router";
import store from '@/store/index'

const {app, permission} = store();

const list = computed(() => {
    return permission.setSideBarRoutes(permission.state.filterRoutes);
})
const defaultActive = computed(() => {
    return app.state.activeRoute;
})
// 頁面方法
const toHome = () => {
    Router.push({
        path: '/'
    });
}

</script>

<template>
    <div class="sidebar-view">
        <div class="logo-view" @click="toHome">
            <img src="https://img1.baidu.com/it/u=3940612183,1877024013&fm=253&app=138&size=w931&n=0&f=JPEG&fmt=auto?sec=1682010000&t=426150bc8f1786722713152075ee49f3"
                alt="" class="logo">
            <div v-if="!app.state.isCollapse">嘻嘻嘻嘻嘻嘻嘻</div>
        </div>
        <div class="menu-view">
            <Menu :list="list" :defaultActive="defaultActive" :collapse="app.state.isCollapse" />
        </div>
    </div>
</template>

<style lang="scss" scoped>
.sidebar-view {
    height: 100%;
    width: 100%;
    background: $primary;
    color: #fff;
}

.logo-view {
    height: 50px;
    display: flex;
    justify-content: space-evenly;
    align-items: center;

    cursor: pointer;

    .logo {
        width: 40px;
        height: 40px;
        border-radius: 50%;
        object-fit: cover;
    }
}

.menu-view {
    width: 100%;
    height: calc(100% - 50px);
    overflow-y: auto;
    overflow-x: hidden;
}
</style>
menu組件代碼:
<template>
    <el-menu class="el-menu-vertical-demo" :default-active="props.defaultActive" router :collapse="collapse"
        :background-color="commonStyle.primary" text-color="#fff" active-text-color="#304156">
        <menu-item v-for="item in list" :key="item.name" :menu="item" />
    </el-menu>
</template>

<script setup lang="ts">
import type {RouteType} from '@/store/types'
import commonStyle from '@/assets/css/variables.module.scss'


interface PropsType {
    list: RouteType[]
    defaultActive?: string
    collapse?: boolean
}
const props = withDefaults(defineProps<PropsType>(), {
    defaultActive: '/dashboard'
})

</script>

<style lang="scss" scoped>
.el-menu {
    border: none!important;
}
</style>
menuItem組件代碼:
<script setup lang="ts">
const props = defineProps(['menu'])

const menu = computed(() => {
    return props.menu;
})
</script>

<template>
    <el-sub-menu v-if="menu.children" :index="menu.fullPath">
        <template #title>
            <el-icon>
                <component v-if="menu.icon" :is="menu.icon"></component>
            </el-icon>
            <span>{{ menu.meta.title }}</span>
        </template>
        <menu-item v-for="item in menu.children" :key="item.name" :menu="item" />
    </el-sub-menu>
    <el-menu-item v-else :index="menu.fullPath">
        <el-icon>
            <component v-if="menu.icon" :is="menu.icon" class="menu-icon"></component>
        </el-icon>
        <template #title>{{ menu.meta.title }}</template>
    </el-menu-item>
</template>

<style lang="scss">
.el-menu-item.is-active {
    font-weight: 700;
}
</style>
src下的permission.ts代碼:

import Router,{addRouter} from '@/router'
import store from '@/store/index'

Router.beforeEach(async (to, from, next) => {
    const { permission, app } = store();
    const whiteList = ['/login', '/dashboard'];
    const toPath = to.path;
    const isLogin = localStorage.getItem("NJX_TOKEN");

    if (isLogin) {
        // 權(quán)限判斷
        const hasPermission = permission.state.modulePermission.length > 0;
        if (hasPermission) {
            next();
        } else {
            permission.clearPermission();
            try {
                // 獲取角色
                await permission.getPermission();
                app.setActiveRoute(to);
                addRouter(permission.state.filterRoutes);

                next({
                    ...to,
                    replace: true
                })
            } catch { 
                Router.push('/login')
            }
        }
    }
    else {
        const isWhite = whiteList.findIndex(item => toPath === item) !== -1;
        if (!isWhite) {
            Router.push('/login')
        } else {
            next()
        }
    }
})

Router.afterEach((to) => {
    document.title = to.meta.title as string;
})

tada~~一個可以配合動態(tài)權(quán)限的菜單就完成啦~

明天加一個login頁的redirect哈

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容