feat: modal state locked on submitting (#5401)

* feat: modal state locked on submitting

* docs: 更新modal文档
This commit is contained in:
Netfan 2025-01-15 17:00:46 +08:00 committed by GitHub
parent 13087a10b7
commit 8cc903c0e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 71 additions and 25 deletions

View File

@ -113,6 +113,7 @@ const [Modal, modalApi] = useVbenModal({
| bordered | 是否显示border | `boolean` | `false` | | bordered | 是否显示border | `boolean` | `false` |
| zIndex | 弹窗的ZIndex层级 | `number` | `1000` | | zIndex | 弹窗的ZIndex层级 | `number` | `1000` |
| overlayBlur | 遮罩模糊度 | `number` | - | | overlayBlur | 遮罩模糊度 | `number` | - |
| submitting | 标记为提交中,锁定弹窗当前状态 | `boolean` | `false` |
::: info appendToMain ::: info appendToMain
@ -126,7 +127,7 @@ const [Modal, modalApi] = useVbenModal({
| 事件名 | 描述 | 类型 | 版本号 | | 事件名 | 描述 | 类型 | 版本号 |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| onBeforeClose | 关闭前触发,返回 `false`则禁止关闭 | `()=>boolean` | | | onBeforeClose | 关闭前触发,返回 `false`或者被`reject`则禁止关闭 | `()=>Promise<boolean>\|boolean` | |
| onCancel | 点击取消按钮触发 | `()=>void` | | | onCancel | 点击取消按钮触发 | `()=>void` | |
| onClosed | 关闭动画播放完毕时触发 | `()=>void` | >5.4.3 | | onClosed | 关闭动画播放完毕时触发 | `()=>void` | >5.4.3 |
| onConfirm | 点击确认按钮触发 | `()=>void` | | | onConfirm | 点击确认按钮触发 | `()=>void` | |
@ -153,3 +154,10 @@ const [Modal, modalApi] = useVbenModal({
| setData | 设置共享数据 | `<T>(data:T)=>modalApi` | | setData | 设置共享数据 | `<T>(data:T)=>modalApi` |
| getData | 获取共享数据 | `<T>()=>T` | | getData | 获取共享数据 | `<T>()=>T` |
| useStore | 获取可响应式状态 | - | | useStore | 获取可响应式状态 | - |
| lock | 将弹窗标记为提交中,锁定当前状态 | `(isLock:boolean)=>modalApi` |
::: info lock
`lock`方法用于锁定当前弹窗的状态一般用于提交数据的过程中防止用户重复提交或者弹窗被意外关闭、表单数据被改变等等。当处于锁定状态时弹窗的确认按钮会变为loading状态同时禁用确认按钮、隐藏关闭按钮、禁止ESC或者点击遮罩等方式关闭弹窗、开启弹窗的spinner动画以遮挡弹窗内容。调用`close`方法关闭处于锁定状态的弹窗时,会自动解锁。
:::

View File

@ -109,6 +109,8 @@ type MergeAll<
type EmitType = (name: Name, ...args: any[]) => void; type EmitType = (name: Name, ...args: any[]) => void;
type MaybePromise<T> = Promise<T> | T;
export type { export type {
AnyFunction, AnyFunction,
AnyNormalFunction, AnyNormalFunction,
@ -118,6 +120,7 @@ export type {
EmitType, EmitType,
IntervalHandle, IntervalHandle,
MaybeComputedRef, MaybeComputedRef,
MaybePromise,
MaybeReadonlyRef, MaybeReadonlyRef,
Merge, Merge,
MergeAll, MergeAll,

View File

@ -95,13 +95,18 @@ export class ModalApi {
/** /**
* *
* @description onBeforeClose onBeforeClose false
*/ */
close() { async close() {
// 通过 onBeforeClose 钩子函数来判断是否允许关闭弹窗 // 通过 onBeforeClose 钩子函数来判断是否允许关闭弹窗
// 如果 onBeforeClose 返回 false则不关闭弹窗 // 如果 onBeforeClose 返回 false则不关闭弹窗
const allowClose = this.api.onBeforeClose?.() ?? true; const allowClose = (await this.api.onBeforeClose?.()) ?? true;
if (allowClose) { if (allowClose) {
this.store.setState((prev) => ({ ...prev, isOpen: false })); this.store.setState((prev) => ({
...prev,
isOpen: false,
submitting: false,
}));
} }
} }
@ -109,6 +114,15 @@ export class ModalApi {
return (this.sharedData?.payload ?? {}) as T; return (this.sharedData?.payload ?? {}) as T;
} }
/**
*
* @description 使spinner覆盖弹窗内容loading状态
* @param isLocked
*/
lock(isLocked = true) {
return this.setState({ submitting: isLocked });
}
/** /**
* *
*/ */

View File

@ -1,5 +1,7 @@
import type { Component, Ref } from 'vue'; import type { Component, Ref } from 'vue';
import type { MaybePromise } from '@vben-core/typings';
import type { ModalApi } from './modal-api'; import type { ModalApi } from './modal-api';
export interface ModalProps { export interface ModalProps {
@ -113,6 +115,10 @@ export interface ModalProps {
* @default true * @default true
*/ */
showConfirmButton?: boolean; showConfirmButton?: boolean;
/**
*
*/
submitting?: boolean;
/** /**
* *
*/ */
@ -155,7 +161,7 @@ export interface ModalApiOptions extends ModalState {
* false * false
* @returns * @returns
*/ */
onBeforeClose?: () => void; onBeforeClose?: () => MaybePromise<boolean | undefined>;
/** /**
* *
*/ */

View File

@ -80,6 +80,7 @@ const {
overlayBlur, overlayBlur,
showCancelButton, showCancelButton,
showConfirmButton, showConfirmButton,
submitting,
title, title,
titleTooltip, titleTooltip,
zIndex, zIndex,
@ -115,9 +116,9 @@ watch(
); );
watch( watch(
() => showLoading.value, () => [showLoading.value, submitting.value],
(v) => { ([l, s]) => {
if (v && wrapperRef.value) { if ((s || l) && wrapperRef.value) {
wrapperRef.value.scrollTo({ wrapperRef.value.scrollTo({
// behavior: 'smooth', // behavior: 'smooth',
top: 0, top: 0,
@ -135,13 +136,13 @@ function handleFullscreen() {
}); });
} }
function interactOutside(e: Event) { function interactOutside(e: Event) {
if (!closeOnClickModal.value) { if (!closeOnClickModal.value || submitting.value) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
} }
} }
function escapeKeyDown(e: KeyboardEvent) { function escapeKeyDown(e: KeyboardEvent) {
if (!closeOnPressEscape.value) { if (!closeOnPressEscape.value || submitting.value) {
e.preventDefault(); e.preventDefault();
} }
} }
@ -156,7 +157,11 @@ function handerOpenAutoFocus(e: Event) {
function pointerDownOutside(e: Event) { function pointerDownOutside(e: Event) {
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
const isDismissableModal = target?.dataset.dismissableModal; const isDismissableModal = target?.dataset.dismissableModal;
if (!closeOnClickModal.value || isDismissableModal !== id) { if (
!closeOnClickModal.value ||
isDismissableModal !== id ||
submitting.value
) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
} }
@ -174,7 +179,7 @@ const getAppendTo = computed(() => {
<Dialog <Dialog
:modal="false" :modal="false"
:open="state?.isOpen" :open="state?.isOpen"
@update:open="() => modalApi?.close()" @update:open="() => (!submitting ? modalApi?.close() : undefined)"
> >
<DialogContent <DialogContent
ref="contentRef" ref="contentRef"
@ -195,7 +200,7 @@ const getAppendTo = computed(() => {
" "
:modal="modal" :modal="modal"
:open="state?.isOpen" :open="state?.isOpen"
:show-close="closable" :show-close="submitting ? false : closable"
:z-index="zIndex" :z-index="zIndex"
:overlay-blur="overlayBlur" :overlay-blur="overlayBlur"
close-class="top-3" close-class="top-3"
@ -247,12 +252,12 @@ const getAppendTo = computed(() => {
ref="wrapperRef" ref="wrapperRef"
:class=" :class="
cn('relative min-h-40 flex-1 overflow-y-auto p-3', contentClass, { cn('relative min-h-40 flex-1 overflow-y-auto p-3', contentClass, {
'pointer-events-none overflow-hidden': showLoading, 'pointer-events-none overflow-hidden': showLoading || submitting,
}) })
" "
> >
<VbenLoading <VbenLoading
v-if="showLoading" v-if="showLoading || submitting"
class="size-full h-auto min-h-full" class="size-full h-auto min-h-full"
spinning spinning
/> />
@ -287,6 +292,7 @@ const getAppendTo = computed(() => {
:is="components.DefaultButton || VbenButton" :is="components.DefaultButton || VbenButton"
v-if="showCancelButton" v-if="showCancelButton"
variant="ghost" variant="ghost"
:disabled="submitting"
@click="() => modalApi?.onCancel()" @click="() => modalApi?.onCancel()"
> >
<slot name="cancelText"> <slot name="cancelText">
@ -298,7 +304,7 @@ const getAppendTo = computed(() => {
:is="components.PrimaryButton || VbenButton" :is="components.PrimaryButton || VbenButton"
v-if="showConfirmButton" v-if="showConfirmButton"
:disabled="confirmDisabled" :disabled="confirmDisabled"
:loading="confirmLoading" :loading="confirmLoading || submitting"
@click="() => modalApi?.onConfirm()" @click="() => modalApi?.onConfirm()"
> >
<slot name="confirmText"> <slot name="confirmText">

View File

@ -54,7 +54,3 @@
.ant-app .form-valid-error .ant-picker-focused { .ant-app .form-valid-error .ant-picker-focused {
box-shadow: 0 0 0 2px rgb(255 38 5 / 6%); box-shadow: 0 0 0 2px rgb(255 38 5 / 6%);
} }
.ant-message {
z-index: var(--popup-z-index);
}

View File

@ -9,10 +9,6 @@ defineOptions({
name: 'FormModelDemo', name: 'FormModelDemo',
}); });
function onSubmit(values: Record<string, any>) {
message.info(JSON.stringify(values)); //
}
const [Form, formApi] = useVbenForm({ const [Form, formApi] = useVbenForm({
handleSubmit: onSubmit, handleSubmit: onSubmit,
schema: [ schema: [
@ -70,6 +66,23 @@ const [Modal, modalApi] = useVbenModal({
}, },
title: '内嵌表单示例', title: '内嵌表单示例',
}); });
function onSubmit(values: Record<string, any>) {
message.loading({
content: '正在提交中...',
duration: 0,
key: 'is-form-submitting',
});
modalApi.lock();
setTimeout(() => {
modalApi.close();
message.success({
content: `提交成功:${JSON.stringify(values)}`,
duration: 2,
key: 'is-form-submitting',
});
}, 3000);
}
</script> </script>
<template> <template>
<Modal> <Modal>

View File

@ -97,7 +97,7 @@ function openFormModal() {
formModalApi formModalApi
.setData({ .setData({
// //
values: { field1: 'abc' }, values: { field1: 'abc', field2: '123' },
}) })
.open(); .open();
} }