This commit is contained in:
oneao committed 2026-10-08 22:38:34 +08:00
1 parent f572dce2f6
commit e99a9fb274
356 files changed
+28877 -1055

No files matched your search

@@ -0,0 +1,116 @@
<script setup lang="ts">
import { inject, isVNode, type VNode } from 'vue'
import { ChevronRight } from '@lucide/vue'
import { useNamespace } from '../_composables/useNamespace'
import G3Popover from '../popover/Popover.vue'
import { dropdownActiveIndexKey, dropdownSelectKey } from './context'
import type { DropdownItemData } from './types'
/**
* 下拉菜单列表(内部组件)—— 把 items 数组渲染成分隔线 / 分组标签 / 子菜单 / 普通项。
*/
defineOptions({ name: 'G3DropdownMenu' })
const props = withDefaults(
defineProps<{
/** 菜单项数组 */
items?: DropdownItemData[]
/** 是否参与主菜单的键盘 / 鼠标高亮联动;递归子菜单时关闭 */
interactive?: boolean
}>(),
{ items: () => [], interactive: true },
)
const { b, e, m, is } = useNamespace('dropdown')
const selectItem = inject(dropdownSelectKey, null)
const activeIndex = inject(dropdownActiveIndexKey, null)
function isActive(index: number): boolean {
return props.interactive && activeIndex !== null && activeIndex.value === index
}
/** 鼠标移入时同步键盘高亮索引(子菜单不参与联动) */
function setActive(index: number) {
if (!props.interactive || activeIndex === null) return
activeIndex.value = index
}
function onItemClick(item: DropdownItemData) {
if (item.disabled) return
item.onClick?.(item)
selectItem?.(item)
}
/** 渲染函数组件:把 VNode 形态的图标原样渲染 */
const VNodeRenderer = (renderProps: { node: VNode }) => renderProps.node
</script>
<template>
<template v-for="(item, index) in props.items" :key="item?.key ?? index">
<!-- 分隔线 -->
<div v-if="item?.divider" :class="e('divider')" role="separator" />
<!-- 分组标签 -->
<div v-else-if="item?.type === 'label'" :class="e('label')" role="group">
{{ item.label }}
</div>
<!-- 子菜单:嵌套 Popover,hover 展开 -->
<G3Popover
v-else-if="item?.children?.length"
trigger="hover"
placement="right-start"
:offset="0"
:arrow="false"
unwrapped
>
<template #trigger>
<span
:class="[
e('item'),
{ [is('item-active')]: isActive(index), [is('item-disabled')]: item.disabled },
]"
role="menuitem"
aria-haspopup="menu"
>
<span v-if="item.icon" :class="e('item-icon')">
<component :is="item.icon" v-if="!isVNode(item.icon)" />
<VNodeRenderer v-else :node="(item.icon as VNode)" />
</span>
<span :class="e('item-label')">{{ item.label }}</span>
<ChevronRight :class="e('item-caret')" aria-hidden="true" />
</span>
</template>
<template #content>
<div :class="e('menu')" role="menu">
<G3DropdownMenu :items="item.children" :interactive="false" />
</div>
</template>
</G3Popover>
<!-- 普通项 -->
<div
v-else-if="item"
:class="[
e('item'),
{
[is('item-active')]: isActive(index),
[is('item-disabled')]: item.disabled,
[is('item-danger')]: item.danger,
},
]"
role="menuitem"
:aria-disabled="item.disabled || undefined"
@mouseenter="!item.disabled && setActive(index)"
@click="onItemClick(item)"
>
<span v-if="item.icon" :class="e('item-icon')">
<component :is="item.icon" v-if="!isVNode(item.icon)" />
<VNodeRenderer v-else :node="(item.icon as VNode)" />
</span>
<span :class="e('item-label')">{{ item.label }}</span>
<span v-if="item.shortcut" :class="e('item-shortcut')">{{ item.shortcut }}</span>
</div>
</template>
</template>