Vue3.5 企业级管理系统实战(十三):TagsView标签栏导航

发布于:2025-04-06 ⋅ 阅读:(12) ⋅ 点赞:(0)

本篇探讨标签栏导航 TagsView 组件的开发,TagsView 组件滚动及固定的处理、组件缓存、右键菜单功能、重定向实现等。

定义 TagsView store

在 src/stores 下新建 tagsView.ts 文件,代码如下:

//src/stores/tagsView.ts
import {
  type RouteRecordName,
  type RouteLocationNormalizedLoadedGeneric
} from "vue-router";

export const useTagsView = defineStore("tagsView", () => {
  // 存储已访问的视图列表
  const visitedViews = ref<RouteLocationNormalizedLoadedGeneric[]>([]);
  // 存储需要缓存的视图名称列表
  const cacheViews = ref<RouteRecordName[]>([]);

  // 添加视图到已访问视图列表和缓存视图列表中
  const addView = (view: RouteLocationNormalizedLoadedGeneric) => {
    const exits = visitedViews.value.some((v) => v.path === view.path);
    addCacheView(view); // 重新添加缓存视图以防止缓存被清除
    if (exits) return;
    const newView = {
      ...view,
      title: view.meta.title // 设置页签标题
    };
    visitedViews.value.push(newView);
  };

  // 从已访问视图列表和缓存视图列表中删除指定视图
  const deleteView = (view: RouteLocationNormalizedLoadedGeneric) => {
    const index = visitedViews.value.findIndex((v) => v.path === view.path);
    if (index > -1) {
      visitedViews.value.splice(index, 1);
    }
    deleteCacheView(view);
  };

  // 添加视图名称到缓存视图列表中,除非视图不需要缓存
  const addCacheView = (view: RouteLocationNormalizedLoadedGeneric) => {
    if (cacheViews.value.includes(view.name)) return;
    if (!view.meta.noCache) {
      cacheViews.value.push(view.name);
    }
  };

  // 从缓存视图列表中删除指定视图名称
  const deleteCacheView = (view: RouteLocationNormalizedLoadedGeneric) => {
    const index = cacheViews.value.indexOf(view.name);
    if (index > -1) {
      cacheViews.value.splice(index, 1);
    }
  };

  // 删除所有非固定视图,并清空缓存视图列表
  const delAllView = () => {
    visitedViews.value = visitedViews.value.filter((view) => view.meta.affix);
    cacheViews.value = [];
  };

  // 删除除指定视图外的所有视图,并保留指定视图的缓存
  const deleteOtherView = (view: RouteLocationNormalizedLoadedGeneric) => {
    visitedViews.value = visitedViews.value.filter(
      (v) => v.meta.affix || v.path === view.path
    );
    cacheViews.value = cacheViews.value.filter((name) => name === view.name);
  };

  return {
    visitedViews,
    addView,
    deleteView,
    cacheViews,
    delAllView,
    deleteOtherView,
    deleteCacheView
  };
});

2 重定向实现

在 src/views 下新建文件夹 redirect,新建文件 index.vue,代码如下:

//src/views/redirect/index.vue
<script lang="ts">
export default {
  setup() {
    const route = useRoute();

    const router = useRouter();
    // 只是为了实现重定向功能
    router.replace({
      path: "/" + route.params.path,
      query: route.query
    });
  },

  render() {
    return h("template");
  }
};
</script>

在 src/router/index.ts 中配置重定向路由 redirect,代码如下:

//src/router/index.ts
import {
  createRouter,
  createWebHistory,
  type RouteRecordRaw
} from "vue-router";
import Layout from "@/layout/index.vue";
export const constantRoutes: RouteRecordRaw[] = [
  {
    path: "/",
    component: Layout,
    redirect: "/dashboard",
    children: [
      {
        path: "dashboard",
        name: "dashboard",
        component: () => import("@/views/dashboard/index.vue"),
        meta: {
          icon: "ant-design:bank-outlined",
          title: "dashboard",
          affix: true, // 固定在tagsViews中
          noCache: true //   不需要缓存
        }
      }
    ]
  },
  {
    path: "/redirect",
    component: Layout,
    meta: {
      hidden: true
    },
    // 当跳转到  /redirect/a/b/c/d?query=1
    children: [
      {
        path: "/redirect/:path(.*)",
        component: () => import("@/views/redirect/index.vue")
      }
    ]
  },
];
export const asyncRoutes: RouteRecordRaw[] = [
  {
    path: "/documentation",
    component: Layout,
    redirect: "/documentation/index",
    children: [
      {
        path: "index",
        name: "documentation",
        component: () => import("@/views/documentation/index.vue"),
        meta: {
          icon: "ant-design:database-filled",
          title: "documentation"
        }
      }
    ]
  },

  {
    path: "/guide",
    component: Layout,
    redirect: "/guide/index",
    children: [
      {
        path: "index",
        name: "guide",
        component: () => import("@/views/guide/index.vue"),
        meta: {
          icon: "ant-design:car-twotone",
          title: "guide"
        }
      }
    ]
  },
  {
    path: "/system",
    component: Layout,
    redirect: "/system/menu",
    meta: {
      icon: "ant-design:unlock-filled",
      title: "system",
      alwaysShow: true
      // breadcrumb: false
      // 作为父文件夹一直显示
    },
    children: [
      {
        path: "menu",
        name: "menu",
        component: () => import("@/views/system/menu/index.vue"),
        meta: {
          icon: "ant-design:unlock-filled",
          title: "menu"
        }
      },

      {
        path: "role",
        name: "role",
        component: () => import("@/views/system/role/index.vue"),
        meta: {
          icon: "ant-design:unlock-filled",
          title: "role"
        }
      },
      {
        path: "user",
        name: "user",
        component: () => import("@/views/system/user/index.vue"),
        meta: {
          icon: "ant-design:unlock-filled",
          title: "user"
        }
      }
    ]
  },

  {
    path: "/external-link",
    component: Layout,
    children: [
      {
        path: "http://www.baidu.com",
        redirect: "/",
        meta: {
          icon: "ant-design:link-outlined",
          title: "link Baidu"
        }
      }
    ]
  }
];
// 需要根据用户赋予的权限来动态添加异步路由
export const routes = [...constantRoutes, ...asyncRoutes];
export default createRouter({
  routes, // 路由表
  history: createWebHistory() //  路由模式
});

3 组件缓存处理

在 src/layout/components/Appmain.vue 中修改缓存相关配置,代码如下:

//src/layout/components/Appmain.vue
<template>
  <router-view v-slot="{ Component }">
    <transition name="fade">
      <keep-alive :include="inclueds">
        <component :is="Component" :key="$route.path"></component>
      </keep-alive>
    </transition>
  </router-view>
</template>

<script lang="ts" setup>
import { useTagsView } from "@/stores/tagsView";
const store = useTagsView();
const inclueds = computed(() => store.cacheViews as string[]);
</script>

<style lang="scss">
.fade-enter-active,
.fade-leave-active {
  @apply transition-all duration-500 pos-absolute;
}
.fade-enter-from {
  @apply opacity-0 translate-x-[50px];
}
.fade-leave-to {
  @apply opacity-0 translate-x-[-50px];
}
</style>

在 src/router/index.ts 中配置路由的 noCache 属性,定义组件是否需要缓存。

注意,需要缓存的组件中需要定义 name 属性,这样 include 才能正常工作。示例如下:

 

 4 TagsView 组件开发

在 src/layout/components 下新建 TagsView 文件夹,新建文件 index.vue,在此进行 TagsView 组件滚动及固定的处理、右键菜单功能、重定向实现等。代码如下:

//src/layout/components/TagsView/index.vue
<template>
  <div class="tags-view-container">
    <el-scrollbar w-full whitespace-nowrap>
      <router-link
        class="tags-view-item"
        v-for="(tag, index) in visitedViews"
        :class="{
          active: isActive(tag)
        }"
        :key="index"
        :to="{ path: tag.path, query: tag.query }"
      >
        <el-dropdown
          placement="top-start"
          trigger="contextmenu"
          @command="(command) => handleCommand(command, tag)"
        >
          <span leading-28px class="title">{{ (tag as any).title }}</span>
          <template #dropdown>
            <el-dropdown-menu>
              <el-dropdown-item command="all">关闭所有</el-dropdown-item>
              <el-dropdown-item command="other">关闭其他</el-dropdown-item>
              <el-dropdown-item command="self" v-if="!tag.meta.affix"
                >关闭</el-dropdown-item
              >
              <el-dropdown-item command="refresh">刷新</el-dropdown-item>
            </el-dropdown-menu>
          </template>
        </el-dropdown>

        <svg-icon
          v-if="!isAffix(tag)"
          icon-name="ant-design:close-circle-outlined"
          mx-2px
          @click.prevent="closeSelectedTag(tag)"
        ></svg-icon>
      </router-link>
    </el-scrollbar>
  </div>
</template>

<script lang="ts" setup>
import { useTagsView } from "@/stores/tagsView";
import type {
  RouteLocationNormalizedGeneric,
  RouteRecordRaw
} from "vue-router";
import { join } from "path-browserify";
import { routes } from "@/router/index"; //从应用的路由配置文件中导入了所有的路由定义

const store = useTagsView();
const { deleteView, addView, delAllView, deleteOtherView, deleteCacheView } =
  store;
//  必须采用storeToRefs 进行解构出来,否则会丧失响应式
const { visitedViews } = storeToRefs(store);

/*
useRouter 创建了一个 router 实例,该实例用于程序化的导航。
useRouter 是 Vue Router 提供的组合式 API,用于在组件中操作路由。
有了 router 实例,组件可以调用 router.push、router.replace 等方法来导航到不同的页面或视图。
*/
const router = useRouter();
/*
useRoute 创建了一个 route 实例,该实例包含了当前路由的信息。
useRoute 是 Vue Router 提供的组合式 API,用于在组件中获取当前路由的详细信息。
route 对象包含了当前路由的路径、名称、查询参数等信息,组件可以利用这些信息来判断当前活动的标签页、或者执行一些路由相关的逻辑操作。
*/
const route = useRoute();

// 判断当前路由是否激活状态
const isActive = (tag: RouteLocationNormalizedGeneric) => {
  return tag.path === route.path;
};

// 判断标签是否为可关闭状态
function isAffix(tag: RouteLocationNormalizedGeneric) {
  return tag.meta.affix;
}

// 添加当前路由到标签视图
const addTags = () => {
  if (route.name) {
    // 需要添加到tags中
    addView(route);
  }
};

// 导航到最后一个标签视图
const toLastView = () => {
  const lastView = visitedViews.value[visitedViews.value.length - 1];
  if (lastView) {
    router.push(lastView.path);
  } else {
    router.push("/");
  }
};

// 关闭选中的标签视图
const closeSelectedTag = (tag: RouteLocationNormalizedGeneric) => {
  // ...
  deleteView(tag);

  if (isActive(tag)) {
    // 如果删掉了自己,需要导航到当前list中的最后一个
    toLastView();
  }
};

//  此方法用于计算 哪些tag应该默认展示在列表中
function filterAffix(routes: RouteRecordRaw[], basePath = "/") {
  const tags: RouteLocationNormalizedGeneric[] = [];
  for (let route of routes) {
    if (route.meta?.affix) {
      tags.push({
        name: route.name,
        path: join(basePath, route.path),
        meta: route.meta
      } as RouteLocationNormalizedGeneric);
    }
    if (route.children) {
      tags.push(...filterAffix(route.children, route.path));
    }
  }

  return tags;
}

// 初始化标签视图,添加固定标签和当前路由标签
const initTags = () => {
  const filterAffixTags = filterAffix(routes);
  filterAffixTags.forEach((tag) => {
    //添加固定标签
    addView(tag);
  });
  //添加当前路由标签
  addTags();
};

// 页面加载后 需要初始化固定 + 默认当前路径的
onMounted(() => {
  initTags();
});

//  路径变化时重新添加
watch(() => route.path, addTags);

// 点击菜单

const enum CommandType {
  All = "all",
  Other = "other",
  Self = "self",
  Refresh = "refresh"
}

// 处理菜单命令
const handleCommand = (
  command: CommandType,
  view: RouteLocationNormalizedGeneric
) => {
  switch (command) {
    case CommandType.All:
      delAllView();
      break;
    case CommandType.Other:
      deleteOtherView(view);
      if (!isActive(view)) {
        router.push(view.path);
      }
      break;
    case CommandType.Self:
      closeSelectedTag(view);
      break;
    case CommandType.Refresh:
      // 如果本次路径和上次路径相同,刷新会没有效果
      // 解决方法:跳转到专门做刷新的一个路由,在通过这个路由回来即可
      deleteCacheView(view);

      router.push("/redirect" + view.path);
      break;
  }
};
</script>

<style scoped>
.tags-view-container {
  @apply w-full overflow-hidden  @apply h-[var(--tagsview-height)] shadow-sm shadow-gray-300 bg-gray-100;
}
.tags-view-item {
  @apply inline-block h-28px leading-28px  px-3px mx-3px  text-black mt-1 bg-gray-300;
  &.active {
    @apply text-white border-none bg-green;
    .title {
      @apply text-white;
    }
    &::before {
      content: "";
      @apply inline-block w-8px h-8px rounded-full  bg-white mr-3px;
    }
  }
}
</style>

5 页面引入

在 src/layout/components/index.vue 中引入 TagsView 组件,代码如下:

//src/layout/components/index.vue
<template>
  <div class="app-wrapper">
    <div class="sidebar-container">
      <sidebar></sidebar>
    </div>
    <div class="main-container">
      <div class="header">
        <!--  上边包含收缩的导航条 -->
        <navbar></navbar>
        <tags-view></tags-view>
      </div>
      <div class="app-main">
        <app-main></app-main>
      </div>
    </div>
  </div>
</template>
<style lang="scss" scoped>
.app-wrapper {
  @apply flex w-full h-full;
  .sidebar-container {
    // 跨组件设置样式
    @apply bg-[var(--menu-bg)];
    :deep(.sidebar-container-menu:not(.el-menu--collapse)) {
      @apply w-[var(--sidebar-width)];
    }
  }
  .main-container {
    @apply flex flex-col flex-1;
  }
  .header {
    @apply h-84px;
    .navbar {
      @apply h-[var(--navbar-height)] bg-yellow;
    }
    .tags-view {
      @apply h-[var(--tagsview-height)] bg-blue;
    }
  }
  .app-main {
    @apply bg-cyan;
    min-height: calc(100vh - var(--tagsview-height) - var(--navbar-height));
  }
}
</style>

npm run dev 启动后,页面效果如下:

以上,TagsView 组件就开发完成了。

 下一篇将继续探讨动态主题切换,敬请期待~