Files
MemeStudio/components/levels/image-uploader.tsx

160 lines
4.5 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, useRef } from 'react'
import { Button } from '@/components/ui/button'
import { X, Image as ImageIcon } from 'lucide-react'
import Image from 'next/image'
import { Spinner } from '@/components/ui/spinner'
import COS from 'cos-js-sdk-v5'
import { apiFetch } from '@/lib/api'
interface ImageUploaderProps {
value: string
onChange: (url: string) => void
}
export function ImageUploader({ value, onChange }: ImageUploaderProps) {
const [isUploading, setIsUploading] = useState(false)
const [error, setError] = useState('')
const fileInputRef = useRef<HTMLInputElement>(null)
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
// Validate file type
if (!file.type.startsWith('image/')) {
setError('请选择图片文件')
return
}
// Validate file size (max 5MB)
if (file.size > 5 * 1024 * 1024) {
setError('图片大小不能超过 5MB')
return
}
setError('')
setIsUploading(true)
try {
// Get temp key
const keyRes = await apiFetch('/api/cos/temp-key')
if (!keyRes.ok) {
throw new Error('获取上传凭证失败')
}
const keyData = await keyRes.json()
// Generate unique filename
const ext = file.name.split('.').pop() || 'jpg'
const timestamp = Date.now()
const randomStr = Math.random().toString(36).substring(2, 8)
const filename = `mini_game/images/${timestamp}_${randomStr}.${ext}`
// Initialize COS with temp credentials
const cos = new COS({
getAuthorization: (_options, callback) => {
callback({
TmpSecretId: keyData.credentials.tmpSecretId,
TmpSecretKey: keyData.credentials.tmpSecretKey,
SecurityToken: keyData.credentials.sessionToken,
StartTime: keyData.startTime,
ExpiredTime: keyData.expiredTime,
})
},
})
// Upload file
const uploadUrl = await new Promise<string>((resolve, reject) => {
cos.putObject(
{
Bucket: keyData.bucket,
Region: keyData.region,
Key: filename,
Body: file,
},
(err, data) => {
if (err) {
reject(new Error(err.message || '上传失败'))
return
}
const url = `https://${keyData.bucket}.cos.${keyData.region}.myqcloud.com/${filename}`
resolve(url)
}
)
})
onChange(uploadUrl)
} catch (err) {
console.error('Upload error:', err)
setError(err instanceof Error ? err.message : '上传失败')
} finally {
setIsUploading(false)
// Reset file input
if (fileInputRef.current) {
fileInputRef.current.value = ''
}
}
}
const handleRemove = () => {
onChange('')
setError('')
}
return (
<div className="space-y-2">
{value ? (
<div className="relative w-full h-40 rounded-lg border overflow-hidden bg-gray-50">
<Image
src={value}
alt="预览图片"
fill
className="object-contain"
sizes="(max-width: 500px) 100vw, 500px"
/>
<Button
type="button"
variant="destructive"
size="icon"
className="absolute top-2 right-2 h-8 w-8"
onClick={handleRemove}
>
<X className="h-4 w-4" />
</Button>
</div>
) : (
<div
className="w-full h-40 border-2 border-dashed rounded-lg flex flex-col items-center justify-center cursor-pointer hover:border-primary hover:bg-gray-50 transition-colors"
onClick={() => fileInputRef.current?.click()}
>
{isUploading ? (
<>
<Spinner size="lg" />
<p className="mt-2 text-sm text-gray-500">...</p>
</>
) : (
<>
<ImageIcon className="h-10 w-10 text-gray-400" />
<p className="mt-2 text-sm text-gray-500"></p>
<p className="text-xs text-gray-400"> JPGPNG 5MB</p>
</>
)}
</div>
)}
{error && (
<p className="text-sm text-red-600">{error}</p>
)}
<input
ref={fileInputRef}
type="file"
accept="image/*"
className="hidden"
onChange={handleFileSelect}
/>
</div>
)
}