20260817172952
This commit is contained in:
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>
|
||||
|
||||
Reference in new issue
Block a user