Files
cmgd-mini-app/uni_modules/nutui-uni/components/popover/popover.vue
2026-01-05 12:47:14 +08:00

296 lines
7.6 KiB
Vue

<script setup lang="ts">
import { computed, defineComponent, getCurrentInstance, nextTick, onMounted, ref, watch } from 'vue'
import type { ComponentInternalInstance, CSSProperties } from 'vue'
import { CHOOSE_EVENT, CLOSE_EVENT, OPEN_EVENT, PREFIX, UPDATE_VISIBLE_EVENT } from '../_constants'
import { useRect } from '../_hooks'
import { getMainClass, getMainStyle, getRandomId } from '../_utils'
import NutIcon from '../icon/icon.vue'
import NutPopup from '../popup/popup.vue'
import { popoverEmits, popoverProps } from './popover'
import type { PopoverRootPosition } from './type'
const props = defineProps(popoverProps)
const emit = defineEmits(popoverEmits)
const instance = getCurrentInstance() as ComponentInternalInstance
const popoverID = `popoverRef${getRandomId()}`
const popoverContentID = `popoverContentRef${getRandomId()}`
const showPopup = ref(props.visible)
const rootPosition = ref<PopoverRootPosition>()
const elRect = ref({
width: 0,
height: 0,
})
const classes = computed(() => {
return getMainClass(props, componentName, {
[`nut-popover--${props.theme}`]: true,
})
})
const popoverArrow = computed(() => {
const prefixCls = 'nut-popover-arrow'
const loca = props.location
const direction = loca.split('-')[0]
return `${prefixCls} ${prefixCls}-${direction} ${prefixCls}--${loca}`
})
const popoverArrowStyle = computed(() => {
const styles: CSSProperties = {}
const { bgColor, arrowOffset, location } = props
const direction = location.split('-')[0]
const skew = location.split('-')[1]
const base = 16
if (bgColor)
styles[`border${upperCaseFirst(direction)}Color` as any] = bgColor
if (props.arrowOffset !== 0) {
if (['bottom', 'top'].includes(direction)) {
if (!skew)
styles.left = `calc(50% + ${arrowOffset}px)`
if (skew === 'start')
styles.left = `${base + arrowOffset}px`
if (skew === 'end')
styles.right = `${base - arrowOffset}px`
}
if (['left', 'right'].includes(direction)) {
if (!skew)
styles.top = `calc(50% - ${arrowOffset}px)`
if (skew === 'start')
styles.top = `${base - arrowOffset}px`
if (skew === 'end')
styles.bottom = `${base + arrowOffset}px`
}
}
return styles
})
function upperCaseFirst(str: string) {
str = str.toLowerCase()
str = str.replace(/\b\w+\b/g, word => word.substring(0, 1).toUpperCase() + word.substring(1))
return str
}
const getRootPosition = computed(() => {
const styles: CSSProperties = {}
if (!rootPosition.value) {
styles.visibility = 'hidden'
return styles
}
const contentWidth = elRect.value.width
const contentHeight = elRect.value.height
const { width, height, left, top, right } = rootPosition.value
const { location, offset } = props
const direction = location?.split('-')[0]
const skew = location?.split('-')[1]
let cross = 0
let parallel = 0
if (Array.isArray(offset) && offset?.length === 2) {
cross += Number(offset[1])
parallel += Number(offset[0])
}
if (width) {
if (['bottom', 'top'].includes(direction)) {
const h = direction === 'bottom' ? height + cross : -(contentHeight + cross)
styles.top = `${top + h}px`
if (!skew)
styles.left = `${-(contentWidth - width) / 2 + left + parallel}px`
if (skew === 'start')
styles.left = `${left + parallel}px`
if (skew === 'end')
styles.left = `${right + parallel}px`
}
if (['left', 'right'].includes(direction)) {
const contentW = direction === 'left' ? -(contentWidth + cross) : width + cross
styles.left = `${left + contentW}px`
if (!skew)
styles.top = `${top - contentHeight / 2 + height / 2 - 4 + parallel}px`
if (skew === 'start')
styles.top = `${top + parallel}px`
if (skew === 'end')
styles.top = `${top + height + parallel}px`
}
}
if (elRect.value.width === 0)
styles.visibility = 'hidden'
else
styles.visibility = 'initial'
return styles
})
const styles = computed(() => {
const styles: CSSProperties = {}
if (props.bgColor)
styles.background = props.bgColor
return getMainStyle(props, styles)
})
// 获取宽度
async function getContentWidth() {
uni.createSelectorQuery().selectViewport().scrollOffset((res: any) => {
const distance = res.scrollTop
if (props.targetId) {
useRect(props.targetId, instance.root).then((rect) => {
rootPosition.value = {
width: rect.width!,
height: rect.height!,
left: rect.left!,
top: rect.top! + distance!,
right: rect.right!,
}
})
}
else {
useRect(popoverID, instance).then((rect) => {
rootPosition.value = {
width: rect.width!,
height: rect.height!,
left: rect.left!,
top: rect.top! + distance!,
right: rect.right!,
}
})
}
}).exec()
setTimeout(() => {
getPopoverContentW()
}, 300)
}
async function getPopoverContentW() {
useRect(popoverContentID, instance).then((rect) => {
elRect.value = {
width: rect.width!,
height: rect.height!,
}
})
}
watch(
() => props.visible,
(value) => {
showPopup.value = value
if (value) {
nextTick(() => {
getContentWidth()
})
}
},
)
watch(
() => props.location,
(value) => {
getContentWidth()
},
)
function update(val: boolean) {
emit('update', val)
emit(UPDATE_VISIBLE_EVENT, val)
}
function openPopover() {
update(!props.visible)
emit(OPEN_EVENT)
}
function closePopover() {
emit(UPDATE_VISIBLE_EVENT, false)
emit(CLOSE_EVENT)
}
function chooseItem(item: any, index: number) {
!item.disabled && emit(CHOOSE_EVENT, item, index)
}
function clickContent() {
if (props.closeOnClickAction)
closePopover()
}
function clickAway() {
props.closeOnClickOutside && closePopover()
}
onMounted(() => {
setTimeout(() => {
getContentWidth()
}, 300)
})
</script>
<script lang="ts">
const componentName = `${PREFIX}-popover`
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
</script>
<template>
<view
v-if="!targetId"
:id="popoverID"
class="nut-popover-wrapper"
@click="openPopover"
>
<slot name="reference" />
</view>
<view :class="classes" :style="getRootPosition">
<NutPopup
v-model:visible="showPopup"
:destroy-on-close="false"
:pop-class="`nut-popover-content nut-popover-content--${location}`"
:custom-style="styles"
:position="`` as any"
:transition="`nut-popover` as any"
:overlay="overlay"
:duration="+duration"
:overlay-style="overlayStyle"
:overlay-class="overlayClass"
:close-on-click-overlay="closeOnClickOverlay"
>
<view :id="popoverContentID" class="nut-popover-content-group" @click.stop="clickContent">
<view v-if="showArrow" :class="popoverArrow" :style="popoverArrowStyle" />
<slot name="content" />
<view
v-for="(item, index) in list"
:key="index"
class="nut-popover-menu-item"
:class="[item.className, item.disabled && 'nut-popover-menu-disabled']"
@click="chooseItem(item, index)"
>
<NutIcon v-if="item.icon" :name="item.icon" custom-class="nut-popover-item-img" />
<view class="nut-popover-menu-item-name">
{{ item.name }}
</view>
</view>
</view>
</NutPopup>
<view
class="nut-popover-content-bg"
:class="{ 'nut-hidden': !showPopup }"
@touchmove="clickAway"
@click="clickAway"
/>
</view>
</template>
<style lang="scss">
@import './index';
</style>