界面正在加载中,请稍后

Antd proLayout更改菜单配置(Vue)

JarryChenJarryChen 发布时间:2020-11-30 文章字数:1596 预计用时:7分59秒

  • 近期在项目开发过程中,由于内容过多及原页面菜单臃肿,故在此需求下,需要一个好的菜单显示方式,即不直接使用ant-sub-menu来处理。
  • 又由于使用了proLayout来做基本布局,不想放弃已有的切换导航的功能,且加上插槽无法改到顶部的样式,故退而求其次,拉取proLayout未打包过的代码进行一个"魔改"

# 1. 拉取 proLayout 代码

git clone https://github.com/vueComponent/pro-layout
// 然后找到里面的src占到项目中,整个一起带过来,防止缺少啥而带来的难以调试的bug

# 2. 修改主要的代码

  • 由于我们只需要修改菜单的样式和响应方式,所以只需修改RouteMenu下的BaseMenu.jsx。因为本人不太习惯jsx,所以是改成了vue template语法进行一个处理,具体代码如下:

  • 源代码BaseMenu.jsx

import PropTypes from "ant-design-vue/es/_util/vue-types";

import "ant-design-vue/es/menu/style";
import Menu from "ant-design-vue/es/menu";
import "ant-design-vue/es/icon/style";
import Icon from "ant-design-vue/es/icon";

const { Item: MenuItem, SubMenu } = Menu;

export const RouteMenuProps = {
  menus: PropTypes.array,
  theme: PropTypes.string.def("dark"),
  mode: PropTypes.string.def("inline"),
  collapsed: PropTypes.bool.def(false),
  i18nRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]).def(false),
};

const httpReg = /(http|https|ftp):\/\/([\w.]+\/?)\S*/;

const renderMenu = (h, item, i18nRender) => {
  if (item && !item.hidden) {
    const bool = item.children && !item.hideChildrenInMenu;
    return bool
      ? renderSubMenu(h, item, i18nRender)
      : renderMenuItem(h, item, i18nRender);
  }
  return null;
};

const renderSubMenu = (h, item, i18nRender) => {
  return (
    <SubMenu
      key={item.path}
      title={
        <span>
          {" "}
          {renderIcon(h, item.meta.icon)} <span>
            {" "}
            {renderTitle(h, item.meta.title, i18nRender)}{" "}
          </span>{" "}
        </span>
      }
    >
      {" "}
      {!item.hideChildrenInMenu &&
        item.children.map((cd) => renderMenu(h, cd, i18nRender))}{" "}
    </SubMenu>
  );
};

const renderMenuItem = (h, item, i18nRender) => {
  const meta = Object.assign({}, item.meta);
  const target = meta.target || null;
  const hasRemoteUrl = httpReg.test(item.path);
  const CustomTag = (target && "a") || "router-link";
  const props = {
    to: {
      name: item.name,
    },
  };
  const attrs =
    hasRemoteUrl || target
      ? {
          href: item.path,
          target: target,
        }
      : {};
  if (item.children && item.hideChildrenInMenu) {
    // 把有子菜单的 并且 父菜单是要隐藏子菜单的
    // 都给子菜单增加一个 hidden 属性
    // 用来给刷新页面时, selectedKeys 做控制用
    item.children.forEach((cd) => {
      cd.meta = Object.assign(cd.meta || {}, {
        hidden: true,
      });
    });
  }
  return (
    <MenuItem key={item.path}>
      <CustomTag
        {...{
          props,
          attrs,
        }}
      >
        {" "}
        {renderIcon(h, meta.icon)} {renderTitle(h, meta.title, i18nRender)}{" "}
      </CustomTag>{" "}
    </MenuItem>
  );
};

const renderIcon = (h, icon) => {
  if (icon === undefined || icon === "none" || icon === null) {
    return null;
  }
  const props = {};
  typeof icon === "object" ? (props.component = icon) : (props.type = icon);
  return (
    <Icon
      {...{
        props,
      }}
    />
  );
};

const renderTitle = (h, title, i18nRender) => {
  return <span> {(i18nRender && i18nRender(title)) || title} </span>;
};

const RouteMenu = {
  name: "RouteMenu",
  props: RouteMenuProps,
  data() {
    return {
      openKeys: [],
      selectedKeys: [],
      cachedOpenKeys: [],
      cachedSelectedKeys: [],
    };
  },
  render(h) {
    const { mode, theme, menus, i18nRender } = this;
    const handleOpenChange = (openKeys) => {
      // 在水平模式下时,不再执行后续
      if (mode === "horizontal") {
        this.openKeys = openKeys;
        return;
      }
      const latestOpenKey = openKeys.find(
        (key) => !this.openKeys.includes(key)
      );
      if (!this.rootSubmenuKeys.includes(latestOpenKey)) {
        this.openKeys = openKeys;
      } else {
        this.openKeys = latestOpenKey ? [latestOpenKey] : [];
      }
    };

    const dynamicProps = {
      props: {
        mode,
        theme,
        openKeys: this.openKeys,
        selectedKeys: this.selectedKeys,
      },
      on: {
        select: (menu) => {
          this.$emit("select", menu);
          if (!httpReg.test(menu.key)) {
            this.selectedKeys = menu.selectedKeys;
          }
        },
        openChange: handleOpenChange,
      },
    };

    const menuItems = menus.map((item) => {
      if (item.hidden) {
        return null;
      }
      return renderMenu(h, item, i18nRender);
    });
    return <Menu {...dynamicProps}> {menuItems} </Menu>;
  },
  methods: {
    updateMenu() {
      const routes = this.$route.matched.concat();
      const { hidden } = this.$route.meta;
      if (routes.length >= 3 && hidden) {
        routes.pop();
        this.selectedKeys = [routes[routes.length - 1].path];
      } else {
        this.selectedKeys = [routes.pop().path];
      }
      const openKeys = [];
      if (this.mode === "inline") {
        routes.forEach((item) => {
          item.path && openKeys.push(item.path);
        });
      }

      this.collapsed
        ? (this.cachedOpenKeys = openKeys)
        : (this.openKeys = openKeys);
    },
  },
  computed: {
    rootSubmenuKeys: (vm) => {
      const keys = [];
      vm.menus.forEach((item) => keys.push(item.path));
      return keys;
    },
  },
  created() {
    this.$watch("$route", () => {
      this.updateMenu();
    });
    this.$watch("collapsed", (val) => {
      if (val) {
        this.cachedOpenKeys = this.openKeys.concat();
        this.openKeys = [];
      } else {
        this.openKeys = this.cachedOpenKeys;
      }
    });
  },
  mounted() {
    this.updateMenu();
  },
};

export default RouteMenu;
  • 我修改后的代码BaseMenu.vue
<!-- eslint-disable -->
<!-- 这个文件原js部分除了html render部分修改,其他保持不变-->
<template>
  <a-menu
    v-bind="{ mode, theme, openKeys, selectedKeys }"
    @select="selectMenus"
    @openChange="(keys) => handleOpenChange(keys, mode)"
    class="my-topmenu"
    tabindex="0"
    @blur.native="openKeys = rootSubmenuKeys"
  >
    <template v-for="item in menus">
      <a-sub-menu :key="item.path">
        <template slot="title">
          <a-icon v-if="item.meta" :type="item.meta.icon" /><span
            >{{ i18nRender(item.meta.title) }}</span
          ></template
        >
        <!--水平模式-->
        <template v-if="mode === 'horizontal'">
          <MySubMenu
            :i18n="i18nRender"
            :submenu="item.children"
            :mode="mode"
            :collapsed="collapsed"
          />
        </template>
        <!--竖直模式-->
        <template v-else>
          <template v-for="subItem in item.children">
            <a-menu-item
              :key="subItem.path"
              v-if="!subItem.children || subItem.children.length === 0"
            >
              <router-link :to="subItem.path">
                <a-icon v-if="subItem.meta" :type="subItem.meta.icon" />
                {{ subItem.meta ? i18nRender(subItem.meta.title) : ''
                }}</router-link
              >
            </a-menu-item>
            <a-sub-menu v-else :key="subItem.path">
              <template slot="title">
                <a-icon v-if="subItem.meta" :type="subItem.meta.icon" /><span
                  >{{ i18nRender(subItem.meta.title) }}</span
                ></template
              >
              <MySubMenu
                :i18n="i18nRender"
                :submenu="subItem.children"
                :mode="mode"
                :collapsed="collapsed"
              />
            </a-sub-menu>
          </template>
        </template>
      </a-sub-menu>
    </template>
  </a-menu>
</template>

<script>
  /* eslint-disable*/
  import PropTypes from "ant-design-vue/es/_util/vue-types";
  import MySubMenu from "../MySubMenu/SubMenu.vue";
  import $ from "jquery";
  const RouteMenuProps = {
    menus: PropTypes.array,
    theme: PropTypes.string.def("dark"),
    mode: PropTypes.string.def("inline"),
    collapsed: PropTypes.bool.def(false),
    i18nRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]).def(
      false
    ),
  };

  export default {
    name: "RouteMenu",
    props: RouteMenuProps,
    components: {
      MySubMenu,
    },
    data() {
      return {
        openKeys: [],
        selectedKeys: [],
        cachedOpenKeys: [],
      };
    },
    computed: {
      rootSubmenuKeys: (vm) => {
        const keys = [];
        vm.menus.forEach((item) => keys.push(item.path));
        return keys;
      },
    },
    methods: {
      updateMenu() {
        const routes = this.$route.matched.concat();
        const { hidden } = this.$route.meta;
        if (routes.length >= 3 && hidden) {
          routes.pop();
          this.selectedKeys = [routes[routes.length - 1].path];
        } else {
          this.selectedKeys = [routes.pop().path];
        }
        let openKeys = [];
        if (this.mode === "inline") {
          openKeys = this.rootSubmenuKeys;
        }

        if (this.collapsed) {
          this.cachedOpenKeys = openKeys;
        }
        this.openKeys = openKeys;
      },
      handleOpenChange(openKeys, mode) {
        // 在水平模式下时,不再执行后续
        if (mode === "horizontal") {
          console.log(openKeys);
          this.openKeys = openKeys;
          return;
        }
        const latestOpenKey = openKeys.find(
          (key) => !this.openKeys.includes(key)
        );

        if (!this.rootSubmenuKeys.includes(latestOpenKey) && openKeys.length) {
          this.openKeys = [openKeys[0], latestOpenKey].filter(Boolean);
        } else {
          this.openKeys = latestOpenKey ? [latestOpenKey] : [];
        }
      },
      selectMenus(menu) {
        this.selectedKeys = menu?.selectedKeys;
        this.$emit("select", menu);
      },
    },
    watch: {
      $route() {
        this.updateMenu();
      },
      collapsed(val) {
        if (val) {
          this.cachedOpenKeys = this.openKeys.concat();
          this.openKeys = [];
        } else {
          this.openKeys = this.cachedOpenKeys;
        }
      },
    },
    mounted() {
      this.updateMenu();
    },
  };
</script>

<style lang="less">
  /* 这段样式的目的,在于让顶部时菜单出来别抖动和别有额外的因为定位造成的延误动画 */
  @import "../SiderMenu/index.less";
  .ant-menu-submenu-popup.ant-menu-submenu-placement-bottomLeft {
    position: fixed !important;
    left: 10px !important;
    top: @nav-header-height + 5 !important;
    right: 20px !important;
    width: 100% !important;
    background-color: transparent !important;
    transform: translateZ(0);
  }
</style>

# 3. 添加自定义的二三级菜单

  • 按照原来的做法,ant-sub-menu很难改成我们那种复杂菜单的样式,所以考虑直接用ant-sub-menu做个外壳套着我们的组件进行响应,具体代码如下:

  • 我的子菜单SubMenu.vue

<!--这里区分顶部和侧边的情况,顶部是二级三级在我自定义的里面,侧边的话二级还是原来下拉,只不过三级在面板上-->
<template>
  <div
    class="my-submenu-item"
    :class="{ [$attrs.mode]: true, collapsed: $attrs.collapsed }"
  >
    <div class="two-level-menu" v-if="$attrs.mode === 'horizontal'">
      <ul class="menu-list">
        <li
          v-for="(route, index) in submenu"
          :key="route.path"
          @mouseenter="showDeepMenu(route, index)"
          :class="index === showIndex ? 'choose' : undefined"
        >
          <component
            :is="route.children ? 'span' : 'router-link'"
            :to="route.children ? undefined : route.path"
            :title="i18n(route.meta.title)"
          >
            {{ route.meta ? i18n(route.meta.title) : '' }}</component
          >
          <a-divider />
        </li>
      </ul>
    </div>
    <div class="deep-menu">
      <header>
        <a-input @change="searchRoute" v-model="searchValue">
          <a-icon type="search" slot="prefix" />
        </a-input>
      </header>
      <section>
        <template v-if="showWichRouteShow.length">
          <ul class="deep-menu-list">
            <li v-for="route in showWichRouteShow" :key="route.path">
              <router-link
                :to="route.path"
                :title="i18n(route.meta.title)"
                :class="{ now: $route.path === route.path }"
              >
                {{ route.meta ? i18n(route.meta.title) : '' }}
              </router-link>
            </li>
          </ul>
        </template>
        <template v-else>
          <p class="no-route">——————&emsp;暂无数据&emsp;——————</p>
        </template>
      </section>
    </div>
  </div>
</template>

<script>
  /* eslint-disable*/
  export default {
    name: "MySubMenu",
    props: {
      submenu: {
        type: [Array, Object],
        required: true,
      },
      i18n: {
        type: Function,
      },
    },
    data() {
      let showWichRoute = [];
      if (this.$attrs.mode === "horizontal") {
        showWichRoute = this.submenu[0].children.filter((item) => !item.hidden);
      } else {
        showWichRoute = JSON.parse(JSON.stringify(this.submenu));
        showWichRoute = showWichRoute.filter((item) => !item.hidden);
      }
      return {
        showWichRoute,
        showIndex: 0,
        pageSize: Math.floor(showWichRoute.length / 4),
        showWichRouteShow: JSON.parse(JSON.stringify(showWichRoute)),
        searchValue: "",
        debounce: null,
      };
    },
    methods: {
      showDeepMenu(route, index) {
        this.showWichRoute = route?.children || this.showWichRoute;
        if (this.showIndex !== index) {
          this.searchValue = "";
        }
        this.showIndex = index;
        this.showWichRouteShow = JSON.parse(JSON.stringify(this.showWichRoute));
        this.showWichRouteShow = this.showWichRouteShow.filter(
          (item) => !item.hidden
        );
        this.pageSize = Math.floor(this.showWichRouteShow.length / 4);
      },
      arrayEnd(index) {
        if (this.pageSize === 0) return this.showWichRouteShow.length;
        else return index * this.pageSize;
      },
      searchRoute() {
        if (this.debounce) {
          clearTimeout(this.debounce);
        }
        this.debounce = setTimeout(() => {
          if (this.searchValue.trim().length > 0) {
            this.showWichRouteShow = this.showWichRoute.filter((item) => {
              return (
                item.meta?.title &&
                this.i18n(item.meta.title)
                  .toLowerCase()
                  .includes(this.searchValue.toLowerCase()) &&
                !item.hidden
              );
            });
          } else {
            this.showWichRouteShow = JSON.parse(
              JSON.stringify(this.showWichRoute)
            );
            this.showWichRouteShow = this.showWichRouteShow.filter(
              (item) => !item.hidden
            );
          }
        }, 1000);
      },
    },
    beforeDestroy() {
      clearTimeout(this.debounce);
      this.debounce = null;
      this.searchValue = "";
    },
  };
</script>

<style lang="less" src="./index.less" />
  • 对应的样式代码比较多,另立文件如下
@import "../SiderMenu/index.less";

.my-submenu-item {
  transform: translateZ(0);
  position: fixed;
  display: flex;
  cursor: default;
  right: 20px;
  background-color: rgba(0, 47, 91, 0.25);
  // transition            : all 0.3s;
  z-index: 9999;
  box-shadow: 0px 0px 20px 0px rgba(41, 55, 129, 0.25);

  &.inline {
    left: 260px;
    top: @nav-header-height + 5;

    &.collapsed {
      left: 243px;
    }
  }

  &.horizontal {
    left: 10px;
    top: 0;
  }

  a {
    text-decoration: none;
    color: #fff;
    cursor: pointer;
  }

  .two-level-menu {
    width: 12%;
    min-width: 180px;
    overflow: hidden auto;
    background-color: #5875a1;

    .menu-list {
      margin: 0;
      padding: 0;
      padding-top: 60px;
      outline: none;

      li {
        line-height: 52px;
        color: #fff;
        height: 52px;
        font-size: 16px;
        font-weight: normal;
        padding: 0 40px;
        cursor: pointer;
        position: relative;
        display: flex;
        flex-direction: column;
        transition: all 0.3s;

        span {
          display: inline-block;
          width: 100%;
        }

        &.choose {
          background-color: #fff;

          a {
            color: #1890ff;
          }

          span {
            color: #1890ff;
          }

          .ant-divider.ant-divider-horizontal {
            min-width: calc(100% - 40px);
          }
        }

        .ant-divider.ant-divider-horizontal {
          position: absolute;
          display: inline-block;
          top: auto;
          bottom: 5px;
          height: 4px;
          left: 20px;
          margin: 0 auto;
          min-width: 0;
          width: 0;
          background-color: @primary-color;
          border-radius: 8px;
          transition: all 0.5s;
        }

        &:hover {
          .ant-divider.ant-divider-horizontal {
            min-width: calc(100% - 40px);
          }
        }
      }
    }
  }

  .deep-menu {
    flex: 1;
    display: flex;
    flex-direction: column;
    transition: all 0.3s;
    background-color: #fff;

    header {
      padding: 20px 20px 10px;

      .ant-input-affix-wrapper {
        height: 40px;

        .ant-input-prefix {
          color: #1890ff;
          left: 0;
          width: 35px;

          i {
            padding-top: 5px;
            line-height: 40px;
            font-size: 16px;
            margin: 0 auto;
          }
        }

        .ant-input {
          padding-left: 35px;
          border-color: #f0f2f5;
          background-color: #f0f2f5;
          border-radius: 4px;
          line-height: 40px;
          height: 40px;
        }
      }
    }

    section {
      flex: 1;
      overflow: hidden auto;
      padding: 10px 20px 0;

      a {
        color: black;
      }

      .deep-menu-list {
        margin: 0 auto;
        padding: 0 10px;
        display: flex;
        flex-flow: row wrap;

        li {
          line-height: 35px;
          font-size: 13px;
          flex: 0 0 calc(20% - 20px);
          margin: 0 10px;

          a {
            color: #5f5f5f;

            &:hover {
              color: @primary-color;
              text-decoration: underline;
            }

            &.now {
              color: @primary-color;
              text-decoration: underline;
            }
          }
        }
      }

      .no-route {
        text-align: center;
        color: #d9d9d9;
        font-size: 16px;
        margin: 20px auto;
      }
    }
  }
}
  • 得到的具体结果如下图:

鸣谢

proLayout 是一个集成了多种功能的标准管理页面布局,感谢作者github