Skip to content

模态框 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() 取得同一实例

关于 ModalProviderModal

  • 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

vue
<script setup lang="ts">
import { ModalProvider } from 'vue-amazing-ui'
</script>
<template>
  <ModalProvider>
    <RouterView />
  </ModalProvider>
</template>

2. 在任意层级组件中调用 useModal()

XXX.vue

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

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: falsemaskClosable: true
  • 内容来源:命令式通过 title / content 等参数传入;声明式除参数外,还可用 #icon / #title / #default / #footer / #closeIcon 插槽自定义

本文档网站已在主题层全局包裹 <ModalProvider>,以下演示均通过 useModal() 获取实例(与真实项目用法一致);需要不同组件级配置的演示,均通过页面内局部嵌套 <ModalProvider> 实现。

基本使用

共有六种内置形态:info / success / error / warning 为单按钮(知道了),confirm / erase 为双按钮(取消 + 确定)


Info
Success
Error
Warning
Confirm
Erase
Show Code
vue
<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
vue
<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,关闭后保留内容。下面分三组演示内容保留、预渲染与关闭回调的差异


1
destroyOnClose:内容是否保留

两个弹窗都含 Switch 开关:打开开关 → 关闭 → 重新打开,对比开关状态是否保留。

草稿弹窗(保留内容)
false

实例保留,重复打开复用同一实例,内部状态持久化

已打开
0
一次性弹窗(关闭销毁)
true

实例销毁,每次打开都创建全新实例,状态重置

已打开
0
2
renderBeforeOpen:预渲染

弹窗内容的渲染时长实时同步到下方:懒渲染首次打开前为「未渲染」,预渲染打开前已在计时

懒渲染(默认)
false

首次打开时才渲染内容

渲染状态
未渲染
预渲染
true

随页面一起渲染,打开即可见已有计时

渲染状态
未渲染
3
afterClose:关闭后回调

点击「知道了」或按 Esc 关闭均可触发,回调在关闭动画播放结束后才执行。

打开弹窗

适合做资源清理或路由跳转

触发次数
0
最近触发
Show Code
vue
<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
vue
<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
vue
<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>

自定义图标

iconcloseIcon 属性支持 VNode / 渲染函数(命令式与声明式一致);插槽形态仅声明式用法下可用,通过 #icon / #closeIcon 插槽接管默认图标


VNode 图标
渲染函数图标
声明式渲染函数图标
Show Code
vue
<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
vue
<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
vue
<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
vue
<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
vue
<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 序列


显示关闭按钮
自定义关闭图标
关闭按钮不参与 Tab 序列
Show Code
vue
<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
vue
<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
vue
<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 返回 falsePromise reject 时阻止关闭,其余情况(含 Promise resolve)自动关闭;返回 Promise 期间按钮保持 loading


异步关闭(自动)
阻止关闭(失败)
阻止关闭(同步)
阻止取消
Show Code
vue
<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
vue
<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() 关闭并销毁全部


开启 3 层弹窗
开启 3 层并 2 秒后全部销毁
Show Code
vue
<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: falsekeyboard: trueblockScroll: trueonMaskClick / onEsc 无论是否允许关闭都会触发


无遮罩
点击遮罩关闭
禁用 Esc 关闭
遮罩与 Esc 回调
锁定背景滚动
不锁定滚动
Show Code
vue
<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
vue
<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
vue
<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
vue
<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


组件级配置属性:使用 useModal() 时设置在 <ModalProvider> 上(会透传给内部 Modal),直接使用 <Modal> 组件时设置在 <Modal> 上,两者等价。


每次调用的个性化配置请参考 ModalOptions Type

参数说明类型默认值
width模态框宽度,单位 pxstring | number420
height内容区高度,单位 px,默认自适应内容高度;指定后内容区内部滚动string | number'auto'
icon自定义图标,prop 支持 VNode / 渲染函数;插槽形态请用同名 #icon 插槽VNode | (() => VNode)undefined
title模态框标题string | VNode | (() => VNode)undefined
titleClass自定义标题类名stringundefined
titleStyle自定义标题样式CSSProperties{}
content模态框内容string | VNode | (() => VNode)undefined
contentClass自定义内容类名stringundefined
contentStyle自定义内容样式CSSProperties{}
scrollbarProps内容滚动条 Scrollbar 属性配置ScrollbarProps{}
bodyClass自定义弹窗卡片(.modal-body-wrap)类名,用于定制背景 / 圆角 / 阴影等外观stringundefined
bodyStyle自定义弹窗卡片(.modal-body-wrap)样式,用于定制背景 / 圆角 / 阴影等外观CSSProperties{}
cancelText取消按钮文字string'取消'
cancelProps取消按钮 props 配置,参考 Button PropsButtonProps{}
okText确认按钮文字string'确定'
okType确认按钮类型'default' | 'reverse' | 'primary' | 'danger' | 'dashed' | 'text' | 'link''primary'
okProps确认按钮 props 配置,优先级高于 okType,参考 Button PropsButtonProps{}
noticeText通知按钮文字string'知道了'
noticeProps通知按钮 props 配置,参考 Button PropsButtonProps{}
footer是否显示底部按钮区:false 隐藏,true 渲染内置按钮组,传函数则完全自定义;插槽形态请用 #footerboolean | (() => VNode)true
closable是否显示右上角关闭按钮,默认 false,需要时显式开启booleanfalse
closeIcon自定义关闭图标,prop 支持 VNode / 渲染函数;插槽形态请用同名 #closeIcon 插槽VNode | (() => VNode)undefined
closeFocusable关闭按钮是否可聚焦,设为 false 后关闭按钮 tabindex-1,不参与 Tab 序列booleantrue
destroyOnClose关闭时是否销毁 Modal 里的子元素,实例栈下关闭即从栈中移除,内容随之销毁booleanfalse
renderBeforeOpen首次打开前是否渲染内容(关闭懒渲染)booleanfalse
centered是否水平垂直居中,否则固定高度水平居中booleanfalse
top固定高度水平居中时,距顶部高度,仅当 centered: false 时生效,单位 pxstring | number100
transformOrigin模态框动画出现的位置'mouse' | 'center''mouse'
confirmLoading确定按钮 loadingbooleanfalse
blockScroll是否在打开模态框时禁用背景滚动booleantrue
keyboard是否支持键盘 Esc 关闭。Esc 监听在弹窗主体上,焦点离开弹窗(如 mask: false 时点击背景)后不再响应booleantrue
mask是否展示遮罩booleantrue
maskClosable点击蒙层是否允许关闭booleantrue
maskClass自定义蒙层类名stringundefined
maskStyle自定义蒙层样式CSSProperties{}
wrapClass自定义外层容器(.modal-wrap)类名,多实例同时打开时以栈顶为准stringundefined
wrapStyle自定义外层容器(.modal-wrap)样式,多实例同时打开时以栈顶为准CSSProperties{}
containerClass自定义弹窗定位层(.modal-container)类名,用于覆盖 width / top / zIndex 等定位表现stringundefined
containerStyle自定义弹窗定位层(.modal-container)样式,优先级高于 width / top / zIndex 等内置样式;卡片外观(背景 / 圆角 / 阴影)请用 bodyClass / bodyStyleCSSProperties{}
zIndex模态框层级,遮罩取该值,弹窗取该值 + 10number1000
autoFocusButton打开时自动聚焦的按钮;Esc 监听绑定在弹窗主体上,需聚焦到弹窗内才响应'ok' | 'cancel''ok'
focusTriggerAfterClose关闭后是否将焦点归还给触发元素booleantrue
modalRender自定义渲染弹窗内容,常用于包裹拖拽逻辑;与 #modalRender 插槽等价,该属性优先级更高(arg: { originVNode: VNode }) => VNodeundefined
afterClose完全关闭(离场动画结束)后的回调() => voidundefined
onEsc按下 Esc 键的回调,无论是否允许关闭都会触发(e: KeyboardEvent) => voidundefined
onMaskClick点击遮罩的回调,无论是否允许关闭都会触发(e: MouseEvent) => voidundefined
to容器 Teleport 的目标string | HTMLElement'body'
open(v-model) 模态框是否可见,声明式用法下生效booleanfalse

多实例同时打开时,各实例按自身 zIndex 分层(遮罩取 zIndex,弹窗取 zIndex + 10);外层容器的层级取栈中打开实例的最大 zIndexwrapClass / wrapStyle 以栈顶为准。

ModalOptions Type


调用时传入的 ModalOptions 类型(info / success / error / warning / confirm / erase / create 的参数),以下属性均具有更高优先级(覆盖组件级配置)

名称说明类型默认值
width?模态框宽度,单位 pxstring | numberundefined
height?内容区高度,单位 px,指定后内容区内部滚动string | numberundefined
icon?自定义图标VNode | (() => VNode)undefined
title?模态框标题string | VNode | (() => VNode)undefined
titleClass?自定义标题类名stringundefined
titleStyle?自定义标题样式CSSPropertiesundefined
content?模态框内容string | VNode | (() => VNode)undefined
contentClass?自定义内容类名stringundefined
contentStyle?自定义内容样式CSSPropertiesundefined
scrollbarProps?内容滚动条 Scrollbar 属性配置ScrollbarPropsundefined
bodyClass?自定义弹窗卡片(.modal-body-wrap)类名,用于定制背景 / 圆角 / 阴影等外观stringundefined
bodyStyle?自定义弹窗卡片(.modal-body-wrap)样式,用于定制背景 / 圆角 / 阴影等外观CSSPropertiesundefined
showCancel?是否显示取消按钮,仅 confirm | erase 双按钮形态生效,默认 trueinfo 等单按钮形态与 create 完全自定义形态不生效booleanundefined
cancelText?取消按钮文字stringundefined
cancelProps?取消按钮 props 配置,参考 Button PropsButtonPropsundefined
okText?确认按钮文字stringundefined
okType?确认按钮类型'default' | 'reverse' | 'primary' | 'danger' | 'dashed' | 'text' | 'link'undefined
okProps?确认按钮 props 配置,优先级高于 okType,参考 Button PropsButtonPropsundefined
noticeText?通知按钮文字stringundefined
noticeProps?通知按钮 props 配置,参考 Button PropsButtonPropsundefined
footer?底部区域,false 隐藏,函数则完全自定义;无内置按钮组时(create 调用)即便为 true 也不渲染空白区域boolean | (() => VNode)undefined
closable?是否显示右上角关闭按钮,默认 false,需要时显式开启booleanundefined
closeIcon?自定义关闭图标VNode | (() => VNode)undefined
closeFocusable?关闭按钮是否可聚焦,设为 false 后不参与 Tab 序列booleanundefined
destroyOnClose?关闭时是否销毁 Modal 里的子元素,命令式默认 truebooleanundefined
centered?是否水平垂直居中,否则固定高度水平居中booleanundefined
top?固定高度水平居中时,距顶部高度,仅当 centered: false 时生效,单位 pxstring | numberundefined
transformOrigin?模态框动画出现的位置'mouse' | 'center'undefined
blockScroll?是否在打开模态框时禁用背景滚动booleanundefined
keyboard?是否支持键盘 esc 关闭booleanundefined
mask?是否展示遮罩booleanundefined
maskClosable?点击蒙层是否允许关闭,命令式调用默认 false(避免误触关闭),需要时显式传 truebooleanundefined
maskClass?自定义蒙层类名stringundefined
maskStyle?自定义蒙层样式CSSPropertiesundefined
wrapClass?自定义外层容器(.modal-wrap)类名,多实例同时打开时以栈顶为准stringundefined
wrapStyle?自定义外层容器(.modal-wrap)样式,多实例同时打开时以栈顶为准CSSPropertiesundefined
containerClass?自定义弹窗定位层(.modal-container)类名,用于覆盖 width / top / zIndex 等定位表现stringundefined
containerStyle?自定义弹窗定位层(.modal-container)样式,优先级高于 width / top / zIndex 等内置样式;卡片外观(背景 / 圆角 / 阴影)请用 bodyClass / bodyStyleCSSPropertiesundefined
zIndex?模态框层级,遮罩取该值,弹窗取该值 + 10numberundefined
autoFocusButton?打开时自动聚焦的按钮'ok' | 'cancel'undefined
focusTriggerAfterClose?关闭后是否将焦点归还给触发元素booleanundefined
modalRender?自定义渲染弹窗内容,常用于包裹拖拽逻辑(arg: { originVNode: VNode }) => VNodeundefined
afterClose?完全关闭(离场动画结束)后的回调() => voidundefined
onKnow?点击知道了按钮的回调,返回 falsePromise reject 时阻止关闭() => unknown | Promise<unknown>undefined
onOk?点击确认按钮的回调,返回 falsePromise reject 时阻止关闭() => unknown | Promise<unknown>undefined
onCancel?点击遮罩层、Esc 键或取消按钮的回调,返回 falsePromise reject 时阻止关闭() => unknown | Promise<unknown>undefined
onEsc?按下 Esc 键的回调,无论是否允许关闭都会触发(e: KeyboardEvent) => voidundefined
onMaskClick?点击遮罩的回调,无论是否允许关闭都会触发(e: MouseEvent) => voidundefined

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() 销毁该实例
  • 内部会访问 documentSSR 场景请在客户端(点击回调、onMounted 等)中调用
  • 不建议与 useModal() 在同一 App 中混用:两者各自持有独立的弹窗栈与挂载容器,zIndex 层级互不感知

独立实例的复用与销毁(dispose)

独立实例创建后不会随组件卸载自动销毁,也不会因内部弹窗全部关闭而回收,需根据使用场景决定是否手动 dispose()

场景 A · 全局单例,缓存复用(推荐):适用于 axios 拦截器、路由守卫、Pinia action 等常驻场景,整个应用生命周期内复用同一实例:

ts
import { createDiscreteApi } from 'vue-amazing-ui'

// 在模块顶层创建一次,全局复用
const { modal } = createDiscreteApi(['modal'])

modal.info({ title: 'Modal Title', content: '这是一个弹窗' })

场景 B · 临时使用后手动 dispose():适用于测试用例、单次任务等按需回收场景,用完即销毁:

ts
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 = null

XXX.ts(任意 .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()
})
Discrete Modal(脱离组件树调用)

选择 2:挂载到 window(复用组件树内实例)

注意

如果你想在 setup 外使用 modal,需要在顶层 setup 中把 useModal() 返回的实例挂载到 window 下然后再调用,调用前需要确保实例已经挂载成功。

App.vue

vue
<script setup lang="ts">
import { ModalProvider } from 'vue-amazing-ui'
</script>
<template>
  <ModalProvider>
    <Content />
  </ModalProvider>
</template>

content.vue(<ModalProvider> 内的顶层组件)

vue
<script setup lang="ts">
import { useModal } from 'vue-amazing-ui'

// 挂载到 window 后,即可在任意非组件环境(工具函数、事件监听等)中调用
window.$modal = useModal()
</script>

XXX.ts(任意 .ts 文件)

ts
// 需确保已在顶层 setup 中执行了 window.$modal = useModal()
window.$modal?.confirm({
  title: '确认操作',
  content: '确定要执行该操作吗?'
})

可选:为 window.$modal 补充 TypeScript 类型声明

ts
// types/global.d.ts
import type { ModalApi } from 'vue-amazing-ui'

declare global {
  interface Window {
    $modal?: ModalApi
  }
}

Released under the MIT License.