feat: update card models, admin pages, and add migrations
All checks were successful
Build and Deploy LTY / build-and-deploy (push) Successful in 1h5m35s
All checks were successful
Build and Deploy LTY / build-and-deploy (push) Successful in 1h5m35s
- Update card models, serializers, views and URLs - Update dances, songs, users admin pages and API modules - Add card migrations (merge furniture into decoration) - Update middleware and settings Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
55ca2cbdaf
commit
c0fe1f502b
@ -153,7 +153,25 @@
|
|||||||
"Bash(python -c \"from card.serializers import MobileClothingTemplateSerializer; print\\(''Serializer OK''\\)\")",
|
"Bash(python -c \"from card.serializers import MobileClothingTemplateSerializer; print\\(''Serializer OK''\\)\")",
|
||||||
"Bash(python -c \"from card.views import MobileClothingListView; print\\(''View OK''\\)\")",
|
"Bash(python -c \"from card.views import MobileClothingListView; print\\(''View OK''\\)\")",
|
||||||
"Bash(python -c \"from card.urls import urlpatterns; print\\(f''URLs OK, {len\\(urlpatterns\\)} routes''\\)\")",
|
"Bash(python -c \"from card.urls import urlpatterns; print\\(f''URLs OK, {len\\(urlpatterns\\)} routes''\\)\")",
|
||||||
"Bash(DJANGO_SETTINGS_MODULE=qy_lty.settings python -c \"import django; django.setup\\(\\); from card.serializers import MobileClothingTemplateSerializer; print\\(''Serializer OK''\\); from card.views import MobileClothingListView; print\\(''View OK''\\); from card.urls import urlpatterns; print\\(f''URLs OK, {len\\(urlpatterns\\)} routes''\\)\")"
|
"Bash(DJANGO_SETTINGS_MODULE=qy_lty.settings python -c \"import django; django.setup\\(\\); from card.serializers import MobileClothingTemplateSerializer; print\\(''Serializer OK''\\); from card.views import MobileClothingListView; print\\(''View OK''\\); from card.urls import urlpatterns; print\\(f''URLs OK, {len\\(urlpatterns\\)} routes''\\)\")",
|
||||||
|
"Read(//c/Unity2022project/LTY_App_Project_URP/Assets/Scripts/Manager/**)",
|
||||||
|
"Read(//c/Unity2022project/LTY_App_Project_URP/Assets/Scripts/**)",
|
||||||
|
"Bash(netstat -ano)",
|
||||||
|
"Bash(powershell -Command \"Get-CimInstance Win32_Process -Filter ''ProcessId=266196 or ProcessId=158040'' | Select-Object ProcessId,CommandLine | Format-List\")",
|
||||||
|
"Bash(curl -s -o /dev/null -w \"%{http_code}\" http://127.0.0.1:8000/api/card/mobile/clothing/)",
|
||||||
|
"Bash(powershell -Command \"Get-CimInstance Win32_Process -Filter \"\"Name=''python.exe''\"\" | Select-Object ProcessId, CommandLine | Format-List\")",
|
||||||
|
"Bash(C:/python/python.exe manage.py runserver 0.0.0.0:8000)",
|
||||||
|
"Bash(curl -s http://localhost:8000/api/card/mobile/clothing/)",
|
||||||
|
"Read(//c/Users/admin/AppData/LocalLow/qy/LiLa/products/**)",
|
||||||
|
"Bash(C:/python/python.exe -c \"import json; data=json.load\\(open\\(r''c:\\\\Users\\\\admin\\\\AppData\\\\LocalLow\\\\qy\\\\LiLa\\\\products\\\\products.json'',''r'',encoding=''utf-8''\\)\\); json.dump\\(data, open\\(r''c:\\\\Users\\\\admin\\\\AppData\\\\LocalLow\\\\qy\\\\LiLa\\\\products\\\\products.json'',''w'',encoding=''utf-8''\\), ensure_ascii=False, indent=2\\)\")",
|
||||||
|
"Bash(curl -s http://localhost:8000/api/card/mobile/products.json)",
|
||||||
|
"Bash(C:/python/python.exe -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(''''Categories:'''', list\\(d.keys\\(\\)\\)\\); [print\\(f''''{k}: {v[\"\"count\"\"]} items''''\\) for k,v in d.items\\(\\)]\")",
|
||||||
|
"Bash(C:/python/python.exe manage.py shell -c \":*)",
|
||||||
|
"Bash(C:/python/python.exe -c \":*)",
|
||||||
|
"Bash(curl -s http://localhost:8000/api/card/category/dance/)",
|
||||||
|
"Bash(C:/python/python.exe -X utf8 -c \"import sys,json; sys.stdout.reconfigure\\(encoding=''''utf-8''''\\); d=json.load\\(sys.stdin\\); [print\\(f''''ID={i[\"\"id\"\"]} status={i.get\\(\"\"status\"\",\"\"?\"\"\\)} name={i[\"\"name\"\"]}''''\\) for i in d.get\\(''''data'''',d\\).get\\(''''results'''',d.get\\(''''data'''',[]\\)\\)]\")",
|
||||||
|
"Bash(ls c:/Users/admin/Desktop/Lila-Server/qy_lty/settings*)",
|
||||||
|
"Bash(python manage.py makemigrations card)"
|
||||||
],
|
],
|
||||||
"additionalDirectories": [
|
"additionalDirectories": [
|
||||||
"C:\\Users\\admin\\.claude"
|
"C:\\Users\\admin\\.claude"
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState } from "react"
|
import { useState, useEffect, useCallback } from "react"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { DashboardShell } from "@/components/dashboard-shell"
|
import { DashboardShell } from "@/components/dashboard-shell"
|
||||||
import { DashboardHeader } from "@/components/dashboard-header"
|
import { DashboardHeader } from "@/components/dashboard-header"
|
||||||
@ -9,149 +9,130 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
|
|||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Search, Edit, Video, Eye, Plus, Trash2 } from "lucide-react"
|
import { Search, Edit, Video, Eye, Plus, Loader2, Archive } from "lucide-react"
|
||||||
import { AddDanceDialog } from "@/components/dances/add-dance-dialog"
|
import { AddDanceDialog } from "@/components/dances/add-dance-dialog"
|
||||||
import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog"
|
import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog"
|
||||||
|
import { PublishConfirmationDialog } from "@/components/publish-confirmation-dialog"
|
||||||
import { useToast } from "@/hooks/use-toast"
|
import { useToast } from "@/hooks/use-toast"
|
||||||
|
import { getDances, deleteDance, publishDance, archiveDance } from "@/lib/api/dances"
|
||||||
|
import { isSuperUser } from "@/lib/api/auth"
|
||||||
import type { Dance } from "@/lib/api/types"
|
import type { Dance } from "@/lib/api/types"
|
||||||
|
|
||||||
// 初始舞蹈数据
|
function formatDate(dateStr?: string): string {
|
||||||
const initialDances: Dance[] = [
|
if (!dateStr) return ""
|
||||||
{
|
try {
|
||||||
id: "1",
|
return new Date(dateStr).toLocaleDateString("zh-CN", {
|
||||||
name: "千本樱",
|
year: "numeric",
|
||||||
choreographer: "洛天依工作室",
|
month: "2-digit",
|
||||||
duration: "3:45",
|
day: "2-digit",
|
||||||
difficulty: "中等",
|
})
|
||||||
videoUrl: "/placeholder.svg?height=300&width=400",
|
} catch {
|
||||||
coverUrl: "/placeholder.svg?height=300&width=400",
|
return dateStr
|
||||||
description: "基于《千本樱》歌曲的经典舞蹈编排,动作流畅优美,适合中等水平的舞者。",
|
}
|
||||||
motionFile: "senbonzakura_motion.fbx",
|
}
|
||||||
category: "日式",
|
|
||||||
tags: ["经典", "流行", "日式"],
|
|
||||||
createdAt: "2023-01-15T08:30:00Z",
|
|
||||||
updatedAt: "2023-02-20T14:15:00Z",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export default function DancesPage() {
|
export default function DancesPage() {
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [dances, setDances] = useState<Dance[]>(initialDances)
|
const [dances, setDances] = useState<Dance[]>([])
|
||||||
const [searchTerm, setSearchTerm] = useState("")
|
const [searchTerm, setSearchTerm] = useState("")
|
||||||
const [currentPage, setCurrentPage] = useState(1)
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
|
const [totalItems, setTotalItems] = useState(0)
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
|
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
|
||||||
const [danceToEdit, setDanceToEdit] = useState<Dance | undefined>(undefined)
|
const [danceToEdit, setDanceToEdit] = useState<Dance | undefined>(undefined)
|
||||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
|
|
||||||
const [danceToDelete, setDanceToDelete] = useState<Dance | null>(null)
|
|
||||||
|
|
||||||
const itemsPerPage = 5
|
const itemsPerPage = 10
|
||||||
|
|
||||||
// 过滤和分页
|
const fetchDances = useCallback(async () => {
|
||||||
const filteredDances = dances.filter(
|
setIsLoading(true)
|
||||||
(dance) =>
|
try {
|
||||||
dance.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
const result = await getDances(currentPage, itemsPerPage, searchTerm)
|
||||||
dance.id.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
if (result.data) {
|
||||||
(dance.choreographer && dance.choreographer.toLowerCase().includes(searchTerm.toLowerCase())) ||
|
setDances(result.data)
|
||||||
(dance.category && dance.category.toLowerCase().includes(searchTerm.toLowerCase())) ||
|
setTotalItems(result.pagination?.totalItems || 0)
|
||||||
(dance.tags && dance.tags.some((tag) => tag.toLowerCase().includes(searchTerm.toLowerCase()))),
|
}
|
||||||
)
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "加载失败",
|
||||||
|
description: "无法获取舞蹈列表",
|
||||||
|
variant: "destructive",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}, [currentPage, searchTerm, itemsPerPage, toast])
|
||||||
|
|
||||||
const totalPages = Math.ceil(filteredDances.length / itemsPerPage)
|
useEffect(() => {
|
||||||
const paginatedDances = filteredDances.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage)
|
fetchDances()
|
||||||
|
}, [fetchDances])
|
||||||
|
|
||||||
// 处理添加舞蹈
|
const totalPages = Math.ceil(totalItems / itemsPerPage)
|
||||||
const handleAddDance = (newDance: Dance) => {
|
|
||||||
setDances((prevDances) => [...prevDances, newDance])
|
|
||||||
toast({
|
|
||||||
title: "添加成功",
|
|
||||||
description: `舞蹈 ${newDance.name} 已成功添加`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理编辑舞蹈
|
// 处理添加/编辑舞蹈后刷新列表
|
||||||
const handleEditDance = (updatedDance: Dance) => {
|
const handleDanceSaved = () => {
|
||||||
setDances((prevDances) => prevDances.map((dance) => (dance.id === updatedDance.id ? updatedDance : dance)))
|
|
||||||
setDanceToEdit(undefined)
|
setDanceToEdit(undefined)
|
||||||
setIsAddDialogOpen(false)
|
setIsAddDialogOpen(false)
|
||||||
|
fetchDances()
|
||||||
toast({
|
toast({
|
||||||
title: "更新成功",
|
title: "保存成功",
|
||||||
description: `舞蹈 ${updatedDance.name} 已成功更新`,
|
description: "舞蹈数据已更新",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理删除舞蹈
|
// 处理删除舞蹈
|
||||||
const handleDeleteDance = () => {
|
const handleDeleteDance = async (danceId: string, danceName: string) => {
|
||||||
if (!danceToDelete) return
|
try {
|
||||||
|
await deleteDance(danceId)
|
||||||
|
await fetchDances()
|
||||||
|
toast({
|
||||||
|
title: "删除成功",
|
||||||
|
description: `舞蹈 ${danceName} 已成功删除`,
|
||||||
|
variant: "destructive",
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "删除失败",
|
||||||
|
description: "无法删除该舞蹈",
|
||||||
|
variant: "destructive",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setDances((prevDances) => prevDances.filter((dance) => dance.id !== danceToDelete.id))
|
// 发布舞蹈
|
||||||
setDanceToDelete(null)
|
const handlePublishDance = async (danceId: string, danceName: string) => {
|
||||||
setIsDeleteDialogOpen(false)
|
try {
|
||||||
toast({
|
await publishDance(danceId)
|
||||||
title: "删除成功",
|
await fetchDances()
|
||||||
description: `舞蹈 ${danceToDelete.name} 已成功删除`,
|
toast({
|
||||||
variant: "destructive",
|
title: "发布成功",
|
||||||
})
|
description: `舞蹈 "${danceName}" 已成功发布`,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "发布失败",
|
||||||
|
description: "无法发布舞蹈,请稍后重试",
|
||||||
|
variant: "destructive",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 归档舞蹈
|
||||||
|
const handleArchiveDance = async (danceId: string, danceName: string) => {
|
||||||
|
try {
|
||||||
|
await archiveDance(danceId)
|
||||||
|
await fetchDances()
|
||||||
|
toast({
|
||||||
|
title: "归档成功",
|
||||||
|
description: `舞蹈 "${danceName}" 已归档`,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "归档失败",
|
||||||
|
description: "无法归档舞蹈,请稍后重试",
|
||||||
|
variant: "destructive",
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开编辑对话框
|
// 打开编辑对话框
|
||||||
@ -165,12 +146,6 @@ export default function DancesPage() {
|
|||||||
router.push(`/dances/${dance.id}`)
|
router.push(`/dances/${dance.id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开删除确认对话框
|
|
||||||
const openDeleteDialog = (dance: Dance) => {
|
|
||||||
setDanceToDelete(dance)
|
|
||||||
setIsDeleteDialogOpen(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardShell>
|
<DashboardShell>
|
||||||
<DashboardHeader heading="舞蹈管理" text="管理洛天依的舞蹈库">
|
<DashboardHeader heading="舞蹈管理" text="管理洛天依的舞蹈库">
|
||||||
@ -197,7 +172,7 @@ export default function DancesPage() {
|
|||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSearchTerm(e.target.value)
|
setSearchTerm(e.target.value)
|
||||||
setCurrentPage(1) // 重置到第一页
|
setCurrentPage(1)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -213,100 +188,148 @@ export default function DancesPage() {
|
|||||||
<CardDescription>管理洛天依可以表演的舞蹈</CardDescription>
|
<CardDescription>管理洛天依可以表演的舞蹈</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Table>
|
{isLoading ? (
|
||||||
<TableHeader className="bg-gray-50">
|
<div className="flex items-center justify-center h-48">
|
||||||
<TableRow>
|
<Loader2 className="h-8 w-8 animate-spin text-purple-500" />
|
||||||
<TableHead className="w-[50px]"></TableHead>
|
</div>
|
||||||
<TableHead className="w-[80px]">ID</TableHead>
|
) : (
|
||||||
<TableHead>舞蹈名称</TableHead>
|
<Table>
|
||||||
<TableHead>编舞者</TableHead>
|
<TableHeader className="bg-gray-50">
|
||||||
<TableHead>难度</TableHead>
|
|
||||||
<TableHead>分类</TableHead>
|
|
||||||
<TableHead>时长</TableHead>
|
|
||||||
<TableHead className="text-right">操作</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{paginatedDances.map((dance) => (
|
|
||||||
<TableRow key={dance.id} className="hover:bg-gray-50 transition-colors">
|
|
||||||
<TableCell>
|
|
||||||
<div className="h-8 w-8 rounded-full bg-purple-100 flex items-center justify-center text-purple-600">
|
|
||||||
<Video className="h-4 w-4" />
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="font-medium">{dance.id}</TableCell>
|
|
||||||
<TableCell className="font-medium text-purple-600">{dance.name}</TableCell>
|
|
||||||
<TableCell>{dance.choreographer || "未知"}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className={
|
|
||||||
dance.difficulty === "初级"
|
|
||||||
? "bg-green-100 text-green-800"
|
|
||||||
: dance.difficulty === "中等"
|
|
||||||
? "bg-blue-100 text-blue-800"
|
|
||||||
: dance.difficulty === "中高级"
|
|
||||||
? "bg-yellow-100 text-yellow-800"
|
|
||||||
: dance.difficulty === "高级"
|
|
||||||
? "bg-orange-100 text-orange-800"
|
|
||||||
: dance.difficulty === "专业"
|
|
||||||
? "bg-red-100 text-red-800"
|
|
||||||
: "bg-gray-100 text-gray-800"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{dance.difficulty || "未知"}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{dance.category || "未分类"}</TableCell>
|
|
||||||
<TableCell>{dance.duration || "未知"}</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
<div className="flex justify-end space-x-1">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="hover:bg-purple-50 hover:text-purple-600"
|
|
||||||
onClick={() => viewDanceDetails(dance)}
|
|
||||||
>
|
|
||||||
<Eye className="h-4 w-4" />
|
|
||||||
<span className="sr-only">查看详情</span>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="hover:bg-purple-50 hover:text-purple-600"
|
|
||||||
onClick={() => openEditDialog(dance)}
|
|
||||||
>
|
|
||||||
<Edit className="h-4 w-4" />
|
|
||||||
<span className="sr-only">编辑</span>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="hover:bg-red-50 hover:text-red-600"
|
|
||||||
onClick={() => openDeleteDialog(dance)}
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
<span className="sr-only">删除</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{paginatedDances.length === 0 && (
|
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={8} className="h-24 text-center">
|
<TableHead className="w-[50px]"></TableHead>
|
||||||
没有找到匹配的舞蹈
|
<TableHead className="w-[80px]">ID</TableHead>
|
||||||
</TableCell>
|
<TableHead>舞蹈名称</TableHead>
|
||||||
|
<TableHead>编舞者</TableHead>
|
||||||
|
<TableHead>难度</TableHead>
|
||||||
|
<TableHead>分类</TableHead>
|
||||||
|
<TableHead>时长</TableHead>
|
||||||
|
<TableHead>发布日期</TableHead>
|
||||||
|
<TableHead>状态</TableHead>
|
||||||
|
<TableHead className="text-right">操作</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
</TableHeader>
|
||||||
</TableBody>
|
<TableBody>
|
||||||
</Table>
|
{dances.map((dance) => (
|
||||||
|
<TableRow key={dance.id} className="hover:bg-gray-50 transition-colors">
|
||||||
|
<TableCell>
|
||||||
|
<div className="h-8 w-8 rounded-full bg-purple-100 flex items-center justify-center text-purple-600">
|
||||||
|
<Video className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-medium">{dance.id}</TableCell>
|
||||||
|
<TableCell className="font-medium text-purple-600">{dance.name}</TableCell>
|
||||||
|
<TableCell>{dance.choreographer || "未知"}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={
|
||||||
|
dance.difficulty === "初级"
|
||||||
|
? "bg-green-100 text-green-800"
|
||||||
|
: dance.difficulty === "中等" || dance.difficulty === "中级"
|
||||||
|
? "bg-blue-100 text-blue-800"
|
||||||
|
: dance.difficulty === "中高级"
|
||||||
|
? "bg-yellow-100 text-yellow-800"
|
||||||
|
: dance.difficulty === "高级"
|
||||||
|
? "bg-orange-100 text-orange-800"
|
||||||
|
: dance.difficulty === "专业"
|
||||||
|
? "bg-red-100 text-red-800"
|
||||||
|
: "bg-gray-100 text-gray-800"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{dance.difficulty || "未知"}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{dance.category || "未分类"}</TableCell>
|
||||||
|
<TableCell>{dance.duration || "未知"}</TableCell>
|
||||||
|
<TableCell>{formatDate(dance.publishedAt) || "-"}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge
|
||||||
|
className={
|
||||||
|
dance.status === "已发布"
|
||||||
|
? "bg-green-500 hover:bg-green-600"
|
||||||
|
: dance.status === "已归档"
|
||||||
|
? "bg-orange-500 hover:bg-orange-600"
|
||||||
|
: "bg-gray-500 hover:bg-gray-600"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{dance.status || "草稿"}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<div className="flex justify-end space-x-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="hover:bg-purple-50 hover:text-purple-600"
|
||||||
|
onClick={() => viewDanceDetails(dance)}
|
||||||
|
>
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
<span className="sr-only">查看详情</span>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* 草稿状态:显示发布按钮 */}
|
||||||
|
{dance.status === "草稿" && (
|
||||||
|
<PublishConfirmationDialog
|
||||||
|
title="发布舞蹈"
|
||||||
|
description="发布后该舞蹈将对手机端可见"
|
||||||
|
itemName={dance.name}
|
||||||
|
onPublish={() => handlePublishDance(dance.id, dance.name)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 已发布状态:显示归档按钮 */}
|
||||||
|
{dance.status === "已发布" && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="hover:bg-orange-50 hover:text-orange-600"
|
||||||
|
title="归档"
|
||||||
|
onClick={() => handleArchiveDance(dance.id, dance.name)}
|
||||||
|
>
|
||||||
|
<Archive className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(dance.status !== "已发布" || isSuperUser()) && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="hover:bg-purple-50 hover:text-purple-600"
|
||||||
|
onClick={() => openEditDialog(dance)}
|
||||||
|
>
|
||||||
|
<Edit className="h-4 w-4" />
|
||||||
|
<span className="sr-only">编辑</span>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<DeleteConfirmationDialog
|
||||||
|
title="删除舞蹈"
|
||||||
|
description="此操作将永久删除该舞蹈及其所有相关数据。"
|
||||||
|
itemName={dance.name}
|
||||||
|
onDelete={() => handleDeleteDance(dance.id, dance.name)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{dances.length === 0 && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={10} className="h-24 text-center">
|
||||||
|
没有找到匹配的舞蹈
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter className="flex justify-between">
|
<CardFooter className="flex justify-between">
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
显示 {paginatedDances.length > 0 ? (currentPage - 1) * itemsPerPage + 1 : 0}-
|
显示 {dances.length > 0 ? (currentPage - 1) * itemsPerPage + 1 : 0}-
|
||||||
{Math.min(currentPage * itemsPerPage, filteredDances.length)} 共 {filteredDances.length} 个舞蹈
|
{Math.min(currentPage * itemsPerPage, totalItems)} 共 {totalItems} 个舞蹈
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Button
|
<Button
|
||||||
@ -335,18 +358,10 @@ export default function DancesPage() {
|
|||||||
<AddDanceDialog
|
<AddDanceDialog
|
||||||
open={isAddDialogOpen}
|
open={isAddDialogOpen}
|
||||||
onOpenChange={setIsAddDialogOpen}
|
onOpenChange={setIsAddDialogOpen}
|
||||||
onDanceAdded={danceToEdit ? handleEditDance : handleAddDance}
|
onDanceAdded={handleDanceSaved}
|
||||||
editDance={danceToEdit}
|
editDance={danceToEdit}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 删除确认对话框 */}
|
|
||||||
<DeleteConfirmationDialog
|
|
||||||
open={isDeleteDialogOpen}
|
|
||||||
onOpenChange={setIsDeleteDialogOpen}
|
|
||||||
onConfirm={handleDeleteDance}
|
|
||||||
title="删除舞蹈"
|
|
||||||
description={`确定要删除舞蹈 "${danceToDelete?.name}" 吗?此操作无法撤销。`}
|
|
||||||
/>
|
|
||||||
</DashboardShell>
|
</DashboardShell>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,13 +8,14 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
|
|||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Search, Edit, Play, Pause, Eye, Volume2, VolumeX, Send } from "lucide-react"
|
import { Search, Edit, Play, Pause, Eye, Volume2, VolumeX, Archive } from "lucide-react"
|
||||||
import { AddSongDialog } from "@/components/songs/add-song-dialog"
|
import { AddSongDialog } from "@/components/songs/add-song-dialog"
|
||||||
import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog"
|
import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog"
|
||||||
import { PublishConfirmationDialog } from "@/components/publish-confirmation-dialog"
|
import { PublishConfirmationDialog } from "@/components/publish-confirmation-dialog"
|
||||||
import { useToast } from "@/components/ui/use-toast"
|
import { useToast } from "@/components/ui/use-toast"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { getSongs, publishSong } from "@/lib/api/songs"
|
import { isSuperUser } from "@/lib/api/auth"
|
||||||
|
import { getSongs, publishSong, deleteSong, archiveSong } from "@/lib/api/songs"
|
||||||
import type { Song } from "@/lib/api/types"
|
import type { Song } from "@/lib/api/types"
|
||||||
import type { Song as ComponentSong } from "@/components/songs/song-detail-dialog"
|
import type { Song as ComponentSong } from "@/components/songs/song-detail-dialog"
|
||||||
import { apiSongToComponentSong, componentSongToApiSong } from "@/lib/api/adapters"
|
import { apiSongToComponentSong, componentSongToApiSong } from "@/lib/api/adapters"
|
||||||
@ -42,7 +43,7 @@ export default function SongsPage() {
|
|||||||
const audioRef = useRef<HTMLAudioElement | null>(null)
|
const audioRef = useRef<HTMLAudioElement | null>(null)
|
||||||
const currentSongUrlRef = useRef<string | null>(null)
|
const currentSongUrlRef = useRef<string | null>(null)
|
||||||
|
|
||||||
const itemsPerPage = 5
|
const itemsPerPage = 10
|
||||||
|
|
||||||
// 加载歌曲数据
|
// 加载歌曲数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -197,22 +198,47 @@ export default function SongsPage() {
|
|||||||
|
|
||||||
// 处理删除歌曲
|
// 处理删除歌曲
|
||||||
const handleDeleteSong = async (songId: string) => {
|
const handleDeleteSong = async (songId: string) => {
|
||||||
// 如果正在播放该歌曲,先停止播放
|
try {
|
||||||
if (playingSongId === songId && audioRef.current) {
|
// 如果正在播放该歌曲,先停止播放
|
||||||
audioRef.current.pause()
|
if (playingSongId === songId && audioRef.current) {
|
||||||
setPlayingSongId(null)
|
audioRef.current.pause()
|
||||||
}
|
setPlayingSongId(null)
|
||||||
|
}
|
||||||
// 模拟API请求
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
||||||
|
|
||||||
setSongs((prevSongs) => prevSongs.filter((song) => song.id !== songId))
|
await deleteSong(songId)
|
||||||
setRefreshTrigger(prev => prev + 1) // 触发刷新
|
setRefreshTrigger(prev => prev + 1)
|
||||||
toast({
|
toast({
|
||||||
title: "删除成功",
|
title: "删除成功",
|
||||||
description: "歌曲已成功删除",
|
description: "歌曲已成功删除",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
})
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error("删除歌曲失败:", error)
|
||||||
|
toast({
|
||||||
|
title: "删除失败",
|
||||||
|
description: "无法删除该歌曲,请稍后重试",
|
||||||
|
variant: "destructive",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 归档歌曲
|
||||||
|
const handleArchiveSong = async (songId: string, songName: string) => {
|
||||||
|
try {
|
||||||
|
await archiveSong(songId)
|
||||||
|
setRefreshTrigger(prev => prev + 1)
|
||||||
|
toast({
|
||||||
|
title: "归档成功",
|
||||||
|
description: `歌曲 "${songName}" 已归档`,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error("归档歌曲失败:", error)
|
||||||
|
toast({
|
||||||
|
title: "归档失败",
|
||||||
|
description: "无法归档歌曲,请稍后重试",
|
||||||
|
variant: "destructive",
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开编辑对话框
|
// 打开编辑对话框
|
||||||
@ -502,7 +528,11 @@ export default function SongsPage() {
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge
|
<Badge
|
||||||
className={
|
className={
|
||||||
song.status === "已发布" ? "bg-green-500 hover:bg-green-600" : "bg-gray-500 hover:bg-gray-600"
|
song.status === "已发布"
|
||||||
|
? "bg-green-500 hover:bg-green-600"
|
||||||
|
: song.status === "已归档"
|
||||||
|
? "bg-orange-500 hover:bg-orange-600"
|
||||||
|
: "bg-gray-500 hover:bg-gray-600"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{song.status}
|
{song.status}
|
||||||
@ -516,7 +546,30 @@ export default function SongsPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{song.status !== "已发布" && (
|
{/* 草稿状态:显示发布按钮 */}
|
||||||
|
{song.status !== "已发布" && song.status !== "已归档" && (
|
||||||
|
<PublishConfirmationDialog
|
||||||
|
title="发布歌曲"
|
||||||
|
description="发布后该歌曲将可被用户使用"
|
||||||
|
itemName={song.name}
|
||||||
|
onPublish={() => handlePublishSong(song.id)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 已发布状态:显示归档按钮 */}
|
||||||
|
{song.status === "已发布" && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="hover:bg-orange-50 hover:text-orange-600"
|
||||||
|
title="归档"
|
||||||
|
onClick={() => handleArchiveSong(song.id, song.name)}
|
||||||
|
>
|
||||||
|
<Archive className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(song.status !== "已发布" || isSuperUser()) && (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@ -527,13 +580,6 @@ export default function SongsPage() {
|
|||||||
<Edit className="h-4 w-4" />
|
<Edit className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<PublishConfirmationDialog
|
|
||||||
title="发布歌曲"
|
|
||||||
description="发布后该歌曲将可被用户使用"
|
|
||||||
itemName={song.name}
|
|
||||||
onPublish={() => handlePublishSong(song.id)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DeleteConfirmationDialog
|
<DeleteConfirmationDialog
|
||||||
title="删除歌曲"
|
title="删除歌曲"
|
||||||
description="此操作将永久删除该歌曲及其所有相关数据。"
|
description="此操作将永久删除该歌曲及其所有相关数据。"
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export default function UsersPage() {
|
|||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
|
|
||||||
const itemsPerPage = 5
|
const itemsPerPage = 10
|
||||||
|
|
||||||
// 获取用户列表
|
// 获取用户列表
|
||||||
const fetchUsers = async () => {
|
const fetchUsers = async () => {
|
||||||
|
|||||||
@ -17,6 +17,10 @@ function mapBackendDance(item: any): Dance {
|
|||||||
motionFile: item.model_url || "",
|
motionFile: item.model_url || "",
|
||||||
category: attrs.style || "",
|
category: attrs.style || "",
|
||||||
tags: [],
|
tags: [],
|
||||||
|
status: item.status_display || item.status || "",
|
||||||
|
statusDisplay: item.status_display || item.status || "",
|
||||||
|
publishedAt: item.published_at || "",
|
||||||
|
activeCardsCount: item.active_cards_count || 0,
|
||||||
createdAt: item.created_at || "",
|
createdAt: item.created_at || "",
|
||||||
updatedAt: item.updated_at || "",
|
updatedAt: item.updated_at || "",
|
||||||
}
|
}
|
||||||
@ -119,3 +123,17 @@ export async function deleteDance(id: string) {
|
|||||||
return handleApiError(error)
|
return handleApiError(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 发布舞蹈
|
||||||
|
export async function publishDance(id: string) {
|
||||||
|
const response = await apiClient.post(`/card/templates/${id}/publish/`)
|
||||||
|
const data = response.data?.template || response.data
|
||||||
|
return { data: mapBackendDance(data) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 归档舞蹈
|
||||||
|
export async function archiveDance(id: string) {
|
||||||
|
const response = await apiClient.post(`/card/templates/${id}/archive/`)
|
||||||
|
const data = response.data?.template || response.data
|
||||||
|
return { data: mapBackendDance(data) }
|
||||||
|
}
|
||||||
|
|||||||
@ -227,6 +227,24 @@ export const publishSong = async (id: string): Promise<Song> => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 归档歌曲
|
||||||
|
export const archiveSong = async (id: string): Promise<Song> => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.post(`/card/templates/${id}/archive/`);
|
||||||
|
|
||||||
|
const data = response.data;
|
||||||
|
|
||||||
|
if (!data.success) {
|
||||||
|
throw new Error(data.message || '归档歌曲失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformCardSongDetailToSong(data.data.template);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('归档歌曲失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 获取歌曲批次数据
|
// 获取歌曲批次数据
|
||||||
export const getSongBatches = async (songId: string): Promise<SongBatch[]> => {
|
export const getSongBatches = async (songId: string): Promise<SongBatch[]> => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -162,6 +162,10 @@ export interface Dance {
|
|||||||
motionFile?: string // 动作文件
|
motionFile?: string // 动作文件
|
||||||
category?: string
|
category?: string
|
||||||
tags?: string[]
|
tags?: string[]
|
||||||
|
status?: string
|
||||||
|
statusDisplay?: string
|
||||||
|
publishedAt?: string
|
||||||
|
activeCardsCount?: number
|
||||||
createdAt?: string
|
createdAt?: string
|
||||||
updatedAt?: string
|
updatedAt?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,66 @@
|
|||||||
|
"""
|
||||||
|
Data migration: merge furniture category into decoration.
|
||||||
|
|
||||||
|
For each CardTemplate with category='furniture':
|
||||||
|
1. Create a DecorationAttributes record from FurnitureAttributes data
|
||||||
|
2. Change the template's category to 'decoration'
|
||||||
|
"""
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def merge_furniture_into_decoration(apps, schema_editor):
|
||||||
|
CardTemplate = apps.get_model('card', 'CardTemplate')
|
||||||
|
FurnitureAttributes = apps.get_model('card', 'FurnitureAttributes')
|
||||||
|
DecorationAttributes = apps.get_model('card', 'DecorationAttributes')
|
||||||
|
|
||||||
|
furniture_templates = CardTemplate.objects.filter(category='furniture')
|
||||||
|
|
||||||
|
for template in furniture_templates:
|
||||||
|
# Get existing furniture attributes if any
|
||||||
|
try:
|
||||||
|
fa = FurnitureAttributes.objects.get(template=template)
|
||||||
|
# Create decoration attributes from furniture data
|
||||||
|
DecorationAttributes.objects.create(
|
||||||
|
template=template,
|
||||||
|
decoration_type=fa.furniture_type,
|
||||||
|
style=fa.style,
|
||||||
|
material=fa.material,
|
||||||
|
size=fa.dimensions, # dimensions -> size
|
||||||
|
placement='',
|
||||||
|
indoor_outdoor='室内',
|
||||||
|
installation_required=fa.assembly_required,
|
||||||
|
care_instructions=fa.care_instructions,
|
||||||
|
)
|
||||||
|
# Delete old furniture attributes
|
||||||
|
fa.delete()
|
||||||
|
except FurnitureAttributes.DoesNotExist:
|
||||||
|
# No furniture attrs, just create empty decoration attrs
|
||||||
|
DecorationAttributes.objects.create(template=template)
|
||||||
|
|
||||||
|
# Change category
|
||||||
|
template.category = 'decoration'
|
||||||
|
template.save(update_fields=['category'])
|
||||||
|
|
||||||
|
# Also update any Card records that reference furniture category
|
||||||
|
Card = apps.get_model('card', 'Card')
|
||||||
|
Card.objects.filter(category='furniture').update(category='decoration')
|
||||||
|
|
||||||
|
# Also update any CardBatch records
|
||||||
|
CardBatch = apps.get_model('card', 'CardBatch')
|
||||||
|
CardBatch.objects.filter(category='furniture').update(category='decoration')
|
||||||
|
|
||||||
|
|
||||||
|
def reverse_migration(apps, schema_editor):
|
||||||
|
# Cannot reliably reverse this migration
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('card', '0012_decoration_furniture_optional_fields'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(merge_furniture_into_decoration, reverse_migration),
|
||||||
|
]
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 5.2.12 on 2026-03-25 09:29
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('card', '0013_merge_furniture_into_decoration'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='card',
|
||||||
|
name='category',
|
||||||
|
field=models.CharField(choices=[('clothing', '服装'), ('prop', '道具'), ('song', '音乐'), ('dance', '舞蹈'), ('decoration', '家居装饰')], max_length=20, verbose_name='类别'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cardbatch',
|
||||||
|
name='category',
|
||||||
|
field=models.CharField(choices=[('clothing', '服装'), ('prop', '道具'), ('song', '音乐'), ('dance', '舞蹈'), ('decoration', '家居装饰')], max_length=20, verbose_name='类别'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cardtemplate',
|
||||||
|
name='category',
|
||||||
|
field=models.CharField(choices=[('clothing', '服装'), ('prop', '道具'), ('song', '音乐'), ('dance', '舞蹈'), ('decoration', '家居装饰')], max_length=20, verbose_name='类别'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -18,8 +18,7 @@ class CardTemplate(models.Model):
|
|||||||
('prop', '道具'),
|
('prop', '道具'),
|
||||||
('song', '音乐'),
|
('song', '音乐'),
|
||||||
('dance', '舞蹈'),
|
('dance', '舞蹈'),
|
||||||
('furniture', '家具'),
|
('decoration', '家居装饰'),
|
||||||
('decoration', '装饰'),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
RARITY_CHOICES = [
|
RARITY_CHOICES = [
|
||||||
|
|||||||
@ -2,7 +2,7 @@ from rest_framework import serializers
|
|||||||
from .models import (
|
from .models import (
|
||||||
CardTemplate, Card, CardBatch, CardUsageLog,
|
CardTemplate, Card, CardBatch, CardUsageLog,
|
||||||
ClothingAttributes, PropAttributes, SongAttributes,
|
ClothingAttributes, PropAttributes, SongAttributes,
|
||||||
DanceAttributes, FurnitureAttributes, DecorationAttributes
|
DanceAttributes, DecorationAttributes
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -42,15 +42,6 @@ class DanceAttributesSerializer(serializers.ModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class FurnitureAttributesSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = FurnitureAttributes
|
|
||||||
fields = [
|
|
||||||
'furniture_type', 'style', 'material', 'dimensions',
|
|
||||||
'weight', 'assembly_required', 'max_weight_capacity', 'care_instructions'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class DecorationAttributesSerializer(serializers.ModelSerializer):
|
class DecorationAttributesSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DecorationAttributes
|
model = DecorationAttributes
|
||||||
@ -72,7 +63,6 @@ class CardTemplateSerializer(serializers.ModelSerializer):
|
|||||||
prop_attributes = PropAttributesSerializer(write_only=True, required=False)
|
prop_attributes = PropAttributesSerializer(write_only=True, required=False)
|
||||||
song_attributes = SongAttributesSerializer(write_only=True, required=False)
|
song_attributes = SongAttributesSerializer(write_only=True, required=False)
|
||||||
dance_attributes = DanceAttributesSerializer(write_only=True, required=False)
|
dance_attributes = DanceAttributesSerializer(write_only=True, required=False)
|
||||||
furniture_attributes = FurnitureAttributesSerializer(write_only=True, required=False)
|
|
||||||
decoration_attributes = DecorationAttributesSerializer(write_only=True, required=False)
|
decoration_attributes = DecorationAttributesSerializer(write_only=True, required=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -84,7 +74,7 @@ class CardTemplateSerializer(serializers.ModelSerializer):
|
|||||||
'status', 'status_display', 'published_at', 'price',
|
'status', 'status_display', 'published_at', 'price',
|
||||||
'created_at', 'updated_at',
|
'created_at', 'updated_at',
|
||||||
# 新增
|
# 新增
|
||||||
'clothing_attributes', 'prop_attributes', 'song_attributes', 'dance_attributes', 'furniture_attributes', 'decoration_attributes',
|
'clothing_attributes', 'prop_attributes', 'song_attributes', 'dance_attributes', 'decoration_attributes',
|
||||||
]
|
]
|
||||||
read_only_fields = ['created_at', 'updated_at', 'published_at']
|
read_only_fields = ['created_at', 'updated_at', 'published_at']
|
||||||
|
|
||||||
@ -95,7 +85,6 @@ class CardTemplateSerializer(serializers.ModelSerializer):
|
|||||||
validated_data.pop('prop_attributes', None)
|
validated_data.pop('prop_attributes', None)
|
||||||
validated_data.pop('song_attributes', None)
|
validated_data.pop('song_attributes', None)
|
||||||
validated_data.pop('dance_attributes', None)
|
validated_data.pop('dance_attributes', None)
|
||||||
validated_data.pop('furniture_attributes', None)
|
|
||||||
validated_data.pop('decoration_attributes', None)
|
validated_data.pop('decoration_attributes', None)
|
||||||
return super().create(validated_data)
|
return super().create(validated_data)
|
||||||
|
|
||||||
@ -123,8 +112,6 @@ class CardTemplateSerializer(serializers.ModelSerializer):
|
|||||||
representation['attributes'] = SongAttributesSerializer(instance.song_attrs).data
|
representation['attributes'] = SongAttributesSerializer(instance.song_attrs).data
|
||||||
elif instance.category == 'dance' and hasattr(instance, 'dance_attrs'):
|
elif instance.category == 'dance' and hasattr(instance, 'dance_attrs'):
|
||||||
representation['attributes'] = DanceAttributesSerializer(instance.dance_attrs).data
|
representation['attributes'] = DanceAttributesSerializer(instance.dance_attrs).data
|
||||||
elif instance.category == 'furniture' and hasattr(instance, 'furniture_attrs'):
|
|
||||||
representation['attributes'] = FurnitureAttributesSerializer(instance.furniture_attrs).data
|
|
||||||
elif instance.category == 'decoration' and hasattr(instance, 'decoration_attrs'):
|
elif instance.category == 'decoration' and hasattr(instance, 'decoration_attrs'):
|
||||||
representation['attributes'] = DecorationAttributesSerializer(instance.decoration_attrs).data
|
representation['attributes'] = DecorationAttributesSerializer(instance.decoration_attrs).data
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -137,7 +124,6 @@ class CardTemplateSerializer(serializers.ModelSerializer):
|
|||||||
prop_data = validated_data.pop('prop_attributes', None)
|
prop_data = validated_data.pop('prop_attributes', None)
|
||||||
song_data = validated_data.pop('song_attributes', None)
|
song_data = validated_data.pop('song_attributes', None)
|
||||||
dance_data = validated_data.pop('dance_attributes', None)
|
dance_data = validated_data.pop('dance_attributes', None)
|
||||||
furniture_data = validated_data.pop('furniture_attributes', None)
|
|
||||||
decoration_data = validated_data.pop('decoration_attributes', None)
|
decoration_data = validated_data.pop('decoration_attributes', None)
|
||||||
|
|
||||||
# 更新主表
|
# 更新主表
|
||||||
@ -160,10 +146,6 @@ class CardTemplateSerializer(serializers.ModelSerializer):
|
|||||||
for k, v in dance_data.items():
|
for k, v in dance_data.items():
|
||||||
setattr(instance.dance_attrs, k, v)
|
setattr(instance.dance_attrs, k, v)
|
||||||
instance.dance_attrs.save()
|
instance.dance_attrs.save()
|
||||||
if furniture_data is not None and hasattr(instance, 'furniture_attrs'):
|
|
||||||
for k, v in furniture_data.items():
|
|
||||||
setattr(instance.furniture_attrs, k, v)
|
|
||||||
instance.furniture_attrs.save()
|
|
||||||
if decoration_data is not None and hasattr(instance, 'decoration_attrs'):
|
if decoration_data is not None and hasattr(instance, 'decoration_attrs'):
|
||||||
for k, v in decoration_data.items():
|
for k, v in decoration_data.items():
|
||||||
setattr(instance.decoration_attrs, k, v)
|
setattr(instance.decoration_attrs, k, v)
|
||||||
@ -200,14 +182,12 @@ class CardTemplateDetailSerializer(CardTemplateSerializer):
|
|||||||
representation['attributes'] = SongAttributesSerializer(instance.song_attrs).data
|
representation['attributes'] = SongAttributesSerializer(instance.song_attrs).data
|
||||||
elif instance.category == 'dance' and hasattr(instance, 'dance_attrs'):
|
elif instance.category == 'dance' and hasattr(instance, 'dance_attrs'):
|
||||||
representation['attributes'] = DanceAttributesSerializer(instance.dance_attrs).data
|
representation['attributes'] = DanceAttributesSerializer(instance.dance_attrs).data
|
||||||
elif instance.category == 'furniture' and hasattr(instance, 'furniture_attrs'):
|
|
||||||
representation['attributes'] = FurnitureAttributesSerializer(instance.furniture_attrs).data
|
|
||||||
elif instance.category == 'decoration' and hasattr(instance, 'decoration_attrs'):
|
elif instance.category == 'decoration' and hasattr(instance, 'decoration_attrs'):
|
||||||
representation['attributes'] = DecorationAttributesSerializer(instance.decoration_attrs).data
|
representation['attributes'] = DecorationAttributesSerializer(instance.decoration_attrs).data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 处理相关属性不存在的情况
|
# 处理相关属性不存在的情况
|
||||||
representation['attributes'] = None
|
representation['attributes'] = None
|
||||||
|
|
||||||
return representation
|
return representation
|
||||||
|
|
||||||
|
|
||||||
@ -391,19 +371,18 @@ class CategoryTemplateSerializer(CardTemplateDetailSerializer):
|
|||||||
representation['attributes'] = SongAttributesSerializer(instance.song_attrs).data
|
representation['attributes'] = SongAttributesSerializer(instance.song_attrs).data
|
||||||
elif instance.category == 'dance' and hasattr(instance, 'dance_attrs'):
|
elif instance.category == 'dance' and hasattr(instance, 'dance_attrs'):
|
||||||
representation['attributes'] = DanceAttributesSerializer(instance.dance_attrs).data
|
representation['attributes'] = DanceAttributesSerializer(instance.dance_attrs).data
|
||||||
elif instance.category == 'furniture' and hasattr(instance, 'furniture_attrs'):
|
|
||||||
representation['attributes'] = FurnitureAttributesSerializer(instance.furniture_attrs).data
|
|
||||||
elif instance.category == 'decoration' and hasattr(instance, 'decoration_attrs'):
|
elif instance.category == 'decoration' and hasattr(instance, 'decoration_attrs'):
|
||||||
representation['attributes'] = DecorationAttributesSerializer(instance.decoration_attrs).data
|
representation['attributes'] = DecorationAttributesSerializer(instance.decoration_attrs).data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Handle the case where related attributes don't exist
|
# Handle the case where related attributes don't exist
|
||||||
representation['attributes'] = None
|
representation['attributes'] = None
|
||||||
|
|
||||||
return representation
|
return representation
|
||||||
|
|
||||||
|
|
||||||
class MobileClothingTemplateSerializer(serializers.ModelSerializer):
|
class MobileProductSerializer(serializers.ModelSerializer):
|
||||||
"""手机端服装模板序列化器 - 只返回已发布服装的必要字段"""
|
"""手机端通用产品序列化器 - 适用于所有分类"""
|
||||||
|
category_display = serializers.SerializerMethodField()
|
||||||
card_type_display = serializers.SerializerMethodField()
|
card_type_display = serializers.SerializerMethodField()
|
||||||
rarity_display = serializers.SerializerMethodField()
|
rarity_display = serializers.SerializerMethodField()
|
||||||
status_display = serializers.SerializerMethodField()
|
status_display = serializers.SerializerMethodField()
|
||||||
@ -415,6 +394,7 @@ class MobileClothingTemplateSerializer(serializers.ModelSerializer):
|
|||||||
model = CardTemplate
|
model = CardTemplate
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'name', 'description',
|
'id', 'name', 'description',
|
||||||
|
'category', 'category_display',
|
||||||
'card_type', 'card_type_display',
|
'card_type', 'card_type_display',
|
||||||
'rarity', 'rarity_display',
|
'rarity', 'rarity_display',
|
||||||
'image_url',
|
'image_url',
|
||||||
@ -424,6 +404,9 @@ class MobileClothingTemplateSerializer(serializers.ModelSerializer):
|
|||||||
'attributes',
|
'attributes',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def get_category_display(self, obj):
|
||||||
|
return obj.get_category_display()
|
||||||
|
|
||||||
def get_card_type_display(self, obj):
|
def get_card_type_display(self, obj):
|
||||||
return obj.get_card_type_display()
|
return obj.get_card_type_display()
|
||||||
|
|
||||||
@ -438,13 +421,23 @@ class MobileClothingTemplateSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
def get_attributes(self, obj):
|
def get_attributes(self, obj):
|
||||||
try:
|
try:
|
||||||
if hasattr(obj, 'clothing_attrs'):
|
attr_map = {
|
||||||
return ClothingAttributesSerializer(obj.clothing_attrs).data
|
'clothing': ('clothing_attrs', ClothingAttributesSerializer),
|
||||||
|
'prop': ('prop_attrs', PropAttributesSerializer),
|
||||||
|
'song': ('song_attrs', SongAttributesSerializer),
|
||||||
|
'dance': ('dance_attrs', DanceAttributesSerializer),
|
||||||
|
'decoration': ('decoration_attrs', DecorationAttributesSerializer),
|
||||||
|
}
|
||||||
|
if obj.category in attr_map:
|
||||||
|
attr_name, serializer_cls = attr_map[obj.category]
|
||||||
|
if hasattr(obj, attr_name):
|
||||||
|
return serializer_cls(getattr(obj, attr_name)).data
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 带有专有属性的卡牌详情序列化器
|
# 带有专有属性的卡牌详情序列化器
|
||||||
class CategoryCardDetailSerializer(CardDetailSerializer):
|
class CategoryCardDetailSerializer(CardDetailSerializer):
|
||||||
"""Card detail serializer with category-specific attributes"""
|
"""Card detail serializer with category-specific attributes"""
|
||||||
@ -465,8 +458,6 @@ class CategoryCardDetailSerializer(CardDetailSerializer):
|
|||||||
representation['attributes'] = SongAttributesSerializer(template.song_attrs).data
|
representation['attributes'] = SongAttributesSerializer(template.song_attrs).data
|
||||||
elif template.category == 'dance' and hasattr(template, 'dance_attrs'):
|
elif template.category == 'dance' and hasattr(template, 'dance_attrs'):
|
||||||
representation['attributes'] = DanceAttributesSerializer(template.dance_attrs).data
|
representation['attributes'] = DanceAttributesSerializer(template.dance_attrs).data
|
||||||
elif template.category == 'furniture' and hasattr(template, 'furniture_attrs'):
|
|
||||||
representation['attributes'] = FurnitureAttributesSerializer(template.furniture_attrs).data
|
|
||||||
elif template.category == 'decoration' and hasattr(template, 'decoration_attrs'):
|
elif template.category == 'decoration' and hasattr(template, 'decoration_attrs'):
|
||||||
representation['attributes'] = DecorationAttributesSerializer(template.decoration_attrs).data
|
representation['attributes'] = DecorationAttributesSerializer(template.decoration_attrs).data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@ -11,6 +11,6 @@ urlpatterns = [
|
|||||||
path('', include(router.urls)),
|
path('', include(router.urls)),
|
||||||
path('user/cards/', views.UserCardListView.as_view(), name='user-cards'),
|
path('user/cards/', views.UserCardListView.as_view(), name='user-cards'),
|
||||||
path('category/<str:category>/', views.CategoryCardTemplateListView.as_view(), name='category-templates'),
|
path('category/<str:category>/', views.CategoryCardTemplateListView.as_view(), name='category-templates'),
|
||||||
path('mobile/clothing/', views.MobileClothingListView.as_view(), name='mobile-clothing-list'),
|
path('mobile/products.json', views.MobileProductsDownloadView.as_view(), name='mobile-products-download'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
from django.shortcuts import render, get_object_or_404
|
from django.shortcuts import render, get_object_or_404
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse, JsonResponse
|
||||||
|
import json
|
||||||
from rest_framework import viewsets, status, generics, permissions
|
from rest_framework import viewsets, status, generics, permissions
|
||||||
from rest_framework.decorators import api_view, permission_classes, action
|
from rest_framework.decorators import api_view, permission_classes, action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -27,9 +28,10 @@ from .serializers import (
|
|||||||
CardSerializer, CardDetailSerializer, CardBatchSerializer, CardUsageLogSerializer,
|
CardSerializer, CardDetailSerializer, CardBatchSerializer, CardUsageLogSerializer,
|
||||||
CardScanSerializer, CardUseSerializer, CardBatchGenerateSerializer,
|
CardScanSerializer, CardUseSerializer, CardBatchGenerateSerializer,
|
||||||
CardTemplatePublishSerializer, CardBatchPublishSerializer, CardBatchManufactureSerializer,
|
CardTemplatePublishSerializer, CardBatchPublishSerializer, CardBatchManufactureSerializer,
|
||||||
CategoryTemplateSerializer, CategoryCardDetailSerializer, MobileClothingTemplateSerializer,
|
CategoryTemplateSerializer, CategoryCardDetailSerializer,
|
||||||
|
MobileProductSerializer,
|
||||||
ClothingAttributesSerializer, PropAttributesSerializer, SongAttributesSerializer,
|
ClothingAttributesSerializer, PropAttributesSerializer, SongAttributesSerializer,
|
||||||
DanceAttributesSerializer, FurnitureAttributesSerializer, DecorationAttributesSerializer
|
DanceAttributesSerializer, DecorationAttributesSerializer
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -236,13 +238,7 @@ class CardTemplateViewSet(viewsets.ModelViewSet):
|
|||||||
dance_serializer = DanceAttributesSerializer(data=dance_data)
|
dance_serializer = DanceAttributesSerializer(data=dance_data)
|
||||||
dance_serializer.is_valid(raise_exception=True)
|
dance_serializer.is_valid(raise_exception=True)
|
||||||
dance_serializer.save(template=template)
|
dance_serializer.save(template=template)
|
||||||
# 家具
|
# 家居装饰
|
||||||
if category == 'furniture' and 'furniture_attributes' in request.data:
|
|
||||||
furniture_data = request.data['furniture_attributes']
|
|
||||||
furniture_serializer = FurnitureAttributesSerializer(data=furniture_data)
|
|
||||||
furniture_serializer.is_valid(raise_exception=True)
|
|
||||||
furniture_serializer.save(template=template)
|
|
||||||
# 装饰
|
|
||||||
if category == 'decoration' and 'decoration_attributes' in request.data:
|
if category == 'decoration' and 'decoration_attributes' in request.data:
|
||||||
decoration_data = request.data['decoration_attributes']
|
decoration_data = request.data['decoration_attributes']
|
||||||
decoration_serializer = DecorationAttributesSerializer(data=decoration_data)
|
decoration_serializer = DecorationAttributesSerializer(data=decoration_data)
|
||||||
@ -1022,64 +1018,70 @@ class CategoryCardTemplateListView(generics.ListAPIView):
|
|||||||
return super().list(request, *args, **kwargs)
|
return super().list(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class MobileClothingListView(generics.ListAPIView):
|
class MobileProductsDownloadView(generics.GenericAPIView):
|
||||||
"""
|
"""
|
||||||
手机端服装列表接口
|
手机端产品数据下载接口
|
||||||
|
|
||||||
仅返回已发布的服装模板,供手机端展示。
|
将所有已发布的产品(服装、道具、歌曲、舞蹈、家居装饰、食物)
|
||||||
支持按稀有度、类型筛选和关键词搜索。
|
整合为一个 JSON 文件供手机端下载。
|
||||||
"""
|
"""
|
||||||
serializer_class = MobileClothingTemplateSerializer
|
permission_classes = []
|
||||||
permission_classes = [IsAuthenticated]
|
authentication_classes = []
|
||||||
authentication_classes = [RedisTokenAuthentication]
|
|
||||||
tags = ['手机端']
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get(self, request):
|
||||||
queryset = CardTemplate.objects.filter(
|
from food_app.models import Food
|
||||||
category='clothing',
|
from food_app.serializers import FoodSerializer
|
||||||
|
|
||||||
|
templates = CardTemplate.objects.filter(
|
||||||
status='published',
|
status='published',
|
||||||
).order_by('-published_at')
|
).order_by('category', '-published_at')
|
||||||
|
|
||||||
# 按稀有度筛选
|
categories = ['clothing', 'prop', 'song', 'dance', 'decoration']
|
||||||
rarity = self.request.query_params.get('rarity')
|
category_labels = {
|
||||||
if rarity:
|
'clothing': '服装',
|
||||||
queryset = queryset.filter(rarity=rarity)
|
'prop': '道具',
|
||||||
|
'song': '歌曲',
|
||||||
|
'dance': '舞蹈',
|
||||||
|
'decoration': '家居装饰',
|
||||||
|
}
|
||||||
|
|
||||||
# 按类型筛选
|
def normalize_image_url(items):
|
||||||
card_type = self.request.query_params.get('card_type')
|
"""统一 image_url 字段:有值则保留,空值设为 null;删除多余的 image 字段"""
|
||||||
if card_type:
|
for item in items:
|
||||||
queryset = queryset.filter(card_type=card_type)
|
# 兼容 food_app 使用 image 而非 image_url
|
||||||
|
if 'image_url' not in item and 'image' in item:
|
||||||
|
item['image_url'] = item['image']
|
||||||
|
# 删除原始 image 字段,只保留 image_url
|
||||||
|
item.pop('image', None)
|
||||||
|
# 空字符串统一为 null
|
||||||
|
if not item.get('image_url'):
|
||||||
|
item['image_url'] = None
|
||||||
|
return items
|
||||||
|
|
||||||
# 关键词搜索
|
data = {}
|
||||||
search = self.request.query_params.get('search')
|
for cat in categories:
|
||||||
if search:
|
cat_templates = templates.filter(category=cat)
|
||||||
queryset = queryset.filter(name__icontains=search)
|
if cat_templates.exists():
|
||||||
|
serializer = MobileProductSerializer(cat_templates, many=True)
|
||||||
|
items = normalize_image_url(serializer.data)
|
||||||
|
data[cat] = {
|
||||||
|
'label': category_labels.get(cat, cat),
|
||||||
|
'count': cat_templates.count(),
|
||||||
|
'items': items,
|
||||||
|
}
|
||||||
|
|
||||||
return queryset
|
# 添加食物数据(来自独立的 food_app 模块)
|
||||||
|
published_foods = Food.objects.filter(status='published').order_by('-published_at')
|
||||||
|
if published_foods.exists():
|
||||||
|
food_serializer = FoodSerializer(published_foods, many=True)
|
||||||
|
items = normalize_image_url(food_serializer.data)
|
||||||
|
data['food'] = {
|
||||||
|
'label': '食物',
|
||||||
|
'count': published_foods.count(),
|
||||||
|
'items': items,
|
||||||
|
}
|
||||||
|
|
||||||
@swagger_schema(
|
content = json.dumps(data, ensure_ascii=False, indent=2)
|
||||||
responses={
|
response = HttpResponse(content, content_type='application/json; charset=utf-8')
|
||||||
200: openapi.Response('查询成功', schema=openapi.Schema(
|
response['Content-Disposition'] = 'attachment; filename="products.json"'
|
||||||
type=openapi.TYPE_OBJECT,
|
return response
|
||||||
properties={
|
|
||||||
'count': openapi.Schema(type=openapi.TYPE_INTEGER, description='总数'),
|
|
||||||
'next': openapi.Schema(type=openapi.TYPE_STRING, description='下一页URL', nullable=True),
|
|
||||||
'previous': openapi.Schema(type=openapi.TYPE_STRING, description='上一页URL', nullable=True),
|
|
||||||
'results': openapi.Schema(
|
|
||||||
type=openapi.TYPE_ARRAY,
|
|
||||||
items=openapi.Schema(type=openapi.TYPE_OBJECT, description='服装模板'),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
},
|
|
||||||
operation_description="获取已发布的服装列表(手机端专用)",
|
|
||||||
tags=['手机端'],
|
|
||||||
manual_parameters=[
|
|
||||||
openapi.Parameter('rarity', openapi.IN_QUERY, description="按稀有度筛选 (common/uncommon/rare/epic/legendary/limited)", type=openapi.TYPE_STRING, required=False),
|
|
||||||
openapi.Parameter('card_type', openapi.IN_QUERY, description="按类型筛选 (regular/seasonal/event)", type=openapi.TYPE_STRING, required=False),
|
|
||||||
openapi.Parameter('search', openapi.IN_QUERY, description="按名称搜索", type=openapi.TYPE_STRING, required=False),
|
|
||||||
],
|
|
||||||
security=[{'Bearer': []}]
|
|
||||||
)
|
|
||||||
def list(self, request, *args, **kwargs):
|
|
||||||
return super().list(request, *args, **kwargs)
|
|
||||||
|
|||||||
@ -110,7 +110,7 @@ class StandardResponseMiddleware(MiddlewareMixin):
|
|||||||
|
|
||||||
# 确保content也更新
|
# 确保content也更新
|
||||||
try:
|
try:
|
||||||
response.content = json.dumps(response.data)
|
response.content = json.dumps(response.data, ensure_ascii=False, indent=2)
|
||||||
response['Content-Type'] = 'application/json'
|
response['Content-Type'] = 'application/json'
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@ -281,6 +281,7 @@ REST_FRAMEWORK = {
|
|||||||
},
|
},
|
||||||
'DEFAULT_PAGINATION_CLASS': 'common.pagination.CustomPageNumberPagination',
|
'DEFAULT_PAGINATION_CLASS': 'common.pagination.CustomPageNumberPagination',
|
||||||
'PAGE_SIZE': 10,
|
'PAGE_SIZE': 10,
|
||||||
|
'UNICODE_JSON': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Disable CSRF checks for API views
|
# Disable CSRF checks for API views
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user