Vue3+TypeScript实现递归菜单组件的完整实例

目录
  • 前言
  • 需求
  • 实现
    • 首次渲染
    • 点击菜单项
    • 样式区分
    • 默认高亮
    • 数据源变动引发的 bug
  • 完整代码
    • App.vue
  • 总结

前言

小伙伴们好久不见,最近刚入职新公司,需求排的很满,平常是实在没时间写文章了,更新频率会变得比较慢。

周末在家闲着无聊,突然小弟过来紧急求助,说是面试腾讯的时候,对方给了个 Vue 的递归菜单要求实现,回来找我复盘。

正好这周是小周,没想着出去玩,就在家写写代码吧,我看了一下需求,确实是比较复杂,需要利用好递归组件,正好趁着这

个机会总结一篇 Vue3 + TS 实现递归组件的文章。

需求

可以先在Github Pages中预览一下效果。

需求是这样的,后端会返回一串可能有无限层级的菜单,格式如下:

[
  {
    id: 1,
    father_id: 0,
    status: 1,
    name: '生命科学竞赛',
    _child: [
      {
        id: 2,
        father_id: 1,
        status: 1,
        name: '野外实习类',
        _child: [{ id: 3, father_id: 2, status: 1, name: '植物学' }],
      },
      {
        id: 7,
        father_id: 1,
        status: 1,
        name: '科学研究类',
        _child: [
          { id: 8, father_id: 7, status: 1, name: '植物学与植物生理学' },
          { id: 9, father_id: 7, status: 1, name: '动物学与动物生理学' },
          { id: 10, father_id: 7, status: 1, name: '微生物学' },
          { id: 11, father_id: 7, status: 1, name: '生态学' },
        ],
      },
      { id: 71, father_id: 1, status: 1, name: '添加' },
    ],
  },
  {
    id: 56,
    father_id: 0,
    status: 1,
    name: '考研相关',
    _child: [
      { id: 57, father_id: 56, status: 1, name: '政治' },
      { id: 58, father_id: 56, status: 1, name: '外国语' },
    ],
  },
]

1、每一层的菜单元素如果有 _child 属性,这一项菜单被选中以后就要继续展示这一项的所有子菜单,预览一下动图:

2、并且点击其中的任意一个层级,都需要把菜单的 完整的 id 链路 传递到最外层,给父组件请求数据用。比如点击了 科学研究类。那么向外 emit 的时候还需要带上它的第一个子菜单 植物学与植物生理学 的 id,以及它的父级菜单 生命科学竞赛 的 id,也就是 [1, 7, 8]。

3、每一层的样式还可以自己定制。

实现

这很显然是一个递归组件的需求,在设计递归组件的时候,我们要先想清楚数据到视图的映射。

在后端返回的数据中,数组的每一层可以分别对应一个菜单项,那么数组的层则就对应视图中的一行,当前这层的菜单中,被点击选中 的那一项菜单的 child 就会被作为子菜单数据,交给递归的 NestMenu 组件,直到某一层的高亮菜单不再有 child,则递归终止。

由于需求要求每一层的样式可能是不同的,所以再每次调用递归组件的时候,我们都需要从父组件的 props 中拿到一个 depth 代表层级,并且把这个 depth + 1 继续传递给递归的 NestMenu 组件。

重点主要就是这些,接下来编码实现。

先看 NestMenu 组件的 template 部分的大致结构:

<template>
  <div class="wrap">
    <div class="menu-wrap">
      <div
        class="menu-item"
        v-for="menuItem in data"
      >{{menuItem.name}}</div>
    </div>
    <nest-menu
      :key="activeId"
      :data="subMenu"
      :depth="depth + 1"
    ></nest-menu>
  </div>
</template>

和我们预想设计中的一样, menu-wrap 代表当前菜单层, nest-menu 则就是组件本身,它负责递归的渲染子组件。

首次渲染

在第一次获取到整个菜单的数据的时候,我们需要先把每层菜单的选中项默认设置为第一个子菜单,由于它很可能是异步获取的,所以我们最好是 watch 这个数据来做这个操作。

// 菜单数据源发生变化的时候 默认选中当前层级的第一项
const activeId = ref<number | null>(null)

watch(
  () => props.data,
  (newData) => {
    if (!activeId.value) {
      if (newData && newData.length) {
        activeId.value = newData[0].id
      }
    }
  },
  {
    immediate: true,
  }
)

现在我们从最上层开始讲起,第一层的 activeId 被设置成了 生命科学竞赛 的 id,注意我们传递给递归子组件的 data ,也就是 生命科学竞赛 的 child,是通过 subMenu 获取到的,它是一个计算属性:

const getActiveSubMenu = () => {
  return data.find(({ id }) => id === activeId.value)._child
}
const subMenu = computed(getActiveSubMenu)

这样,就拿到了 生命科学竞赛 的 child,作为子组件的数据传递下去了。

点击菜单项

回到之前的需求设计,在点击了菜单项后,无论点击的是哪层,都需要把完整的 id 链路通过 emit 传递到最外层去,所以这里我们需要多做一些处理:

/**
 * 递归收集子菜单第一项的 id
 */
const getSubIds = (child) => {
  const subIds = []
  const traverse = (data) => {
    if (data && data.length) {
      const first = data[0]
      subIds.push(first.id)
      traverse(first._child)
    }
  }
  traverse(child)
  return subIds
}

const onMenuItemClick = (menuItem) => {
  const newActiveId = menuItem.id
  if (newActiveId !== activeId.value) {
    activeId.value = newActiveId
    const child = getActiveSubMenu()
    const subIds = getSubIds(child)
    // 把子菜单的默认第一项 ids 也拼接起来 向父组件 emit
    context.emit('change', [newActiveId, ...subIds])
  }
}

由于我们之前定的规则是,点击了新的菜单以后默认选中子菜单的第一项,所以这里我们也递归去找子菜单数据里的第一项,放到 subIds 中,直到最底层。

注意这里的 context.emit("change", [newId, ...subIds]);,这里是把事件向上 emit,如果这个菜单是中间层级的菜单,那么它的父组件也是 NestMenu,我们需要在父层级递归调用 NestMenu 组件的时候监听这个 change 事件。

<nest-menu
    :key="activeId"
    v-if="activeId !== null"
    :data="getActiveSubMenu()"
    :depth="depth + 1"
    @change="onSubActiveIdChange"
></nest-menu>

在父层级的菜单接受到了子层级的菜单的 change 事件后,需要怎么做呢?没错,需要进一步的再向上传递:

const onSubActiveIdChange = (ids) => {
  context.emit('change', [activeId.value].concat(ids))
}

这里就只需要简单的把自己当前的 activeId 拼接到数组的最前面,再继续向上传递即可。

这样,任意一层的组件点击了菜单后,都会先用自己的 activeId 拼接好所有子层级的默认 activeId,再一层层向上 emit。并且向上的每一层父菜单都会把自己的 activeId 拼在前面,就像接力一样。

最后,我们在应用层级的组件里,就可以轻松的拿到完整的 id 链路:

<template>
  <nest-menu :data="menu" @change="activeIdsChange" />
</template>

export default {
  methods: {
    activeIdsChange(ids) {
      this.ids = ids;
      console.log("当前选中的id路径", ids);
  },
},

样式区分

由于我们每次调用递归组件的时候,都会把 depth + 1,那么就可以通过把这个数字拼接到类名后面来实现样式区分了。

<template>
  <div class="wrap">
    <div class="menu-wrap" :class="`menu-wrap-${depth}`">
      <div class="menu-item">{{menuItem.name}}</div>
    </div>
    <nest-menu />
  </div>
</template>

<style>
.menu-wrap-0 {
  background: #ffccc7;
}

.menu-wrap-1 {
  background: #fff7e6;
}

.menu-wrap-2 {
  background: #fcffe6;
}
</style>

默认高亮

上面的代码写完后,应对没有默认值时的需求已经足够了,这时候面试官说,产品要求这个组件能通过传入任意一个层级的 id 来默认展示高亮。

其实这也难不倒我们,稍微改造一下代码,在父组件里假设我们通过 url 参数或者任意方式拿到了一个 activeId,先通过深度优先遍历的方式查找到这个 id 的所有父级。

const activeId = 7

const findPath = (menus, targetId) => {
  let ids

  const traverse = (subMenus, prev) => {
    if (ids) {
      return
    }
    if (!subMenus) {
      return
    }
    subMenus.forEach((subMenu) => {
      if (subMenu.id === activeId) {
        ids = [...prev, activeId]
        return
      }
      traverse(subMenu._child, [...prev, subMenu.id])
    })
  }

  traverse(menus, [])

  return ids
}

const ids = findPath(data, activeId)

这里我选择在递归的时候带上上一层的 id,在找到了目标 id 以后就能轻松的拼接处完整的父子 id 数组。

然后我们把构造好的 ids 作为 activeIds 传递给 NestMenu,此时这时候 NestMenu 就要改变一下设计,成为一个「受控组件」,它的渲染状态是受我们外层传递的数据控制的。

所以我们需要在初始化参数的时候改变一下取值逻辑,优先取 activeIds[depth] ,并且在点击菜单项的时候,要在最外层的页面组件中,接收到 change 事件时,把 activeIds 的数据同步改变。这样继续传递下去才不会导致 NestMenu 接收到的数据混乱。

<template>
  <nest-menu :data="data" :defaultActiveIds="ids" @change="activeIdsChange" />
</template>

NestMenu 初始化的时候,对有默认值的情况做一下处理,优先使用数组中取到的 id 值。

setup(props: IProps, context) {
  const { depth = 0, activeIds } = props;

  /**
   * 这里 activeIds 也可能是异步获取到的 所以用 watch 保证初始化
   */
  const activeId = ref<number | null | undefined>(null);
  watch(
    () => activeIds,
    (newActiveIds) => {
      if (newActiveIds) {
        const newActiveId = newActiveIds[depth];
        if (newActiveId) {
          activeId.value = newActiveId;
        }
      }
    },
    {
      immediate: true,
    }
  );
}

这样,如果 activeIds 数组中取不到的话,默认还是 null,在 watch 到菜单数据变化的逻辑中,如果 activeId 是 null 的话,会被初始化为第一个子菜单的 id。

watch(
  () => props.data,
  (newData) => {
    if (!activeId.value) {
      if (newData && newData.length) {
        activeId.value = newData[0].id
      }
    }
  },
  {
    immediate: true,
  }
)

在最外层页面容器监听到 change 事件的时候,要把数据源同步一下:

<template>
  <nest-menu :data="data" :activeIds="ids" @change="activeIdsChange" />
</template>

<script>
import { ref } from "vue";

export default {
  name: "App",
  setup() {
    const activeIdsChange = (newIds) => {
      ids.value = newIds;
    };

    return {
      ids,
      activeIdsChange,
    };
  },
};
</script>

如此一来,外部传入 activeIds 的时候,就可以控制整个 NestMenu 的高亮选中逻辑了。

数据源变动引发的 bug

这时候,面试官对着你的 App 文件稍作改动,然后演示了这样一个 bug:

App.vue 的 setup 函数中加了这样的一段逻辑:

onMounted(() => {
  setTimeout(() => {
    menu.value = [data[0]].slice()
  }, 1000)
})

也就是说,组件渲染完成后过了一秒,菜单的最外层只剩下一项了,这时候面试官在一秒之内点击了最外层的第二项,这个组件在数据源改变之后,会报错:

这是因为数据源已经改变了,但是组件内部的 activeId 状态依然停留在了一个已经不存在了的 id 上。

这会导致 subMenu 这个 computed 属性在计算时出错。

我们对 watch data 观测数据源的这段逻辑稍加改动:

watch(
  () => props.data,
  (newData) => {
    if (!activeId.value) {
      if (newData && newData.length) {
        activeId.value = newData[0].id
      }
    }
    // 如果当前层级的 data 中遍历无法找到 `activeId` 的值 说明这个值失效了
    // 把它调整成数据源中第一个子菜单项的 id
    if (!props.data.find(({ id }) => id === activeId.value)) {
      activeId.value = props.data?.[0].id
    }
  },
  {
    immediate: true,
    // 在观测到数据变动之后 同步执行 这样会防止渲染发生错乱
    flush: 'sync',
  }
)

注意这里的 flush: "sync" 很关键,Vue3 对于 watch 到数据源变动之后触发 callback 这一行为,默认是以 post 也就是渲染之后再执行的,但是在当前的需求下,如果我们用错误的 activeId 去渲染,就会直接导致报错了,所以我们需要手动把这个 watch 变成一个同步行为。

这下再也不用担心数据源变动导致渲染错乱了。

完整代码

App.vue

<template>
  <nest-menu :data="data" :activeIds="ids" @change="activeIdsChange" />
</template>

<script>
import { ref } from "vue";
import NestMenu from "./components/NestMenu.vue";
import data from "./menu.js";
import { getSubIds } from "./util";

export default {
  name: "App",
  setup() {
    // 假设默认选中 id 为 7
    const activeId = 7;

    const findPath = (menus, targetId) => {
      let ids;

      const traverse = (subMenus, prev) => {
        if (ids) {
          return;
        }
        if (!subMenus) {
          return;
        }
        subMenus.forEach((subMenu) => {
          if (subMenu.id === activeId) {
            ids = [...prev, activeId];
            return;
          }
          traverse(subMenu._child, [...prev, subMenu.id]);
        });
      };

      traverse(menus, []);

      return ids;
    };

    const ids = ref(findPath(data, activeId));

    const activeIdsChange = (newIds) => {
      ids.value = newIds;
      console.log("当前选中的id路径", newIds);
    };

    return {
      ids,
      activeIdsChange,
      data,
    };
  },
  components: {
    NestMenu,
  },
};
</script>

NestMenu.vue

<template>
  <div class="wrap">
    <div class="menu-wrap" :class="`menu-wrap-${depth}`">
      <div
        class="menu-item"
        v-for="menuItem in data"
        :class="getActiveClass(menuItem.id)"
        @click="onMenuItemClick(menuItem)"
        :key="menuItem.id"
      >{{menuItem.name}}</div>
    </div>
    <nest-menu
      :key="activeId"
      v-if="subMenu && subMenu.length"
      :data="subMenu"
      :depth="depth + 1"
      :activeIds="activeIds"
      @change="onSubActiveIdChange"
    ></nest-menu>
  </div>
</template>

<script lang="ts">
import { watch, ref, onMounted, computed } from "vue";
import data from "../menu";

interface IProps {
  data: typeof data;
  depth: number;
  activeIds?: number[];
}

export default {
  name: "NestMenu",
  props: ["data", "depth", "activeIds"],
  setup(props: IProps, context) {
    const { depth = 0, activeIds, data } = props;

    /**
     * 这里 activeIds 也可能是异步获取到的 所以用 watch 保证初始化
     */
    const activeId = ref<number | null | undefined>(null);
    watch(
      () => activeIds,
      (newActiveIds) => {
        if (newActiveIds) {
          const newActiveId = newActiveIds[depth];
          if (newActiveId) {
            activeId.value = newActiveId;
          }
        }
      },
      {
        immediate: true,
        flush: 'sync'
      }
    );

    /**
     * 菜单数据源发生变化的时候 默认选中当前层级的第一项
     */
    watch(
      () => props.data,
      (newData) => {
        if (!activeId.value) {
          if (newData && newData.length) {
            activeId.value = newData[0].id;
          }
        }
        // 如果当前层级的 data 中遍历无法找到 `activeId` 的值 说明这个值失效了
        // 把它调整成数据源中第一个子菜单项的 id
        if (!props.data.find(({ id }) => id === activeId.value)) {
          activeId.value = props.data?.[0].id;
        }
      },
      {
        immediate: true,
        // 在观测到数据变动之后 同步执行 这样会防止渲染发生错乱
        flush: "sync",
      }
    );

    const onMenuItemClick = (menuItem) => {
      const newActiveId = menuItem.id;
      if (newActiveId !== activeId.value) {
        activeId.value = newActiveId;
        const child = getActiveSubMenu();
        const subIds = getSubIds(child);
        // 把子菜单的默认第一项 ids 也拼接起来 向父组件 emit
        context.emit("change", [newActiveId, ...subIds]);
      }
    };
    /**
     * 接受到子组件更新 activeId 的同时
     * 需要作为一个中介告知父组件 activeId 更新了
     */
    const onSubActiveIdChange = (ids) => {
      context.emit("change", [activeId.value].concat(ids));
    };
    const getActiveSubMenu = () => {
      return props.data?.find(({ id }) => id === activeId.value)._child;
    };
    const subMenu = computed(getActiveSubMenu);

    /**
     * 样式相关
     */
    const getActiveClass = (id) => {
      if (id === activeId.value) {
        return "menu-active";
      }
      return "";
    };

    /**
     * 递归收集子菜单第一项的 id
     */
    const getSubIds = (child) => {
      const subIds = [];
      const traverse = (data) => {
        if (data && data.length) {
          const first = data[0];
          subIds.push(first.id);
          traverse(first._child);
        }
      };
      traverse(child);
      return subIds;
    };

    return {
      depth,
      activeId,
      subMenu,
      onMenuItemClick,
      onSubActiveIdChange,
      getActiveClass,
    };
  },
};
</script>

<style>
.wrap {
  padding: 12px 0;
}

.menu-wrap {
  display: flex;
  flex-wrap: wrap;
}

.menu-wrap-0 {
  background: #ffccc7;
}

.menu-wrap-1 {
  background: #fff7e6;
}

.menu-wrap-2 {
  background: #fcffe6;
}

.menu-item {
  margin-left: 16px;
  cursor: pointer;
  white-space: nowrap;
}

.menu-active {
  color: #f5222d;
}
</style>

源码地址

github.com/sl1673495/v

总结

一个递归的菜单组件,说简单也简单,说难也有它的难点。如果我们不理解 Vue 的异步渲染和观察策略,可能中间的 bug 就会困扰我们许久。所以适当学习原理还是挺有必要的。

在开发通用组件的时候,一定要注意数据源的传入时机(同步、异步),对于异步传入的数据,要利用好 watch 这个 API 去观测变动,做相应的操作。并且要考虑数据源的变化是否会和组件内原来保存的状态冲突,在适当的时机要做好清理操作。

另外留下一个小问题,我在 NestMenu 组件 watch 数据源的时候,选择这样去做:

watch((() => props.data);

而不是解构后再去观测:

const { data } = props;
watch(() => data);

这两者之间有区别吗?这又是一道考察深度的面试题。

到此这篇关于Vue3+TypeScript实现递归菜单组件的文章就介绍到这了,更多相关Vue3+TypeScript递归菜单组件内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 用 Vue.js 递归组件实现可折叠的树形菜单(demo)

    在Vue.js中一个递归组件调用的是其本身,如: Vue.component('recursive-component', { template: `<!--Invoking myself!--> <recursive-component></recursive-component>` }); 递归组件常用于在blog上显示注释.嵌套的菜单,或者基本上是父和子相同的类型,尽管具体内容不同.例如: 现在给您演示一下如何有效地使用递归组件,我将通过建立一个可扩展/收缩的树形

  • Vue2递归组件实现树形菜单

    今天看了老长时间递归组件,官方给的教程太简便了,根本看不出到底怎么用.于是自己查网摸索了一下,这儿只把核心思想写出来. 效果如下图,点击后打开二级菜单,再点击后打开三级. //js //引子 //思想:当v-if='false'时,循环时进行的.所以一开始就设置为false. ggg:{ name:'gs', template:` <div> <p @click.stop='show=!show'>我是p标签</p> //这儿show必须要初始值为false,不然就是堆

  • Vue.js递归组件构建树形菜单

    在Vue.js中一个递归组件调用的是其本身,如: Vue.component('recursive-component', { template: `<!--Invoking myself!--> <recursive-component></recursive-component> }); 递归组件常用于在blog上显示注释.嵌套的菜单,或者基本上是父和子相同的类型,尽管具体内容不同.例如: 现在给您演示一下如何有效地使用递归组件,我将通过建立一个可扩展/收缩的树形菜

  • vuejs实现递归树型菜单组件

    前言 前段时间使用vue做了一套后台管理系统,其中使用最多就是递归组件,也因为自己对官方文档的不熟悉使得自己踩了不少坑,今天写出来和大家一起分享. 递归组件 组件在它的模板内可以递归地调用自己,只有当它有 name 选项时才可以. 在官网这句话就是关键定义组件是一定要有name属性. 模拟数据格式如下: [ { "id": "1", "menuName": "基础管理", "menuCode": "

  • Vue 递归多级菜单的实例代码

    考虑以下菜单数据: [ { name: "About", path: "/about", children: [ { name: "About US", path: "/about/us" }, { name: "About Comp", path: "/about/company", children: [ { name: "About Comp A", path:

  • Vue递归实现树形菜单方法实例

    什么是树形菜单还是要简单的啰嗦一下,比如: 上图是截图自elementui的实例,实现方式是用文档结构(类似像原生Dom文档结构的写法)的方式,好处就是很灵活,可以方便的自定义,作为一个通用视图组件库这是正确的做法. 在实际的企业应用中,菜单要比这复杂很多,层次也要多很多,如果我们采取手动编写文档结构的方式,会导致代码亢长,阅读和维护都很低效.毫无疑问所有Vuer都会想到用一个数据结构来驱动文档结构.vue-router的数据结构恰恰就是完美的嵌套层次结构(树结构),同时vue文档中也提到了递归

  • Vue.js 递归组件实现树形菜单(实例分享)

    最近看了 Vue.js 的递归组件,实现了一个最基本的树形菜单. 项目结构: main.js 作为入口,很简单: import Vue from 'vue' Vue.config.debug = true import main from './components/main.vue' new Vue({ el: '#app', render: h => h(main) }) 它引入了一个组件 main.vue: <template> <div class="tree-m

  • vue左侧菜单,树形图递归实现代码

    学习vue有一段时间了,最近使用vue做了一套后台管理系统,左侧菜单需求是这样的,可以多层,数据由后台传递.也因为自己对官方文档的不熟悉使得自己踩了不少坑,今天写出来和大家一起分享. 效果图如下所示: 先说说遇到的坑,由于是子父组件,当时传递使用的是子父组件的传递,但是这时候只能获取到第一层的数据,第二层往后获取不到数据,踩了一下午坑以后才知道,子组件使用了递归组件,这时候他已经不能往父组件传递了,子传父,只能逐层传递这时候已经隔层了,所以我使用了非子父组件传递,两个页面都引入bus.js,这里

  • Vue3+TypeScript实现递归菜单组件的完整实例

    目录 前言 需求 实现 首次渲染 点击菜单项 样式区分 默认高亮 数据源变动引发的 bug 完整代码 App.vue 总结 前言 小伙伴们好久不见,最近刚入职新公司,需求排的很满,平常是实在没时间写文章了,更新频率会变得比较慢. 周末在家闲着无聊,突然小弟过来紧急求助,说是面试腾讯的时候,对方给了个 Vue 的递归菜单要求实现,回来找我复盘. 正好这周是小周,没想着出去玩,就在家写写代码吧,我看了一下需求,确实是比较复杂,需要利用好递归组件,正好趁着这 个机会总结一篇 Vue3 + TS 实现递

  • vue3中的伸缩菜单组件

    目录 vue3伸缩菜单组件 效果图 总结 vue3伸缩菜单组件 最近一直在学习分装组件,学到了一个伸缩菜单栏组件,浅浅的记录一下,想要封装菜单的,代码可以直接拿去用,稍作修改即可! 效果图 1.在components下面创建一个container的文件,在container文件下面创建一个src文件,然后在src文件下创建index.vue文件 这个文件里写入 <template> <div class="common-layout"> <el-conta

  • jQuery进行组件开发完整实例

    本文实例讲述了jQuery进行组件开发的方法,分享给大家供大家参考,具体如下: 前面的<JavaScript组件开发>分析了JavaScript进行组件开发的技巧,这里分析使用jQuery进行组件开发的方法. 使用jQuery进行组件开发和使用纯JavaScript脚本(不使用框架)原理基本类似,特别是公共方法的组织是一样的. 不同点是,jQuery使用了插件机制,通过$()直接进行操作对象(DOM元素)绑定,然后对DOM元素或HTML代码进行绑定事件等的操作. 另一个不同点则是把jQuery

  • Vue封装全局toast组件的完整实例

    目录 前言 一. 借助 vue-cli 1. 定义 Toast 组件 2. 在 main.js 里面配置 3. 在其他组件内使用 二.不借助 vue-cli 1. 注册 toast 组件 2. 注册 toast 插件 3. 在其他组件内使用 总结 前言 最近体验了下Vue,Toast都是前端常用组件,本文详细介绍了Vue封装全局toast组件的过程,下面话不多说了,来一起看看详细的介绍吧 一. 借助 vue-cli 1. 定义 Toast 组件 // components/Toast <temp

  • vue3 element-plus二次封装组件系列之伸缩菜单制作

    目录 1.效果 2.主要逻辑代码 1.效果 折叠效果--只剩图标 展开效果--有图标有文字 2.主要逻辑代码 home.vue--主页代码 <template> <div class="common-layout"> <el-container> <!-- 侧边栏菜单 --> <el-aside width="auto"> <nav-menu :collpase="state.isColla

  • Vue3 封装 Element Plus Menu 无限级菜单组件功能的详细代码

    目录 1 数据结构定义 1.1 菜单项数据结构 1.2 菜单配置数据结构 2 使用 tsx 实现封装 2.1 tsx 基本结构 2.2 定义 prop 2.3 递归实现组件 3 使用 SFC 实现菜单封装 3.1 封装菜单项的渲染 3.2 封装菜单组件 4 测试组件 4.1 菜单测试数据 4.2 测试页面 4.3 运行效果 总结: 本文分别使用 SFC(模板方式)和 tsx 方式对 Element Plus el-menu 组件进行二次封装,实现配置化的菜单,有了配置化的菜单,后续便可以根据路由

  • vue3中单文件组件<script setup>实例详解

    目录 一.相比普通script语法的优势 二.基本语法 三.响应式 四.使用组件 五.使用自定义指令 六.defineProps 和 defineEmits 七.defineExpose 八.useSlots 和 useAttrs 九.顶层 await 附:<script setup>和 <script>之间的主要区别 总结 一.相比普通script语法的优势 <script setup>是在单文件组件 (SFC) 中使用组合式 API的编译时语法糖.相比于普通的 &l

  • 使用Vite+Vue3+TypeScript 搭建开发脚手架的详细过程

    目录 Vite前端开发与构建工具 Vue3 与 Vue2区别 TypeScript 使用Vite创建脚手架 1.创建项目文件夹 2.选择Vue 3.选择TypeScript 4.完成后可以看到项目文件夹(my-vue-app) 配置文件引用别名 alias 修改 vite.config.ts 文件配置(此时:会报错 path 未定义,接下来定义path) 定义path,修改tsconfig.json 安装css处理器插件scss 配置全局scss样式(在src/assets 下创建style 文

  • vue3+typeScript穿梭框的实现示例

    前言 实现功能:模仿element穿梭框的简单功能 每周分享一个vue3+typeScript的小组件,我只想分享下自己的实现思路,楼主是个菜鸡前端,记录下实现过程,说不定对你有帮助. 效果展示 预览地址 github地址 开发过程 思路:用两个数组分别记录左右框框里面的值,根据复选框选中状态来实现删除增加即可 html部分 <div class="shuttle"> <!-- 左边列表 --> <div class="shuttle-box&q

  • Vue3 + TypeScript 开发总结

    目录 Vue3 + TypeScript 学习 一, 环境配置 1.1 安装最新 Vue 脚手架 1.2 创建Vue3 项目 二, 进击Vue3 2. 1 Vue 2 局限性 2.2 Vue 3 如何解决Vue 2 局限 三,Vue3 Composition Ap i 3.1 关于 Composition Api 3.2 什么时候使用Composition Api 四,Composition Api 必备基础 4.2 ref 创建响应式变量 4.3 生命周期 4.4 watch 4.5 comp

随机推荐