--- title: Modal 对话框 description: 模态窗口,支持拖动、缩放、全屏与命令式确认框 --- # Modal 对话框 `Modal` 在当前页面上方打开一个模态窗口,用户必须先处理它才能继续。 ## 何时使用 - 需要用户**聚焦处理**一件事:填写表单、确认危险操作、查看详情; - 需要打断当前流程、且不该被忽略时(用 `maskClosable=false`、`keyboard=false`、`closable=false` 收紧关闭入口); - 只是提示结果、不需要用户操作时,用 [Message](/ui/feedback/message) 更轻; - 内容很长、或需要保留页面上下文时,可以考虑 [Drawer](/ui/overlay/drawer)。 ## 基础用法 `v-model:open` 控制显隐。三个事件区分关闭来源:`confirm`(底部确认按钮)、`cancel`(取消按钮 / 遮罩 / Esc / 右上角关闭)、`closed`(退场动画结束)。 :::demo{name="modal/basic"} ::: ## 拖动、缩放与全屏 默认允许拖动标题栏移动面板、拖拽四边缩放、右上角切换全屏。三者互相协调:进入全屏会复位拖拽位置并禁用缩放手柄。 :::demo{name="modal/resize"} ::: ## 居中显示 面板默认贴顶(`align-items: flex-start`)——这样长内容不会把标题栏顶出视口。确认类、短表单可以开 `centered` 垂直居中。 :::demo{name="modal/resize"} ::: ## 控制关闭入口 `maskClosable` / `keyboard` / `closable` / `footer` 分别控制遮罩点击、Esc、右上角关闭按钮与底部按钮区。 :::demo{name="modal/mask"} ::: ## 表单弹窗与异步确认 `confirmLoading` 期间**阻止一切关闭**(含遮罩、Esc、取消按钮),避免异步提交被打断;底部按钮自动进入加载态。 :::demo{name="modal/form"} ::: ## 命令式确认框 ```ts const handle = G3Modal.confirm({ title: '确定删除?', content: '删除后不可恢复', confirmText: '删除', confirmButtonProps: { danger: true }, onConfirm: async () => { await api.remove() // 返回 Promise 时按钮 loading }, onCancel: () => {}, }) handle.destroy() // 立即关闭并卸载(不等退场动画) ``` :::demo{name="modal/confirm"} ::: **`onConfirm` 的 Promise 语义**:resolve → 关闭;reject → 保持打开、解除 loading,错误由调用方提示。 **`onCancel` 的触发范围**:只有点击「取消」按钮才会调用 `onCancel`;点遮罩空白、Esc、右上角关闭按钮都是**静默关闭**(只关窗,不跑取消逻辑),避免用户随手点空白就触发取消副作用。 :::demo{name="modal/confirm-async"} ::: ## 自定义底部与头部 `#footer` 完全接管底部;`#header-extra` 放标题右侧的额外动作。 :::demo{name="modal/footer-slot"} ::: ## API ### Props(Modal) | 名称 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | `open` | `boolean` | — | 受控打开状态;不传即非受控 | | `defaultOpen` | `boolean` | `false` | 非受控时的初始状态 | | `title` | `string` | `''` | 标题 | | `width` | `number \| string` | `520` | 面板宽度。数字按 px,`'60%'` 之类的字符串原样使用 | | `centered` | `boolean` | `false` | 垂直居中(默认贴顶,长内容不会把标题顶出视口) | | `mask` | `boolean` | `true` | 是否显示遮罩 | | `maskClosable` | `boolean` | `true` | 点击遮罩空白关闭 | | `keyboard` | `boolean` | `true` | Esc 关闭 | | `closable` | `boolean` | `true` | 显示右上角关闭按钮 | | `fullscreen` | `boolean` | `true` | 显示右上角全屏切换按钮 | | `resizable` | `boolean` | `true` | 允许拖拽四边缩放宽高 | | `footer` | `boolean` | `true` | 显示底部按钮区(取消 + 确认) | | `confirmText` | `string` | 语言包 `common.confirm` | 确认按钮文字 | | `cancelText` | `string` | 语言包 `common.cancel` | 取消按钮文字 | | `confirmLoading` | `boolean` | `false` | 确认中:按钮 loading,并**阻止一切关闭操作** | | `popupContainer` | `PopupContainer` | ConfigProvider 配置 | 挂载容器,默认 `body` | ### Events(Modal) | 名称 | 参数 | 说明 | | --- | --- | --- | | `update:open` | `(open: boolean)` | 打开状态变化 | | `confirm` | `()` | 点击底部确认按钮(不自动关闭,由你决定何时关) | | `cancel` | `()` | 取消按钮 / 遮罩 / Esc / 右上角关闭被触发 | | `closed` | `()` | 退场动画结束、DOM 已销毁(此时滚动锁已释放、焦点已归还) | ### Slots(Modal) | 名称 | 参数 | 说明 | | --- | --- | --- | | `default` | — | 正文 | | `header-extra` | — | 标题右侧的自定义动作(在全屏/关闭按钮之前) | | `footer` | — | 自定义底部;不传时渲染「取消 + 确认」 | ### G3Modal.confirm 参数 | 名称 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | `title` | `string` | `''` | 标题 | | `content` | `string` | `''` | 正文纯文本 | | `confirmText` | `string` | 语言包 | 确认按钮文字 | | `cancelText` | `string` | 语言包 | 取消按钮文字 | | `confirmButtonProps` | `Partial & { danger?: boolean }` | `{}` | 透传给确认按钮;`danger: true` 等价于 `type: 'danger'` | | `onConfirm` | `() => void \| Promise` | — | 确认回调,返回 Promise 时按钮 loading | | `onCancel` | `() => void` | — | 取消回调(**仅**点击取消按钮触发) | **返回值**:`{ destroy: () => void }`。 ### 类型定义 ```ts export interface ModalProps { open?: boolean defaultOpen?: boolean title?: string width?: number | string centered?: boolean mask?: boolean maskClosable?: boolean keyboard?: boolean closable?: boolean fullscreen?: boolean resizable?: boolean footer?: boolean confirmText?: string cancelText?: string confirmLoading?: boolean popupContainer?: PopupContainer } export interface ModalConfirmOptions { title?: string content?: string confirmText?: string cancelText?: string confirmButtonProps?: Partial & { danger?: boolean } onConfirm?: () => void | Promise onCancel?: () => void } export interface ModalConfirmHandle { destroy: () => void } ``` ## 实现说明 - **遮罩点击判定**:只响应「点击事件的目标就是外层容器」且「mousedown 不在面板内」的情况。后者是为了防拖选文本:在输入框里按下鼠标、拖到遮罩上松开时,`click.target` 会回溯成共同的祖先容器,如果只看 click 就会误判成「点了遮罩要关闭」; - **焦点与滚动**:打开时记录当前焦点、锁 body 滚动、把焦点移进面板,并用 Tab 循环把焦点约束在面板内;关闭时**等退场动画结束**才解锁滚动、复位尺寸、把焦点还给触发元素——否则动画还在播的时候背景就能滚动、焦点提前跳走,屏幕阅读器会跟丢; - **滚动锁是引用计数**:Modal 里再开 Drawer、或弹窗里开下拉,不会互相提前解锁; - **命令式挂载**:`G3Modal.confirm()` 自建容器挂到 `body`,`open` 初始即为 `true`,因此内部对 `isOpen` 用了 `immediate: true` 的 watch、并给 `Transition` 加了 `appear`,让「挂载」与「受控打开」走同一段初始化逻辑,退场动画结束后再卸载容器; - **层级**:打开时从 z-index 注册表取 `modal` 档,闭包回收;浮层类(下拉、日期面板)取 `popup` 档,所以弹窗里的浮层一定盖在弹窗上。