Sidebar

132 阅读2分钟

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>