lty/qy-lty-admin/components/dances/add-dance-dialog.tsx
pmc bd95ba470c feat: update admin panel, API modules, and add migrations
- 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>
2026-03-20 13:06:50 +08:00

418 lines
15 KiB
TypeScript

"use client"
import type React from "react"
import { useState, useEffect } from "react"
import type { Dance } from "@/lib/api/types"
import { useToast } from "@/hooks/use-toast"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Loader2, Trash2 } from "lucide-react"
import { FileUpload } from "@/components/ui/file-upload"
interface AddDanceDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
onDanceAdded: (dance: Dance) => void
editDance?: Dance
}
export function AddDanceDialog({ open, onOpenChange, onDanceAdded, editDance }: AddDanceDialogProps) {
const { toast } = useToast()
const [isSubmitting, setIsSubmitting] = useState(false)
// 表单状态
const [formData, setFormData] = useState<Partial<Dance>>({
name: "",
choreographer: "",
duration: "",
difficulty: "中等",
description: "",
category: "流行",
tags: [],
motionFile: "",
videoUrl: "",
coverUrl: "",
})
// 预览ID
const [previewId, setPreviewId] = useState(
"DNC" +
Math.floor(Math.random() * 1000)
.toString()
.padStart(3, "0"),
)
// 当编辑模式且有舞蹈数据时,初始化表单
useEffect(() => {
if (editDance) {
setFormData({
name: editDance.name || "",
choreographer: editDance.choreographer || "",
duration: editDance.duration || "",
difficulty: editDance.difficulty || "中等",
description: editDance.description || "",
category: editDance.category || "流行",
tags: editDance.tags || [],
motionFile: editDance.motionFile || "",
videoUrl: editDance.videoUrl || "",
coverUrl: editDance.coverUrl || "",
})
} else {
// 重置表单
setFormData({
name: "",
choreographer: "",
duration: "",
difficulty: "中等",
description: "",
category: "流行",
tags: [],
motionFile: "",
videoUrl: "",
coverUrl: "",
})
// 生成新的预览ID
setPreviewId(
"DNC" +
Math.floor(Math.random() * 1000)
.toString()
.padStart(3, "0"),
)
}
}, [editDance, open])
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target
setFormData((prev) => ({ ...prev, [name]: value }))
}
const handleSelectChange = (name: string, value: string) => {
setFormData((prev) => ({ ...prev, [name]: value }))
}
const handleTagsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const tagsString = e.target.value
const tagsArray = tagsString
.split(",")
.map((tag) => tag.trim())
.filter((tag) => tag !== "")
setFormData((prev) => ({ ...prev, tags: tagsArray }))
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
// 表单验证
if (!formData.name) {
toast({
title: "表单不完整",
description: "请填写舞蹈名称",
variant: "destructive",
})
return
}
setIsSubmitting(true)
try {
// 模拟API请求延迟
await new Promise((resolve) => setTimeout(resolve, 1000))
const newDance: Dance = {
id: editDance?.id || previewId,
name: formData.name || "",
choreographer: formData.choreographer || "",
duration: formData.duration || "",
difficulty: formData.difficulty || "中等",
description: formData.description || "",
category: formData.category || "流行",
tags: formData.tags || [],
motionFile: formData.motionFile || "",
videoUrl: formData.videoUrl || "",
coverUrl: formData.coverUrl || editDance?.coverUrl || "/placeholder.svg?height=300&width=400",
createdAt: editDance?.createdAt || new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
onDanceAdded(newDance)
onOpenChange(false)
} catch (error) {
toast({
title: "操作失败",
description: "添加或更新舞蹈时出现错误,请重试。",
variant: "destructive",
})
} finally {
setIsSubmitting(false)
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-purple-600 to-pink-600">
{editDance ? "编辑舞蹈" : "添加新舞蹈"}
</DialogTitle>
<DialogDescription>
{editDance ? "修改舞蹈信息,完成后点击保存。" : "填写舞蹈信息,完成后点击添加。"}
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="name">
<span className="text-red-500">*</span>
</Label>
<Input
id="name"
name="name"
value={formData.name || ""}
onChange={handleChange}
className="border-gray-300 focus-visible:ring-purple-500"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="choreographer"></Label>
<Input
id="choreographer"
name="choreographer"
value={formData.choreographer || ""}
onChange={handleChange}
className="border-gray-300 focus-visible:ring-purple-500"
/>
</div>
<div className="space-y-2">
<Label htmlFor="duration"></Label>
<Input
id="duration"
name="duration"
value={formData.duration || ""}
onChange={handleChange}
placeholder="例如: 3:45"
className="border-gray-300 focus-visible:ring-purple-500"
/>
</div>
<div className="space-y-2">
<Label htmlFor="difficulty"></Label>
<Select
value={formData.difficulty || "中等"}
onValueChange={(value) => handleSelectChange("difficulty", value)}
>
<SelectTrigger className="border-gray-300 focus:ring-purple-500">
<SelectValue placeholder="选择难度" />
</SelectTrigger>
<SelectContent>
<SelectItem value="初级"></SelectItem>
<SelectItem value="中等"></SelectItem>
<SelectItem value="中高级"></SelectItem>
<SelectItem value="高级"></SelectItem>
<SelectItem value="专业"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="category"></Label>
<Select
value={formData.category || "流行"}
onValueChange={(value) => handleSelectChange("category", value)}
>
<SelectTrigger className="border-gray-300 focus:ring-purple-500">
<SelectValue placeholder="选择分类" />
</SelectTrigger>
<SelectContent>
<SelectItem value="流行"></SelectItem>
<SelectItem value="中国风"></SelectItem>
<SelectItem value="日式"></SelectItem>
<SelectItem value="现代"></SelectItem>
<SelectItem value="古典"></SelectItem>
<SelectItem value="街舞"></SelectItem>
<SelectItem value="其他"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="tags"></Label>
<Input
id="tags"
name="tags"
value={formData.tags?.join(", ") || ""}
onChange={handleTagsChange}
placeholder="用逗号分隔多个标签"
className="border-gray-300 focus-visible:ring-purple-500"
/>
</div>
<div className="space-y-2">
<Label htmlFor="motionFile"></Label>
<Input
id="motionFile"
name="motionFile"
value={formData.motionFile || ""}
onChange={handleChange}
placeholder="动作文件名称,例如: dance_motion.fbx"
className="border-gray-300 focus-visible:ring-purple-500"
/>
</div>
<div className="space-y-2">
<Label htmlFor="videoUrl"></Label>
<Input
id="videoUrl"
name="videoUrl"
value={formData.videoUrl || ""}
onChange={handleChange}
placeholder="舞蹈视频链接"
className="border-gray-300 focus-visible:ring-purple-500"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="description"></Label>
<Textarea
id="description"
name="description"
value={formData.description || ""}
onChange={handleChange}
rows={4}
placeholder="请输入舞蹈的详细描述..."
className="min-h-[100px] border-gray-300 focus-visible:ring-purple-500"
/>
</div>
<div className="space-y-2">
<Label className="text-right"></Label>
{formData.coverUrl && !formData.coverUrl.includes('placeholder') ? (
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-lg border">
<div className="flex items-center space-x-3">
<img
src={formData.coverUrl}
alt="舞蹈封面"
className="h-16 w-16 object-cover rounded border"
/>
<span className="text-sm text-gray-700"></span>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setFormData((prev) => ({ ...prev, coverUrl: "" }))}
className="text-red-500 hover:text-red-700 hover:bg-red-50"
disabled={isSubmitting}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
) : (
<FileUpload
imageOnly={true}
multiple={false}
maxFiles={1}
maxSize={5 * 1024 * 1024}
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.gif', '.webp'] }}
placeholder="点击或拖拽上传舞蹈封面"
disabled={isSubmitting}
onUploadSuccess={(files) => {
if (files.length > 0) {
setFormData((prev) => ({ ...prev, coverUrl: files[0].url }))
}
}}
/>
)}
</div>
<div className="space-y-2">
<Label className="text-right"></Label>
{formData.motionFile ? (
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-lg border">
<div className="flex items-center space-x-3">
<div className="h-10 w-10 bg-purple-100 rounded border flex items-center justify-center">
<span className="text-xs font-medium text-purple-600">FBX</span>
</div>
<span className="text-sm text-gray-700"></span>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setFormData((prev) => ({ ...prev, motionFile: "" }))}
className="text-red-500 hover:text-red-700 hover:bg-red-50"
disabled={isSubmitting}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
) : (
<FileUpload
imageOnly={false}
multiple={false}
maxFiles={1}
maxSize={20 * 1024 * 1024}
accept={{ 'application/octet-stream': ['.fbx', '.bvh'] }}
placeholder="点击或拖拽上传动作文件"
disabled={isSubmitting}
onUploadSuccess={(files) => {
if (files.length > 0) {
setFormData((prev) => ({ ...prev, motionFile: files[0].url }))
}
}}
/>
)}
</div>
{!editDance && (
<div className="p-4 bg-gray-50 rounded-lg">
<h3 className="text-sm font-medium mb-2"></h3>
<p className="text-sm text-gray-600">
ID: <span className="font-medium text-purple-600">{previewId}</span>
</p>
</div>
)}
<DialogFooter>
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={isSubmitting}>
</Button>
<Button
type="submit"
className="bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600"
disabled={isSubmitting}
>
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{editDance ? "更新中..." : "创建中..."}
</>
) : editDance ? (
"保存修改"
) : (
"添加舞蹈"
)}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}