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

433 lines
13 KiB
Vue
Raw Normal View History

2026-03-25 14:54:15 +08:00
<template>
<view>
<u-popup
v-model="popupValue"
mode="center"
:zoom="effectiveConfig.zoom"
:popup="false"
:z-index="uZIndex"
:length="effectiveConfig.width"
:mask-close-able="effectiveConfig.maskCloseAble"
:border-radius="effectiveConfig.borderRadius"
:negative-top="effectiveConfig.negativeTop"
:custom-class="effectiveConfig.customClass"
@close="popupClose"
>
<view class="u-model" :style="$u.toStyle(effectiveConfig.customStyle)">
<view
v-if="effectiveConfig.showTitle"
class="u-model__title u-line-1"
:style="$u.toStyle(effectiveConfig.titleStyle)"
>
{{ effectiveConfig.title }}
</view>
<view class="u-model__content">
<view v-if="slots.default" :style="$u.toStyle(effectiveConfig.contentStyle)">
<slot />
</view>
<view v-else class="u-model__content__message" :style="$u.toStyle(effectiveConfig.contentStyle)">
<text>{{ effectiveConfig.content }}</text>
</view>
</view>
<view
v-if="effectiveConfig.showCancelButton || effectiveConfig.showConfirmButton"
class="u-model__footer u-border-top"
>
<view
v-if="effectiveConfig.showCancelButton"
:hover-stay-time="100"
hover-class="u-model__btn--hover"
class="u-model__footer__button"
:style="$u.toStyle(cancelBtnStyle)"
@tap="cancel"
>
{{ effectiveConfig.cancelText }}
</view>
<view
v-if="effectiveConfig.showConfirmButton || slots['confirm-button']"
class="u-model__footer__button hairline-left"
:hover-stay-time="100"
:hover-class="effectiveConfig.asyncClose ? 'none' : 'u-model__btn--hover'"
:style="[confirmBtnStyle]"
@tap="confirm"
>
<slot v-if="slots['confirm-button']" name="confirm-button"></slot>
<template v-else>
<u-loading mode="circle" :color="effectiveConfig.confirmColor" v-if="loading"></u-loading>
<template v-else>
{{ effectiveConfig.confirmText }}
</template>
</template>
</view>
</view>
</view>
</u-popup>
</view>
</template>
<script lang="ts">
export default {
name: 'u-modal',
options: {
addGlobalClass: true,
// #ifndef MP-TOUTIAO
virtualHost: true,
// #endif
styleIsolation: 'shared'
}
};
</script>
<script setup lang="ts">
import { ref, computed, watch, onMounted, onBeforeUnmount, useSlots } from 'vue';
import { $u } from '../..';
import { ModalProps } from './types';
import {
U_MODAL_EVENT_CLEAR_LOADING,
U_MODAL_EVENT_HIDE,
U_MODAL_EVENT_SHOW,
U_MODAL_GLOBAL_EVENT_CLEAR_LOADING,
U_MODAL_GLOBAL_EVENT_HIDE,
U_MODAL_GLOBAL_EVENT_SHOW,
getEventWithCurrentPage,
type ModalPayload
} from './service';
/**
* modal 模态框
* @description 弹出模态框常用于消息提示消息确认在当前页面内完成特定的交互操作
* @tutorial https://uviewpro.cn/zh/components/modal.html
* @property {Boolean} value 是否显示模态框
* @property {String | Number} z-index 层级
* @property {String} title 模态框标题默认"提示"
* @property {String | Number} width 模态框宽度默认600
* @property {String} content 模态框内容默认"内容"
* @property {Boolean} show-title 是否显示标题默认true
* @property {Boolean} async-close 是否异步关闭只对确定按钮有效默认false
* @property {Boolean} show-confirm-button 是否显示确认按钮默认true
* @property {String | Number} negative-top modal往上偏移的值
* @property {Boolean} show-cancel-button 是否显示取消按钮默认false
* @property {Boolean} mask-close-able 是否允许点击遮罩关闭modal默认false
* @property {String} confirm-text 确认按钮的文字内容默认"确认"
* @property {String} cancel-text 取消按钮的文字内容默认"取消"
* @property {String} cancel-color 取消按钮的颜色默认"var(--u-content-color)"
* @property {String} confirm-color 确认按钮的文字内容默认主题色primary
* @property {String | Number} border-radius 模态框圆角值单位rpx默认16
* @property {Object} title-style 自定义标题样式对象形式
* @property {Object} content-style 自定义内容样式对象形式
* @property {Object} cancel-style 自定义取消按钮样式对象形式
* @property {Object} confirm-style 自定义确认按钮样式对象形式
* @property {Boolean} zoom 是否开启缩放模式默认true
* @event {Function} confirm 确认按钮被点击
* @event {Function} cancel 取消按钮被点击
* @example <u-modal :src="title" :content="content"></u-modal>
*/
const props = defineProps(ModalProps);
const emit = defineEmits(['update:modelValue', 'confirm', 'cancel']);
const slots = useSlots();
// 确认按钮是否正在加载中
const loading = ref(false);
const isGlobal = computed(() => !!props.global);
const isPage = computed(() => !!props.page);
const showEvent = computed(() =>
isGlobal.value
? U_MODAL_GLOBAL_EVENT_SHOW
: isPage.value
? getEventWithCurrentPage(U_MODAL_EVENT_SHOW, props.page)
: ''
);
const hideEvent = computed(() =>
isGlobal.value
? U_MODAL_GLOBAL_EVENT_HIDE
: isPage.value
? getEventWithCurrentPage(U_MODAL_EVENT_HIDE, props.page)
: ''
);
const clearLoadingEvent = computed(() =>
isGlobal.value
? U_MODAL_GLOBAL_EVENT_CLEAR_LOADING
: isPage.value
? getEventWithCurrentPage(U_MODAL_EVENT_CLEAR_LOADING, props.page)
: ''
);
// 存储用户传入的回调函数
let userOnConfirm: (() => void) | null = null;
let userOnCancel: (() => void) | null = null;
// 需要与 effectiveConfig 合并的 props 键名列表(用于函数式调用)
const MERGE_PROPS_KEYS = [
'title',
'content',
'showTitle',
'showConfirmButton',
'showCancelButton',
'confirmText',
'cancelText',
'confirmColor',
'cancelColor',
'confirmStyle',
'cancelStyle',
'titleStyle',
'contentStyle',
'asyncClose',
'borderRadius',
'width',
'zoom',
'maskCloseAble',
'negativeTop',
'zIndex',
'customStyle',
'customClass'
] as const;
// 函数式调用时的临时配置
const tempConfig = ref<Partial<ModalPayload>>({});
// 函数式调用时的内部显示状态(用于 global 模式)
const internalShow = ref(false);
// 有效的配置(函数式调用时合并 tempConfig 和 propsv-model 时使用 props
const effectiveConfig = computed(() => {
// 如果有临时配置(函数式调用),合并用户配置与 props 默认值
if (Object.keys(tempConfig.value).length > 0) {
const result: Record<string, any> = {};
for (const key of MERGE_PROPS_KEYS) {
// 用户配置优先,否则使用 props 默认值
result[key] = (tempConfig.value as Record<string, any>)[key] ?? (props as Record<string, any>)[key];
}
result.zIndex = tempConfig.value.zIndex ?? props.zIndex ?? $u.zIndex.popup;
return result;
}
// v-model 直接控制时使用 props
return props;
});
// 取消按钮样式
const cancelBtnStyle = computed(() => {
return Object.assign({ color: effectiveConfig.value.cancelColor }, effectiveConfig.value.cancelStyle);
});
// 确认按钮样式
const confirmBtnStyle = computed(() => {
return Object.assign({ color: effectiveConfig.value.confirmColor }, effectiveConfig.value.confirmStyle);
});
// 弹窗的样式
const uZIndex = computed(() => effectiveConfig.value.zIndex ?? $u.zIndex.popup);
// 是否使用内部状态控制显示global 模式)
const useInternalShow = computed(() => isGlobal.value || isPage.value);
// 最终显示状态
const finalShow = computed(() => {
if (useInternalShow.value) {
return internalShow.value;
}
return props.modelValue;
});
// u-popup 绑定的值
const popupValue = computed({
get: () => finalShow.value,
set: (val: boolean) => {
if (useInternalShow.value) {
internalShow.value = val;
} else {
emit('update:modelValue', val);
}
}
});
// 如果是异步关闭时外部修改v-model的值为false时重置内部的loading状态避免下次打开的时候状态混乱
watch(
() => popupValue.value,
n => {
if (n === true) loading.value = false;
}
);
/**
* 确认按钮点击事件
*/
function confirm() {
// 先调用回调,再重置配置
const onConfirm = userOnConfirm;
// 异步关闭
if (effectiveConfig.value.asyncClose) {
loading.value = true;
} else {
popupValue.value = false;
// 延迟重置配置,避免闪屏
setTimeout(() => resetTempConfig(), 300);
}
emit('confirm');
// 调用函数式调用时用户传入的回调
onConfirm?.();
}
/**
* 取消按钮点击事件
*/
function cancel() {
// 先调用回调,再重置配置
const onCancel = userOnCancel;
// 延迟重置配置,避免闪屏
setTimeout(() => resetTempConfig(), 300);
emit('cancel');
popupValue.value = false;
// 调用函数式调用时用户传入的回调
onCancel?.();
// 目前popup弹窗关闭有一个延时操作此处做一个延时
// 避免确认按钮文字变成了"确定"字样modal还没消失造成视觉不好的效果
setTimeout(() => {
loading.value = false;
}, 300);
}
/**
* 点击遮罩关闭modal设置v-model的值为false否则无法第二次弹起modal
*/
function popupClose() {
popupValue.value = false;
resetTempConfig();
}
/**
* 清除加载中的状态
*/
function clearLoading() {
loading.value = false;
}
/**
* 函数式调用显示 modal
*/
function onServiceShow(payload: ModalPayload) {
// 保存回调函数
userOnConfirm = payload.onConfirm ?? null;
userOnCancel = payload.onCancel ?? null;
// 只保存用户传入的配置(过滤掉回调)
const { onConfirm, onCancel, ...rest } = payload;
tempConfig.value = rest;
// 使用内部状态控制显示
internalShow.value = true;
}
/**
* 函数式调用关闭 modal
*/
function onServiceHide() {
internalShow.value = false;
resetTempConfig();
}
/**
* 重置临时配置
*/
function resetTempConfig() {
tempConfig.value = {};
userOnConfirm = null;
userOnCancel = null;
}
// 开始监听事件
function startListeners() {
// 如果为全局 toast则先移除所有事件监听再重新监听
if (isGlobal.value) {
removeAllListeners();
}
if (showEvent.value) {
uni?.$on && uni.$on(showEvent.value, onServiceShow);
}
if (hideEvent.value) {
uni?.$on && uni.$on(hideEvent.value, onServiceHide);
}
if (clearLoadingEvent.value) {
uni?.$on && uni.$on(clearLoadingEvent.value, clearLoading);
}
}
// 停止监听事件
function stopListeners() {
if (showEvent.value) {
uni?.$off && uni.$off(showEvent.value, onServiceShow);
}
if (hideEvent.value) {
uni?.$off && uni.$off(hideEvent.value, onServiceHide);
}
if (clearLoadingEvent.value) {
uni?.$off && uni.$off(clearLoadingEvent.value, clearLoading);
}
}
// 移除所有事件监听
function removeAllListeners() {
if (showEvent.value) {
uni?.$off && uni.$off(showEvent.value);
}
if (hideEvent.value) {
uni?.$off && uni.$off(hideEvent.value);
}
if (clearLoadingEvent.value) {
uni?.$off && uni.$off(clearLoadingEvent.value);
}
}
onMounted(() => {
startListeners();
});
onBeforeUnmount(() => {
stopListeners();
});
defineExpose({
clearLoading
});
</script>
<style lang="scss" scoped>
@import '../../libs/css/style.components.scss';
.u-model {
height: auto;
overflow: hidden;
font-size: 32rpx;
background-color: var(--u-bg-white);
&__btn--hover {
background-color: rgb(230, 230, 230);
}
&__title {
padding-top: 48rpx;
font-weight: 500;
text-align: center;
color: $u-main-color;
}
&__content {
&__message {
padding: 48rpx;
font-size: 30rpx;
text-align: center;
color: $u-content-color;
}
}
&__footer {
@include vue-flex;
&__button {
flex: 1;
height: 100rpx;
line-height: 100rpx;
font-size: 32rpx;
box-sizing: border-box;
cursor: pointer;
text-align: center;
border-radius: 4rpx;
}
}
}
</style>