在前端开发中,一个功能齐全、灵活易用的 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 提供了严格的类型检查,确保组件的健壮性和可维护性。