gongxiangp2/components/upload/StandardModelWhiteBackgroundUpload.tsx

281 lines
10 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 { Plus } from 'lucide-react'
import {
STANDARD_MODEL_WHITE_BG_VIEWS,
type StandardModelWhiteBgKey,
} from '@/lib/standard-model-white-background'
import { hasDragLeftDropZone, prepareFileDrag } from './drag-drop-utils'
import { dropZoneActiveClass, dropZoneIdleClass, fieldHintClass, fieldLabelClass, uploadAreaTileClass } from './upload-field-styles'
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) => {
prepareFileDrag(event, uploading)
if (uploading) return
setDraggingKey(key)
}
const handleDragLeave = (event: React.DragEvent, key: StandardModelWhiteBgKey) => {
event.preventDefault()
event.stopPropagation()
if (hasDragLeftDropZone(event)) {
if (draggingKey === key) {
setDraggingKey(null)
}
}
}
const handleDragOver = (event: React.DragEvent) => {
prepareFileDrag(event, uploading)
}
const handleDrop = (event: React.DragEvent, key: StandardModelWhiteBgKey) => {
prepareFileDrag(event, uploading)
setDraggingKey(null)
if (uploading) return
const file = event.dataTransfer.files?.[0]
if (!file) {
return
}
onSelectImage(key, file)
}
const handleOtherDrop = (event: React.DragEvent) => {
prepareFileDrag(event, uploading)
setIsOtherDragging(false)
if (uploading) return
const files = event.dataTransfer.files
if (!files || files.length === 0) {
return
}
onSelectOtherImages(files)
}
return (
<div className="space-y-4">
{error && (
<div className="rounded-[8px] border border-[#fecdca] bg-[#fff1f0] px-3 py-2 text-sm text-[#b42318]">
{error}
</div>
)}
<div>
<div className="flex flex-wrap items-baseline justify-between gap-2">
<h4 className={fieldLabelClass}>固定角度</h4>
<span className={fieldHintClass}>已传 {uploadedRequiredCount} / {STANDARD_MODEL_WHITE_BG_VIEWS.length} · 可分次补传</span>
</div>
<p className={`mb-2 ${fieldHintClass}`}>
{missingLabels.length > 0 ? `待补:${missingLabels.join('、')}` : '固定角度已齐'}
</p>
<div className="grid grid-cols-2 gap-2 sm: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}>
<p className="mb-1 text-sm font-medium text-[#1a1c1d]">{view.label}</p>
<input
id={inputId}
type="file"
accept="image/*"
className="hidden"
disabled={uploading}
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}
aria-disabled={uploading}
className={`group block p-1.5 ${
uploading ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'
} ${isDragging ? dropZoneActiveClass : dropZoneIdleClass}`}
onDragEnter={(event) => handleDragEnter(event, view.key)}
onDragLeave={(event) => handleDragLeave(event, view.key)}
onDragOver={handleDragOver}
onDrop={(event) => handleDrop(event, view.key)}
>
{imageUrl ? (
<div>
<div className="relative h-16 overflow-hidden rounded-[6px]">
<Image
src={imageUrl}
alt={view.label}
fill
unoptimized
className="object-cover"
/>
<button
type="button"
aria-label={`删除${view.label}`}
disabled={uploading}
className="absolute right-1 top-1 inline-flex h-7 w-7 items-center justify-center rounded-[6px] bg-[#1a1c1d]/80 text-sm font-semibold text-white transition hover:bg-[#1a1c1d] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1677ff]"
onClick={(event) => {
event.preventDefault()
event.stopPropagation()
onRemoveImage(view.key)
}}
>
×
</button>
</div>
</div>
) : (
<div className="flex h-16 items-center justify-center gap-1 text-sm text-[#6b7075]">
<Plus className="h-4 w-4" aria-hidden="true" />
<span>添加图片</span>
</div>
)}
</label>
</div>
)
})}
</div>
</div>
<div>
<div className="flex items-baseline justify-between gap-2">
<h4 className={fieldLabelClass}>其他图片</h4>
<span className={fieldHintClass}>可选 · 多张{otherImages.length > 0 ? ` · 已选 ${otherImages.length} 张` : ''}</span>
</div>
<input
id="white-bg-other-images"
type="file"
accept="image/*"
multiple
className="hidden"
disabled={uploading}
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"
aria-disabled={uploading}
className={`block p-3 ${
uploading ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'
} ${isOtherDragging ? dropZoneActiveClass : dropZoneIdleClass}`}
onDragEnter={(event) => {
prepareFileDrag(event, uploading)
if (uploading) return
setIsOtherDragging(true)
}}
onDragLeave={(event) => {
event.preventDefault()
event.stopPropagation()
if (hasDragLeftDropZone(event)) {
setIsOtherDragging(false)
}
}}
onDragOver={handleDragOver}
onDrop={handleOtherDrop}
>
{otherImages.length > 0 ? (
<div className="flex flex-wrap gap-2">
{otherImages.map((image) => (
<div key={image.id} className={uploadAreaTileClass} title={image.name}>
<Image
src={image.url}
alt={image.name}
fill
unoptimized
className="object-cover"
/>
<button
type="button"
aria-label={`删除${image.name}`}
disabled={uploading}
className="absolute right-1 top-1 inline-flex h-7 w-7 items-center justify-center rounded-[6px] bg-[#1a1c1d]/80 text-sm font-semibold text-white transition hover:bg-[#1a1c1d] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1677ff]"
onClick={(event) => {
event.preventDefault()
event.stopPropagation()
onRemoveOtherImage(image.id)
}}
>
×
</button>
</div>
))}
<div className="flex h-20 w-20 items-center justify-center rounded-[8px] border border-dashed border-[#cbd5e1] bg-white text-sm text-[#1a1c1d]">
<Plus className="h-5 w-5" aria-hidden="true" />
<span className="sr-only">继续添加</span>
</div>
</div>
) : (
<div className="flex min-h-12 items-center gap-2 text-sm font-medium text-[#1a1c1d]">
<Plus className="h-5 w-5 text-[#6b7075]" aria-hidden="true" />
<span>添加其他图片,或拖入文件</span>
</div>
)}
</label>
</div>
</div>
)
}