This commit is contained in:
2026-09-24 16:25:22 +08:00
commit 7428184f01
1198 changed files with 314515 additions and 0 deletions
@@ -0,0 +1,628 @@
<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>