模态框 Modal
模态对话框
何时使用
- 在当前页面正中打开一个浮层,承载相应的操作或者提示内容
提示
Modal 提供两种组件内用法:命令式 useModal()(需外层存在 <ModalProvider>)与声明式 <Modal v-model:open>(由 v-model:open 驱动显隐)。
如果你想在 setup 外使用(例如路由守卫、axios 拦截器、Pinia action 等),请参考文档末尾的 在 setup 外使用。
使用方式
| 调用方式 | API | 适用位置 |
|---|---|---|
| 组件树内调用 推荐 | useModal() | 组件 setup 内,需外层存在 <ModalProvider> |
| 声明式用法 插槽自定义 | <Modal v-model:open> | 模板中,标题 / 内容 / 底部需用插槽自定义时 |
| 脱离组件树调用 无需 ModalProvider | createDiscreteApi() | 任意位置(axios 拦截器、路由守卫、Pinia action 等) |
一、组件树内使用:useModal() 推荐
适用于命令式调用:先在应用根节点放置一次 <ModalProvider>,之后任意层级组件均可通过 useModal() 取得同一实例
关于 ModalProvider 与 Modal
ModalProvider内部渲染一个Modal组件,并通过provide/inject向下提供useModal()所需的 API,自身不渲染任何可见内容- 组件级配置属性(
width/height/centered/top/blockScroll/to等)会透传给内部的Modal,因此直接参考下方 Modal Props 设置即可 - 使用
useModal()时,组件级配置设置在<ModalProvider>上(无法直接接触内部Modal);每次调用的个性化配置(title/content/icon/width/onOk等)则在调用info/confirm等方法时作为参数传入,参考 ModalOptions Type
1. 在应用根节点放置 ModalProvider
App.vue
<script setup lang="ts">
import { ModalProvider } from 'vue-amazing-ui'
</script>
<template>
<ModalProvider>
<RouterView />
</ModalProvider>
</template>2. 在任意层级组件中调用 useModal()
XXX.vue
<script setup lang="ts">
import { useModal } from 'vue-amazing-ui'
const modal = useModal()
function onClick() {
modal.confirm({
title: 'Confirm Title',
content: 'Some descriptions ...',
onOk: () => {
console.log('点击了确定按钮')
},
onCancel: () => {
console.log('点击了取消按钮')
}
})
}
</script>
<template>
<Button @click="onClick">按钮</Button>
</template>二、声明式用法:<Modal v-model:open> 插槽自定义
适用于内容需要用插槽自定义的场景:由 v-model:open 驱动显隐,标题、内容、底部均可交由插槽接管
XXX.vue
<script setup lang="ts">
import { ref } from 'vue'
import { Modal } from 'vue-amazing-ui'
const open = ref(false)
</script>
<template>
<Button type="primary" @click="open = true">打开弹窗</Button>
<Modal v-model:open="open" title="This is a declarative modal" content="Some descriptions ..." />
</template>命令式与声明式的差异
- 实例生命周期:命令式(
info/confirm等)每次调用入栈一个新实例,默认关闭即销毁(destroyOnClose: true);声明式实例常驻(默认destroyOnClose: false),关闭后内容 DOM 与内部状态保留 - 默认行为:命令式默认不显示右上角关闭按钮、不响应遮罩点击,需显式传
closable: true/maskClosable: true;声明式回落到组件默认值(closable: false、maskClosable: true) - 内容来源:命令式通过
title/content等参数传入;声明式除参数外,还可用#icon/#title/#default/#footer/#closeIcon插槽自定义
本文档网站已在主题层全局包裹
<ModalProvider>,以下演示均通过useModal()获取实例(与真实项目用法一致);需要不同组件级配置的演示,均通过页面内局部嵌套<ModalProvider>实现。
基本使用
共有六种内置形态:info / success / error / warning 为单按钮(知道了),confirm / erase 为双按钮(取消 + 确定)
Show Code
<script setup lang="ts">
import { useMessage, useModal } from 'vue-amazing-ui'
const modal = useModal()
const message = useMessage()
// info / success / error / warning 为单按钮(知道了)
function onInfo() {
modal.info({
title: 'This is an info modal',
content: 'Some descriptions ...',
onKnow: () => message.info('点击了「知道了」')
})
}
function onSuccess() {
modal.success({
title: 'This is a success modal',
content: 'Some descriptions ...',
onKnow: () => message.success('点击了「知道了」')
})
}
function onError() {
modal.error({
title: 'This is an error modal',
content: 'Some descriptions ...',
onKnow: () => message.error('点击了「知道了」')
})
}
function onWarning() {
modal.warning({
title: 'This is a warning modal',
content: 'Some descriptions ...',
onKnow: () => message.warning('点击了「知道了」')
})
}
// confirm / erase 为双按钮(取消 + 确定)
function onConfirm() {
modal.confirm({
title: 'This is a confirm modal',
content: 'Some descriptions ...',
onOk: () => message.success('点击了「确定」'),
onCancel: () => message.error('点击了「取消」')
})
}
function onErase() {
modal.erase({
title: 'This is an erase modal',
content: 'Some descriptions ...',
okType: 'danger',
onOk: () => message.success('点击了「删除」'),
onCancel: () => message.error('点击了「取消」')
})
}
</script>
<template>
<Space>
<Button type="primary" @click="onInfo">Info</Button>
<Button type="primary" @click="onSuccess">Success</Button>
<Button type="primary" @click="onError">Error</Button>
<Button type="primary" @click="onWarning">Warning</Button>
<Button type="primary" @click="onConfirm">Confirm</Button>
<Button type="primary" @click="onErase">Erase</Button>
</Space>
</template>声明式用法
用 v-model:open 控制显隐,标题、内容、底部均可用插槽自定义;change 事件在每次开关时触发,并携带该实例的 key
Show Code
<script setup lang="ts">
import { ref } from 'vue'
import { FireFilled } from '@ant-design/icons-vue'
import { useMessage } from 'vue-amazing-ui'
const message = useMessage()
const basicOpen = ref(false)
const slotOpen = ref(false)
// change 事件在每次开关时触发,并携带该实例的 key
function onDeclarativeChange(open: boolean, key: string) {
message.info(`最近一次 change 事件:open: ${open},key: ${key}`)
}
function onDeclarativeOk() {
message.success('点击了「确定」,v-model:open 已同步为 false')
}
function onDeclarativeCancel() {
message.warning('点击了「取消」/ 遮罩 / Esc,v-model:open 已同步为 false')
}
function onSlotOk() {
message.success('已提交处理')
slotOpen.value = false
}
</script>
<template>
<Space>
<Button type="primary" @click="basicOpen = true">打开声明式弹窗</Button>
<Button type="primary" @click="slotOpen = true">插槽自定义标题与底部</Button>
</Space>
<Modal
v-model:open="basicOpen"
title="This is a declarative modal"
content="v-model:open 为 true 时打开,点击确定 / 取消 / 遮罩 / Esc 均会同步回写 false。"
@ok="onDeclarativeOk"
@cancel="onDeclarativeCancel"
@change="onDeclarativeChange"
/>
<Modal v-model:open="slotOpen" :width="460">
<template #icon>
<FireFilled style="color: #ff6900" />
</template>
<template #title>
<span>Vue Amazing UI</span>
</template>
<p>Some descriptions ...</p>
<p>Some descriptions ...</p>
<template #footer>
<Space>
<Button @click="slotOpen = false">稍后处理</Button>
<Button type="primary" @click="onSlotOk">立即处理</Button>
</Space>
</template>
</Modal>
</template>内容保留、预渲染与关闭回调
命令式弹窗默认 destroyOnClose: true,关闭即销毁;声明式弹窗默认 false,关闭后保留内容。下面分三组演示内容保留、预渲染与关闭回调的差异
两个弹窗都含 Switch 开关:打开开关 → 关闭 → 重新打开,对比开关状态是否保留。
实例保留,重复打开复用同一实例,内部状态持久化
实例销毁,每次打开都创建全新实例,状态重置
弹窗内容的渲染时长实时同步到下方:懒渲染首次打开前为「未渲染」,预渲染打开前已在计时。
首次打开时才渲染内容
随页面一起渲染,打开即可见已有计时
点击「知道了」或按 Esc 关闭均可触发,回调在关闭动画播放结束后才执行。
适合做资源清理或路由跳转
Show Code
<script setup lang="ts">
import { computed, defineComponent, h, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import type { CSSProperties } from 'vue'
import { format } from 'date-fns'
import { Switch, useMessage, useModal } from 'vue-amazing-ui'
import type { ModalReactive } from 'vue-amazing-ui'
const modal = useModal()
const message = useMessage()
// Statistic 默认 24px 字号在卡片内偏大,统一收窄
const statisticValueStyle: CSSProperties = { fontSize: '20px' }
// 一、destroyOnClose:内容是否保留
// destroyOnClose: false 时关闭不销毁内容,组件实例保留,内部状态持久化
let draftModal: ModalReactive | null = null
const draftOpenCount = ref(0)
const onceOpenCount = ref(0)
// Switch 是受控组件(点击只 emit update:modelValue),需自行持有选中态才能响应点击;
// 状态随组件实例存活:destroyOnClose: false 时实例保留则状态持久化,true 时销毁重建则状态重置
const SwitchDemo = defineComponent({
setup() {
const checked = ref(false)
return () =>
h(Switch, {
modelValue: checked.value,
'onUpdate:modelValue': (value: boolean) => {
checked.value = value
}
})
}
})
function onOpenDraftModal() {
draftOpenCount.value += 1
if (draftModal) {
draftModal.show()
return
}
draftModal = modal.confirm({
title: '开关状态',
content: () =>
h('div', { style: 'display: flex; flex-direction: column; align-items: flex-start; gap: 8px; padding: 4px;' }, [
h('span', { style: 'flex: 1' }, '打开开关后关闭弹窗,重新打开对比状态是否保留:'),
h(SwitchDemo)
]),
destroyOnClose: false,
onOk: () => message.success('已保存开关状态(组件实例保留)')
})
}
function onOpenOnceModal() {
onceOpenCount.value += 1
modal.confirm({
title: '开关状态',
content: () =>
h('div', { style: 'display: flex; flex-direction: column; align-items: flex-start; gap: 8px; padding: 4px;' }, [
h('span', { style: 'flex: 1' }, '打开开关后关闭弹窗,重新打开对比状态是否重置:'),
h(SwitchDemo)
]),
destroyOnClose: true,
onOk: () => message.success('已提交(组件实例已销毁,下次打开全新)')
})
}
// 二、renderBeforeOpen:预渲染
// 页面级计时状态:弹窗内的渲染时长实时同步到页面,打开弹窗前即可看出懒渲染与预渲染的差异
const renderSeconds = reactive<{ lazy: number | null; pre: number | null }>({ lazy: null, pre: null })
function formatRenderSeconds(seconds: number | null): string {
return seconds === null ? '未渲染' : `已渲染 ${seconds} 秒`
}
// 未渲染时置灰,已渲染时高亮,直观区分懒渲染与预渲染
function renderValueStyle(seconds: number | null): CSSProperties {
return { fontSize: '20px', color: seconds === null ? 'rgba(0, 0, 0, 0.45)' : '#1677ff' }
}
const lazyRender = computed(() => ({
text: formatRenderSeconds(renderSeconds.lazy),
style: renderValueStyle(renderSeconds.lazy)
}))
const preRender = computed(() => ({
text: formatRenderSeconds(renderSeconds.pre),
style: renderValueStyle(renderSeconds.pre)
}))
const ContentTimer = defineComponent({
emits: ['tick'],
setup(_, { emit }) {
const elapsed = ref(0)
// 定时器在 onMounted 中启动:SSR 阶段不执行挂载生命周期,window 仅在客户端可用
let timer: number | undefined
onMounted(() => {
timer = window.setInterval(() => {
elapsed.value += 1
// 上报渲染时长,供页面级状态展示
emit('tick', elapsed.value)
}, 1000)
})
onBeforeUnmount(() => {
window.clearInterval(timer)
})
return () => h('p', { style: 'margin: 0' }, `内容已渲染 ${elapsed.value} 秒`)
}
})
function onLazyRenderTick(seconds: number): void {
renderSeconds.lazy = seconds
}
function onPreRenderTick(seconds: number): void {
renderSeconds.pre = seconds
}
const lazyOpen = ref(false)
const preRenderOpen = ref(false)
// 三、afterClose:关闭后回调
const afterCloseCount = ref(0)
const lastAfterCloseTime = ref<string | null>(null)
function onAfterCloseModal() {
modal.info({
title: 'afterClose 回调',
content: '关闭动画结束后才触发 afterClose,适合做资源清理或跳转。',
afterClose: () => {
afterCloseCount.value += 1
lastAfterCloseTime.value = format(Date.now(), 'yyyy-MM-dd HH:mm:ss')
message.success('afterClose 触发:弹窗已完全关闭')
}
})
}
</script>
<template>
<Card class="demo-group">
<template #title>
<Space :gap="8">
<Badge :value="1" color="blue" />
<span>destroyOnClose:内容是否保留</span>
</Space>
</template>
<p class="demo-group-desc">
两个弹窗都含 <code>Switch</code> 开关:打开开关 → 关闭 → 重新打开,对比开关状态是否保留。
</p>
<Flex wrap="wrap" :gap="16">
<Card class="demo-variant">
<template #title>
<Button type="primary" @click="onOpenDraftModal">草稿弹窗(保留内容)</Button>
</template>
<template #extra>
<Tag>false</Tag>
</template>
<p class="demo-variant-desc">实例保留,重复打开复用同一实例,内部状态持久化</p>
<Statistic title="已打开" :value="draftOpenCount" suffix="次" :value-style="statisticValueStyle" />
</Card>
<Card class="demo-variant">
<template #title>
<Button type="primary" @click="onOpenOnceModal">一次性弹窗(关闭销毁)</Button>
</template>
<template #extra>
<Tag color="success">true</Tag>
</template>
<p class="demo-variant-desc">实例销毁,每次打开都创建全新实例,状态重置</p>
<Statistic title="已打开" :value="onceOpenCount" suffix="次" :value-style="statisticValueStyle" />
</Card>
</Flex>
</Card>
<Card class="demo-group">
<template #title>
<Space :gap="8">
<Badge :value="2" color="blue" />
<span>renderBeforeOpen:预渲染</span>
</Space>
</template>
<p class="demo-group-desc">
弹窗内容的渲染时长实时同步到下方:懒渲染<b>首次打开前为「未渲染」</b>,预渲染<b>打开前已在计时</b>。
</p>
<Flex wrap="wrap" :gap="16">
<Card class="demo-variant">
<template #title>
<Button type="primary" @click="lazyOpen = true">懒渲染(默认)</Button>
</template>
<template #extra>
<Tag>false</Tag>
</template>
<p class="demo-variant-desc">首次打开时才渲染内容</p>
<Statistic title="渲染状态" :value-style="lazyRender.style">{{ lazyRender.text }}</Statistic>
</Card>
<Card class="demo-variant">
<template #title>
<Button type="primary" @click="preRenderOpen = true">预渲染</Button>
</template>
<template #extra>
<Tag color="success">true</Tag>
</template>
<p class="demo-variant-desc">随页面一起渲染,打开即可见已有计时</p>
<Statistic title="渲染状态" :value-style="preRender.style">{{ preRender.text }}</Statistic>
</Card>
</Flex>
<Modal v-model:open="lazyOpen" title="懒渲染(默认)">
<ContentTimer @tick="onLazyRenderTick" />
</Modal>
<Modal v-model:open="preRenderOpen" title="预渲染(render-before-open)" render-before-open>
<ContentTimer @tick="onPreRenderTick" />
</Modal>
</Card>
<Card class="demo-group">
<template #title>
<Space :gap="8">
<Badge :value="3" color="blue" />
<span>afterClose:关闭后回调</span>
</Space>
</template>
<p class="demo-group-desc">
点击「知道了」或按 <code>Esc</code> 关闭均可触发,回调在<b>关闭动画播放结束后</b>才执行。
</p>
<Flex wrap="wrap" :gap="16">
<Card class="demo-variant">
<template #title>
<Button type="primary" @click="onAfterCloseModal">打开弹窗</Button>
</template>
<p class="demo-variant-desc">适合做资源清理或路由跳转</p>
<Space :gap="32" wrap>
<Statistic title="触发次数" :value="afterCloseCount" :value-style="statisticValueStyle" />
<Statistic title="最近触发" :value-style="statisticValueStyle">
{{ lastAfterCloseTime ?? '—' }}
</Statistic>
</Space>
</Card>
</Flex>
</Card>
</template>
<style lang="less" scoped>
.demo-group + .demo-group {
margin-top: 16px;
}
.demo-group-desc {
margin: 0 0 16px;
color: rgba(0, 0, 0, 0.65);
font-size: 14px;
line-height: 1.6;
}
.demo-variant {
flex: 1 1 280px;
min-width: 260px;
}
.demo-variant-desc {
margin: 0 0 12px;
color: rgba(0, 0, 0, 0.65);
font-size: 14px;
line-height: 1.6;
}
</style>内容区高度与滚动
Show Code
<script setup lang="ts">
import { h } from 'vue'
import type { VNode } from 'vue'
import { useModal } from 'vue-amazing-ui'
const modal = useModal()
// 生成演示用的超长内容:每次调用返回新的 VNode,避免复用同一 VNode 实例导致渲染异常
function createLongContent(): VNode {
return h(
'div',
null,
Array.from({ length: 30 }, (_, index) =>
h('p', { style: 'margin: 0 0 8px' }, `第 ${index + 1} 行:这是一段用于演示超高内容滚动行为的示例文本。`)
)
)
}
// height 为 auto(默认)时内容由弹框自然撑开,超高后整个弹框随外层容器滚动
function onWholeScrollModal() {
modal.info({
title: '整体弹框滚动(height 默认 auto)',
content: createLongContent,
centered: true,
width: 520
})
}
// 指定 height 后内容区内部滚动,标题与按钮固定可见
function onInnerScrollModal() {
modal.info({
title: '内容区内部滚动(height + scrollbarProps)',
content: createLongContent,
height: 300,
scrollbarProps: { trigger: 'none', contentStyle: { paddingRight: '12px' } },
centered: true,
width: 520
})
}
// scrollbarProps 透传给内置 Scrollbar,可定制滚动条大小与位置
function onCustomScrollbarModal() {
modal.info({
title: '自定义滚动条',
content: createLongContent,
height: 300,
scrollbarProps: { trigger: 'none', size: 10, yPlacement: 'left', contentStyle: { paddingLeft: '12px' } },
centered: true,
width: 520
})
}
</script>
<template>
<Space>
<Button type="primary" @click="onWholeScrollModal">整体弹框滚动</Button>
<Button type="primary" @click="onInnerScrollModal">内容区内部滚动</Button>
<Button type="primary" @click="onCustomScrollbarModal">自定义滚动条</Button>
</Space>
</template>自定义宽度
Show Code
<script setup lang="ts">
import { useModal } from 'vue-amazing-ui'
const modal = useModal()
function onNumberWidthModal() {
modal.info({
title: '数值宽度',
content: 'Some descriptions ...',
width: 365
})
}
function onPercentWidthModal() {
modal.confirm({
title: '百分比宽度',
content: 'Some descriptions ...',
width: '28%'
})
}
</script>
<template>
<Space>
<Button type="primary" @click="onNumberWidthModal">数值宽度</Button>
<Button type="primary" @click="onPercentWidthModal">百分比宽度</Button>
</Space>
</template>自定义图标
icon 与 closeIcon 属性支持 VNode / 渲染函数(命令式与声明式一致);插槽形态仅声明式用法下可用,通过 #icon / #closeIcon 插槽接管默认图标
Show Code
<script setup lang="ts">
import { h, ref } from 'vue'
import { CloseCircleFilled, CloudFilled, SoundFilled } from '@ant-design/icons-vue'
import { Modal, useModal } from 'vue-amazing-ui'
const modal = useModal()
function onVNodeIconModal() {
modal.info({
title: '节点图标',
content: 'Some descriptions ...',
icon: h(CloudFilled)
})
}
// icon 也支持渲染函数形态,每次渲染时动态生成图标
function onRenderFnIconModal() {
modal.confirm({
title: '渲染函数图标',
content: 'Some descriptions ...',
icon: () => h(SoundFilled, { style: 'color: gold' })
})
}
// 声明式用法下 icon / closeIcon 同样支持渲染函数形态
const renderFnIconOpen = ref(false)
</script>
<template>
<Space>
<Button type="primary" @click="onVNodeIconModal">VNode 图标</Button>
<Button type="primary" @click="onRenderFnIconModal">渲染函数图标</Button>
<Button type="primary" @click="renderFnIconOpen = true">声明式渲染函数图标</Button>
</Space>
<Modal
v-model:open="renderFnIconOpen"
title="声明式渲染函数图标"
content="icon 与 closeIcon 在声明式用法下同样支持渲染函数形态,右上角关闭图标也是渲染函数。"
closable
:icon="() => h(SoundFilled, { style: 'color: gold' })"
:close-icon="() => h(CloseCircleFilled, { style: 'color: #ff4d4f' })"
/>
</template>自定义样式
Modal 渲染为多层结构,各层的外观 / 定位由对应的 XxxClass / XxxStyle 控制:
| 层级 | DOM 类名 | 职责 | 对应配置项 |
|---|---|---|---|
| 外层容器 | .modal-wrap | 铺满视口,多实例共享 | wrapClass / wrapStyle |
| 蒙层 | .modal-mask | 遮罩 | maskClass / maskStyle |
| 定位层 | .modal-container | 承载 width / top / zIndex,本身无视觉样式 | containerClass / containerStyle |
| 卡片层 | .modal-body-wrap | 白底 / 圆角 / 阴影所在的弹窗卡片 | bodyClass / bodyStyle |
| 标题 | .modal-title | 标题文字 | titleClass / titleStyle |
| 内容 | .modal-content | 正文区 | contentClass / contentStyle |
Show Code
<script setup lang="ts">
import { h } from 'vue'
import { CrownFilled, FireFilled, NotificationFilled } from '@ant-design/icons-vue'
import { useModal } from 'vue-amazing-ui'
const modal = useModal()
function onCustomClass() {
modal.info({
title: '自定义卡片类名(bodyClass)',
content: 'bodyClass 挂到卡片层 .modal-body-wrap,配合全局 less 将白卡改为橙色渐变 + 描边。',
icon: h(FireFilled),
bodyClass: 'custom-modal-body'
})
}
function onBodyMaskStyle() {
modal.confirm({
title: '自定义卡片与遮罩样式(bodyStyle / maskStyle)',
content: 'maskStyle 将遮罩染为半透明蓝,bodyStyle 为卡片加上内边距、蓝色描边与圆角。',
icon: h(NotificationFilled),
bodyStyle: {
padding: '32px',
borderRadius: '20px',
border: '2px solid #1677ff',
boxShadow: '0 8px 32px rgba(22, 119, 255, 0.25)'
},
maskStyle: { backgroundColor: 'rgba(22, 119, 255, 0.45)' }
})
}
function onTitleContentStyle() {
modal.success({
title: '自定义标题与内容样式(titleStyle / contentStyle)',
content: '上方标题经 titleStyle 放大加粗变红,本段正文经 contentStyle 放大并调色。',
icon: h(CrownFilled),
titleStyle: { fontSize: '20px', fontWeight: 600, color: '#d4380d' },
contentStyle: { fontSize: '15px', lineHeight: 1.8, color: '#d4380d' }
})
}
// containerClass / containerStyle 作用于定位层 .modal-container,用于覆盖 width / top / zIndex;
// 卡片外观(背景 / 圆角 / 阴影 / 描边)请用作用于 .modal-body-wrap 的 bodyClass / bodyStyle
function onContainerClass() {
modal.info({
title: '自定义定位层类名(containerClass)',
content: '类名挂在定位层 .modal-container 上,全局样式将默认顶距覆盖为 200px、宽度覆盖为 480px。',
containerClass: 'custom-modal-container'
})
}
function onContainerStyle() {
modal.info({
title: '自定义定位层样式(containerStyle)',
content: 'containerStyle 优先级更高,将 width: 420 与默认顶距分别覆盖为 480px 宽、180px 顶距。',
width: 420,
containerStyle: { width: '480px', top: '180px' }
})
}
</script>
<template>
<Space>
<Button type="primary" @click="onCustomClass">自定义卡片类名</Button>
<Button type="primary" @click="onBodyMaskStyle">自定义卡片与遮罩样式</Button>
<Button type="primary" @click="onTitleContentStyle">自定义标题与内容样式</Button>
<Button type="primary" @click="onContainerClass">自定义定位层类名</Button>
<Button type="primary" @click="onContainerStyle">自定义定位层样式</Button>
</Space>
</template>
<style lang="less">
// 弹窗通过 Teleport 挂载到 body 下,scoped 样式无法命中,需使用全局样式
// bodyClass 演示:类名挂在卡片层 .modal-body-wrap 上,让默认白底卡片变为橙色渐变 + 描边,一眼可辨命中层
.custom-modal-body {
background: linear-gradient(135deg, #fff7e6 0%, #ffd591 100%) !important;
border: 2px solid #ff6900 !important;
border-radius: 16px !important;
box-shadow: 0 6px 24px rgba(255, 105, 0, 0.18) !important;
}
// containerClass 演示:类名挂在定位层 .modal-container 上(透明、本身无视觉),
// 类内用 !important 覆盖内置 top / width,即可直观看到「定位被类接管」
.custom-modal-container {
top: 200px !important;
width: 480px !important;
}
</style>自定义按钮
Show Code
<script setup lang="ts">
import { useMessage, useModal } from 'vue-amazing-ui'
const modal = useModal()
const message = useMessage()
function onNoticeBtnModal() {
modal.info({
title: '自定义按钮文案',
content: 'Some descriptions ...',
noticeText: 'Noted',
noticeProps: {
shape: 'round'
},
onKnow: () => message.success('点击了 Noted')
})
}
function onConfirmBtnsModal() {
modal.confirm({
title: '自定义确认按钮',
content: 'Some descriptions ...',
cancelText: 'No',
cancelProps: { type: 'danger', ghost: true },
okText: 'Yes',
okType: 'danger',
okProps: { ghost: true },
onOk: () => message.success('点击了 Yes'),
onCancel: () => message.error('点击了 No')
})
}
// showCancel 仅在 confirm / erase 双按钮形态生效
function onHideCancelModal() {
modal.confirm({
title: '隐藏取消按钮',
content: 'showCancel: false 时只保留确定按钮。',
showCancel: false,
okText: '知道了',
onOk: () => message.success('点击了「知道了」')
})
}
</script>
<template>
<Space>
<Button type="primary" @click="onNoticeBtnModal">自定义按钮文案</Button>
<Button type="primary" @click="onConfirmBtnsModal">自定义确认按钮</Button>
<Button type="primary" @click="onHideCancelModal">隐藏取消按钮</Button>
</Space>
</template>自定义底部区域
Show Code
<script setup lang="ts">
import { h } from 'vue'
import { Button, useMessage, useModal } from 'vue-amazing-ui'
import type { ModalReactive } from 'vue-amazing-ui'
const modal = useModal()
const message = useMessage()
// footer 传渲染函数时完全接管底部,内置按钮组不再渲染
function onFooterRenderModal() {
const handle: ModalReactive = modal.info({
title: '自定义底部渲染',
content: 'footer 传渲染函数时,底部区域完全由该函数接管。',
closable: true,
footer: () =>
h('div', { style: 'display: flex; justify-content: flex-end; gap: 8px' }, [
h(Button, { onClick: () => handle.destroy() }, { default: () => '稍后处理' }),
h(
Button,
{
type: 'primary',
onClick: () => {
message.success('已立即处理')
handle.destroy()
}
},
{ default: () => '立即处理' }
)
])
})
}
function onFooterlessModal() {
modal.info({
title: '无底部按钮',
content: 'footer: false 时底部整块隐藏,配合 closable 用右上角关闭。',
closable: true,
footer: false
})
}
</script>
<template>
<Space>
<Button type="primary" @click="onFooterRenderModal">自定义底部渲染</Button>
<Button type="primary" @click="onFooterlessModal">无底部按钮</Button>
</Space>
</template>完全自定义
create() 不渲染内置图标与按钮组,图标、内容、底部均由 icon / content / footer 自行组合
Show Code
<script setup lang="ts">
import { h } from 'vue'
import { CrownFilled } from '@ant-design/icons-vue'
import { Button, useMessage, useModal } from 'vue-amazing-ui'
import type { ModalReactive } from 'vue-amazing-ui'
const modal = useModal()
const message = useMessage()
// create() 不渲染内置图标与按钮组,图标、底部均由 icon / footer 自行组合
function onCreateModal() {
const handle: ModalReactive = modal.create({
title: 'Custom Modal(create)',
content: h('p', { style: 'margin: 0' }, '通过 create() 创建,图标、内容、底部按钮均完全自定义。'),
width: 480,
closable: true,
icon: h(CrownFilled, { style: 'color: #faad14' }),
footer: () =>
h('div', { style: 'display: flex; justify-content: flex-end; gap: 8px' }, [
h(Button, { onClick: () => handle.destroy() }, { default: () => '取消' }),
h(
Button,
{
type: 'primary',
onClick: () => {
message.success('自定义确定')
handle.destroy()
}
},
{ default: () => '确定' }
)
])
})
}
</script>
<template>
<Button type="primary" @click="onCreateModal">自定义模态框</Button>
</template>关闭按钮
命令式弹窗默认不显示右上角关闭按钮,closable: true 时显示;closeIcon 支持 VNode 与渲染函数两种形态;closeFocusable: false 让关闭按钮不参与 Tab 序列
Show Code
<script setup lang="ts">
import { h } from 'vue'
import { CloseCircleFilled } from '@ant-design/icons-vue'
import { useMessage, useModal } from 'vue-amazing-ui'
const modal = useModal()
const message = useMessage()
// 命令式弹窗默认无关闭按钮,closable: true 显示右上角 X
function onClosableModal() {
modal.confirm({
title: '显示关闭按钮',
content: '命令式弹窗默认不显示右上角关闭按钮,设置 closable: true 后显示。',
closable: true,
onOk: () => message.success('点击了「确定」')
})
}
// closeIcon 支持 VNode 与渲染函数
function onCustomCloseIconModal() {
modal.confirm({
title: '自定义关闭图标',
content: 'closeIcon 支持 VNode 与渲染函数两种形态。',
closable: true,
closeIcon: () => h(CloseCircleFilled, { style: 'color: #ff4d4f' }),
onOk: () => message.success('点击了「确定」')
})
}
// 关闭按钮不参与 Tab 序列
function onNoCloseFocusableModal() {
modal.confirm({
title: '关闭按钮不参与 Tab 序列',
content: 'closeFocusable: false 时右上角关闭按钮 tabindex 为 -1,Tab / Shift + Tab 会跳过它,但 Esc 与鼠标点击照常。',
closable: true,
closeFocusable: false,
onOk: () => message.success('点击了「确定」')
})
}
</script>
<template>
<Space>
<Button type="primary" @click="onClosableModal">显示关闭按钮</Button>
<Button type="primary" @click="onCustomCloseIconModal">自定义关闭图标</Button>
<Button type="primary" @click="onNoCloseFocusableModal">关闭按钮不参与 Tab 序列</Button>
</Space>
</template>自定义位置
Show Code
<script setup lang="ts">
import { useModal } from 'vue-amazing-ui'
const modal = useModal()
function onNumberTopModal() {
modal.info({
title: '60px This is a number fixed modal',
content: 'Some descriptions ...',
centered: false,
top: 60
})
}
function onPercentTopModal() {
modal.info({
title: '20% This is a percent fixed modal',
content: 'Some descriptions ...',
centered: false,
top: '20%'
})
}
function onCenteredModal() {
modal.info({
title: 'This is a vertically centered modal',
content: 'Some descriptions ...',
centered: true
})
}
</script>
<template>
<Space>
<Button type="primary" @click="onNumberTopModal">数值顶距</Button>
<Button type="primary" @click="onPercentTopModal">百分比顶距</Button>
<Button type="primary" @click="onCenteredModal">垂直居中</Button>
</Space>
</template>动画出现位置
Show Code
<script setup lang="ts">
import { useModal } from 'vue-amazing-ui'
const modal = useModal()
function onMouseOriginModal() {
modal.info({
title: '从鼠标位置展开(默认)',
content: 'transformOrigin: mouse 时,弹窗从鼠标点击位置放大。',
transformOrigin: 'mouse'
})
}
function onCenterOriginModal() {
modal.info({
title: '从中心展开',
content: 'transformOrigin: center 时,弹窗从自身中心放大。',
transformOrigin: 'center'
})
}
</script>
<template>
<Space>
<Button type="primary" @click="onMouseOriginModal">从鼠标位置展开(默认)</Button>
<Button type="primary" @click="onCenterOriginModal">从中心展开</Button>
</Space>
</template>异步关闭与阻止关闭
onOk / onKnow / onCancel 返回 false 或 Promise reject 时阻止关闭,其余情况(含 Promise resolve)自动关闭;返回 Promise 期间按钮保持 loading
Show Code
<script setup lang="ts">
import { h } from 'vue'
import { ExclamationCircleFilled } from '@ant-design/icons-vue'
import { useMessage, useModal } from 'vue-amazing-ui'
const modal = useModal()
const message = useMessage()
// onOk 返回 Promise:resolve 后自动关闭,期间按钮保持 loading
function onAsyncOkModal() {
modal.confirm({
title: '确认提交这些项?',
content: '点击确定后等待 1.5s,Promise resolve 后自动关闭。',
icon: h(ExclamationCircleFilled),
onOk: () =>
new Promise<boolean>((resolve) => {
setTimeout(() => {
message.success('提交成功')
resolve(true)
}, 1500)
}),
onCancel: () => message.error('已取消提交')
})
}
// onOk 返回的 Promise reject 时阻止关闭(在 catch 中返回 false,避免异常冒泡到控制台)
function onRejectOkModal() {
modal.confirm({
title: '确认删除这些项?',
content: '点击确定后服务端校验失败,Promise reject 并阻止关闭。',
icon: h(ExclamationCircleFilled),
onOk: () =>
new Promise((_resolve, reject) => {
setTimeout(() => reject(new Error('服务端校验失败')), 1200)
}).catch(() => {
message.error('校验失败,弹窗保持打开')
return false
})
})
}
// onOk 同步返回 false 同样阻止关闭
function onPreventOkModal() {
let submitted = false
modal.confirm({
title: '同步阻止关闭',
content: '首次点击「确定」返回 false 阻止关闭,再次点击则正常关闭。',
onOk: () => {
if (!submitted) {
submitted = true
message.warning('还有必填项未完成,已阻止关闭')
return false
}
message.success('校验通过,弹窗关闭')
}
})
}
// onCancel 返回 false 时,遮罩 / Esc / 取消按钮都无法关闭
function onPreventCancelModal() {
modal.confirm({
title: '取消时阻止关闭',
content: '点击遮罩、按下 Esc 或点击「取消」都会被阻止,只有「确定」可关闭。',
maskClosable: true,
onOk: () => message.success('点击了「确定」'),
onCancel: () => {
message.warning('操作未完成,已阻止关闭')
return false
}
})
}
</script>
<template>
<Space>
<Button type="primary" @click="onAsyncOkModal">异步关闭(自动)</Button>
<Button type="primary" @click="onRejectOkModal">阻止关闭(失败)</Button>
<Button type="primary" @click="onPreventOkModal">阻止关闭(同步)</Button>
<Button type="primary" @click="onPreventCancelModal">阻止取消</Button>
</Space>
</template>原地更新
update 可更新 ModalOptions 的全部属性,另支持 mode(切换弹窗类型与内置按钮组)与 loading(手动驱动按钮 loading)
Show Code
<script setup lang="ts">
import { h, onBeforeUnmount, ref } from 'vue'
import { useMessage, useModal } from 'vue-amazing-ui'
import type { ModalUpdate } from 'vue-amazing-ui'
const modal = useModal()
const message = useMessage()
// 页面级定时器统一登记,卸载时清理,避免组件销毁后仍在跑
const timers: number[] = []
function onProgressUpload() {
const percent = ref(0)
const handle = modal.info({
title: '正在上传附件',
// content 传渲染函数:内部引用响应式 percent,进度变化时弹窗内容自动更新
content: () =>
h('span', { style: 'white-space: nowrap' }, [
'正在上传附件...',
h(
'span',
{
style: `
display: inline-block;
min-width: 4ch;
text-align: right;
font-variant-numeric: tabular-nums;
font-weight: 600;
color: #1677ff;
`
},
`${percent.value}%`
)
]),
noticeText: '上传中…',
// 上传未完成时阻止关闭
onKnow: () => {
message.warning('上传完成前不可关闭')
return false
}
})
// update 支持 loading:手动驱动按钮 loading
handle.update({ loading: true })
const timer = window.setInterval(() => {
percent.value += 4
if (percent.value < 100) {
return
}
clearInterval(timer)
// update 支持 mode:原地把 info 切换为 success,并重设文案与回调
const doneOptions: ModalUpdate = {
title: '上传完成',
content: '附件已上传,mode 已切换为 success。',
mode: 'success',
noticeText: '知道了',
loading: false,
onKnow: () => message.success('已确认上传结果')
}
handle.update(doneOptions)
}, 120)
timers.push(timer)
}
onBeforeUnmount(() => {
timers.forEach((timer) => {
clearInterval(timer)
})
})
</script>
<template>
<Button type="primary" @click="onProgressUpload">异步上传</Button>
</template>多实例层叠
连续调用依次入栈,各实例按自身 zIndex 分层(遮罩取 zIndex,弹窗取 zIndex + 10);点击遮罩只关闭栈顶,destroyAll() 关闭并销毁全部
Show Code
<script setup lang="ts">
import { useMessage, useModal } from 'vue-amazing-ui'
const modal = useModal()
const message = useMessage()
const timers: number[] = []
// 遮罩会挡住页面,弹窗打开后无法再点击页面按钮,故一次点击开启多层以便观察层叠与逐层关闭
function openStackLayers(content: string): void {
for (let layer = 1; layer <= 3; layer += 1) {
modal.info({
title: `第 ${layer} 层弹窗`,
content,
zIndex: 1000 + layer * 20,
maskClosable: true
})
}
}
function onStackModal() {
openStackLayers('点击遮罩或「知道了」只关闭栈顶弹窗;各实例按自身 zIndex 分层(遮罩取 zIndex,弹窗取 zIndex + 10)。')
}
// destroyAll 同样无法在弹窗打开后从页面触发,故开启多层后用定时器演示一次性全部销毁
function onDestroyAllModals() {
openStackLayers('2s 后 destroyAll() 会一次性关闭并销毁全部弹窗。')
timers.push(
window.setTimeout(() => {
modal.destroyAll()
message.success('destroyAll:已关闭全部弹窗')
}, 2000)
)
}
</script>
<template>
<Space>
<Button type="primary" @click="onStackModal">开启 3 层弹窗</Button>
<Button type="danger" @click="onDestroyAllModals">开启 3 层并 2 秒后全部销毁</Button>
</Space>
</template>遮罩、键盘与滚动锁定
命令式调用默认 maskClosable: false、keyboard: true、blockScroll: true;onMaskClick / onEsc 无论是否允许关闭都会触发
Show Code
<script setup lang="ts">
import { useMessage, useModal } from 'vue-amazing-ui'
const modal = useModal()
const message = useMessage()
function onNoMaskModal() {
modal.info({
title: '无遮罩',
content: '不渲染遮罩层,背景仍可交互,只能通过底部按钮关闭。注意:点击背景会使焦点移出弹窗,此时 Esc 不再响应。',
mask: false,
maskClosable: false
})
}
// 命令式调用默认 maskClosable: false,需显式开启
function onMaskClosableModal() {
modal.confirm({
title: '点击遮罩关闭',
content: '命令式调用默认 maskClosable: false,此处显式传 true。',
maskClosable: true,
onOk: () => message.success('点击了「确定」'),
onCancel: () => message.error('点击了遮罩 / Esc / 取消')
})
}
// keyboard: false 禁用 Esc 关闭,但 onEsc 回调仍会触发
function onNoKeyboardModal() {
modal.info({
title: '禁用 Esc 关闭',
content: '按下 Esc 不会关闭弹窗,但 onEsc 回调仍会触发。',
keyboard: false,
onEsc: () => message.info('onEsc 回调触发,但已禁用 Esc 关闭')
})
}
// onMaskClick / onEsc 无论是否允许关闭都会触发
function onMaskEscCallbackModal() {
modal.info({
title: '遮罩与 Esc 回调',
content: '遮罩点击与 Esc 均被禁用关闭,但两个回调仍会触发。',
maskClosable: false,
keyboard: false,
onMaskClick: () => message.info('onMaskClick 回调触发'),
onEsc: () => message.info('onEsc 回调触发')
})
}
function onBlockScrollModal() {
modal.info({
title: 'blockScroll: true(默认)',
content: '打开时锁定背景滚动,此时滚动页面无效,全部关闭后自动解锁。'
})
}
function onNoBlockScrollModal() {
modal.info({
title: 'blockScroll: false',
content: '不锁定背景滚动,可用滚轮滚动页面,与上一例对比即可看出差异。',
blockScroll: false
})
}
</script>
<template>
<Space>
<Button type="primary" @click="onNoMaskModal">无遮罩</Button>
<Button type="primary" @click="onMaskClosableModal">点击遮罩关闭</Button>
<Button type="primary" @click="onNoKeyboardModal">禁用 Esc 关闭</Button>
<Button type="primary" @click="onMaskEscCallbackModal">遮罩与 Esc 回调</Button>
<Button type="primary" @click="onBlockScrollModal">锁定背景滚动</Button>
<Button type="primary" @click="onNoBlockScrollModal">不锁定滚动</Button>
</Space>
</template>焦点管理
autoFocusButton 控制入场后自动聚焦的按钮(默认确定按钮),Tab 焦点锁定在弹窗内循环;focusTriggerAfterClose 控制关闭后是否把焦点归还触发元素。注意:Esc 监听绑定在弹窗主体上,焦点移出弹窗(如 mask: false 时点击背景)后不再响应
Show Code
<script setup lang="ts">
import { useMessage, useModal } from 'vue-amazing-ui'
const modal = useModal()
const message = useMessage()
function onAutoFocusOkModal() {
modal.confirm({
title: '聚焦确定按钮(默认)',
content: '入场动画结束后,焦点自动落在「确定」按钮上。',
autoFocusButton: 'ok',
onOk: () => message.success('点击了「确定」')
})
}
function onAutoFocusCancelModal() {
modal.confirm({
title: '聚焦取消按钮',
content: '入场动画结束后,焦点自动落在「取消」按钮上。',
autoFocusButton: 'cancel',
onCancel: () => message.error('点击了「取消」')
})
}
function onNoFocusRestoreModal() {
modal.confirm({
title: '关闭不归还焦点',
content: '关闭后焦点不归还给触发按钮;默认 true 时会归还。',
focusTriggerAfterClose: false,
onOk: () => message.success('点击了「确定」')
})
}
</script>
<template>
<Space>
<Button type="primary" @click="onAutoFocusOkModal">聚焦确定按钮</Button>
<Button type="primary" @click="onAutoFocusCancelModal">聚焦取消按钮</Button>
<Button type="primary" @click="onNoFocusRestoreModal">关闭不归还焦点</Button>
</Space>
</template>自定义渲染
modalRender 可拿到默认内容节点 originVNode,包一层自定义容器即可叠加拖拽等增强能力;声明式用法下也可使用同名的 #modalRender 作用域插槽,属性优先级更高
Show Code
<script setup lang="ts">
import { defineComponent, h, ref, withDirectives } from 'vue'
import type { Directive, PropType, VNode } from 'vue'
import { Modal, useModal } from 'vue-amazing-ui'
const modal = useModal()
// 指令卸载时移除监听,避免实例销毁后残留
const dragCleanups = new WeakMap<HTMLElement, () => void>()
// 把弹窗内容包进一层可拖拽容器:按住标题栏即可拖动,常用于拖拽场景
const vDragModal: Directive<HTMLElement> = {
mounted(el) {
let offsetX = 0
let offsetY = 0
function onMousedown(e: MouseEvent) {
// 仅按住标题栏才触发拖拽,避免影响文本选择与按钮点击
if (!(e.target as HTMLElement).closest('.modal-header')) {
return
}
e.preventDefault()
const startX = e.clientX - offsetX
const startY = e.clientY - offsetY
function onMousemove(event: MouseEvent) {
offsetX = event.clientX - startX
offsetY = event.clientY - startY
el.style.transform = `translate(${offsetX}px, ${offsetY}px)`
}
function onMouseup() {
document.removeEventListener('mousemove', onMousemove)
document.removeEventListener('mouseup', onMouseup)
document.body.style.userSelect = ''
}
document.addEventListener('mousemove', onMousemove)
document.addEventListener('mouseup', onMouseup)
document.body.style.userSelect = 'none'
}
el.addEventListener('mousedown', onMousedown)
dragCleanups.set(el, () => {
el.removeEventListener('mousedown', onMousedown)
document.body.style.userSelect = ''
})
},
unmounted(el) {
dragCleanups.get(el)?.()
dragCleanups.delete(el)
}
}
function onDraggableModal() {
modal.info({
title: '按住标题栏拖动我',
content: 'modalRender 可拿到默认内容节点 originVNode,包一层自定义容器即可实现拖拽。',
width: 460,
centered: true,
modalRender: ({ originVNode }) =>
withDirectives(h('div', { class: 'draggable-modal' }, [originVNode]), [[vDragModal]])
})
}
// 声明式:与命令式等价,改用 #modalRender 作用域插槽拿到 originVNode
const dragModalOpen = ref(false)
// 模板无法直接摆放已构造的 VNode,用透传组件承接 originVNode 后原样渲染
const VNodeRenderer = defineComponent({
props: {
vnode: {
type: Object as PropType<VNode>,
required: true
}
},
setup(props) {
return () => props.vnode
}
})
</script>
<template>
<Button type="primary" @click="onDraggableModal">可拖拽模态框(命令式)</Button>
<Button type="primary" @click="dragModalOpen = true">可拖拽模态框(声明式)</Button>
<Modal v-model:open="dragModalOpen" title="按住标题栏拖动我(声明式)" :width="460" centered>
<template #modalRender="{ originVNode }">
<div class="draggable-modal" v-drag-modal>
<VNodeRenderer :vnode="originVNode" />
</div>
</template>
<p>
声明式用法下同样通过 <code>#modalRender</code> 作用域插槽拿到 <code>originVNode</code>,
外包一层 <code>.draggable-modal</code> 容器并挂上 <code>v-drag-modal</code> 指令即可。
</p>
</Modal>
</template>
<style lang="less">
.draggable-modal {
.modal-header {
cursor: move;
user-select: none;
}
}
</style>自定义挂载容器
提示
Teleport 的目标必须已存在于文档中。
当目标与组件位于同一组件树内时,组件挂载瞬间目标尚未插入文档,需用 v-if 将组件延迟到挂载完成后再渲染。
Show Code
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { ModalProvider } from 'vue-amazing-ui'
import type { ModalApi } from 'vue-amazing-ui'
// to 为 Provider 级配置,无法逐条传入,故在页面局部嵌套 Provider 演示;@ready 可取到该 Provider 作用域内的 api
const toModal = ref<ModalApi>()
const toReady = ref(false)
// 目标容器与组件位于同一组件树,需等挂载完成(目标已插入文档)后再渲染组件,Teleport 才能定位到目标
onMounted(() => {
toReady.value = true
})
function onToModal() {
toModal.value?.info({
title: '自定义挂载容器',
content: '这个弹窗被挂载到下方虚线容器中'
})
}
</script>
<template>
<ModalProvider v-if="toReady" to="#modal-to-container" @ready="toModal = $event" />
<div id="modal-to-container" class="modal-to-container"></div>
<Button type="primary" @click="onToModal">挂载到指定容器</Button>
</template>
<style lang="less" scoped>
.modal-to-container {
position: relative;
transform: translateZ(0); // 建立包含块,使内部 fixed 定位的蒙层与弹窗相对该容器定位
max-width: 800px;
height: 320px;
margin-bottom: 10px;
border: 1px dashed #d9d9d9;
border-radius: 8px;
}
</style>APIs
Modal
组件级配置属性:使用 useModal() 时设置在 <ModalProvider> 上(会透传给内部 Modal),直接使用 <Modal> 组件时设置在 <Modal> 上,两者等价。
每次调用的个性化配置请参考 ModalOptions Type
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| width | 模态框宽度,单位 px | string | number | 420 |
| height | 内容区高度,单位 px,默认自适应内容高度;指定后内容区内部滚动 | string | number | 'auto' |
| icon | 自定义图标,prop 支持 VNode / 渲染函数;插槽形态请用同名 #icon 插槽 | VNode | (() => VNode) | undefined |
| title | 模态框标题 | string | VNode | (() => VNode) | undefined |
| titleClass | 自定义标题类名 | string | undefined |
| titleStyle | 自定义标题样式 | CSSProperties | {} |
| content | 模态框内容 | string | VNode | (() => VNode) | undefined |
| contentClass | 自定义内容类名 | string | undefined |
| contentStyle | 自定义内容样式 | CSSProperties | {} |
| scrollbarProps | 内容滚动条 Scrollbar 属性配置 | ScrollbarProps | {} |
| bodyClass | 自定义弹窗卡片(.modal-body-wrap)类名,用于定制背景 / 圆角 / 阴影等外观 | string | undefined |
| bodyStyle | 自定义弹窗卡片(.modal-body-wrap)样式,用于定制背景 / 圆角 / 阴影等外观 | CSSProperties | {} |
| cancelText | 取消按钮文字 | string | '取消' |
| cancelProps | 取消按钮 props 配置,参考 Button Props | ButtonProps | {} |
| okText | 确认按钮文字 | string | '确定' |
| okType | 确认按钮类型 | 'default' | 'reverse' | 'primary' | 'danger' | 'dashed' | 'text' | 'link' | 'primary' |
| okProps | 确认按钮 props 配置,优先级高于 okType,参考 Button Props | ButtonProps | {} |
| noticeText | 通知按钮文字 | string | '知道了' |
| noticeProps | 通知按钮 props 配置,参考 Button Props | ButtonProps | {} |
| footer | 是否显示底部按钮区:false 隐藏,true 渲染内置按钮组,传函数则完全自定义;插槽形态请用 #footer | boolean | (() => VNode) | true |
| closable | 是否显示右上角关闭按钮,默认 false,需要时显式开启 | boolean | false |
| closeIcon | 自定义关闭图标,prop 支持 VNode / 渲染函数;插槽形态请用同名 #closeIcon 插槽 | VNode | (() => VNode) | undefined |
| closeFocusable | 关闭按钮是否可聚焦,设为 false 后关闭按钮 tabindex 为 -1,不参与 Tab 序列 | boolean | true |
| destroyOnClose | 关闭时是否销毁 Modal 里的子元素,实例栈下关闭即从栈中移除,内容随之销毁 | boolean | false |
| renderBeforeOpen | 首次打开前是否渲染内容(关闭懒渲染) | boolean | false |
| centered | 是否水平垂直居中,否则固定高度水平居中 | boolean | false |
| top | 固定高度水平居中时,距顶部高度,仅当 centered: false 时生效,单位 px | string | number | 100 |
| transformOrigin | 模态框动画出现的位置 | 'mouse' | 'center' | 'mouse' |
| confirmLoading | 确定按钮 loading | boolean | false |
| blockScroll | 是否在打开模态框时禁用背景滚动 | boolean | true |
| keyboard | 是否支持键盘 Esc 关闭。Esc 监听在弹窗主体上,焦点离开弹窗(如 mask: false 时点击背景)后不再响应 | boolean | true |
| mask | 是否展示遮罩 | boolean | true |
| maskClosable | 点击蒙层是否允许关闭 | boolean | true |
| maskClass | 自定义蒙层类名 | string | undefined |
| maskStyle | 自定义蒙层样式 | CSSProperties | {} |
| wrapClass | 自定义外层容器(.modal-wrap)类名,多实例同时打开时以栈顶为准 | string | undefined |
| wrapStyle | 自定义外层容器(.modal-wrap)样式,多实例同时打开时以栈顶为准 | CSSProperties | {} |
| containerClass | 自定义弹窗定位层(.modal-container)类名,用于覆盖 width / top / zIndex 等定位表现 | string | undefined |
| containerStyle | 自定义弹窗定位层(.modal-container)样式,优先级高于 width / top / zIndex 等内置样式;卡片外观(背景 / 圆角 / 阴影)请用 bodyClass / bodyStyle | CSSProperties | {} |
| zIndex | 模态框层级,遮罩取该值,弹窗取该值 + 10 | number | 1000 |
| autoFocusButton | 打开时自动聚焦的按钮;Esc 监听绑定在弹窗主体上,需聚焦到弹窗内才响应 | 'ok' | 'cancel' | 'ok' |
| focusTriggerAfterClose | 关闭后是否将焦点归还给触发元素 | boolean | true |
| modalRender | 自定义渲染弹窗内容,常用于包裹拖拽逻辑;与 #modalRender 插槽等价,该属性优先级更高 | (arg: { originVNode: VNode }) => VNode | undefined |
| afterClose | 完全关闭(离场动画结束)后的回调 | () => void | undefined |
| onEsc | 按下 Esc 键的回调,无论是否允许关闭都会触发 | (e: KeyboardEvent) => void | undefined |
| onMaskClick | 点击遮罩的回调,无论是否允许关闭都会触发 | (e: MouseEvent) => void | undefined |
| to | 容器 Teleport 的目标 | string | HTMLElement | 'body' |
| open | (v-model) 模态框是否可见,声明式用法下生效 | boolean | false |
多实例同时打开时,各实例按自身
zIndex分层(遮罩取zIndex,弹窗取zIndex + 10);外层容器的层级取栈中打开实例的最大zIndex,wrapClass/wrapStyle以栈顶为准。
ModalOptions Type
调用时传入的 ModalOptions 类型(info / success / error / warning / confirm / erase / create 的参数),以下属性均具有更高优先级(覆盖组件级配置)
| 名称 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| width? | 模态框宽度,单位 px | string | number | undefined |
| height? | 内容区高度,单位 px,指定后内容区内部滚动 | string | number | undefined |
| icon? | 自定义图标 | VNode | (() => VNode) | undefined |
| title? | 模态框标题 | string | VNode | (() => VNode) | undefined |
| titleClass? | 自定义标题类名 | string | undefined |
| titleStyle? | 自定义标题样式 | CSSProperties | undefined |
| content? | 模态框内容 | string | VNode | (() => VNode) | undefined |
| contentClass? | 自定义内容类名 | string | undefined |
| contentStyle? | 自定义内容样式 | CSSProperties | undefined |
| scrollbarProps? | 内容滚动条 Scrollbar 属性配置 | ScrollbarProps | undefined |
| bodyClass? | 自定义弹窗卡片(.modal-body-wrap)类名,用于定制背景 / 圆角 / 阴影等外观 | string | undefined |
| bodyStyle? | 自定义弹窗卡片(.modal-body-wrap)样式,用于定制背景 / 圆角 / 阴影等外观 | CSSProperties | undefined |
| showCancel? | 是否显示取消按钮,仅 confirm | erase 双按钮形态生效,默认 true;info 等单按钮形态与 create 完全自定义形态不生效 | boolean | undefined |
| cancelText? | 取消按钮文字 | string | undefined |
| cancelProps? | 取消按钮 props 配置,参考 Button Props | ButtonProps | undefined |
| okText? | 确认按钮文字 | string | undefined |
| okType? | 确认按钮类型 | 'default' | 'reverse' | 'primary' | 'danger' | 'dashed' | 'text' | 'link' | undefined |
| okProps? | 确认按钮 props 配置,优先级高于 okType,参考 Button Props | ButtonProps | undefined |
| noticeText? | 通知按钮文字 | string | undefined |
| noticeProps? | 通知按钮 props 配置,参考 Button Props | ButtonProps | undefined |
| footer? | 底部区域,false 隐藏,函数则完全自定义;无内置按钮组时(create 调用)即便为 true 也不渲染空白区域 | boolean | (() => VNode) | undefined |
| closable? | 是否显示右上角关闭按钮,默认 false,需要时显式开启 | boolean | undefined |
| closeIcon? | 自定义关闭图标 | VNode | (() => VNode) | undefined |
| closeFocusable? | 关闭按钮是否可聚焦,设为 false 后不参与 Tab 序列 | boolean | undefined |
| destroyOnClose? | 关闭时是否销毁 Modal 里的子元素,命令式默认 true | boolean | undefined |
| centered? | 是否水平垂直居中,否则固定高度水平居中 | boolean | undefined |
| top? | 固定高度水平居中时,距顶部高度,仅当 centered: false 时生效,单位 px | string | number | undefined |
| transformOrigin? | 模态框动画出现的位置 | 'mouse' | 'center' | undefined |
| blockScroll? | 是否在打开模态框时禁用背景滚动 | boolean | undefined |
| keyboard? | 是否支持键盘 esc 关闭 | boolean | undefined |
| mask? | 是否展示遮罩 | boolean | undefined |
| maskClosable? | 点击蒙层是否允许关闭,命令式调用默认 false(避免误触关闭),需要时显式传 true | boolean | undefined |
| maskClass? | 自定义蒙层类名 | string | undefined |
| maskStyle? | 自定义蒙层样式 | CSSProperties | undefined |
| wrapClass? | 自定义外层容器(.modal-wrap)类名,多实例同时打开时以栈顶为准 | string | undefined |
| wrapStyle? | 自定义外层容器(.modal-wrap)样式,多实例同时打开时以栈顶为准 | CSSProperties | undefined |
| containerClass? | 自定义弹窗定位层(.modal-container)类名,用于覆盖 width / top / zIndex 等定位表现 | string | undefined |
| containerStyle? | 自定义弹窗定位层(.modal-container)样式,优先级高于 width / top / zIndex 等内置样式;卡片外观(背景 / 圆角 / 阴影)请用 bodyClass / bodyStyle | CSSProperties | undefined |
| zIndex? | 模态框层级,遮罩取该值,弹窗取该值 + 10 | number | undefined |
| autoFocusButton? | 打开时自动聚焦的按钮 | 'ok' | 'cancel' | undefined |
| focusTriggerAfterClose? | 关闭后是否将焦点归还给触发元素 | boolean | undefined |
| modalRender? | 自定义渲染弹窗内容,常用于包裹拖拽逻辑 | (arg: { originVNode: VNode }) => VNode | undefined |
| afterClose? | 完全关闭(离场动画结束)后的回调 | () => void | undefined |
| onKnow? | 点击知道了按钮的回调,返回 false 或 Promise reject 时阻止关闭 | () => unknown | Promise<unknown> | undefined |
| onOk? | 点击确认按钮的回调,返回 false 或 Promise reject 时阻止关闭 | () => unknown | Promise<unknown> | undefined |
| onCancel? | 点击遮罩层、Esc 键或取消按钮的回调,返回 false 或 Promise reject 时阻止关闭 | () => unknown | Promise<unknown> | undefined |
| onEsc? | 按下 Esc 键的回调,无论是否允许关闭都会触发 | (e: KeyboardEvent) => void | undefined |
| onMaskClick? | 点击遮罩的回调,无论是否允许关闭都会触发 | (e: MouseEvent) => void | undefined |
ModalUpdate Type
update 可更新的字段为 ModalOptions 的全部属性,另支持 mode 用于切换弹窗类型,进而决定内置图标与按钮组。
注意:若该弹窗设置了自定义
icon,图标不随mode切换。
| 名称 | 说明 | 类型 |
|---|---|---|
| mode? | 弹窗类型,决定内置图标与按钮组(仅 update 可传此字段) | 'info' | 'success' | 'error' | 'warning' | 'confirm' | 'erase' | 'custom' |
| loading? | 手动控制按钮 loading,供外部异步流程驱动 | boolean |
Slots
| 名称 | 说明 | 类型 |
|---|---|---|
| icon | 自定义图标 | v-slot:icon |
| title | 自定义模态框标题 | v-slot:title |
| default | 自定义模态框内容 | v-slot:default |
| footer | 自定义底部按钮区 | v-slot:footer |
| closeIcon | 自定义关闭图标 | v-slot:closeIcon |
| modalRender | 自定义渲染弹窗内容 | v-slot:modalRender |
modalRender为作用域插槽,可接收{ originVNode }对弹窗内容做包裹式自定义渲染(常用于拖拽等场景),仅声明式用法下生效;命令式用法请使用同名的modalRender属性。当属性与该插槽同时配置时,属性优先级更高。
Methods
useModal() 返回的 ModalApi,或通过 <Modal> / <ModalProvider> 的 @ready 事件获取:
| 名称 | 说明 | 类型 |
|---|---|---|
| info | 信息提示模态框 | (data: ModalOptions) => ModalReactive |
| success | 成功提示模态框 | (data: ModalOptions) => ModalReactive |
| error | 错误提示模态框 | (data: ModalOptions) => ModalReactive |
| warning | 警告提示模态框 | (data: ModalOptions) => ModalReactive |
| confirm | 确认提示模态框 | (data: ModalOptions) => ModalReactive |
| erase | 删除提示模态框 | (data: ModalOptions) => ModalReactive |
| create | 完全自定义模态框(不渲染内置图标与按钮组,顶部图标与底部区域由 icon / footer 自行组合) | (data: ModalOptions) => ModalReactive |
| destroyAll | 关闭并销毁所有模态框,逐实例走正常关闭流程以保留离场动画 | () => void |
ModalReactive Type
单个模态框的句柄,由 info / success 等方法调用后返回:
| 名称 | 说明 | 类型 |
|---|---|---|
| key | 该模态框的唯一标识(只读) | string |
| destroy | 关闭该模态框 | () => void |
| update | 更新该模态框,mode 可切换弹窗类型与内置图标 | (options: ModalUpdate) => void |
| show | 重新打开该模态框,实例已销毁时调用无效 | () => void |
Events
cancel / ok / know / change / ready 为 <Modal> 与 <ModalProvider> 组件的事件(需通过组件标签监听);使用 useModal() 时,请在调用参数中使用 onOk / onCancel / onKnow 等回调。
| 名称 | 说明 | 类型 |
|---|---|---|
| ready | 实例挂载完成时触发,参数为该实例的 api | (api: ModalApi) => void |
| cancel | 点击蒙层或 Esc 键或取消按钮的回调 | (e: Event) => void |
| ok | 点击确定按钮的回调 | (e: MouseEvent) => void |
| know | 点击知道了按钮的回调 | (e: MouseEvent) => void |
| change | 任一弹窗打开 / 关闭时触发,多实例下携带该实例 key | (open: boolean, key: string) => void |
| update:open | 声明式用法下 v-model:open 对应的更新事件 | (open: boolean) => void |
在 setup 外使用
选择 1:createDiscreteApi()(脱离组件树)
适用于 axios 拦截器、路由守卫、Pinia action 等任意位置:内部会创建一个独立的应用实例,因此可在任意位置调用,无需外层 ModalProvider
注意
- 主题使用内置默认值;如需自定义,通过第二个参数
configProviderProps(支持Ref/computed响应式)显式传入,详见 全局化配置 ConfigProvider「主题同步到离散 API」 - 每次调用都会创建一套独立实例(独立的容器与弹窗栈),建议缓存返回值复用,避免重复创建;不再使用时可通过返回的
dispose()销毁该实例 - 内部会访问
document,SSR场景请在客户端(点击回调、onMounted等)中调用 - 不建议与
useModal()在同一 App 中混用:两者各自持有独立的弹窗栈与挂载容器,zIndex层级互不感知
独立实例的复用与销毁(dispose)
独立实例创建后不会随组件卸载自动销毁,也不会因内部弹窗全部关闭而回收,需根据使用场景决定是否手动 dispose():
场景 A · 全局单例,缓存复用(推荐):适用于 axios 拦截器、路由守卫、Pinia action 等常驻场景,整个应用生命周期内复用同一实例:
import { createDiscreteApi } from 'vue-amazing-ui'
// 在模块顶层创建一次,全局复用
const { modal } = createDiscreteApi(['modal'])
modal.info({ title: 'Modal Title', content: '这是一个弹窗' })场景 B · 临时使用后手动 dispose():适用于测试用例、单次任务等按需回收场景,用完即销毁:
import { createDiscreteApi } from 'vue-amazing-ui'
import type { DiscreteApiInstance } from 'vue-amazing-ui'
let discrete: DiscreteApiInstance<'modal'> | null = null
function getModalApi() {
if (!discrete) {
discrete = createDiscreteApi(['modal'])
}
return discrete.modal
}
getModalApi().info({ title: 'Modal Title', content: '这是一个弹窗' })
// 不再需要时手动销毁:卸载内部应用并移除挂载容器
// 重复调用无副作用(内部有 disposed 保护);再次调用 getModalApi() 会安全重建
discrete?.dispose()
discrete = nullXXX.ts(任意 .ts 文件)
import { createDiscreteApi } from 'vue-amazing-ui'
// 任意位置调用,无需外层 ModalProvider
const { modal } = createDiscreteApi(['modal'])
// 例:路由守卫中离开页面前二次确认
router.beforeEach((to, from, next) => {
if (to.meta.needConfirm) {
modal.confirm({
title: '离开当前页面',
content: '存在未保存的修改,确认离开?',
onOk: () => next(),
onCancel: () => next(false)
})
return
}
next()
})选择 2:挂载到 window(复用组件树内实例)
注意
如果你想在 setup 外使用 modal,需要在顶层 setup 中把 useModal() 返回的实例挂载到 window 下然后再调用,调用前需要确保实例已经挂载成功。
App.vue
<script setup lang="ts">
import { ModalProvider } from 'vue-amazing-ui'
</script>
<template>
<ModalProvider>
<Content />
</ModalProvider>
</template>content.vue(<ModalProvider> 内的顶层组件)
<script setup lang="ts">
import { useModal } from 'vue-amazing-ui'
// 挂载到 window 后,即可在任意非组件环境(工具函数、事件监听等)中调用
window.$modal = useModal()
</script>XXX.ts(任意 .ts 文件)
// 需确保已在顶层 setup 中执行了 window.$modal = useModal()
window.$modal?.confirm({
title: '确认操作',
content: '确定要执行该操作吗?'
})可选:为 window.$modal 补充 TypeScript 类型声明
// types/global.d.ts
import type { ModalApi } from 'vue-amazing-ui'
declare global {
interface Window {
$modal?: ModalApi
}
}