u
This commit is contained in:
1 parent
869900f7f0
commit
5a3f484b8b
2 files changed
+74
-47
No files matched your search
@@ -1,18 +1,20 @@
|
|||||||
import type { VNode } from 'vue'
|
import type { VNode } from 'vue'
|
||||||
import { arrow, offset, useFloating } from '@floating-ui/vue'
|
import { arrow, offset, useFloating } from '@floating-ui/vue'
|
||||||
import { cloneVNode, defineComponent, onMounted, ref, Teleport ,watch} from 'vue'
|
import {
|
||||||
|
cloneVNode,
|
||||||
|
defineComponent,
|
||||||
|
nextTick,
|
||||||
|
onMounted,
|
||||||
|
ref,
|
||||||
|
Teleport,
|
||||||
|
watch,
|
||||||
|
} from 'vue'
|
||||||
|
|
||||||
import { getPrefixCls } from '../_utils/getPrefixCls'
|
import { getPrefixCls } from '../_utils/getPrefixCls'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'APopup',
|
name: 'APopup',
|
||||||
props: {
|
props: {
|
||||||
/**
|
|
||||||
* 触发方式
|
|
||||||
* - 'hover': 鼠标悬停显示/隐藏
|
|
||||||
* - 'click': 点击切换显示/隐藏
|
|
||||||
* - 'focus': 聚焦显示,失焦隐藏
|
|
||||||
*/
|
|
||||||
trigger: {
|
trigger: {
|
||||||
type: String as () => 'hover' | 'click' | 'focus',
|
type: String as () => 'hover' | 'click' | 'focus',
|
||||||
default: 'hover',
|
default: 'hover',
|
||||||
@@ -20,13 +22,24 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
setup(props, { slots }) {
|
setup(props, { slots }) {
|
||||||
const prefixCls = getPrefixCls('popup')
|
const prefixCls = getPrefixCls('popup')
|
||||||
|
|
||||||
const triggerRef = ref<HTMLElement | null>(null)
|
const triggerRef = ref<HTMLElement | null>(null)
|
||||||
const floatingRef = ref<HTMLElement | null>(null)
|
const floatingRef = ref<HTMLElement | null>(null)
|
||||||
const arrowRef = ref<HTMLElement | null>(null)
|
const arrowRef = ref<HTMLElement | null>(null)
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
|
|
||||||
// 使用 useFloating 管理浮动层和箭头
|
// 浮层样式和箭头数据
|
||||||
const { floatingStyles, middlewareData } = useFloating(
|
const floatingStyles = ref({})
|
||||||
|
const middlewareData = ref({ arrow: {} as any })
|
||||||
|
|
||||||
|
let floatingInstance: ReturnType<typeof useFloating> | null = null
|
||||||
|
|
||||||
|
// 初始化 FloatingUI,确保触发节点 DOM 已挂载
|
||||||
|
const initFloating = async () => {
|
||||||
|
if (!triggerRef.value || floatingInstance)
|
||||||
|
return
|
||||||
|
await nextTick()
|
||||||
|
const { x, y, strategy, middlewareData: md, update } = useFloating(
|
||||||
triggerRef,
|
triggerRef,
|
||||||
floatingRef,
|
floatingRef,
|
||||||
{
|
{
|
||||||
@@ -34,77 +47,90 @@ export default defineComponent({
|
|||||||
middleware: [offset(10), arrow({ element: arrowRef })],
|
middleware: [offset(10), arrow({ element: arrowRef })],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
floatingInstance = { x, y, strategy, middlewareData: md, update }
|
||||||
|
|
||||||
// 监听 triggerRef 挂载完成
|
// watch 可见性变化,更新位置
|
||||||
onMounted(() => {
|
watch(
|
||||||
console.log('triggerRef 已挂载:', triggerRef.value) // 正确打印
|
visible,
|
||||||
// 可以在此处调用 useFloating 的更新逻辑(如果有需要)
|
(v) => {
|
||||||
})
|
if (v && floatingInstance?.update)
|
||||||
watch(triggerRef, (newVal) => {
|
floatingInstance.update()
|
||||||
if (newVal !== null) {
|
},
|
||||||
console.log('triggerRef 已挂载:', newVal) // 正确打印 DOM 元素
|
)
|
||||||
|
|
||||||
|
// watch x/y 变化,实时更新样式
|
||||||
|
watch([x, y, md], () => {
|
||||||
|
floatingStyles.value = {
|
||||||
|
position: strategy.value,
|
||||||
|
left: `${x.value ?? 0}px`,
|
||||||
|
top: `${y.value ?? 0}px`,
|
||||||
|
}
|
||||||
|
middlewareData.value = md.value
|
||||||
|
}, { immediate: true })
|
||||||
}
|
}
|
||||||
}, { immediate: true }) // 立即执行一次(可选)
|
|
||||||
|
|
||||||
// 显示/隐藏/切换浮动层
|
// popup 显隐
|
||||||
const showPopup = () => (visible.value = true)
|
const showPopup = () => { visible.value = true; initFloating() }
|
||||||
const hidePopup = () => (visible.value = false)
|
const hidePopup = () => { visible.value = false }
|
||||||
const togglePopup = () => (visible.value = !visible.value)
|
const togglePopup = () => { visible.value = !visible.value; initFloating() }
|
||||||
|
|
||||||
// 绑定事件
|
|
||||||
const eventProps: Record<string, (e: Event) => void> = {}
|
const eventProps: Record<string, (e: Event) => void> = {}
|
||||||
if (props.trigger === 'hover') {
|
if (props.trigger === 'hover') {
|
||||||
eventProps.onMouseenter = () => showPopup()
|
eventProps.onMouseenter = showPopup
|
||||||
eventProps.onMouseleave = () => hidePopup()
|
eventProps.onMouseleave = hidePopup
|
||||||
}
|
}
|
||||||
else if (props.trigger === 'click') {
|
else if (props.trigger === 'click') {
|
||||||
eventProps.onClick = () => togglePopup()
|
eventProps.onClick = togglePopup
|
||||||
}
|
}
|
||||||
else if (props.trigger === 'focus') {
|
else if (props.trigger === 'focus') {
|
||||||
eventProps.onFocus = () => showPopup()
|
eventProps.onFocus = showPopup
|
||||||
eventProps.onBlur = () => hidePopup()
|
eventProps.onBlur = hidePopup
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取 reference 插槽或默认插槽
|
return () => {
|
||||||
const slotContent = slots.reference?.() || slots.default?.() || []
|
const slotContent = slots.reference?.() || slots.default?.() || []
|
||||||
let triggerSlot: VNode | null = null
|
let triggerSlot: VNode | null = null
|
||||||
|
|
||||||
// 单元素直接绑定事件,多元素套 span
|
|
||||||
if (slotContent.length === 1) {
|
if (slotContent.length === 1) {
|
||||||
triggerSlot = cloneVNode(slotContent[0], {
|
const child = slotContent[0]
|
||||||
ref: triggerRef,
|
const isHoisted = child.patchFlag != null && child.patchFlag === -1
|
||||||
...eventProps,
|
|
||||||
})
|
if (!isHoisted) {
|
||||||
|
triggerSlot = cloneVNode(child, { ref: triggerRef, ...eventProps })
|
||||||
}
|
}
|
||||||
else if (slotContent.length > 1) {
|
else {
|
||||||
triggerSlot = (
|
triggerSlot = (
|
||||||
<span ref={triggerRef} style="display: inline-flex;">
|
<span ref={triggerRef} style="display:inline-flex;" {...eventProps}>
|
||||||
{slotContent.map(vnode => cloneVNode(vnode, { ...eventProps }))}
|
{slotContent.map(v => cloneVNode(v))}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
triggerSlot = (
|
||||||
|
<span ref={triggerRef} style="display:inline-flex;" {...eventProps}>
|
||||||
|
{slotContent.map(v => cloneVNode(v))}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* 原位渲染触发元素 */}
|
|
||||||
{triggerSlot}
|
{triggerSlot}
|
||||||
|
|
||||||
{/* 浮动层挂载到 body */}
|
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
{visible.value && (
|
{visible.value && (
|
||||||
<div ref={floatingRef} class={prefixCls} style={floatingStyles}>
|
<div ref={floatingRef} class={prefixCls} style={floatingStyles.value}>
|
||||||
Popup 内容
|
Popup 内容
|
||||||
<div
|
<div
|
||||||
class={`${prefixCls}-arrow`}
|
class={`${prefixCls}-arrow`}
|
||||||
ref={arrowRef}
|
ref={arrowRef}
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left:
|
left: middlewareData.value.arrow?.x != null
|
||||||
middlewareData.value.arrow?.x != null
|
|
||||||
? `${middlewareData.value.arrow.x}px`
|
? `${middlewareData.value.arrow.x}px`
|
||||||
: undefined,
|
: undefined,
|
||||||
top:
|
top: middlewareData.value.arrow?.y != null
|
||||||
middlewareData.value.arrow?.y != null
|
|
||||||
? `${middlewareData.value.arrow.y}px`
|
? `${middlewareData.value.arrow.y}px`
|
||||||
: undefined,
|
: undefined,
|
||||||
}}
|
}}
|
||||||
@@ -114,5 +140,6 @@ export default defineComponent({
|
|||||||
</Teleport>
|
</Teleport>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -5,9 +5,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page-center">
|
<div class="page-center">
|
||||||
<APopup trigger="click">
|
<APopup trigger="click">
|
||||||
<AButton style="margin-right: 12px;">
|
<button style="margin-right: 12px;">
|
||||||
测试
|
测试
|
||||||
</AButton>
|
</button>
|
||||||
</APopup>
|
</APopup>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in new issue
Block a user