Vue3+Ts项目(Naive UI组件)——创建有图标可伸缩的左边菜单栏


前言:在我不懈的摸索中终于实现了。我主要是总结另一位博主的创建过程,就是他标题取的,感觉有被白嫖到(手摸手创建…),哈哈哈哈哈哈,后面会附上原博主地址。以及我个人的总结理解。

在这里插入图片描述

安装、配置vue-router

1、安装
npm install vue-router
2、main.ts配置
// router
  import { useRouter } from '@/router'
  useRouter(app)
// 要放在app.mount('#app')之前
  app.mount('#app')
3、在App.vue中,渲染路由配置到的组件
<script setup lang="ts">
</script>

<template>
 <router-view></router-view>
</template>

<style scoped>
</style>

创建测试路径页面

后面都请无脑复制,因为我已经把原博主的过程都走了一遍。虽然他有提供项目git地址,但是很多地方不一样。(原文有提供处理没设置路径404页面)

在 src 目录下,新建 views 目录,用于存放页面文件(我们创建三个页面

1、src\views\dashboard\index.vue
<script setup lang="ts">
</script>

<template>
	<div v-for="i of 10">
		<h3>Dashboard {{ i }}</h3>
		<router-link to="/tableCombination/table">Go to Table</router-link>
	</div>
</template>

<style scoped>

</style>
2、src\views\dashboard\test.vue
<script setup lang="ts">
</script>

<template>
	<div>测试页面</div>
</template>

<style scoped>

</style>
3、src\views\table\index.vue
  <script setup lang="ts">
  
  </script>
  
  <template>
   <div>
     <h3>Table</h3>
    <router-link to="/combination/dashboard">Go to Dashboard</router-link>
   </div>
  </template>
  
  <style scoped>
  
  </style>

配置页面路由

在 src 目录下,新建 router 文件夹,用于存放路由配置文件。首先在其下创建一个 modules 文件夹,区分各模块不同的路由配置文件(接下来我们要创建6个ts文件)页面会因为找不到一些依赖和文件而报红,咱先不管,后面再处理

1、src\router\modules\dashboard.ts
import type { RouteRecord } from '@/router/type'
import BasicLayout from "@/layouts/BasicLayout.vue"

import { CalendarSettings } from '@vicons/carbon'
  
const dashboardRoutes: RouteRecord[] = [
  {
    path: "/combination",
    name:'combination',
    component: BasicLayout,
    meta:{
      icon: CalendarSettings
    },
    children: [
      {
        path: "/combination/dashboard",
        name: "dashboard",
        component: () => import("@/views/dashboard/index.vue"),
      },
      {
        path: "/combination/test",
        name: "test",
        component: () => import("@/views/dashboard/test.vue"),
      },
    ],
  },
]

export default dashboardRoutes
2、src\router\modules\index.ts (主要用于测试不想展示的菜单路径隐藏)
import type { RouteRecord } from '@/router/type'
  
const rootRoutes: RouteRecord[] = [
  {
    path: '/',
    name: 'home',
    redirect: '/combination/dashboard',
    meta: {
        hidden: true
      },
  }
]

export default rootRoutes
3、src\router\modules\table.ts
import type { RouteRecord } from '@/router/type'
import BasicLayout from "@/layouts/BasicLayout.vue";
import { CalendarTools } from '@vicons/carbon'  

const tableRoutes: RouteRecord[] = [
  {
    path: "/tableCombination",
    name:'tableCombination',
    component: BasicLayout,
    meta:{
      icon: CalendarTools
    },
    children: [
      {
        path: "/tableCombination/table",
        name: "table",
        component: () => import("@/views/table/index.vue"),
      },
    ],
  },
]

export default tableRoutes
4、src\router\index.ts 启用 vue-router
import {createWebHistory, createRouter} from "vue-router";
import type {App} from 'vue'
// 获取所有路由
import routes from './routes'

const router = createRouter({
  routes,
  // 这里使用历史记录模式
  history: createWebHistory()
})

export const useRouter = (app: App<Element>): void => {
    app.use(router)
}

5、src\router\routes.ts 动态获取所有路由配置
/**
 * 动态加载路由配置
 */
import type { RouteRecordRaw } from "vue-router";

const modules = import.meta.glob("./modules/**/*.ts", { eager: true });

const routes = Object.keys(modules).reduce(
  (routes: RouteRecordRaw[], key: string) => {
    // @ts-ignore
    const module = modules[key].default
    console.log('module===', module);
    
    if (Array.isArray(module)) {
      return [...routes, ...module]
    } else {
      return [...routes, ...module.routes]
    }
  }, [] as RouteRecordRaw[]
);
console.log('routes===>',routes);
// 根据需要左边菜单栏排序(type文件meta和路由文件里分别加sortIdex:number/路由文件里给数字)
//export const sortRoutes =function (routes : RouteRecordRaw[]): RouteRecordRaw[] {
//  routes.sort((a :any,b :any) =>{
//    return a.meta?.sortIndex - b.meta?.sortIndex;
//  }) 
//  return routes
};


export default routes

6、src\router\type.ts
import type { RouteRecordRaw } from "vue-router"
import type { Component } from 'vue'

interface RouteRecordMeta {
  hidden?: boolean,
  icon?: Component
}

// @ts-expect-error
export interface RouteRecord extends Omit<RouteRecordRaw, 'meta'> {
  name?: string,
  meta?: RouteRecordMeta,
  children?: RouteRecord[]
}

绘制有图标可伸缩的菜单栏(重要部分)

这里需要创建一个菜单栏vue页面以及一个ts文件用来数据处理。我这个项目Naive UI组件是手动导入。所以会跟原文有些许不一样。原文没有可伸缩部分。

1、src\layouts\BasicLayout.vue
<script lang="ts">
import { useMenu } from "@/composables/useMenu";
import { ref ,defineComponent} from "vue";
import {NLayout, NLayoutSider, NScrollbar,NMenu, NCol, NSwitch} from 'naive-ui'
export default defineComponent({
  components: {
    NLayout,
    NLayoutSider,
    NScrollbar,
    NMenu,
      NCol,
      NSwitch,
    },
    setup(){
      let collapsed = ref(false)
      const { menuOptions, expandKeys, updateExpandKeys, currentMenu, updateValue } = useMenu();
      return{
        menuOptions,
        expandKeys,
        updateExpandKeys,
        currentMenu,
        updateValue,
        collapsed
      }
    }
  })
</script>

<template>
   <!-- <n-switch v-model:value="collapsed" /> -->
  <n-layout has-sider>
    <n-layout-sider
      bordered
      collapse-mode="width"
      :width="240"
      :collapsed-width="64"
      :collapsed="collapsed"
      show-trigger
      @collapse="collapsed = true"
      @expand="collapsed = false"
      :native-scrollbar="false"
    >
      <n-scrollbar>
        <n-menu
          :options="menuOptions"
          :expanded-keys="expandKeys"
          :on-update:expanded-keys="updateExpandKeys"
          :value="currentMenu"
          :on-update:value="updateValue"
        ></n-menu>
      </n-scrollbar>
    </n-layout-sider>

    <article flex-1 flex flex-col overflow-hidden>
      <section flex-1 overflow-hidden bg="#f5f6fb">
        <router-view v-slot="{ Component, route }">
          <template v-if="Component">
            <component :is="Component" :key="route.path" />
          </template>
        </router-view>
      </section>
    </article>
  </n-layout>
</template>

<style scoped></style>
2、src\composables\useMenu.ts 主要是这个文件
import type { Ref,Component  } from "vue";
import { ref, watch, h } from "vue";
import type{ MenuOption } from "naive-ui";
import { NIcon } from "naive-ui";
import routes from "@/router/routes";
// import type { RouteRecordRaw } from "vue-router";
import type { RouteRecord } from '@/router/type'

import { useRoute, RouterLink } from "vue-router";

const renderIcon = (icon: Component) => {
  return () => h(NIcon, null, { default: () => h(icon) })
}

export interface UserMenu {
  /**
   * 菜单选项
   */
  menuOptions: Ref<MenuOption[]>;
  /**
   * 展开的子菜单标识符数组
   */
  expandKeys: Ref<string[]>;
  /**
   * 更改子菜单标识符数组回调方法
   */
  updateExpandKeys: (keys: string[]) => void;
  /**
   * 当前选中的菜单
   */
  currentMenu: Ref<string>;
  /**
   * 修改选中菜单时的回调方法
   */
  updateValue: (key: string) => void;
}

/**
 * 判断路由是否只有一个子路由
 * @param route  路由
 * @returns  如果该路由只有一个子路由,则返回 true;否则返回 false
 */
const isSingleChildren = (route: RouteRecord): boolean => {
  // return route?.children?.length === 1; 
  //看需求需要一个children时是否展示上级name。false:展示父级(后期可以根据meta中字段判断某一菜单是否展示父级)
  return false;
};

/**
 * 过滤路由配置中需要在菜单中隐藏的路由
 * @param routes 路由列表
 * @returns 路由列表
 */
const filterHiddenRouter = (routes: RouteRecord[]): RouteRecord[] => {
  return routes.filter((item: RouteRecord) => {
    return !item.meta?.hidden;
  });
};

/**
 * 将路由信息转换为菜单信息
 * @param route  路由信息
 * @returns   菜单信息
 */
const getMenuOption = (route: RouteRecord[]): MenuOption | undefined => {
  const routeInfo = isSingleChildren(route) ? route.children[0] : route;
  const menuOption: MenuOption = {
    label: () => {
      if (routeInfo.children && Array.isArray(routeInfo.children)) {
        return routeInfo.name;
      } else {
        return h(
          RouterLink,
          { to: { name: routeInfo.name } },
          { default: () => routeInfo.name }
        );
      }
    },
    key: routeInfo.name as string,
    icon: routeInfo.meta?.icon ? renderIcon(routeInfo.meta?.icon as Component) : undefined
  };
  if (routeInfo.children && routeInfo.children.length > 0) {
    menuOption.children = getMenuOptions(routeInfo.children);
  }
  return menuOption;
};

const getMenuOptions = (routes: RouteRecord[]): MenuOption[] => {
  let menuOptions: MenuOption[] = [];
  filterHiddenRouter(routes).forEach((route: RouteRecord) => {
    // @ts-ignore
    const menuOption = getMenuOption(route);
    if (menuOption) {
      menuOptions.push(menuOption);
    }
  });
  return menuOptions;
};

export function useMenu(): UserMenu {
  const menus: MenuOption[] = getMenuOptions(routes);

  /**
   * 菜单选项
   */
  const menuOptions = ref(menus);

  /**
   * 展开的子菜单标识符数组
   */
  const expandKeys: Ref<string[]> = ref<string[]>([]);

  /**
   * 当前菜单
   */
  const currentMenu: Ref<string> = ref<string>("");

  const route = useRoute();
  /**
   * 监听路由变化
   */
  watch(
    () => route.path,
    () => {
      routeChanged();
    },
    { immediate: true }
  );

  /**
   * 判断路由是否包含在菜单列表中
   *
   * @param routeName 路由名称
   * @param menuList  菜单列表
   * @returns 如果包含则返回 true;否则返回 false
   */
  function menuContains(routeName: string, menuList: MenuOption[]): boolean {
    for (let menu of menuList) {
      if (menu.key === routeName) {
        return true;
      }
      if (menu.children && menu.children.length > 0) {
        const childMenuContains = menuContains(routeName, menu.children);
        if (childMenuContains) {
          return true;
        }
      }
    }
    return false;
  }

  /**
   * 路由发生变化时的回调
   */
  function routeChanged(): void {
    // 获取匹配到的路由列表
    const matched = route.matched;
    // 获取匹配到路由名称
    const matchedNames = matched
      .filter((it) => menuContains(it.name as string, menus))
      .map((it) => it.name as string);
    const matchLen = matchedNames.length;
    const matchExpandKeys = matchedNames.slice(0, matchLen - 1);
    const openKey = matchedNames[matchLen - 1];
    expandKeys.value = matchExpandKeys;
    currentMenu.value = openKey;
  }

  /**
   * 更改子菜单标识符数组回调方法
   */
  function updateExpandKeys(keys: string[]): void {
    expandKeys.value = keys
  }

  /**
   * 选中的菜单发生改变
   */
  function updateValue(key: string): void {
    currentMenu.value = key
  }

  return {
    menuOptions,
    expandKeys,
    updateExpandKeys,
    currentMenu,
    updateValue
  } as UserMenu
}

处理找不到@vicons/carbon报红、菜单栏不是整个画面样式以及遗留下来的问题

1、naive-ui 推荐使用 xicons 作为图标库。

个人理解vicons是个图标库,你想使用谁的图标引入谁的

npm i -D @vicons/fluent
npm i -D @vicons/ionicons4
npm i -D @vicons/ionicons5
npm i -D @vicons/antd
npm i -D @vicons/material
npm i -D @vicons/fa 
npm i -D @vicons/tabler
npm i -D @vicons/carbon

在这里插入图片描述

2、菜单栏不是整个画面样式

我这边是简单处理:创建了一个css文件。import '@/styles/index.css'引入main.ts
我看很多推荐使用Tailwind CSS,我还需要再研究研究。

src\styles\index.css

html,
body {
  width: 100%;
  height: 100%;
  overflow: hidden;
}

#app {
  width: 100%;
  height: 100%;
}

.n-layout {
    height: 100%;
    width: 100%;
}
3、遗留下的问题

在useMenu.ts文件有个别字段报红,运行是没问题。打包的时候会异常,解决方式是在报红行上方加// @ts-ignore
作用是:取消TypeScript的强类型校验

原地址:手摸手创建一个 Vue + Ts 项目(二) —— 实现一个左侧菜单栏

  • 21
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值