ImageSelector 图片选择器
ImageSelector
是一个用于上传和显示图片的组件。它允许用户选择多张图片,并提供了一个简单的界面来查看和删除已上传的图片。
API
ImageSelectorProps
属性 | 说明 | 类型 | 默认值 |
---|---|---|---|
name | input 文件输入的 name 属性 | string | - |
className | 组件的类名 | string | - |
itemClass | 图片项的类名 | string | - |
defaultImages | 默认显示的图片数组 | string[] | - |
maxSize | 允许上传的最大文件大小(以字节为单位) | number | 3 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不可修改的主图
+
自定义添加按钮和关闭图标
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>
)
}
注意
maxSize
属性定义了可以上传的最大文件大小字节数,其默认值为 3MB (310241024)。- 上传函数
upload
是一个必须实现的函数,它接收一个文件对象作为参数,并返回一个 Promise。如果上传成功,Promise 应该解析为图片的 URL;如果上传失败,应该解析为 null。 onError
函数是一个可选的回调,它在文件上传错误时被调用,接收失败的文件对象作为参数。renderAddButton
和renderCloseIcon
允许你自定义添加按钮和关闭图标的渲染。- 删除图片时,目前的实现有一个小错误,它总是删除第一张图片而不是选定的图片。你应该使用
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