AniBTUI
✦ 组件@anibt/confirm-dialogregistry JSON

Confirm Dialog

破坏性或不可撤销操作前的一次确认。返回 Promise 时弹窗保持打开并显示进行中,失败不关闭。

使用中

安装

npx shadcn@latest add @anibt/confirm-dialog

第一次用之前:在 components.json 里登记 @anibt,再单独运行一次 npx shadcn@latest add @anibt/theme(见快速开始)。只有单独安装主题时,CLI 才会把 shadcn 的标准颜色换成白樱 / 夜樱;随组件带进来的主题只补上 AniBT 自己的变量和配方。 CLI 会一并装好 @anibt/alert-dialog、@anibt/spinner。

手动安装
  1. 1. 先装主题组件只读 @anibt/theme 的 token 和配方,没有主题就没有样子。
    npx shadcn@latest add @anibt/theme
  2. 2. 安装依赖
    npm install lucide-react
  3. 3. 它还用到
    npx shadcn@latest add @anibt/alert-dialog @anibt/spinner
  4. 4. 把下面的文件放进项目
    components/ui/confirm-dialog.tsx
    "use client"
    
    import * as React from "react"
    import { TriangleAlertIcon } from "lucide-react"
    
    import {
      AlertDialog,
      AlertDialogAction,
      AlertDialogCancel,
      AlertDialogContent,
      AlertDialogDescription,
      AlertDialogFooter,
      AlertDialogHeader,
      AlertDialogMedia,
      AlertDialogTitle,
      AlertDialogTrigger,
    } from "@/components/ui/alert-dialog"
    import { Spinner } from "@/components/ui/spinner"
    
    /**
     * ConfirmDialog — AniBT 「贴纸手账」: the one way to ask before a destructive
     * or irreversible action, built on `@anibt/alert-dialog`.
     *
     * The footer is one order everywhere: the quiet 取消 on the left, the confirm
     * on the right — candy, or for `destructive` the red danger key, with A's red
     * mark in the bar.
     *
     * Closing:
     * - `onConfirm` returning nothing closes the dialog at once.
     * - `onConfirm` returning a promise keeps it open and busy until it settles:
     *   it closes on resolve and stays open on reject, so a failed delete leaves
     *   the reader where they can read the error and retry.
     * - A caller that owns the pending flag can pass `loading`; the confirm then
     *   shows the spinner and refuses further clicks.
     */
    type ConfirmDialogProps = {
      /** Controlled open state. Omit when using `trigger`. */
      open?: boolean
      onOpenChange?: (open: boolean) => void
      /** The element that opens the dialog (uncontrolled use). */
      trigger?: React.ReactNode
      title: React.ReactNode
      description?: React.ReactNode
      /** Extra content between the bar and the buttons. */
      children?: React.ReactNode
      confirmLabel: React.ReactNode
      cancelLabel?: React.ReactNode
      /** The red confirm and the red mark. Deletes and revokes are destructive. */
      destructive?: boolean
      /** The icon in the bar's mark; a destructive dialog defaults to a warning sign. */
      icon?: React.ReactNode
      /** External pending state, merged with the promise `onConfirm` returns. */
      loading?: boolean
      confirmDisabled?: boolean
      onConfirm: () => void | Promise<unknown>
    }
    
    function ConfirmDialog({
      open,
      onOpenChange,
      trigger,
      title,
      description,
      children,
      confirmLabel,
      cancelLabel = "取消",
      destructive = false,
      icon,
      loading = false,
      confirmDisabled = false,
      onConfirm,
    }: ConfirmDialogProps) {
      const [pending, setPending] = React.useState(false)
      const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false)
      const busy = loading || pending
      const isOpen = open ?? uncontrolledOpen
      const setOpen = React.useCallback(
        (next: boolean) => {
          if (open === undefined) setUncontrolledOpen(next)
          onOpenChange?.(next)
        },
        [open, onOpenChange]
      )
    
      const handleConfirm = (event: React.MouseEvent<HTMLButtonElement>) => {
        if (busy) {
          event.preventDefault()
          return
        }
        const result = onConfirm()
        if (!isPromiseLike(result)) return
    
        // Radix closes an alert dialog from its action unless the click's default
        // is prevented: keep it open while the work runs.
        event.preventDefault()
        setPending(true)
        void Promise.resolve(result).then(
          () => {
            setPending(false)
            setOpen(false)
          },
          () => {
            setPending(false)
          }
        )
      }
    
      const mark = icon ?? (destructive ? <TriangleAlertIcon /> : null)
    
      return (
        <AlertDialog open={isOpen} onOpenChange={setOpen}>
          {trigger ? <AlertDialogTrigger {...{ asChild: true }}>{trigger}</AlertDialogTrigger> : null}
          <AlertDialogContent>
            <AlertDialogHeader>
              {mark ? <AlertDialogMedia tone={destructive ? "destructive" : "accent"}>{mark}</AlertDialogMedia> : null}
              <AlertDialogTitle>{title}</AlertDialogTitle>
              {description ? <AlertDialogDescription>{description}</AlertDialogDescription> : null}
            </AlertDialogHeader>
            {children}
            <AlertDialogFooter>
              <AlertDialogCancel disabled={busy}>{cancelLabel}</AlertDialogCancel>
              <AlertDialogAction
                variant={destructive ? "destructive" : "default"}
                disabled={busy || confirmDisabled}
                aria-busy={busy || undefined}
                onClick={handleConfirm}
              >
                {busy ? <Spinner className="size-4" /> : null}
                {confirmLabel}
              </AlertDialogAction>
            </AlertDialogFooter>
          </AlertDialogContent>
        </AlertDialog>
      )
    }
    
    function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
      return typeof value === "object" && value !== null && typeof (value as PromiseLike<unknown>).then === "function"
    }
    
    export { ConfirmDialog }
    export type { ConfirmDialogProps }

用法

components/revoke-key.tsx
import { Button } from "@/components/ui/button"
import { ConfirmDialog } from "@/components/ui/confirm-dialog"

export function RevokeKey({ revoke }: { revoke: () => Promise<void> }) {
  return (
    <ConfirmDialog
      destructive
      trigger={<Button variant="destructive">撤销 API 密钥</Button>}
      title="撤销这个 API 密钥?"
      description="用它发布的脚本会立刻失效,撤销后不能恢复。"
      confirmLabel="撤销"
      onConfirm={revoke}
    />
  )
}

关闭的时机

  • onConfirm 不返回东西:点确认立即关闭。
  • onConfirm 返回 Promise:弹窗保持打开,确认键显示转圈并拒绝再次点击;成功后关闭,失败则留在原地,读者能看到错误再试一次。
  • 调用方自己管着进行中的状态时,传 loading。

非破坏性

不传 destructive 时确认键是糖果主按钮;传了 icon 就在头里放一个粉色标记。这个例子用 open / onOpenChange 受控打开,不用 trigger。

属性

ConfirmDialog
titleReactNode默认 —必填:问题本身
confirmLabelReactNode默认 —必填:确认键的文字,写动作(「删除」「撤销」)
onConfirm() => void | Promise<unknown>默认 —必填:返回 Promise 时等它结束再关闭
descriptionReactNode默认 —头部标题下的一行说明
childrenReactNode默认 —头和按钮之间的额外内容
destructiveboolean默认 false红色确认键和红色警示标记
iconReactNode默认 —头部标记里的图标;破坏性时默认是警示三角
cancelLabelReactNode默认 "取消"取消键的文字
triggerReactNode默认 —打开弹窗的元素(非受控用法)
open / onOpenChangeboolean / (open) => void默认 —受控用法
loadingboolean默认 false外部的进行中状态
confirmDisabledboolean默认 false禁用确认键