本篇探讨标签栏导航 TagsView 组件的开发,TagsView 组件滚动及固定的处理、组件缓存、右键菜单功能、重定向实现等。
1 定义 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 组件就开发完成了。
下一篇将继续探讨动态主题切换,敬请期待~