wui
组件

确认对话框 Confirm Dialog

由 Dialog 和 Button 深度封装的开箱即用确认弹窗,专为危险操作、异步确认及高优先级决定设计。

第三方依赖 · lucide-react

基础用法

通过简洁的声明式属性即可快速完成“点击触发 -> 弹出确认弹窗 -> 确认执行”的闭环流程:

Loading…

安装与引入

通过 CLI 自动添加组件,或手动复制源码至项目中:

pnpm dlx @wui-design/cli@latest add @wui/confirm-dialog

Registry 依赖说明

confirm-dialog 声明了对 dialog 和 button 的组件依赖,CLI 会自动解析并一并安装所需的基础组件。

安装基础依赖与动效库
pnpm add radix-ui motion lucide-react clsx tailwind-merge
复制组件源码到 components/confirm-dialog.tsx
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 提供开箱即用的高阶配置属性,并支持自定义扩展内容:

属性类型默认值说明
titlestring—确认对话框顶部的主标题文本。
descriptionstring—主标题下方的补充说明与风险警示文本。
triggerReact.ReactNode—触发对话框弹出的元素(如按钮),会自动通过 asChild 绑定触发逻辑。
confirmLabelstring"确认"确认按钮上显示的文本标签。
cancelLabelstring"取消"取消按钮上显示的文本标签。
variant"default" | "destructive""default"确认按钮的视觉变体。"destructive" 会使用醒目的警示红色。
openboolean—受控模式下的弹窗开启状态。
defaultOpenbooleanfalse非受控模式下的初始打开状态。
loadingboolean—受控的加载中状态。处于加载中时确认与取消按钮均禁用,确认按钮显示旋转图标与「处理中…」文案。onConfirm 返回的 Promise 被拒绝时弹窗保持打开,便于重试。
childrenReact.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:在取消与确认按钮之间循环切换焦点。