跳到主要内容

ImageSelector 图片选择器

ImageSelector 是一个用于上传和显示图片的组件。它允许用户选择多张图片,并提供了一个简单的界面来查看和删除已上传的图片。

API

ImageSelectorProps

属性说明类型默认值
nameinput 文件输入的 name 属性string-
className组件的类名string-
itemClass图片项的类名string-
defaultImages默认显示的图片数组string[]-
maxSize允许上传的最大文件大小(以字节为单位)number3 1024 1024
images当前已上传的图片的 URL 数组string[]-
onChange当图片数组发生变化时的回调函数,参数为当前的图片 URL 数组(images: string[]) => void-
upload上传函数,成功返回图片 url,失败返回 null(file: File) => Promise<string \| null>-
onError文件上传错误时的回调函数(file: File) => void-
addButtonClass添加按钮的类名string-
renderAddButton自定义渲染添加按钮的函数({ triggerFileInput }: { triggerFileInput: () => void }) => JSX.Element-
closeIconClass关闭图标的类名string-
renderCloseIcon自定义渲染关闭图标的函数({ handleRemoveImage, index }: { index: number; handleRemoveImage: (index: number) => void }) => JSX.Element-

代码演示

基本使用

在这个示例中,我们展示了 ImageSelector 组件的基本使用。用户可以通过点击 "+" 按钮来上传新的图片。上传的图片 URL 是一个模拟的 URL。

基础使用

+

default不可修改的主图

https://fakeimg.pl/350x200/?text=Hello
+

自定义添加按钮和关闭图标

https://fakeimg.pl/350x200/?text=Test1
https://fakeimg.pl/350x200/?text=Test2
Custom Add Button
收起源代码
import React, { useState } from 'react'
import { Space, ImageSelector } from '@dance-ui/ui'

export default () => {
const [selectedImages, setSelectedImages] = useState<string[]>([])

const upload = (file: File) => {
console.log(`uploadingImg`, file)
return 'https://fakeimg.pl/350x200/?text=MockUploadBackUrl'
}
const handleImagesSelected = (urls: string[]) => {
console.log(`handleImagesSelected`, urls)
setSelectedImages(urls)
}

const handleFileError = (file: File) => {
console.error(`File upload error: ${file.name}`)
}

const renderAddButton = ({ triggerFileInput }: { triggerFileInput: () => void }) => (
<div onClick={triggerFileInput} style={{ backgroundColor: 'lightblue', padding: '10px', borderRadius: '5px' }}>
Custom Add Button
</div>
)

const renderCloseIcon = ({ handleRemoveImage, index }: { index: number; handleRemoveImage: (index: number) => void }) => (
<div
onClick={() => handleRemoveImage(index)}
className="flex items-center justify-center"
style={{
position: 'absolute',
backgroundColor: 'red',
color: 'white',
top: '-12px',
right: '-12px',
borderRadius: '100%',
width: '24px',
height: '24px',
}}>
x
</div>
)
return (
<Space direction="vertical">
<Space direction="vertical">
<p>基础使用</p>
<ImageSelector upload={upload} images={selectedImages} onChange={handleImagesSelected} />
</Space>
<Space direction="vertical">
<p>default不可修改的主图</p>
<ImageSelector
upload={upload}
images={selectedImages}
onChange={handleImagesSelected}
defaultImages={['https://fakeimg.pl/350x200/?text=Hello']}
/>
</Space>
<Space direction="vertical">
<p>自定义添加按钮和关闭图标</p>
<ImageSelector
upload={upload}
images={selectedImages}
onChange={handleImagesSelected}
onError={handleFileError}
renderAddButton={renderAddButton}
renderCloseIcon={renderCloseIcon}
defaultImages={['https://fakeimg.pl/350x200/?text=Test1', 'https://fakeimg.pl/350x200/?text=Test2']}
/>
</Space>
</Space>
)
}

注意

  1. maxSize 属性定义了可以上传的最大文件大小字节数,其默认值为 3MB (310241024)。
  2. 上传函数 upload 是一个必须实现的函数,它接收一个文件对象作为参数,并返回一个 Promise。如果上传成功,Promise 应该解析为图片的 URL;如果上传失败,应该解析为 null。
  3. onError 函数是一个可选的回调,它在文件上传错误时被调用,接收失败的文件对象作为参数。
  4. renderAddButtonrenderCloseIcon 允许你自定义添加按钮和关闭图标的渲染。
  5. 删除图片时,目前的实现有一个小错误,它总是删除第一张图片而不是选定的图片。你应该使用 updatedImages.splice(index, 1) 而不是 updatedImages.shift() 来修复这个问题。

组件源码

组件源码
import { ChangeEvent, Ref, forwardRef, useCallback, useImperativeHandle, useRef } from 'react'
import { twMerge } from 'tailwind-merge'
import Icon, { IconType } from '../Icon'

export type ImageSelectorProps = {
name?: string
className?: string
itemClass?: string
defaultImages?: string[]
maxSize?: number
images: string[]
onChange: (images: string[]) => void
upload: (file: File) => Promise<string | null> // 上传函数,成功返回图片url,失败返回false
onError?: (file: File) => void
addButtonClass?: string
renderAddButton?: ({ triggerFileInput }: { triggerFileInput: () => void }) => JSX.Element
closeIconClass?: string
renderCloseIcon?: ({ handleRemoveImage, index }: { index: number; handleRemoveImage: (index: number) => void }) => JSX.Element
}
const MAX_IMAGE_SIZE = 3 * 1024 * 1024 // 3M in bytes
const ImageSelector = forwardRef(
(
{
name,
className,
itemClass,
defaultImages,
images,
onChange,
maxSize = MAX_IMAGE_SIZE,
upload,
onError,

addButtonClass,
renderAddButton,
closeIconClass,
renderCloseIcon,
}: ImageSelectorProps,
ref: Ref<HTMLInputElement>,
) => {
const fileInputRef = useRef<HTMLInputElement>(null)
const handleImageChange = useCallback(
async (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
const filesArray = Array.from(e.target.files).filter((file) => {
if (file.size > maxSize) {
onError?.(file)
return false
}
return true
})
try {
const res = await Promise.all(filesArray.map(upload))
const urls = res.filter((url) => {
if (url) return true
return false
})
onChange([...images, ...urls])
} catch (e) {
console.log(e)
}
}
},
[images, maxSize, onChange, onError, upload],
)

const handleRemoveImage = (index: number) => {
const updatedImages = [...images]
updatedImages.splice(index, 1)
onChange(updatedImages)
}
// 使用useImperativeHandle来同步内部ref和外部ref
useImperativeHandle(ref, () => fileInputRef.current)

const triggerFileInput = () => {
fileInputRef.current?.click()
}

return (
<div className={twMerge('flex flex-wrap items-center gap-3', className)}>
{defaultImages?.length
? defaultImages.map((url) => (
<div key={url} className={twMerge('relative h-32 w-32 rounded-lg bg-black/20 dark:bg-white/20', itemClass)}>
<img src={url} alt={url} className="h-full w-full rounded-lg object-cover" />
</div>
))
: null}
{images.map((url, index) => (
<div key={index} className={twMerge('relative h-32 w-32 rounded-lg bg-black/20 dark:bg-white/20', itemClass)}>
<img src={url} alt={url} className="h-full w-full rounded-lg object-cover" />
{renderCloseIcon ? (
renderCloseIcon({ handleRemoveImage, index })
) : (
<Icon
onClick={() => {
handleRemoveImage(index)
}}
type={IconType.CLOSE}
className={twMerge('absolute -right-3 -top-3 h-6 w-6 cursor-pointer fill-red-500', closeIconClass)}
/>
)}
</div>
))}
<input
type="file"
name={name}
ref={fileInputRef}
multiple
onChange={(e) => {
void handleImageChange(e)
}}
className="hidden"
/>
{renderAddButton ? (
renderAddButton({ triggerFileInput })
) : (
<div
className={twMerge(
'flex h-20 w-28 cursor-pointer items-center justify-center rounded-lg bg-black/10 text-xl font-bold dark:bg-white/20',
addButtonClass,
)}
onClick={triggerFileInput}>
+
</div>
)}
</div>
)
},
)
ImageSelector.displayName = 'ImageSelector'
ImageSelector.defaultProps = {
maxSize: MAX_IMAGE_SIZE,
}

export default ImageSelector