20260817172952

This commit is contained in:
oneao committed 2026-08-17 17:29:54 +08:00
1 parent 4ef678ba65
commit 5bbd3c88b4
1865 files changed
+2261 -901

No files matched your search

@@ -1,11 +1,12 @@
<script setup>
import { computed, nextTick, provide, ref } from 'vue'
import { computed, nextTick, provide, ref, useSlots } from 'vue'
import Popover from '../popover/popover.vue'
import DropdownMenu from './menu.vue'
import DropdownItem from './DropdownItem.vue'
/* ---------- props / emits / slots ---------- */
const props = defineProps({
/** 菜单项数组;不传时使用 #menu 插槽自定义菜单内容(逃生舱) */
/** 菜单项数组;不传时若默认插槽含 <DropdownItem> 则由其收集渲染,否则用 #menu 插槽(逃生舱) */
items: { type: Array, default: undefined },
/** 受控打开状态;不传则非受控 */
open: { type: Boolean, default: undefined },
@@ -30,6 +31,43 @@ const props = defineProps({
})
const emit = defineEmits(['update:open', 'openChange', 'click'])
const slots = useSlots()
/* ---------- items 来源:优先 props.items,未传时从默认插槽收集 <DropdownItem> ---------- */
function collectDropdownItems() {
const defaultSlot = slots.default
if (!defaultSlot) return []
const items = []
for (const vnode of defaultSlot()) {
// 跳过注释 / 文本节点
if (!vnode.type) continue
// 仅收集 <DropdownItem> 类型节点
if (vnode.type !== DropdownItem) continue
const p = vnode.props || {}
// divider 项:忽略其余属性
if (p.divider != null && p.divider !== false) {
items.push({ divider: true })
continue
}
const item = {}
if (p.key != null) item.key = p.key
if (p.icon != null) item.icon = p.icon
if (p.label != null) item.label = p.label
if (p.danger) item.danger = true
if (p.disabled) item.disabled = true
if (p.onClick != null) item.onClick = p.onClick
items.push(item)
}
return items
}
const resolvedItems = computed(() => {
if (props.items != null) return props.items
// 未传 items 时,仅当默认插槽确实声明了 <DropdownItem> 才收集并启用数组渲染;
// 否则保留 undefined,回退到 #menu 逃生舱(避免空数组误吞 #menu 内容)
const collected = collectDropdownItems()
return collected.length ? collected : undefined
})
/* ---------- 公共状态 ---------- */
const open = ref(props.defaultOpen)
@@ -41,7 +79,7 @@ const isOpen = computed(() => (props.open !== undefined ? props.open : open.valu
// 可键盘导航的项索引(跳过 divider / label / disabled / 空节点)
const enabledIndices = computed(() => {
const list = props.items || []
const list = resolvedItems.value || []
return list.reduce((acc, item, index) => {
if (item && !item.divider && item.type !== 'label' && !item.disabled) acc.push(index)
return acc
@@ -117,7 +155,7 @@ function onMenuKeydown(event) {
break
case 'Enter': {
event.preventDefault()
const item = props.items?.[activeIndex.value]
const item = resolvedItems.value?.[activeIndex.value]
// 子菜单父项 Enter 不选中(子菜单为 hover 展开,键盘 MVP 不支持逐层进入)
if (item && enabledIndices.value.includes(activeIndex.value) && !item.children) {
selectItem(item)
@@ -157,7 +195,12 @@ provide('dropdownActiveIndex', activeIndex)
tabindex="-1"
@keydown="onMenuKeydown"
>
<DropdownMenu v-if="items != null" :items="items" />
<!-- 富头部插槽:菜单项列表上方的自定义区域(头像+名字+组织等),有内容时才渲染 -->
<div v-if="$slots.header" class="dropdown-header">
<slot name="header" />
</div>
<DropdownMenu v-if="resolvedItems != null" :items="resolvedItems" />
<slot v-else name="menu" :close="close" />
</div>
</template>