mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-08-26 16:46:19 +08:00
feat: modal state locked on submitting (#5401)
* feat: modal state locked on submitting * docs: 更新modal文档
This commit is contained in:
3
packages/@core/base/typings/src/helper.d.ts
vendored
3
packages/@core/base/typings/src/helper.d.ts
vendored
@@ -109,6 +109,8 @@ type MergeAll<
|
||||
|
||||
type EmitType = (name: Name, ...args: any[]) => void;
|
||||
|
||||
type MaybePromise<T> = Promise<T> | T;
|
||||
|
||||
export type {
|
||||
AnyFunction,
|
||||
AnyNormalFunction,
|
||||
@@ -118,6 +120,7 @@ export type {
|
||||
EmitType,
|
||||
IntervalHandle,
|
||||
MaybeComputedRef,
|
||||
MaybePromise,
|
||||
MaybeReadonlyRef,
|
||||
Merge,
|
||||
MergeAll,
|
||||
|
@@ -95,13 +95,18 @@ export class ModalApi {
|
||||
|
||||
/**
|
||||
* 关闭弹窗
|
||||
* @description 关闭弹窗时会调用 onBeforeClose 钩子函数,如果 onBeforeClose 返回 false,则不关闭弹窗
|
||||
*/
|
||||
close() {
|
||||
async close() {
|
||||
// 通过 onBeforeClose 钩子函数来判断是否允许关闭弹窗
|
||||
// 如果 onBeforeClose 返回 false,则不关闭弹窗
|
||||
const allowClose = this.api.onBeforeClose?.() ?? true;
|
||||
const allowClose = (await this.api.onBeforeClose?.()) ?? true;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 锁定弹窗状态(用于提交过程中的等待状态)
|
||||
* @description 锁定状态将禁用默认的取消按钮,使用spinner覆盖弹窗内容,隐藏关闭按钮,阻止手动关闭弹窗,将默认的提交按钮标记为loading状态
|
||||
* @param isLocked 是否锁定
|
||||
*/
|
||||
lock(isLocked = true) {
|
||||
return this.setState({ submitting: isLocked });
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消操作
|
||||
*/
|
||||
|
@@ -1,5 +1,7 @@
|
||||
import type { Component, Ref } from 'vue';
|
||||
|
||||
import type { MaybePromise } from '@vben-core/typings';
|
||||
|
||||
import type { ModalApi } from './modal-api';
|
||||
|
||||
export interface ModalProps {
|
||||
@@ -113,6 +115,10 @@ export interface ModalProps {
|
||||
* @default true
|
||||
*/
|
||||
showConfirmButton?: boolean;
|
||||
/**
|
||||
* 提交中(锁定弹窗状态)
|
||||
*/
|
||||
submitting?: boolean;
|
||||
/**
|
||||
* 弹窗标题
|
||||
*/
|
||||
@@ -155,7 +161,7 @@ export interface ModalApiOptions extends ModalState {
|
||||
* 关闭前的回调,返回 false 可以阻止关闭
|
||||
* @returns
|
||||
*/
|
||||
onBeforeClose?: () => void;
|
||||
onBeforeClose?: () => MaybePromise<boolean | undefined>;
|
||||
/**
|
||||
* 点击取消按钮的回调
|
||||
*/
|
||||
|
@@ -80,6 +80,7 @@ const {
|
||||
overlayBlur,
|
||||
showCancelButton,
|
||||
showConfirmButton,
|
||||
submitting,
|
||||
title,
|
||||
titleTooltip,
|
||||
zIndex,
|
||||
@@ -115,9 +116,9 @@ watch(
|
||||
);
|
||||
|
||||
watch(
|
||||
() => showLoading.value,
|
||||
(v) => {
|
||||
if (v && wrapperRef.value) {
|
||||
() => [showLoading.value, submitting.value],
|
||||
([l, s]) => {
|
||||
if ((s || l) && wrapperRef.value) {
|
||||
wrapperRef.value.scrollTo({
|
||||
// behavior: 'smooth',
|
||||
top: 0,
|
||||
@@ -135,13 +136,13 @@ function handleFullscreen() {
|
||||
});
|
||||
}
|
||||
function interactOutside(e: Event) {
|
||||
if (!closeOnClickModal.value) {
|
||||
if (!closeOnClickModal.value || submitting.value) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
function escapeKeyDown(e: KeyboardEvent) {
|
||||
if (!closeOnPressEscape.value) {
|
||||
if (!closeOnPressEscape.value || submitting.value) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
@@ -156,7 +157,11 @@ function handerOpenAutoFocus(e: Event) {
|
||||
function pointerDownOutside(e: Event) {
|
||||
const target = e.target as HTMLElement;
|
||||
const isDismissableModal = target?.dataset.dismissableModal;
|
||||
if (!closeOnClickModal.value || isDismissableModal !== id) {
|
||||
if (
|
||||
!closeOnClickModal.value ||
|
||||
isDismissableModal !== id ||
|
||||
submitting.value
|
||||
) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
@@ -174,7 +179,7 @@ const getAppendTo = computed(() => {
|
||||
<Dialog
|
||||
:modal="false"
|
||||
:open="state?.isOpen"
|
||||
@update:open="() => modalApi?.close()"
|
||||
@update:open="() => (!submitting ? modalApi?.close() : undefined)"
|
||||
>
|
||||
<DialogContent
|
||||
ref="contentRef"
|
||||
@@ -195,7 +200,7 @@ const getAppendTo = computed(() => {
|
||||
"
|
||||
:modal="modal"
|
||||
:open="state?.isOpen"
|
||||
:show-close="closable"
|
||||
:show-close="submitting ? false : closable"
|
||||
:z-index="zIndex"
|
||||
:overlay-blur="overlayBlur"
|
||||
close-class="top-3"
|
||||
@@ -247,12 +252,12 @@ const getAppendTo = computed(() => {
|
||||
ref="wrapperRef"
|
||||
:class="
|
||||
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
|
||||
v-if="showLoading"
|
||||
v-if="showLoading || submitting"
|
||||
class="size-full h-auto min-h-full"
|
||||
spinning
|
||||
/>
|
||||
@@ -287,6 +292,7 @@ const getAppendTo = computed(() => {
|
||||
:is="components.DefaultButton || VbenButton"
|
||||
v-if="showCancelButton"
|
||||
variant="ghost"
|
||||
:disabled="submitting"
|
||||
@click="() => modalApi?.onCancel()"
|
||||
>
|
||||
<slot name="cancelText">
|
||||
@@ -298,7 +304,7 @@ const getAppendTo = computed(() => {
|
||||
:is="components.PrimaryButton || VbenButton"
|
||||
v-if="showConfirmButton"
|
||||
:disabled="confirmDisabled"
|
||||
:loading="confirmLoading"
|
||||
:loading="confirmLoading || submitting"
|
||||
@click="() => modalApi?.onConfirm()"
|
||||
>
|
||||
<slot name="confirmText">
|
||||
|
@@ -54,7 +54,3 @@
|
||||
.ant-app .form-valid-error .ant-picker-focused {
|
||||
box-shadow: 0 0 0 2px rgb(255 38 5 / 6%);
|
||||
}
|
||||
|
||||
.ant-message {
|
||||
z-index: var(--popup-z-index);
|
||||
}
|
||||
|
Reference in New Issue
Block a user