最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 手摸手,带你尝鲜 naiveui 撸 admin 骨架(核心骨架篇)

    正文概述 掘金(Biu前端)   2021-08-12   703

    前言

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

    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介绍

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

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

    实现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>
    

    最终实现的效果如下,

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

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

    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组件

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

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

    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>
    

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

    最后上成品

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

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


    起源地下载网 » 手摸手,带你尝鲜 naiveui 撸 admin 骨架(核心骨架篇)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元