lty/qy-lty-admin/components/home-decor/add-home-decor-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

394 lines
14 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useState, useEffect } 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, AlertTriangle, Loader2, Trash2 } from "lucide-react"
import { Switch } from "@/components/ui/switch"
import { FileUpload } from "@/components/ui/file-upload"
import type { HomeDecor } from "./home-decor-detail-dialog"
import { createHomeDecor, updateHomeDecor } from "@/lib/api/home-decor"
// 判断是否为有效图片URL非placeholder
function isRealImageUrl(url?: string): boolean {
if (!url) return false
return !url.includes("placeholder")
}
// 中文稀有度 -> 后端 rarity key
const RARITY_MAP: Record<string, string> = {
"普通": "common",
"稀有": "rare",
"史诗": "epic",
"传说": "legendary",
}
type AddHomeDecorDialogProps = {
mode?: "create" | "edit"
initialDecor?: HomeDecor
open?: boolean
onOpenChange?: (open: boolean) => void
onSave?: (decor: HomeDecor) => void
}
export function AddHomeDecorDialog({
mode = "create",
initialDecor,
open: controlledOpen,
onOpenChange: setControlledOpen,
onSave,
}: AddHomeDecorDialogProps) {
const [open, setOpen] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
// 表单状态
const [name, setName] = useState("")
const [decorType, setDecorType] = useState("")
const [rarity, setRarity] = useState("")
const [description, setDescription] = useState("")
const [isLimited, setIsLimited] = useState(false)
const [imageUrl, setImageUrl] = useState<string | undefined>()
const [previewId, setPreviewId] = useState(
"DEC" +
Math.floor(Math.random() * 1000)
.toString()
.padStart(3, "0"),
)
// 当编辑模式且有装饰数据时,初始化表单
useEffect(() => {
if (mode === "edit" && initialDecor) {
setName(initialDecor.name)
setDecorType(initialDecor.type)
setRarity(initialDecor.rarity)
setDescription(initialDecor.description)
setIsLimited(initialDecor.type.includes("限定"))
setPreviewId(initialDecor.id)
setImageUrl(isRealImageUrl(initialDecor.image) ? initialDecor.image : undefined)
}
}, [mode, initialDecor])
// 处理受控和非受控开关状态
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("")
setDecorType("")
setRarity("")
setDescription("")
setIsLimited(false)
setImageUrl(undefined)
setPreviewId(
"DEC" +
Math.floor(Math.random() * 1000)
.toString()
.padStart(3, "0"),
)
}
}
const handleSubmit = async () => {
// 表单验证
if (!name || !decorType || !rarity || !description) {
alert("请填写所有必填字段!")
return
}
setIsSubmitting(true)
try {
const actualType = isLimited ? `限定${decorType}` : decorType
if (mode === "create") {
// 调用后端创建 API
const created = await createHomeDecor({
name,
description,
category: actualType,
rarityValue: RARITY_MAP[rarity] || rarity,
imageUrl,
})
// 构建组件格式的 HomeDecor 用于回调
const decor: HomeDecor = {
id: created.id,
name: created.name,
type: created.category || actualType,
rarity: created.rarity || rarity,
description: created.description || description,
releaseDate: created.createdAt || "",
status: created.status || "未发布",
activatedCount: created.activeCardsCount || 0,
image: created.imageUrl || "/placeholder.svg?height=300&width=300",
}
if (onSave) {
onSave(decor)
}
} else {
// 调用后端更新 API
const updated = await updateHomeDecor(initialDecor!.id, {
name,
description,
category: actualType,
imageUrl,
})
const decor: HomeDecor = {
id: updated.id,
name: updated.name,
type: updated.category || actualType,
rarity: updated.rarity || rarity,
description: updated.description || description,
releaseDate: initialDecor?.releaseDate || "",
status: updated.status || initialDecor?.status || "未发布",
activatedCount: updated.activeCardsCount || initialDecor?.activatedCount || 0,
image: updated.imageUrl || initialDecor?.image || "/placeholder.svg?height=300&width=300",
}
if (onSave) {
onSave(decor)
}
}
// 关闭对话框
handleOpenChange(false)
} catch (error) {
console.error(mode === "create" ? "创建家居装饰失败:" : "更新家居装饰失败:", error)
alert(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-[550px]">
<DialogHeader>
<DialogTitle className="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-purple-600 to-pink-600">
{mode === "create" ? "添加新家居装饰" : "编辑家居装饰"}
</DialogTitle>
<DialogDescription>
{mode === "create"
? "填写家居装饰信息以创建新的装饰卡牌。创建后将生成唯一的卡牌ID。"
: "修改家居装饰信息。"}
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="name" className="text-right">
<span className="text-red-500">*</span>
</Label>
<Input
id="name"
placeholder="输入装饰名称"
className="border-gray-300 focus-visible:ring-pink-500"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="type" className="text-right">
<span className="text-red-500">*</span>
</Label>
<Select value={decorType} onValueChange={setDecorType} required>
<SelectTrigger className="border-gray-300 focus:ring-pink-500">
<SelectValue placeholder="选择装饰类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="灯饰"></SelectItem>
<SelectItem value="墙饰"></SelectItem>
<SelectItem value="地饰"></SelectItem>
<SelectItem value="家具"></SelectItem>
<SelectItem value="科技装饰"></SelectItem>
<SelectItem value="家具套装"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="rarity" className="text-right">
<span className="text-red-500">*</span>
</Label>
<Select value={rarity} onValueChange={setRarity} required>
<SelectTrigger className="border-gray-300 focus:ring-pink-500">
<SelectValue placeholder="选择稀有度" />
</SelectTrigger>
<SelectContent>
<SelectItem value="普通"></SelectItem>
<SelectItem value="稀有"></SelectItem>
<SelectItem value="史诗"></SelectItem>
<SelectItem value="传说"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="quantity" className="text-right">
<span className="text-red-500">*</span>
</Label>
<Input
id="quantity"
type="number"
min="1"
defaultValue="1000"
className="border-gray-300 focus-visible:ring-pink-500"
required
disabled={mode === "edit"}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="description" className="text-right">
<span className="text-red-500">*</span>
</Label>
<Textarea
id="description"
placeholder="输入装饰描述"
className="min-h-[100px] border-gray-300 focus-visible:ring-pink-500"
value={description}
onChange={(e) => setDescription(e.target.value)}
required
/>
</div>
<div className="space-y-2">
<Label className="text-right">
{mode === "create" && <span className="text-red-500">*</span>}
</Label>
{imageUrl ? (
<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={imageUrl}
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={() => setImageUrl(undefined)}
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) {
setImageUrl(files[0].url)
}
}}
/>
)}
</div>
<div className="flex items-center space-x-2 pt-2">
<Switch id="limited" checked={isLimited} onCheckedChange={setIsLimited} />
<Label htmlFor="limited" className="cursor-pointer">
</Label>
</div>
{isLimited && (
<div className="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" />
<div className="text-sm text-amber-700">
<p className="font-medium"></p>
<p></p>
</div>
</div>
)}
{mode === "create" && (
<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-pink-600">{previewId}</span>
</p>
<p className="text-sm text-gray-600 mt-1">
: <span className="font-medium"></span>
</p>
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => handleOpenChange(false)} disabled={isSubmitting}>
</Button>
<Button
className="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>
)
}