从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 提供了严格的类型检查,确保组件的健壮性和可维护性。

相关推荐
mldong2 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排2 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60613 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角4 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!4 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰4 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi6 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen6 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马7 小时前
AI源码分析:拆解模型底层实现逻辑
前端
BillKu7 小时前
全局样式变量:CSS 自定义属性(--border-color)和SCSS 变量($border-color)的说明
javascript·css·scss