- 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>
251 lines
9.4 KiB
TypeScript
251 lines
9.4 KiB
TypeScript
"use client"
|
||
|
||
import React, { useState } from 'react'
|
||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||
import { FileUpload } from '@/components/ui/file-upload'
|
||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||
import { Image, Play, Music, Trash2 } from 'lucide-react'
|
||
import { Button } from '@/components/ui/button'
|
||
|
||
interface FoodMediaUploadProps {
|
||
/** 当前图片URL */
|
||
imageUrl?: string
|
||
/** 当前动画URL */
|
||
animationUrl?: string
|
||
/** 当前音频URL */
|
||
audioUrl?: string
|
||
/** 图片上传成功回调 */
|
||
onImageUpload?: (url: string) => void
|
||
/** 动画上传成功回调 */
|
||
onAnimationUpload?: (url: string) => void
|
||
/** 音频上传成功回调 */
|
||
onAudioUpload?: (url: string) => void
|
||
/** 文件移除回调 */
|
||
onRemove?: (type: 'image' | 'animation' | 'audio') => void
|
||
/** 是否禁用 */
|
||
disabled?: boolean
|
||
}
|
||
|
||
export function FoodMediaUpload({
|
||
imageUrl,
|
||
animationUrl,
|
||
audioUrl,
|
||
onImageUpload,
|
||
onAnimationUpload,
|
||
onAudioUpload,
|
||
onRemove,
|
||
disabled = false,
|
||
}: FoodMediaUploadProps) {
|
||
const [activeTab, setActiveTab] = useState('image')
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||
<TabsList className="grid w-full grid-cols-3">
|
||
<TabsTrigger value="image" className="flex items-center gap-2">
|
||
<Image className="h-4 w-4" />
|
||
图片
|
||
</TabsTrigger>
|
||
<TabsTrigger value="animation" className="flex items-center gap-2">
|
||
<Play className="h-4 w-4" />
|
||
动画
|
||
</TabsTrigger>
|
||
<TabsTrigger value="audio" className="flex items-center gap-2">
|
||
<Music className="h-4 w-4" />
|
||
音频
|
||
</TabsTrigger>
|
||
</TabsList>
|
||
|
||
{/* 图片上传 */}
|
||
<TabsContent value="image" className="space-y-4">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="text-lg flex items-center gap-2">
|
||
<Image className="h-5 w-5" />
|
||
食物图片
|
||
</CardTitle>
|
||
<CardDescription>
|
||
上传食物的展示图片,支持 JPEG、PNG、GIF 格式,建议尺寸 300x300px
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{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={() => onRemove?.('image')}
|
||
className="text-red-500 hover:text-red-700 hover:bg-red-50"
|
||
disabled={disabled}
|
||
>
|
||
<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={disabled}
|
||
onUploadSuccess={(files) => {
|
||
if (files.length > 0) {
|
||
onImageUpload?.(files[0].url)
|
||
}
|
||
}}
|
||
onRemove={() => onRemove?.('image')}
|
||
/>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</TabsContent>
|
||
|
||
{/* 动画上传 */}
|
||
<TabsContent value="animation" className="space-y-4">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="text-lg flex items-center gap-2">
|
||
<Play className="h-5 w-5" />
|
||
食物动画
|
||
</CardTitle>
|
||
<CardDescription>
|
||
上传食物的动画效果,支持 MP4、GIF、Lottie 格式,最大 50MB
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{animationUrl ? (
|
||
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-lg border">
|
||
<div className="flex items-center space-x-3">
|
||
{animationUrl.endsWith('.gif') ? (
|
||
<img
|
||
src={animationUrl}
|
||
alt="动画预览"
|
||
className="h-16 w-16 object-cover rounded border"
|
||
/>
|
||
) : animationUrl.match(/\.(mp4|avi|mov|wmv|flv)$/i) ? (
|
||
<video
|
||
src={animationUrl}
|
||
className="h-16 w-16 object-cover rounded border"
|
||
muted
|
||
loop
|
||
autoPlay
|
||
/>
|
||
) : (
|
||
<div className="h-16 w-16 bg-gray-200 rounded border flex items-center justify-center">
|
||
<Play className="h-6 w-6 text-gray-500" />
|
||
</div>
|
||
)}
|
||
<span className="text-sm text-gray-700">当前动画</span>
|
||
</div>
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => onRemove?.('animation')}
|
||
className="text-red-500 hover:text-red-700 hover:bg-red-50"
|
||
disabled={disabled}
|
||
>
|
||
<Trash2 className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
) : (
|
||
<FileUpload
|
||
imageOnly={false}
|
||
multiple={false}
|
||
maxFiles={1}
|
||
maxSize={50 * 1024 * 1024}
|
||
accept={{
|
||
'video/*': ['.mp4', '.avi', '.mov', '.wmv', '.flv'],
|
||
'image/gif': ['.gif'],
|
||
'application/json': ['.json'],
|
||
'application/x-lottie': ['.lottie']
|
||
}}
|
||
placeholder="选择或拖拽动画文件"
|
||
disabled={disabled}
|
||
onUploadSuccess={(files) => {
|
||
if (files.length > 0) {
|
||
onAnimationUpload?.(files[0].url)
|
||
}
|
||
}}
|
||
onRemove={() => onRemove?.('animation')}
|
||
/>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</TabsContent>
|
||
|
||
{/* 音频上传 */}
|
||
<TabsContent value="audio" className="space-y-4">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="text-lg flex items-center gap-2">
|
||
<Music className="h-5 w-5" />
|
||
食物音效
|
||
</CardTitle>
|
||
<CardDescription>
|
||
上传食物相关的音效,支持 MP3、WAV、OGG 等格式,最大 20MB
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{audioUrl ? (
|
||
<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-16 w-16 bg-blue-100 rounded border flex items-center justify-center">
|
||
<Music className="h-6 w-6 text-blue-500" />
|
||
</div>
|
||
<div className="flex-1">
|
||
<span className="text-sm text-gray-700 block">当前音频</span>
|
||
<audio controls className="mt-1 w-full max-w-xs">
|
||
<source src={audioUrl} />
|
||
您的浏览器不支持音频播放
|
||
</audio>
|
||
</div>
|
||
</div>
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => onRemove?.('audio')}
|
||
className="text-red-500 hover:text-red-700 hover:bg-red-50"
|
||
disabled={disabled}
|
||
>
|
||
<Trash2 className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
) : (
|
||
<FileUpload
|
||
imageOnly={false}
|
||
multiple={false}
|
||
maxFiles={1}
|
||
maxSize={20 * 1024 * 1024}
|
||
accept={{
|
||
'audio/*': ['.mp3', '.wav', '.ogg', '.aac', '.flac', '.wma', '.m4a']
|
||
}}
|
||
placeholder="选择或拖拽音频文件"
|
||
disabled={disabled}
|
||
onUploadSuccess={(files) => {
|
||
if (files.length > 0) {
|
||
onAudioUpload?.(files[0].url)
|
||
}
|
||
}}
|
||
onRemove={() => onRemove?.('audio')}
|
||
/>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</TabsContent>
|
||
</Tabs>
|
||
</div>
|
||
)
|
||
} |