Dropdown 下拉菜单



Script 部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<script setup lang="ts">
import { ref, computed, provide } from 'vue'
import { omit, isNil } from 'lodash-es'
import type { DropdownProps, DropdownEmits, DropdownContext } from './types'
import { useId, useDisabledStyle } from '@veyra/hooks'
import { DROPDOWN_CTX_KEY } from './constants'
import VrButton from '../Button/index.vue'
import VrButtonGroup from '../ButtonGroup/index.vue'
import DropdownItem from './DropdownItem.vue'
import Tooltip from '../Tooltip/Tooltip.vue'

defineOptions({
name: 'VrDropdown',
inheritAttrs: false,
})

const props = withDefaults(defineProps<DropdownProps>(), {
hideOnClick: true,
items: () => [] as DropdownItemProps[],
})

const emits = defineEmits<DropdownEmits>()
const slots = defineSlots()

const tooltipRef = ref<TooltipInstance>()
const triggerRef = ref<typeof VrButton>()

const tooltipProps = computed(() =>
omit(props, ['items', 'hideAfterClick', 'size', 'type', 'splitButton'])
)

function handleItemClick(item: DropdownItemProps) {
props.hideOnClick && tooltipRef.value?.hide()
if (!isNil(item.command)) emits('command', item.command)
}

useDisabledStyle()

defineExpose<DropdownInstance>({
open: () => tooltipRef.value?.show(),
close: () => tooltipRef.value?.hide(),
})

provide(DROPDOWN_CTX_KEY, {
handleItemClick,
size: computed(() => props.size),
})
</script>

关键点解释

  1. 组件结构

    • 继承自 Tooltip,通过 tooltipProps 传递配置,支持弹出位置、触发方式等。
    • 支持 splitButton 模式:组合主按钮和下拉按钮(如 VrButtonGroup)。
    • 提供 openclose 方法,通过 defineExpose 暴露给外部调用。
  2. Props & 默认值

    • items:下拉项数组,每个项需包含 commandlabeldisableddivided 等属性。
    • splitButton:启用分体按钮模式(主按钮 + 下拉按钮)。
    • hideOnClick:点击下拉项后自动关闭菜单。
  3. 事件处理

    • command:当点击有 command 的项时触发,传递 command 值。
    • visible-change:弹出层显示/隐藏时触发,传递布尔值。
  4. 提供上下文(Provide/Inject)

    • 通过 DROPDOWN_CTX_KEY 向子组件 DropdownItem 提供 handleItemClicksize
    • size 用于子项样式适配。

Template 部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<template>
<div class="vr-dropdown" :id="`dropdown-${useId().value}`" :class="{ 'is-disabled': props.disabled }">
<tooltip
ref="tooltipRef"
v-bind="tooltipProps"
:virtual-triggering="splitButton"
:virtual-ref="triggerRef"
@visible-change="$emit('visible-change', $event)"
>
<!-- 触发器 -->
<vr-button-group
v-if="splitButton"
:type="type"
:size="size"
:disabled="disabled"
>
<vr-button @click="$emit('click', $event)">
<slot name="default"></slot>
</vr-button>
<vr-button ref="triggerRef" icon="angle-down" />
</vr-button-group>
<!-- 单一触发器 -->
<slot v-else name="default"></slot>

<!-- 下拉内容 -->
<template #content>
<ul class="vr-dropdown__menu">
<slot name="dropdown">
<template v-for="item in items" :key="item.command ?? useId().value">
<dropdown-item v-bind="item" />
</template>
</slot>
</ul>
</template>
</tooltip>
</div>
</template>

关键点解释

  1. 触发器模式

    • 分体按钮模式(splitButton)
      • 使用 VrButtonGroup 包含主按钮和下拉箭头按钮。
      • 主按钮点击触发 click 事件,下拉箭头按钮控制弹出层。
    • 普通模式
      • 直接使用插槽内容作为触发器(如自定义按钮或图标)。
  2. 下拉内容渲染

    • 通过 #content 插槽渲染下拉菜单,优先使用 slot="dropdown" 的自定义内容。
    • 默认遍历 items 数组,渲染 DropdownItem 组件。
  3. 样式与禁用状态

    • is-disabled 类控制禁用状态样式。
    • 分体按钮模式下,按钮组的最后一个按钮(箭头)样式通过 :deep 进行 CSS 覆盖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<script setup lang="ts">
import { inject, computed } from 'vue'
import { useId } from '@veyra/hooks'
import { DROPDOWN_CTX_KEY } from './constants'
import type { DropdownItemProps } from './types'

defineOptions({
name: 'VrDropdownItem',
})

const props = withDefaults(defineProps<DropdownItemProps>(), {
command: useId().value,
divided: false,
disabled: false,
})

const ctx = inject(DROPDOWN_CTX_KEY)
const size = computed(() => ctx?.size.value)

function handleClick() {
if (props.disabled) return
ctx?.handleItemClick(props)
}
</script>

<template>
<li v-if="divided" role="separator" class="divided-placeholder"></li>
<li
:id="`dropdown-item-${command ?? useId().value}`"
:class="{
'vr-dropdown__item': true,
[`vr-dropdown__item--${size}`]: size,
'is-disabled': disabled,
'is-divided': divided,
}"
@click="handleClick"
>
<slot>{{ label }}</slot>
</li>
</template>

关键点解释

  1. 项的渲染

    • divided 属性:当为 true 时渲染分隔符(role="separator"li 元素)。
    • command:唯一标识符,用于触发 command 事件。
  2. 样式适配

    • 根据父组件的 size 动态添加类名(如 vr-dropdown__item--large)。
    • is-disabledis-divided 控制禁用和分隔符样式。
  3. 事件触发

    • 点击非禁用项时,调用父组件的 handleItemClick 方法,并传递自身 props

辅助文件

  1. constants.ts

    1
    2
    3
    4
    import type { InjectionKey } from "vue"
    import type { DropdownContext } from "./types"

    export const DROPDOWN_CTX_KEY: InjectionKey<DropdownContext> = Symbol("dropdownContext")
    • 定义注入键 DROPDOWN_CTX_KEY,用于在子组件中注入上下文。
  2. types.ts

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    export interface DropdownItemProps {
    command?: string | number
    label?: string | VNode
    disabled?: boolean
    divided?: boolean
    }

    export interface DropdownProps extends TooltipProps {
    type?: ButtonType
    size?: ButtonSize
    items?: DropdownItemProps[]
    hideOnClick?: boolean
    splitButton?: boolean
    }

    export interface DropdownEmits {
    (e: "visible-change", value: boolean): void
    (e: "command", value: DropdownCommand): void
    (e: "click", value: MouseEvent): void
    }
    • DropdownProps:继承 TooltipProps,支持 placementtrigger 等属性。
    • DropdownItemProps:定义下拉项属性,如 commanddisableddivided

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<template>
<vr-dropdown
v-model:visible="visible"
:items="items"
split-button
type="primary"
@command="handleCommand"
@visible-change="handleVisibleChange"
>
<template #default>
<vr-button>自定义触发器</vr-button>
</template>
<template #dropdown>
<ul class="custom-menu">
<vr-dropdown-item command="custom1">自定义项1</vr-dropdown-item>
</ul>
</template>
</vr-dropdown>
</template>

<script setup>
import { ref } from 'vue'

const items = ref([
{ label: '选项1', command: 'cmd1' },
{ label: '选项2', command: 'cmd2', divided: true },
])

const handleCommand = (command: string) => {
console.log('Command:', command)
}
</script>

核心机制总结

  1. 弹出层集成:基于 Tooltip 实现,支持位置、触发方式等配置。
  2. 分体按钮模式:通过 splitButton 展示主按钮和下拉按钮组合。
  3. 动态项渲染:通过 items 数组或 slot="dropdown" 自定义下拉内容。
  4. 事件交互
    • command:点击带 command 的项时触发。
    • visible-change:弹出层状态变化时触发。
  5. 样式适配:通过 sizetype 继承按钮样式,支持分隔符和禁用状态。

通过 provide/inject 实现父子组件通信,确保下拉项点击能触发父组件的事件。