254 lines
5.2 KiB
Vue
254 lines
5.2 KiB
Vue
<template>
|
||
<view @touchmove.stop.prevent>
|
||
<view v-if="popupValue" class="u-loading-init" :class="[direction]">
|
||
<u-loading :mode="mode" :color="color" :size="size" />
|
||
<view v-if="currentText" class="u-loading-tips">
|
||
{{ currentText }}
|
||
</view>
|
||
</view>
|
||
<view class="u-loading-mask" :class="[popupValue ? 'u-mask-show' : '']" @click="onMaskClick" />
|
||
</view>
|
||
</template>
|
||
|
||
<script lang="ts">
|
||
export default {
|
||
name: 'u-loading-popup',
|
||
options: {
|
||
addGlobalClass: true,
|
||
// #ifndef MP-TOUTIAO
|
||
virtualHost: true,
|
||
// #endif
|
||
styleIsolation: 'shared'
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<script lang="ts" setup>
|
||
import { computed, onUnmounted, ref, watch } from 'vue';
|
||
import { LoadingPopupProps } from './types';
|
||
|
||
// 组件props类型
|
||
const props = defineProps(LoadingPopupProps);
|
||
const emit = defineEmits(['update:modelValue', 'cancel']);
|
||
|
||
// 自动关闭的持续时间定时器
|
||
let durationTimer: ReturnType<typeof setTimeout> | null = null;
|
||
// 关闭按钮倒计时定时器
|
||
let cancelTimer: ReturnType<typeof setTimeout> | null = null;
|
||
// 记录弹窗显示的时间戳
|
||
const now = ref(0);
|
||
// 点击遮罩层是否可关闭(超时后)
|
||
const canClose = ref(false);
|
||
// 当前显示的text,优先级:loading参数 > props.text
|
||
const currentText = ref(props.text);
|
||
|
||
const popupValue = computed({
|
||
get: () => props.modelValue,
|
||
set: (val: boolean) => emit('update:modelValue', val)
|
||
});
|
||
|
||
watch(
|
||
() => popupValue.value,
|
||
val => {
|
||
if (val) {
|
||
doOpen(currentText.value);
|
||
} else {
|
||
doClose();
|
||
}
|
||
}
|
||
);
|
||
|
||
// 响应 props 变更,自动刷新当前 text
|
||
watch(
|
||
() => props.text,
|
||
val => {
|
||
currentText.value = val;
|
||
}
|
||
);
|
||
|
||
// 响应 cancelTime/duration 变化,重启定时器
|
||
watch(
|
||
() => [props.cancelTime, props.duration],
|
||
() => {
|
||
if (popupValue.value) {
|
||
clearDurationTimer();
|
||
startCancelTime();
|
||
startDurationTime();
|
||
}
|
||
}
|
||
);
|
||
|
||
/**
|
||
* 启动超时关闭按钮计时
|
||
*/
|
||
function startCancelTime() {
|
||
clearCancelTimer();
|
||
canClose.value = false;
|
||
if (props.cancelTime > 0) {
|
||
cancelTimer = setTimeout(() => {
|
||
canClose.value = true;
|
||
}, props.cancelTime);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 启动持续时间计时
|
||
*/
|
||
function startDurationTime() {
|
||
clearDurationTimer();
|
||
if (props.duration) {
|
||
durationTimer = setTimeout(() => {
|
||
close();
|
||
}, props.duration);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 内部显示逻辑,初始化所有状态
|
||
*/
|
||
function doOpen(text?: string) {
|
||
canClose.value = false;
|
||
clearDurationTimer();
|
||
clearCancelTimer();
|
||
now.value = Date.now();
|
||
if (typeof text === 'string' && text !== '') {
|
||
currentText.value = text;
|
||
} else {
|
||
currentText.value = props.text;
|
||
}
|
||
startCancelTime();
|
||
startDurationTime();
|
||
}
|
||
|
||
/**
|
||
* 内部关闭逻辑,重置所有状态
|
||
*/
|
||
function doClose() {
|
||
canClose.value = false;
|
||
currentText.value = props.text;
|
||
clearDurationTimer();
|
||
clearCancelTimer();
|
||
}
|
||
|
||
/**
|
||
* 显示弹窗
|
||
* @param text 可选,优先显示的文案
|
||
*/
|
||
function open(text?: string) {
|
||
currentText.value = text || props.text;
|
||
popupValue.value = true;
|
||
}
|
||
|
||
/**
|
||
* 隐藏弹窗
|
||
*/
|
||
function close() {
|
||
popupValue.value = false;
|
||
}
|
||
|
||
// 清理定时器
|
||
function clearDurationTimer() {
|
||
if (durationTimer) {
|
||
clearTimeout(durationTimer);
|
||
durationTimer = null;
|
||
}
|
||
}
|
||
function clearCancelTimer() {
|
||
if (cancelTimer) {
|
||
clearTimeout(cancelTimer);
|
||
cancelTimer = null;
|
||
}
|
||
}
|
||
|
||
// 遮罩点击事件
|
||
function onMaskClick() {
|
||
// 只有显示关闭按钮时才允许关闭
|
||
if (canClose.value) {
|
||
emit('cancel');
|
||
close();
|
||
}
|
||
}
|
||
|
||
onUnmounted(() => {
|
||
clearDurationTimer();
|
||
clearCancelTimer();
|
||
});
|
||
|
||
defineExpose({
|
||
open,
|
||
close
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss">
|
||
.u-loading-mask {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0);
|
||
z-index: 9999996;
|
||
transition: all 0.3s ease-in-out;
|
||
opacity: 0;
|
||
visibility: hidden;
|
||
}
|
||
|
||
.u-mask-show {
|
||
visibility: visible;
|
||
opacity: 1;
|
||
}
|
||
|
||
.u-loading-init {
|
||
position: relative;
|
||
min-width: 200rpx;
|
||
min-height: 200rpx;
|
||
max-width: 500rpx;
|
||
padding: 15rpx 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-direction: column;
|
||
position: fixed;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
z-index: 9999;
|
||
font-size: 30rpx;
|
||
color: var(--u-white-color);
|
||
background: rgba(0, 0, 0, 0.7);
|
||
border-radius: 7px;
|
||
|
||
.u-icon-close {
|
||
position: absolute;
|
||
top: 4rpx;
|
||
right: 2rpx;
|
||
color: var(--u-white-color);
|
||
opacity: 0.8;
|
||
}
|
||
|
||
&.horizontal {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: left;
|
||
width: 600rpx;
|
||
max-width: 600rpx;
|
||
min-height: 150rpx;
|
||
padding-left: 40rpx;
|
||
|
||
.u-loading-tips {
|
||
margin-top: 0;
|
||
margin-left: 20rpx;
|
||
font-size: 32rpx;
|
||
}
|
||
}
|
||
}
|
||
|
||
.u-loading-tips {
|
||
text-align: center;
|
||
padding: 0 20rpx;
|
||
box-sizing: border-box;
|
||
margin-top: 36rpx;
|
||
}
|
||
</style>
|