后端管理系统,前后端分离的框架若依管理后台,来看下vue3+element-plus版本。
静态文本 assets
assets 静态img、svg、style
main.js import '@/assets/styles/index.scss' // global css
引入了全局样式
组件 components
breadcrumb 面包屑
从路由中获取面包屑路径
<template> <el-breadcrumb class="app-breadcrumb" separator="/"> <transition-group name="breadcrumb"> <el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path"> <span v-if="item.redirect === 'noRedirect' || index == levelList.length - 1" class="no-redirect">{{ item.meta.title }}</span> <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a> </el-breadcrumb-item> </transition-group> </el-breadcrumb> </template> <script setup> const route = useRoute(); const router = useRouter(); const levelList = ref([]) function getBreadcrumb() { // only show routes with meta.title let matched = route.matched.filter(item => item.meta && item.meta.title); const first = matched[0] // 判断是否为首页 if (!isDashboard(first)) { matched = [{ path: '/index', meta: { title: '首页' } }].concat(matched) } levelList.value = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false) } function isDashboard(route) { const name = route && route.name if (!name) { return false } return name.trim() === 'Index' } function handleLink(item) { const { redirect, path } = item if (redirect) { router.push(redirect) return } router.push(path) } watchEffect(() => { // if you go to the redirect page, do not update the breadcrumbs if (route.path.startsWith('/redirect/')) { return } getBreadcrumb() }) getBreadcrumb(); </script> <style lang='scss' scoped> .app-breadcrumb.el-breadcrumb { display: inline-block; font-size: 14px; line-height: 50px; margin-left: 8px; .no-redirect { color: #97a8be; cursor: text; } } </style>
hamburger
展示按钮svg图标
headerSearch 搜索框
Fuse.js——用于JavaScript中数据的模糊搜索
pagination 分页
treeSelect 树选取器
topNav 顶部导航
<template> <el-menu :default-active="activeMenu" mode="horizontal" @select="handleSelect" > <template v-for="(item, index) in topMenus"> <el-menu-item :style="{'--theme': theme}" :index="item.path" :key="index" v-if="index < visibleNumber" ><svg-icon :icon-class="item.meta.icon" /> {{ item.meta.title }}</el-menu-item > </template> <!-- 顶部菜单超出数量折叠 --> <el-sub-menu :style="{'--theme': theme}" index="more" v-if="topMenus.length > visibleNumber"> <template #title>更多菜单</template> <template v-for="(item, index) in topMenus"> <el-menu-item :index="item.path" :key="index" v-if="index >= visibleNumber" ><svg-icon :icon-class="item.meta.icon" /> {{ item.meta.title }}</el-menu-item > </template> </el-sub-menu> </el-menu> </template> <script setup> import { constantRoutes } from "@/router" import { isHttp } from '@/utils/validate' // 顶部栏初始数 const visibleNumber = ref(null); // 是否为首次加载 const isFrist = ref(null); // 当前激活菜单的 index const currentIndex = ref(null); const store = useStore(); const route = useRoute(); // 主题颜色 const theme = computed(() => store.state.settings.theme); // 所有的路由信息 const routers = computed(() => store.state.permission.topbarRouters); // 顶部显示菜单 const topMenus = computed(() => { let topMenus = []; routers.value.map((menu) => { if (menu.hidden !== true) { // 兼容顶部栏一级菜单内部跳转 if (menu.path === "/") { topMenus.push(menu.children[0]); } else { topMenus.push(menu); } } }) return topMenus; }) // 设置子路由 const childrenMenus = computed(() => { let childrenMenus = []; routers.value.map((router) => { for (let item in router.children) { if (router.children[item].parentPath === undefined) { if(router.path === "/") { router.children[item].path = "/redirect/" + router.children[item].path; } else { if(!isHttp(router.children[item].path)) { router.children[item].path = router.path + "/" + router.children[item].path; } } router.children[item].parentPath = router.path; } childrenMenus.push(router.children[item]); } }) return constantRoutes.concat(childrenMenus); }) // 默认激活的菜单 const activeMenu = computed(() => { const path = route.path; let activePath = defaultRouter.value; if (path !== undefined && path.lastIndexOf("/") > 0) { const tmpPath = path.substring(1, path.length); activePath = "/" + tmpPath.substring(0, tmpPath.indexOf("/")); } else if ("/index" == path || "" == path) { if (!isFrist.value) { isFrist.value = true; } else { activePath = "index"; } } let routes = activeRoutes(activePath); if (routes.length === 0) { activePath = currentIndex.value || defaultRouter.value activeRoutes(activePath); } return activePath; }) // 默认激活的路由 const defaultRouter = computed(() => { let router; Object.keys(routers.value).some((key) => { if (!routers.value[key].hidden) { router = routers.value[key].path; return true; } }); return router; }) function setVisibleNumber() { const width = document.body.getBoundingClientRect().width / 3; visibleNumber.value = parseInt(width / 85); } function handleSelect(key, keyPath) { currentIndex.value = key; if (isHttp(key)) { // http(s):// 路径新窗口打开 window.open(key, "_blank"); } else if (key.indexOf("/redirect") !== -1) { // /redirect 路径内部打开 router.push({ path: key.replace("/redirect", "") }); } else { // 显示左侧联动菜单 activeRoutes(key); } } function activeRoutes(key) { let routes = []; if (childrenMenus.value && childrenMenus.value.length > 0) { childrenMenus.value.map((item) => { if (key == item.parentPath || (key == "index" && "" == item.path)) { routes.push(item); } }); } if(routes.length > 0) { store.commit("SET_SIDEBAR_ROUTERS", routes); } return routes; } onMounted(() => { window.addEventListener('resize', setVisibleNumber) }) onBeforeUnmount(() => { window.removeEventListener('resize', setVisibleNumber) }) onMounted(() => { setVisibleNumber() }) </script> <style lang="scss"> .topmenu-container.el-menu--horizontal > .el-menu-item { float: left; height: 50px !important; line-height: 50px !important; color: #999093 !important; padding: 0 5px !important; margin: 0 10px !important; } .topmenu-container.el-menu--horizontal > .el-menu-item.is-active, .el-menu--horizontal > .el-sub-menu.is-active .el-submenu__title { border-bottom: 2px solid #{'var(--theme)'} !important; color: #303133; } /* sub-menu item */ .topmenu-container.el-menu--horizontal > .el-sub-menu .el-submenu__title { float: left; height: 50px !important; line-height: 50px !important; color: #999093 !important; padding: 0 5px !important; margin: 0 10px !important; } </style>
router.js中指定了布局
import Layout from '@/layout' { path: '/user', // 页面布局 component: Layout, hidden: true, redirect: 'noredirect', children: [ { path: 'profile', component: () => import('@/views/system/user/profile/index'), name: 'Profile', meta: { title: '个人中心', icon: 'user' } } ] },
layout/index.vue 实现了页面的布局
<template> <div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme }"> <div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside"/> <!-- 菜单栏 --> <sidebar class="sidebar-container" /> <!-- 标签 --> <div :class="{ hasTagsView: needTagsView }" class="main-container"> <div :class="{ 'fixed-header': fixedHeader }"> <navbar @setLayout="setLayout" /> <tags-view v-if="needTagsView" /> </div> <!-- 主视图 --> <app-main /> <settings ref="settingRef" /> </div> </div> </template>
前端框架的 组件及页面布局完成。