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,119 @@
'use client'
import { Level } from '@/types'
import { Button } from '@/components/ui/button'
import { Pencil, Trash2 } from 'lucide-react'
import Image from 'next/image'
// 列宽定义header 和 row 必须保持一致。用 grid-template-columns 统一控制。
// 序号 | 图片 | 答案 | 谐音梗 | 提示 | 创建时间 | 操作
export const GRID_TEMPLATE =
'minmax(60px,60px) minmax(120px,120px) minmax(80px,1fr) minmax(100px,1fr) minmax(160px,2fr) minmax(100px,100px) minmax(100px,100px)'
interface LevelRowProps {
level: Level
index: number
onEdit: (level: Level) => void
onDelete: (id: string) => void
}
export function LevelRow({ level, index, onEdit, onDelete }: LevelRowProps) {
return (
<div
style={{ gridTemplateColumns: GRID_TEMPLATE }}
className="grid items-center gap-3 px-3 border-b bg-background text-sm min-h-[64px] hover:bg-muted/30 transition-colors"
>
{/* 序号:用数组 index + 1而不是 DB 里已弃用的 sortOrder */}
<span className="font-mono tabular-nums text-muted-foreground">
{index + 1}
</span>
{/* 图片 */}
<div className="flex gap-1.5">
<div className="relative w-12 h-12 rounded overflow-hidden bg-gray-100 flex-shrink-0">
{level.image1Url ? (
<Image
src={level.image1Url}
alt="图片1"
fill
className="object-cover"
sizes="48px"
/>
) : (
<div className="w-full h-full flex items-center justify-center text-gray-400 text-[10px]">
</div>
)}
</div>
<div className="relative w-12 h-12 rounded overflow-hidden bg-gray-100 flex-shrink-0">
{level.image2Url ? (
<Image
src={level.image2Url}
alt="图片2"
fill
className="object-cover"
sizes="48px"
/>
) : (
<div className="w-full h-full flex items-center justify-center text-gray-400 text-[10px]">
</div>
)}
</div>
</div>
{/* 答案 */}
<span className="font-medium truncate">{level.answer}</span>
{/* 谐音梗 */}
<span className="truncate">
{level.punchline ? (
<span className="text-orange-600">{level.punchline}</span>
) : (
<span className="text-muted-foreground"></span>
)}
</span>
{/* 提示 */}
<span className="truncate text-muted-foreground">
{(() => {
const hints = [level.hint1, level.hint2, level.hint3].filter(Boolean)
return hints.length > 0 ? hints.join('、') : '—'
})()}
</span>
{/* 创建时间 */}
<span className="text-muted-foreground whitespace-nowrap">
{new Date(level.createdAt).toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
})}
</span>
{/* 操作 */}
<div className="flex items-center gap-2">
<Button
variant="outline"
size="icon"
className="h-8 w-8"
onClick={() => onEdit(level)}
aria-label="编辑关卡"
title="编辑"
>
<Pencil className="h-3.5 w-3.5" />
</Button>
<Button
variant="outline"
size="icon"
className="h-8 w-8 border-red-200 text-red-600 hover:border-red-600 hover:bg-red-600 hover:text-white focus-visible:ring-red-500"
onClick={() => onDelete(level.id)}
aria-label="删除关卡"
title="删除"
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
</div>
)
}