512 lines
11 KiB
Vue
Raw Permalink Normal View History

2025-05-08 09:16:37 +08:00
<template>
<view class="Popover" :style="{'--theme-bg-color':bgStyleColor}">
<view class="mark" @click="close()" v-show="inited"></view>
<view @click.stop="handleClick" class="zb-button-popover">
<slot></slot>
</view>
<view class="zb-popover" v-show="inited" ref="zb-transition" :class="[classes,`zb-popover-${placement}`]"
:style="[mergeStyle]" @touchmove="noop">
<view class="zb-popover-arrow" :style="[arrowStyle]" :class="[{
'zb_popper__up':placement.indexOf('bottom')===0,
'zb_popper__arrow':placement.indexOf('top')===0,
'zb_popper__right':placement.indexOf('right')===0,
'zb_popper__left':placement.indexOf('left')===0,
}]">
</view>
<slot name="content">
<view :class="[{'horizontal__action':actionsDirection==='horizontal'}]">
<view @click.stop="actionAction(item)" v-for="item,index in options" class="zb-popover__action"
:class="[{'dark__action':theme==='dark'}]" :key="index">
<up-icon :name="item.icon" size="18" color="#333" />
<view class="zb-popover__action-text">{{item.text}}</view>
</view>
</view>
</slot>
</view>
</view>
</template>
<script>
const tranClass = {
enter: "zb-fade-zoom-enter zb-fade-zoom-enter-active",
'enter-to': "zb-fade-zoom-enter-to zb-fade-zoom-enter-active",
leave: "zb-fade-zoom-leave zb-fade-zoom-leave-active",
'leave-to': "zb-fade-zoom-leave-to zb-fade-zoom-leave-active",
}
export default {
props: {
options: {
type: Array,
default: () => []
},
placement: {
type: String,
default: 'bottom-start'
},
bgColor: {
type: String,
},
// light dark
theme: {
type: String,
default: 'light'
},
// horizontal vertical
actionsDirection: {
type: String,
default: 'vertical'
}
},
name: "Popover",
watch: {
show: {
handler(newVal) {
newVal ? this.vueEnter() : this.vueLeave()
},
// 表示同时监听初始化时的props的show的意思
immediate: true
}
},
data() {
return {
show: false,
inited: false, // 是否显示/隐藏组件
classes: '', // 应用的类名
display: false, // 组件是否展示
duration: 100,
popoverStyle: {
},
arrowOldStyle: {}
};
},
computed: {
bgStyleColor() {
if (this.bgColor) {
return this.bgColor
}
if (this.theme === 'light') {
return 'white'
}
if (this.theme === 'dark') {
return '#4a4a4a'
}
},
mergeStyle() {
return {
transitionDuration: `${this.duration}ms`,
transitionTimingFunction: `ease-out`,
...this.popoverStyle
}
},
arrowStyle() {
return {
...this.arrowOldStyle
}
}
},
mounted() {
// window.addEventListener('click', () => {
// this.show = false
// })
},
methods: {
handleClick() {
if (this.show) {
this.show = false
} else {
this.show = true
}
this.$emit('handleClick', this.show)
},
close() {
this.show = false
},
actionAction(item) {
this.$emit('select', item)
this.show = false
},
sleep(value) {
return new Promise((resolve) => {
setTimeout(() => {
resolve()
}, value)
})
},
vueEnter() {
this.inited = true
this.getPosition()
this.classes = tranClass.enter
this.$nextTick(async () => {
await this.sleep(30)
this.classes = tranClass['enter-to']
})
},
vueLeave() {
this.classes = tranClass.leave
this.$nextTick(async () => {
this.classes = tranClass['leave-to']
await this.sleep(120)
this.inited = false
})
},
// 阻止事件冒泡
preventEvent(e) {
e && typeof(e.stopPropagation) === 'function' && e.stopPropagation()
},
getPosition() {
return new Promise((resolve) => {
this.$nextTick(() => {
let selectorQuery = uni.createSelectorQuery().in(this).selectAll(
'.zb-button-popover,.zb-popover')
selectorQuery.boundingClientRect(async (data) => {
let {
left,
bottom,
right,
top,
width,
height
} = data[0]
let popoverClientRect = data[1]
let popoverStyle = {}
let arrowOldStyle = {}
switch (this.placement) {
case 'top':
if (popoverClientRect.width > width) {
popoverStyle.left =
`-${(popoverClientRect.width - width)/2}px`
} else {
popoverStyle.left =
`${Math.abs(popoverClientRect.width - width)/2}px`
}
popoverStyle.bottom = `${height+8}px`
arrowOldStyle.left = (popoverClientRect.width / 2 - 6) +
'px'
break;
case 'top-start':
popoverStyle.left = `0px`
popoverStyle.bottom = `${height+8}px`
arrowOldStyle.left = '16px'
break;
case 'top-end':
popoverStyle.right = `0px`
popoverStyle.bottom = `${height+8}px`
arrowOldStyle.right = '16px'
break;
case 'bottom':
if (popoverClientRect.width > width) {
popoverStyle.left =
`-${(popoverClientRect.width - width)/2}px`
} else {
popoverStyle.left =
`${Math.abs(popoverClientRect.width - width)/2}px`
}
popoverStyle.top = `${height+8}px`
arrowOldStyle.left = (popoverClientRect.width / 2 - 6) +
'px'
break;
case 'bottom-start':
popoverStyle.top = `${height+8}px`
popoverStyle.left = `0px`
arrowOldStyle.left = '16px'
break;
case 'bottom-end':
popoverStyle.top = `${height+8}px`
popoverStyle.right = `0px`
arrowOldStyle.right = '16px'
break;
case 'right':
popoverStyle.left = `${width+8}px`
if (popoverClientRect.height > height) {
popoverStyle.top =
`-${(popoverClientRect.height - height)/2}px`
} else {
popoverStyle.top =
`${Math.abs((popoverClientRect.height - height)/2)}px`
}
arrowOldStyle.top = `${popoverClientRect.height/2-6}px`
break;
case 'right-start':
popoverStyle.left = `${width+8}px`
popoverStyle.top = `0px`
arrowOldStyle.top = `8px`
break;
case 'right-end':
popoverStyle.left = `${width+8}px`
popoverStyle.bottom = `0px`
arrowOldStyle.bottom = `8px`
break;
case 'left':
popoverStyle.right = `${width+8}px`
if (popoverClientRect.height > height) {
popoverStyle.top =
`-${(popoverClientRect.height - height)/2}px`
} else {
popoverStyle.top =
`${Math.abs((popoverClientRect.height - height)/2)}px`
}
arrowOldStyle.top = `${popoverClientRect.height/2-6}px`
break;
case 'left-start':
popoverStyle.right = `${width+8}px`
popoverStyle.top = `0px`
arrowOldStyle.top = `8px`
break;
case 'left-end':
popoverStyle.right = `${width+8}px`
popoverStyle.bottom = `0px`
arrowOldStyle.bottom = `8px`
break;
}
this.popoverStyle = popoverStyle
this.arrowOldStyle = arrowOldStyle
resolve()
}).exec()
})
})
},
// 空操作
noop(e) {
this.preventEvent(e)
}
}
}
</script>
<style lang="scss" scoped>
.mark { position: fixed; left: 0; right: 0; top: 0; bottom: 0; z-index: 1; background-color: rgba(0, 0, 0, 0.4); }
$theme-bg-color: var(--theme-bg-color);
.Popover {
position: relative;
}
.zb-button-popover {
display: inline-block;
}
.zb-popover {
border-radius: 8px;
z-index: 2144;
position: absolute;
background-color: $theme-bg-color;
box-shadow: 0 2px 12px #3232331f;
}
.zb-popover-top {
transform-origin: 50% bottom;
}
.zb-popover-top-start {
transform-origin: 50% bottom;
}
.zb-popover-top-end {
transform-origin: 0 bottom;
}
.zb-popover-bottom {
transform-origin: 50% 0;
}
.zb-popover-bottom-end {
transform-origin: 100% 0;
}
.zb-popover-bottom-start {
transform-origin: 0 0;
}
.zb-popover-right {
transform-origin: left 50%;
}
.zb-popover-right-start {
transform-origin: left 0;
}
.zb-popover-right-end {
transform-origin: left 100%;
}
.zb-popover-left {
transform-origin: right 50%;
}
.zb-popover-left-start {
transform-origin: right 0;
}
.zb-popover-left-end {
transform-origin: right 100%;
}
.zb-popover-arrow {
position: absolute;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
border-width: 6px;
color: $theme-bg-color;
}
.zb_popper__up {
border-top-width: 0;
border-bottom-color: currentColor;
top: -6px;
}
.zb_popper__right {
border-left-width: 0;
border-right-color: currentColor;
left: -5px;
}
.zb_popper__left {
border-right-width: 0;
border-left-color: currentColor;
right: -5px;
}
.zb_popper__arrow {
border-bottom-width: 0;
border-top-color: currentColor;
bottom: -6px;
}
.zb-popover__action {
position: relative;
display: flex;
box-sizing: border-box;
height: 88rpx;
padding: 0 30rpx;
font-size: 30rpx;
cursor: pointer;
}
.zb-popover__action-text {
display: flex;
flex: 1;
align-items: center;
height: 100%;
padding: 0 24rpx;
border-bottom: 1rpx solid #ebedf0;
word-wrap: break-word;
white-space: nowrap;
}
.zb-popover__action:last-child {
.zb-popover__action-text {
border-bottom: none;
}
}
.dark__action {
color: white;
.zb-popover__action-text {
border-bottom: 1rpx solid #ebedf033
}
}
.horizontal__action {
display: flex;
.zb-popover__action {
padding: 0 20rpx;
border-right: 1rpx solid #ebedf0;
}
.zb-popover__action-text {
padding: 0;
//border-right:1rpx solid #ebedf0;
}
}
$u-zoom-scale: scale(0.95);
.zb-fade-enter-active,
.zb-fade-leave-active {
transition-property: opacity;
}
.zb-fade-enter,
.zb-fade-leave-to {
opacity: 0
}
.zb-fade-zoom-enter,
.zb-fade-zoom-leave-to {
transform: $u-zoom-scale;
opacity: 0;
}
.zb-fade-zoom-enter-active,
.zb-fade-zoom-leave-active {
transition-property: transform, opacity;
}
.zb-fade-down-enter-active,
.zb-fade-down-leave-active,
.zb-fade-left-enter-active,
.zb-fade-left-leave-active,
.zb-fade-right-enter-active,
.zb-fade-right-leave-active,
.zb-fade-up-enter-active,
.zb-fade-up-leave-active {
transition-property: opacity, transform;
}
.zb-fade-up-enter,
.zb-fade-up-leave-to {
transform: translate3d(0, 100%, 0);
opacity: 0
}
.zb-fade-down-enter,
.zb-fade-down-leave-to {
transform: translate3d(0, -100%, 0);
opacity: 0
}
.zb-fade-left-enter,
.zb-fade-left-leave-to {
transform: translate3d(-100%, 0, 0);
opacity: 0
}
.zb-fade-right-enter,
.zb-fade-right-leave-to {
transform: translate3d(100%, 0, 0);
opacity: 0
}
.zb-popover__action {
&:active {
background-color: rgba(0, 0, 0, 0.2);
}
&--disabled {
color: var(--van-popover-dark-action-disabled-text-color);
&:active {
background-color: transparent;
}
}
}
</style>