基于蚂蚁金服"AntDesignVue-Menu导航菜单"实现根据初始路由自动选中对应菜单解决刷新后菜单选择状态丢失问题(支持根路径菜单)

基于Ant Design Vue实现根据初始路由自动选定对应菜单,其实NG-ZORRO(AntDesign Angular版本)官方已经做了原生实现,但是VUE版本需要自己实现。

功能:

  • 刷新或直接打开某路径时,自动根据路由选中当前路由对应的菜单
  • 支持根目录(/)
  • 匹配规则可以自定义,即使同样的path下对应多个菜单也可以,总之,只要Vue路由能正确导航就能正确匹配(判断是否选中时使用模拟导航)

注意:

  • 由于vue2和vue3版本有差异,并且同一份代码兼容两个版本会使得代码冗余且难维护,因此争对两个版本的vue分别提供
  • 由于逻辑复杂故采用TypeScript做类型声明,如果没有使用TypeScript的项目,可以把类容保存为".jsx"后缀,并且去除相关的类型声明即可
  • 具体部分用法参见源码最前面的文档注释部分,完整用法可以参考源码中的 MenuOption 类型。
  • 具体导航到哪里 MenuOption.routerNavigation.location(RawLocation,原生VUE路由对象)决定,也可以使用自定义事件进行导航。

源码,vue2.x版本(可去除ts类型声明后保存为.jsx) - menu.tsx

import Vue, { VNode } from 'vue'
import { RawLocation } from 'vue-router/types/router'
import { Route } from 'vue-router'

/**
 * 申明式嵌套菜单导航.
 * 官方文档: https://www.antdv.com/components/menu-cn/#API
 * <a-menu>
 *   <MenuItem>菜单项</MenuItem>
 *   <a-sub-menu title="子菜单">
 *     <MenuItem>子菜单项</MenuItem>
 *   </a-sub-menu>
 * </a-menu>
 * 根据提供的数据进行菜单渲染.
 * <p>
 *   示例:
 *   <pre><code>
 *    import Menu, { MenuOption } from '@/components/menu'
 *
 *    const menus: Array<MenuOption> = [
 *    {
 *       // 菜单名称,用于显示
 *       name: '菜单1',
 *       // [可选]用于自动导航和判断当前路由与当前菜单是否匹配,以实现刷新自动选中或菜单组自动展开(如果不需要则建议使用原生组件)
 *       routerNavigation: {
 *         // 是否自动导航,如果开启则会自动根据{@code location}值进行导航.默认开启
 *         autoNavigation: true,
 *         // 该属性类型为Vue原生导航参数类型(RawLocation)
 *         location: { name: 'Login' }
 *       }
 *     },
 *    {
 *       name: '菜单组',
 *       children: [
 *         {
 *           name: '菜单2',
 *           routerNavigation: {
 *             location: '/reg'
 *           },
 *           // 还可以为菜单项添加原生事件
 *           on: {
 *             click: () => {}
 *           }
 *         },
 *         {
 *           name: '菜单3',
 *           routerNavigation: {
 *             location: { path: '/reg2' }
 *           }
 *         }
 *       ]
 *     }
 *    ];
 *
 *    // template中使用(不要忘记在components中导入): <Menu mode="inline" :menus="menus"></Menu>
 *   </pre></code>
 *   如示例所示,只要使用时申明需要显示的菜单名字以及该菜单导航所需的对象即可(该对象默认用于导航已经自动根据当前路由进行高亮)
 *   但是可以禁用自动导航功能.并可以自定义菜单的点击事件
 *   如果不需要自动根据当前路由进行高亮和展开,请直接使用原生组件即可.
 * </p>
 * @author Laeni<m@laeni.cn>
 */
export default Vue.extend({
  name: 'Menu',
  props: {
    /**
     * 需要进行渲染的菜单,
     * 类型为{@link MenuOption}.
     */
    menus: {
      type: Array,
      required: true
    },
    /**
     * 菜单类型
     * 水平: vertical vertical-right
     * 垂直: horizontal
     * 内嵌: inline
     */
    mode: {
      type: String,
      default: ''
    },
    /**
     * 主题.
     * 详情参考官网: https://www.antdv.com/components/menu-cn/#components-menu-demo-menu-themes
     * light dark
     */
    theme: {
      type: String,
      default: ''
    },
    /**
     * 事件.
     * 如
     * {
     *   click: () => {},
     * }
     */
    on: {
      type: Object,
      default: () => {}
    },
  },
  data(): {
    // this.menus的扩展,方便使用
    menuOptionLocals: Array<MenuOptionLocal>,
    // 菜单展开的分组
    openKeys: Array<string>,
    // 菜单选中的菜单项
    selectedKeys: Array<string>
  } {
    return {
      menuOptionLocals: [],
      // 菜单展开的分组
      openKeys: [],
      // 菜单选中的菜单项
      selectedKeys: []
    }
  },
  methods: {
    /**
     * 初始化菜单选中状态.
     * <p>
     *   功能:
     *   <ul>
     *     <li>当路由改变时计算出路由对应的菜单,并选中</li>
     *     <li>如果选择的菜单在菜单组中,则将该组展开</li>
     *   </ul>
     * </p>
     */
    initMenu(): void {
      // 从{@code this.menuOptionLocals}中将找出与当前路由相符的选项
      const menuOptionLocal: MenuOptionLocal | null = this.findMenuOptionLocal(this.menuOptionLocals, this.$route);

      if (menuOptionLocal !== null) {
        // 设置当前选择的key
        this.selectedKeys = [menuOptionLocal.key];
        // 依次将当前菜单所在的父菜单展开
        this.openKeying(menuOptionLocal);
      }
    },

    /**
     * 渲染菜单项或菜单组.
     * @param menu 单个MenuOption类型的对象
     */
    renderMenu(menu: MenuOptionLocal): VNode {
      if (menu.children && menu.children.length > 0) {
        return this.renderSubMenu(menu);
      } else {
        return this.renderMenuItem(menu);
      }
    },
    // 渲染菜单组
    renderSubMenu(menu: MenuOptionLocal): VNode {
      const props = {
        key: menu.key,
        title: menu.name,
      }
      return (
        <a-sub-menu key={ menu.key } {...{ props }}>
          { menu.children && menu.children.map(v => this.renderMenu(v)) }
        </a-sub-menu>
      );
    },
    // 渲染菜单项
    renderMenuItem(menu: MenuOptionLocal): VNode {
      const props = {
        disabled: menu.disabled
      }
      const on = {
        ...menu.on,
        // 注入点击事件
        click: () => {
          // 原始事件
          if (menu.on && typeof menu.on.click === 'function') {
            menu.on.click();
          }

          // 如果配置了自动导航,则根据配置规则进行点击时自动导航
          if (menu.routerNavigation && (menu.routerNavigation.autoNavigation || menu.routerNavigation.autoNavigation === undefined)) {
            // 导航到指定路由(如果当前路由处于目标路由则不进行导航)
            if (this.$router.resolve(menu.routerNavigation.location).route.fullPath !== this.$route.fullPath) {
              this.$router.push(menu.routerNavigation.location);
            }
          }
        }
      }
      return (
        <a-menu-item key={ menu.key } {...{ props, on }}>
          { menu.name }
        </a-menu-item>
      );
    },

    /*region private*/
    /**
     * 根据菜单的层次结构自动生成该菜单下唯一的 key.
     * 添加该元素的父元素(如果有).
     * @param menus  需要生产key则菜单.
     * @param parent [递归调用时使用]父节点
     */
    toMenuOptionLocal(menus: Array<MenuOption>, parent?: MenuOptionLocal): Array<MenuOptionLocal> {
      // 默认key前缀,如果父元素为空则使用,用于父元素
      const defaultPrefix = 'k';

      const menuLocals: Array<MenuOptionLocal> = [];

      menus.forEach((v, i) => {
        const menuLocal: MenuOptionLocal = {
          ...v,
          children: undefined,
          key: (parent ? parent.key : defaultPrefix) + '-' + i,
          parent: parent,
        };

        // 生成子节点的key
        if (v.children && v.children.length > 0) {
          menuLocal.children = this.toMenuOptionLocal(v.children, menuLocal);
        }
        // 如果可以导航则生成导航对象
        if (v.routerNavigation && v.routerNavigation.location) {
          menuLocal.route = this.$router.resolve(v.routerNavigation.location).route;
        }

        menuLocals.push(menuLocal);
      })

      return menuLocals;
    },
    /**
     * 从{@code this.menuOptionLocals}中将找出与当前路由相符的选项.
     * @param menuOptionLocals 菜单选项
     * @param route            当前路由对象
     * @return MenuOptionLocal 如果没有找到则返回null
     */
    findMenuOptionLocal(menuOptionLocals: Array<MenuOptionLocal>, route: Route): MenuOptionLocal | null {
      // 从节点树中找出所有符合要求的节点
      const menuOptionLocalAll: Array<MenuOptionLocal> = this.findMenuOptionLocalAll(menuOptionLocals, route);

      // 从所有符合要求的节点中找出优先级最高的一个节点(如"/xxx"优先级高于"/")
      const high = this.findHighPriority(menuOptionLocalAll);

      // 对首页进行特殊处理
      if (high) {
        // 如果 high 一定不满足要求则跳过
        if (!high.routerNavigation || !high.routerNavigation.location) {
          return null;
        }

        const highRoute: Route = high.route || this.$router.resolve(high.routerNavigation.location).route;
        const homeRoute: Route = this.$router.resolve({ name: 'Home' }).route;

        if (highRoute.fullPath !== route.fullPath && highRoute.fullPath === homeRoute.fullPath) {
          return null;
        }
      }

      return high;
    },
    /**
     * 从{@code this.menuOptionLocals}中将找出与当前路由相符的<b>所有</b>选项.
     * @param menuOptionLocals        菜单选项
     * @param route                   当前路由对象
     * @return Array<MenuOptionLocal> 返回0个或多个菜单项数组
     */
    findMenuOptionLocalAll(menuOptionLocals: Array<MenuOptionLocal>, route: Route): Array<MenuOptionLocal> {
      // 用于放满足要求的选项节点
      const menuOptionLocalAll: Array<MenuOptionLocal> = [];

      for (const menu of menuOptionLocals) {
        // 如果有子元素则先找子元素
        if (menu.children && menu.children.length > 0) {
          const menuOptionLocal: MenuOptionLocal | null = this.findMenuOptionLocal(menu.children, route);
          if (menuOptionLocal) {
            menuOptionLocalAll.push(menuOptionLocal);
          }
        }
        // 如果不是子元素则判断当前元素是否为目标元素
        else if (menu.routerNavigation && menu.routerNavigation.location) {
          // 利用Vue模拟解析生成一个路由对象
          const resolveRoute: Route = menu.route || this.$router.resolve(menu.routerNavigation.location).route;
          // 通过比较{@link Route#path}和{@link Route#query}以判断该菜单是否为目标菜单
          if (route.path.indexOf(resolveRoute.path) > -1) {
            // 定义一个变量假设本元素符合要求,如果不符合则改为false
            let ok: boolean = true;

            for (const key of Object.keys(resolveRoute.query)) {
              if (resolveRoute.query[key] && resolveRoute.query[key] !== route.query[key]) {
                ok = false;
                break;
              } else if (typeof route.query[key] === 'undefined') {
                ok = false;
                break;
              }
            }

            if (ok) {
              menuOptionLocalAll.push(menu);
            }
          }
        }
      }

      return menuOptionLocalAll;
    },
    // 从所有符合要求的节点中找出优先级最高的一个节点(如"/xxx"优先级高于"/")
    findHighPriority(menuOptionLocals: Array<MenuOptionLocal>): MenuOptionLocal | null {
      if (menuOptionLocals.length === 0) {
        return null;
      }
      // 假定第一个元素优先级最高
      let high: MenuOptionLocal = menuOptionLocals[0];

      // 通过path进行比较
      for (let i = 1; i < menuOptionLocals.length; i++) {
        const menu = menuOptionLocals[i];
        // 如果 high 或 menu 一定不满足要求则跳过
        if (!high.routerNavigation || !high.routerNavigation.location || !menu.routerNavigation || !menu.routerNavigation.location) {
          high = menu;
          continue;
        }

        // 利用Vue模拟解析生成一个路由对象
        const highRoute: Route = high.route || this.$router.resolve(high.routerNavigation.location).route;
        const menuRoute: Route = menu.route || this.$router.resolve(menu.routerNavigation.location).route;
        // 优先级比较
        if (menuRoute.path.indexOf(highRoute.path) > -1) {
          high = menu;
        }
      }

      return high;
    },
    /**
     * 依次将当前菜单所在的父菜单展开.
     * @param menuOptionLocal 当前url导航对应的菜单.
     */
    openKeying(menuOptionLocal: MenuOptionLocal): void {
      if (menuOptionLocal.parent) {
        this.openKeys.push(menuOptionLocal.parent.key);
        this.openKeying(menuOptionLocal.parent);
      }
    }
    /*endregion*/
  },
  created(): void {
    // @ts-ignore
    this.menuOptionLocals = this.toMenuOptionLocal(this.menus);
  },
  mounted(): void {
    this.initMenu();
  },
  render(): VNode {
    const props: any = {
      openKeys: this.openKeys,
      selectedKeys: this.selectedKeys,
    }
    if (this.mode) {
      props.mode = this.mode;
    }
    if (this.theme) {
      props.theme = this.theme;
    }

    const on: any = {
      // 同步展开或关闭时的标签
      openChange: (openKeys: never[]) => this.openKeys = openKeys,
      // select: (item: any) => this.selectedKeys = item.selectedKeys,
    }

    return (
      <a-menu {...{props, on}}>
        {this.menuOptionLocals.map((menu) => this.renderMenu(menu))}
      </a-menu>
    );
  },
  watch: {
    $route() {
      this.initMenu();
    },
    menus() {
      // @ts-ignore
      this.menuOptionLocals = this.toMenuOptionLocal(this.menus);
      this.initMenu();
    }
  },
})

/**
 * 菜单组或菜单项.
 * 如果{@link #children}为空则表示菜单项(父菜单),否则为菜单.
 */
export interface MenuOption {
  /**
   * 菜单或菜单项的名字.
   */
  name: string;
  /**
   * 路由导航规则,为菜单项(children 未定义 或 children.length === 0)时有效.
   * 根据路由动态选中该菜单,可以根据该定义的判断在当前路由下是否应该选择该菜单.
   */
  routerNavigation?: {
    /**
     * 是否根据定义的结构进行自动导航.
     * 即点击时自动导航到声明的路由中.如果在复杂条件不能自动导航时,可禁用,并且添加{@link MenuOption#on}相关事件进行自定义导航.
     * 如"/xxx?a=1&b=2"中,仅仅依据path"/xxx"和查询参数"a=1"作为是否选中的判断依据,但是查询参数"b=2"不作为判断依据时,
     * 应该在这里不定义查询参数"b=2"这个条件,所以不能自动获取并传递该参数,这个时候应该禁用自动导航.
     */
    autoNavigation?: boolean;
    /**
     * Vue路由导航参数.其作用最多有两个.
     * 1.点击时自动导航({@link #autoNavigation}为{@code undefined}或为{@code true}时有效.)
     * 2.自动根据其定义的类容判断是否应该选中该菜单.
     */
    location: RawLocation;
  };
  /**
   * 子菜单组或者子菜单项.
   * 如果为空则本菜单表示菜单组(父菜单),否则为菜单项
   */
  children?: Array<MenuOption>;
  /**
   * 菜单或菜单项logo图标(TODO 暂未实现, 可为框架的图标名或阿里矢量图标库的名称).
   */
  logo?: string;
  /**
   * Menu.Item事件(html原生事件),为菜单项(children.length > 0)时有效.
   */
  on?: any;
  /**
   * 是否禁用.
   */
  disabled?: boolean;
}

/**
 * 仅供本组件使用的扩展
 */
interface MenuOptionLocal extends MenuOption {
  /**
   * 菜单组合菜单项的唯一标识,用于框架组件菜单设置选中状态与展开状态.
   */
  key: string;
  /**
   * 子菜单或者子菜单项.
   * 如果为空则表示菜单项(父菜单),否则为菜单
   */
  children?: Array<MenuOptionLocal>;
  /**
   * 如果该菜单项父元素,则该属性为其父元素,方便反向遍历.
   */
  parent?: MenuOptionLocal;
  /**
   * 本组件对应的路由对象.
   */
  route?: Route;
}
View Code

 源码,vue3.x版本(可去除ts类型声明后保存为.jsx) - menu.tsx

import { VNode, defineComponent, PropType } from 'vue'
import { RouteLocationRaw } from 'vue-router'
import { RouteLocation } from 'vue-router'

/**
 * 申明式嵌套菜单导航.
 * 官方文档: https://www.antdv.com/components/menu-cn/#API
 * <a-menu>
 *   <MenuItem>菜单项</MenuItem>
 *   <a-sub-menu title="子菜单">
 *     <MenuItem>子菜单项</MenuItem>
 *   </a-sub-menu>
 * </a-menu>
 * 根据提供的数据进行菜单渲染.
 * <p>
 *   示例:
 *   <pre><code>
 *    import Menu, { MenuOption } from '@/components/menu'
 *
 *    const menus: Array<MenuOption> = [
 *    {
 *       // 菜单名称,用于显示
 *       name: '菜单1',
 *       // [可选]用于自动导航和判断当前路由与当前菜单是否匹配,以实现刷新自动选中或菜单组自动展开(如果不需要则建议使用原生组件)
 *       routerNavigation: {
 *         // 是否自动导航,如果开启则会自动根据{@code location}值进行导航.默认开启
 *         autoNavigation: true,
 *         // 该属性类型为Vue原生导航参数类型(RouteLocationRaw)
 *         location: { name: 'Login' }
 *       }
 *     },
 *    {
 *       name: '菜单组',
 *       children: [
 *         {
 *           name: '菜单2',
 *           routerNavigation: {
 *             location: '/reg'
 *           },
 *           // 还可以为菜单项添加原生事件
 *           on: {
 *             click: () => {}
 *           }
 *         },
 *         {
 *           name: '菜单3',
 *           routerNavigation: {
 *             location: { path: '/reg2' }
 *           }
 *         }
 *       ]
 *     }
 *    ];
 *
 *    // template中使用(不要忘记在components中导入): <Menu mode="inline" :menus="menus"></Menu>
 *   </pre></code>
 *   如示例所示,只要使用时申明需要显示的菜单名字以及该菜单导航所需的对象即可(该对象默认用于导航已经自动根据当前路由进行高亮)
 *   但是可以禁用自动导航功能.并可以自定义菜单的点击事件
 *   如果不需要自动根据当前路由进行高亮和展开,请直接使用原生组件即可.
 * </p>
 * @author Laeni<m@laeni.cn>
 */
export default defineComponent({
  name: 'Menu',
  props: {
    /**
     * 需要进行渲染的菜单,
     * 类型为{@link MenuOption}.
     */
    menus: {
      type: Array as PropType<Array<MenuOption>>,
      required: true
    },
    /**
     * 菜单类型
     * 水平: vertical vertical-right
     * 垂直: horizontal
     * 内嵌: inline
     */
    mode: {
      type: String,
      default: ''
    },
    /**
     * 主题.
     * 详情参考官网: https://www.antdv.com/components/menu-cn/#components-menu-demo-menu-themes
     * light dark
     */
    theme: {
      type: String,
      default: 'light'
    },
    /**
     * 事件.
     * 如
     * {
     *   click: () => {},
     * }
     */
    on: {
      type: Object,
      default: () => {
        // do nothing.
      }
    },
  },
  data() {
    return {
      menuOptionLocals: [],
      // 菜单展开的分组
      openKeys: [],
      // 菜单选中的菜单项
      selectedKeys: []
    } as {
      // this.menus的扩展,方便使用
      menuOptionLocals: Array<MenuOptionLocal>;
      // 菜单展开的分组
      openKeys: Array<string>;
      // 菜单选中的菜单项
      selectedKeys: Array<string>;
    }
  },
  methods: {
    /**
     * 初始化菜单选中状态.
     * <p>
     *   功能:
     *   <ul>
     *     <li>当路由改变时计算出路由对应的菜单,并选中</li>
     *     <li>如果选择的菜单在菜单组中,则将该组展开</li>
     *   </ul>
     * </p>
     */
    initMenu(): void {
      // 从{@code this.menuOptionLocals}中将找出与当前路由相符的选项
      const menuOptionLocal: MenuOptionLocal | null = this.findMenuOptionLocal(this.menuOptionLocals, this.$route);

      if (menuOptionLocal !== null) {
        // 设置当前选择的key
        this.selectedKeys = [menuOptionLocal.key];
        // 依次将当前菜单所在的父菜单展开
        this.openKeying(menuOptionLocal);
      }
    },

    /**
     * 渲染菜单项或菜单组.
     * @param menu 单个MenuOption类型的对象
     */
    renderMenu(menu: MenuOptionLocal): VNode {
      if (menu.children && menu.children.length > 0) {
        return this.renderSubMenu(menu);
      } else {
        return this.renderMenuItem(menu);
      }
    },
    // 渲染菜单组
    renderSubMenu(menu: MenuOptionLocal): VNode {
      return (
        <a-sub-menu key={ menu.key } title={menu.name}>
          { menu.children && menu.children.map(v => this.renderMenu(v)) }
        </a-sub-menu>
      );
    },
    // 渲染菜单项
    renderMenuItem(menu: MenuOptionLocal): VNode {
      // 注入点击事件
      const click = () => {
        // 原始事件
        if (menu.on && typeof menu.on.click === 'function') {
          menu.on.click();
        }

        // 如果配置了自动导航,则根据配置规则进行点击时自动导航
        if (menu.routerNavigation && (menu.routerNavigation.autoNavigation || menu.routerNavigation.autoNavigation === undefined)) {
          // 导航到指定路由(如果当前路由处于目标路由则不进行导航)
          if (this.$router.resolve(menu.routerNavigation.location).fullPath !== this.$route.fullPath) {
            this.$router.push(menu.routerNavigation.location);
          }
        }
      }
      return (
        <a-menu-item key={ menu.key } disabled={menu.disabled} onClick={click} on={menu.on}>
          { menu.name }
        </a-menu-item>
      );
    },

    /*region private*/
    /**
     * 根据菜单的层次结构自动生成该菜单下唯一的 key.
     * 添加该元素的父元素(如果有).
     * @param menus  需要生产key则菜单.
     * @param parent [递归调用时使用]父节点
     */
    toMenuOptionLocal(menus: Array<MenuOption>, parent?: MenuOptionLocal): Array<MenuOptionLocal> {
      // 默认key前缀,如果父元素为空则使用,用于父元素
      const defaultPrefix = 'k';

      const menuLocals: Array<MenuOptionLocal> = [];

      menus.forEach((v, i) => {
        const menuLocal: MenuOptionLocal = {
          ...v,
          children: undefined,
          key: (parent ? parent.key : defaultPrefix) + '-' + i,
          parent: parent,
        };

        // 生成子节点的key
        if (v.children && v.children.length > 0) {
          menuLocal.children = this.toMenuOptionLocal(v.children, menuLocal);
        }
        // 如果可以导航则生成导航对象
        if (v.routerNavigation && v.routerNavigation.location) {
          menuLocal.route = this.$router.resolve(v.routerNavigation.location);
        }

        menuLocals.push(menuLocal);
      })

      return menuLocals;
    },
    /**
     * 从{@code this.menuOptionLocals}中将找出与当前路由相符的选项.
     * @param menuOptionLocals 菜单选项
     * @param route            当前路由对象
     * @return MenuOptionLocal 如果没有找到则返回null
     */
    findMenuOptionLocal(menuOptionLocals: Array<MenuOptionLocal>, route: RouteLocation): MenuOptionLocal | null {
      // 从节点树中找出所有符合要求的节点
      const menuOptionLocalAll: Array<MenuOptionLocal> = this.findMenuOptionLocalAll(menuOptionLocals, route);

      // 从所有符合要求的节点中找出优先级最高的一个节点(如"/xxx"优先级高于"/")
      const high = this.findHighPriority(menuOptionLocalAll);

      // 对首页进行特殊处理
      if (high) {
        // 如果 high 一定不满足要求则跳过
        if (!high.routerNavigation || !high.routerNavigation.location) {
          return null;
        }

        const highRoute: RouteLocation = high.route || this.$router.resolve(high.routerNavigation.location);
        const homeRoute: RouteLocation = this.$router.resolve({ name: 'Home' });

        if (highRoute.fullPath !== route.fullPath && highRoute.fullPath === homeRoute.fullPath) {
          return null;
        }
      }

      return high;
    },
    /**
     * 从{@code this.menuOptionLocals}中将找出与当前路由相符的<b>所有</b>选项.
     * @param menuOptionLocals        菜单选项
     * @param route                   当前路由对象
     * @return Array<MenuOptionLocal> 返回0个或多个菜单项数组
     */
    findMenuOptionLocalAll(menuOptionLocals: Array<MenuOptionLocal>, route: RouteLocation): Array<MenuOptionLocal> {
      // 用于放满足要求的选项节点
      const menuOptionLocalAll: Array<MenuOptionLocal> = [];

      for (const menu of menuOptionLocals) {
        // 如果有子元素则先找子元素
        if (menu.children && menu.children.length > 0) {
          const menuOptionLocal: MenuOptionLocal | null = this.findMenuOptionLocal(menu.children, route);
          if (menuOptionLocal) {
            menuOptionLocalAll.push(menuOptionLocal);
          }
        }
        // 如果不是子元素则判断当前元素是否为目标元素
        else if (menu.routerNavigation && menu.routerNavigation.location) {
          // 利用Vue模拟解析生成一个路由对象
          const resolveRoute: RouteLocation = menu.route || this.$router.resolve(menu.routerNavigation.location);
          // 通过比较{@link RouteLocation#path}和{@link RouteLocation#query}以判断该菜单是否为目标菜单
          if (route.path.indexOf(resolveRoute.path) > -1) {
            // 定义一个变量假设本元素符合要求,如果不符合则改为false
            let ok = true;

            for (const key of Object.keys(resolveRoute.query)) {
              if (resolveRoute.query[key] && resolveRoute.query[key] !== route.query[key]) {
                ok = false;
                break;
              } else if (typeof route.query[key] === 'undefined') {
                ok = false;
                break;
              }
            }

            if (ok) {
              menuOptionLocalAll.push(menu);
            }
          }
        }
      }

      return menuOptionLocalAll;
    },
    // 从所有符合要求的节点中找出优先级最高的一个节点(如"/xxx"优先级高于"/")
    findHighPriority(menuOptionLocals: Array<MenuOptionLocal>): MenuOptionLocal | null {
      if (menuOptionLocals.length === 0) {
        return null;
      }
      // 假定第一个元素优先级最高
      let high: MenuOptionLocal = menuOptionLocals[0];

      // 通过path进行比较
      for (let i = 1; i < menuOptionLocals.length; i++) {
        const menu = menuOptionLocals[i];
        // 如果 high 或 menu 一定不满足要求则跳过
        if (!high.routerNavigation || !high.routerNavigation.location || !menu.routerNavigation || !menu.routerNavigation.location) {
          high = menu;
          continue;
        }

        // 利用Vue模拟解析生成一个路由对象
        const highRoute: RouteLocation = high.route || this.$router.resolve(high.routerNavigation.location);
        const menuRoute: RouteLocation = menu.route || this.$router.resolve(menu.routerNavigation.location);
        // 优先级比较
        if (menuRoute.path.indexOf(highRoute.path) > -1) {
          high = menu;
        }
      }

      return high;
    },
    /**
     * 依次将当前菜单所在的父菜单展开.
     * @param menuOptionLocal 当前url导航对应的菜单.
     */
    openKeying(menuOptionLocal: MenuOptionLocal): void {
      if (menuOptionLocal.parent) {
        this.openKeys.push(menuOptionLocal.parent.key);
        this.openKeying(menuOptionLocal.parent);
      }
    }
    /*endregion*/
  },
  created(): void {
    this.menuOptionLocals = this.toMenuOptionLocal(this.menus);
  },
  mounted(): void {
    this.initMenu();
  },
  render(): VNode {
    return (
      <a-menu theme={this.theme} mode={this.mode} openKeys={this.openKeys} selectedKeys={this.selectedKeys}
              onOpenChange={(openKeys: string[]) => this.openKeys = openKeys}
      >
        {this.menuOptionLocals.map((menu) => this.renderMenu(menu))}
      </a-menu>
    );
  },
  watch: {
    $route() {
      this.initMenu();
    },
    menus() {
      this.menuOptionLocals = this.toMenuOptionLocal(this.menus);
      this.initMenu();
    }
  },
})

/**
 * 菜单组或菜单项.
 * 如果{@link #children}为空则表示菜单项(父菜单),否则为菜单.
 */
export interface MenuOption {
  /**
   * 菜单或菜单项的名字.
   */
  name: string;
  /**
   * 路由导航规则,当菜单项(children 未定义 或 children.length === 0)时有效.
   * 根据路由动态选中该菜单,可以根据该定义的判断在当前路由下是否应该选择该菜单.
   */
  routerNavigation?: {
    /**
     * 是否根据定义的结构进行自动导航.
     * 即点击时自动导航到声明的路由中.如果在复杂条件不能自动导航时,可禁用,并且添加{@link MenuOption#on}相关事件进行自定义导航.
     * 如"/xxx?a=1&b=2"中,仅仅依据path"/xxx"和查询参数"a=1"作为是否选中的判断依据,但是查询参数"b=2"不作为判断依据时,
     * 应该在这里不定义查询参数"b=2"这个条件,所以不能自动获取并传递该参数,这个时候应该禁用自动导航.
     */
    autoNavigation?: boolean;
    /**
     * Vue路由导航参数.其作用最多有两个.
     * 1.点击时自动导航({@link #autoNavigation}为{@code undefined}或为{@code true}时有效.)
     * 2.自动根据其定义的类容判断是否应该选中该菜单.
     */
    location: RouteLocationRaw;
  };
  /**
   * 子菜单组或者子菜单项.
   * 如果为空则本菜单表示菜单组(父菜单),否则为菜单项
   */
  children?: Array<MenuOption>;
  /**
   * 菜单或菜单项logo图标(TODO 暂未实现, 可为框架的图标名或阿里矢量图标库的名称).
   */
  logo?: string;
  /**
   * Menu.Item事件(html原生事件),为菜单项(children.length > 0)时有效.
   */
  on?: any;
  /**
   * 是否禁用.
   */
  disabled?: boolean;
}

/**
 * 仅供本组件使用的扩展
 */
interface MenuOptionLocal extends MenuOption {
  /**
   * 菜单组合菜单项的唯一标识,用于框架组件菜单设置选中状态与展开状态.
   */
  key: string;
  /**
   * 子菜单或者子菜单项.
   * 如果为空则表示菜单项(父菜单),否则为菜单
   */
  children?: Array<MenuOptionLocal>;
  /**
   * 如果该菜单项父元素,则该属性为其父元素,方便反向遍历.
   */
  parent?: MenuOptionLocal;
  /**
   * 本组件对应的路由对象.
   */
  route?: RouteLocation;
}
View Code
原文地址:https://www.cnblogs.com/laeni/p/13362003.html