This commit is contained in:
oneao committed 2026-10-09 17:32:14 +08:00
1 parent e99a9fb274
commit 0be0b0767a
788 files changed
+112023 -14941

No files matched your search

@@ -0,0 +1,175 @@
---
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 vue="modal/basic.vue" />
## 拖动、缩放与全屏
默认允许拖动标题栏移动面板、拖拽四边缩放、右上角切换全屏。三者互相协调:进入全屏会复位拖拽位置并禁用缩放手柄。
<demo vue="modal/resize.vue" />
## 居中显示
面板默认贴顶(`align-items: flex-start`)——这样长内容不会把标题栏顶出视口。确认类、短表单可以开 `centered` 垂直居中。
<demo vue="modal/resize.vue" />
## 控制关闭入口
`maskClosable` / `keyboard` / `closable` / `footer` 分别控制遮罩点击、Esc、右上角关闭按钮与底部按钮区。
<demo vue="modal/mask.vue" />
## 表单弹窗与异步确认
`confirmLoading` 期间**阻止一切关闭**(含遮罩、Esc、取消按钮),避免异步提交被打断;底部按钮自动进入加载态。
<demo vue="modal/form.vue" />
## 命令式确认框
```ts
const handle = G3Modal.confirm({
title: '确定删除?',
content: '删除后不可恢复',
confirmText: '删除',
confirmButtonProps: { danger: true },
onConfirm: async () => {
await api.remove() // 返回 Promise 时按钮 loading
},
onCancel: () => {},
})
handle.destroy() // 立即关闭并卸载(不等退场动画)
```
<demo vue="modal/confirm.vue" />
**`onConfirm` 的 Promise 语义**:resolve → 关闭;reject → 保持打开、解除 loading,错误由调用方提示。
**`onCancel` 的触发范围**:只有点击「取消」按钮才会调用 `onCancel`;点遮罩空白、Esc、右上角关闭按钮都是**静默关闭**(只关窗,不跑取消逻辑),避免用户随手点空白就触发取消副作用。
<demo vue="modal/confirm-async.vue" />
## 自定义底部与头部
`#footer` 完全接管底部;`#header-extra` 放标题右侧的额外动作。
<demo vue="modal/footer-slot.vue" />
## 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<ButtonProps> & { danger?: boolean }` | `{}` | 透传给确认按钮;`danger: true` 等价于 `type: 'danger'` |
| `onConfirm` | `() => void \| Promise<unknown>` | — | 确认回调,返回 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<ButtonProps> & { danger?: boolean }
onConfirm?: () => void | Promise<unknown>
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` 档,所以弹窗里的浮层一定盖在弹窗上。