Sidebar
目录结构
src/components/Sidebar/
├── index.vue
├── Link.vue
├── Item.vue
└── SidebarItem.vue
整体架构
index.vue (主容器)
↓
SidebarItem.vue (菜单项处理核心)
↓
Item.vue (菜单项内容渲染)
Link.vue (链接处理)
index.vue
作为侧边栏的入口组件,负责渲染系统 Logo、标题,并遍历权限路由生成菜单项。
<template>
<div class="has-logo">
<!-- 系统标题和logo -->
<p @click="goHome" class="system-title">
<img src="../../assets/logo.png" alt="">
CMS页面配置系统
</p>
<!-- Element UI 菜单容器 -->
<el-menu>
<!-- 遍历路由数据,为每个路由创建 SidebarItem 组件 -->
<sidebar-item
v-for="route in permission_routes"
:key="route.path"
:item="route"
:base-path="route.path"
/>
</el-menu>
</div>
</template>
SidebarItem.vue
SidebarItem 组件是一个高度可复用的递归组件,能够根据路由配置动态生成不同层级的菜单结构。它通过 hasOneShowingChild 方法判断菜单项的显示方式,并使用 resolvePath 方法处理路径解析,支持内部路由和外部链接。
<template>
<div v-if="!item.hidden" class="menu-wrapper">
<!-- 如果只有一个要显示的子菜单项,则直接显示 -->
<template v-if="hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.alwaysShow">
<app-link v-if="item.redirect !== 'noRedirect' && onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
<el-menu-item
:index="resolvePath(onlyOneChild.path)"
class="submenu-title-noDropdown"
>
<i class="el-icon-tickets" />
{{ onlyOneChild.meta.title }}
</el-menu-item>
</app-link>
</template>
<!-- 如果有多个要显示的子菜单项,则显示为子菜单 -->
<el-submenu
v-else
ref="subMenu"
:index="resolvePath(item.path)"
popper-append-to-body
>
<template slot="title">
<item
v-if="item.meta"
:icon="item.meta && item.meta.icon"
:title="item.meta.title"
/>
</template>
<sidebar-item
v-for="child in item.children"
:key="child.path"
:item="child"
:base-path="resolvePath(child.path)"
class="nest-menu"
/>
</el-submenu>
</div>
</template>
<script>
import path from 'path'
import { Validator } from '@bigbighu/cms-utils'
import Item from './Item'
import AppLink from './Link'
export default {
name: 'SidebarItem',
components: { Item, AppLink },
props: {
// 菜单项数据
item: {
type: Object,
required: true
},
// 基础路径
basePath: {
type: String,
default: ''
}
},
data() {
// 用于存储唯一子项的变量
this.onlyOneChild = null
return {}
},
methods: {
/**
* 判断是否只有一个要显示的子菜单项
* @param {Array} children 子菜单项数组
* @param {Object} parent 父级菜单项
* @returns {Boolean} 是否只有一个要显示的子菜单项
*/
hasOneShowingChild(children = [], parent) {
// 过滤出不隐藏的子菜单项
const showingChildren = children.filter(item => {
if (item.hidden) {
return false
} else {
return true
}
})
// 如果只有一个不隐藏的子菜单项
if (showingChildren.length === 1) {
this.onlyOneChild = showingChildren[0]
return true
}
// 如果没有不隐藏的子菜单项
if (showingChildren.length === 0) {
this.onlyOneChild = { ...parent, path: '', noShowingChildren: true }
return true
}
// 如果有多个不隐藏的子菜单项
return false
},
/**
* 解析路由路径
* @param {String} routePath 路由路径
* @returns {String} 解析后的完整路径
*/
resolvePath(routePath) {
// 如果是外部链接,直接返回
if (Validator.isExternal(routePath)) {
return routePath
}
// 如果基础路径是外部链接,返回基础路径
if (Validator.isExternal(this.basePath)) {
return this.basePath
}
// 合并基础路径和路由路径
return path.resolve(this.basePath, routePath)
}
}
}
</script>
Item.vue
定义了一个 轻量级、无状态的函数式 Vue 组件,名为 MenuItem,在侧边栏菜单中渲染 图标 + 标题 的组合内容。
<script>
export default {
name: 'MenuItem',
functional: true,
props: {
icon: {
type: String,
default: ''
},
title: {
type: String,
default: ''
}
},
//渲染逻辑
render(h, context) {
const { icon, title } = context.props
const vnodes = []
const classStr = 'iconfont icon-' + icon
if (icon) {
vnodes.push(<i class={classStr}> </i>)
}
if (title) {
vnodes.push(<span slot="title"> {title}</span>)
}
return vnodes
}
}
</script>
Link.vue
根据传入的链接地址(to)自动判断:是跳转内部路由(使用 <router-link>),还是打开外部链接(使用 <a> 标签)。
<template>
<component v-bind="linkProps(to)">
<slot />
</component>
</template>
<script>
// 工具函数,判断是否为外部链接
import { Validator } from '@bigbighu/cms-utils'
export default {
props: {
to: {
type: String,
required: true
}
},
methods: {
linkProps(url) {
// 如果是外部链接(如 http:// 或 https:// 开头)
if (Validator.isExternal(url)) {
return {
is: 'a', // 渲染为 <a> 标签
href: url, // 外部链接地址
target: '_blank',// 新窗口打开
rel: 'noopener'
}
}
// 否则视为内部路由
return {
is: 'router-link',// 渲染为 <router-link>
to: url
}
}
}
}
</script>