基於螞蟻金服"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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM