naive-ui Dialog 对话框:函数式 API 与组件式用法的完整指南
【免费下载链接】naive-uiA Vue 3 Component Library. Fairly Complete. Theme Customizable. Uses TypeScript. Fast.项目地址: https://gitcode.com/gh_mirrors/na/naive-ui
naive-ui的 Dialog 对话框组件同时提供了「函数式调用」(useDialog注入的dialog.warning(options)等形式)与「组件式使用」(<n-dialog>)两条使用路径,并允许通过n-dialog-provider集中管理所有对话框实例。本文以官方中文文档(src/dialog/demos/zhCN/index.demo-entry.md)为主线,结合仓库源码与全部演示示例,完整讲解使用前提、useDialog/useDialogReactiveListAPI、DialogOptions/DialogReactive全部配置项、组件 Props 与 Slots,以及底层实现原理,帮助你从“能跑通 Demo”进阶到“理解并驾驭对话框的每一个细节”。
阅读本文前,建议先浏览官方演示目录 src/dialog/demos/zhCN,其中包含 8 个可直接运行的
.demo.vue示例。
使用前提:把组件放进n-dialog-provider内
官方文档在开篇就用警示框强调了使用函数式 Dialog 的前提:
如果你想使用对话框,你需要把调用其方法的组件放在
n-dialog-provider内部,并且使用useDialog去获取 API。
也就是说,函数式 Dialog 依赖**依赖注入(provide / inject)**机制工作,n-dialog-provider负责在组件树顶层注册 API,任何后代组件都可以通过useDialog()获取到同一个 API 对象。典型结构如下:
<!-- App.vue --> <n-dialog-provider> <content /> </n-dialog-provider>import { useDialog } from 'naive-ui' import { defineComponent } from 'vue' // content export default defineComponent({ setup() { const dialog = useDialog() return { warning() { dialog.warning(options) } } } })从源码看,useDialog的实现非常直接(src/dialog/src/composables.ts):
export function useDialog(): DialogApiInjection { const dialog = inject(dialogApiInjectionKey, null) if (dialog === null) { throwError('use-dialog', 'No outer <n-dialog-provider /> founded.') } return dialog }可以看到:
- 通过
inject(dialogApiInjectionKey, null)从组件树上取 API; - 如果取不到(即没有外层
n-dialog-provider),会直接抛出错误No outer <n-dialog-provider /> founded.。
因此,凡是使用useDialog的组件,都必须保证其上方存在n-dialog-provider,否则运行时会报错而非静默失败。这与useMessage、useNotification等组件的使用方式一致。
另外,n-dialog-provider本身也接收两个可选 Props(见 DialogProvider.ts):
| 名称 | 类型 | 说明 |
|---|---|---|
injectionKey | String | 自定义注入的 key,用于在特殊场景下隔离/区分多个 provider |
to | String \| HTMLElement | 对话框挂载的目标位置,默认跟随 teleport 逻辑 |
useDialog API:五种入口方法
useDialog()返回的DialogApiInjection对象共暴露 5 个方法:
| 名称 | 类型 | 说明 |
|---|---|---|
destroyAll | () => void | 销毁所有弹出的对话框 |
create | (options: DialogOptions) => DialogReactive | 创建对话框(不预设类型) |
error | (options: DialogOptions) => DialogReactive | 调用error类型的对话框 |
info | (options: DialogOptions) => DialogReactive | 调用info类型的对话框 |
success | (options: DialogOptions) => DialogReactive | 调用success类型的对话框 |
warning | (options: DialogOptions) => DialogReactive | 调用warning类型的对话框 |
在源码实现中(DialogProvider.ts),info/success/warning/error四个类型化方法其实都是create的包装:
const typedApi = ( ['info', 'success', 'warning', 'error'] as Array< 'info' | 'success' | 'warning' | 'error' > ).map(type => (options: DialogOptions): DialogReactive => { return create({ ...options, type }) })也就是说,dialog.warning({ title: '...' })等价于dialog.create({ type: 'warning', title: '...' }),只是语法糖更直观。create本身会:
- 用
createId()生成唯一key; - 把传入的
options与key、destroy一起包装成reactive对象(这正是后面“属性可动态修改”的基础); - 推入
dialogListRef,交由 provider 渲染(DialogProvider.ts)。
DialogOptions:函数式调用时的完整配置
调用dialog.create(options)时传入的options即DialogOptions。下表为官方文档列出的全部属性(含默认值与引入版本):
| 名称 | 类型 | 默认值 | 说明 | 版本 |
|---|---|---|---|---|
action | () => VNodeChild | undefined | 操作区域的内容,需要是渲染函数 | |
actionClass | string | undefined | 操作区域的类名 | 2.38.2 |
actionStyle | Object \| string | undefined | 操作区域的样式 | 2.38.2 |
autoFocus | boolean | true | 是否自动聚焦 Modal 第一个可聚焦的元素 | 2.28.3 |
blockScroll | boolean | true | 是否在打开时禁用 body 滚动 | 2.28.3 |
bordered | boolean | false | 是否显示border | |
class | any | undefined | 类名 | 2.33.0 |
closable | boolean | true | 是否显示close图标 | |
closeFocusable | boolean | false | 关闭按钮是否可以聚焦 | 2.43.0 |
closeOnEsc | boolean | true | 是否在摁下 Esc 键的时候关闭对话框 | 2.26.4 |
content | string \| (() => VNodeChild) | undefined | 对话框内容,可以是渲染函数 | |
contentClass | string | undefined | 内容的类名 | 2.38.2 |
contentStyle | Object \| string | undefined | 内容的样式 | 2.38.2 |
draggable | boolean \| { bounds?: 'none' } | false | 是否可拖拽 | 2.41.0 |
iconPlacement | 'left' \| 'top' | 'left' | 图标的位置 | |
icon | () => VNodeChild | undefined | 对话框icon,需要是渲染函数 | |
loading | boolean | false | 是否显示loading状态 | |
maskClosable | boolean | true | 是否可以通过点击mask关闭对话框 | |
negativeButtonProps | ButtonProps | undefined | 取消按钮的属性 | 2.27.0 |
negativeText | string | undefined | 取消按钮的文字,不填对应的按钮不会出现 | |
positiveButtonProps | ButtonProps | undefined | 确认按钮的属性 | 2.27.0 |
positiveText | string | undefined | 确认按钮的文字,不填对应的按钮不会出现 | |
showIcon | boolean | true | 是否显示icon | |
style | string \| Object | undefined | 样式 | |
title | string \| (() => VNodeChild) | undefined | 标题,可以是渲染函数 | |
titleClass | string | undefined | 标题的类名 | 2.38.2 |
titleStyle | Object \| string | undefined | 标题的样式 | 2.38.2 |
transformOrigin | 'mouse' \| 'center' | 'mouse' | 对话框动画出现的位置 | 2.34.0 |
type | 'error' \| 'success' \| 'warning' | 'warning' | 对话框类型 | |
zIndex | number | undefined | Dialog 的 z-index | 2.43.0 |
onAfterEnter | () => void | undefined | 出现动画完成执行的回调 | 2.33.0 |
onAfterLeave | () => void | undefined | 关闭动画完成执行的回调 | 2.33.3 |
onClose | () => boolean \| Promise<boolean> \| any | undefined | 默认行为是关闭确认框。返回false或者resolve false或者Promise被reject会避免默认行为 | |
onNegativeClick | (e: MouseEvent) => boolean \| Promise<boolean> \| any | undefined | 默认行为是关闭确认框。返回false或者resolve false或者Promise被reject会避免默认行为 | |
onPositiveClick | (e: MouseEvent) => boolean \| Promise<boolean> \| any | undefined | 默认行为是关闭确认框。返回false或者resolve false或者Promise被reject会避免默认行为 | |
onMaskClick | () => void | undefined | 点击蒙层后执行的回调 |
几个容易被忽视的细节
positiveText/negativeText决定按钮是否出现:不填对应文字,则对应按钮不会渲染。这是函数式 Dialog 最常用的“二选一/不显示按钮”控制手段。onPositiveClick等回调的“拦截”语义:默认行为是关闭对话框;只要回调返回false、resolve(false)或 Promise 被reject,就会阻止默认的关闭行为。这一点在“异步确认”场景中非常有用(详见下文异步示例)。transformOrigin: 'mouse':对话框的出现动画会以鼠标点击位置为原点展开,这是 naive-ui 对话框的默认行为,交互上更“跟手”;可改为'center'让动画从屏幕中心展开。其底层通过useClicked与useClickPosition(来自vooks)记录点击位置,见 DialogProvider.ts。draggable: { bounds?: 'none' }:自 2.41.0 起支持拖拽,可通过bounds: 'none'允许拖出可视区域边界。- 渲染函数型属性:
title、content、icon、action都支持传渲染函数(() => VNodeChild),可以实现任意复杂的自定义内容。
DialogReactive:动态修改与主动销毁
dialog.xxx(options)的返回值是DialogReactive,它由options加两个只读字段组成(DialogProvider.ts):
export interface DialogReactive extends DialogOptions { readonly key: string readonly destroy: () => void }DialogReactive Properties
官方文档明确说明:下列属性都可以被动态修改(修改后对话框会实时响应)。
| 名称 | 类型 | 说明 | 版本 |
|---|---|---|---|
actionClass | string | 操作区域的类名 | 2.38.2 |
actionStyle | Object \| string | 操作区域的样式 | 2.38.2 |
bordered | boolean | 是否显示border | |
class | any | 类名 | 2.33.0 |
closable | boolean | 是否显示close图标 | |
closeFocusable | boolean | 关闭按钮是否可以聚焦 | 2.43.0 |
closeOnEsc | boolean | 是否在摁下 Esc 键的时候关闭对话框 | 2.26.4 |
content | string \| (() => VNodeChild) | 对话框内容,可以是渲染函数 | |
contentClass | string | 内容的类名 | 2.38.2 |
contentStyle | Object \| string | 内容的样式 | 2.38.2 |
iconPlacement | 'left' \| 'top' | 图标的位置 | |
icon | () => VNodeChild | 对话框icon,需要是渲染函数 | |
loading | boolean | 是否显示loading状态 | |
maskClosable | boolean | 是否可以通过点击mask关闭对话框 | |
negativeButtonProps | ButtonProps | 取消按钮的属性 | 2.27.0 |
negativeText | string | 取消按钮的文字,不填对应的按钮不会出现 | |
positiveButtonProps | ButtonProps | 确认按钮的属性 | 2.27.0 |
positiveText | string | 确认按钮的文字,不填对应的按钮不会出现 | |
showIcon | boolean | 是否显示icon | |
style | string \| Object | 样式 | |
title | string \| (() => VNodeChild) | 可以是渲染函数 | |
titleClass | string | 标题的类名 | 2.38.2 |
titleStyle | Object \| string | 标题的样式 | 2.38.2 |
transformOrigin | 'mouse' \| 'center' | 对话框动画出现的位置 | 2.34.0 |
type | 'error' \| 'success' \| 'warning' | 对话框类型 | |
onAfterEnter | () => void \| undefined | 出现动画完成执行的回调 | 2.33.0 |
onAfterLeave | () => void \| undefined | 关闭动画完成执行的回调 | 2.33.3 |
onClose | () => boolean \| Promise<boolean> \| any | 默认行为是关闭确认框。返回false或者resolve false或者Promise被reject会避免默认行为 | |
onEsc | () => void | 焦点在 dialog 内部时按下 Esc 键的回调 | 2.32.0 |
onNegativeClick | (e: MouseEvent) => boolean \| Promise<boolean> \| any | 默认行为是关闭确认框。返回false或者resolve false或者Promise被reject会避免默认行为 | |
onPositiveClick | (e: MouseEvent) => boolean \| Promise<boolean> \| any | 默认行为是关闭确认框。返回false或者resolve false或者Promise被reject会避免默认行为 |
相比
DialogOptions,DialogReactive额外暴露了onEsc回调(2.32.0),用于监听焦点在对话框内部时按下 Esc 键的事件;onAfterEnter/onAfterLeave的类型标注为() => void | undefined。
DialogReactive Methods
| 名称 | 类型 | 说明 |
|---|---|---|
destroy | () | 关闭Dialog |
在源码中,destroy被实现为调用对应对话框实例的hide()方法(DialogProvider.ts):
destroy: () => { dialogInstRefs[`n-dialog-${key}`]?.hide() }而destroyAll()则遍历所有实例统一调用hide()(DialogProvider.ts)。对话框关闭动画结束后,handleAfterLeave会把对应实例从dialogListRef中移除(DialogProvider.ts)。
组件式用法:n-dialog Props 与 Slots
除了函数式调用,<n-dialog>也可以像普通组件一样直接写在模板中(官方演示 use-component.demo.vue 即为此用法):
<template> <n-dialog title="确认" content="你确定" negative-text="不确认" positive-text="确认" @positive-click="handlePositiveClick" @negative-click="handleNegativeClick" /> </template>Dialog Props
| 名称 | 类型 | 默认值 | 说明 | 版本 |
|---|---|---|---|---|
action-class | string | undefined | 操作区域的类名 | 2.38.2 |
action-style | Object \| string | undefined | 操作区域的样式 | 2.38.2 |
bordered | boolean | false | 是否显示border | |
closable | boolean | true | 是否显示close图标 | |
close-focusable | boolean | false | 关闭按钮是否可以聚焦 | 2.43.0 |
content | string \| (() => VNodeChild) | undefined | 对话框内容,可以是渲染函数 | |
content-class | string | undefined | 内容的类名 | 2.38.2 |
content-style | Object \| string | undefined | 内容的样式 | 2.38.2 |
icon-placement | 'left' \| 'top' | 'left' | 图标放置的位置 | |
icon | () => VNodeChild | undefined | 需要是渲染函数 | |
loading | boolean | false | 是否显示loading状态 | |
negative-button-props | ButtonProps | undefined | 取消按钮的属性 | 2.27.0 |
negative-text | string | undefined | 取消按钮的文字,不填对应的按钮不会出现 | |
positive-button-props | ButtonProps | undefined | 确认按钮的属性 | 2.27.0 |
positive-text | string | undefined | 确认按钮的文字,不填对应的按钮不会出现 | |
show-icon | boolean | true | 是否显示icon | |
title | string \| (() => VNodeChild) | undefined | 对话框标题,可以是渲染函数 | |
title-class | string | undefined | 标题的类名 | 2.38.2 |
title-style | Object \| string | undefined | 标题的样式 | 2.38.2 |
type | 'error' \| 'success' \| 'warning' \| 'info' | 'warning' | 对话框类型 | |
on-close | () => void | undefined | 点击关闭时执行的回调函数 | |
on-negative-click | (e: MouseEvent) => void | undefined | 执行negative时执行的回调函数 | |
on-positive-click | (e: MouseEvent) => void | undefined | 执行positive时执行的回调函数 |
注意两点差异:
- 组件 Props 采用kebab-case命名(
action-class、positive-text等); - 组件模式下
type支持四种值(比DialogOptions多一个'info'),且事件回调类型为() => void(不参与关闭拦截逻辑,拦截能力仅存在于函数式 API 中)。
Dialog Slots
| 名称 | 参数 | 说明 | 版本 |
|---|---|---|---|
action | () | action内容 | |
default | () | 对话框内容 | |
header | () | header内容 | |
icon | () | icon内容 | |
close | () | close内容 | 2.36.0 |
官方演示逐例解读
官方中文文档在“演示”一节中按顺序引入了 8 个示例(src/dialog/demos/zhCN),下面逐一解读其核心要点。
1. 基础用法(basic.demo.vue)
通过dialog.warning / success / error分别触发三种类型的对话框,演示了title、content、positiveText、negativeText、draggable及两个点击回调的常规组合:
dialog.warning({ title: '警告', content: '你确定?', positiveText: '确定', negativeText: '不确定', draggable: true, onPositiveClick: () => message.success('确定'), onNegativeClick: () => message.error('不确定') })这是日常“删除确认”“操作确认”弹窗的最典型写法:类型(图标与配色)+ 标题 + 内容 + 确认/取消文案 + 回调。
2. 异步(async.demo.vue)
演示了onPositiveClick返回 Promise 时对话框会保持打开并进入 loading 状态,直到 Promise resolve 才关闭。示例中点击确认后依次展示“倒计时 3 秒 → 2 秒 → 1 秒 → 0 秒”,期间通过修改响应式属性d.loading和d.content实时驱动 UI:
const d = dialog.success({ title: '异步', content: '点击,倒计时 3 秒', positiveText: '确认', onPositiveClick: () => { d.loading = true return new Promise((resolve) => { sleep() .then(() => { d.content = countDown(2); return sleep() }) .then(() => { d.content = countDown(1); return sleep() }) .then(() => { d.content = countDown(0) }) .then(resolve) }) } })这正是前面提到的“回调拦截 + 动态属性”两大能力的组合应用:回调返回 Promise,关闭行为被挂起,d.loading控制确认按钮 loading,d.content实时更新内容。
3. 使用组件(use-component.demo.vue)
直接使用<n-dialog>组件,通过@positive-click/@negative-click事件与negative-text/positive-textProps 完成交互,适合把对话框作为页面内固定区块(而非浮层)使用的场景。
4. 点击遮罩(mask.demo.vue)
演示maskClosable: false禁止点击遮罩关闭,同时通过onMaskClick在点击遮罩时给出提示、通过onEsc监听键盘 Esc:
dialog.success({ title: '关闭', content: '你确定?', positiveText: '确定', negativeText: '不确定', maskClosable: false, onMaskClick: () => message.success('不能关闭'), onEsc: () => message.success('通过 esc 关闭') })适合“强确认”场景(如不可撤销的删除操作),强制用户只能通过明确按钮关闭。
5. 自定义 Action(action.demo.vue)
演示title、content、action三个渲染函数属性,任意输出自定义 VNode:
dialog.warning({ title: '使用渲染函数', content: () => 'Content', action: () => 'Action' })当默认的确认/取消按钮无法满足需求(例如需要嵌入表单、多按钮或自定义布局)时,可完全接管action区域的内容。
6. 访问全部 Dialog 实例(use-dialog-reactive-list.demo.vue)
使用useDialogReactiveList()获取当前n-dialog-provider下全部对话框实例的响应式数组,直接渲染数量:
const dialogReactiveList = useDialogReactiveList() // 模板中:目前页面中共有 {{ dialogReactiveList.length }} 个对话框。useDialogReactiveList的实现同样基于依赖注入(composables.ts),返回的是Ref<readonly DialogReactive[]>;由于 provider 内部维护的是同一个dialogListRef,列表会随对话框的创建与销毁自动增删。官方文档给出的类型签名为:
() => Ref<readonly DialogReactive[]>7. Focus debug(focus-debug.demo.vue)
该示例通过渲染函数在content中放置两个NTimePicker,用于验证对话框打开后的焦点管理行为(配合autoFocus、closeFocusable等配置调试键盘可达性)。
8. RTL debug(rtl-debug.demo.vue)
用于验证n-config-provider开启 RTL(从右到左)布局后对话框的样式表现,样式定义可参见 src/dialog/src/styles/rtl.cssr.ts。
底层原理:DialogProvider 的完整工作流
综合 DialogProvider.ts 与 composables.ts,函数式 Dialog 的完整调用链可以概括为:
- 注册:
NDialogProvider.setup中创建dialogListRef、dialogInstRefs,并把 API 对象与响应式列表通过三个 injection key 注入组件树(dialogApiInjectionKey、dialogProviderInjectionKey、dialogReactiveListInjectionKey),见 context.ts 与 DialogProvider.ts。 - 创建:调用
dialog.warning(options)→create({ ...options, type })→ 生成key、包装成reactive对象、push进dialogListRef。 - 渲染:provider 的
render()遍历dialogList,为每个实例渲染一个NDialogEnvironment(DialogEnvironment.tsx),并把destroy与style剥离、以internalStyle/internalKey传入;每个实例通过ref回调登记到dialogInstRefs,以便destroy/destroyAll找到它。 - 销毁:
destroy()/destroyAll()调用hide()播放关闭动画,动画结束后handleAfterLeave将实例从列表移除,实现“创建即入列、销毁即出列”的完整生命周期。
函数式 Dialog 的核心优势由此体现:调用方无需维护组件挂载状态,所有实例由 provider 统一管理,并且由于DialogReactive是响应式对象,调用方持有引用后可以随时动态修改任意属性——这是组件式<n-dialog>难以直接实现的“远程控制”能力。
在测试层面,仓库提供了 src/dialog/tests/Dialog.spec.tsx 覆盖交互行为,以及 server.spec.tsx 验证服务端渲染(SSR)场景下的稳定性;对话框的明暗主题变量定义在 src/dialog/styles/light.ts 与 src/dialog/styles/dark.ts,可通过主题覆盖机制统一定制。
结语
naive-ui 的 Dialog 组件在设计上提供了“双轨制”使用方式:函数式 API(useDialog+DialogOptions)适合需要程序化控制、异步确认、动态更新的场景;组件式<n-dialog>适合在模板中静态声明、以事件驱动交互的场景。官方中文文档(src/dialog/demos/zhCN/index.demo-entry.md)中列出的全部 API 表格与 8 个演示示例覆盖了从基础确认框到异步倒计时、从遮罩控制到自定义渲染函数的完整能力图谱;结合 DialogProvider.ts 的实现,可以清晰理解其依赖注入、响应式实例管理与生命周期清理机制,从而在真实项目中得心应手地使用与调试。
【免费下载链接】naive-uiA Vue 3 Component Library. Fairly Complete. Theme Customizable. Uses TypeScript. Fast.项目地址: https://gitcode.com/gh_mirrors/na/naive-ui
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考