2026-03-17 13:17:02 +08:00

277 lines
10 KiB
TypeScript

"use client"
import { useState, useEffect } from "react"
import { DashboardShell } from "@/components/dashboard-shell"
import { DashboardHeader } from "@/components/dashboard-header"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Badge } from "@/components/ui/badge"
import { Search, Edit, Eye, Loader2 } from "lucide-react"
import { AddFoodDialog } from "@/components/food/add-food-dialog"
import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog"
import { useToast } from "@/components/ui/use-toast"
import Link from "next/link"
import type { Food } from "@/components/food/food-detail-dialog"
import { getFoods, deleteFood as deleteFoodApi } from "@/lib/api/food"
export default function FoodPage() {
const { toast } = useToast()
const [foods, setFoods] = useState<Food[]>([])
const [loading, setLoading] = useState(true)
const [searchTerm, setSearchTerm] = useState("")
const [currentPage, setCurrentPage] = useState(1)
const [totalPages, setTotalPages] = useState(1)
const [total, setTotal] = useState(0)
const [selectedFood, setSelectedFood] = useState<Food | null>(null)
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
const itemsPerPage = 10
// 状态显示映射
const getStatusDisplay = (status: string) => {
const statusMap: Record<string, string> = {
'published': '已发布',
'draft': '草稿',
'pending': '待审核',
}
return statusMap[status] || status
}
// 获取食物列表数据
const fetchFoods = async (page: number = 1, search: string = "") => {
try {
setLoading(true)
const response = await getFoods({
page,
pageSize: itemsPerPage,
search: search || undefined,
})
if (response.success && response.data) {
setFoods(response.data.results || [])
setTotal(response.data.count || 0)
setTotalPages(Math.ceil((response.data.count || 0) / itemsPerPage))
}
} catch (error) {
console.error("获取食物列表失败:", error)
toast({
title: "获取数据失败",
description: error instanceof Error ? error.message : "无法获取食物列表",
variant: "destructive",
})
setFoods([])
} finally {
setLoading(false)
}
}
// 初始化数据
useEffect(() => {
fetchFoods(currentPage, searchTerm)
}, [currentPage])
// 搜索处理
const handleSearch = (value: string) => {
setSearchTerm(value)
setCurrentPage(1)
fetchFoods(1, value)
}
// 处理添加食物成功后的刷新
const handleAddFood = async () => {
// 食物已经在 AddFoodDialog 中创建成功,这里只需要刷新列表
fetchFoods(currentPage, searchTerm)
}
// 处理编辑食物成功后的刷新
const handleEditFood = async () => {
// 食物已经在 AddFoodDialog 中更新成功,这里只需要刷新列表和关闭对话框
setSelectedFood(null)
setIsEditDialogOpen(false)
fetchFoods(currentPage, searchTerm)
}
// 处理删除食物
const handleDeleteFood = async (foodId: string) => {
try {
const response = await deleteFoodApi(foodId)
if (response.success) {
toast({
title: "删除成功",
description: "食物已成功删除",
variant: "destructive",
})
// 重新获取当前页数据
fetchFoods(currentPage, searchTerm)
}
} catch (error) {
console.error("删除食物失败:", error)
toast({
title: "删除失败",
description: error instanceof Error ? error.message : "删除食物失败",
variant: "destructive",
})
}
}
// 打开编辑对话框
const openEditDialog = (food: Food) => {
setSelectedFood(food)
setIsEditDialogOpen(true)
}
return (
<DashboardShell>
<DashboardHeader heading="食物管理" text="管理洛天依的食物卡牌">
<AddFoodDialog onSave={handleAddFood} />
</DashboardHeader>
<div className="flex items-center justify-between space-y-2 mb-6">
<div className="flex items-center space-x-2">
<div className="relative">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="搜索食物..."
className="w-[300px] pl-8 border-none bg-white shadow-md focus-visible:ring-pink-500"
value={searchTerm}
onChange={(e) => handleSearch(e.target.value)}
/>
</div>
</div>
</div>
<Card className="border-none shadow-lg bg-gradient-to-br from-white to-purple-50">
<CardHeader>
<CardTitle className="text-xl font-bold flex items-center">
<span className="bg-clip-text text-transparent bg-gradient-to-r from-purple-600 to-pink-600"></span>
<div className="ml-2 h-1 w-10 bg-gradient-to-r from-purple-600 to-pink-600 rounded-full"></div>
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-8 w-8 animate-spin text-pink-500" />
<span className="ml-2 text-gray-500">...</span>
</div>
) : (
<Table>
<TableHeader className="bg-gray-50">
<TableRow>
<TableHead className="w-[100px]">ID</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{foods.map((food) => (
<TableRow key={food.id} className="hover:bg-gray-50 transition-colors">
<TableCell className="font-medium">{food.id}</TableCell>
<TableCell className="font-medium text-pink-600">{food.name}</TableCell>
<TableCell>{food.food_type}</TableCell>
<TableCell>{food.rarity}</TableCell>
<TableCell>{food.releaseDate || "-"}</TableCell>
<TableCell>
<Badge
className={
(food.status === "published" || food.status === "已发布")
? "bg-green-500 hover:bg-green-600"
: "bg-gray-500 hover:bg-gray-600"
}
>
{getStatusDisplay(food.status)}
</Badge>
</TableCell>
<TableCell className="font-medium">{food.activatedCount}</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="icon" className="hover:bg-pink-50 hover:text-pink-600" asChild>
<Link href={`/food/${food.id}`}>
<Eye className="h-4 w-4" />
<span className="sr-only"></span>
</Link>
</Button>
{food.status !== "published" && food.status !== "已发布" && (
<>
<Button
variant="ghost"
size="icon"
className="hover:bg-pink-50 hover:text-pink-600"
onClick={() => openEditDialog(food)}
>
<Edit className="h-4 w-4" />
</Button>
<DeleteConfirmationDialog
title="删除食物"
description="此操作将永久删除该食物及其所有相关数据。"
itemName={food.name}
onDelete={() => handleDeleteFood(food.id)}
/>
</>
)}
</TableCell>
</TableRow>
))}
{foods.length === 0 && !loading && (
<TableRow>
<TableCell colSpan={8} className="h-24 text-center">
{searchTerm ? "没有找到匹配的食物" : "暂无食物数据"}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</CardContent>
<CardFooter className="flex justify-between">
<div className="text-sm text-muted-foreground">
{foods.length > 0 ? (currentPage - 1) * itemsPerPage + 1 : 0}-
{Math.min(currentPage * itemsPerPage, total)} {total}
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
className="hover:bg-pink-50 hover:text-pink-700 transition-all duration-200"
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
disabled={currentPage === 1 || loading}
>
</Button>
<Button
variant="outline"
size="sm"
className="hover:bg-pink-50 hover:text-pink-700 transition-all duration-200"
onClick={() => setCurrentPage((prev) => Math.min(prev + 1, totalPages))}
disabled={currentPage === totalPages || totalPages === 0 || loading}
>
</Button>
</div>
</CardFooter>
</Card>
{/* 编辑食物对话框 */}
{selectedFood && isEditDialogOpen && (
<AddFoodDialog
mode="edit"
initialFood={selectedFood}
open={isEditDialogOpen}
onOpenChange={setIsEditDialogOpen}
onSave={handleEditFood}
/>
)}
</DashboardShell>
)
}