menu.vue 8.37 KB
<script lang="tsx">
  import { compile, computed, defineComponent, h, ref, watch } from 'vue';

  import { RouteRecordRaw, useRoute, useRouter } from 'vue-router';
  import { useAppStore, useAuthorizedStore } from '@/store';
  import usePermission from '@/hooks/permission';
  import { last, orderBy } from 'lodash';
  import { storeToRefs } from 'pinia';
  import { useIntervalFn } from '@vueuse/core';

  export default defineComponent({
    emit: ['collapse'],
    setup() {
      const appStore = useAppStore();
      const permission = usePermission();
      const authorizedStore = useAuthorizedStore();
      const router = useRouter();
      const route = useRoute();

      const { appMenu } = storeToRefs(appStore);
      const { auditUserCount, auditActivityCount, activityApplyFailCount, permissions } = storeToRefs(authorizedStore);
      const collapsed = ref(false);

      const formatBadgeNum = (num: number) => {
        return num <= 99 ? num : '99+';
      };

      const userBadge = computed((): number => {
        let count = 0;
        if (permissions.value?.includes('user-certify')) {
          count += auditUserCount.value as number;
        }
        return count;
      });

      const auditionBadge = computed((): number => {
        let count = 0;
        if (permissions.value?.includes('audition-activity-audit')) {
          count += auditActivityCount.value as number;
        }
        if (permissions.value?.includes('audition-activity-apply')) {
          count += activityApplyFailCount.value as number;
        }
        return count;
      });

      useIntervalFn(() => {
        authorizedStore.syncAuditUser();
        authorizedStore.syncAuditActivity();
      }, 30000);

      const syncServicePermission = (item: RouteRecordRaw) => {
        if (item.meta) {
          const serverConfig = appMenu.value.find((menu) => item.name === menu.name);
          item.meta.title = serverConfig?.label || item.meta?.title || '';
          item.meta.order = serverConfig?.weight || item.meta?.order || 0;
          item.meta.icon = serverConfig?.icon || item.meta?.icon || '';
        }
        item.children?.map((child) => syncServicePermission(child));
        return item;
      };

      const appRoute = computed((): RouteRecordRaw[] => {
        return router
          .getRoutes()
          .find((el) => el.name === 'root')
          ?.children.filter((element) => element.meta?.hideInMenu !== true)
          .map((item: RouteRecordRaw) => syncServicePermission(item)) as RouteRecordRaw[];
      });

      const menuTree = computed((): RouteRecordRaw[] => {
        function travel(_routes: RouteRecordRaw[], layer: number) {
          if (!_routes) return null;
          const collector: any = orderBy(_routes, 'meta.order', 'desc').map((element) => {
            // no access
            if (!permission.accessRouter(element)) {
              return null;
            }

            // leaf node
            if (!element.children) {
              return element;
            }

            // route filter hideInMenu true
            element.children = element.children.filter((x) => x.meta?.hideInMenu !== true);

            // Associated child node
            const subItem = travel(element.children, layer);
            if (subItem.length) {
              element.children = subItem;
              return element;
            }
            // the else logic
            if (layer > 1) {
              element.children = subItem;
              return element;
            }

            if (element.meta?.hideInMenu === false) {
              return element;
            }

            return null;
          });

          return collector.filter(Boolean);
        }

        return travel(appRoute.value, 0);
      });

      // In this case only two levels of menus are available
      // You can expand as needed

      const selectedKey = ref<string[]>([]);
      const openKey = ref<string[]>([]);

      const goto = (item: RouteRecordRaw) => {
        router.push({ name: item.name });
      };
      watch(
        route,
        (newVal) => {
          if (newVal.meta.requiresAuth) {
            const key = newVal.meta.hideInMenu ? last(newVal.matched)?.meta?.menuSelectKey : last(newVal.matched)?.name;
            selectedKey.value = [key as string];
            openKey.value = newVal.meta.breadcrumb || [];
          }
        },
        { immediate: true }
      );
      watch(
        () => appStore.menuCollapse,
        (newVal) => {
          collapsed.value = newVal;
        },
        { immediate: true }
      );
      const setCollapse = (val: boolean) => appStore.updateSettings({ menuCollapse: val });

      const renderSubMenu = () => {
        function travel(_route: RouteRecordRaw[], nodes = []) {
          if (_route) {
            _route.forEach((element) => {
              let icon = element?.meta?.icon ? `<${element?.meta?.icon}/>` : ``;
              let title = `<span>${element.meta?.title}</span>`;

              if (element?.name === 'user' && userBadge.value !== 0) {
                if (icon && collapsed.value) {
                  icon = `<a-badge :count="${userBadge.value}" :offset="[2, -2]" dot>${icon}</a-badge>`;
                }
                title += `<span class="menu-number">${formatBadgeNum(userBadge.value)}</span>`;
              }
              if (element?.name === 'audition' && auditionBadge.value !== 0) {
                if (icon && collapsed.value) {
                  icon = `<a-badge :count="${auditionBadge.value}" :offset="[4, -2]" dot>${icon}</a-badge>`;
                }
                title += `<span class="menu-number">${formatBadgeNum(auditionBadge.value)}</span>`;
              }

              let r;

              if (element && element.children) {
                r = (
                  <a-sub-menu key={element?.name} v-slots={{ icon: () => h(compile(icon)), title: () => h(compile(title)) }}>
                    {element?.children?.map((elem) => {
                      return (
                        <a-menu-item key={elem.name} onClick={() => goto(elem)}>
                          <span>{elem.meta?.title}</span>
                          {elem.name === 'user-certify' && auditUserCount.value !== 0 ? (
                            <span class="menu-number">{formatBadgeNum(auditUserCount.value as number)}</span>
                          ) : (
                            ''
                          )}
                          {elem.name === 'audition-activity-audit' && auditActivityCount.value !== 0 ? (
                            <span class="menu-number">{formatBadgeNum(auditActivityCount.value as number)}</span>
                          ) : (
                            ''
                          )}
                          {elem.name === 'audition-activity-apply' && activityApplyFailCount.value !== 0 ? (
                            <span class="menu-number">{formatBadgeNum(activityApplyFailCount.value as number)}</span>
                          ) : (
                            ''
                          )}
                          {travel(elem.children ?? [])}
                        </a-menu-item>
                      );
                    })}
                  </a-sub-menu>
                );
              } else {
                r = (
                  <a-menu-item key={element.name} v-slots={{ icon: () => h(compile(icon)) }} onClick={() => goto(element)}>
                    {element.meta?.title}
                  </a-menu-item>
                );
              }
              nodes.push(r as never);
            });
          }
          return nodes;
        }

        return travel(menuTree.value);
      };

      return () => (
        <a-menu
          v-model:collapsed={collapsed.value}
          show-collapse-button
          v-model:selected-keys={selectedKey.value}
          v-model:open-keys={openKey.value}
          auto-open-selected={true}
          level-indent={34}
          style={{ height: '100%' }}
          onCollapse={setCollapse}
        >
          {renderSubMenu()}
        </a-menu>
      );
    },
  });
</script>

<style lang="less" scoped>
  :deep(.arco-menu-inner) {
    .arco-menu-inline-header {
      display: flex;
      align-items: center;
    }

    .arco-icon {
      &:not(.arco-icon-down) {
        font-size: 18px;
      }
    }
  }

  .menu-number {
    background-color: red;
    color: #fff;
    margin-left: 12px;
    padding: 0 6px;
    border-radius: 8px;
    font-size: 12px;
    font-weight: 500;
  }
</style>