You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
341 lines
10 KiB
341 lines
10 KiB
|
3 years ago
|
<template>
|
||
|
|
<view class="s-popup" :class="c_class" :style="c_style">
|
||
|
|
<view
|
||
|
|
v-if="mask"
|
||
|
|
class="s-popup__mask"
|
||
|
|
:style="mask_Style"
|
||
|
|
@touchmove.stop.prevent
|
||
|
|
@click="onMaskClick"
|
||
|
|
></view>
|
||
|
|
<view
|
||
|
|
:id="$sUid"
|
||
|
|
class="s-popup__wrap"
|
||
|
|
:class="wrap_class"
|
||
|
|
:style="wrap_Style"
|
||
|
|
@touchmove.stop.prevent
|
||
|
|
@animationend="onAnimationend"
|
||
|
|
>
|
||
|
|
<s-safe-area-inset v-if="position === 'top' && safeAreaInsetTop" position="top" />
|
||
|
|
<slot />
|
||
|
|
<view
|
||
|
|
v-if="closeable"
|
||
|
|
class="s-popup__close"
|
||
|
|
:style="close_style"
|
||
|
|
@click="onCloseClick"
|
||
|
|
>
|
||
|
|
<slot name="close">
|
||
|
|
<s-icon :name="closeIcon" :size="closeSize" :color="closeColor" />
|
||
|
|
</slot>
|
||
|
|
</view>
|
||
|
|
<s-safe-area-inset
|
||
|
|
v-if="position === 'bottom' && safeAreaInsetBottom"
|
||
|
|
position="bottom"
|
||
|
|
/>
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
import componentMixin from '../../mixins/componentMixin';
|
||
|
|
|
||
|
|
// 记录弹框的zIndex
|
||
|
|
const zIndexMap = new Map();
|
||
|
|
const getMaxZindex = () => {
|
||
|
|
return Math.max(200, ...zIndexMap.values());
|
||
|
|
};
|
||
|
|
|
||
|
|
// position 默认效果
|
||
|
|
const autoAnimateMap = {
|
||
|
|
center: 'fade',
|
||
|
|
left: 'slide-left',
|
||
|
|
right: 'slide-right',
|
||
|
|
top: 'slide-top',
|
||
|
|
bottom: 'slide-bottom',
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* s-popup 弹框
|
||
|
|
* @description 基础弹框组件
|
||
|
|
* @property {String} position = [center | left | right | top | bottom ] 显示位置
|
||
|
|
* @property {Boolean} value v-model双向绑定显示关闭
|
||
|
|
* @property {String} background 背景色
|
||
|
|
* @property {Number|String} width wrap容器宽度
|
||
|
|
* @property {Number|String} height wrap容器高度
|
||
|
|
* @property {Number|String} radius wrap容器圆角
|
||
|
|
* @property {Boolean} round 是否显示圆角
|
||
|
|
* @property {String|Object} wrapStyle wrap容器style样式
|
||
|
|
* @property {Number|String} zIndex z-index,如没指定,则在显示时使用比较自增
|
||
|
|
* @property {String} animatePrefix 内置动画class前缀,默认为 s-animate
|
||
|
|
* @property {String|Boolean|Object} animate = [false|auto|fade|fade-top|slide-top|{enter:'fade',leave:'fade-top'}] 过渡效果,默认 auto 为s-ui styles下animate.scss中定义的效果
|
||
|
|
* @property {Number|String|Object} animateDuration 显示关闭过渡效果时间 ms
|
||
|
|
* @property {Boolean} mask 是否显示遮罩
|
||
|
|
* @property {Number|String} maskOpacity 遮罩透明度 0.7
|
||
|
|
* @property {String|Object} maskStyle 遮罩自定样式
|
||
|
|
* @property {Boolean} maskClose 点击遮罩是否关闭
|
||
|
|
* @property {Number|String} duration 自动关闭时间,为 0不会自动关闭
|
||
|
|
* @property {Boolean} closeable 是否显示关闭图标
|
||
|
|
* @property {Boolean} closeIcon 关闭图标名称或图片链接
|
||
|
|
* @property {String} closeColor 关闭图标颜色
|
||
|
|
* @property {String|Object} closeStyle 关闭图标样式
|
||
|
|
* @property {Function} beforeOpen 显示时拦截钩子返回Boolean或Promise通过resolve(Boolean)拦截
|
||
|
|
* @property {Function} beforeClose 关闭时拦截钩子返回Boolean或Promise通过resolve(Boolean)拦截
|
||
|
|
* @property {Boolean} safeAreaInsetTop 是否留出顶部安全距离(状态栏高度),默认false
|
||
|
|
* @property {Boolean} safeAreaInsetBottom 是否开启底部安全区域适配,默认false
|
||
|
|
* @event {Function} open 打开弹出层时触发
|
||
|
|
* @event {Function} opened 打开弹出层且动画结束后触发
|
||
|
|
* @event {Function} close 关闭弹出层时触发
|
||
|
|
* @event {Function} closed 关闭弹出层且动画结束后触发
|
||
|
|
* @event {Function} mask-close 点击遮罩层关闭时触发
|
||
|
|
* @event {Function} icon-close 点击关闭图标关闭时触发
|
||
|
|
* @example <s-popup v-model="visible"></s-popup>
|
||
|
|
*/
|
||
|
|
export default {
|
||
|
|
name: 'SPopup',
|
||
|
|
mixins: [componentMixin],
|
||
|
|
props: {
|
||
|
|
position: {
|
||
|
|
type: String,
|
||
|
|
default: 'center',
|
||
|
|
},
|
||
|
|
value: Boolean,
|
||
|
|
background: String,
|
||
|
|
width: [Number, String],
|
||
|
|
height: [Number, String],
|
||
|
|
radius: [Number, String],
|
||
|
|
round: Boolean,
|
||
|
|
wrapStyle: [String, Object],
|
||
|
|
zIndex: [Number, String],
|
||
|
|
animatePrefix: {
|
||
|
|
type: String,
|
||
|
|
default: 's-animate',
|
||
|
|
},
|
||
|
|
animate: {
|
||
|
|
type: [String, Boolean, Object],
|
||
|
|
default: 'auto',
|
||
|
|
},
|
||
|
|
animateDuration: {
|
||
|
|
type: [Number, String, Object],
|
||
|
|
default: 300,
|
||
|
|
},
|
||
|
|
mask: {
|
||
|
|
type: Boolean,
|
||
|
|
default: true,
|
||
|
|
},
|
||
|
|
maskOpacity: [Number, String],
|
||
|
|
maskStyle: [String, Object],
|
||
|
|
maskClose: {
|
||
|
|
type: Boolean,
|
||
|
|
default: true,
|
||
|
|
},
|
||
|
|
duration: [Number, String],
|
||
|
|
closeable: Boolean,
|
||
|
|
closeIcon: {
|
||
|
|
type: String,
|
||
|
|
default: 'cross',
|
||
|
|
},
|
||
|
|
closeSize: [Number, String],
|
||
|
|
closeColor: String,
|
||
|
|
closeStyle: [String, Object],
|
||
|
|
beforeOpen: Function,
|
||
|
|
beforeClose: Function,
|
||
|
|
safeAreaInsetTop: Boolean,
|
||
|
|
safeAreaInsetBottom: Boolean,
|
||
|
|
},
|
||
|
|
data() {
|
||
|
|
return {
|
||
|
|
animateStatus: '',
|
||
|
|
isVisible: false,
|
||
|
|
zIndexValue: 1,
|
||
|
|
};
|
||
|
|
},
|
||
|
|
computed: {
|
||
|
|
c_class() {
|
||
|
|
const { position, round, animateStatus, custom_class } = this;
|
||
|
|
return this.$mergeClass({
|
||
|
|
[`s-popup--${position}`]: position && position !== 'custom',
|
||
|
|
's-popup--round': round,
|
||
|
|
's-popup--enter': animateStatus === 'enter',
|
||
|
|
's-popup--leave': animateStatus === 'leave',
|
||
|
|
}, custom_class);
|
||
|
|
},
|
||
|
|
c_style() {
|
||
|
|
return this.$mergeStyle({
|
||
|
|
animationDuration: this.animateStatus ? this.animateMap[this.animateStatus].duration + 'ms' : '',
|
||
|
|
display: this.isVisible ? '' : 'none',
|
||
|
|
zIndex: this.zIndexValue > 0 ? this.zIndexValue : '',
|
||
|
|
}, this.custom_style);
|
||
|
|
},
|
||
|
|
wrap_class() {
|
||
|
|
const animateName = this.animateStatus ? this.animateMap[this.animateStatus].name : '';
|
||
|
|
return animateName ? `${this.animatePrefix ? this.animatePrefix + '-' : ''}${animateName}-${this.animateStatus}` : '';
|
||
|
|
},
|
||
|
|
wrap_Style() {
|
||
|
|
return this.$mergeStyle({
|
||
|
|
width: this.$addUnit(this.width),
|
||
|
|
height: this.$addUnit(this.height),
|
||
|
|
background: this.background,
|
||
|
|
borderRadius: this.$addUnit(this.radius),
|
||
|
|
}, this.wrapStyle);
|
||
|
|
},
|
||
|
|
mask_Style() {
|
||
|
|
return this.$mergeStyle({
|
||
|
|
backgroundColor: this.$isDef(this.maskOpacity) ? 'rgba(0, 0, 0, ' + this.maskOpacity + ')' : '',
|
||
|
|
}, this.maskStyle);
|
||
|
|
},
|
||
|
|
close_style() {
|
||
|
|
return this.$mergeStyle(this.closeStyle);
|
||
|
|
},
|
||
|
|
animateMap() {
|
||
|
|
let animate = this.animate;
|
||
|
|
let duration = this.animateDuration;
|
||
|
|
|
||
|
|
if (animate === 'auto') {
|
||
|
|
animate = autoAnimateMap[this.position];
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this.$getType(animate) !== 'object') {
|
||
|
|
animate = { enter: animate, leave: animate };
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this.$getType(duration) !== 'object') {
|
||
|
|
duration = { enter: duration, leave: duration };
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
enter: {
|
||
|
|
name: animate.enter,
|
||
|
|
duration: parseInt(animate.enter ? duration.enter : 0) || 0,
|
||
|
|
},
|
||
|
|
leave: {
|
||
|
|
name: animate.leave,
|
||
|
|
duration: parseInt(animate.leave ? duration.leave : 0) || 0,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
},
|
||
|
|
},
|
||
|
|
watch: {
|
||
|
|
value() {
|
||
|
|
this.updateVisible();
|
||
|
|
},
|
||
|
|
},
|
||
|
|
created() {
|
||
|
|
Object.assign(this, {
|
||
|
|
visibleId: 0,
|
||
|
|
visibleStatus: false,
|
||
|
|
animationendCallback: null,
|
||
|
|
timer: null,
|
||
|
|
});
|
||
|
|
},
|
||
|
|
mounted() {
|
||
|
|
this.updateVisible();
|
||
|
|
},
|
||
|
|
beforeDestroy() {
|
||
|
|
zIndexMap.delete(this.$sUid);
|
||
|
|
this.removeAnimationendCallback();
|
||
|
|
this.clearTimer();
|
||
|
|
},
|
||
|
|
methods: {
|
||
|
|
updateVisible() {
|
||
|
|
if (this._isMounted && this.visibleStatus !== this.value) {
|
||
|
|
this[this.value ? 'open' : 'close']();
|
||
|
|
}
|
||
|
|
},
|
||
|
|
setAnimationendCallback(callback, duration) {
|
||
|
|
if (duration > 0) {
|
||
|
|
this.animationendCallback = callback;
|
||
|
|
} else {
|
||
|
|
callback();
|
||
|
|
}
|
||
|
|
},
|
||
|
|
removeAnimationendCallback() {
|
||
|
|
if (this.animationendCallback) {
|
||
|
|
this.animationendCallback = null;
|
||
|
|
}
|
||
|
|
},
|
||
|
|
onAnimationend(e) {
|
||
|
|
if (e.target.id === this.$sUid && this.animationendCallback) {
|
||
|
|
this.animationendCallback();
|
||
|
|
this.animationendCallback = null;
|
||
|
|
}
|
||
|
|
},
|
||
|
|
clearTimer() {
|
||
|
|
if (this.timer) {
|
||
|
|
clearTimeout(this.timer);
|
||
|
|
this.timer = null;
|
||
|
|
}
|
||
|
|
},
|
||
|
|
async open() {
|
||
|
|
if (!this.visibleStatus) {
|
||
|
|
const visibleId = ++this.visibleId;
|
||
|
|
const beforeOpen = this.$getPropsFn('beforeOpen');
|
||
|
|
const flag = beforeOpen ? await beforeOpen() && visibleId === this.visibleId : true;
|
||
|
|
if (flag) {
|
||
|
|
this.removeAnimationendCallback();
|
||
|
|
this.clearTimer();
|
||
|
|
let zIndex = this.zIndex;
|
||
|
|
if (!(+zIndex > 0)) {
|
||
|
|
zIndex = getMaxZindex() + 1;
|
||
|
|
zIndexMap.set(this.$sUid, zIndex);
|
||
|
|
}
|
||
|
|
this.visibleStatus = true;
|
||
|
|
this.zIndexValue = zIndex;
|
||
|
|
this.isVisible = true;
|
||
|
|
this.animateStatus = 'enter';
|
||
|
|
this.$emit('input', true);
|
||
|
|
this.$emit('open');
|
||
|
|
this.setAnimationendCallback(() => {
|
||
|
|
this.animateStatus = '';
|
||
|
|
this.$emit('opened');
|
||
|
|
// 自动关闭
|
||
|
|
const duration = parseInt(this.duration);
|
||
|
|
if (duration > 0) {
|
||
|
|
this.timer = setTimeout(() => {
|
||
|
|
this.visibleStatus && this.close();
|
||
|
|
}, duration);
|
||
|
|
}
|
||
|
|
}, this.animateMap.enter.duration);
|
||
|
|
} else {
|
||
|
|
this.$emit('input', false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
async close() {
|
||
|
|
if (this.visibleStatus) {
|
||
|
|
const visibleId = ++this.visibleId;
|
||
|
|
const beforeClose = this.$getPropsFn('beforeClose');
|
||
|
|
const flag = beforeClose ? await beforeClose() && visibleId === this.visibleId : true;
|
||
|
|
if (flag) {
|
||
|
|
this.removeAnimationendCallback();
|
||
|
|
this.clearTimer();
|
||
|
|
this.visibleStatus = false;
|
||
|
|
this.animateStatus = 'leave';
|
||
|
|
this.$emit('input', false);
|
||
|
|
this.$emit('close');
|
||
|
|
this.setAnimationendCallback(() => {
|
||
|
|
this.isVisible = false;
|
||
|
|
this.zIndexValue = 0;
|
||
|
|
this.animateStatus = '';
|
||
|
|
zIndexMap.delete(this.$sUid);
|
||
|
|
this.$emit('closed');
|
||
|
|
}, this.animateMap.leave.duration);
|
||
|
|
} else {
|
||
|
|
this.$emit('input', true);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
onMaskClick() {
|
||
|
|
if (this.maskClose) {
|
||
|
|
this.close();
|
||
|
|
this.$emit('mask-close');
|
||
|
|
}
|
||
|
|
},
|
||
|
|
onCloseClick() {
|
||
|
|
this.close();
|
||
|
|
this.$emit('icon-close');
|
||
|
|
},
|
||
|
|
},
|
||
|
|
};
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style lang="scss" src="./index.scss"></style>
|