【Vue3+Vite+TS】8.0 组件六:导航菜单

必备UI组件

将用到的组件:
Menu 菜单

组件设计

新建src\components\baseline\menu\src\index.vue

<template>
    <div>Menu</div>
</template>
<script lang="ts" setup>
import { PropType } from 'vue'
import { MenuItem } from './types'

const props = defineProps({
    //说明:
    data: {
        required: true,
        type: Array as PropType<MenuItem[]>,
    },
})
console.log('data:', props.data)
</script>
<style lang="scss" scoped></style>

新建src\components\baseline\menu\src\types.ts

export interface MenuItem {
    //导航菜单的图标
    icon?: string
    //导航菜单的名字
    name: string
    //导航菜单的标识
    code: string
    //子菜单
    children?: MenuItem[]
}

新建src\components\baseline\menu\index.ts

import { App } from 'vue'
import Menu from './src/index.vue'

export { Menu }

//组件可通过use的形式使用
export default {
    Menu,
    install(app: App) {
        app.component('bs-menu', Menu)
    },
}

修改src\components\baseline\index.ts

import { App } from 'vue'
import ChooseArea from './chooseArea'
import ChooseIcon from './chooseIcon'
import Container from './container'
import Trend from './trend'
import Notification from './notification'
import List from './list'
import Menu from './menu'
const components = [
    ChooseArea,
    ChooseIcon,
    Container,
    Trend,
    Notification,
    List,
    Menu,
]
export { ChooseArea, ChooseIcon, Container, Trend, Notification, List, Menu }

//组件可通过use的形式使用
export default {
    install(app: App) {
        components.map(item => {
            app.use(item)
        })
    },
    ChooseArea,
    ChooseIcon,
    Container,
    Trend,
    Notification,
    List,
    Menu,
}

修改src\router\index.ts,新增路由参数

......
      {
                path: '/menu',
                component: () =>
                    import('../views/baseline/menu/index.vue'),
            },
......

新建src\views\baseline\menu\index.vue

<template>
    <div><bs-menu :data="data"></bs-menu></div>
</template>
<script lang="ts" setup>
let data = [
    { name: '首页', code: '1', icon: 'el-icon-document' },
    { name: '图标选择器', code: '2', icon: 'el-icon-document' },
    {
        name: '省市区选择组件',
        code: '3',
        icon: 'el-icon-document',
        children: [
            { name: '省市选择组件', code: '3-1', icon: 'el-icon-document' },
            { name: '省市区村选择组件', code: '3-2', icon: 'el-icon-document' },
        ],
    },
]
</script>
<style lang="scss" scoped></style>

如下,可见数据已经传达到基础组件:

image.png

完善组件

首先实现一级菜单。
修改src\components\baseline\menu\src\index.vue

<template>
    <div>
        <el-menu>
            <template v-for="(item, index) in data" :key="index">
                <div>
                    <el-menu-item
                        v-if="!item.children || !item.children.length"
                        :index="item.code"
                    >
                        <component v-if="item.icon" :is="item.icon"></component>
                        <span>{{ item.name }}</span>
                    </el-menu-item>
                </div>
            </template>
        </el-menu>
    </div>
</template>
<script lang="ts" setup>
import { PropType } from 'vue'
import { MenuItem } from './types'

const props = defineProps({
    //说明:
    data: {
        required: true,
        type: Array as PropType<MenuItem[]>,
    },
})
console.log('data:', props.data)
</script>
<style lang="scss" scoped></style>

修改src\views\baseline\menu\index.vue

<template>
    <div style="width: 2rem"><bs-menu :data="data"></bs-menu></div>
</template>
<script lang="ts" setup>
let data = [
    { name: '首页', code: '1', icon: 'el-icon-document' },
    { name: '图标选择器', code: '2', icon: 'el-icon-document' },
    {
        name: '省市区选择组件',
        code: '3',
        icon: 'el-icon-document',
        children: [
            { name: '省市选择组件', code: '3-1', icon: 'el-icon-document' },
            { name: '省市区村选择组件', code: '3-2', icon: 'el-icon-document' },
        ],
    },
]
</script>
<style lang="scss" scoped></style>

效果如下:


image.png

接下来实现多级菜单
修改src\components\baseline\menu\src\types.ts

export interface MenuItem {
    //导航菜单的图标
    icon?: string
    //导航菜单的名字
    name: string
    //导航菜单的标识
    index: string
    //子菜单
    children?: MenuItem[]
}

修改src\components\baseline\menu\src\index.vue

<template>
    <div>
        <el-menu
            :default-active="defaultActive"
            :router="router"
            v-bind="$attrs"
        >
            <template v-for="(item, index) in data" :key="index">
                <div>
                    <!-- 一级无二级菜单的菜单栏 -->
                    <el-menu-item
                        v-if="!item.children || !item.children.length"
                        :index="item.index"
                    >
                        <component v-if="item.icon" :is="item.icon"></component>
                        <span>{{ item.name }}</span>
                    </el-menu-item>
                    <el-sub-menu
                        v-if="item.children && item.children.length"
                        :index="item.index"
                    >
                        <template #title>
                            <component
                                v-if="item.icon"
                                :is="item.icon"
                            ></component>
                            <span>{{ item.name }}</span>
                        </template>
                        <!-- 二级菜单栏 -->
                        <el-menu-item
                            v-for="(item2, index2) in item.children"
                            :index="item2.index"
                        >
                            <component
                                v-if="item2.icon"
                                :is="item2.icon"
                            ></component>
                            <span>{{ item2.name }}</span>
                        </el-menu-item>
                    </el-sub-menu>
                </div>
            </template>
        </el-menu>
    </div>
</template>
<script lang="ts" setup>
import { PropType } from 'vue'
import { MenuItem } from './types'

const props = defineProps({
    //说明:
    data: {
        required: true,
        type: Array as PropType<MenuItem[]>,
    },
    // 默认选中的菜单
    defaultActive: {
        type: String,
        default: '',
    },
    //是否是路由模式 router,
    //是否启用 vue-router 模式
    //启用该模式会在激活导航时以 index 作为 path 进行路由跳转
    router: {
        type: Boolean,
        default: false,
    },
})
// console.log('data:', props.data)
</script>
<style lang="scss" scoped>
svg {
    margin-right: 0.04rem;
}
</style>

修改src\views\baseline\menu\index.vue

<template>
    <div style="width: 2rem">
        <bs-menu :data="data" defaultActive="3-2"></bs-menu>
    </div>
</template>
<script lang="ts" setup>
let data = [
    { name: '首页', index: '1', icon: 'el-icon-document' },
    { name: '图标选择器', index: '2', icon: 'el-icon-document' },
    {
        name: '行政区域选择组件',
        index: '3',
        icon: 'el-icon-document',
        children: [
            { name: '省市选择组件', index: '3-1', icon: 'el-icon-document' },
            { name: '省市区选择组件', index: '3-2', icon: 'el-icon-document' },
            {
                name: '省市区村选择组件',
                index: '3-3',
                icon: 'el-icon-document',
            },
        ],
    },
    0,
]
</script>
<style lang="scss" scoped></style>

实现效果如下:


image.png

v-bind="$attrs":接受父组件传入的数据和方法,并排除在组件的props中响应的参数。
具体可以参考: vue中使用v-bind="$attrs"和v-on="$listeners"进行多层组件监听

TSX实现无限层级的导航菜单

首先需要安装插件。

npm i -D @vitejs/plugin-vue-jsx

修改vite.config.ts

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
//******************* Element自动导入  *******************
//目前语言包存在报错,无法自动导出打包,暂时注释
// import AutoImport from 'unplugin-auto-import/vite'
// import Components from 'unplugin-vue-components/vite'
// import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
//******************* Element自动导入  *******************

//*******************  rollup 打包体积分析插件可视化工具  *******************
import { visualizer } from 'rollup-plugin-visualizer'
//*******************  rollup 打包体积分析插件可视化工具  *******************
// https://vitejs.dev/config/
export default defineConfig({
    plugins: [
        vue(),
        vueJsx(),
        //******************* Element自动导入  *******************
        // AutoImport({
        //     resolvers: [ElementPlusResolver()],
        // }),
        // Components({
        //     resolvers: [ElementPlusResolver()],
        // }),
        //******************* Element自动导入  *******************

        //******************* 打包插件可视化工具  *******************
        visualizer(),
        //******************* 打包插件可视化工具  *******************
    ],
    resolve: {
        alias: {
            '@': '/src',
            '@style': '/src/style',
            '@com': '/src/components',
            '@baseline': '/src/components/baseline',
            '@business': '/src/components/business',
        },
    },
    server: {
        port: 8080,
    },
})

这里也顺便提一下tsconfig.json的配置:

{
    // 指定要编译的路径列表
    "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
    "compilerOptions": {
        // target用于指定编译之后的版本目录
        "target": "esnext",
        "useDefineForClassFields": true,
        //module用来指定要使用的模板标准
        "module": "esnext",
        // 用于选择模块解析策略,有"node"和"classic"两种类型
        "moduleResolution": "node",
        "strict": true,
        // 指定jsx代码用于的开发环境:'preserve','react-native',or 'react
        "jsx": "preserve",
        // 指定是否将map文件内容和js文件编译在一个同一个js文件中
        // 如果设为true,则map的内容会以//#soureMappingURL=开头,然后接base64字符串的形式插入在js文件底部
        "sourceMap": true,
        "skipLibCheck": true,
        "resolveJsonModule": true,
        //通过导入内容创建命名空间,实现CommonJS和ES模块之间的互操作性
        "esModuleInterop": true,
        "forceConsistentCasingInFileNames": true,
        // 指定是否引入tslib里的复制工具函数,默认为false
        "importHelpers": true,
        // 指定是否将每个文件作为单独的模块,默认为true,他不可以和declaration同时设定
        "isolatedModules": true,
        // removeComments用于指定是否将编译后的文件注释删掉,设为true的话即删除注释,默认为false
        "removeComments": true,
        // lib用于指定要包含在编译中的库文件
        "lib": ["esnext", "dom"],
        //指定全局组件类型:element:"element-plus/global"
        "types": ["vite/client", "element-plus/global"],
        // ++ 这里加上baseUrl 和 path即可 ++
        // 用于设置解析非相对模块名称的基本目录,相对模块不会受到baseUrl的影响
        "baseUrl": "./",
        //用于设置模块名到基于baseUrl的路径映射
        "paths": {
            // 根据别名配置相关路径
            "@/*": ["./src/*"],
            "@style/*": ["./src/style/*"],
            "@com/*": ["./src/components/*"],
            "@baseline/*": ["./src/components/baseline/*"],
            "@business/*": ["./src/components/business/*"]
        }
        //****************** 未配置选项 ******************
        // 用来指定是否允许编译JS文件,默认false,即不编译JS文件
        // "allowJs": false,
        // 用来指定是否在编译的时候生成相的d.ts声明文件
        // 如果设为true,编译每个ts文件之后会生成一个js文件和一个声明文件,但是declaration和allowJs不能同时设为true
        // "declaration": true,
        // 用来指定编译时是否生成.map文件
        // "declarationMap": true,
        // 用来指定输出文件夹,值为一个文件夹路径字符串,输出的文件都将放置在这个文件夹
        // "outDir": "./",
        // 用来指定输出文件夹,值为一个文件夹路径字符串,输出的文件都将放置在这个文件夹
        // "outDir": "./",
        // 用于指定输出文件合并为一个文件
        //只有设置module的值为amd和system模块时才支持这个配置
        // "outFile": "./",
        // 是否编译构建引用项目
        // "composite": true,
        // 不生成编译文件
        // "noEmit": true,
        // 当target为"ES5"或"ES3"时,为"for-of" "spread"和"destructuring"中的迭代器提供完全支持
        // "downlevelIteration": true,
        // 用于指定是否启动所有类型检查,如果设为true这回同时开启下面这几个严格检查,默认为false
        // "strict": true,
        // 如果我们没有一些值设置明确类型,编译器会默认认为这个值为any类型,如果将noImplicitAny设为true,则如果没有设置明确的类型会报错,默认值为false
        // "noImplicitAny": true,
        // 当设为true时,null和undefined值不能赋值给非这两种类型的值,别的类型的值也不能赋给他们,除了any类型,还有个例外就是undefined可以赋值给void类型
        // "strictNullChecks": true,
        // 用来指定是否使用函数参数双向协变检查
        // "strictFunctionTypes": true,
        // 设为true后对bind、call和apply绑定的方法的参数的检测是严格检测
        // "strictBindCallApply": true,
        // 设为true后会检查类的非undefined属性是否已经在构造函数里初始化,如果要开启这项,需要同时开启strictNullChecks,默认为false
        // "strictPropertyInitialization": true,
        // 当this表达式的值为any类型的时候,生成一个错误
        // "noImplicitThis": true,
        // alwaysStrict指定始终以严格模式检查每个模块,并且在编译之后的JS文件中加入"use strict"字符串,用来告诉浏览器该JS为严格模式
        // "alwaysStrict": true,
        // 用于检查是否有定义了但是没有使用变量,对于这一点的检测,使用ESLint可以在你书写代码的时候做提示,你可以配合使用,他的默认值为false
        // "noUnusedLocals": true,
        // 用于检测是否在函数中没有使用的参数
        // "noUnusedParameters": true,
        // 用于检查函数是否有返回值,设为true后,如果函数没有返回值则会提示,默认为false
        // "noImplicitReturns": true,
        // 用于检查switch中是否有case没有使用break跳出switch,默认为false
        // "noFallthroughCasesInSwitch": true,
        // 可以指定一个路径列表,在构建时编译器会将这个路径中的内容都放到一个文件夹中
        // "rootDirs": [],
        // 用来指定声明文件或文件夹的路径列表,如果指定了此项,则只有在这里列出的声明文件才会被加载
        // "typeRoots": [],
        // types用于指定需要包含的模块,只有在这里列出的模块的声明文件才会被加载
        // "types": [],
        // 用来指定允许从没有默认导出的模块中默认导入
        // "allowSyntheticDefaultImports": true,
        // 不把符号链接解析为真实路径,具体可以了解下webpack和node.js的symlink相关知识
        // "preserveSymlinks": true,
        // 用于指定调试器应该找到TypeScript文件而不是源文件的位置,这个值会被写进.map文件里
        // "sourceRoot": "",
        // 用于指定调试器找到映射文件而非生成文件的位置,指定map文件的根路径
        // 该选项会影响.map文件中的sources属性
        // "mapRoot": "",
        // s用于指定是否进一步将ts文件的内容也包含到输出文件中
        // "inlineSources": true,
        // 用于指定是否启用实验性的装饰器特性
        // "experimentalDecorators": true,
        // 用于指定是否为装上去提供元数据支持
        // 关于元数据,也是ES6的新标准,可以通过Reflect提供的静态方法获取元数据
        // 如果需要使用Reflect的一些方法,需要引用ES2015.Reflect这个库
        // "emitDecoratorMetadata": true,
        // 可以配置一个数组列表
        // "files":[],
        // exclude表示要排除的,不编译的文件
        // "exclude":[]
        // include也可以指定要编译的路径列表
        // "include":[],
        //它也可以指定一个列表,规则和include一样,可以是文件可以是文件夹,可以是相对路径或绝对路径,可以使用通配符
        // "exclude":[]
        // 可以通过指定一个其他的tsconfig.json文件路径,来继承这个配置文件里的配置,继承来的文件的配置会覆盖当前文件定义的配置
        // "extends":""
        // 如果设为true,在我们编辑了项目文件保存的时候,编辑器会根据tsconfig.json的配置更新重新生成文本,不过这个编辑器支持
        // "compileOnSave":true
        // 一个对象数组,指定要引用的项目
        // "references":[]
        //****************** 未配置选项 ******************
    }
}

新建src\components\baseline\menu\src\menu.tsx

import { defineComponent, PropType } from 'vue'
import { MenuItem } from './types'

export default defineComponent({
    name: 'infiniteMenu',
    props: {
        //说明:
        data: {
            required: true,
            type: Array as PropType<MenuItem[]>,
        },
        // 默认选中的菜单
        defaultActive: {
            type: String,
            default: '',
        },
        //是否是路由模式 router,
        //是否启用 vue-router 模式
        //启用该模式会在激活导航时以 index 作为 path 进行路由跳转
        router: {
            type: Boolean,
            default: false,
        },
    },
    setup(props, ctx) {
        return () => {
            return <div>menus</div>
        }
    },
})

修改src\views\baseline\menu\index.vue

<template>
    <div style="width: 2rem">
        <!-- <bs-menu :data="data" defaultActive="3-2"></bs-menu> -->
        <bs-infinite-menu :data="data2"></bs-infinite-menu>
    </div>
</template>
<script lang="ts" setup>
let data = [
    { name: '首页', index: '1', icon: 'el-icon-document' },
    { name: '图标选择器', index: '2', icon: 'el-icon-document' },
    {
        name: '行政区域选择组件',
        index: '3',
        icon: 'el-icon-document',
        children: [
            { name: '省市选择组件', index: '3-1', icon: 'el-icon-document' },
            { name: '省市区选择组件', index: '3-2', icon: 'el-icon-document' },
            {
                name: '省市区村选择组件',
                index: '3-3',
                icon: 'el-icon-document',
            },
        ],
    },
    0,
]
let data2 = [
    { name: '首页', index: '1', icon: 'el-icon-document' },
    { name: '图标选择器', index: '2', icon: 'el-icon-document' },
    {
        name: '行政区域选择组件',
        index: '3',
        icon: 'el-icon-document',
        children: [
            {
                name: '省市选择组件',
                index: '3-1',
                icon: 'el-icon-document',
                children: [
                    {
                        name: '组件演示',
                        index: '3-1-1',
                        icon: 'el-icon-document',
                    },
                ],
            },
            { name: '省市区选择组件', index: '3-2', icon: 'el-icon-document' },
            {
                name: '省市区村选择组件',
                index: '3-3',
                icon: 'el-icon-document',
            },
        ],
    },
    0,
]
</script>
<style lang="scss" scoped></style>

本文章由javascript技术分享原创和收集

发表评论 (审核通过后显示评论):