Popconfirm

View source
基于气泡弹出层的操作确认组件。

简介

MPopconfirm 是一个气泡式确认组件,在用户执行危险或不可逆操作前弹出确认气泡。支持同步与异步确认回调,并完整透传 UPopover 的所有定位参数。

基于 Nuxt UI 的 Popover 组件封装

用法

default 插槽放置触发元素,点击后弹出确认气泡:

<template>
  <MPopconfirm title="确认删除?" description="删除后数据将无法恢复。">
    <UButton label="删除记录" color="neutral" variant="outline" icon="i-lucide-trash" />
  </MPopconfirm>
</template>

type 类型

type 影响默认图标与确认按钮颜色,包含六种语义化值:

<template>
  <MPopconfirm type="warning">
    <UButton label="打开" color="neutral" variant="soft" />
  </MPopconfirm>
</template>

icon 图标

icon 覆盖 type 默认图标,传入任意 Iconify 图标名称:

<template>
  <MPopconfirm type="error" icon="i-lucide-trash-2">
    <UButton label="危险操作" color="error" variant="soft" icon="i-lucide-alert-triangle" />
  </MPopconfirm>
</template>

confirmButton 确认按钮

confirmButton 接收完整 ButtonProps,可定制 label、color、icon、variant:

<template>
  <MPopconfirm :confirm-button="{ color: 'error', label: '确认删除', icon: 'i-lucide-trash-2' }">
    <UButton label="删除记录" color="error" variant="soft" />
  </MPopconfirm>
</template>

cancelButton 取消按钮

cancelButton 接收 ButtonPropsfalse,传 false 时隐藏取消按钮以强制确认:

<template>
  <MPopconfirm :cancel-button="false">
    <UButton label="强制执行" color="warning" variant="soft" />
  </MPopconfirm>
</template>

dismissible 关闭策略

dismissible 默认为 false 严格模式;开启后允许点击遮罩或按 Esc 关闭气泡:

<template>
  <MPopconfirm>
    <UButton label="关闭策略" color="neutral" variant="outline" />
  </MPopconfirm>
</template>

arrow 箭头

arrow 控制气泡指向触发器的箭头,默认开启:

<template>
  <MPopconfirm>
    <UButton label="箭头开关" color="neutral" variant="subtle" />
  </MPopconfirm>
</template>

示例

异步确认

:on-confirm 支持返回 Promise,期间确认按钮自动进入 loading 状态,成功后自动关闭弹层:

<script setup lang="ts">
const result = ref('')

async function handleSubmit() {
  await new Promise((resolve) => setTimeout(resolve, 1500))
  result.value = '提交成功'
}
</script>

<template>
  <div class="flex flex-wrap items-center gap-3">
    <MPopconfirm
      type="primary"
      title="确认提交?"
      description="提交后将立即生效,请确认操作。"
      :on-confirm="handleSubmit"
    >
      <UButton color="primary" label="提交申请" icon="i-lucide-send" />
    </MPopconfirm>

    <span v-if="result" class="text-sm text-success">{{ result }}</span>
  </div>
</template>

正文插槽

使用 body 插槽插入任意内容,description 传空字符串可隐藏默认描述区:

<script setup lang="ts">
const result = ref('')
</script>

<template>
  <div class="flex flex-wrap items-center gap-3">
    <MPopconfirm
      title="删除用户"
      :description="''"
      :on-confirm="
        () => {
          result = '用户已删除'
        }
      "
    >
      <template #body>
        <div class="flex flex-col gap-2 py-1">
          <p class="text-sm text-muted">即将删除以下用户,操作不可撤销:</p>
          <div class="flex items-center gap-2 rounded-md bg-elevated px-3 py-2">
            <UAvatar size="xs" icon="i-lucide-user" />
            <div class="flex flex-col">
              <span class="text-xs font-medium text-highlighted">张三</span>
              <span class="text-xs text-muted">zhangsan@example.com</span>
            </div>
          </div>
        </div>
      </template>

      <UButton color="error" variant="soft" label="删除用户" icon="i-lucide-user-x" />
    </MPopconfirm>

    <span v-if="result" class="text-sm text-error">{{ result }}</span>
  </div>
</template>

样式定制

通过 ui 属性覆盖内部各区块的 class,支持 titledescriptionfootercontent 等:

<script setup lang="ts">
const result = ref('')
</script>

<template>
  <div class="flex flex-wrap items-center gap-3">
    <MPopconfirm
      type="info"
      title="样式定制示例"
      description="通过 ui 属性覆盖内部各区块的 class,实现精细样式控制。"
      :on-confirm="() => { result = '已确认' }"
      :ui="{
        content: 'w-72',
        title: 'text-info text-base',
        description: 'text-xs text-info/70',
        footer: 'justify-between pt-2 border-t border-default'
      }"
      :confirm-button="{ label: '好的,执行', color: 'info' }"
      :cancel-button="{ label: '不了,取消', variant: 'ghost' }"
    >
      <UButton color="info" variant="soft" label="打开定制气泡" icon="i-lucide-palette" />
    </MPopconfirm>

    <span v-if="result" class="text-sm text-muted">{{ result }}</span>
  </div>
</template>

弹出方向

透传 content 属性给底层 UPopover,支持 topbottomleftright 四个方向:

<script setup lang="ts">
type Side = 'top' | 'bottom' | 'left' | 'right'

const sides: Side[] = ['top', 'bottom', 'left', 'right']
const result = ref('')
</script>

<template>
  <div class="flex flex-wrap items-center gap-2">
    <MPopconfirm
      v-for="side in sides"
      :key="side"
      :content="{ side }"
      :title="`从 ${side} 方弹出`"
      description="透传 Popover 定位参数,支持 top / bottom / left / right。"
      :on-confirm="() => { result = `已从 ${side} 方确认` }"
    >
      <UButton color="neutral" variant="outline" size="sm" :label="side" />
    </MPopconfirm>

    <span v-if="result" class="ml-2 text-sm text-muted">{{ result }}</span>
  </div>
</template>

错误处理

onConfirm 回调抛出异常时,弹层保持打开并触发 @error 事件,可在此处展示错误反馈:

<script setup lang="ts">
const errorMsg = ref('')

const toast = useToast()

async function failingAction() {
  await new Promise(resolve => setTimeout(resolve, 800))
  throw new Error('服务器返回错误:操作被拒绝')
}

function handleError(err: unknown) {
  errorMsg.value = err instanceof Error ? err.message : '未知错误'
  toast.add({
    color: 'error',
    title: '操作失败',
    description: errorMsg.value
  })
}
</script>

<template>
  <div class="flex flex-col gap-3">
    <div class="flex flex-wrap items-center gap-3">
      <MPopconfirm
        type="error"
        title="模拟操作失败"
        description="确认后将触发一个失败的异步操作,弹层保持打开并上报错误。"
        :on-confirm="failingAction"
        @error="handleError"
      >
        <UButton color="error" variant="soft" label="触发失败操作" icon="i-lucide-zap-off" />
      </MPopconfirm>
    </div>
  </div>
</template>

事件回调

同步确认、取消、异步确认与异常依次触发 confirmcancelerror

暂无事件记录

<script setup lang="ts">
const log = ref<string[]>([])

function record(msg: string) {
  log.value = [`[${new Date().toLocaleTimeString()}] ${msg}`, ...log.value].slice(0, 8)
}

async function asyncConfirm() {
  await new Promise(resolve => setTimeout(resolve, 1200))
  record('async confirmed')
}

function rejectedConfirm() {
  throw new Error('被拒绝')
}
</script>

<template>
  <div class="grid grid-cols-1 lg:grid-cols-[1fr_220px] gap-3">
    <div class="flex flex-wrap gap-2">
      <MPopconfirm
        type="warning"
        title="点击确认或取消"
        description="确认会触发 confirm 事件,取消会触发 cancel 事件。"
        :on-confirm="() => record('confirmed')"
        @cancel="record('cancelled')"
      >
        <UButton color="primary" variant="soft" label="同步事件" />
      </MPopconfirm>

      <MPopconfirm
        type="info"
        title="异步事件"
        description="确认按钮等待 onConfirm 异步完成后才触发 confirm 并关闭。"
        :on-confirm="asyncConfirm"
      >
        <UButton color="info" variant="soft" label="异步确认" />
      </MPopconfirm>

      <MPopconfirm
        type="error"
        title="抛错保留"
        description="onConfirm 抛出错误时弹层保持打开,并通过 error 事件暴露异常。"
        :on-confirm="rejectedConfirm"
        @error="(e: unknown) => record(`error: ${(e as Error).message}`)"
      >
        <UButton color="error" variant="outline" label="抛错保留" />
      </MPopconfirm>
    </div>

    <div class="rounded border border-default p-2 text-xs text-muted space-y-1 max-h-40 overflow-auto">
      <p v-if="!log.length">
        暂无事件记录
      </p>
      <p v-for="(item, idx) in log" :key="idx">
        {{ item }}
      </p>
    </div>
  </div>
</template>

API

Props

Prop Default Type
type'neutral'"error" | "primary" | "info" | "success" | "warning" | "neutral"

预设的语义化颜色主题,会影响图标。

title'确认操作'string

确认气泡的标题文本。

description'请确认是否执行此操作?'string

标题下方的补充说明。 传入空字符串时可隐藏描述区。

icon'i-lucide-circle-question-mark'any

标题前展示的图标名称。

confirmButtonOmitByKey<ButtonProps, "loading" | LinkPropsKeys>

透传给确认按钮的属性。 loading 状态由组件内部托管。

cancelButtontrueboolean | ButtonProps

透传给取消按钮的属性。 传入 false 可完全隐藏取消按钮。

mode'click'"click" | "hover"

The display mode of the popover.

content{ side: 'bottom', sideOffset: 8, collisionPadding: 8 }PopoverContentProps & Partial<EmitsToProps<PopoverContentImplEmits>>

The content of the popover.

portaltruestring | false | true | HTMLElement

Render the popover in a portal.

referenceElement | VirtualElement

The reference (or anchor) element that is being referred to for positioning.

If not provided will use the current component as anchor.

openDelaynumber

The duration from when the mouse enters the trigger until the hover card opens.

closeDelaynumber

The duration from when the mouse leaves the trigger or content until the hover card closes.

arrowtrueboolean

气泡内容与触发器之间的箭头指示。

dismissiblefalseboolean

false 时,点击遮罩层或按下 Esc 键将不会关闭弹层。

modalfalseboolean

The modality of the popover. When set to true, interaction with outside elements will be disabled and only popover content will be visible to screen readers.

ui{ content?: ClassNameValue; arrow?: ClassNameValue; header?: ClassNameValue; title?: ClassNameValue; description?: ClassNameValue; body?: ClassNameValue; footer?: ClassNameValue; icon?: ClassNameValue; }

Emits

Event Type
confirm[]
cancel[]
error[error: unknown]

Slots

Slot Type
default{ open: boolean; }
header{ close: () => void; }
title{ close: () => void; }
description{ close: () => void; }
actions{ close: () => void; }
body{ close: () => void; }
footer{ close: () => void; }

Theme

app.config.ts
export default defineAppConfig({
  ui: {
    popconfirm: {
      slots: {
        content: 'px-4 py-3 flex flex-col gap-2',
        arrow: '',
        header: 'flex flex-col gap-1.5',
        title: 'flex gap-2 items-center text-sm text-highlighted font-semibold',
        description: 'text-muted text-xs',
        body: '',
        footer: 'mt-1 flex items-center justify-end gap-1.5',
        icon: 'size-4 shrink-0'
      },
      variants: {
        type: {
          primary: {
            icon: 'text-primary'
          },
          info: {
            icon: 'text-info'
          },
          success: {
            icon: 'text-success'
          },
          warning: {
            icon: 'text-warning'
          },
          error: {
            icon: 'text-error'
          },
          neutral: {
            icon: 'text-muted'
          }
        }
      }
    }
  }
})

Changelog

No recent changes
Copyright © 2025 - 2026 YiXuan - MIT License