Popconfirm
简介
MPopconfirm 是一个气泡式确认组件,在用户执行危险或不可逆操作前弹出确认气泡。支持同步与异步确认回调,并完整透传 UPopover 的所有定位参数。
用法
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 接收 ButtonProps 或 false,传 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,支持 title、description、footer、content 等:
<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,支持 top、bottom、left、right 四个方向:
<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>
事件回调
同步确认、取消、异步确认与异常依次触发 confirm、cancel 与 error:
暂无事件记录
<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标题前展示的图标名称。 |
confirmButton | OmitByKey<ButtonProps, "loading" | LinkPropsKeys>透传给确认按钮的属性。
| |
cancelButton | true | boolean | ButtonProps透传给取消按钮的属性。
传入 |
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. |
portal | true | string | false | true | HTMLElementRender the popover in a portal. |
reference | Element | VirtualElementThe reference (or anchor) element that is being referred to for positioning. If not provided will use the current component as anchor. | |
openDelay | numberThe duration from when the mouse enters the trigger until the hover card opens. | |
closeDelay | numberThe duration from when the mouse leaves the trigger or content until the hover card closes. | |
arrow | true | boolean气泡内容与触发器之间的箭头指示。 |
dismissible | false | boolean当 |
modal | false | booleanThe 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
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'
}
}
}
}
}
})