手摸手,带你尝鲜 naiveui 撸 admin 骨架(核心骨架篇)

前言

根据以往几篇手摸手系列文章发布,以及粉丝私信反馈,多数还是希望实例直接上代码,方便复制粘贴,这个确实是个不好的习惯哈,俗话说自己动起手来,发现问题才能解决问题嘛。

App.vue

说明:首先从app.vue开始,由于naiveui框架, 几个提示类型的组件(Dialog,Loading Bar,等),都需要把模板在app中引入,并且需要RouterView同级或者父级,方可优雅使用组件。

其次:如果你想在js中优雅地使用,也是一个问题,官方文档例子,只支持setup中使用,咋办这不科学啊,别急,上有政策下有对策,看我慢慢道来。


<template>
  <NConfigProvider>
    <AppProvider>
      <RouterView />
    </AppProvider>
  </NConfigProvider>
</template>
<script lang="ts">
  import { defineComponent } from 'vue';
  import { AppProvider } from '@/components/Application';
  export default defineComponent({
    name: 'App',
    components: { AppProvider },
    setup() {
    }
  })
</script>
复制代码

以上用一个,AppProvider组件包裹RouterView, 为了实现组件引入嵌套问题,AppProvider组件实现代码如下:

<template>
  <n-loading-bar-provider>
    <n-dialog-provider>
      <DialogContent />
      <n-notification-provider>
        <n-message-provider>
          <MessageContent />
          <slot slot="default"></slot>
        </n-message-provider>
      </n-notification-provider>
    </n-dialog-provider>
  </n-loading-bar-provider>
</template>

<script lang="ts">
  import { defineComponent } from 'vue';
  import {
    NDialogProvider,
    NNotificationProvider,
    NMessageProvider,
    NLoadingBarProvider,
  } from 'naive-ui';
  import { MessageContent } from '@/components/MessageContent';
  import { DialogContent } from '@/components/DialogContent';

  export default defineComponent({
    name: 'Application',
    components: {
      NDialogProvider,
      NNotificationProvider,
      NMessageProvider,
      NLoadingBarProvider,
      MessageContent,
      DialogContent,
    },
    setup() {
      return {};
    },
  });
</script>
复制代码

解释一下, 相当于app.vue中的 RouterView, 以上组件必须这么用,才能在setup中正常使用。

Layout介绍

这是整个后台骨架核心入口模板,就是登录之后的页面,通常会包含,左侧菜单导航,顶部,内容区域,如下:

实现Layout

这里直接用框架提供的,layout组件,自带了折叠,深色,固定定位,等功能,相对来说是非常方便,改动样式很少即可实现一个骨架。

<template>
  <NLayout class="layout" :position="fixedMenu" has-sider>
    <!-- 左侧区域 -->
    <NLayoutSider>
      <!-- logo -->
      <Logo :collapsed="collapsed" />
      <!-- 左侧菜单 -->
      <AsideMenu v-model:collapsed="collapsed" v-model:location="getMenuLocation" />
    </NLayoutSider>
    <!-- 右侧区域-->
    <NLayout>
      <!-- header区域-->
      <NLayoutHeader :inverted="getHeaderInverted" :position="fixedHeader">
        <PageHeader v-model:collapsed="collapsed" :inverted="inverted" />
      </NLayoutHeader>
      <!-- 页面内容区域-->
      <NLayoutContent>
        <!-- 多标签组件-->
        <TabsView v-if="isMultiTabs" v-model:collapsed="collapsed" />
        <!-- 主内容组件-->
        <MainView />
      </NLayoutContent>
    </NLayout>
  </NLayout>
</template>
复制代码

最终实现的效果如下,

以上核心骨架和组件拆分,就已经规划完成,接下来只需要填充对应的组件内容即可

AsideMenu组件

菜单组件封装,包含垂直菜单,和水平菜单,实现如下:

<template>
  <NMenu
    :options="menus"
    :inverted="inverted"
    :mode="mode"
    :collapsed="collapsed"
    :collapsed-width="64"
    :collapsed-icon-size="20"
    :indent="24"
    :expanded-keys="openKeys"
    :value="getSelectedKeys"
    @update:value="clickMenuItem"
    @update:expanded-keys="menuExpanded"
  />
</template>

<script lang="ts">
  import { defineComponent, ref, onMounted, reactive, computed, watch, toRefs, unref } from 'vue';
  import { useRoute, useRouter } from 'vue-router';
  import { useAsyncRouteStore } from '@/store/modules/asyncRoute';
  import { generatorMenu, generatorMenuMix } from '@/utils';
  import { useProjectSettingStore } from '@/store/modules/projectSetting';
  import { useProjectSetting } from '@/hooks/setting/useProjectSetting';

  export default defineComponent({
    name: 'Menu',
    components: {},
    props: {
      mode: {
        // 菜单模式
        type: String,
        default: 'vertical',
      },
      collapsed: {
        // 侧边栏菜单是否收起
        type: Boolean,
      },
      //位置
      location: {
        type: String,
        default: 'left',
      },
    },
    emits: ['update:collapsed'],
    setup(props, { emit }) {
      // 当前路由
      const currentRoute = useRoute();
      const router = useRouter();
      const asyncRouteStore = useAsyncRouteStore();
      const settingStore = useProjectSettingStore();
      const menus = ref<any[]>([]);
      const selectedKeys = ref<string>(currentRoute.name as string);
      const headerMenuSelectKey = ref<string>('');

      const { getNavMode } = useProjectSetting();

      const navMode = getNavMode;

      // 获取当前打开的子菜单
      const matched = currentRoute.matched;

      const getOpenKeys = matched && matched.length ? matched.map((item) => item.name) : [];

      const state = reactive({
        openKeys: getOpenKeys,
      });

      const inverted = computed(() => {
        return ['dark', 'header-dark'].includes(settingStore.navTheme);
      });

      const getSelectedKeys = computed(() => {
        let location = props.location;
        return location === 'left' || (location === 'header' && unref(navMode) === 'horizontal')
          ? unref(selectedKeys)
          : unref(headerMenuSelectKey);
      });

      // 监听分割菜单
      watch(
        () => settingStore.menuSetting.mixMenu,
        () => {
          updateMenu();
          if (props.collapsed) {
            emit('update:collapsed', !props.collapsed);
          }
        }
      );

      // 监听菜单收缩状态
      watch(
        () => props.collapsed,
        (newVal) => {
          state.openKeys = newVal ? [] : getOpenKeys;
          selectedKeys.value = currentRoute.name as string;
        }
      );

      // 跟随页面路由变化,切换菜单选中状态
      watch(
        () => currentRoute.fullPath,
        () => {
          updateMenu();
          const matched = currentRoute.matched;
          state.openKeys = matched.map((item) => item.name);
          const activeMenu: string = (currentRoute.meta?.activeMenu as string) || '';
          selectedKeys.value = activeMenu ? (activeMenu as string) : (currentRoute.name as string);
        }
      );

      function updateMenu() {
        if (!settingStore.menuSetting.mixMenu) {
          menus.value = generatorMenu(asyncRouteStore.getMenus);
        } else {
          //混合菜单
          const firstRouteName: string = (currentRoute.matched[0].name as string) || '';
          menus.value = generatorMenuMix(asyncRouteStore.getMenus, firstRouteName, props.location);
          const activeMenu: string = currentRoute?.matched[0].meta?.activeMenu as string;
          headerMenuSelectKey.value = (activeMenu ? activeMenu : firstRouteName) || '';
        }
      }

      // 点击菜单
      function clickMenuItem(key: string) {
        if (/http(s)?:/.test(key)) {
          window.open(key);
        } else {
          router.push({ name: key });
        }
      }

      //展开菜单
      function menuExpanded(openKeys: string[]) {
        if (!openKeys) return;
        const latestOpenKey = openKeys.find((key) => state.openKeys.indexOf(key) === -1);
        const isExistChildren = findChildrenLen(latestOpenKey as string);
        state.openKeys = isExistChildren ? (latestOpenKey ? [latestOpenKey] : []) : openKeys;
      }

      //查找是否存在子路由
      function findChildrenLen(key: string) {
        if (!key) return false;
        const subRouteChildren: string[] = [];
        for (const { children, key } of unref(menus)) {
          if (children && children.length) {
            subRouteChildren.push(key as string);
          }
        }
        return subRouteChildren.includes(key);
      }

      onMounted(() => {
        updateMenu();
      });

      return {
        ...toRefs(state),
        inverted,
        menus,
        selectedKeys,
        headerMenuSelectKey,
        getSelectedKeys,
        clickMenuItem,
        menuExpanded,
      };
    },
  });
</script>
复制代码

代码实现比较简单,官方提供的menu组件,会自动帮我们递归创建出子菜单,这里我们只需把数据丢给他即可,真是大快人心啊。

说一下核心的2个方法:

1、左侧普通菜单

/**
 * 递归组装菜单格式
 */
export function generatorMenu(routerMap: Array<any>) {
  return filterRouter(routerMap).map((item) => {
    const isRoot = isRootRouter(item);
    const info = isRoot ? item.children[0] : item;
    const currentMenu = {
      ...info,
      ...info.meta,
      label: info.meta?.title,
      key: info.name,
    };
    // 是否有子菜单,并递归处理
    if (info.children && info.children.length > 0) {
      // Recursion
      currentMenu.children = generatorMenu(info.children);
    }
    return currentMenu;
  });
}
复制代码

这里通过路由对象数组,组装成menu组件需要的格式,并且还可以自定义一些逻辑在这里实现

2、顶部菜单模式-混合菜单

/**
 * 混合菜单
 * */
export function generatorMenuMix(routerMap: Array<any>, routerName: string, location: string) {
  const cloneRouterMap = cloneDeep(routerMap);
  const newRouter = filterRouter(cloneRouterMap);
  if (location === 'header') {
    const firstRouter: any[] = [];
    newRouter.forEach((item) => {
      const isRoot = isRootRouter(item);
      const info = isRoot ? item.children[0] : item;
      info.children = undefined;
      const currentMenu = {
        ...info,
        ...info.meta,
        label: info.meta?.title,
        key: info.name,
      };
      firstRouter.push(currentMenu);
    });
    return firstRouter;
  } else {
    return getChildrenRouter(newRouter.filter((item) => item.name === routerName));
  }
}

/**
 * 递归组装子菜单
 * */
export function getChildrenRouter(routerMap: Array<any>) {
  return filterRouter(routerMap).map((item) => {
    const isRoot = isRootRouter(item);
    const info = isRoot ? item.children[0] : item;
    const currentMenu = {
      ...info,
      ...info.meta,
      label: info.meta?.title,
      key: info.name,
    };
    // 是否有子菜单,并递归处理
    if (info.children && info.children.length > 0) {
      // Recursion
      currentMenu.children = getChildrenRouter(info.children);
    }
    return currentMenu;
  });
}
复制代码

这里也是为menu组件提供数据支持

PageHeader组件

这个组件主要是实现了这些功能,比较简单,而且不一定是你想要的,所以不说了哈~

TabsView

多标签页,实现的功能比较多,后面单独抽一起来解析,手摸手带你撸。

MainView

<template>
  <RouterView>
    <template #default="{ Component, route }">
      <transition name="zoom-fade" mode="out-in" appear>
        <keep-alive v-if="keepAliveComponents" :include="keepAliveComponents">
          <component :is="Component" :key="route.fullPath" />
        </keep-alive>
        <component v-else :is="Component" :key="route.fullPath" />
      </transition>
    </template>
  </RouterView>
</template>

<script>
  import { defineComponent, computed } from 'vue';
  import { useAsyncRouteStore } from '@/store/modules/asyncRoute';

  export default defineComponent({
    name: 'MainView',
    components: {},
    props: {
      notNeedKey: {
        type: Boolean,
        default: false,
      },
      animate: {
        type: Boolean,
        default: true,
      },
    },
    setup() {
      const asyncRouteStore = useAsyncRouteStore();
      // 需要缓存的路由组件
      const keepAliveComponents = computed(() => asyncRouteStore.keepAliveComponents);
      return {
        keepAliveComponents,
      };
    },
  });
</script>
复制代码

这里主要是做一个缓存路由,和动画效果,以及公共布局风格统一处理

最后上成品

以上只展示一些核心代码,和思路,想看完整代码,可找一下,naive-ui-admin 官方仓库

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享