lty/qy-lty-admin/components/songs/add-song-dialog.tsx
2026-03-17 13:17:02 +08:00

338 lines
13 KiB
TypeScript

"use client"
import { useState, useEffect, useRef } from "react"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
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 { Plus, Upload, Loader2, Music } from "lucide-react"
import { Switch } from "@/components/ui/switch"
import type { Song } from "./song-detail-dialog"
import { uploadSongFile, createSong, updateSong } from "@/lib/api/songs"
type AddSongDialogProps = {
mode?: "create" | "edit"
initialSong?: Song
open?: boolean
onOpenChange?: (open: boolean) => void
onSave?: (song: Song) => void
}
export function AddSongDialog({
mode = "create",
initialSong,
open: controlledOpen,
onOpenChange: setControlledOpen,
onSave,
}: AddSongDialogProps) {
const [open, setOpen] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
// 表单状态
const [name, setName] = useState("")
const [songType, setSongType] = useState("")
const [composer, setComposer] = useState("")
const [lyricist, setLyricist] = useState("")
const [duration, setDuration] = useState("")
const [bpm, setBpm] = useState("")
const [description, setDescription] = useState("")
const [isOriginal, setIsOriginal] = useState(true)
const [previewId, setPreviewId] = useState(
"SNG" +
Math.floor(Math.random() * 1000)
.toString()
.padStart(3, "0"),
)
const [audioFile, setAudioFile] = useState<File | null>(null)
const [audioUrl, setAudioUrl] = useState<string>("")
const [isUploading, setIsUploading] = useState(false)
const [uploadError, setUploadError] = useState<string>("")
const audioInputRef = useRef<HTMLInputElement>(null)
// 当编辑模式且有歌曲数据时,初始化表单
useEffect(() => {
if (mode === "edit" && initialSong) {
setName(initialSong.name)
setComposer(initialSong.composer)
setLyricist(initialSong.lyricist)
setDuration(initialSong.duration)
setIsOriginal(true) // 假设所有编辑的歌曲都是原创
setPreviewId(initialSong.id)
// 初始化歌曲类型
if (initialSong.genre) {
setSongType(initialSong.genre)
}
// 初始化BPM
if (initialSong.bpm) {
setBpm(String(initialSong.bpm))
}
// 初始化描述
if (initialSong.description) {
setDescription(initialSong.description)
}
// 初始化音频URL
if (initialSong.audioUrl) {
setAudioUrl(initialSong.audioUrl)
}
}
}, [mode, initialSong])
// 处理受控和非受控开关状态
useEffect(() => {
if (controlledOpen !== undefined) {
setOpen(controlledOpen)
}
}, [controlledOpen])
// 处理对话框关闭
const handleOpenChange = (newOpen: boolean) => {
setOpen(newOpen)
if (setControlledOpen) {
setControlledOpen(newOpen)
}
// 如果关闭对话框,重置表单(仅在创建模式下)
if (!newOpen && mode === "create") {
resetForm()
}
}
// 重置表单
const resetForm = () => {
if (mode === "create") {
setName("")
setSongType("")
setComposer("")
setLyricist("")
setDuration("")
setBpm("")
setDescription("")
setIsOriginal(true)
setPreviewId(
"SNG" +
Math.floor(Math.random() * 1000)
.toString()
.padStart(3, "0"),
)
}
}
// 歌曲文件选择和上传
const handleAudioFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
setAudioFile(file)
setIsUploading(true)
setUploadError("")
try {
const res = await uploadSongFile({ file, isPermanent: true, filename: file.name })
setAudioUrl(res.url)
} catch (err: any) {
setUploadError(err.message || "上传失败")
setAudioUrl("")
} finally {
setIsUploading(false)
}
}
const handleSubmit = async () => {
// 表单验证
if (!name || !composer || !lyricist || !duration) {
alert("请填写所有必填字段!")
return
}
// 创建模式下必须上传音频
if (mode === "create" && !audioUrl) {
alert("请上传歌曲文件!")
return
}
setIsSubmitting(true)
try {
// 构造基本的请求数据
const songAttributes: any = {
genre: songType || undefined,
duration,
composer,
lyricist,
arrangement: isOriginal ? undefined : "",
lyrics: description || undefined,
}
// 只有在有音频URL时才添加
if (audioUrl) {
songAttributes.audio_file = encodeURI(audioUrl)
}
// 只有在有BPM时才添加
if (bpm) {
songAttributes.bpm = Number(bpm)
}
if (mode === "create") {
// 创建模式
const payload = {
name,
category: "song",
card_type: "regular",
rarity: "epic",
price: 39.99,
status: "draft",
description: description || undefined,
song_attributes: songAttributes
}
const created = await createSong(payload)
if (onSave) onSave(created)
} else if (mode === "edit" && initialSong) {
// 编辑模式
const payload = {
name,
category: "song",
song_attributes: songAttributes
}
const updated = await updateSong(initialSong.id, payload)
if (onSave) onSave(updated)
}
handleOpenChange(false)
} catch (error: any) {
alert(error.message || (mode === "create" ? "创建歌曲失败" : "更新歌曲失败"))
} finally {
setIsSubmitting(false)
}
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
{mode === "create" && (
<DialogTrigger asChild>
<Button 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">
<Plus className="mr-2 h-4 w-4" />
</Button>
</DialogTrigger>
)}
<DialogContent className="sm:max-w-[520px] p-4">
<DialogHeader>
<DialogTitle className="text-lg font-bold bg-clip-text text-transparent bg-gradient-to-r from-purple-600 to-pink-600">
{mode === "create" ? "添加新歌曲" : "编辑歌曲"}
</DialogTitle>
<DialogDescription className="text-xs mt-1 mb-2">{mode === "create" ? "填写歌曲信息以创建新的歌曲。" : "修改歌曲信息。"}</DialogDescription>
</DialogHeader>
<div className="grid grid-cols-2 gap-x-3 gap-y-2 py-2">
<div className="space-y-1">
<Label htmlFor="name" className="text-xs"> <span className="text-red-500">*</span></Label>
<Input id="name" placeholder="输入歌曲名称" className="h-8 text-xs border-gray-300 focus-visible:ring-pink-500" value={name} onChange={(e) => setName(e.target.value)} required />
</div>
<div className="space-y-1">
<Label htmlFor="type" className="text-xs"></Label>
<Select value={songType} onValueChange={setSongType}>
<SelectTrigger className="h-8 text-xs border-gray-300 focus:ring-pink-500">
<SelectValue placeholder="选择歌曲类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="pop"></SelectItem>
<SelectItem value="rock"></SelectItem>
<SelectItem value="electronic"></SelectItem>
<SelectItem value="folk"></SelectItem>
<SelectItem value="classical"></SelectItem>
<SelectItem value="other"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label htmlFor="composer" className="text-xs"> <span className="text-red-500">*</span></Label>
<Input id="composer" placeholder="输入作曲者" className="h-8 text-xs border-gray-300 focus-visible:ring-pink-500" value={composer} onChange={(e) => setComposer(e.target.value)} required />
</div>
<div className="space-y-1">
<Label htmlFor="lyricist" className="text-xs"> <span className="text-red-500">*</span></Label>
<Input id="lyricist" placeholder="输入作词者" className="h-8 text-xs border-gray-300 focus-visible:ring-pink-500" value={lyricist} onChange={(e) => setLyricist(e.target.value)} required />
</div>
<div className="space-y-1">
<Label htmlFor="duration" className="text-xs"> <span className="text-red-500">*</span></Label>
<Input id="duration" placeholder="格式: MM:SS" className="h-8 text-xs border-gray-300 focus-visible:ring-pink-500" value={duration} onChange={(e) => setDuration(e.target.value)} required />
</div>
<div className="space-y-1">
<Label htmlFor="bpm" className="text-xs">BPM</Label>
<Input id="bpm" type="number" placeholder="输入BPM" className="h-8 text-xs border-gray-300 focus-visible:ring-pink-500" value={bpm} onChange={(e) => setBpm(e.target.value)} />
</div>
<div className="space-y-1">
<Label className="text-xs"> {mode === "create" && <span className="text-red-500">*</span>}</Label>
<div
className="relative border-2 border-dashed border-gray-300 rounded-md p-2 flex flex-col items-center justify-center hover:border-pink-500 transition-colors cursor-pointer min-h-[60px]"
onClick={() => !isUploading && !isSubmitting && audioInputRef.current?.click()}
style={{ position: 'relative' }}
>
<Music className="h-5 w-5 text-gray-400 mb-1" />
<span className="text-xs text-gray-500">{audioFile ? audioFile.name : "上传"}</span>
<input
ref={audioInputRef}
type="file"
accept="audio/*"
className="hidden"
onChange={handleAudioFileChange}
disabled={isUploading || isSubmitting}
/>
{isUploading && <span className="text-xs text-pink-500 mt-1">...</span>}
{audioUrl && !isUploading && <span className="text-xs text-green-600 mt-1"></span>}
{uploadError && <span className="text-xs text-red-500 mt-1">{uploadError}</span>}
</div>
</div>
<div className="space-y-1">
<Label className="text-xs"></Label>
<div className="border-2 border-dashed border-gray-300 rounded-md p-2 flex flex-col items-center justify-center hover:border-pink-500 transition-colors cursor-pointer min-h-[60px]">
<Upload className="h-5 w-5 text-gray-400 mb-1" />
<span className="text-xs text-gray-500"></span>
</div>
</div>
</div>
{/* 可折叠描述和预览信息 */}
<div className="mt-1">
<details className="mb-1">
<summary className="cursor-pointer text-xs text-gray-600 select-none"></summary>
<Textarea id="description" placeholder="输入歌曲描述" className="min-h-[60px] text-xs border-gray-300 focus-visible:ring-pink-500 mt-1" value={description} onChange={(e) => setDescription(e.target.value)} />
</details>
{mode === "create" && (
<details>
<summary className="cursor-pointer text-xs text-gray-600 select-none"></summary>
<div className="p-2 bg-gray-50 rounded mt-1">
<p className="text-xs text-gray-600">ID: <span className="font-medium text-pink-600">{previewId}</span></p>
<p className="text-xs text-gray-600 mt-1">: <span className="font-medium"></span></p>
</div>
</details>
)}
</div>
<div className="flex items-center space-x-2 pt-1 pb-2">
<Switch id="original" checked={isOriginal} onCheckedChange={setIsOriginal} />
<Label htmlFor="original" className="cursor-pointer text-xs"></Label>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => handleOpenChange(false)} disabled={isSubmitting} className="h-8 px-4 text-xs"></Button>
<Button className="h-8 px-4 text-xs bg-gradient-to-r from-pink-500 to-purple-600 hover:from-pink-600 hover:to-purple-700" onClick={handleSubmit} disabled={isSubmitting}>
{isSubmitting ? (<><Loader2 className="mr-2 h-4 w-4 animate-spin" />{mode === "create" ? "创建中..." : "更新中..."}</>) : mode === "create" ? "创建歌曲" : "更新歌曲"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}