vue3.0 layout 以及路由示例 缓存tabs以及menu

layout

<template>

    <el-container class="layout-container">

        <Sidebar />

        <el-container>

            <el-header class="layout-header" :style="layoutHeaderHeight">

                <Navbar />

                <Tabs v-if="theme.isTabsView" />

            </el-header>

            <el-scrollbar>

                <el-main class="layout-main">

                    <el-scrollbar class="layout-scrollbar">

                        <div class="layout-card" :style="layoutMainHeight">

                            <router-view v-slot="{ Component, route }">

                                <keep-alive v-if="theme.isTabsCache" :include="[...store.tabsStore.cachedViews]">

                                    <component :is="Component" :key="route.name" />

                                </keep-alive>

                                <component :is="Component" v-else :key="route.name" />

                            </router-view>

                        </div>

                    </el-scrollbar>

                </el-main>

            </el-scrollbar>

        </el-container>

    </el-container>

</template>

<script setup lang="ts">

import { RouterView } from 'vue-router'

import store from '@/store'

import Sidebar from '@/layout/components/Sidebar/index.vue'

import Navbar from '@/layout/components/Navbar/index.vue'

import Tabs from '@/layout/components/Tabs/index.vue'

import { computed } from 'vue'

const theme = computed(() => store.appStore.theme)

const layoutHeaderHeight = computed(() => {

    if (!theme.value.isTabsView) {

        return 'height:var(--theme-header-height) !important'

    } else {

        return ''

    }

})

const layoutMainHeight = computed(() => {

    if (!theme.value.isTabsView) {

        return 'min-height: calc(100vh - var(--theme-header-height) - 30px) !important'

    } else {

        return ''

    }

})

</script>

router

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

import NProgress from 'nprogress'

import 'nprogress/nprogress.css'

import store from '@/store'

import { i18n } from '@/i18n'

import { pathToCamel } from '@/utils/tool'

NProgress.configure({ showSpinner: false })

const constantRoutes: RouteRecordRaw[] = [

    {

        path: '/redirect',

        component: () => import('../layout/index.vue'),

        children: [

            {

                path: '/redirect/:path(.*)',

                component: () => import('../layout/components/Router/Redirect.vue')

            }

        ]

    },

    {

        path: '/login',

        component: () => import('../views/login/login.vue')

    },

    {

        path: '/404',

        component: () => import('../views/404.vue')

    }

]

const asyncRoutes: RouteRecordRaw = {

    path: '/',

    component: () => import('../layout/index.vue'),

    redirect: '/home',

    children: [

        {

            path: '/home',

            name: 'Home',

            component: () => import('../views/home.vue'),

            meta: {

                title: i18n.global.t('router.home'),

                affix: true

            }

        },

        {

            path: '/profile/password',

            name: 'ProfilePassword',

            component: () => import('../views/profile/password.vue'),

            meta: {

                title: i18n.global.t('router.profilePassword'),

                cache: true

            }

        }

    ]

}

export const errorRoute: RouteRecordRaw = {

    path: '/:pathMatch(.*)',

    redirect: '/404'

}

export const router = createRouter({

    history: createWebHashHistory(),

    routes: constantRoutes

})

// 白名单列表

const whiteList = ['/login']

// 路由加载前

router.beforeEach(async (to, from, next) => {

    NProgress.start()

    // token存在的情况

    if (store.userStore.token) {

        if (to.path === '/login') {

            next('/home')

        } else {

            // 用户信息不存在,则重新拉取用户等信息

            if (!store.userStore.user.id) {

                await store.userStore.getUserInfoAction()

                const menuRoutes = await store.routerStore.getMenuRoutes()

                console.log('menu',menuRoutes)

                // 根据后端菜单路由,生成KeepAlive路由

                const keepAliveRoutes = getKeepAliveRoutes(menuRoutes, [])

                console.log('menu11',keepAliveRoutes)

                // 添加菜单路由

                asyncRoutes.children?.push(...keepAliveRoutes)

                router.addRoute(asyncRoutes)

                // 错误路由

                router.addRoute(errorRoute)

                // 保存路由数据

                store.routerStore.setRoutes(constantRoutes.concat(asyncRoutes))

                // 搜索菜单需要使用

                store.routerStore.setSearchMenu(keepAliveRoutes)

                next({ ...to, replace: true })

            } else {

                next()

            }

        }

    } else {

        // 没有token的情况下,可以进入白名单

        if (whiteList.indexOf(to.path) > -1) {

            next()

        } else {

            next('/login')

        }

    }

})

// 路由加载后

router.afterEach(() => {

    NProgress.done()

})

// 获取扁平化路由,将多级路由转换成一级路由

export const getKeepAliveRoutes = (rs: RouteRecordRaw[], breadcrumb: string[]): RouteRecordRaw[] => {

    const routerList: RouteRecordRaw[] = []

    rs.forEach((item: any) => {

        if (item.meta.title) {

            breadcrumb.push(item.meta.title)

        }

        if (item.children && item.children.length > 0) {

            routerList.push(...getKeepAliveRoutes(item.children, breadcrumb))

        } else {

            item.meta.breadcrumb.push(...breadcrumb)

            routerList.push(item)

        }

        breadcrumb.pop()

    })

    return routerList

}

// 加载vue组件

const layoutModules = import.meta.glob('/src/views/**/*.vue')

// 根据路径,动态获取vue组件

const getDynamicComponent = (path: string): any => {

    const component = layoutModules[`/src/views/${path}.vue`]

    if (!component) {

        console.error('component error', path)

    }

    return component

}

// 根据菜单列表,生成路由数据

export const generateRoutes = (menuList: any): RouteRecordRaw[] => {

    const routerList: RouteRecordRaw[] = []

    menuList.forEach((menu: any) => {

        let component

        let path

        if (menu.children && menu.children.length > 0) {

            component = () => import('@/layout/index.vue')

            path = '/p/' + menu.id

        } else {

            component = getDynamicComponent(menu.url)

            path = '/' + menu.url

        }

        const route: RouteRecordRaw = {

            path: path,

            name: pathToCamel(path),

            component: component,

            children: [],

            meta: {

                title: menu.name,

                icon: menu.icon,

                id: '' + menu.id,

                cache: true,

                _blank: menu.openStyle === 1,

                breadcrumb: []

            }

        }

        // 有子菜单的情况

        if (menu.children && menu.children.length > 0) {

            route.children?.push(...generateRoutes(menu.children))

        }

        routerList.push(route)

    })

    return routerList

}

tabs

<template>

    <div class="tabs-container">

        <div class="tabs-item">

            <el-tabs v-model="activeTabName" :class="tabsStyleClass" @tab-click="tabClick" @tab-remove="tabRemove">

                <el-tab-pane

                    v-for="tab in store.tabsStore.visitedViews"

                    :key="tab"

                    :label="tab.title"

                    :name="tab.path"

                    :closable="!isAffix(tab)"

                ></el-tab-pane>

            </el-tabs>

        </div>

        <el-dropdown class="tabs-action" trigger="click" placement="bottom-end" @command="onClose">

            <template #dropdown>

                <el-dropdown-menu>

                    <el-dropdown-item :icon="Close" command="close">{{ $t('app.close') }}</el-dropdown-item>

                    <el-dropdown-item :icon="CircleClose" command="closeOthers">{{ $t('app.closeOthers') }}</el-dropdown-item>

                    <el-dropdown-item :icon="CircleCloseFilled" command="closeAll">{{ $t('app.closeAll') }}</el-dropdown-item>

                </el-dropdown-menu>

            </template>

            <el-icon><arrow-down /></el-icon>

        </el-dropdown>

    </div>

</template>

<script setup lang="ts">

import { watch, onMounted, ref, computed } from 'vue'

import { useRoute, useRouter } from 'vue-router'

import store from '@/store'

import { closeAllTabs, closeOthersTabs, closeTab } from '@/utils/tabs'

import { ArrowDown, Close, CircleClose, CircleCloseFilled } from '@element-plus/icons-vue'

const route = useRoute()

const router = useRouter()

const activeTabName = ref(route.path)

const tabsStyleClass = computed(() => 'tabs-item-' + store.appStore.theme.tabsStyle)

// 是否固定

const isAffix = (tab: any) => {

    return tab.meta && tab.meta.affix

}

watch(route, () => {

    // 当前路由,添加到tabs里

    if (route.name) {

        addTab()

    }

})

onMounted(() => {

    // 初始化

    initTabs()

    addTab()

})

// 初始化固定tab

const initTabs = () => {

    const affixTabs = getAffixTabs(store.routerStore.routes)

    for (const tab of affixTabs) {

        // 需要有tab名称

        if (tab.name) {

            store.tabsStore.addView(tab)

        }

    }

}

// 获取需要固定的tabs

const getAffixTabs = (routes: any) => {

    let tabs: any[] = []

    routes.forEach((route: any) => {

        if (route.meta && route.meta.affix) {

            tabs.push({

                fullPath: route.path,

                path: route.path,

                name: route.name,

                meta: { ...route.meta }

            })

        }

        if (route.children) {

            const tempTabs = getAffixTabs(route.children)

            if (tempTabs.length >= 1) {

                tabs = [...tabs, ...tempTabs]

            }

        }

    })

    return tabs

}

// 添加tab

const addTab = () => {

    store.tabsStore.addView(route)

    store.tabsStore.addCachedView(route)

    activeTabName.value = route.path

}

// tab被选中

const tabClick = (tab: any) => {

    tab.props.name && router.push(tab.props.name)

}

// 点击关闭tab

const tabRemove = (path: string) => {

    const tab = store.tabsStore.visitedViews.filter((tab: any) => tab.path === path)

    closeTab(router, tab[0])

}

// dropdown 关闭事件

const onClose = (type: string) => {

    switch (type) {

        case 'close':

            closeTab(router, route)

            break

        case 'closeOthers':

            closeOthersTabs(router, route)

            break

        case 'closeAll':

            closeAllTabs(router, route)

            break

    }

}

</script>

<style lang="scss" scoped>

.tabs-container {

    display: flex;

    position: relative;

    z-index: 6;

    height: 40px;

    .tabs-item {

        transition: left 0.3s;

        flex-grow: 1;

        overflow: hidden;

        ::v-deep(.el-tabs__nav-prev) {

            padding: 0 10px;

            border-right: var(--el-border-color-extra-light) 1px solid;

        }

        ::v-deep(.el-tabs__nav-next) {

            padding: 0 10px;

            border-left: var(--el-border-color-extra-light) 1px solid;

        }

        ::v-deep(.is-scrollable) {

            padding: 0 32px;

        }

        ::v-deep(.el-tabs__active-bar) {

            height: 0;

        }

        ::v-deep(.el-tabs__item) {

            .is-icon-close {

                transition: none !important;

                &:hover {

                    color: var(--el-color-primary-light-9);

                    background-color: var(--el-color-primary);

                    border-radius: 50%;

                }

            }

        }

    }

}

.tabs-item-style-1 {

    ::v-deep(.el-tabs__item) {

        padding: 0 15px !important;

        border-right: var(--el-border-color-extra-light) 1px solid;

        user-select: none;

        color: #8c8c8c;

        &:hover {

            color: #444;

            background: rgba(0, 0, 0, 0.02);

        }

        &.is-active {

            color: var(--el-color-primary);

            background-color: var(--el-color-primary-light-9);

            border-bottom: var(--el-border-color-light) 2px solid;

            &::before {

                background-color: var(--el-color-primary);

            }

        }

        &::before {

            content: '';

            width: 9px;

            height: 9px;

            margin-right: 8px;

            display: inline-block;

            background-color: #ddd;

            border-radius: 50%;

        }

    }

}

.tabs-item-style-2 {

    ::v-deep(.el-tabs__item) {

        padding: 0 15px !important;

        border-right: none;

        user-select: none;

        color: #8c8c8c;

        display: inline-block;

        &:hover {

            color: #444;

            background: rgba(0, 0, 0, 0.02);

            border-bottom: var(--el-color-primary) 2px solid;

        }

        &.is-active {

            color: var(--el-color-primary) !important;

            background-color: var(--el-color-primary-light-9) !important;

            border-bottom: var(--el-color-primary) 2px solid;

            &::before {

                background-color: var(--el-color-primary);

            }

        }

    }

}

.tabs-action {

    height: 40px;

    line-height: 40px;

    box-sizing: border-box;

    padding: 0 12px;

    align-items: center;

    cursor: pointer;

    color: #666;

    border-left: var(--el-border-color-extra-light) 1px solid;

    border-bottom: var(--el-border-color-light) 2px solid;

}

</style>

side

<template>

    <el-aside class="layout-sidebar" :class="sidebarClass">

        <Logo v-if="store.appStore.theme.isLogo" />

        <Menu />

    </el-aside>

</template>

<script setup lang="ts">

import store from '@/store'

import Logo from './components/Logo.vue'

import Menu from './components/Menu.vue'

import { computed } from 'vue'

const sidebarClass = computed(() => {

    const sidebarOpened = store.appStore.sidebarOpened ? 'aside-expend' : 'aside-compress'

    const isDark = store.appStore.theme.sidebarStyle === 'dark' ? 'sidebar-dark' : ''

    return sidebarOpened + ' ' + isDark

})

</script>

menu

<template>

    <el-scrollbar>

        <el-menu

            :default-active="defaultActive"

            :collapse="!store.appStore.sidebarOpened"

            :unique-opened="store.appStore.theme.uniqueOpened"

            background-color="transparent"

            :collapse-transition="false"

            mode="vertical"

            router

        >

            <menu-item v-for="menu in store.routerStore.menuRoutes" :key="menu.path" :menu="menu"></menu-item>

        </el-menu>

    </el-scrollbar>

</template>

<script setup lang="ts">

import store from '@/store'

import MenuItem from './MenuItem.vue'

import { useRoute } from 'vue-router'

import { computed } from 'vue'

const route = useRoute()

const defaultActive = computed(() => {

    const { path } = route

    return path

})

</script>

MenuItem

<template>

    <el-sub-menu v-if="menu.children.length > 0" :key="menu.path" :index="menu.path">

        <template #title>

            <svg-icon :icon="menu.meta.icon"></svg-icon>

            <span>{{ menu.meta.title }}</span>

        </template>

        <menu-item v-for="sub in menu.children" :key="sub.path" :menu="sub"></menu-item>

    </el-sub-menu>

    <el-menu-item v-else :key="menu.path" :index="menu.path">

        <svg-icon :icon="menu.meta.icon"></svg-icon>

        <template #title>{{ menu.meta.title }}</template>

    </el-menu-item>

</template>

<script setup lang="ts">

import { PropType } from 'vue'

defineProps({

    menu: {

        type: Object as PropType<any>,

        required: true

    }

})

</script>

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值