Vue3边学边做系列(5)--布局切换&菜单事件&标签页 - 实践

Vue3边学边做系列(5)–布局切换&菜单事件&标签页

前端开发系列(1)-开发环境安装
前端开发系列(2)-项目框架搭建
Vue3边学边做系列(3)-路由缓存接口封装
vue3边学边做系列(4)-工具条&系统设置&动态菜单

本期效果图

在这里插入图片描述

本期实现的功能:

  • 布局的优化
  • 菜单的动态生成
  • 菜单的事件处理
  • 标签页的设置

1. 布局的优化

顶部菜单的布局:

在这里插入图片描述

左侧菜单布局1:

在这里插入图片描述

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

在这里插入图片描述

混合布局:

在这里插入图片描述

每种布局通过动态组件生成.

2. 动态菜单生成

菜单组件进一步优化.

(1) 作为递归调用的菜单组件:

src/layouts/menus/MenuItem.vue


<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之外, 还需要支持右键菜单事件, 右键事件放到标签模版中.

    右键菜单包含: 关闭当前, 关闭左侧, 关闭右侧, 关闭其他, 关闭所有.

  • 单击事件

    需要激活对应的标签, 并且标签和菜单需要联动起来, 所以这里使用了菜单中的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;
      }
    };

这样就实现了标签页和菜单的所有联动信息.

到此, 页面布局, 菜单和标签基本所大功告成了.

posted on 2025-11-14 19:04  slgkaifa  阅读(0)  评论(0)    收藏  举报

导航