组件
确认对话框 Confirm Dialog
由 Dialog 和 Button 深度封装的开箱即用确认弹窗,专为危险操作、异步确认及高优先级决定设计。
基础用法
通过简洁的声明式属性即可快速完成“点击触发 -> 弹出确认弹窗 -> 确认执行”的闭环流程:
Loading…
安装与引入
通过 CLI 自动添加组件,或手动复制源码至项目中:
pnpm dlx @wui-design/cli@latest add @wui/confirm-dialogRegistry 依赖说明
confirm-dialog 声明了对 dialog 和 button 的组件依赖,CLI 会自动解析并一并安装所需的基础组件。
安装基础依赖与动效库
pnpm add radix-ui motion lucide-react clsx tailwind-merge复制组件源码到
components/confirm-dialog.tsx"use client"
import * as React from "react"
import { Loader2Icon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
export interface ConfirmDialogProps {
/** The element that opens the dialog. Rendered inside the trigger via `asChild`. */
trigger?: React.ReactNode
/** Controlled open state. */
open?: boolean
/** Initial open state in uncontrolled mode. @default false */
defaultOpen?: boolean
/** Callback fired when the open state changes. */
onOpenChange?: (open: boolean) => void
/** Heading shown at the top of the dialog. */
title: string
/** Optional supporting text under the title. */
description?: string
/** Optional custom content rendered between description and footer. */
children?: React.ReactNode
/** Label for the confirm button. @default "确认" */
confirmLabel?: string
/** Label for the cancel button. @default "取消" */
cancelLabel?: string
/** Visual style of the confirm button. @default "default" */
variant?: "default" | "destructive"
/** Controlled loading state for the confirm button. */
loading?: boolean
/** Called when the user clicks the confirm button. Can return a Promise for auto-loading. */
onConfirm?: () => void | Promise<void>
}
/**
* A ready-made confirmation dialog composed from `Dialog` and `Button`.
* Demonstrates a component that depends on other registry items.
*/
function ConfirmDialog({
trigger,
open: openProp,
defaultOpen,
onOpenChange,
title,
description,
children,
confirmLabel = "确认",
cancelLabel = "取消",
variant = "default",
loading: loadingProp,
onConfirm,
}: ConfirmDialogProps) {
const [internalOpen, setInternalOpen] = React.useState(defaultOpen ?? false)
const [asyncLoading, setAsyncLoading] = React.useState(false)
const isControlled = openProp !== undefined
const open = isControlled ? openProp : internalOpen
const isLoading = loadingProp ?? asyncLoading
const handleOpenChange = React.useCallback(
(next: boolean) => {
if (!isControlled) {
setInternalOpen(next)
}
onOpenChange?.(next)
},
[isControlled, onOpenChange]
)
const handleConfirm = async (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault()
if (!onConfirm) {
handleOpenChange(false)
return
}
try {
const result = onConfirm()
if (result && typeof (result as Promise<void>).then === "function") {
setAsyncLoading(true)
await result
}
handleOpenChange(false)
} catch {
// Keep the dialog open so the user can retry after a failed action.
} finally {
setAsyncLoading(false)
}
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
{trigger ? <DialogTrigger asChild>{trigger}</DialogTrigger> : null}
<DialogContent>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
{description ? (
<DialogDescription>{description}</DialogDescription>
) : null}
</DialogHeader>
{children ? <div>{children}</div> : null}
<DialogFooter>
<DialogClose asChild disabled={isLoading}>
<Button variant="outline" disabled={isLoading}>
{cancelLabel}
</Button>
</DialogClose>
<Button
variant={variant}
onClick={handleConfirm}
disabled={isLoading}
aria-busy={isLoading || undefined}
>
{isLoading ? <Loader2Icon className="animate-spin" /> : null}
{isLoading ? "处理中…" : confirmLabel}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
export { ConfirmDialog }
属性 Props
ConfirmDialog 提供开箱即用的高阶配置属性,并支持自定义扩展内容:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| title | string | — | 确认对话框顶部的主标题文本。 |
| description | string | — | 主标题下方的补充说明与风险警示文本。 |
| trigger | React.ReactNode | — | 触发对话框弹出的元素(如按钮),会自动通过 asChild 绑定触发逻辑。 |
| confirmLabel | string | "确认" | 确认按钮上显示的文本标签。 |
| cancelLabel | string | "取消" | 取消按钮上显示的文本标签。 |
| variant | "default" | "destructive" | "default" | 确认按钮的视觉变体。"destructive" 会使用醒目的警示红色。 |
| open | boolean | — | 受控模式下的弹窗开启状态。 |
| defaultOpen | boolean | false | 非受控模式下的初始打开状态。 |
| loading | boolean | — | 受控的加载中状态。处于加载中时确认与取消按钮均禁用,确认按钮显示旋转图标与「处理中…」文案。onConfirm 返回的 Promise 被拒绝时弹窗保持打开,便于重试。 |
| children | React.ReactNode | — | 在标题描述与底部操作栏之间渲染的自定义 React 节点(如影响范围列表、预警卡片等)。 |
事件 Events
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| onConfirm | () => void | Promise<void> | — | 用户点击确认按钮时调用的回调函数。若返回 Promise,组件会自动管理并展示加载状态直至完成。 |
| onOpenChange | (open: boolean) => void | — | 弹窗打开或关闭状态变化时的回调函数(点击遮罩、按下 Esc 或点击取消按钮均会触发)。 |
使用场景与设计规范
ConfirmDialog 将常见的“是否继续”二次确认交互抽象为高度一致的模式,避免开发者在各业务模块中重复拼装弹窗框架:
- 何时使用 ConfirmDialog:
- 高风险/不可逆破坏性操作:如删除资源、注销团队、清空缓存、覆盖线上生产配置。
- 可能产生意外开销或中断的操作:如大批量任务终止、重启集群、强制重置密钥。
- 避免过度确认(Confirm Fatigue):
- 常规表单提交、轻量保存、加入收藏等安全操作不要增加确认弹窗,过度打断会使用户产生“确认疲劳”,在真正面临危险时习惯性点击确认。
- 明确的文案与按钮引导:
- 标题具体明确:避免抽象的“提示”或“警告”,直接指明操作对象,如“确定要删除数据库 backup-01 吗?”。
- 动词匹配:确认按钮文案应与行为动词一致(如“删除”、“强制重置”、“发布”),不要使用含糊不清的“确定”或“OK”。
- 说明后果:在
description中告知用户该操作是否可恢复、对关联系统有什么影响。
场景示例
危险破坏性确认
删除资源时,配置 variant="destructive" 并提供明确的后果警告,帮助用户在做出不可逆决定前保持谨慎:
Loading…
异步请求与加载保护
当确认操作需要调用远端 API 接口时,只需在 onConfirm 中返回一个异步 Promise,组件会自动进入加载中状态并阻止重复点击:
Loading…
<ConfirmDialog
trigger={<Button>回滚版本</Button>}
title="确认回滚至上一稳定版本?"
description="预计需要 30 秒服务预热。"
confirmLabel="开始回滚"
onConfirm={async () => {
await api.rollbackCluster()
}}
/>自定义详情内容与受控状态
通过 children 属性可插入影响评估卡片或风险条目;配合 open 与 onOpenChange 实现由外部业务状态驱动的受控弹出:
Loading…
无障碍与交互 Accessibility
- WAI-ARIA 规范:底层直接复用
Dialog的无障碍特性,自动挂载role="alertdialog"与语义标签。 - 焦点控制:
- 弹窗唤起后,自动将初始焦点停留在“取消”按钮或安全区域,防止用户误按 Enter 键触发破坏性确认。
- 确认操作进行(Loading)期间,取消与确认按钮均自动进入
disabled状态,防止误操作中断异步流。
- 键盘支持:
- Esc:快速安全退出并关闭确认弹窗。
- Tab / Shift + Tab:在取消与确认按钮之间循环切换焦点。