Vue3边学边做系列(5)--布局切换&菜单事件&标签页 - 实践
Vue3边学边做系列(5)–布局切换&菜单事件&标签页
前端开发系列(1)-开发环境安装
前端开发系列(2)-项目框架搭建
Vue3边学边做系列(3)-路由缓存接口封装
vue3边学边做系列(4)-工具条&系统设置&动态菜单
本期效果图

本期实现的功能:
- 布局的优化
- 菜单的动态生成
- 菜单的事件处理
- 标签页的设置
1. 布局的优化
顶部菜单的布局:

左侧菜单布局1:

左侧菜单布局2(折叠版):

混合布局:

每种布局通过动态组件生成.
2. 动态菜单生成
菜单组件进一步优化.
(1) 作为递归调用的菜单组件:
src/layouts/menus/MenuItem.vue
{{ menu.title }}
{{ menu.title }}
<script setup lang="ts">
import type { BackendMenu } from "@/stores/route";
import MenuItem from "./MenuItem.vue";
import { computed } from "vue";
const props = withDefaults(
defineProps<{
menu: BackendMenu;
showIcon?: boolean;
}>(),
{
showIcon: true,
}
);
const hasChildren = computed(
() => props.menu.children && props.menu.children?.length > 0
);
</script>
然后, 就是被各个布局管理器调用的菜单, 提供了几种参数设置可以作为外部传入:
interface Props {
menus: BackendMenu[]; //菜单数据
mode?: 'horizontal' | 'vertical'; //方向
collapse?: boolean; //是否折叠
showIcon?: boolean; //是否显示菜单图标
defaultActive?: string; //默认激活菜单
menuTrigger?: 'click' | 'hover'; //触发方式
}
withDefaults(defineProps<Props>(),{
mode: 'horizontal',
collapse: false,
showIcon: true,
defaultActive: '',
menuTrigger: 'click',
});
通过这样几种参数,可以满足基本的布局需求
- defaultActive
默认激活的名称, 这个暴露出去,非常方便在混合模式下, 分别设置父级和子级的激活状态.
在下面的菜单列表中引入上面的菜单来实现树的遍历, 完整的参考如下:
src/layouts/menus/MenuList.vue
<script setup lang="ts">
import type { BackendMenu } from "@/stores/route";
import MenuItem from "./MenuItem.vue";
interface Props {
menus: BackendMenu[];
mode?: 'horizontal' | 'vertical';
collapse?: boolean;
showIcon?: boolean;
defaultActive?: string;
menuTrigger?: 'click' | 'hover';
}
withDefaults(defineProps(),{
mode: 'horizontal',
collapse: false,
showIcon: true,
defaultActive: '',
menuTrigger: 'click',
});
const emit = defineEmits<{
select:[name:string]
}>();
const handleSelect = (name: string) => {
emit('select',name);
};
</script>
在lay-mix混合布局中, 继续引入menulist来实现菜单的展示.
由于混合布局时, 存在顶部菜单和侧边菜单, 所以需要分别在顶部和侧边引入menulist, 然后分别传入菜单数据.
将状态中缓存的menuStore.activeTopName 和menuStore.activeCurrName分别赋值给了顶级菜单和左侧子级菜单, 这样方便后续菜单切换时, 只需要更新缓存中这两个值就可以了.
2. 菜单的缓存
菜单缓存定义的一些路由跳转使用的方法:
菜单缓存特别重要, 几乎所有的菜单状态的激活, 以及路由的跳转,都是通过这里的toSelect方法来控制的, 这样做是为了统一入口.
在上面的混合布局中, @select事件直接调用了menuStore.toSelect方法.
//跳转并添加到标签页
function toSelect(name: string) {
const r = router.getRoutes().find((r) => r.name === name);
if (r) {
const { meta, name, path } = r;
if (meta.type === RouteType.Page) {
const tab = {
title: (meta.title as string) || "",
path,
name: name as string,
keepAlive: true,
};
throttledAddTab(tab);
router.push({ name: name as string });
}
}
//更新激活的路由菜单
updateActiveMenu(name);
}
当点击菜单时,获取当前路由名称, 根据路由name在总的路由中查找, 找到点击的菜单对应的路由信息.
如果是页面类型, 则将其插入到标签页中, 然后通过router.push完成跳转.
最后激活对应的菜单
//更新激活的菜单
function updateActiveMenu(currentRouteName: string) {
activeTopName.value = findTopMenuName(currentRouteName);
activeCurrName.value = currentRouteName;
}
更新激活的菜单中,包含2个缓存值, activeTopName 是用来使顶部菜单高亮的, activeCurrName是使访问的页面的菜单高亮显示的.
当我们单击菜单子项时, 在路由信息中并不包含顶级菜单的信息, 所有需要通过反向查找 findTopMenuName 方法来查找对应的顶级菜单的信息
这样就完整实现了菜单的点击 -> 标签页面的添加 -> 路由的跳转 -> 点击的菜单以及顶级菜单的高亮显示.
3. 标签页的设置
标签页使用的是el-tabs来实现的, 由于需要对页面刷新后,保留标签的状态信息, 所以需要对标签页进行缓存.
3.1 标签页事件
通用的标签页包含下面几个事件:
单击事件
关闭事件
右键事件
所以除了@tab-remove和@tab-click之外, 还需要支持右键菜单事件, 右键事件放到标签模版中.
右键菜单包含: 关闭当前, 关闭左侧, 关闭右侧, 关闭其他, 关闭所有.
{{ item.title }}
关闭当前
关闭左侧
关闭右侧
关闭其他
关闭所有
单击事件
需要激活对应的标签, 并且标签和菜单需要联动起来, 所以这里使用了菜单中的
toSelect方法来改变路由信息.const tabClick = (pane: TabsPaneContext, event: Event) => { menuStore.toSelect(pane.paneName as string); };标签页的监听
watch(activeTab, (newTab) => { menuStore.toSelect(newTab.name); });通过设置监听
activeTab的变化,来自动路由对应的页面
3.2 标签页缓存
状态信息:
//标签页
const tabList = ref([]);
const activeTab = ref({ ...homeTab });
每点击一个菜单, 添加一个标签信息, 所以标签页的缓存使用数组tabList来保存, 并且使用activeTab 来标记当前标签是否被激活.
初始化时,会自动将首页设置为激活状态, 并且首页不可关闭的.
右键事件的处理:
重点是处理右键中各个事件, 通过switch 方式处理右键菜单中不同的事件:
// 处理右键菜单命令
const tabRightClick = (params: {
event: RightEvent;
targetTab: TabType;
}) => {
const event = params.event;
const targetTab = params.targetTab;
const index = tabList.value.findIndex(
(tab) => tab.name === targetTab.name
);
switch (event) {
case RightEvent.closeCurrent:
// 保护性检查:如果目标就是Home页,则不执行关闭
if (targetTab.name === homeTab.name) return;
tabRemove(targetTab.name);
break;
case RightEvent.closeLeft:
// 从索引1开始删除,确保跳过Home页
if (index > 1) {
// 只有左侧有可关闭的标签时才执行
tabList.value.splice(1, index - 1); // 从索引1开始,删除 (index-1) 个元素
// 检查当前激活页是否被删除
if (
tabList.value.findIndex(
(tab) => tab.name === activeTab.value.name
) === -1
) {
activeTab.value = targetTab;
}
}
break;
case RightEvent.closeRight:
// 计算结束索引,确保不会删到Home页
const homeIndex = tabList.value.findIndex(
(tab) => tab.name === homeTab.name
);
const endIndex = index < homeIndex ? homeIndex : tabList.value.length;
const deleteStartIndex = index + 1;
const deleteCount =
(endIndex > index ? endIndex : tabList.value.length) -
deleteStartIndex;
if (deleteCount > 0) {
tabList.value.splice(deleteStartIndex, deleteCount);
if (
tabList.value.findIndex(
(tab) => tab.name === activeTab.value.name
) === -1
) {
activeTab.value = targetTab;
}
}
break;
case RightEvent.closeOthers:
// 保留目标页和Home页
tabList.value = tabList.value.filter(
(tab) => tab.name === targetTab.name || tab.name === homeTab.name
);
activeTab.value = targetTab;
break;
case RightEvent.closeAll:
tabList.value = [{ ...homeTab }]; // 假设homeTab是Home页的完整对象
activeTab.value = { ...homeTab };
break;
}
};
这样就实现了标签页和菜单的所有联动信息.
到此, 页面布局, 菜单和标签基本所大功告成了.
浙公网安备 33010602011771号