-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #55 from dancing-team/feat/ImageSelector
feat: add ImageSelector
- Loading branch information
Showing
6 changed files
with
276 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
--- | ||
'@dance-ui/ui': patch | ||
'@dance-ui/example': patch | ||
'@dance-ui/demo': patch | ||
--- | ||
|
||
feat: 新增图片选择器组件 ImageSelector |
135 changes: 135 additions & 0 deletions
135
packages/components/src/ImageSelector/ImageSelector.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import ImageSelector from './ImageSelector' | ||
export type { ImageSelectorProps } from './ImageSelector' | ||
export default ImageSelector |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
--- | ||
sidebar_position: 6 | ||
--- | ||
|
||
import ComponentSource from '!!raw-loader!../../../components/src/ImageSelector/ImageSelector' | ||
import { ImageSelector } from '@dance-ui/ui' | ||
|
||
# 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。 | ||
|
||
import DemoSrc from '!!raw-loader!../../../components/src/ImageSelector/demo' | ||
import Demo from '../../../components/src/ImageSelector/demo' | ||
|
||
<DemoBlock src={DemoSrc}> | ||
<Demo /> | ||
</DemoBlock> | ||
|
||
### 注意 | ||
|
||
1. `maxSize` 属性定义了可以上传的最大文件大小字节数,其默认值为 3MB (3*1024*1024)。 | ||
2. 上传函数 `upload` 是一个必须实现的函数,它接收一个文件对象作为参数,并返回一个 Promise。如果上传成功,Promise 应该解析为图片的 URL;如果上传失败,应该解析为 null。 | ||
3. `onError` 函数是一个可选的回调,它在文件上传错误时被调用,接收失败的文件对象作为参数。 | ||
4. `renderAddButton` 和 `renderCloseIcon` 允许你自定义添加按钮和关闭图标的渲染。 | ||
5. 删除图片时,目前的实现有一个小错误,它总是删除第一张图片而不是选定的图片。你应该使用 `updatedImages.splice(index, 1)` 而不是 `updatedImages.shift()` 来修复这个问题。 | ||
|
||
### 组件源码 | ||
|
||
<CodeBlock title="组件源码" language={'tsx'} showLineNumbers> | ||
{ComponentSource} | ||
</CodeBlock> |