el-menu:浩瀚无垠

el-menu:浩瀚无垠

0. 缘起

遇到了一个需要展现多级列表的需求,之前写的版本就是我嗯写死的二级菜单,现在想来个Updated版本,所以需要我这里调整。抄了组长整的无极列表组件,不过我还需要对项目兼容进行一些调整。

1. v-if与v-for的循环产生el-menu-item与el-submenu

这个版本的可以实现想要的无极效果,不过时不时element.js爆个堆栈溢出的错误,看的那叫个不爽啊。组长表示可以用高端的函数式组件,直接生成对应render模板就好。事实上那个方法确实比这个版本的好很多~~

Element NavMenu 无限级菜单 - tianyawhl的个人空间 - OSCHINA - 中文开源技术交流社区

2. 函数式组件

函数式组件,我们可以理解为没有内部状态,没有生命周期钩子函数,没有 this(不需要实例化的组件)

Vue JSX、自定义 v-model - 你的美好,我都记得 (ainyi.com)

2.1 何时用到函数式组件?

在作为包装组件时它们也同样非常有用。比如,当你需要做这些时:

  • 程序化地在多个组件中选择一个来代为渲染;
  • 在将 childrenpropsdata 传递给子组件之前操作它们。

2.2 数据传输

组件需要的一切都是通过 context 参数传递,它是一个包括如下字段的对象:

  • props:提供所有 prop 的对象
  • children:VNode 子节点的数组
  • slots:一个函数,返回了包含所有插槽的对象
  • scopedSlots:(2.6.0+) 一个暴露传入的作用域插槽的对象。也以函数形式暴露普通插槽。
  • data:传递给组件的整个数据对象,作为 createElement 的第二个参数传入组件
  • parent:对父组件的引用
  • listeners:(2.3.0+) 一个包含了所有父组件为当前组件注册的事件监听器的对象。这是 data.on 的一个别名。
  • injections:(2.3.0+) 如果使用了 inject 选项,则该对象包含了应当被注入的 property。

渲染函数 & JSX — Vue.js (vuejs.org)

2.3 封装组件

<script>
import Vue from "vue";
import "element-ui/lib/theme-chalk/index.css";

import { Menu, MenuItem, Submenu, Icon } from "element-ui";

Vue.use(Menu).use(MenuItem).use(Submenu).use(Icon);

/// 渲染导航栏
const renderNavBar = (h, context) => {
  // routers格式自定义,menu中有description标题文字,hidden是否隐藏
  // ATTENTION: 注意下方routerChildren是内部子路由,根据项目修改
  // option是传入的样式,这里的不全,只是定义透明背景与文字颜色
  // 还需要加入全局menu样式,来控制hover与focus时候的背景色(直接用了!important)
  const { routers, option, maxLength, className, id } = context.props;
  let routersCopy = JSON.parse(JSON.stringify(routers));
  if (routers.length > maxLength) {
    let emptyRoute = {
      path: "",
      name: "emptyRoute",
      children: routersCopy.slice(maxLength, routers.length),
    };
    // 隐藏路由黏在正常显示的后面
    routersCopy = [...routersCopy.slice(0, maxLength), emptyRoute];
  }
  return h(
    "el-menu",
    {
      class: {
        "navbar-menu": true,

        [`${className}`]: true,
      },
      attrs: {
        id: id,
      },
      props: {
        mode: option.mode || "horizontal",
        router: true,
        backgroundColor: option.backgroundColor || "#545c64",
        activeTextColor: option.activeTextColor || "#ffd04b",
        textColor: option.textColor || "#fff",
        uniqueOpened: option.uniqueOpened || false,
      },
    },
    (routersCopy || [])
      .map((item) => {
        // 大菜单完了,到小菜单的生成
        return renderChildItem(h, item, context);
      })
      .filter((it) => it)
  );
};

/// 渲染子菜单
const renderChildItem = (h, item, context, popperClass = "menu-popup") => {

  // popper-class是element中对el-menu下拉出现的选项框的自定义类名
  // const { popperClass } = context.props;
  let haveRouterChildren =
    Array.isArray(item.routerChildren) && item.routerChildren.length;
    // 将有路由节点的且非隐藏情况的显示
  if (haveRouterChildren && !item.meta?.hidden) {
    return !(item.meta && item.meta.hidden)
      ? h(
          "el-submenu",
          {
            props: {
              // el-menu的路由模式,根据index跳转对应地址,这里item.path例如'/home'
              index: item.path || "",
              popperClass: popperClass,
            },
            class: {
              "empty-menu": item.name === "emptyRoute",

            },
          },
          [
            h(
              "template",
              {
                slot: "title",
              },
              [
                item.meta && item.meta.icon
                // 存在Icon时显示对应图标
                  ? h("em", { class: { [item.meta.icon]: true } }, "")
                  : null,
                h(
                  "span",
                  { slot: "title" },
                  // 显示标题span
                  item.meta && item.meta.description
                    ? item.meta.description
                    : ""
                ),
              ].filter((it) => it)
            ),
            // 子路由进行下一次循环
            ...item.routerChildren.map((child) => {
              return renderChildItem(h, child, context);
            }),
          ]
        )
      : null;
  } else {
    // !(item.meta && item.meta.hidden)
    //   ?
    return h(
      "el-menu-item",
      {
        props: {
          index: `${item.path || ""}`,
        },
      },
      [
        item.meta && item.meta.icon
          ? h("em", { class: { [item.meta.icon]: true } }, "")
          : null,
        h(
          "span",
          { slot: "title" },
          item.meta && item.meta.description ? item.meta.description : ""
        ),
      ].filter((it) => it)
    );
    // : null;
  }
};

export default {
  name: "MenuBar",

  functional: true,
  props: {
    id: {
      type: String,
      default: () => "",
    },
    className: {
      type: String,
      default: () => "",
    },
    // 子菜单弹窗类
    popperClass: {
      type: String,
      default: () => "",
    },
    // 路由列表
    routers: {
      type: Array,
      default: () => [],
    },
    // el-menu option
    option: {
      type: Object,
      default: () => {
        return {};
      },
    },
    // 允许展示的导航数最大值
    maxLength: {
      type: Number,
      default: () => 5,
    },
  },
  render(createElement, context) {
    return renderNavBar(createElement, context);
  },
};
</script>

<style lang="scss">
.navbar-menu {

  min-height: 76px;

  .empty-menu {

    .el-icon-arrow-down {
      font-size: 20px;
      transform: rotate(-90deg);
    }

    .el-icon-arrow-down:before {
      content: "\e7a9";
    }

    .el-submenu__title {
      padding: 0 10px 0 0;
      text-align: center;
    }

    &.is-opened {
      .el-submenu__title .el-icon-arrow-down {
        transform: rotate(-270deg);
      }
    }
  }
}
</style>

2.4 使用组件

注意这里需要将扁平结构转化为树形

JS树结构操作:查找、遍历、筛选、树结构和列表结构相互转换 - 沐码小站 (wintc.top)

   routers: {
      get() {
        let routers = this.$store.state.routers.map((key) => ({
          name: key.router.toUpperCase(),
          path: "/" + key.router,
          id: key.uiWebId,
          parentId: key.parentId,
          meta: {
            hidden: false,
            link: true,
            description: key.menuName,
          },
        }));
        let transferRouters = convertMenus(routers, "parentId");
        console.log('transferRouters: ', transferRouters);
        return transferRouters;

        function convertMenus(list, pid = "pid", id = "id") {
          let info = list.reduce(
            (map, node) => (
              (map[node[`${id}`]] = node), (node.routerChildren = []), map
            ),
            {}
          );
          return list.filter((node) => {
            info[node[`${pid}`]] &&
              info[node[`${pid}`]].routerChildren.push(node);
            return !node[`${pid}`];
          });
        }
      },
    },

2.5 补充的样式代码

CSS苦手,写这个真的头皮发麻,所以用了! important,丑陋至极!

.frame_window_out {

  .el-menu-demo {
    margin-left: 34%;
  }

  .el-menu.el-menu--horizontal {
    border: 0;
  }

  .el-submenu:focus .el-submenu__title,
  .el-submenu:hover .el-submenu__title {
    color: #ffffff;
    background-color: transparent;
  }

  .el-menu {
    background-color: transparent;

    .el-submenu:focus .el-submenu__title,
    .el-submenu:hover .el-submenu__title {
      background-color: rgba(23, 78, 112, 0.7) !important;
    }
    .el-menu-item,
    .el-submenu .el-submenu__title {
      color: #ffffff;
      height: 40px;
      margin: 8px;
      border-radius: 4px;
      line-height: 40px;

      i {
        color: #ffffff;
      }
    }

    .el-menu-item:hover,
    .el-menu-item:focus {
      background-color: transparent !important;
      background-position: center;
    }

    .el-menu-item-group {
      .el-menu-item-group__title {
        color: #ffffff;
      }
    }

    .el-menu--horizontal .el-menu-item:not(.is-disabled):focus,
    .el-menu--horizontal .el-menu-item:not(.is-disabled):hover {
      background-color: #088dc1;
    }
  }
}

.menu-popup .el-menu--popup .el-menu-item {

  background-color: rgba(23, 78, 112, 0.7) !important;
  &:hover,
  &:focus {
    background-color: rgba(16, 130, 201, 0.7) !important;
  }
}

3. JSX

在Vue中使用JSX,很easy的 - 掘金 (juejin.cn)

Vue JSX、自定义 v-model - 你的美好,我都记得 (ainyi.com)

posted @ 2022-03-25 18:23  乐盘游  阅读(553)  评论(0编辑  收藏  举报