Vue + ElementPlus 实现权限管理系统(四): 面包屑与导航标签菜单

本篇文章要实现的功能如下图所示

面包屑导航

面包屑导航在后台管理系统中是一个非常常见的功能,它用于显示用户当前所在页面的位置路径。通常以层次结构的方式呈现,从首页或主目录开始,逐级显示用户所访问的页面的父级页面,直到当前页面。通过面包屑导航,用户可以清楚地知道当前所在页面的位置,并且可以方便地返回到上一级或其他父级页面,大大提高了用户体验。本篇文章最终要实现的面包屑导航如下图示

element plus已经为我们提供了面包屑组件Breadcrumb,我们可以在项目中直接使用

它的用法很简单,separator 属性来规定分隔符,el-breadcrumb-item的 to 属性中的 path 可以配置要跳转的路由。因此我们可以定义一个专门存放面包屑的数组,然后根据这个数组遍历el-breadcrumb-item组件即可

来到 store/index.ts 中

其中它的类型为

在 Navbar.vue 中使用element-plus提供的组件

接下我们需要在路由跳转的时候为这个数组根据跳转的信息添加一些数据,来到utils/routeUtils.ts中的handleRouter函数,当路由切换时如果有路由权限数组,我们需要将当前路由的 name 和 path 存到面包屑数组中

因为面包屑组件是一级一级的,比如在 vue 中我们可以获取到当前页面路由的 path,比如/dd/child_1_1

其中dd对应的是父菜单1,child_1_1对应的是子菜单1.1,那么 breadcrumbs 应该是这样的

js 复制代码
[
  {
    name: "父菜单1",
    path: "dd",
  },
  {
    name: "子菜单1",
    path: "child_1_1",
  },
];

因为我们需要写一个utils/filterBreadCrumb.ts对其进行处理,将 path 按照/拆成一个数组,然后找到数组中元素(path)对应的菜单名称(name)。想要找到 path 对应的菜单名则需要遍历菜单列表,先找外层有每没有,找不到再找它的 children,所以这里又用到了经典的递归写法

js 复制代码
import { Breadcrumb, MenuList } from "@/store/types";
export const filterBreadCrumb = (path: string, menuList: MenuList[]) => {
    let paths = path.split("/");
    //去空
    paths = paths.filter((item) => item);
    const breadCrumbs: Breadcrumb[] = [];
    paths.forEach((item) => {
        breadCrumbs.push({
            name: getMenuTitle(item, menuList),
        });
    });
    return breadCrumbs;
};

export const getMenuTitle = (path: string, menuList: MenuList[]): string => {
    for (let i in menuList) {
        if (menuList[i].path === path) {
            return menuList[i].meta.title!;
        }
        if (menuList[i].menu_type === 1) {
            return getMenuTitle(path, menuList[i].children) || ""
        }
    }
    return ''
};

这样我们便完成了面包屑的导航功能

Tag 标签

除了面包屑导航,在后台管理系统页面中一般都会有导航栏标签这一功能,它可以让我们点击过的菜单以 tab 标签栏的形式展现出来

同样的先定义一个全局变量来存放标签数据

它的类型为

js 复制代码
export type NavTag = {
    name: string
    path: string
    fullpath?: string
}

同时写一个增加 tag 标签的方法

在路由跳转的时候调用它(utils/routeUtils.ts)

然后我们新建一个组件NavBar/components/Tag.vue来实现标签导航,这里使用element-plus提供的tag组件

vue 复制代码
<template>
  <el-scrollbar>
    <div class="flex">
      <el-tag
        @contextmenu.prevent="tagsEmits('openMenu', $event, index)"
        v-for="(item, index) in tagsProps.navTags"
        @click="handelTo(item)"
        :key="item.name"
        class="ml-2 cursor-pointer flex-shrink-0"
        :effect="currentPath === item.path ? 'dark' : undefined"
        type="primary"
        :closable="item.path != '/'"
        @close="handleClose(index, item.path, currentPath)"
      >
        {{ item.name }}
        <slot :item="item" :currentPath="currentPath" :index="index" />
      </el-tag>
    </div>
  </el-scrollbar>
</template>

<script lang="ts" setup>
import { ref, watch } from "vue";

import { useRoute, useRouter } from "vue-router";
import { NavTag } from "@/store/types";
const route = useRoute();
const router = useRouter();

type TagsProps = {
  navTags: NavTag[];
};
const tagsProps = defineProps<TagsProps>();

type TagsEmits = {
  (e: "close", index: number, path: string, currentPath: string): void;
  (e: "openMenu", event: Event, index: number): void;
};
const tagsEmits = defineEmits<TagsEmits>();
//当前路由路径
const currentPath = ref();
watch(
  () => route.path,
  (path) => {
    currentPath.value = path;
  },
  { immediate: true }
);
//路由跳转
const handelTo = (item: any) => {
  router.push(item.path);
};

/**
 *
 * @param index 标签索引
 * @param path 标签路径
 * @param currentPath 当前路由路径
 */
const handleClose = (index: number, path: string, currentPath: string) => {
  tagsEmits("close", index, path, currentPath);
};
</script>

NavBar/index.vue引入该组件,同时实现关闭标签逻辑

html 复制代码
<template>
  <div class="p-2 shadow-sm flex items-center h-[50px] box-border">
    <div @click="appStore.isCollapse = !appStore.isCollapse" class="mr-4">
      <Fold v-if="!appStore.isCollapse" class="w-6 cursor-pointer" />
      <Expand v-else class="w-6 cursor-pointer" />
    </div>
    <el-breadcrumb separator="/">
      <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
      <el-breadcrumb-item v-for="item in appStore.breadcrumbs">
        {{ item.name }}
      </el-breadcrumb-item>
    </el-breadcrumb>
  </div>
  <div class="shadow-sm py-1">
    <Tags
      @open-menu="openMenu"
      :nav-tags="appStore.navTags"
      @close="closeTag"
    />
  </div>
</template>

<script lang="ts" setup>
  import useAppStore from "@/store/index";
  import Tags from "./components/Tags.vue";
  import TagView from "./components/TagView.vue";
  import { useRouter, useRoute } from "vue-router";
  import { NavTag } from "../../../store/types/index";
  import { ref } from "vue";
  const router = useRouter();
  const route = useRoute();
  const appStore = useAppStore();
  //关闭tag
  const closeTag = (index: number, path: string, currentPath: string) => {
    appStore.navTags.splice(index, 1);
    const length = appStore.navTags.length;
    //没用tag标签跳转首页
    if (!length) {
      router.replace("/index");
      return;
    }
    //如果关闭的是当前页,跳转上一个tag页面
    if (path === currentPath) {
      length && router.replace(appStore.navTags.slice(-1)[0].path);
    }
  };
</script>

至此便完成了导航标签的功能

标签右键刷新关闭菜单

接下来我们来实现右键标签可以展示刷新页面,关闭当前,关闭其它,全部关闭选项,如下图所示

同样的我们将其封装到一个组件中NavBar/components/TagView.vue,同时将对应事件发送出去

html 复制代码
<template>
  <div
    class="bg-white shadow-lg rounded-md text-[14px] overflow-hidden cursor-pointer w-[100px] fixed"
  >
    <div
      class="p-1 hover:bg-[#E0E0E0] flex items-center"
      @click="emits('refreshTag')"
    >
      <el-icon> <Refresh /> </el-icon><span>刷新页面</span>
    </div>
    <div
      class="p-1 hover:bg-[#E0E0E0] flex items-center"
      @click="emits('closeCur')"
    >
      <el-icon> <CloseBold /> </el-icon><span>关闭当前</span>
    </div>
    <div
      class="p-1 hover:bg-[#E0E0E0] flex items-center"
      @click="emits('closeOtherTags')"
    >
      <el-icon> <Close /> </el-icon><span>关闭其它</span>
    </div>
    <div
      class="p-1 hover:bg-[#E0E0E0] flex items-center"
      @click="emits('closeAllTags')"
    >
      <el-icon> <Close /> </el-icon><span>全部关闭</span>
    </div>
  </div>
</template>

<script lang="ts" setup>
  type Emits = {
    (e: "closeCur"): void;
    (e: "closeOtherTags"): void;
    (e: "closeAllTags"): void;
    (e: "refreshTag"): void;
  };
  const emits = defineEmits<Emits>();
</script>

这里我们需要在 Tag 组件中引入,但是我们又不想让其逻辑和 Tag 组件的逻辑混在一起,所以我们这里使用slot插槽来引入该组件,同时将参数通过插槽传递出去,并且使用了右击鼠标事件@contextmenu将其事件传递出去,这样我们就可以在NavBar/index.vue中实现逻辑

最后NavBar/index.vue写下对应逻辑,其中实现方式写到了注释中

html 复制代码
<template>
  <div class="p-2 shadow-sm flex items-center h-[50px] box-border">
    <div @click="appStore.isCollapse = !appStore.isCollapse" class="mr-4">
      <Fold v-if="!appStore.isCollapse" class="w-6 cursor-pointer" />
      <Expand v-else class="w-6 cursor-pointer" />
    </div>
    <el-breadcrumb separator="/">
      <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
      <el-breadcrumb-item v-for="item in appStore.breadcrumbs">
        {{ item.name }}
      </el-breadcrumb-item>
    </el-breadcrumb>
  </div>
  <div class="shadow-sm py-1">
    <Tags
      @open-menu="openMenu"
      :nav-tags="appStore.navTags"
      @close="closeTag"
      #default="{ item, currentPath, index }"
    >
      <teleport to="body">
        <TagView
          v-if="isTagView && nowClickIndex == index"
          :style="tagViewStyle"
          @close-cur="closeCur(index, item.path, currentPath)"
          @close-all-tags="closeAllTags"
          @close-other-tags="closeOtherTags(item)"
          @refreshTag="refreshTag(item)"
        />
      </teleport>
    </Tags>
  </div>
</template>

<script lang="ts" setup>
  import useAppStore from "@/store/index";
  import Tags from "./components/Tags.vue";
  import TagView from "./components/TagView.vue";
  import { useRouter, useRoute } from "vue-router";
  import { NavTag } from "../../../store/types/index";
  import { ref } from "vue";
  const router = useRouter();
  const route = useRoute();
  const appStore = useAppStore();

  //关闭tag
  const closeTag = (index: number, path: string, currentPath: string) => {
    appStore.navTags.splice(index, 1);
    const length = appStore.navTags.length;
    //没用tag标签跳转首页
    if (!length) {
      router.replace("/index");
      return;
    }
    //如果关闭的是当前页,跳转上一个tag页面
    if (path === currentPath) {
      length && router.replace(appStore.navTags.slice(-1)[0].path);
    }
  };
  //关闭当前
  const closeCur = (index: number, path: string, currentPath: string) => {
    appStore.navTags.splice(index, 1);

    if (path === currentPath) {
      const length = appStore.navTags.length;
      length && router.push(appStore.navTags[length - 1].path);
      !length && router.push("/");
    }
  };
  //关闭其它
  const closeOtherTags = (item: NavTag) => {
    appStore.$patch({
      navTags: [{ name: "首页", path: "/" }, item],
    });
    router.push(item.path);
    isTagView.value = false;
  };
  //关闭所有
  const closeAllTags = () => {
    appStore.$patch({
      navTags: [{ name: "首页", path: "/" }],
    });
    router.push("/");
    isTagView.value = false;
  };

  //刷新
  const refreshTag = (item: NavTag) => {
    router.push({
      path: "/redirect" + item.fullpath,
      query: route.query,
    });
  };

  const isTagView = ref(false);

  //点击其他地方关闭tagView

  const listener = () => {
    isTagView.value = false;
    document.removeEventListener("click", listener);
  };
  const tagViewStyle = ref({});
  const nowClickIndex = ref();
  const openMenu = (e: any, index: number) => {
    isTagView.value = true;
    document.addEventListener("click", listener);
    //根据当前点击位置定位tagView的位置
    tagViewStyle.value = {
      left: e.clientX + "px",
      top: e.clientY + "px",
    };
    //记录当前点击的tag的索引,用于判断显示哪个tagView
    nowClickIndex.value = index;
  };
</script>

其中刷新的功能实现方式为跳转到一个新的路由传入 path 和 query,然后在新的路由中再跳转回来,其中redirect/index.vue

html 复制代码
<template>
  <div></div>
</template>

<script setup lang="ts">
  import { useRoute, useRouter } from "vue-router";

  const route = useRoute();
  const router = useRouter();
  const { params, query } = route;
  const { path } = params;
  defineOptions({
    name: "Redirect",
  });
  router.replace({ path: "/" + path, query });
</script>

还需要在路由router/index.ts中加一个配置

然后添加标签将其忽略掉,不然会出现一个空标签

ok,到这里整个功能就实现了,后续我们将开始实现菜单管理,角色管理等内容

代码地址 如果文章对你有帮助,给个Star友友们~

相关推荐
言之。19 分钟前
Go 语言中接口类型转换为具体类型
开发语言·后端·golang
L耀早睡19 分钟前
mapreduce打包运行
大数据·前端·spark·mapreduce
MaCa .BaKa32 分钟前
38-日语学习小程序
java·vue.js·spring boot·学习·mysql·小程序·maven
HouGISer33 分钟前
副业小程序YUERGS,从开发到变现
前端·小程序
outstanding木槿39 分钟前
react中安装依赖时的问题 【集合】
前端·javascript·react.js·node.js
diving deep1 小时前
XML简要介绍
xml·java·后端
霸王蟹1 小时前
React中useState中更新是同步的还是异步的?
前端·javascript·笔记·学习·react.js·前端框架
霸王蟹1 小时前
React Hooks 必须在组件最顶层调用的原因解析
前端·javascript·笔记·学习·react.js
专注VB编程开发20年2 小时前
asp.net IHttpHandler 对分块传输编码的支持,IIs web服务器后端技术
服务器·前端·asp.net
爱分享的程序员2 小时前
全栈项目搭建指南:Nuxt.js + Node.js + MongoDB
前端