CNCEC_APP/uni_modules/uview-pro/components/u-popup/u-popup.vue

380 lines
11 KiB
Vue
Raw Normal View History

2026-03-25 14:54:15 +08:00
<template>
<view
v-if="visibleSync"
class="u-drawer"
:style="$u.toStyle({ zIndex: Number(uZIndex) - 1 }, customStyle)"
:class="customClass"
hover-stop-propagation
>
<u-mask
:duration="duration"
:custom-style="maskCustomStyle"
:maskClickAble="maskCloseAble"
:z-index="Number(uZIndex) - 2"
:show="showDrawer && mask"
@click="maskClick"
></u-mask>
<view
class="u-drawer-content"
@tap="modeCenterClose(mode)"
:class="[
safeAreaInsetBottom ? 'safe-area-inset-bottom' : '',
'u-drawer-' + mode,
showDrawer ? 'u-drawer-content-visible' : '',
zoom && mode == 'center' ? 'u-animation-zoom' : ''
]"
@touchmove.stop.prevent
:style="[style]"
>
<view
v-if="mode == 'center'"
class="u-mode-center-box"
@tap.stop.prevent
@touchmove.stop.prevent
:style="[centerStyle]"
>
<view v-if="closeable" @click="close" class="u-close" :class="['u-close--' + closeIconPos]">
<u-icon :name="closeIcon" :color="closeIconColor" :size="closeIconSize"></u-icon>
</view>
<scroll-view class="u-drawer__scroll-view" :scroll-y="true">
<slot />
</scroll-view>
</view>
<scroll-view class="u-drawer__scroll-view" :scroll-y="true" v-else>
<slot />
</scroll-view>
<view
v-if="mode != 'center' && closeable"
@click="close"
class="u-close"
:class="['u-close--' + closeIconPos]"
>
<u-icon :name="closeIcon" :color="closeIconColor" :size="closeIconSize"></u-icon>
</view>
</view>
</view>
</template>
<script lang="ts">
export default {
name: 'u-popup',
options: {
addGlobalClass: true,
// #ifndef MP-TOUTIAO
virtualHost: true,
// #endif
styleIsolation: 'shared'
}
};
</script>
<script setup lang="ts">
import { ref, computed, watch, onMounted, nextTick } from 'vue';
import { $u } from '../..';
import { PopupProps } from './types';
/**
* popup 弹窗
* @description 弹出层容器用于展示弹窗信息提示等内容支持上右和中部弹出组件只提供容器内部内容由用户自定义
* @tutorial https://uviewpro.cn/zh/components/popup.html
* @property {String} mode 弹出方向默认left
* @property {Boolean} mask 是否显示遮罩默认true
* @property {Stringr | Number} length mode=left | 见官网说明默认auto
* @property {Boolean} zoom 是否开启缩放动画只在mode为center时有效默认true
* @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配默认false
* @property {Boolean} mask-close-able 点击遮罩是否可以关闭弹出层默认true
* @property {Object} custom-style 用户自定义样式
* @property {Stringr | Number} negative-top 中部弹出时往上偏移的值
* @property {Numberr | String} border-radius 弹窗圆角值默认0
* @property {Numberr | String} z-index 弹出内容的z-index值默认1075
* @property {Boolean} closeable 是否显示关闭图标默认false
* @property {String} close-icon 关闭图标的名称只能uView的内置图标
* @property {String} close-icon-pos 自定义关闭图标位置默认top-right
* @property {String} close-icon-color 关闭图标的颜色默认var(--u-tips-color)
* @property {Number | String} close-icon-size 关闭图标的大小单位rpx默认30
* @event {Function} open 弹出层打开
* @event {Function} close 弹出层收起
* @example <u-popup v-model="show"><view>出淤泥而不染濯清涟而不妖</view></u-popup>
*/
const props = defineProps(PopupProps);
const emit = defineEmits(['update:modelValue', 'open', 'close']);
// 组件内部状态
const visibleSync = ref(false);
const showDrawer = ref(false);
const timer = ref<ReturnType<typeof setTimeout> | null>(null);
const closeFromInner = ref(false); // value的值改变是发生在内部还是外部
// 根据mode的位置设定其弹窗的宽度(mode = left|right),或者高度(mode = top|bottom)
const style = computed(() => {
let style: Record<string, any> = {};
// 如果是左边或者上边弹出时需要给translate设置为负值用于隐藏
if (props.mode == 'left' || props.mode == 'right') {
style = {
width: props.width ? getUnitValue(props.width) : getUnitValue(props.length),
height: '100%',
transform: `translate3D(${props.mode == 'left' ? '-100%' : '100%'},0px,0px)`
};
} else if (props.mode == 'top' || props.mode == 'bottom') {
style = {
width: '100%',
height: props.height ? getUnitValue(props.height) : getUnitValue(props.length),
transform: `translate3D(0px,${props.mode == 'top' ? '-100%' : '100%'},0px)`
};
}
// 如果用户设置了borderRadius值添加弹窗的圆角
style.zIndex = uZIndex.value;
if (props.borderRadius) {
switch (props.mode) {
case 'left':
style.borderRadius = `0 ${props.borderRadius}rpx ${props.borderRadius}rpx 0`;
break;
case 'top':
style.borderRadius = `0 0 ${props.borderRadius}rpx ${props.borderRadius}rpx`;
break;
case 'right':
style.borderRadius = `${props.borderRadius}rpx 0 0 ${props.borderRadius}rpx`;
break;
case 'bottom':
style.borderRadius = `${props.borderRadius}rpx ${props.borderRadius}rpx 0 0`;
break;
default:
}
// 不加可能圆角无效
style.overflow = 'hidden';
}
if (props.duration) style.transition = `all ${Number(props.duration) / 1000}s linear`;
return style;
});
// 中部弹窗的特有样式
const centerStyle = computed(() => {
let style: Record<string, any> = {};
style.width = props.width ? getUnitValue(props.width) : getUnitValue(props.length);
// 中部弹出的模式如果没有设置高度就用auto值由内容撑开高度
style.height = props.height ? getUnitValue(props.height) : 'auto';
style.zIndex = uZIndex.value;
style.marginTop = `-${$u.addUnit(props.negativeTop)}`;
if (props.borderRadius) {
style.borderRadius = `${props.borderRadius}rpx`;
// 不加可能圆角无效
style.overflow = 'hidden';
}
return style;
});
// 计算整理后的z-index值
const uZIndex = computed(() => (props.zIndex ? props.zIndex : $u.zIndex.popup));
watch(
() => props.modelValue,
val => {
if (val) {
open();
} else if (!closeFromInner.value) {
close();
}
closeFromInner.value = false;
}
);
onMounted(() => {
if (props.modelValue) open();
});
/**
* 判断传入的值是否带有单位如果没有就默认用rpx单位
*/
function getUnitValue(val: string | number) {
if (/(%|px|rpx|auto)$/.test(String(val))) return val;
else return val + 'rpx';
}
/**
* 遮罩被点击
*/
function maskClick() {
close();
}
/**
* 关闭弹窗
*/
function close() {
// 标记关闭是内部发生的否则修改了value值导致watch中对value检测导致再执行一遍close
// 造成@close事件触发两次
closeFromInner.value = true;
change('showDrawer', 'visibleSync', false);
}
/**
* 中部弹出时点击内容区域关闭弹窗
*/
function modeCenterClose(mode: string) {
// 中部弹出时,需要.u-drawer-content将居中内容此元素会铺满屏幕点击需要关闭弹窗
// 让其只在mode=center时起作用
if (mode != 'center' || !props.maskCloseAble) return;
close();
}
/**
* 打开弹窗
*/
function open() {
change('visibleSync', 'showDrawer', true);
}
/**
* 控制弹窗显示/隐藏的动画和状态
* 此处的原理是关闭时先通过动画隐藏弹窗和遮罩再移除整个组件
* 打开时先渲染组件延时一定时间再让遮罩和弹窗的动画起作用
*/
function change(param1: 'showDrawer' | 'visibleSync', param2: 'visibleSync' | 'showDrawer', status: boolean) {
// 如果this.popup为false意味着为pickeractionsheet等组件调用了popup组件
if (props.popup === true) {
emit('update:modelValue', status);
}
(param1 === 'showDrawer' ? showDrawer : visibleSync).value = status;
if (status) {
// #ifdef H5 || MP
timer.value = setTimeout(() => {
(param2 === 'showDrawer' ? showDrawer : visibleSync).value = status;
emit(status ? 'open' : 'close');
}, 50);
// #endif
// #ifndef H5 || MP
nextTick(() => {
(param2 === 'showDrawer' ? showDrawer : visibleSync).value = status;
emit(status ? 'open' : 'close');
});
// #endif
} else {
timer.value = setTimeout(() => {
(param2 === 'showDrawer' ? showDrawer : visibleSync).value = status;
emit(status ? 'open' : 'close');
}, Number(props.duration));
}
}
</script>
<style scoped lang="scss">
@import '../../libs/css/style.components.scss';
.u-drawer {
/* #ifndef APP-NVUE */
display: block;
/* #endif */
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
}
.u-drawer-content {
/* #ifndef APP-NVUE */
display: block;
/* #endif */
position: absolute;
z-index: 1003;
transition: all 0.25s linear;
}
.u-drawer__scroll-view {
width: 100%;
height: 100%;
}
.u-drawer-left {
top: 0;
bottom: 0;
left: 0;
background-color: var(--u-bg-white);
}
.u-drawer-right {
right: 0;
top: 0;
bottom: 0;
background-color: var(--u-bg-white);
}
.u-drawer-top {
top: 0;
left: 0;
right: 0;
background-color: var(--u-bg-white);
}
.u-drawer-bottom {
bottom: 0;
left: 0;
right: 0;
background-color: var(--u-bg-white);
}
.u-drawer-center {
@include vue-flex;
flex-direction: column;
bottom: 0;
left: 0;
right: 0;
top: 0;
justify-content: center;
align-items: center;
opacity: 0;
z-index: 99999;
}
.u-mode-center-box {
min-width: 100rpx;
min-height: 100rpx;
/* #ifndef APP-NVUE */
display: block;
/* #endif */
position: relative;
background-color: var(--u-bg-white);
}
.u-drawer-content-visible.u-drawer-center {
transform: scale(1);
opacity: 1;
}
.u-animation-zoom {
transform: scale(1.15);
}
.u-drawer-content-visible {
transform: translate3D(0px, 0px, 0px) !important;
}
.u-close {
position: absolute;
z-index: 3;
}
.u-close--top-left {
top: 30rpx;
left: 30rpx;
}
.u-close--top-right {
top: 30rpx;
right: 30rpx;
}
.u-close--bottom-left {
bottom: 30rpx;
left: 30rpx;
}
.u-close--bottom-right {
right: 30rpx;
bottom: 30rpx;
}
</style>