feat: 支持批量上传关卡

This commit is contained in:
richarjiang
2026-05-01 08:44:56 +08:00
parent f3f27def2b
commit 66a9ee2950
27 changed files with 5262 additions and 515 deletions

View File

@@ -0,0 +1,156 @@
'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
}
/**
* 关卡删除二次确认弹窗。
* 展示关卡预览(双图 + 答案)让用户确认删除对象,而不是盲点。
* - 确认按钮 autoFocusEnter 直接确认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>
)
}