157 lines
5.0 KiB
TypeScript
157 lines
5.0 KiB
TypeScript
'use client'
|
||
|
||
import Image from 'next/image'
|
||
import { AlertTriangle, Trash2 } from 'lucide-react'
|
||
|
||
import { Button } from '@/components/ui/button'
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogDescription,
|
||
DialogFooter,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from '@/components/ui/dialog'
|
||
import { Spinner } from '@/components/ui/spinner'
|
||
import { Level } from '@/types'
|
||
|
||
interface DeleteConfirmDialogProps {
|
||
open: boolean
|
||
onOpenChange: (open: boolean) => void
|
||
level: Level | null
|
||
/** 列表里的行号(0-based),用于展示 #序号 */
|
||
index?: number
|
||
isLoading?: boolean
|
||
onConfirm: () => void
|
||
}
|
||
|
||
/**
|
||
* 关卡删除二次确认弹窗。
|
||
* 展示关卡预览(双图 + 答案)让用户确认删除对象,而不是盲点。
|
||
* - 确认按钮 autoFocus:Enter 直接确认,Esc 由 Radix Dialog 默认关闭
|
||
* - 删除中禁用所有交互并显示 Spinner
|
||
*/
|
||
export function DeleteConfirmDialog({
|
||
open,
|
||
onOpenChange,
|
||
level,
|
||
index,
|
||
isLoading = false,
|
||
onConfirm,
|
||
}: DeleteConfirmDialogProps) {
|
||
return (
|
||
<Dialog
|
||
open={open}
|
||
onOpenChange={(next) => {
|
||
// 删除进行中不允许关闭,避免中途点取消造成状态不一致
|
||
if (isLoading && !next) return
|
||
onOpenChange(next)
|
||
}}
|
||
>
|
||
<DialogContent className="sm:max-w-md">
|
||
<DialogHeader className="items-center text-center sm:items-center sm:text-center">
|
||
{/* 红色警告图标 + 软光晕,视觉焦点 */}
|
||
<div className="relative mx-auto mb-2 flex h-14 w-14 items-center justify-center">
|
||
<div className="absolute inset-0 rounded-full bg-red-500/10" />
|
||
<div className="absolute inset-1.5 rounded-full bg-red-500/15" />
|
||
<AlertTriangle
|
||
className="relative h-7 w-7 text-red-600"
|
||
strokeWidth={2.25}
|
||
/>
|
||
</div>
|
||
<DialogTitle className="text-base">确认删除此关卡?</DialogTitle>
|
||
<DialogDescription>
|
||
删除后将立即从小程序端下线,且
|
||
<span className="font-medium text-red-600"> 无法恢复</span>
|
||
。请确认无误。
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
{/* 关卡预览卡片 */}
|
||
{level && (
|
||
<div className="rounded-md border border-dashed bg-muted/40 p-3">
|
||
<div className="mb-2 flex items-center justify-between text-xs text-muted-foreground">
|
||
<span>待删除关卡</span>
|
||
{typeof index === 'number' && index >= 0 && (
|
||
<span className="rounded bg-background px-1.5 py-0.5 font-mono text-[11px] tabular-nums">
|
||
#{index + 1}
|
||
</span>
|
||
)}
|
||
</div>
|
||
<div className="flex items-center gap-3">
|
||
<div className="flex flex-shrink-0 gap-1.5">
|
||
<div className="relative h-14 w-14 overflow-hidden rounded-md bg-background ring-1 ring-border">
|
||
{level.image1Url ? (
|
||
<Image
|
||
src={level.image1Url}
|
||
alt="图片1"
|
||
fill
|
||
className="object-cover"
|
||
sizes="56px"
|
||
/>
|
||
) : null}
|
||
</div>
|
||
<div className="relative h-14 w-14 overflow-hidden rounded-md bg-background ring-1 ring-border">
|
||
{level.image2Url ? (
|
||
<Image
|
||
src={level.image2Url}
|
||
alt="图片2"
|
||
fill
|
||
className="object-cover"
|
||
sizes="56px"
|
||
/>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
<div className="min-w-0 flex-1">
|
||
<div className="truncate text-base font-semibold leading-tight">
|
||
{level.answer}
|
||
</div>
|
||
{level.punchline ? (
|
||
<div className="mt-1 truncate text-xs text-orange-600">
|
||
{level.punchline}
|
||
</div>
|
||
) : (
|
||
<div className="mt-1 text-xs text-muted-foreground">
|
||
(无谐音梗)
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<DialogFooter className="gap-2 sm:gap-2">
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
onClick={() => onOpenChange(false)}
|
||
disabled={isLoading}
|
||
>
|
||
取消
|
||
</Button>
|
||
<Button
|
||
type="button"
|
||
variant="destructive"
|
||
onClick={onConfirm}
|
||
disabled={isLoading}
|
||
autoFocus
|
||
>
|
||
{isLoading ? (
|
||
<>
|
||
<Spinner size="sm" className="mr-2" />
|
||
删除中...
|
||
</>
|
||
) : (
|
||
<>
|
||
<Trash2 className="mr-2 h-4 w-4" />
|
||
确认删除
|
||
</>
|
||
)}
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
)
|
||
}
|