467 lines
19 KiB
TypeScript
467 lines
19 KiB
TypeScript
"use client"
|
||
|
||
import { useState, useEffect } from "react"
|
||
import { useParams, useRouter } from "next/navigation"
|
||
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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||
import { Badge } from "@/components/ui/badge"
|
||
import { useToast } from "@/hooks/use-toast"
|
||
import { AddDanceDialog } from "@/components/dances/add-dance-dialog"
|
||
import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog"
|
||
import { ArrowLeft, Download, Edit, Play, Upload, FileText, AlertTriangle } from "lucide-react"
|
||
import type { Dance } from "@/lib/api/types"
|
||
|
||
// 模拟获取舞蹈详情的API
|
||
const getDanceById = async (id: string): Promise<Dance | null> => {
|
||
// 模拟API延迟
|
||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||
|
||
// 模拟舞蹈数据
|
||
const mockDances: Dance[] = [
|
||
{
|
||
id: "1",
|
||
name: "千本樱",
|
||
choreographer: "洛天依工作室",
|
||
duration: "3:45",
|
||
difficulty: "中等",
|
||
videoUrl: "/placeholder.svg?height=300&width=400",
|
||
coverUrl: "/placeholder.svg?height=300&width=400",
|
||
description: "基于《千本樱》歌曲的经典舞蹈编排,动作流畅优美,适合中等水平的舞者。",
|
||
motionFile: "senbonzakura_motion.fbx",
|
||
category: "日式",
|
||
tags: ["经典", "流行", "日式"],
|
||
createdAt: "2023-01-15T08:30:00Z",
|
||
updatedAt: "2023-02-20T14:15:00Z",
|
||
status: "已发布",
|
||
activatedCount: 1245,
|
||
printedCount: 2000,
|
||
},
|
||
{
|
||
id: "2",
|
||
name: "权御天下",
|
||
choreographer: "洛天依动作组",
|
||
duration: "4:20",
|
||
difficulty: "高级",
|
||
videoUrl: "/placeholder.svg?height=300&width=400",
|
||
coverUrl: "/placeholder.svg?height=300&width=400",
|
||
description: "中国风舞蹈,动作幅度大,需要较高的舞蹈基础,展现古风韵味。",
|
||
motionFile: "quanyutianxia_motion.fbx",
|
||
category: "中国风",
|
||
tags: ["高难度", "中国风", "古风"],
|
||
createdAt: "2023-03-10T10:45:00Z",
|
||
updatedAt: "2023-04-05T16:30:00Z",
|
||
status: "已发布",
|
||
activatedCount: 1823,
|
||
printedCount: 3000,
|
||
},
|
||
{
|
||
id: "3",
|
||
name: "达拉崩吧",
|
||
choreographer: "洛天依舞蹈工作室",
|
||
duration: "3:10",
|
||
difficulty: "初级",
|
||
videoUrl: "/placeholder.svg?height=300&width=400",
|
||
coverUrl: "/placeholder.svg?height=300&width=400",
|
||
description: "轻松欢快的舞蹈,适合初学者,动作简单易学。",
|
||
motionFile: "dalabengba_motion.fbx",
|
||
category: "流行",
|
||
tags: ["简单", "欢快", "流行"],
|
||
createdAt: "2023-05-20T09:15:00Z",
|
||
updatedAt: "2023-05-25T11:20:00Z",
|
||
status: "已发布",
|
||
activatedCount: 1356,
|
||
printedCount: 2500,
|
||
},
|
||
{
|
||
id: "4",
|
||
name: "普通DISCO",
|
||
choreographer: "洛天依动作设计组",
|
||
duration: "3:30",
|
||
difficulty: "中等",
|
||
videoUrl: "/placeholder.svg?height=300&width=400",
|
||
coverUrl: "/placeholder.svg?height=300&width=400",
|
||
description: "现代disco风格舞蹈,节奏感强,动作活力四射。",
|
||
motionFile: "disco_motion.fbx",
|
||
category: "现代",
|
||
tags: ["活力", "现代", "disco"],
|
||
createdAt: "2023-06-05T14:30:00Z",
|
||
updatedAt: "2023-06-10T17:45:00Z",
|
||
status: "已发布",
|
||
activatedCount: 1578,
|
||
printedCount: 3000,
|
||
},
|
||
{
|
||
id: "5",
|
||
name: "华灯宴",
|
||
choreographer: "古风舞蹈团队",
|
||
duration: "4:50",
|
||
difficulty: "高级",
|
||
videoUrl: "/placeholder.svg?height=300&width=400",
|
||
coverUrl: "/placeholder.svg?height=300&width=400",
|
||
description: "古风舞蹈,动作优美细腻,需要较高的舞蹈技巧和表现力。",
|
||
motionFile: "hualangyan_motion.fbx",
|
||
category: "中国风",
|
||
tags: ["古风", "优美", "高难度"],
|
||
createdAt: "2023-07-12T11:20:00Z",
|
||
updatedAt: "2023-07-18T13:40:00Z",
|
||
status: "未发布",
|
||
activatedCount: 0,
|
||
printedCount: 2000,
|
||
},
|
||
]
|
||
|
||
const dance = mockDances.find((dance) => dance.id === id)
|
||
return dance || null
|
||
}
|
||
|
||
export default function DanceDetailPage() {
|
||
const params = useParams()
|
||
const router = useRouter()
|
||
const { toast } = useToast()
|
||
const [dance, setDance] = useState<Dance | null>(null)
|
||
const [isLoading, setIsLoading] = useState(true)
|
||
const [isPlaying, setIsPlaying] = useState(false)
|
||
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
|
||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
|
||
|
||
const id = params.id as string
|
||
|
||
useEffect(() => {
|
||
const loadDance = async () => {
|
||
setIsLoading(true)
|
||
try {
|
||
const data = await getDanceById(id)
|
||
setDance(data)
|
||
} catch (error) {
|
||
toast({
|
||
title: "加载失败",
|
||
description: "无法加载舞蹈详情,请重试。",
|
||
variant: "destructive",
|
||
})
|
||
} finally {
|
||
setIsLoading(false)
|
||
}
|
||
}
|
||
|
||
loadDance()
|
||
}, [id, toast])
|
||
|
||
const handleEditDance = (updatedDance: Dance) => {
|
||
setDance(updatedDance)
|
||
toast({
|
||
title: "更新成功",
|
||
description: `舞蹈 ${updatedDance.name} 已成功更新`,
|
||
})
|
||
}
|
||
|
||
const handleDeleteDance = async () => {
|
||
// 模拟删除API
|
||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||
|
||
toast({
|
||
title: "删除成功",
|
||
description: `舞蹈 ${dance?.name} 已成功删除`,
|
||
variant: "destructive",
|
||
})
|
||
|
||
// 返回舞蹈列表页
|
||
router.push("/dances")
|
||
}
|
||
|
||
const togglePlay = () => {
|
||
setIsPlaying(!isPlaying)
|
||
}
|
||
|
||
if (isLoading) {
|
||
return (
|
||
<DashboardShell>
|
||
<DashboardHeader heading="舞蹈详情" text="加载中...">
|
||
<Button variant="outline" onClick={() => router.back()}>
|
||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||
返回
|
||
</Button>
|
||
</DashboardHeader>
|
||
<div className="grid gap-4">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>
|
||
<div className="h-6 w-[200px] bg-gray-200 rounded animate-pulse"></div>
|
||
</CardTitle>
|
||
<CardDescription>
|
||
<div className="h-4 w-[300px] bg-gray-200 rounded animate-pulse"></div>
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="h-[400px] bg-gray-200 rounded animate-pulse"></div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</DashboardShell>
|
||
)
|
||
}
|
||
|
||
if (!dance) {
|
||
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">找不到ID为 {id} 的舞蹈</p>
|
||
<Button asChild>
|
||
<a href="/dances">
|
||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||
返回舞蹈列表
|
||
</a>
|
||
</Button>
|
||
</div>
|
||
</DashboardShell>
|
||
)
|
||
}
|
||
|
||
const isPublished = dance.status === "已发布"
|
||
|
||
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>
|
||
<a href="/dances">
|
||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||
返回列表
|
||
</a>
|
||
</Button>
|
||
<DashboardHeader heading={dance.name} text={`舞蹈ID: ${dance.id}`}>
|
||
<div className="flex space-x-2 ml-auto">
|
||
{!isPublished && (
|
||
<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"
|
||
>
|
||
<a href={`/dances/edit/${params.id}`}>
|
||
<Edit className="mr-2 h-4 w-4" />
|
||
编辑舞蹈
|
||
</a>
|
||
</Button>
|
||
)}
|
||
<Button
|
||
variant="outline"
|
||
onClick={() => {
|
||
// 模拟下载动作文件
|
||
toast({
|
||
title: "开始下载",
|
||
description: `正在下载舞蹈动作文件`,
|
||
})
|
||
}}
|
||
>
|
||
<Download className="mr-2 h-4 w-4" />
|
||
导出动作文件
|
||
</Button>
|
||
</div>
|
||
</DashboardHeader>
|
||
</div>
|
||
|
||
<Tabs defaultValue="details" className="space-y-4">
|
||
<TabsList>
|
||
<TabsTrigger value="details">舞蹈详情</TabsTrigger>
|
||
<TabsTrigger value="batches">批次管理</TabsTrigger>
|
||
<TabsTrigger value="analytics">数据分析</TabsTrigger>
|
||
</TabsList>
|
||
|
||
<TabsContent value="details" className="space-y-4">
|
||
<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 group">
|
||
<img
|
||
src={dance.coverUrl || "/placeholder.svg?height=300&width=300"}
|
||
alt={dance.name}
|
||
className="object-cover"
|
||
/>
|
||
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center cursor-pointer">
|
||
<Play className="h-16 w-16 text-white" />
|
||
</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"}`}>{dance.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">{dance.choreographer}</p>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<p className="text-sm font-medium text-gray-500">分类</p>
|
||
<p className="font-medium">{dance.category}</p>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<p className="text-sm font-medium text-gray-500">时长</p>
|
||
<p className="font-medium">{dance.duration}</p>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<p className="text-sm font-medium text-gray-500">难度</p>
|
||
<p className="font-medium">{dance.difficulty}</p>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<p className="text-sm font-medium text-gray-500">激活数量</p>
|
||
<p className="font-medium">{dance.activatedCount}</p>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<p className="text-sm font-medium text-gray-500">印刷总数</p>
|
||
<p className="font-medium">{dance.printedCount}</p>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<p className="text-sm font-medium text-gray-500">剩余库存</p>
|
||
<p className="font-medium">{dance.printedCount - dance.activatedCount}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mt-4">
|
||
<p className="text-sm font-medium text-gray-500 mb-2">标签</p>
|
||
<div className="flex flex-wrap gap-2">
|
||
{dance.tags?.map((tag, index) => (
|
||
<Badge key={index} variant="outline" className="bg-purple-50 text-purple-700">
|
||
{tag}
|
||
</Badge>
|
||
))}
|
||
</div>
|
||
</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">该舞蹈已发布,基本属性不可修改。您仍可以增加印刷数量。</p>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
<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>
|
||
<p className="text-gray-700">{dance.description || "暂无描述"}</p>
|
||
</CardContent>
|
||
</Card>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="batches" className="space-y-4">
|
||
<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>
|
||
<Button className="bg-gradient-to-r from-pink-500 to-purple-600 hover:from-pink-600 hover:to-purple-700">
|
||
<Upload className="mr-2 h-4 w-4" />
|
||
添加批次
|
||
</Button>
|
||
</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">起始ID</th>
|
||
<th className="py-3 px-4 text-left text-sm font-medium text-gray-500">结束ID</th>
|
||
<th className="py-3 px-4 text-right text-sm font-medium text-gray-500">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr className="border-b hover:bg-gray-50">
|
||
<td className="py-3 px-4 text-sm font-medium">B001</td>
|
||
<td className="py-3 px-4 text-sm">2023-04-01</td>
|
||
<td className="py-3 px-4 text-sm">1000</td>
|
||
<td className="py-3 px-4 text-sm font-mono text-xs">DNC{dance.id}-0001</td>
|
||
<td className="py-3 px-4 text-sm font-mono text-xs">DNC{dance.id}-1000</td>
|
||
<td className="py-3 px-4 text-right">
|
||
<Button variant="ghost" size="sm" className="h-8 hover:bg-blue-50 hover:text-blue-600">
|
||
<FileText className="h-4 w-4 mr-1" />
|
||
导出ID
|
||
</Button>
|
||
</td>
|
||
</tr>
|
||
{isPublished && (
|
||
<tr className="border-b hover:bg-gray-50">
|
||
<td className="py-3 px-4 text-sm font-medium">B002</td>
|
||
<td className="py-3 px-4 text-sm">2023-08-15</td>
|
||
<td className="py-3 px-4 text-sm">1000</td>
|
||
<td className="py-3 px-4 text-sm font-mono text-xs">DNC{dance.id}-1001</td>
|
||
<td className="py-3 px-4 text-sm font-mono text-xs">DNC{dance.id}-2000</td>
|
||
<td className="py-3 px-4 text-right">
|
||
<Button variant="ghost" size="sm" className="h-8 hover:bg-blue-50 hover:text-blue-600">
|
||
<FileText className="h-4 w-4 mr-1" />
|
||
导出ID
|
||
</Button>
|
||
</td>
|
||
</tr>
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="analytics" className="space-y-4">
|
||
<Card className="border-none shadow-lg bg-gradient-to-br from-white to-green-50">
|
||
<CardHeader>
|
||
<CardTitle className="text-lg font-bold">数据分析</CardTitle>
|
||
<CardDescription>查看舞蹈卡牌的激活数据和使用情况</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid gap-6 md:grid-cols-2">
|
||
<div className="h-[300px] bg-gray-50 rounded-lg flex items-center justify-center">
|
||
<p className="text-gray-500">激活数据图表</p>
|
||
</div>
|
||
<div className="h-[300px] bg-gray-50 rounded-lg flex items-center justify-center">
|
||
<p className="text-gray-500">地区分布图表</p>
|
||
</div>
|
||
<div className="md:col-span-2 h-[300px] bg-gray-50 rounded-lg flex items-center justify-center">
|
||
<p className="text-gray-500">时间趋势图表</p>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</TabsContent>
|
||
</Tabs>
|
||
|
||
{/* 编辑舞蹈对话框 */}
|
||
<AddDanceDialog
|
||
open={isEditDialogOpen}
|
||
onOpenChange={setIsEditDialogOpen}
|
||
onDanceAdded={handleEditDance}
|
||
editDance={dance}
|
||
/>
|
||
|
||
{/* 删除确认对话框 */}
|
||
<DeleteConfirmationDialog
|
||
open={isDeleteDialogOpen}
|
||
onOpenChange={setIsDeleteDialogOpen}
|
||
onConfirm={handleDeleteDance}
|
||
title="删除舞蹈"
|
||
description={`确定要删除舞蹈 "${dance.name}" 吗?此操作无法撤销。`}
|
||
/>
|
||
</DashboardShell>
|
||
)
|
||
}
|