从0搭建Vue3组件库之Dropdown组件

在前端开发中,一个功能齐全、灵活易用的 Dropdown 组件是必不可少的。本文将带你从零搭建一个基于 Vue3 的 DropdownP 组件,展示从需求分析到代码实现的完整流程。

需求分析

  • 基于现有 Tooltip 组件扩展功能:复用已有组件,提升开发效率。

  • 显示菜单列表:支持多选项菜单,内容可灵活配置。

  • 自定义复杂选项:用户可传入复杂的自定义节点。

  • 支持语义化结构:提高可访问性与易用性。

确定方案

  • 属性
typescript 复制代码
import type { VNode } from "vue";
import type { TooltipProps } from "../Tooltip/types";
// 需要在Tooltip组件的属性上进行继承和拓展
export interface DropdownProps extends TooltipProps {
  // 传入的菜单项,用一个数组保存
  menuOptions: MenuOption[];
  // 点击某一项之后关闭菜单展示
  afterClickItem?: boolean;
}
// 每一项列表具有的属性
export interface MenuOption {
   // 字符串或自定义节点
  label: string | VNode;
  key: string | number;
  disabled?: boolean;
  // 分割线 
  divided?: boolean;
}
  • 事件

DropdownP 支持以下事件:

  • visible-change:当菜单显隐状态变化时触发。
  • select:当用户选择菜单项时触发。
typescript 复制代码
export interface DropdownEmits {
   // 打开列表派发事件
  (e: "visible-change", value: boolean): void;
  // 选中选项派发事件
  (e: "select", value: MenuOption): void;
}
  • 实例

通过暴露实例方法,用户可以在外部手动控制菜单显隐状态:

dart 复制代码
export interface DropdownInstance {
  show: () => void;
  hide: () => void;
}
  • 组件
xml 复制代码
<template>
    <div class="yv-dropdown">
        <Tooltip>
        <!-- 触发区域 -->
            <slot></slot>
            <!-- 内容区域 -->
            <template #content>
                <ul>
                    <template >
                        <!-- 分割线 -->
                        <hr >
                        <!-- 列表 -->
                        <li>
                            <RenderVnode  />
                        </li>
                    </template>
                </ul>
            </template>
        </Tooltip>
    </div>
</template>

设计思路

  • 使用Tooltip组件进行二次开发
  • 菜单列表使用Vnode节点,用户可自定义
  • 列表之间可以添加分割线
  • 默认插槽为触发区域,触发区域设置为Tooltip中预留的content部分
xml 复制代码
<template>
  <div class="jd-dropdown">
    <Tooltip
      :trigger="trigger"
      :placement="placement"
      :popper-options="popperOptions"
      :open-delay="openDelay"
      :close-delay="closeDelay"
      @visible-change="visibleChange"
      :manual="manual"
      ref="tooltipRef"
    >
      <slot />
      <template #content>
        <ul class="jd-dropdown__menu">
          <template v-for="item in menuOptions" :key="item.key">
            <!-- 分割线 -->
            <li
              v-if="item.divided"
              role="separator"
              class="divided-placeholder"
            />
            <!-- 菜单项 -->
            <li
              class="jd-dropdown__item"
              @click="itemClick(item)"
              :class="{
                'is-disabled': item.disabled,
                'is-divided': item.divided,
              }"
              :id="`dropdown-item-${item.key}`"
            >
              <!-- 中介组件,用户可自定义 -->
              <RenderVnode :vNode="item.label" />
            </li>
          </template>
        </ul>
      </template>
    </Tooltip>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import type { Ref } from "vue";
import type {
  DropdownProps,
  DropdownInstance,
  DropdownEmits,
  MenuOption,
} from "./types";
import type { TooltipInstance } from "../Tooltip/types";
import Tooltip from "../Tooltip/Tooltip.vue";
import RenderVnode from "../Common/RenderVnode";

defineOptions({
  name: "JdDropdown",
});
const props = withDefaults(defineProps<DropdownProps>(), {
  afterClickItem: true,
});
const emits = defineEmits<DropdownEmits>();

const tooltipRef = ref() as Ref<TooltipInstance>;

const visibleChange = (e: boolean) => {
  emits("visible-change", e);
};

const itemClick = (e: MenuOption) => {
  if (e.disabled) return;
  emits("select", e);
  if (props.afterClickItem) {
    tooltipRef.value?.hide();
  }
};

defineExpose<DropdownInstance>({
  // 通过闭包的形式,直接赋值会拿收不到对应的节点,因为是在setup函数中,此时实例还未被挂载
  show: () => tooltipRef.value?.show(),
  hide: () => tooltipRef.value?.hide(),
});
</script>
typescript 复制代码
//types.ts
import type { VNode } from "vue";
import type { TooltipProps } from "../Tooltip/types";

export interface DropdownProps extends TooltipProps {
  // 传入的菜单项,用一个数组保存
  menuOptions: MenuOption[];
  // 点击某一项之后关闭菜单展示
  afterClickItem?: boolean;
}
// 每一项列表具有的属性
export interface MenuOption {
  // 字符串或自定义节点
  label: string | VNode;
  key: string | number;// 每个选项的唯一标识
  disabled?: boolean;
  // 分割线
  divided?: boolean;
}

export interface DropdownEmits {
  // 打开列表派发事件
  (e: "visible-change", value: boolean): void;
  // 选中选项派发事件
  (e: "select", value: MenuOption): void;
}

export interface DropdownInstance {
  show: () => void;
  hide: () => void;
}

总结

通过上述步骤,我们成功地从零搭建了一个高度可定制的 Vue 3 Dropdown 组件。该组件不仅继承了 Tooltip 组件的弹出层管理功能,还增加了菜单列表、自定义选项、分割线等特性,能够满足大多数实际项目中的需求。此外,我们还通过 TypeScript 提供了严格的类型检查,确保组件的健壮性和可维护性。

相关推荐
clawdashi6 分钟前
2026 办公 Agent 选型指南:按场景分五类,一次讲清
前端·人工智能
BreezeJiang12 分钟前
JWT 登录不是存个 token:React 鉴权真正要闭合的是一条数据流
javascript·react.js·axios
驳是12 分钟前
代码洁癖患者会喜欢的几款 CLI 小工具
前端
Csvn13 分钟前
🛋️ requestIdleCallback:把非关键任务塞进浏览器的"空闲时间",告别卡顿
前端
江华森14 分钟前
从 fork 到协程:一次完整的操作系统并发编程实操
前端·程序员
四千岁17 分钟前
稀疏向量BM25Retriever不支持中文怎么办?jieba来帮忙
前端·javascript·后端
这是个栗子20 分钟前
【JS代码分析】前端鉴权基础:Token 的本地存储与状态重置实践
开发语言·前端·javascript
颜进强21 分钟前
14 - OpenSpec 老页面改造骨架:定位 + 增量 + 回归三件套
前端·后端·ai编程
做前端的娜娜子22 分钟前
async/await 错误处理:try...catch vs .catch() 完全指南
前端·面试·掘金·金石计划
半个落月25 分钟前
React 性能优化入门:用 memo 避免无关的子组件重复渲染
前端·react.js