Next.js 14 App Router application for managing homophone pun game levels: - Better Auth with Prisma adapter for authentication - MySQL database with Prisma ORM - Level CRUD operations with drag-and-drop reordering - Tencent COS integration for image uploads - shadcn/ui components with Tailwind CSS - TanStack Query for server state management
135 lines
3.1 KiB
TypeScript
135 lines
3.1 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useCallback } from 'react'
|
|
import { Level } from '@/types'
|
|
import {
|
|
DndContext,
|
|
closestCenter,
|
|
KeyboardSensor,
|
|
PointerSensor,
|
|
useSensor,
|
|
useSensors,
|
|
DragEndEvent,
|
|
} from '@dnd-kit/core'
|
|
import {
|
|
arrayMove,
|
|
SortableContext,
|
|
sortableKeyboardCoordinates,
|
|
verticalListSortingStrategy,
|
|
useSortable,
|
|
} from '@dnd-kit/sortable'
|
|
import { CSS } from '@dnd-kit/utilities'
|
|
import { LevelCard } from './level-card'
|
|
|
|
interface SortableLevelCardProps {
|
|
level: Level
|
|
onEdit: (level: Level) => void
|
|
onDelete: (id: string) => void
|
|
}
|
|
|
|
function SortableLevelCard({ level, onEdit, onDelete }: SortableLevelCardProps) {
|
|
const {
|
|
attributes,
|
|
listeners,
|
|
setNodeRef,
|
|
transform,
|
|
transition,
|
|
isDragging,
|
|
} = useSortable({ id: level.id })
|
|
|
|
const style = {
|
|
transform: CSS.Transform.toString(transform),
|
|
transition,
|
|
}
|
|
|
|
return (
|
|
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
|
|
<LevelCard
|
|
level={level}
|
|
onEdit={onEdit}
|
|
onDelete={onDelete}
|
|
isDragging={isDragging}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
interface LevelListProps {
|
|
levels: Level[]
|
|
onReorder: (orders: { id: string; sortOrder: number }[]) => void
|
|
onEdit: (level: Level) => void
|
|
onDelete: (id: string) => void
|
|
}
|
|
|
|
export function LevelList({ levels, onReorder, onEdit, onDelete }: LevelListProps) {
|
|
const [items, setItems] = useState<Level[]>(levels)
|
|
|
|
// Update items when levels prop changes
|
|
if (JSON.stringify(items.map(i => i.id)) !== JSON.stringify(levels.map(l => l.id))) {
|
|
setItems(levels)
|
|
}
|
|
|
|
const sensors = useSensors(
|
|
useSensor(PointerSensor, {
|
|
activationConstraint: {
|
|
distance: 8,
|
|
},
|
|
}),
|
|
useSensor(KeyboardSensor, {
|
|
coordinateGetter: sortableKeyboardCoordinates,
|
|
})
|
|
)
|
|
|
|
const handleDragEnd = useCallback(
|
|
(event: DragEndEvent) => {
|
|
const { active, over } = event
|
|
|
|
if (over && active.id !== over.id) {
|
|
const oldIndex = items.findIndex((item) => item.id === active.id)
|
|
const newIndex = items.findIndex((item) => item.id === over.id)
|
|
|
|
const newItems = arrayMove(items, oldIndex, newIndex)
|
|
setItems(newItems)
|
|
|
|
// Notify parent of new order
|
|
const orders = newItems.map((item, index) => ({
|
|
id: item.id,
|
|
sortOrder: index,
|
|
}))
|
|
onReorder(orders)
|
|
}
|
|
},
|
|
[items, onReorder]
|
|
)
|
|
|
|
if (items.length === 0) {
|
|
return (
|
|
<div className="text-center py-12 text-gray-500">
|
|
<p>暂无关卡数据</p>
|
|
<p className="text-sm mt-2">点击上方“添加关卡”按钮创建第一个关卡</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<DndContext
|
|
sensors={sensors}
|
|
collisionDetection={closestCenter}
|
|
onDragEnd={handleDragEnd}
|
|
>
|
|
<SortableContext items={items} strategy={verticalListSortingStrategy}>
|
|
<div className="space-y-3">
|
|
{items.map((level) => (
|
|
<SortableLevelCard
|
|
key={level.id}
|
|
level={level}
|
|
onEdit={onEdit}
|
|
onDelete={onDelete}
|
|
/>
|
|
))}
|
|
</div>
|
|
</SortableContext>
|
|
</DndContext>
|
|
)
|
|
}
|