160 lines
4.5 KiB
TypeScript
160 lines
4.5 KiB
TypeScript
'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">支持 JPG、PNG,最大 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>
|
||
)
|
||
}
|