feat: modal and drawer locking improve (#5648)

* feat: add `unlock` for modalApi

* fix: modal's close button style in locking

* fix: fix modal's close button disabled on locking

* feat: add `lock` and `unlock` for drawerApi
This commit is contained in:
Netfan 2025-03-04 22:00:32 +08:00 committed by GitHub
parent decd9c55e5
commit f380452ef0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 95 additions and 21 deletions

View File

@ -137,11 +137,19 @@ const [Drawer, drawerApi] = useVbenDrawer({
### drawerApi
| 方法 | 描述 | 类型 |
| --- | --- | --- |
| 方法 | 描述 | 类型 | 版本限制 |
| --- | --- | --- | --- |
| setState | 动态设置弹窗状态属性 | `(((prev: ModalState) => Partial<ModalState>)\| Partial<ModalState>)=>drawerApi` |
| open | 打开弹窗 | `()=>void` |
| close | 关闭弹窗 | `()=>void` |
| setData | 设置共享数据 | `<T>(data:T)=>drawerApi` |
| getData | 获取共享数据 | `<T>()=>T` |
| useStore | 获取可响应式状态 | - |
| open | 打开弹窗 | `()=>void` | --- |
| close | 关闭弹窗 | `()=>void` | --- |
| setData | 设置共享数据 | `<T>(data:T)=>drawerApi` | --- |
| getData | 获取共享数据 | `<T>()=>T` | --- |
| useStore | 获取可响应式状态 | - | --- |
| lock | 将抽屉标记为提交中,锁定当前状态 | `(isLock:boolean)=>drawerApi` | >5.5.3 |
| unlock | lock方法的反操作解除抽屉的锁定状态也是lock(false)的别名 | `()=>drawerApi` | >5.5.3 |
::: info lock
`lock`方法用于锁定抽屉的状态一般用于提交数据的过程中防止用户重复提交或者抽屉被意外关闭、表单数据被改变等等。当处于锁定状态时抽屉的确认按钮会变为loading状态同时禁用取消按钮和关闭按钮、禁止ESC或者点击遮罩等方式关闭抽屉、开启抽屉的spinner动画以遮挡弹窗内容。调用`close`方法关闭处于锁定状态的抽屉时,会自动解锁。要主动解除这种状态,可以调用`unlock`方法或者再次调用lock方法并传入false参数。
:::

View File

@ -155,9 +155,10 @@ const [Modal, modalApi] = useVbenModal({
| getData | 获取共享数据 | `<T>()=>T` | - |
| useStore | 获取可响应式状态 | - | - |
| lock | 将弹窗标记为提交中,锁定当前状态 | `(isLock:boolean)=>modalApi` | >5.5.2 |
| unlock | lock方法的反操作解除弹窗的锁定状态也是lock(false)的别名 | `()=>modalApi` | >5.5.3 |
::: info lock
`lock`方法用于锁定当前弹窗的状态一般用于提交数据的过程中防止用户重复提交或者弹窗被意外关闭、表单数据被改变等等。当处于锁定状态时弹窗的确认按钮会变为loading状态同时禁用确认按钮、隐藏关闭按钮、禁止ESC或者点击遮罩等方式关闭弹窗、开启弹窗的spinner动画以遮挡弹窗内容。调用`close`方法关闭处于锁定状态的弹窗时,会自动解锁。
`lock`方法用于锁定当前弹窗的状态一般用于提交数据的过程中防止用户重复提交或者弹窗被意外关闭、表单数据被改变等等。当处于锁定状态时弹窗的确认按钮会变为loading状态同时禁用取消按钮和关闭按钮、禁止ESC或者点击遮罩等方式关闭弹窗、开启弹窗的spinner动画以遮挡弹窗内容。调用`close`方法关闭处于锁定状态的弹窗时,会自动解锁。要主动解除这种状态,可以调用`unlock`方法或者再次调用lock方法并传入false参数。
:::

View File

@ -52,6 +52,7 @@ export class DrawerApi {
placement: 'right',
showCancelButton: true,
showConfirmButton: true,
submitting: false,
title: '',
};
@ -92,7 +93,11 @@ export class DrawerApi {
// 如果 onBeforeClose 返回 false则不关闭弹窗
const allowClose = this.api.onBeforeClose?.() ?? true;
if (allowClose) {
this.store.setState((prev) => ({ ...prev, isOpen: false }));
this.store.setState((prev) => ({
...prev,
isOpen: false,
submitting: false,
}));
}
}
@ -100,6 +105,15 @@ export class DrawerApi {
return (this.sharedData?.payload ?? {}) as T;
}
/**
*
* @description 使spinner覆盖抽屉内容loading状态
* @param isLocked
*/
lock(isLocked: boolean = true) {
return this.setState({ submitting: isLocked });
}
/**
*
*/
@ -157,4 +171,12 @@ export class DrawerApi {
}
return this;
}
/**
*
* @description lock方法设置的锁定状态lock(false)
*/
unlock() {
return this.lock(false);
}
}

View File

@ -75,12 +75,12 @@ export interface DrawerProps {
* @default false
*/
loading?: boolean;
/**
*
* @default true
*/
modal?: boolean;
/**
*
*/
@ -89,12 +89,12 @@ export interface DrawerProps {
*
*/
overlayBlur?: number;
/**
*
* @default right
*/
placement?: DrawerPlacement;
/**
*
* @default true
@ -105,6 +105,10 @@ export interface DrawerProps {
* @default true
*/
showConfirmButton?: boolean;
/**
*
*/
submitting?: boolean;
/**
*
*/

View File

@ -36,6 +36,7 @@ const props = withDefaults(defineProps<Props>(), {
appendToMain: false,
closeIconPlacement: 'right',
drawerApi: undefined,
submitting: false,
zIndex: 1000,
});
@ -73,6 +74,7 @@ const {
placement,
showCancelButton,
showConfirmButton,
submitting,
title,
titleTooltip,
zIndex,
@ -91,12 +93,12 @@ watch(
);
function interactOutside(e: Event) {
if (!closeOnClickModal.value) {
if (!closeOnClickModal.value || submitting.value) {
e.preventDefault();
}
}
function escapeKeyDown(e: KeyboardEvent) {
if (!closeOnPressEscape.value) {
if (!closeOnPressEscape.value || submitting.value) {
e.preventDefault();
}
}
@ -104,7 +106,11 @@ function escapeKeyDown(e: KeyboardEvent) {
function pointerDownOutside(e: Event) {
const target = e.target as HTMLElement;
const dismissableDrawer = target?.dataset.dismissableDrawer;
if (!closeOnClickModal.value || dismissableDrawer !== id) {
if (
submitting.value ||
!closeOnClickModal.value ||
dismissableDrawer !== id
) {
e.preventDefault();
}
}
@ -169,6 +175,7 @@ const getAppendTo = computed(() => {
<SheetClose
v-if="closable && closeIconPlacement === 'left'"
as-child
:disabled="submitting"
class="data-[state=open]:bg-secondary ml-[2px] cursor-pointer rounded-full opacity-80 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none"
>
<slot name="close-icon">
@ -209,6 +216,7 @@ const getAppendTo = computed(() => {
<SheetClose
v-if="closable && closeIconPlacement === 'right'"
as-child
:disabled="submitting"
class="data-[state=open]:bg-secondary ml-[2px] cursor-pointer rounded-full opacity-80 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none"
>
<slot name="close-icon">
@ -233,7 +241,11 @@ const getAppendTo = computed(() => {
})
"
>
<VbenLoading v-if="showLoading" class="size-full" spinning />
<VbenLoading
v-if="showLoading || submitting"
class="size-full"
spinning
/>
<slot></slot>
</div>
@ -253,6 +265,7 @@ const getAppendTo = computed(() => {
:is="components.DefaultButton || VbenButton"
v-if="showCancelButton"
variant="ghost"
:disabled="submitting"
@click="() => drawerApi?.onCancel()"
>
<slot name="cancelText">
@ -263,7 +276,7 @@ const getAppendTo = computed(() => {
<component
:is="components.PrimaryButton || VbenButton"
v-if="showConfirmButton"
:loading="confirmLoading"
:loading="confirmLoading || submitting"
@click="() => drawerApi?.onConfirm()"
>
<slot name="confirmText">

View File

@ -180,4 +180,12 @@ export class ModalApi {
}
return this;
}
/**
*
* @description lock方法设置的锁定状态lock(false)
*/
unlock() {
return this.lock(false);
}
}

View File

@ -200,12 +200,13 @@ const getAppendTo = computed(() => {
"
:modal="modal"
:open="state?.isOpen"
:show-close="submitting ? false : closable"
:show-close="closable"
:z-index="zIndex"
:overlay-blur="overlayBlur"
close-class="top-3"
@close-auto-focus="handleFocusOutside"
@closed="() => modalApi?.onClosed()"
:close-disabled="submitting"
@escape-key-down="escapeKeyDown"
@focus-outside="handleFocusOutside"
@interact-outside="interactOutside"

View File

@ -23,6 +23,7 @@ const props = withDefaults(
appendTo?: HTMLElement | string;
class?: ClassType;
closeClass?: ClassType;
closeDisabled?: boolean;
modal?: boolean;
open?: boolean;
overlayBlur?: number;
@ -30,7 +31,7 @@ const props = withDefaults(
zIndex?: number;
}
>(),
{ appendTo: 'body', showClose: true },
{ appendTo: 'body', closeDisabled: false, showClose: true },
);
const emits = defineEmits<
DialogContentEmits & { close: []; closed: []; opened: [] }
@ -108,6 +109,7 @@ defineExpose({
<DialogClose
v-if="showClose"
:disabled="closeDisabled"
:class="
cn(
'data-[state=open]:bg-accent data-[state=open]:text-muted-foreground hover:bg-accent hover:text-accent-foreground text-foreground/80 flex-center absolute right-3 top-3 h-6 w-6 rounded-full px-1 text-lg opacity-70 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none',

View File

@ -1,7 +1,7 @@
<script lang="ts" setup>
import { useVbenDrawer } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { Button, message } from 'ant-design-vue';
const [Drawer, drawerApi] = useVbenDrawer({
onCancel() {
@ -15,12 +15,19 @@ const [Drawer, drawerApi] = useVbenDrawer({
// drawerApi.close();
},
});
function lockDrawer() {
drawerApi.lock();
setTimeout(() => {
drawerApi.unlock();
}, 3000);
}
</script>
<template>
<Drawer title="基础抽屉示例" title-tooltip="标题提示内容">
<template #extra> extra </template>
base demo
<Button type="primary" @click="lockDrawer">锁定抽屉状态</Button>
<!-- <template #prepend-footer> slot </template> -->
<!-- <template #append-footer> prepend slot </template> -->
</Drawer>

View File

@ -1,7 +1,7 @@
<script lang="ts" setup>
import { useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { Button, message } from 'ant-design-vue';
const [Modal, modalApi] = useVbenModal({
onCancel() {
@ -18,9 +18,17 @@ const [Modal, modalApi] = useVbenModal({
message.info('onOpened打开动画结束');
},
});
function lockModal() {
modalApi.lock();
setTimeout(() => {
modalApi.unlock();
}, 3000);
}
</script>
<template>
<Modal class="w-[600px]" title="基础弹窗示例" title-tooltip="标题提示内容">
base demo
<Button type="primary" @click="lockModal">锁定弹窗</Button>
</Modal>
</template>