This commit is contained in:
dap 2025-01-16 17:07:16 +08:00
commit 6716e0c979
14 changed files with 109 additions and 65 deletions

View File

@ -10,7 +10,7 @@ const routes: RouteRecordRaw[] = [
title: $t('page.dashboard.title'),
},
name: 'Dashboard',
path: '/',
path: '/dashboard',
children: [
{
name: 'Analytics',

View File

@ -10,7 +10,7 @@ const routes: RouteRecordRaw[] = [
title: $t('page.dashboard.title'),
},
name: 'Dashboard',
path: '/',
path: '/dashboard',
children: [
{
name: 'Analytics',

View File

@ -10,7 +10,7 @@ const routes: RouteRecordRaw[] = [
title: $t('page.dashboard.title'),
},
name: 'Dashboard',
path: '/',
path: '/dashboard',
children: [
{
name: 'Analytics',

View File

@ -129,7 +129,8 @@ function fetchApi(): Promise<Record<string, any>> {
| 属性名 | 描述 | 类型 | 默认值 |
| --- | --- | --- | --- |
| component | 欲包装的组件 | `Component` | - |
| modelValue(v-model) | 当前值 | `any` | - |
| component | 欲包装的组件(以下称为目标组件) | `Component` | - |
| numberToString | 是否将value从数字转为string | `boolean` | `false` |
| api | 获取数据的函数 | `(arg?: any) => Promise<OptionsItem[] \| Record<string, any>>` | - |
| params | 传递给api的参数 | `Record<string, any>` | - |
@ -137,16 +138,12 @@ function fetchApi(): Promise<Record<string, any>> {
| labelField | label字段名 | `string` | `label` |
| childrenField | 子级数据字段名,需要层级数据的组件可用 | `string` | `` |
| valueField | value字段名 | `string` | `value` |
| optionsPropName | 组件接收options数据的属性名称 | `string` | `options` |
| modelPropName | 组件的双向绑定属性名默认为modelValue。部分组件可能为value | `string` | `modelValue` |
| optionsPropName | 目标组件接收options数据的属性名称 | `string` | `options` |
| modelPropName | 目标组件的双向绑定属性名默认为modelValue。部分组件可能为value | `string` | `modelValue` |
| immediate | 是否立即调用api | `boolean` | `true` |
| alwaysLoad | 每次`visibleEvent`事件发生时都重新请求数据 | `boolean` | `false` |
| beforeFetch | 在api请求之前的回调函数 | `AnyPromiseFunction<any, any>` | - |
| afterFetch | 在api请求之后的回调函数 | `AnyPromiseFunction<any, any>` | - |
| options | 直接传入选项数据也作为api返回空数据时的后备数据 | `OptionsItem[]` | - |
| visibleEvent | 触发重新请求数据的事件名 | `string` | - |
| loadingSlot | 组件的插槽名称,用来显示一个"加载中"的图标 | `string` | - |
```
```
| loadingSlot | 目标组件的插槽名称,用来显示一个"加载中"的图标 | `string` | - |

View File

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

View File

@ -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,

View File

@ -404,9 +404,8 @@ export class FormApi {
const deletedSchema = prevSchema.filter(
(item) => !currentFields.has(item.fieldName),
);
for (const schema of deletedSchema) {
this.form?.setFieldValue(schema.fieldName, undefined);
this.form?.setFieldValue?.(schema.fieldName, undefined);
}
}
}

View File

@ -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 });
}
modalLoading(loading: boolean) {
this.store.setState((prev) => ({
...prev,

View File

@ -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>;
/**
*
*/

View File

@ -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,
'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">

View File

@ -1,12 +1,16 @@
<script lang="ts" setup>
import type { AnyPromiseFunction } from '@vben/types';
import type { Component } from 'vue';
import { LoaderCircle } from '@vben/icons';
import { get, isEqual, isFunction } from '@vben-core/shared/utils';
import { objectOmit } from '@vueuse/core';
import type { AnyPromiseFunction } from '@vben/types';
import { computed, ref, unref, useAttrs, watch } from 'vue';
import { LoaderCircle } from '@vben/icons';
import { get, isEqual, isFunction } from '@vben-core/shared/utils';
import { objectOmit } from '@vueuse/core';
type OptionsItem = {
[name: string]: any;
children?: OptionsItem[];
@ -117,7 +121,7 @@ const bindProps = computed(() => {
[`onUpdate:${props.modelPropName}`]: (val: string) => {
modelValue.value = val;
},
...objectOmit(attrs, ['onUpdate:value']),
...objectOmit(attrs, [`onUpdate:${props.modelPropName}`]),
...(props.visibleEvent
? {
[props.visibleEvent]: handleFetchForVisible,
@ -187,18 +191,16 @@ function emitChange() {
}
</script>
<template>
<div v-bind="{ ...$attrs }">
<component
:is="component"
v-bind="bindProps"
:placeholder="$attrs.placeholder"
>
<template v-for="item in Object.keys($slots)" #[item]="data">
<slot :name="item" v-bind="data || {}"></slot>
</template>
<template v-if="loadingSlot && loading" #[loadingSlot]>
<LoaderCircle class="animate-spin" />
</template>
</component>
</div>
<component
:is="component"
v-bind="bindProps"
:placeholder="$attrs.placeholder"
>
<template v-for="item in Object.keys($slots)" #[item]="data">
<slot :name="item" v-bind="data || {}"></slot>
</template>
<template v-if="loadingSlot && loading" #[loadingSlot]>
<LoaderCircle class="animate-spin" />
</template>
</component>
</template>

View File

@ -112,7 +112,3 @@ button[disabled].btn-success {
color: rgb(50 54 57 / 25%) !important;
border-color: hsl(240deg 5.9% 90%) !important;
}
.ant-message {
z-index: var(--popup-z-index);
}

View File

@ -9,10 +9,6 @@ defineOptions({
name: 'FormModelDemo',
});
function onSubmit(values: Record<string, any>) {
message.info(JSON.stringify(values)); //
}
const [Form, formApi] = useVbenForm({
handleSubmit: onSubmit,
schema: [
@ -70,6 +66,23 @@ const [Modal, modalApi] = useVbenModal({
},
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>
<template>
<Modal>

View File

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