diff --git a/packages/components/_private/footer/src/Footer.tsx b/packages/components/_private/footer/src/Footer.tsx index 3a944dd5f..977920f1d 100644 --- a/packages/components/_private/footer/src/Footer.tsx +++ b/packages/components/_private/footer/src/Footer.tsx @@ -73,9 +73,13 @@ export default defineComponent({ cancelButton && buttonProps.push(cancelButton) } children = buttonProps.map(item => { - const { text, ...rest } = item + const { text, disabled, ...rest } = item const _text = isFunction(text) ? text() : text - return {_text} + return ( + + {_text} + + ) }) } diff --git a/packages/components/_private/footer/src/types.ts b/packages/components/_private/footer/src/types.ts index eee9760c9..c512df6d8 100644 --- a/packages/components/_private/footer/src/types.ts +++ b/packages/components/_private/footer/src/types.ts @@ -25,6 +25,10 @@ export const footerProps = { type: Boolean, default: true, }, + disabled: { + type: Boolean, + default: undefined, + }, footer: [Boolean, Array, Object, Function] as PropType VNodeChild)>, ok: Function as PropType<(evt?: Event | unknown) => Promise | void>, okButton: Object as PropType, diff --git a/packages/components/config/src/defaultConfig.ts b/packages/components/config/src/defaultConfig.ts index 4f7f62ca5..21324c95d 100644 --- a/packages/components/config/src/defaultConfig.ts +++ b/packages/components/config/src/defaultConfig.ts @@ -236,6 +236,7 @@ export const defaultConfig: GlobalConfig = { closeOnEsc: true, mask: true, maskClosable: false, + spinWithFullModal: false, }, notification: { destroyOnHover: false, diff --git a/packages/components/config/src/types.ts b/packages/components/config/src/types.ts index 59bcbf581..4f2cc80c7 100644 --- a/packages/components/config/src/types.ts +++ b/packages/components/config/src/types.ts @@ -340,6 +340,7 @@ export interface ModalConfig { icon?: Partial VNodeChild)>> mask: boolean maskClosable: boolean + spinWithFullModal: boolean /** * @deprecated */ diff --git a/packages/components/modal/demo/Loading.md b/packages/components/modal/demo/Loading.md new file mode 100644 index 000000000..b290c8584 --- /dev/null +++ b/packages/components/modal/demo/Loading.md @@ -0,0 +1,14 @@ +--- +title: + zh: 加载中状态 + en: Loading status +order: 3 +--- + +## zh + +加载中状态。 + +## en + +Loading status. diff --git a/packages/components/modal/demo/Loading.vue b/packages/components/modal/demo/Loading.vue new file mode 100644 index 000000000..f9759b15f --- /dev/null +++ b/packages/components/modal/demo/Loading.vue @@ -0,0 +1,43 @@ + + + diff --git a/packages/components/modal/docs/Api.zh.md b/packages/components/modal/docs/Api.zh.md index a4d830a3a..5b9edb692 100644 --- a/packages/components/modal/docs/Api.zh.md +++ b/packages/components/modal/docs/Api.zh.md @@ -25,6 +25,8 @@ | `offset` | 对话框偏移量 | `number \| string` | `128` | - | 为顶部偏移量,仅在`centered=false` 时生效 | | `okButton` | 确认按钮的属性 | `ButtonProps` | - | - | - | | `okText` | 确认按钮的文本 | `string` | `确定` | - | - | +| `spin` | 弹窗是否是加载中 | `boolean \| SpinProps` | - | - | 当弹窗在加载中时,默认会禁用掉`footer`中的按钮 | +| `spinWithFullModal` | `spin` 是否覆盖整个弹窗 | `boolean` | `false` | ✅ | - | | `title` | 对话框次标题 | `string \| VNode \| (() => VNodeChild) \| #title` | - | - | 当 `type` 不为 `default` 时有效 | | `type` | 对话框类型 | `'default' \| 'confirm' \| 'info' \| 'success' \| 'warning' \| 'error'` | `default` | - | - | | `width` | 对话框宽度 | `string \| number` | - | - | `default` 类型默认宽度 `480px`, 其他类型默认宽度 `400` | @@ -55,6 +57,19 @@ export interface ModalButtonProps extends ButtonProps { | `cancel` | 手动触发当前取消按钮 | `(evt?: Event \| unknown) => Promise` | - | - | `evt` 参数将传给 `onCancel` 回调 | | `ok` | 手动触发当前确定按钮 | `(evt?: Event \| unknown) => Promise` | - | - | `evt` 参数将传给 `onOk` 回调 | +#### ModalSlots + +| 名称 | 说明 | 参数类型 | 备注 | +| --- | --- | --- | --- | +| `default` | 弹窗内容 | - | - | +| `title` | 弹窗内容标题 | - | - | +| `icon` | 弹窗图标 | - | - | +| `header` | 弹窗的header | - | - | +| `footer` | 弹窗的footer | `FooterProps` | - | +| `closeIcon` | 弹窗header的关闭图标 | - | - | +| `spinContent` | spin的内容 | - | - | +| `spinIcon` | spin的图标 | - | - | + ### IxModalProvider 如果你想通过 `useModal` 来创建对话框,则你需要把组件包裹在 `IxModalProvider` 内部,因为这样才不会丢失应用的上下文信息。 diff --git a/packages/components/modal/src/Modal.tsx b/packages/components/modal/src/Modal.tsx index 62f88e8bc..7c6d55626 100644 --- a/packages/components/modal/src/Modal.tsx +++ b/packages/components/modal/src/Modal.tsx @@ -5,6 +5,8 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ +import type { SpinProps } from '@idux/components/spin' + import { type ComputedRef, computed, @@ -18,6 +20,8 @@ import { watch, } from 'vue' +import { isBoolean } from 'lodash-es' + import { CdkPortal } from '@idux/cdk/portal' import { BlockScrollStrategy, type ScrollStrategy } from '@idux/cdk/scroll' import { callEmit, isPromise, useControlledProp } from '@idux/cdk/utils' @@ -44,6 +48,8 @@ export default defineComponent({ const config = useGlobalConfig('modal') const mergedPrefixCls = computed(() => `${common.prefixCls}-modal`) const mergedPortalTarget = usePortalTarget(props, config, common, mergedPrefixCls) + const mergedSpin = computed(() => convertSpinProps(props.spin)) + const mergedSpinWithFullModal = computed(() => props.spinWithFullModal ?? config.spinWithFullModal) const mask = computed(() => props.mask ?? config.mask) const { visible, setVisible, animatedVisible, mergedVisible } = useVisible(props) @@ -58,6 +64,8 @@ export default defineComponent({ locale, config, mergedPrefixCls, + mergedSpin, + mergedSpinWithFullModal, visible, animatedVisible, mergedVisible, @@ -177,3 +185,7 @@ function useTrigger(props: ModalProps, setVisible: (value: boolean) => void) { return { cancelLoading, okLoading, open, close, cancel, ok } } + +function convertSpinProps(spin: boolean | SpinProps | undefined) { + return isBoolean(spin) ? { spinning: spin } : spin +} diff --git a/packages/components/modal/src/ModalBody.tsx b/packages/components/modal/src/ModalBody.tsx index eddad6cc4..fcc77600a 100644 --- a/packages/components/modal/src/ModalBody.tsx +++ b/packages/components/modal/src/ModalBody.tsx @@ -12,6 +12,7 @@ import { computed, defineComponent, inject } from 'vue' import { isFunction, isString } from 'lodash-es' import { IxIcon } from '@idux/components/icon' +import { IxSpin } from '@idux/components/spin' import { modalToken } from './token' @@ -27,7 +28,7 @@ const defaultIconTypes = { export default defineComponent({ setup() { - const { props, slots, config, mergedPrefixCls } = inject(modalToken)! + const { props, slots, config, mergedPrefixCls, mergedSpin, mergedSpinWithFullModal } = inject(modalToken)! const isDefault = computed(() => props.type === 'default') const iconName = computed(() => { const { icon, type } = props @@ -38,22 +39,42 @@ export default defineComponent({ const prefixCls = `${mergedPrefixCls.value}-body` const defaultNode = slots.default?.() ?? props.__content_node + let bodyNode: VNode + if (isDefault.value) { - return
{defaultNode}
- } - const classes = `${prefixCls} ${prefixCls}-${props.type}` - const iconNode = renderIcon(prefixCls, slots.icon, iconName.value) - const titleNode = renderTitle(prefixCls, slots.title, props.title) - - return ( -
- {iconNode} -
- {titleNode} - {defaultNode} + bodyNode =
{defaultNode}
+ } else { + const classes = `${prefixCls} ${prefixCls}-${props.type}` + const iconNode = renderIcon(prefixCls, slots.icon, iconName.value) + const titleNode = renderTitle(prefixCls, slots.title, props.title) + + bodyNode = ( +
+ {iconNode} +
+ {titleNode} + {defaultNode} +
-
- ) + ) + } + + const spinProps = mergedSpin.value + + if (!mergedSpinWithFullModal.value && spinProps) { + const spinSlots = { + default: slots.spinContent, + icon: slots.spinIcon, + } + + return ( + + {bodyNode} + + ) + } + + return bodyNode } }, }) diff --git a/packages/components/modal/src/ModalWrapper.tsx b/packages/components/modal/src/ModalWrapper.tsx index c7699e60f..0f1c92d38 100644 --- a/packages/components/modal/src/ModalWrapper.tsx +++ b/packages/components/modal/src/ModalWrapper.tsx @@ -25,6 +25,7 @@ import { callEmit, convertCssPixel, getOffset } from '@idux/cdk/utils' import { ɵFooter } from '@idux/components/_private/footer' import { ɵHeader } from '@idux/components/_private/header' import { type ModalConfig } from '@idux/components/config' +import { IxSpin } from '@idux/components/spin' import { useThemeToken } from '@idux/components/theme' import ModalBody from './ModalBody' @@ -45,6 +46,8 @@ export default defineComponent({ visible, animatedVisible, mergedVisible, + mergedSpin, + mergedSpinWithFullModal, cancelLoading, okLoading, currentZIndex, @@ -130,7 +133,7 @@ export default defineComponent({ onMounted(() => watchVisibleChange(props, wrapperRef, sentinelStartRef, movableRef, mask)) - return () => { + const renderContent = () => { const prefixCls = mergedPrefixCls.value const okButton = { size: 'md', ...props.okButton } as const const cancelButton = { size: 'md', ...props.cancelButton } as const @@ -139,7 +142,15 @@ export default defineComponent({ header: slots.header, closeIcon: slots.closeIcon, } - const contentNodes = [ + + const spinSlots = { + default: slots.spinContent, + icon: slots.spinIcon, + } + + const spinProps = mergedSpin.value + + const children = [ <ɵHeader ref={headerRef} class={`${prefixCls}-header`} @@ -159,6 +170,7 @@ export default defineComponent({ cancelLoading={cancelLoading.value} cancelText={cancelText.value} cancelVisible={cancelVisible.value} + disabled={spinProps?.spinning} footer={props.footer} ok={handleOk} okButton={okButton} @@ -167,6 +179,34 @@ export default defineComponent({ >, ] + const contentNode = props.draggable ? ( + + {children} + + ) : ( +
{children}
+ ) + + if (mergedSpinWithFullModal.value && spinProps) { + return ( + + {contentNode} + + ) + } + + return contentNode + } + + return () => { + const prefixCls = mergedPrefixCls.value + return (
- {props.draggable ? ( - - {contentNodes} - - ) : ( -
{contentNodes}
- )} + {renderContent()}
diff --git a/packages/components/modal/src/token.ts b/packages/components/modal/src/token.ts index f3c836cb9..2a7f3b49a 100644 --- a/packages/components/modal/src/token.ts +++ b/packages/components/modal/src/token.ts @@ -8,6 +8,7 @@ import type { ModalBindings, ModalProps, ModalProviderRef } from './types' import type { CommonConfig, ModalConfig } from '@idux/components/config' import type { Locale } from '@idux/components/locales' +import type { SpinProps } from '@idux/components/spin' import type { ComputedRef, InjectionKey, Ref, Slots } from 'vue' export interface ModalContext { @@ -20,6 +21,8 @@ export interface ModalContext { visible: ComputedRef animatedVisible: Ref mergedVisible: ComputedRef + mergedSpin: ComputedRef + mergedSpinWithFullModal: ComputedRef cancelLoading: Ref okLoading: Ref currentZIndex: ComputedRef diff --git a/packages/components/modal/src/types.ts b/packages/components/modal/src/types.ts index 849839a12..6f93c60b7 100644 --- a/packages/components/modal/src/types.ts +++ b/packages/components/modal/src/types.ts @@ -11,6 +11,7 @@ import type { ExtractInnerPropTypes, ExtractPublicPropTypes, MaybeArray, VKey } import type { ɵFooterButtonProps } from '@idux/components/_private/footer' import type { ButtonProps } from '@idux/components/button' import type { HeaderProps } from '@idux/components/header' +import type { SpinProps } from '@idux/components/spin' import type { DefineComponent, HTMLAttributes, PropType, VNode, VNodeChild, VNodeProps } from 'vue' export type ModalType = 'default' | 'confirm' | 'info' | 'success' | 'warning' | 'error' @@ -85,6 +86,8 @@ export const modalProps = { okButton: Object as PropType, okText: String, scrollStrategy: Object as PropType, + spin: { type: [Boolean, Object] as PropType, default: undefined }, + spinWithFullModal: { type: Boolean, default: undefined }, title: [String, Object, Function] as PropType VNodeChild)>, type: { type: String as PropType,