628 lines
15 KiB
Plaintext
628 lines
15 KiB
Plaintext
<script lang="ts">
|
||
import { xConfig } from "../../config/xConfig.uts"
|
||
import { checkIsCssUnit, rpx2px, getUid } from "../../core/util/xCoreUtil.uts"
|
||
import { type PropType } from 'vue'
|
||
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
|
||
type NODE_INFO = {
|
||
left : number,
|
||
width : number,
|
||
height : number,
|
||
bottom : number,
|
||
right : number,
|
||
top : number
|
||
}
|
||
type xPopopverPosType = 'bc' | 'br' | 'bl' | 'tr' | 'tc' | 'tl'
|
||
|
||
/**
|
||
* @name 汽泡菜单 xPopover
|
||
* @description 汽泡菜单,警告:如果你弹出的内容比如文本,它会自动宽和高,是个特殊组件在web上,此类需要固定下宽.
|
||
* 建议内容要自己写个固定宽或者高性能更好。另外sdk4.57开始,在微信端你的菜单内容最好不要动态更改导致宽和高动态变化,在微信端只会记录首次的
|
||
* 内容区域的宽和高(触发插槽不限制),如果确实在微信端要动态变更内容可以vif整体组件或者改变key来刷新组件解决。非微信端无此限制。
|
||
* @page /pages/index/popover
|
||
* @category 反馈组件
|
||
* @constant 平台兼容
|
||
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
|
||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
|
||
*/
|
||
export default {
|
||
data() {
|
||
return {
|
||
id: "xPopover" + getUid(),
|
||
menuiId: "xPopoverMenu" + getUid(),
|
||
_width: 0,
|
||
_height: 0,
|
||
trrigerNodeInfo: null as NODE_INFO | null,
|
||
menuNodeInfo: null as NODE_INFO | null,
|
||
showPro: false,
|
||
status: "open" as "open" | "playing" | "close" | "compeleted",
|
||
isGeiNodeInfoOk: false,
|
||
tid: 0,
|
||
windtop: 0,
|
||
showquerinnode:false,
|
||
nowPos:'bc' as xPopopverPosType
|
||
}
|
||
},
|
||
emits: ["update:modelValue"],
|
||
props: {
|
||
/**
|
||
* 弹出的位置。
|
||
* 'bc'|'br'|'bl'|'tr'|'tc'|'tl'
|
||
* 底中|底右|底左|顶右|顶中|顶左
|
||
*/
|
||
position: {
|
||
type: String as PropType<xPopopverPosType>,
|
||
default: 'bc'
|
||
},
|
||
/**
|
||
* 如果你的弹出层内容是异步加载。
|
||
* 你需要刷新下此key以刷新位置。如果你是vif一来切换或者显示前内容是固定的,则不需要刷新此值。
|
||
*/
|
||
keyName: {
|
||
type: Number,
|
||
default: 0
|
||
},
|
||
/**
|
||
* 变量控制是否显示菜单,等同v-model=""
|
||
* 如果你想默认显示,但不受变量控制由内部自行处理可以:model-value="true"即可
|
||
*/
|
||
modelValue: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
/**
|
||
* 点击弹出的内容是否允许关闭。
|
||
*/
|
||
isClickClose: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
/**
|
||
* 容器圆角。ios如果不设置,你内容圆角没有用。
|
||
*/
|
||
round: {
|
||
type: String,
|
||
default: '16'
|
||
},
|
||
/**
|
||
* 遮罩背景,默认是透明
|
||
*/
|
||
maskBgColor:{
|
||
type:String,
|
||
default:"rgba(0,0,0,0)"
|
||
},
|
||
/**
|
||
* 是否显示指示的三角形。
|
||
*/
|
||
showTriangle:{
|
||
type:Boolean,
|
||
default:false
|
||
},
|
||
/**
|
||
* 三角指示的背景色
|
||
*/
|
||
triangleColor:{
|
||
type:String,
|
||
default:'white'
|
||
},
|
||
/**
|
||
* 三角指示的暗黑背景色,空取sheet暗黑背影色
|
||
*/
|
||
triangleDarkColor:{
|
||
type:String,
|
||
default:''
|
||
},
|
||
zIndex:{
|
||
type:Number,
|
||
default:100
|
||
}
|
||
},
|
||
watch: {
|
||
keyName() {
|
||
this.$nextTick(() => {
|
||
this.getNodes()
|
||
})
|
||
},
|
||
modelValue(newval : boolean) {
|
||
if (this.showPro == newval || this.status == 'playing') return;
|
||
if (newval) {
|
||
this.isGeiNodeInfoOk = false
|
||
this.showPro = true;
|
||
this.status = "open"
|
||
this.openProWrap();
|
||
|
||
} else {
|
||
this.closeProWrap();
|
||
}
|
||
|
||
}
|
||
},
|
||
computed: {
|
||
_maskBgColor():string{
|
||
return this.maskBgColor
|
||
},
|
||
_triangleColor():string{
|
||
if(xConfig.dark=='dark'){
|
||
if(this.triangleDarkColor == ''){
|
||
return getDefaultColor(xConfig.sheetDarkColor)
|
||
}
|
||
return getDefaultColor(this.triangleDarkColor)
|
||
}
|
||
return getDefaultColor(this.triangleColor)
|
||
},
|
||
xPopoverWrapPos() : string {
|
||
if (this.trrigerNodeInfo == null) return ""
|
||
const sys = uni.getWindowInfo()
|
||
let tl = this.trrigerNodeInfo!.left as number
|
||
let tr = this.trrigerNodeInfo!.right as number
|
||
let tw = this.trrigerNodeInfo!.width as number
|
||
let mw = this.menuNodeInfo!.width as number
|
||
let mh = this.menuNodeInfo!.height as number + 8
|
||
let mb = this.menuNodeInfo!.bottom as number
|
||
|
||
let tb = this.trrigerNodeInfo!.bottom as number
|
||
let tp = this.trrigerNodeInfo!.top as number
|
||
let nopos = this.position;
|
||
function getStyleCss(pos:string):string{
|
||
if (pos == 'bc') {
|
||
let left = tl - (mw - tw) / 2
|
||
let top = tb + 10
|
||
return `left:${left}px;top:${top}px;transform-origin: 50% 0px;`
|
||
} else if (pos == 'br') {
|
||
let left = tr - mw
|
||
let top = tb + 10
|
||
return `left:${left}px;top:${top}px;transform-origin: 100% 0px;`
|
||
} else if (pos == 'bl') {
|
||
let left = tl
|
||
let top = tb + 10
|
||
return `left:${left}px;top:${top}px;transform-origin: 0px 0px;`
|
||
} else if (pos == 'tc') {
|
||
let left = tl - (mw - tw) / 2
|
||
let top = tp - mh - 10
|
||
return `left:${left}px;top:${top}px;transform-origin: 50% 200%;`
|
||
} else if (pos == 'tl') {
|
||
let left = tl
|
||
let top = tp - mh - 10
|
||
return `left:${left}px;top:${top}px;transform-origin: 0% 200%;`
|
||
} else if (pos == 'tr') {
|
||
let left = tr - mw
|
||
let top = tp - mh - 10
|
||
return `left:${left}px;top:${top}px;transform-origin: 100% 200%;`
|
||
}
|
||
return ""
|
||
}
|
||
let poscss = "";
|
||
switch(this.position){
|
||
case "bc":{
|
||
const diffb = sys.windowHeight - tb
|
||
const difft = tp - sys.windowTop
|
||
// 当底空间不够菜单高且,顶可以容纳菜单时,切换为顶显示,避免裁剪。
|
||
if(diffb<mh&&difft>=diffb&&diffb < mh){
|
||
nopos = "tc"
|
||
}
|
||
poscss = getStyleCss(nopos)
|
||
break;
|
||
}
|
||
case "br":{
|
||
const diffb = sys.windowHeight - tb
|
||
const difft = tp - sys.windowTop
|
||
const diffl = tl;
|
||
const diffr = sys.windowWidth - tr;
|
||
|
||
if(diffb<mh&&difft>=diffb&&diffb < mh){
|
||
nopos = "tr"
|
||
}
|
||
poscss = getStyleCss(nopos)
|
||
break;
|
||
}
|
||
case "bl":{
|
||
const diffb = sys.windowHeight - tb
|
||
const difft = tp - sys.windowTop
|
||
|
||
if(diffb<mh&&difft>=diffb&&diffb < mh){
|
||
nopos = "tl"
|
||
}
|
||
poscss = getStyleCss(nopos)
|
||
break;
|
||
}
|
||
case "tc":{
|
||
const diffb = sys.windowHeight - tb
|
||
const difft = tp - sys.windowTop
|
||
// 逻辑与bc相反。
|
||
if(diffb>mh&&difft<=diffb&&difft < mh){
|
||
nopos = "bc"
|
||
}
|
||
poscss = getStyleCss(nopos)
|
||
break;
|
||
}
|
||
case "tr":{
|
||
const diffb = sys.windowHeight - tb
|
||
const difft = tp - sys.windowTop
|
||
|
||
if(diffb>mh&&difft<=diffb&&difft < mh){
|
||
nopos = "br"
|
||
}
|
||
poscss = getStyleCss(nopos)
|
||
break;
|
||
}
|
||
case "tl":{
|
||
const diffb = sys.windowHeight - tb
|
||
const difft = tp - sys.windowTop
|
||
|
||
if(diffb>mh&&difft<=diffb&&difft < mh){
|
||
nopos = "bl"
|
||
}
|
||
poscss = getStyleCss(nopos)
|
||
break;
|
||
}
|
||
}
|
||
this.nowPos = nopos
|
||
return poscss
|
||
},
|
||
_animationFun() : string {
|
||
return `cubic-bezier(0.07, 0.82, 0.17, 1.20)`
|
||
},
|
||
__height() : string {
|
||
let h = '100%';
|
||
// #ifdef WEB
|
||
h = `calc(100% - ${this.windtop}px)`
|
||
// #endif
|
||
|
||
return h;
|
||
},
|
||
_round() : string {
|
||
return checkIsCssUnit(this.round, xConfig.unit)
|
||
}
|
||
},
|
||
mounted() {
|
||
let t = this;
|
||
|
||
let sys = uni.getWindowInfo()
|
||
this._width = sys.windowWidth
|
||
this._height = sys.windowHeight;
|
||
this.windtop = 0
|
||
|
||
if (this.modelValue) {
|
||
t.isGeiNodeInfoOk = false
|
||
t.showPro = true;
|
||
t.status = "open"
|
||
t.openProWrap();
|
||
}
|
||
|
||
// uni.$on('onPageScroll',this.closeProWrap)
|
||
},
|
||
|
||
beforeUnmount() {
|
||
// uni.$off('onPageScroll',this.closeProWrap)
|
||
clearTimeout(this.tid)
|
||
},
|
||
methods: {
|
||
contentWrapClick(evt : UniPointerEvent) {
|
||
if (this.isClickClose) {
|
||
this.closeProWrap()
|
||
}
|
||
|
||
},
|
||
onEnd() {
|
||
|
||
if (this.status == "close") {
|
||
this.showPro = false
|
||
/**
|
||
* 等同v-model=""
|
||
* @param val {boolean} 当前的显示状态
|
||
*/
|
||
this.$emit('update:modelValue', false)
|
||
}
|
||
this.status = "compeleted"
|
||
},
|
||
openAlert() {
|
||
let t = this;
|
||
if (this.status == 'playing') return;
|
||
this.isGeiNodeInfoOk = false
|
||
this.showPro = true;
|
||
this.$emit('update:modelValue', true)
|
||
this.status = "open"
|
||
|
||
t.openProWrap();
|
||
|
||
// #ifndef MP
|
||
let currentPage = getCurrentPages()[getCurrentPages().length-1]
|
||
currentPage.setPageStyle({
|
||
"disableScroll": true
|
||
})
|
||
// #endif
|
||
|
||
},
|
||
mskerTouch(evt:UniTouchEvent){
|
||
evt.stopPropagation()
|
||
evt.preventDefault()
|
||
},
|
||
openProWrap() {
|
||
this.status = "playing"
|
||
|
||
let _this = this;
|
||
_this.showquerinnode = false
|
||
this.getNodes().then(()=>{
|
||
let ele = _this.$refs['xPopoverWrap'] as UniElement;
|
||
if(ele==null) return;
|
||
ele.style.setProperty('transition-duration','0ms')
|
||
ele.style.setProperty('transform','scale(0)')
|
||
ele.style.setProperty('opacity',0)
|
||
setTimeout(function() {
|
||
ele.style.setProperty('transition-duration','300ms')
|
||
ele.style.setProperty('transform','scale(1)')
|
||
ele.style.setProperty('opacity',1)
|
||
_this.showquerinnode = true;
|
||
}, 25);
|
||
})
|
||
},
|
||
closeProWrap() {
|
||
if (this.status == "close") return;
|
||
this.status = "close"
|
||
try {
|
||
let el = this.$refs['xPopoverWrap'] as Element | null;
|
||
if (el == null) return;
|
||
el.style.setProperty("transition-duration", "300ms")
|
||
el.style.setProperty("transform", "scale(0.64)")
|
||
el.style.setProperty("opacity", "0")
|
||
|
||
} catch (e) {
|
||
console.error(e)
|
||
}
|
||
},
|
||
|
||
getNodes() : Promise<boolean> {
|
||
let _this = this;
|
||
let triele = _this.$refs['xPopover'] as UniElement;
|
||
let delay = 0
|
||
// #ifdef APP-IOS
|
||
delay = 50
|
||
// #endif
|
||
return new Promise(res=>{
|
||
if(triele==null){
|
||
res(true)
|
||
return;
|
||
}
|
||
|
||
setTimeout(function() {
|
||
triele.getBoundingClientRectAsync()
|
||
?.then((ret:DOMRect)=>{
|
||
|
||
let nodeinfo = ret
|
||
let trinode = {
|
||
left: nodeinfo.left!,
|
||
width: nodeinfo.width!,
|
||
height: nodeinfo.height!,
|
||
bottom: nodeinfo.bottom!,
|
||
right: nodeinfo.right!,
|
||
top: nodeinfo.top!,
|
||
} as NODE_INFO
|
||
|
||
if (!_this.showPro) {
|
||
_this.trrigerNodeInfo = trinode
|
||
res(true)
|
||
return;
|
||
}
|
||
// 微信端无法动态获取悬浮节点的宽和高准确的准确性,因为只记录首次正常的值,后面不再更新。
|
||
// #ifdef MP
|
||
if(_this.menuNodeInfo!=null){
|
||
_this.trrigerNodeInfo = trinode
|
||
res(true)
|
||
return;
|
||
}
|
||
// #endif
|
||
|
||
let delayTImeout = 30
|
||
// #ifdef MP
|
||
delayTImeout = 30
|
||
// #endif
|
||
setTimeout(function() {
|
||
let ele = _this.$refs['xPopoverWrap'] as UniElement;
|
||
if(ele==null) return;
|
||
ele!.getBoundingClientRectAsync()
|
||
?.then((rect2:DOMRect)=>{
|
||
nodeinfo = rect2
|
||
_this.trrigerNodeInfo = trinode
|
||
_this.menuNodeInfo = {
|
||
left: nodeinfo.left!,
|
||
width: nodeinfo.width!,
|
||
height: nodeinfo.height!,
|
||
bottom: nodeinfo.bottom!,
|
||
right: nodeinfo.right!,
|
||
top: nodeinfo.top!,
|
||
} as NODE_INFO
|
||
res(true)
|
||
})
|
||
|
||
}, delayTImeout);
|
||
})
|
||
}, delay);
|
||
|
||
|
||
})
|
||
},
|
||
},
|
||
|
||
}
|
||
</script>
|
||
<template>
|
||
<view class="xPopover" ref="xPopover">
|
||
<view :id="id" @click="openAlert" style="flex:1">
|
||
<!--
|
||
@slot 默认插槽触发区域,点击此插槽内容可显示菜单。
|
||
-->
|
||
<slot></slot>
|
||
</view>
|
||
|
||
<!-- #ifdef H5 -->
|
||
<teleport to="uni-app">
|
||
<!-- #endif -->
|
||
|
||
<!-- @touchmove.stop="" @touchstart.stop="" -->
|
||
<view
|
||
@click="closeProWrap"
|
||
@touchmove="closeProWrap"
|
||
v-if="showPro"
|
||
class="xPopoverMasker"
|
||
:style="{
|
||
width:'100%',
|
||
height:__height,
|
||
opacity:showquerinnode?1:0,
|
||
backgroundColor:_maskBgColor,
|
||
zIndex:zIndex
|
||
}">
|
||
|
||
<view
|
||
@click.stop="contentWrapClick"
|
||
@transitionend="onEnd"
|
||
@touchmove.stop=""
|
||
ref="xPopoverWrap"
|
||
:id="menuiId"
|
||
class="xPopoverWrap"
|
||
:style="[
|
||
xPopoverWrapPos,
|
||
{
|
||
'transition-timing-function':_animationFun
|
||
}
|
||
]">
|
||
<view v-if="(nowPos=='bc'||nowPos=='bl'||nowPos=='br')&&showTriangle" :class="`xPopoverUp_${nowPos}` " :style="{padding:`0px ${_round}`}">
|
||
<view class="xPopoverUp " :style="{ 'border-bottom-color': _triangleColor}"></view>
|
||
</view>
|
||
<!-- 多出来的节点是兼容ios -->
|
||
<view :style="{borderRadius:_round}">
|
||
<!--
|
||
@slot 菜单弹出区域的插槽,弹出内容可以在这里自由布局。
|
||
-->
|
||
<slot name="menu"></slot>
|
||
</view>
|
||
<view v-if="(nowPos=='tc'||nowPos=='tl'||nowPos=='tr')&&showTriangle" :class="`xPopoverUp_${nowPos}` " :style="{padding:`0px ${_round}`}">
|
||
<view class="xPopoverBp " :style="{ 'border-bottom-color': _triangleColor}"></view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- #ifdef H5 -->
|
||
</teleport>
|
||
<!-- #endif -->
|
||
</view>
|
||
</template>
|
||
<style scoped lang="scss">
|
||
.xPopoverUp {
|
||
width: 0;
|
||
height: 0;
|
||
border-left: 10px solid transparent;
|
||
border-right: 10px solid transparent;
|
||
border-bottom-width: 10px;
|
||
border-bottom-style: solid;
|
||
transform: rotate(0deg);
|
||
}
|
||
.xPopoverBp{
|
||
width: 0;
|
||
height: 0;
|
||
border-left: 10px solid transparent;
|
||
border-right: 10px solid transparent;
|
||
border-bottom-width: 10px;
|
||
border-bottom-style: solid;
|
||
transform: rotate(180deg);
|
||
}
|
||
.xPopoverUp_bc{
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: center;
|
||
align-items: flex-end;
|
||
}
|
||
.xPopoverUp_bl{
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: flex-start;
|
||
align-items: flex-end;
|
||
}
|
||
.xPopoverUp_br{
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: flex-end;
|
||
align-items: flex-end;
|
||
}
|
||
.xPopoverUp_tc{
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: center;
|
||
align-items: flex-end;
|
||
.xPopoverUp{
|
||
|
||
}
|
||
}
|
||
.xPopoverUp_tl{
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: flex-start;
|
||
align-items: flex-end;
|
||
}
|
||
.xPopoverUp_tr{
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: flex-end;
|
||
align-items: flex-end;
|
||
}
|
||
.xPoperPlace{
|
||
/* transform: translateX(-1000px); */
|
||
}
|
||
.xPopover {
|
||
display: flex;
|
||
flex-direction: row;
|
||
}
|
||
|
||
.xPopoverMasker {
|
||
position: fixed;
|
||
// z-index: 100;
|
||
/* background-color: rgba(0, 0, 0, 0); */
|
||
left: 0;
|
||
top: 0;
|
||
}
|
||
|
||
.xPopoverWrap {
|
||
position: absolute;
|
||
transition-property: transform, opacity;
|
||
transition-timing-function: cubic-bezier(0.07, 0.82, 0.17, 1.20);
|
||
transform: scale(1);
|
||
transition-duration: 300ms;
|
||
opacity: 0;
|
||
/* box-shadow: 0 3px 12px rgba(0, 0, 0, 0.06); */
|
||
/* overflow: hidden; */
|
||
}
|
||
|
||
/* #ifdef MP-WEIXIN */
|
||
.xoverflayShow{
|
||
animation:showAniMation 300ms cubic-bezier(0.07, 0.82, 0.17, 1.20) forwards ;
|
||
transform: scale(1);
|
||
opacity: 1;
|
||
}
|
||
.xoverflayHide{
|
||
animation:hideAniMation 300ms cubic-bezier(0.07, 0.82, 0.17, 1.20) forwards ;
|
||
transform: scale(0.6);
|
||
opacity: 0;
|
||
}
|
||
|
||
@keyframes showAniMation {
|
||
0%{
|
||
transform: scale(0.6);
|
||
opacity: 0;
|
||
}
|
||
100%{
|
||
transform: scale(1);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
@keyframes hideAniMation {
|
||
0%{
|
||
transform: scale(1);
|
||
opacity: 1;
|
||
}
|
||
100%{
|
||
transform: scale(0.6);
|
||
opacity: 0;
|
||
}
|
||
}
|
||
|
||
/* #endif */
|
||
</style> |