gongxiangp2/components/upload/StandardModelWhiteBackgroundUpload.tsx

324 lines
14 KiB
TypeScript
Raw 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 { useMemo, useState } from 'react'
import Image from 'next/image'
import {
STANDARD_MODEL_WHITE_BG_VIEWS,
type StandardModelWhiteBgKey,
} from '@/lib/standard-model-white-background'
interface OtherImagePreview {
id: string
url: string
name: string
}
interface StandardModelWhiteBackgroundUploadProps {
imageUrls: Partial<Record<StandardModelWhiteBgKey, string>>
otherImages: OtherImagePreview[]
uploading?: boolean
error?: string
onSelectImage: (key: StandardModelWhiteBgKey, file: File) => void
onRemoveImage: (key: StandardModelWhiteBgKey) => void
onSelectOtherImages: (files: FileList | File[]) => void
onRemoveOtherImage: (id: string) => void
}
export default function StandardModelWhiteBackgroundUpload({
imageUrls,
otherImages,
uploading = false,
error,
onSelectImage,
onRemoveImage,
onSelectOtherImages,
onRemoveOtherImage,
}: StandardModelWhiteBackgroundUploadProps) {
const [draggingKey, setDraggingKey] = useState<StandardModelWhiteBgKey | null>(null)
const [isOtherDragging, setIsOtherDragging] = useState(false)
const uploadedRequiredCount = useMemo(
() => STANDARD_MODEL_WHITE_BG_VIEWS.filter((view) => !!imageUrls[view.key]).length,
[imageUrls],
)
const missingLabels = useMemo(
() =>
STANDARD_MODEL_WHITE_BG_VIEWS
.filter((view) => !imageUrls[view.key])
.map((view) => view.label),
[imageUrls],
)
const handleDragEnter = (event: React.DragEvent, key: StandardModelWhiteBgKey) => {
event.preventDefault()
event.stopPropagation()
setDraggingKey(key)
}
const handleDragLeave = (event: React.DragEvent, key: StandardModelWhiteBgKey) => {
event.preventDefault()
event.stopPropagation()
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect()
const { clientX, clientY } = event
if (clientX <= rect.left || clientX >= rect.right || clientY <= rect.top || clientY >= rect.bottom) {
if (draggingKey === key) {
setDraggingKey(null)
}
}
}
const handleDragOver = (event: React.DragEvent) => {
event.preventDefault()
event.stopPropagation()
event.dataTransfer.dropEffect = 'copy'
}
const handleDrop = (event: React.DragEvent, key: StandardModelWhiteBgKey) => {
event.preventDefault()
event.stopPropagation()
setDraggingKey(null)
const file = event.dataTransfer.files?.[0]
if (!file) {
return
}
onSelectImage(key, file)
}
const handleOtherDrop = (event: React.DragEvent) => {
event.preventDefault()
event.stopPropagation()
setIsOtherDragging(false)
const files = event.dataTransfer.files
if (!files || files.length === 0) {
return
}
onSelectOtherImages(files)
}
return (
<div className="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm dark:border-gray-700 dark:bg-gray-800">
<div className="flex flex-wrap items-center gap-2">
<span className="inline-flex items-center rounded-full bg-blue-50 px-3 py-1 text-xs font-medium text-blue-700 dark:bg-blue-900/30 dark:text-blue-300">
固定角度 {uploadedRequiredCount} / {STANDARD_MODEL_WHITE_BG_VIEWS.length}
</span>
<span className="inline-flex items-center rounded-full bg-gray-100 px-3 py-1 text-xs font-medium text-gray-700 dark:bg-gray-700 dark:text-gray-200">
可分次补传
</span>
{otherImages.length > 0 && (
<span className="inline-flex items-center rounded-full bg-gray-100 px-3 py-1 text-xs font-medium text-gray-700 dark:bg-gray-700 dark:text-gray-200">
其他图片 {otherImages.length} 张
</span>
)}
{uploading && (
<span className="inline-flex items-center rounded-full bg-amber-50 px-3 py-1 text-xs font-medium text-amber-700 dark:bg-amber-900/30 dark:text-amber-300">
上传处理中
</span>
)}
</div>
<div className="mt-3 rounded-xl border border-dashed border-gray-200 bg-gray-50/80 px-3 py-2 text-xs text-gray-600 dark:border-gray-700 dark:bg-gray-900/30 dark:text-gray-300">
{missingLabels.length > 0
? `未上传:${missingLabels.join('、')}(可先保存,后续再补)`
: '8 张固定角度已齐,可直接保存。'}
</div>
{error && (
<div className="mt-4 rounded-xl border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600 dark:border-red-900/40 dark:bg-red-900/20 dark:text-red-300">
{error}
</div>
)}
<div className="mt-5">
<div className="mb-3 flex items-center justify-between">
<h4 className="text-sm font-semibold text-gray-900 dark:text-white">固定角度</h4>
<span className="text-xs text-gray-500 dark:text-gray-400">可选,建议补齐 8 张</span>
</div>
<div className="grid grid-cols-2 gap-4 xl:grid-cols-4">
{STANDARD_MODEL_WHITE_BG_VIEWS.map((view) => {
const inputId = `white-bg-${view.key}`
const imageUrl = imageUrls[view.key]
const isDragging = draggingKey === view.key
return (
<div key={view.key} className="space-y-2">
<div className="flex items-center justify-between">
<p className="text-xs font-medium text-gray-700 dark:text-gray-200">{view.label}</p>
<span className="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-[10px] font-medium text-gray-600 dark:bg-gray-700 dark:text-gray-300">
可选
</span>
</div>
<input
id={inputId}
type="file"
accept="image/*"
className="hidden"
onChange={(event) => {
const file = event.target.files?.[0]
if (file) {
onSelectImage(view.key, file)
}
event.currentTarget.value = ''
}}
/>
{/* biome-ignore lint/a11y/noStaticElementInteractions: 上传槽位需要拖拽和点击交互 */}
<label
htmlFor={inputId}
className={`group block cursor-pointer rounded-2xl border-2 border-dashed p-3 transition-colors ${
isDragging
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: imageUrl
? 'border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-900/30'
: 'border-gray-300 bg-gray-50 dark:border-gray-600 dark:bg-gray-800/50 hover:border-gray-400 dark:hover:border-gray-500'
}`}
onDragEnter={(event) => handleDragEnter(event, view.key)}
onDragLeave={(event) => handleDragLeave(event, view.key)}
onDragOver={handleDragOver}
onDrop={(event) => handleDrop(event, view.key)}
>
{imageUrl ? (
<div className="space-y-2">
<div className="relative h-36 overflow-hidden rounded-xl border border-gray-200 dark:border-gray-700">
<Image
src={imageUrl}
alt={view.label}
fill
unoptimized
className="object-cover"
/>
<button
type="button"
aria-label={`删除${view.label}`}
className="absolute right-2 top-2 inline-flex h-7 w-7 items-center justify-center rounded-full bg-black/55 text-sm font-semibold text-white transition hover:bg-black/70"
onClick={(event) => {
event.preventDefault()
event.stopPropagation()
onRemoveImage(view.key)
}}
>
×
</button>
</div>
<p className="text-[11px] text-gray-500 dark:text-gray-400">点击或拖拽可重新上传</p>
</div>
) : (
<div className="flex h-36 flex-col items-center justify-center rounded-xl bg-white text-center dark:bg-gray-800">
<svg className="h-6 w-6 text-gray-400 group-hover:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
<span className="mt-2 text-xs text-gray-500 dark:text-gray-400">拖拽或点击上传</span>
</div>
)}
</label>
</div>
)
})}
</div>
</div>
<div className="mt-6 border-t border-gray-200 pt-5 dark:border-gray-700">
<div className="mb-3 flex items-center justify-between">
<h4 className="text-sm font-semibold text-gray-900 dark:text-white">其他图片</h4>
<span className="text-xs text-gray-500 dark:text-gray-400">可选,多张</span>
</div>
<input
id="white-bg-other-images"
type="file"
accept="image/*"
multiple
className="hidden"
onChange={(event) => {
if (event.target.files && event.target.files.length > 0) {
onSelectOtherImages(event.target.files)
}
event.currentTarget.value = ''
}}
/>
{/* biome-ignore lint/a11y/noStaticElementInteractions: 其他图片区需要拖拽上传 */}
<label
htmlFor="white-bg-other-images"
className={`block min-h-28 cursor-pointer rounded-2xl border-2 border-dashed px-4 py-5 transition-colors ${
isOtherDragging
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-300 bg-gray-50 dark:border-gray-600 dark:bg-gray-800/40 hover:border-gray-400 dark:hover:border-gray-500'
}`}
onDragEnter={(event) => {
event.preventDefault()
event.stopPropagation()
setIsOtherDragging(true)
}}
onDragLeave={(event) => {
event.preventDefault()
event.stopPropagation()
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect()
const { clientX, clientY } = event
if (clientX <= rect.left || clientX >= rect.right || clientY <= rect.top || clientY >= rect.bottom) {
setIsOtherDragging(false)
}
}}
onDragOver={handleDragOver}
onDrop={handleOtherDrop}
>
{otherImages.length > 0 ? (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4 xl:grid-cols-4">
{otherImages.map((image) => (
<div key={image.id} className="rounded-2xl border border-gray-200 bg-white/80 p-3 dark:border-gray-700 dark:bg-gray-900/40">
<div className="relative h-32 overflow-hidden rounded-xl border border-gray-200 dark:border-gray-700">
<Image
src={image.url}
alt={image.name}
fill
unoptimized
className="object-cover"
/>
<button
type="button"
aria-label={`删除${image.name}`}
className="absolute right-2 top-2 inline-flex h-7 w-7 items-center justify-center rounded-full bg-black/55 text-sm font-semibold text-white transition hover:bg-black/70"
onClick={(event) => {
event.preventDefault()
event.stopPropagation()
onRemoveOtherImage(image.id)
}}
>
×
</button>
</div>
<p className="mt-2 truncate text-xs text-gray-500 dark:text-gray-400">{image.name}</p>
</div>
))}
<div className="flex h-full min-h-[10rem] flex-col items-center justify-center rounded-2xl border border-dashed border-gray-300 bg-white/70 px-4 text-center dark:border-gray-600 dark:bg-gray-800/50">
<svg className="h-6 w-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<span className="mt-2 text-sm font-medium text-gray-700 dark:text-gray-200">继续添加</span>
<span className="mt-1 text-xs text-gray-500 dark:text-gray-400">拖拽或点击追加</span>
</div>
</div>
</div>
) : (
<div className="flex min-h-28 flex-col items-center justify-center text-center">
<svg className="h-6 w-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<span className="mt-2 text-sm font-medium text-gray-700 dark:text-gray-200">拖拽图片到这里,或点击追加</span>
<span className="mt-1 text-xs text-gray-500 dark:text-gray-400">支持一次添加多张其他图片</span>
</div>
)}
</label>
</div>
</div>
)
}