一些很哇塞的小技巧

139 阅读3分钟

1、设置标志位

在使用 vue-element-plus-admin 进行二次开发的时候发现了一个小bug,动态设置Search组件的componentProps.options时接口被多次调用。

发现是在Form组件中,getOptions()在没有获取到接口返回值后又进行了多次调用,直到optins有值。解决方法:通过设置标识位,在获取接口状态下不再调用

const isUpdating = ref(false)
const getOptions = async (fn: Function, item: FormSchema) => {
  isUpdating.value = true
  try {
    const options = await fn()
    setSchema([
      {
        field: item.field,
        path:
            item.component === ComponentNameEnum.TREE_SELECT ||
            item.component === ComponentNameEnum.TRANSFER
                ? 'componentProps.data'
                : 'componentProps.options',
        value: options
      }
    ])
  } catch (error) {
    console.error('Failed to fetch options:', error)
  } finally {
    // 确保无论成功失败,都会重置标志
    isUpdating.value = false
  }
}

renderFormItem()渲染formItem方法里添加判断!isUpdating.value时获取

if (
  item.optionApi &&
  (!item.componentProps?.options || !item.componentProps?.options.length) && !isUpdating.value
) {
  // 内部自动调用接口,不影响其它渲染
  getOptions(item.optionApi, item)
}

2、菜单结构树的修改

UI改版:先上需求图

image.png 之前样式,是很常规后台管理的菜单

image.png

处理方式:

  • 递归遍历最后一级非按钮级别的菜单全部挪到一级菜单下
  • 并且之前二级菜单不能点击,有下级菜单的不能加减操作

隐藏hidden=true的菜单

function filterHiddenMenus(menus) {
  return menus
    .filter((menu) => !menu.meta?.hidden) // 过滤掉 meta.hidden 为 true 的菜单项
    .map((menu) => {
      if (menu.children) {
        // 递归处理子菜单
        menu.children = filterHiddenMenus(menu.children);
      }
      return menu;
    });
}

递归便利最后一级非按钮级别的菜单全部挪到一级菜单下,添加parentPath(完整路由好跳转)

function processMenu(menu) {
  // 初始化第一层的 childrenUse 数组
  menu.childrenUse = [];
  // 递归遍历子节点,收集 meta.iscatalog=1 的节点
  function collectCatalogNodes(node) {
    if (node.children && node.children.length > 0) {
      for (let child of node.children) {
        // 递归处理子节点
        child.parentPath = node.parentPath ? `${node.parentPath}/${node.path}` : node.path;
        collectCatalogNodes(child);
      }
    }
    // 如果当前节点的 meta.iscatalog=1,将其添加到第一层的 childrenUse 中
    if (node.meta && node.meta.iscatalog === 1) {
      if (node.children?.length) {
        if (node.children[0].meta.iscatalog === 0) {
          menu.childrenUse.push(node);
        }
      } else {
        menu.childrenUse.push(node);
      }
    }
  }
  // 从第一层开始递归收集
  if (menu.children && menu.children.length > 0) {
    for (let child of menu.children) {
      child.parentPath = menu.path;
      collectCatalogNodes(child);
    }
  }
  return menu;
}

完整代码

<script lang="tsx">
import { computed, defineComponent, ref, unref, watch } from 'vue';
import { usePermissionStore } from '@/store/modules/permission';
import { Icon } from '@/components/Icon';
import { Router, useRouter } from 'vue-router';

const permissionStore = usePermissionStore();
const routers = computed(() => {
  let rou = filterHiddenMenus(permissionStore.getRouters);
  rou.forEach((v) => processMenu(v));
  return rou;
});

function processMenu(menu) {
  // 初始化第一层的 childrenUse 数组
  menu.childrenUse = [];
  // 递归遍历子节点,收集 meta.iscatalog=1 的节点
  function collectCatalogNodes(node) {
    if (node.children && node.children.length > 0) {
      for (let child of node.children) {
        // 递归处理子节点
        child.parentPath = node.parentPath ? `${node.parentPath}/${node.path}` : node.path;
        collectCatalogNodes(child);
      }
    }
    // 如果当前节点的 meta.iscatalog=1,将其添加到第一层的 childrenUse 中
    if (node.meta && node.meta.iscatalog === 1) {
      if (node.children?.length) {
        if (node.children[0].meta.iscatalog === 0) {
          menu.childrenUse.push(node);
        }
      } else {
        menu.childrenUse.push(node);
      }
    }
  }
  // 从第一层开始递归收集
  if (menu.children && menu.children.length > 0) {
    for (let child of menu.children) {
      child.parentPath = menu.path;
      collectCatalogNodes(child);
    }
  }
  return menu;
}

function filterHiddenMenus(menus) {
  return menus
    .filter((menu) => !menu.meta?.hidden) // 过滤掉 meta.hidden 为 true 的菜单项
    .map((menu) => {
      if (menu.children) {
        // 递归处理子菜单
        menu.children = filterHiddenMenus(menu.children);
      }
      return menu;
    });
}

const currentPath = ref('');
const showPop = ref(false);
const currentMenu: any = ref(null);
const mouseEnterHandler = (v) => {
  showPop.value = true;
  currentMenu.value = v;
};

const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => {
  return routers
    .filter((v) => !v.meta?.hidden)
    .map((v) => {
      const meta = v.meta ?? {};
      const fullPath = isUrl(v.path) ? v.path : `${v.parentPath}/${v.path}`;
      if (!v.childrenUse?.length) {
        return (
          <router-link to={fullPath}>
            <div class={['ju-menu-item', currentPath.value === fullPath ? 'is-active' : '']}>{meta.title}</div>
          </router-link>
        );
      } else {
        return (
          <div class="ju-menu" onMouseenter={() => mouseEnterHandler(v)}>
            <div class="flex items-center ju-sub-menu">
              {meta.icon ? <Icon icon={meta.icon}></Icon> : null}
              <p class="flex-1 v-menu__title overflow-hidden overflow-ellipsis whitespace-nowrap">{meta.title}</p>
              <Icon class="ju-sub-menu__icon-arrow" icon="svg-icon:arrow" />
            </div>
            <div class="flex ju-menu">{renderMenuItem(v.childrenUse!, fullPath)}</div>
          </div>
        );
      }
    });
};

const renderMenuPop = (router: Router) => {
  const meta = currentMenu.value.meta ?? {};
  return (
    <div class="jmenu-pop" onMouseleave={() => (showPop.value = false)}>
      {!meta.hidden && (
        <>
          <h3>{currentMenu.value.meta.title}</h3>
          <div class="jmemu-pop-w">
            {currentMenu.value.children.map((c) => {
              if (c.meta.hidden) {
                return null;
              } else {
                return (
                  <div class="w-108px">
                    <h4 class={[c.children.length ? '' : 'cursor-pointer flex items-center justify-between']}>
                      <p onClick={() => popItemClick(router, currentMenu, c)}>{c.meta.title}</p>
                      {!c.children.length ? <Icon icon="svg-icon:add_circle" /> : null}
                    </h4>
                    {c.children.map((cc) => {
                      if (cc.meta.hidden) {
                        return null;
                      } else {
                        return (
                          <div class="jmenu-pop-last-item">
                            <p
                              class="cursor-pointer single-line-ellipsis"
                              onClick={() => popItemClick(router, currentMenu, c, cc)}
                            >
                              {cc.meta.title}
                            </p>
                            <Icon icon="svg-icon:add_circle" />
                          </div>
                        );
                      }
                    })}
                  </div>
                );
              }
            })}
          </div>
        </>
      )}
    </div>
  );
};

const popItemClick = (router, c1, c, cc?) => {
  if (!c.children.length) {
    router.push(`${unref(c1).path}/${unref(c).path}`);
  } else {
    if (cc) {
      router.push(`${unref(c1).path}/${unref(c).path}/${unref(cc).path}`);
    }
  }
};

export default defineComponent({
  name: 'JMenuItem',
  setup() {
    const router = useRouter();
    watch(
      () => router.currentRoute.value,
      (v) => {
        currentPath.value = v.path;
        showPop.value = false;
      },
      { immediate: true }
    );
    return () => (
      <>
        <div>
          {renderMenuItem(routers.value)}
          {showPop.value && renderMenuPop(router)}
        </div>
      </>
    );
  }
});
</script>

<style lang="less" scoped>
.jmenu-pop {
  position: fixed;
  top: calc(var(--top-tool-height) + var(--spacing-lg));
  left: calc(var(--spacing-lg) + 200px);
  height: calc(100vh - var(--top-tool-height) - (var(--spacing-lg)*2));
  border-radius: 0px var(--l) var(--l) 0px;
  border-left: 1px solid var(--colors-boder-secondary);
  background: var(--colors-elevation-surface-default);
  box-shadow: 12px 12px 16px -4px rgba(16, 24, 40, 0.08), 4px 0px 6px -2px rgba(16, 24, 40, 0.03);
  padding: var(--spacing-2xl);
  z-index: 1000;
  h3 {
    color: var(--colors-text-primary);
    font-size: 16px;
    font-weight: 500;
  }
  .jmemu-pop-w {
    margin-top: var(--spacing-2xl);
    display: flex;
    align-items: flex-start;
    gap: 64px;
    h4 {
      color: var(--colors-text-primary);
      font-size: 14px;
      font-weight: 500;
      margin-bottom: var(--spacing-md);
    }
    .jmenu-pop-last-item {
      margin-bottom: var(--spacing-sm);
      display: flex;
      justify-content: space-between;
      gap: var(--spacing-sm);
      align-items: center;
      p {
        color: var(--colors-text-secondary);
        font-size: 14px;
        font-weight: 400;
        width: 84px;
      }
    }
  }
}
</style>