diff --git a/devui/modal/index.ts b/devui/modal/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..f324c5d93ca4aa857ce61de342fb8505b3597315 --- /dev/null +++ b/devui/modal/index.ts @@ -0,0 +1,29 @@ +import type { App } from 'vue' +import Modal from './src/modal' +import { ModalService } from './src/services/modal-service' +import { DialogService } from './src/services/dialog-service' + +Modal.install = function(app: App): void { + app.component(Modal.name, Modal) +} + +export { Modal } + +export default { + title: 'Modal 弹窗', + category: '反馈', + status: undefined, // TODO: 组件若开发完成则填入"已完成",并删除该注释 + install(app: App): void { + app.use(Modal as any) + + let anchorsContainer = document.getElementById('d-modal-anchors-container'); + if (!anchorsContainer) { + anchorsContainer = document.createElement('div'); + anchorsContainer.setAttribute('id', 'd-modal-anchors-container'); + document.body.appendChild(anchorsContainer); + } + // 新增 modalService + app.provide(ModalService.token, new ModalService(anchorsContainer)); + app.provide(DialogService.token, new DialogService(anchorsContainer)); + } +} diff --git a/devui/modal/src/dialog-types.ts b/devui/modal/src/dialog-types.ts new file mode 100644 index 0000000000000000000000000000000000000000..dbae5c5eae7c0d627d5fe978842a5f99c557ee7d --- /dev/null +++ b/devui/modal/src/dialog-types.ts @@ -0,0 +1,95 @@ +import type { PropType, ExtractPropTypes } from 'vue' +import { IButtonStyle } from '../../button/src/button' + +export interface ButtonOptions { + btnStyle: IButtonStyle + text: string + disabled: boolean + handler: ($event: Event) => void +} + +export const dialogProps = { + // id: { + // type: String, + // required: true + // }, + width: { + type: String, + default: '300px' + }, + maxHeight: { + type: String, + }, + + zIndex: { + type: Number, + default: 1050 + }, + backdropZIndex: { + type: Number, + default: 1049 + }, + + placement: { + type: String as PropType<'center' | 'top' | 'bottom'>, + default: 'center' + }, + offsetX: { + type: String, + default: '0px' + }, + + offsetY: { + type: String, + default: '0px' + }, + + title: { + type: String + }, + + showAnimation: { + type: Boolean, + default: true + }, + backdropCloseable: { + type: Boolean, + default: false + }, + bodyScrollable: { + type: Boolean, + default: true + }, + + escapeable: { + type: Boolean, + default: true + }, + + onClose: { + type: Function as PropType<() => void>, + }, + beforeHidden: { + type: [Promise, Function] as PropType | (() => boolean | Promise)> + }, + + buttons: { + type: Array as PropType, + default: [] + }, + + dialogType: { + type: String as PropType<'standard' | 'success' | 'failed' | 'warning' | 'info'>, + default: 'standard' + }, + + + modelValue: { + type: Boolean, + }, + 'onUpdate:modelValue': { + type: Function as PropType<(value: boolean) => void> + } +} as const + +export type DialogProps = ExtractPropTypes diff --git a/devui/modal/src/dialog.tsx b/devui/modal/src/dialog.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c493aeac0be6d06b1f91a3bd75f3798ec89e8102 --- /dev/null +++ b/devui/modal/src/dialog.tsx @@ -0,0 +1,135 @@ +import { defineComponent, computed, CSSProperties, watch, ref } from 'vue'; +import { DialogProps, dialogProps } from './dialog-types'; +import { useMoveable } from './use-moveable'; + +import { Button } from '../../button'; +import Modal from './modal'; + +import './modal.scss'; +import { Icon } from '../../icon'; + +export default defineComponent({ + name: 'DModal', + inheritAttrs: false, + props: dialogProps, + emits: ['onUpdate:modelValue'], + setup(props: DialogProps, ctx) { + + // 获取鼠标拖拽的偏移量 + const { + movingX, + movingY, + handleRef, + moveElRef, + reset + } = useMoveable(); + + watch(() => props.modelValue, (value) => { + if (value) { + reset(); + } + }); + + // 拖拽的样式 + const movingStyle = computed(() => ({ + position: 'relative', + left: `${movingX.value}px`, + top: `${movingY.value}px`, + })); + + // 容器的样式 + const containerStyle = computed(() => ({ + width: props.width, + maxHeight: props.maxHeight, + transform: `translate(${props.offsetX}, ${props.offsetY})`, + zIndex: props.zIndex + })); + + const iconName = computed(() => { + switch (props.dialogType) { + case 'standard': + return ''; + case 'info': + return 'icon-info-o'; + case 'success': + return 'icon-right-o'; + case 'warning': + return 'icon-warning-o'; + case 'failed': + return 'icon-error-o'; + default: + return ''; + } + }); + + // 处理按钮 + const buttonsRef = computed(() => { + return props.buttons.map((buttonProps, index) => { + const { btnStyle, disabled, handler, text } = buttonProps; + return ( + + ); + }); + }); + + const modalRef = ref<{ onVisibleChange(v: boolean): void; } | null>(); + const closeModal = () => { + modalRef.value?.onVisibleChange?.(false) + } + ctx.expose({ closeModal }); + + return () => ( + +
+
+ {!!iconName.value ? ( + + ) : null} + + {props.title} + +
+
+ {ctx.slots.default?.()} +
+ +
+
+ ); + } +}); \ No newline at end of file diff --git a/devui/modal/src/modal-types.ts b/devui/modal/src/modal-types.ts new file mode 100644 index 0000000000000000000000000000000000000000..897eae827a53431aaf08fc54113011d9fa2ed30d --- /dev/null +++ b/devui/modal/src/modal-types.ts @@ -0,0 +1,72 @@ +import type { PropType, ExtractPropTypes } from 'vue' + +export const modalProps = { + // id: { + // type: String, + // required: true + // }, + width: { + type: String, + default: '300px' + }, + maxHeight: { + type: String, + }, + + zIndex: { + type: Number, + default: 1050 + }, + backdropZIndex: { + type: Number, + default: 1049 + }, + + placement: { + type: String as PropType<'center' | 'top' | 'bottom'>, + default: 'center' + }, + offsetX: { + type: String, + default: '0px' + }, + + offsetY: { + type: String, + default: '0px' + }, + + showAnimation: { + type: Boolean, + default: true + }, + backdropCloseable: { + type: Boolean, + default: false + }, + bodyScrollable: { + type: Boolean, + default: true + }, + + escapeable: { + type: Boolean, + default: true + }, + + onClose: { + type: Function as PropType<() => void>, + }, + beforeHidden: { + type: [Object, Function] as PropType | (() => boolean| Promise)> + }, + + modelValue: { + type: Boolean, + }, + 'onUpdate:modelValue': { + type: Function as PropType<(value: boolean) => void> + } +} as const + +export type ModalProps = ExtractPropTypes diff --git a/devui/modal/src/modal.scss b/devui/modal/src/modal.scss new file mode 100644 index 0000000000000000000000000000000000000000..dda282b5b6bc46c612bab3557123aa11dd56a099 --- /dev/null +++ b/devui/modal/src/modal.scss @@ -0,0 +1,92 @@ +@import '../../style/theme/color'; +@import '../../style/theme/corner'; +@import '../../style/theme/font'; +@import '../../style/theme/animation'; + +.devui-modal-wrapper { + justify-content: center; + align-items: center; + background-color: $devui-shadow; +} + +.devui-modal-content { + background: $devui-fullscreen-overlay-bg; + border-radius: $devui-border-radius; +} + +.devui-modal-body { + padding: 20px 32px; + color: $devui-text-weak; +} + +.devui-modal-header { + padding: 32px 32px 0; + height: 56px; + position: relative; + border: none; + user-select: none; + + .btn-close { + position: absolute; + right: 20px; + top: 20px; + font-size: $devui-font-size-icon; + font-weight: 700; + line-height: 1; + color: #000000; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; + } + + .header-alert-icon { + display: inline-block; + vertical-align: middle; + margin-right: 8px; + line-height: 16px; + text-align: center; + } +} + +.devui-modal-footer { + border-top: none; + text-align: center; + padding: 0 32px 24px; +} + +.devui-modal-wipe { + @mixin wipe-in-out-animation { + animation-name: wipe-in-out; + animation-duration: 0.3s; + } + @keyframes wipe-in-out { + 0% { + opacity: 0.2; + transform: translateY(-24px); + } + + 100% { + opacity: 1; + transform: translateY(0); + } + } + + &-enter-from { + opacity: 0.2; + } + + &-enter-active { + @include wipe-in-out-animation; + } + + &-leave-to { + opacity: 1; + } + + &-leave-active { + @include wipe-in-out-animation; + + animation-direction: reverse; + } +} diff --git a/devui/modal/src/modal.tsx b/devui/modal/src/modal.tsx new file mode 100644 index 0000000000000000000000000000000000000000..82002416f281c4ccb0a3ee512614889ea20da00c --- /dev/null +++ b/devui/modal/src/modal.tsx @@ -0,0 +1,63 @@ +import { + computed, + defineComponent, + Transition, + watch +} from 'vue' +import { modalProps, ModalProps } from './modal-types' +import { FixedOverlay } from '../../overlay' +import './modal.scss'; + +export default defineComponent({ + name: 'DModal', + props: modalProps, + emits: ['onUpdate:modelValue'], + setup(props: ModalProps, ctx) { + const animatedVisible = computed(() => { + return props.showAnimation ? props.modelValue : true; + }); + + // 处理取消事件 + const onVisibleChange = (value: boolean) => { + const update = props['onUpdate:modelValue']; + if (value) { + update?.(value); + } else { + const beforeHidden = props.beforeHidden; + const close = (enabledClose: boolean) => { + if (enabledClose) { + update?.(false); + props.onClose?.(); + } + } + // true: 确认关闭 + // false: 仍然开启 + const result = (typeof beforeHidden === 'function' ? beforeHidden() : beforeHidden) ?? true; + if (result instanceof Promise) { + result.then(close); + } else { + close(result); + } + } + } + + ctx.expose({ onVisibleChange }); + + return () => ( + + + {animatedVisible.value ? ctx.slots.default?.() : null} + + + ) + } +}) + diff --git a/devui/modal/src/services/common-modal-service.ts b/devui/modal/src/services/common-modal-service.ts new file mode 100644 index 0000000000000000000000000000000000000000..befc1d05216f31c1fb816f2cceb3848b82bf0712 --- /dev/null +++ b/devui/modal/src/services/common-modal-service.ts @@ -0,0 +1,27 @@ +import { h, render, Slots, VNode } from 'vue'; + +export interface ModalOpenResult { + hide(): void +} + +export abstract class CommonModalService { + + constructor(public anchorContainer: HTMLElement) {} + + abstract component(): any; + + abstract open(options: Partial): ModalOpenResult; + + protected renderModal(anchor: HTMLElement, props: Partial, children?: Slots): VNode { + const vnode = h(this.component(), props, children); + render(vnode, anchor); + return vnode; + } + + protected renderNull(anchor: HTMLElement): void { + // 动画运行完毕后 + setTimeout(() => { + render(null, anchor); + }, 500); + } +} \ No newline at end of file diff --git a/devui/modal/src/services/dialog-service.ts b/devui/modal/src/services/dialog-service.ts new file mode 100644 index 0000000000000000000000000000000000000000..31eb9dd811775220849f46635c1b5a6a2adf268e --- /dev/null +++ b/devui/modal/src/services/dialog-service.ts @@ -0,0 +1,119 @@ +import { Slot, InjectionKey } from 'vue'; +// import {h, ref, SetupContext, defineComponent, customRef} from 'vue' +import { CommonModalService, ModalOpenResult } from './common-modal-service'; +import Dialog from '../dialog'; +import { ButtonOptions, DialogProps } from '../dialog-types'; + + +export interface DialogOptions { + width: string + maxHeight: string + zIndex: number + backdropZIndex: number + placement: 'center' | 'top' | 'bottom' + offsetX: string + offsetY: string + showAnimation: boolean + backdropCloseable: boolean + escapeable: boolean + bodyScrollable: boolean + dialogtype: 'standard' | 'success' | 'failed' | 'warning'| 'info' + + title: string + content: Slot + buttons: ButtonOptions[] + + onClose(): void + beforeHidden: (() => boolean | Promise) | Promise +} + +export class DialogService extends CommonModalService { + + static token = 'DIALOG_SERVICE_TOKEN' as unknown as InjectionKey; + + component(): any { + return Dialog; + } + + open(props: Partial = {}): ModalOpenResult & {updateButtonOptions(options: ButtonOptions[]): void;} { + // TODO:手动的方式可能抛弃了 content 内部的响应式,这里需要再优化。 + const anchor = document.createElement('div'); + this.anchorContainer.appendChild(anchor); + + const {content, ...resProps} = props; + + const needHideOrNot = (value: boolean) => { + if (!value) { + hide(); + } + } + + const renderOrigin = (props: typeof resProps, onUpdateModelValue = needHideOrNot) => { + return this.renderModal(anchor, { + ...props, + modelValue: true, + 'onUpdate:modelValue': onUpdateModelValue + }, {default: content}); + } + + + + // 隐藏按钮 + const hide = () => { + const vnode = renderOrigin(resProps, (value: boolean) => { + if (!value) { + this.renderModal(anchor, {...resProps, modelValue: false}); + this.renderNull(anchor); + } else { + renderOrigin(resProps); + } + }); + vnode.component.exposed.closeModal?.(); + } + + // 更新按钮选项 + const updateButtonOptions = (buttonOptions: ButtonOptions[]) => { + const { buttons, ...innerResProps } = resProps; + const newButtonOptions = buttons.map((option, index) => ({ + ...option, + ...buttonOptions[index] + })); + renderOrigin({...innerResProps, buttons: newButtonOptions}); + } + + // 先渲染一次,触发动画用 + this.renderModal(anchor, { modelValue: false }); + + // 再渲染详细内容 + renderOrigin(resProps); + + return { hide, updateButtonOptions } + + // TODO: 这个需要再考虑设计 + // const CurrentDialog = defineComponent((currentProps: typeof props, ctx: SetupContext) => { + // const dialogRef = ref<{closeModal(): void;} | null>(); + // const visibleRef = ref(true); + // const buttonsRef = ref(currentProps.buttons); + // ctx.expose({ + // closeModal() { + // dialogRef.value?.closeModal?.(); + // }, + // updateButtons(buttons: ButtonOptions) { + // buttonsRef.value = buttonsRef.value.map((option, index) => ({ + // ...option, + // ...buttons[index] + // })); + // } + // }); + // return () => { + // const {content, ...resProps} = currentProps; + // return h(Dialog, { + // ...resProps, + // ref: dialogRef, + // modelValue: visibleRef.value, + // 'onUpdate:modelValue': (value) => visibleRef.value = value + // }, {default: content}); + // }; + // }); + } +} \ No newline at end of file diff --git a/devui/modal/src/services/modal-service.ts b/devui/modal/src/services/modal-service.ts new file mode 100644 index 0000000000000000000000000000000000000000..6e7ffe2ff257e34784b5c370c6606025238149ae --- /dev/null +++ b/devui/modal/src/services/modal-service.ts @@ -0,0 +1,76 @@ +import { InjectionKey, Slot } from 'vue'; +import { ModalProps } from '../modal-types'; +import { CommonModalService, ModalOpenResult } from './common-modal-service'; +import Modal from '../modal'; + +export interface ModalOptions { + width: string + maxHeight: string + zIndex: number + backdropZIndex: number + placement: 'center' | 'top' | 'bottom' + offsetX: string + offsetY: string + showAnimation: boolean + backdropCloseable: boolean + escapeable: boolean + bodyScrollable: boolean + content: Slot + + onClose(): void + beforeHidden: (() => boolean | Promise) | Promise +} + + +export class ModalService extends CommonModalService { + + static token = 'MODAL_SERVICE_TOKEN' as unknown as InjectionKey; + + component(): any { + return Modal; + } + + open(props: Partial = {}): ModalOpenResult { + // TODO:手动的方式可能抛弃了 content 内部的响应式,这里需要再优化。 + const anchor = document.createElement('div'); + this.anchorContainer.appendChild(anchor); + + const { content, ...resProps } = props; + + const needHideOrNot = (value: boolean) => { + if (!value) { + hide(); + } + } + const renderOrigin = (props: typeof resProps, onUpdateModelValue = needHideOrNot) => { + return this.renderModal(anchor, { + ...props, + modelValue: true, + 'onUpdate:modelValue': onUpdateModelValue + }, { default: content }); + } + + + // 隐藏按钮 + const hide = () => { + const vnode = renderOrigin(resProps, (value: boolean) => { + if (!value) { + this.renderModal(anchor, { ...resProps, modelValue: false }); + this.renderNull(anchor); + } else { + renderOrigin(resProps); + } + }); + vnode.component.exposed.onVisibleChange?.(false); + } + + + // 先渲染一次,触发动画用 + this.renderModal(anchor, { modelValue: false }); + + // 再渲染详细内容 + renderOrigin(resProps); + + return { hide } + } +} diff --git a/devui/modal/src/use-moveable.ts b/devui/modal/src/use-moveable.ts new file mode 100644 index 0000000000000000000000000000000000000000..4f8b50b69f280ac3242cff4b09e780c649a19e0b --- /dev/null +++ b/devui/modal/src/use-moveable.ts @@ -0,0 +1,127 @@ +import { + ref, + watch, + readonly, + Ref, + isRef, +} from 'vue' + +export interface MoveableResult { + movingX: Ref + movingY: Ref + // 可拖拽的元素 + handleRef: Ref + // 可移动的元素 + moveElRef: Ref + reset(): void +} + +// 当前某个元素被拖拽时鼠标的偏移量 +export const useMoveable = (moveable: Ref | boolean = true): MoveableResult => { + // X,Y 偏移量 + const movingX = ref(0); + const movingY = ref(0); + const reset = () => { + movingX.value = 0; + movingY.value = 0; + } + + // 可拖拽的元素 + const handleRef = ref(); + // 可移动的元素 + const moveElRef = ref(); + // 是否允许拖拽 + const enabledMoving = isRef(moveable) ? moveable : ref(moveable); + + // 可视化 + watch([moveElRef, handleRef], ([container, target], ov, onInvalidate) => { + if (!(target instanceof HTMLElement && container instanceof HTMLElement)) { + return; + } + // 更改为拖动样式 + target.style.cursor = 'all-scroll'; + + // 初始化内容 + let startX = 0; + let startY = 0; + let prevMovingX = 0; + let prevMovingY = 0; + let containerRect = container.getBoundingClientRect(); + let bodyRect = document.body.getBoundingClientRect(); + let isDown = false; + + const handleMouseDown = (event: MouseEvent) => { + event.preventDefault(); + if (!enabledMoving.value) { + return; + } + startX = event.clientX; + startY = event.clientY; + // 只拿最新的 + const targetRect = target.getBoundingClientRect(); + // 判断鼠标点是否在 target 元素内 + if ( + (target === event.target || target.contains(event.target as Node)) && + targetRect.x < startX && + targetRect.y < startY && + (targetRect.width + targetRect.x) >= startX && + (targetRect.height + targetRect.y) >= startY + ) { + isDown = true; + prevMovingX = movingX.value; + prevMovingY = movingY.value; + bodyRect = document.body.getBoundingClientRect(); + containerRect = container.getBoundingClientRect(); + } + } + + const handleMouseMove = (event: MouseEvent) => { + event.preventDefault(); + if (!isDown) { + return; + } + const currentX = prevMovingX + event.clientX - startX; + const currentY = prevMovingY + event.clientY - startY; + const containerOriginX = containerRect.x - prevMovingX; + const containerOriginY = containerRect.y - prevMovingY; + movingX.value = getRangeValueOf(currentX, -containerOriginX, bodyRect.width - containerRect.width - containerOriginX); + movingY.value = getRangeValueOf(currentY, -containerOriginY, bodyRect.height - containerRect.height - containerOriginY); + } + + const handleMouseUp = (event: MouseEvent) => { + event.preventDefault(); + if (!isDown) { + return; + } + isDown = false; + } + + window.addEventListener('mousedown', handleMouseDown); + window.addEventListener('mousemove', handleMouseMove); + window.addEventListener('mouseup', handleMouseUp); + onInvalidate(() => { + window.removeEventListener('mousedown', handleMouseDown); + window.removeEventListener('mousemove', handleMouseMove); + window.removeEventListener('mouseup', handleMouseUp); + }); + }); + + return { + movingX: readonly(movingX), + movingY: readonly(movingY), + handleRef, + moveElRef, + reset + } +} + + +const getRangeValueOf = (value: number, min: number, max: number) => { + if (value < min) { + return min; + } + if (value > max) { + return max; + } + return value; +} diff --git a/docs/components/modal/index.md b/docs/components/modal/index.md new file mode 100644 index 0000000000000000000000000000000000000000..cda89377815b812d4eccb89305b601dbd56c16b4 --- /dev/null +++ b/docs/components/modal/index.md @@ -0,0 +1,395 @@ +# Modal 模态弹窗 +模态对话框。 +### 何时使用 +1.需要用户处理事务,又不希望跳转页面以致打断工作流程时,可以使用 Modal 在当前页面正中打开一个浮层,承载相应的操作。 + +2.弹窗起到与用户进行交互的作用,用户可以在对话框中输入信息、阅读提示、设置选项等操作。 + +#### 标准对话框 +使用dialogService可拖拽的标准对话框。 +:::demo +```vue + + +``` +::: + +#### 自定义对话框 +使用modalService可以自定义对话框内的所有内容。 +:::demo + +```vue + + + +``` +::: + +#### 拦截对话框关闭 +通过 beforeHidden 设置在关闭弹出框时的拦截方法。 +:::demo + +```vue + + + +``` +::: + + +#### 信息提示 +各种类型的信息提示框。 +:::demo +```vue + + + +``` +::: + +#### 更新标准弹出框按钮状态 +通过update方法来更新dialog配置的buttons配置。 +:::demo +```vue + + +``` +::: + +#### 配置按钮自动获得焦点 +配置dialogService的buttons中的autofocus属性可以设置按钮自动获得焦点,可以通过回车直接触发按钮点击。 +:::demo +```vue + + +``` +::: + +#### 通过外层fixed同时避免滚动和抖动 +通过外层fixed同时避免滚动和抖动,在使用这种方式时,页面内所有fixed元素需要给定具体的位置值,使用默认定位值会导致位置偏移。 +:::demo +```vue + + +``` +::: + + +### API +Modal 和 Dialog 均以 service 方式来构造。 + +他们通过这种方式引入: +```vue + +{ + setup() { + const modalService = inject('MODAL_SERVICE_TOKEN'); + const dialogService = inject('DIALOG_SERVICE_TOKEN'); + } +} +``` +#### Modal + +ModalService.open(props: ModalOptions) + +ModalOptions 属性 + +| 属性 | 类型 | 默认 | 说明 | +| :---------------: | :-------------------------------------------------------: | :------: | :----------------------------------------------- | +| width | `string` | -- | 可选,弹出框宽度(e.g '300px') | +| zIndex | `number` | 1050 | 可选,弹出框 z-index 值 | +| backdropZIndex | `number` | 1049 | 可选,如果为 true,背景不能滚动 | +| placement | `'center' \| 'top' \| 'bottom'` | 'center' | 可选,弹出框出现的位置 | +| offsetX | `string` | '0px' | 可选,弹出框纵向偏移 | +| offsetY | `string` | '0px' | 可选,弹出框横向偏移 | +| bodyScrollable | `boolean` | true | 可选,modal 打开后,body是否可滚动,默认可滚动。 | +| backdropCloseable | `boolean` | true | 可选,点击空白处是否能关闭弹出框 | +| showAnimation | `boolean` | true | 可选,是否显示动画 | +| escapable | `boolean` | true | 可选,点击背景触发的事件 | +| content | `Slot` | true | 可选,弹出框内容 | +| onClose | `() => void` | -- | 可选,弹出框关闭之后回调的函数 | +| beforeHidden | `(() => Promise \| boolean) \| Promise` | -- | 可选,关闭窗口之前的回调 | + +#### Dialog + +DialogService.open(props: DialogOptions) + +DialogOptions 属性 + +| 属性 | 类型 | 默认 | 说明 | +| :---------------: | :-------------------------------------------------------: | :--------: | :----------------------------------------------------- | +| width | `string` | -- | 可选,弹出框宽度(e.g '300px') | +| zIndex | `number` | 1050 | 可选,弹出框 z-index 值 | +| backdropZIndex | `number` | 1049 | 可选,如果为 true,背景不能滚动 | +| placement | `'center' \| 'top' \| 'bottom'` | 'center' | 可选,弹出框出现的位置 | +| offsetX | `string` | '0px' | 可选,弹出框纵向偏移 | +| offsetY | `string` | '0px' | 可选,弹出框横向偏移 | +| bodyScrollable | `boolean` | true | 可选,modal 打开后,body是否可滚动,默认可滚动。 | +| backdropCloseable | `boolean` | true | 可选,点击空白处是否能关闭弹出框 | +| showAnimation | `boolean` | true | 可选,是否显示动画 | +| escapable | `boolean` | true | 可选,点击背景触发的事件 | +| draggable | `boolean` | true | 可选,弹出框是否可拖拽 | +| dialogType | `'standard'\|'success'\|'failed'\|'warning'\|'info'` | 'standard' | 可选,弹出框类型,有四种选择 | +| title | `string` | -- | 可选,弹出框 title | +| content | `Slot` | -- | 可选,弹出框内容,支持字符串和组件 | +| buttons | `ButtonOptions[]` | [] | 可选,弹出框按钮,支持自定义文本、样式、禁用、点击事件 | +| onClose | `() => void` | -- | 可选,弹出框关闭之后回调的函数 | +| beforeHidden | `(() => Promise \| boolean) \| Promise` | -- | 可选,关闭窗口之前的回调 | + +### Other + +ButtonOptions 定义 +| 属性 | 类型 | 默认 | 说明 | +| :-------: | :-----------------------: | :---: | :----------------- | +| text | `string` | -- | 可选,按钮文本内容 | +| handler | `($event: Event) => void` | -- | 可选,按钮点击事件 | +| autofocus | `boolean` | false | 可选,自动聚焦 | +| disabled | `boolean` | false | 可选,禁用按钮 | +