- Update food, outfits, props, home-decor pages and components - Add permissions page and sidebar updates - Update API client and all API modules (auth, food, dances, etc.) - Add card model migrations for optional fields - Update Django views, serializers, and authentication - Add affinity level migrations and user app updates - Add project documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
282 lines
13 KiB
TypeScript
282 lines
13 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect, use } from "react"
|
|
import { DashboardShell } from "@/components/dashboard-shell"
|
|
import { DashboardHeader } from "@/components/dashboard-header"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
|
import { ArrowLeft, Edit, AlertTriangle, FileText, Plus, Download, Loader2 } from "lucide-react"
|
|
import Link from "next/link"
|
|
import { AddPrintBatchDialog } from "@/components/props/add-print-batch-dialog"
|
|
import { ExportCardsDialog } from "@/components/props/export-cards-dialog"
|
|
import { isSuperUser } from "@/lib/api/auth"
|
|
import { getProp } from "@/lib/api/props"
|
|
import type { Prop } from "@/lib/api/types"
|
|
|
|
export default function PropDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
|
const { id } = use(params)
|
|
const [prop, setProp] = useState<Prop | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
useEffect(() => {
|
|
const fetchProp = async () => {
|
|
try {
|
|
setLoading(true)
|
|
setError(null)
|
|
const data = await getProp(id)
|
|
setProp(data)
|
|
} catch (err) {
|
|
console.error("获取道具详情失败:", err)
|
|
setError(`找不到ID为 ${id} 的道具`)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
fetchProp()
|
|
}, [id])
|
|
|
|
if (loading) {
|
|
return (
|
|
<DashboardShell>
|
|
<div className="flex items-center justify-center h-[60vh]">
|
|
<Loader2 className="h-8 w-8 animate-spin text-pink-500" />
|
|
<span className="ml-2 text-muted-foreground">加载中...</span>
|
|
</div>
|
|
</DashboardShell>
|
|
)
|
|
}
|
|
|
|
if (error || !prop) {
|
|
return (
|
|
<DashboardShell>
|
|
<div className="flex flex-col items-center justify-center h-[60vh]">
|
|
<AlertTriangle className="h-16 w-16 text-red-500 mb-4" />
|
|
<h1 className="text-2xl font-bold mb-2">道具不存在</h1>
|
|
<p className="text-gray-500 mb-6">{error || `找不到ID为 ${id} 的道具`}</p>
|
|
<Button asChild>
|
|
<Link href="/props">
|
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
返回道具列表
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
</DashboardShell>
|
|
)
|
|
}
|
|
|
|
const isPublished = prop.status === "已发布"
|
|
const printedCount = prop.batchesCount || 0
|
|
const activatedCount = prop.activeCardsCount || 0
|
|
const activationRate = printedCount > 0 ? Math.round((activatedCount / printedCount) * 100) : 0
|
|
|
|
return (
|
|
<DashboardShell>
|
|
<div className="absolute top-0 right-0 w-1/2 h-48 bg-gradient-to-bl from-purple-200 via-pink-200 to-transparent opacity-20 rounded-bl-full" />
|
|
|
|
<div className="flex items-center mb-6">
|
|
<Button variant="ghost" size="sm" className="mr-4" asChild>
|
|
<Link href="/props">
|
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
返回列表
|
|
</Link>
|
|
</Button>
|
|
<DashboardHeader heading={prop.name} text={`道具ID: ${prop.id}`}>
|
|
<div className="flex space-x-2 ml-auto">
|
|
{(!isPublished || isSuperUser()) && (
|
|
<Button
|
|
asChild
|
|
className="bg-gradient-to-r from-pink-500 to-purple-600 hover:from-pink-600 hover:to-purple-700 transition-all duration-300 shadow-md hover:shadow-lg"
|
|
>
|
|
<Link href={`/props/edit/${id}`}>
|
|
<Edit className="mr-2 h-4 w-4" />
|
|
编辑道具
|
|
</Link>
|
|
</Button>
|
|
)}
|
|
<ExportCardsDialog propId={prop.id} />
|
|
</div>
|
|
</DashboardHeader>
|
|
</div>
|
|
|
|
<Tabs defaultValue="details" className="w-full">
|
|
<TabsList className="grid w-full md:w-auto grid-cols-2 md:grid-cols-3 mb-4">
|
|
<TabsTrigger value="details">道具详情</TabsTrigger>
|
|
<TabsTrigger value="batches">批次管理</TabsTrigger>
|
|
<TabsTrigger value="analytics">数据分析</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="details" className="space-y-6">
|
|
<div className="grid gap-6 md:grid-cols-3">
|
|
<Card className="md:col-span-1 border-none shadow-lg bg-white">
|
|
<CardHeader>
|
|
<CardTitle className="text-lg font-bold">道具预览</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="flex justify-center">
|
|
<div className="relative w-full aspect-square max-w-[300px] rounded-lg overflow-hidden bg-gray-100 flex items-center justify-center">
|
|
{prop.imageUrl && !prop.imageUrl.includes("placeholder") ? (
|
|
<img src={prop.imageUrl} alt={prop.name} className="object-cover w-full h-full" />
|
|
) : (
|
|
<div className="flex flex-col items-center justify-center text-gray-400">
|
|
<svg className="h-16 w-16 mb-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0022.5 18.75V5.25A2.25 2.25 0 0020.25 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z" />
|
|
</svg>
|
|
<span className="text-sm">暂无图片</span>
|
|
</div>
|
|
)}
|
|
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-3">
|
|
<Badge className={`${isPublished ? "bg-green-500" : "bg-gray-500"}`}>{prop.status || "未发布"}</Badge>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="md:col-span-2 border-none shadow-lg bg-gradient-to-br from-white to-purple-50">
|
|
<CardHeader>
|
|
<CardTitle className="text-lg font-bold">道具详情</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-1">
|
|
<p className="text-sm font-medium text-gray-500">道具类型</p>
|
|
<p className="font-medium">{prop.category || "-"}</p>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<p className="text-sm font-medium text-gray-500">稀有度</p>
|
|
<p className="font-medium">{prop.rarity || "-"}</p>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<p className="text-sm font-medium text-gray-500">发布日期</p>
|
|
<p className="font-medium">{prop.publishedAt || "尚未发布"}</p>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<p className="text-sm font-medium text-gray-500">激活数量</p>
|
|
<p className="font-medium">{activatedCount}</p>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<p className="text-sm font-medium text-gray-500">创建日期</p>
|
|
<p className="font-medium">{prop.createdAt || "-"}</p>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<p className="text-sm font-medium text-gray-500">激活率</p>
|
|
<p className="font-medium">{activationRate}%</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-6">
|
|
<p className="text-sm font-medium text-gray-500 mb-2">道具描述</p>
|
|
<p className="text-gray-700">{prop.description || "暂无描述"}</p>
|
|
</div>
|
|
|
|
{isPublished && (
|
|
<div className="mt-6 p-3 bg-amber-50 border border-amber-200 rounded-lg flex items-start">
|
|
<AlertTriangle className="h-5 w-5 text-amber-500 mr-2 flex-shrink-0 mt-0.5" />
|
|
<p className="text-sm text-amber-700">
|
|
{isSuperUser()
|
|
? "该道具已发布,您以超级管理员身份仍可编辑和删除。请谨慎操作。"
|
|
: "该道具已发布,基本属性不可修改。您仍可以增加印刷数量。"}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="batches" className="space-y-6">
|
|
<Card className="border-none shadow-lg bg-gradient-to-br from-white to-blue-50">
|
|
<CardHeader className="flex flex-row items-center justify-between">
|
|
<div>
|
|
<CardTitle className="text-lg font-bold">印刷批次管理</CardTitle>
|
|
<CardDescription>管理道具卡牌的印刷批次和卡牌ID</CardDescription>
|
|
</div>
|
|
<AddPrintBatchDialog propId={prop.id} isPublished={isPublished} />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="rounded-md border">
|
|
<table className="w-full">
|
|
<thead>
|
|
<tr className="bg-gray-50 border-b">
|
|
<th className="py-3 px-4 text-left text-sm font-medium text-gray-500">批次ID</th>
|
|
<th className="py-3 px-4 text-left text-sm font-medium text-gray-500">创建日期</th>
|
|
<th className="py-3 px-4 text-left text-sm font-medium text-gray-500">数量</th>
|
|
<th className="py-3 px-4 text-left text-sm font-medium text-gray-500">状态</th>
|
|
<th className="py-3 px-4 text-right text-sm font-medium text-gray-500">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td colSpan={5} className="py-8 text-center text-gray-500">
|
|
批次数据将从后端加载
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="border-none shadow-lg bg-white">
|
|
<CardHeader>
|
|
<CardTitle className="text-lg font-bold">批次操作</CardTitle>
|
|
<CardDescription>批量管理卡牌批次</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex flex-wrap gap-4">
|
|
<Button variant="outline" className="border-blue-200 hover:bg-blue-50 hover:text-blue-700">
|
|
<Download className="mr-2 h-4 w-4" />
|
|
导出所有批次
|
|
</Button>
|
|
<Button variant="outline" className="border-purple-200 hover:bg-purple-50 hover:text-purple-700">
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
批量添加批次
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="analytics" className="space-y-6">
|
|
<Card className="border-none shadow-lg bg-white">
|
|
<CardHeader>
|
|
<CardTitle className="text-lg font-bold">激活数据分析</CardTitle>
|
|
<CardDescription>道具卡牌激活情况统计</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="h-[300px] flex items-center justify-center bg-gray-50 rounded-lg">
|
|
<p className="text-gray-500">激活数据图表将在此显示</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<div className="grid gap-6 md:grid-cols-2">
|
|
<Card className="border-none shadow-lg bg-gradient-to-br from-white to-green-50">
|
|
<CardHeader>
|
|
<CardTitle className="text-lg font-bold">地区分布</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="h-[200px] flex items-center justify-center bg-gray-50 rounded-lg">
|
|
<p className="text-gray-500">地区分布图表将在此显示</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="border-none shadow-lg bg-gradient-to-br from-white to-blue-50">
|
|
<CardHeader>
|
|
<CardTitle className="text-lg font-bold">时间趋势</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="h-[200px] flex items-center justify-center bg-gray-50 rounded-lg">
|
|
<p className="text-gray-500">时间趋势图表将在此显示</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</DashboardShell>
|
|
)
|
|
}
|