提交 bfdfeaa6 编写于 作者: L liuyijun

feat: 提交uploader组件第一版

上级 d30aa9b1
......@@ -170,7 +170,18 @@
},
{
"name": "数据录入",
"packages": []
"packages": [
{
"version": "1.0.0",
"name": "Uploader",
"type": "component",
"cName": "上传",
"desc": "用于将本地的图片或文件上传至服务器。",
"sort": 1,
"show": true,
"author": "swag~jun"
}
]
},
{
"name": "业务组件",
......
......@@ -13,6 +13,10 @@
left: 50%;
transform: translate(-50%, -50%);
}
.nut-icon__img {
width: 100%;
height: 100%;
}
.text {
display: inline-block;
width: 100%;
......
.demo-avatar {
color: #fff;
}
import React from 'react'
import { Avatar } from './avatar'
import Cell from '@/packages/cell'
import './demo.scss'
const AvatarDemo = () => {
const AvatarStyle = {
......@@ -29,16 +30,16 @@ const AvatarDemo = () => {
</Cell>
<h2>修改形状</h2>
<Cell>
<Avatar shape="square"></Avatar>
<Avatar shape="round"></Avatar>
<Avatar icon="my" shape="square"></Avatar>
<Avatar icon="my" shape="round"></Avatar>
</Cell>
<h2>修改背景色</h2>
<Cell>
<Avatar bgColor="#FA2C19"></Avatar>
<Avatar className="demo-avatar" bgColor="#FA2C19" icon="my"></Avatar>
</Cell>
<h2>修改背景图片</h2>
<Cell>
<Avatar icon="https://img30.360buyimg.com/uba/jfs/t1/84318/29/2102/10483/5d0704c1Eb767fa74/fc456b03fdd6cbab.png"></Avatar>
<Avatar icon="https://img12.360buyimg.com/imagetools/jfs/t1/196430/38/8105/14329/60c806a4Ed506298a/e6de9fb7b8490f38.png"></Avatar>
</Cell>
<h2>可以修改头像的内容</h2>
<Cell>
......
.demo.bg-w {
background: #fff;
}
import React from 'react'
import { Uploader, FileItem } from './uploader'
import Button from '@/packages/button'
const UploaderDemo = () => {
const uploadUrl = 'https://my-json-server.typicode.com/linrufeng/demo/posts'
const formData = {
custom: 'test',
}
const fileToDataURL = (file: Blob): Promise<any> => {
return new Promise((resolve) => {
const reader = new FileReader()
reader.onloadend = (e) => resolve((e.target as FileReader).result)
reader.readAsDataURL(file)
})
}
const dataURLToImage = (dataURL: string): Promise<HTMLImageElement> => {
return new Promise((resolve) => {
const img = new Image()
img.onload = () => resolve(img)
img.src = dataURL
})
}
const canvastoFile = (
canvas: HTMLCanvasElement,
type: string,
quality: number
): Promise<Blob | null> => {
return new Promise((resolve) => canvas.toBlob((blob) => resolve(blob), type, quality))
}
const onOversize = (files: File[]) => {
console.log('oversize 触发 文件大小不能超过 50kb', files)
}
const onStart = () => {
console.log('start 触发')
}
const onDelete = (file: FileItem, fileList: FileItem[]) => {
console.log('delete 事件触发', file, fileList)
}
const beforeUpload = async (files: File[]) => {
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d') as CanvasRenderingContext2D
const base64 = await fileToDataURL(files[0])
const img = await dataURLToImage(base64)
canvas.width = img.width
canvas.height = img.height
context.clearRect(0, 0, img.width, img.height)
context.drawImage(img, 0, 0, img.width, img.height)
let blob = (await canvastoFile(canvas, 'image/jpeg', 0.5)) as Blob //quality:0.5可根据实际情况计算
const f = await new File([blob], files[0].name, { type: files[0].type })
return [f]
}
return (
<>
<div className="demo bg-w">
<h2>基础用法</h2>
<Uploader url={uploadUrl} start={onStart}></Uploader>
<h2>自定义上传样式</h2>
<Uploader url={uploadUrl}>
<Button type="primary" icon="uploader">
上传文件
</Button>
</Uploader>
<h2>直接调起摄像头(移动端生效)</h2>
<Uploader capture></Uploader>
<h2>上传状态</h2>
<Uploader url={uploadUrl} multiple removeImage={onDelete}></Uploader>
<h2>限制上传数量5个</h2>
<Uploader url={uploadUrl} multiple maximum="5"></Uploader>
<h2>限制上传大小(每个文件最大不超过 50kb)</h2>
<Uploader url={uploadUrl} multiple maximize={1024 * 50} oversize={onOversize}></Uploader>
<h2>限制上传大小(在beforeupload钩子中处理)</h2>
<Uploader
url={uploadUrl}
multiple
beforeUpload={beforeUpload}
maximize={1024 * 50}
oversize={onOversize}
></Uploader>
<h2>自定义数据 FormData 、 headers </h2>
<Uploader
url={uploadUrl}
data={formData}
headers={formData}
withCredentials={true}
></Uploader>
<h2>禁用状态</h2>
<Uploader disabled></Uploader>
</div>
</>
)
}
export default UploaderDemo
# Uploader 上传
### 介绍
用于将本地的图片或文件上传至服务器。
### 安装
``` javascript
import { Uploader } from '@nutui/nutui';
```
## 代码示例
### 基本用法
``` tsx
<Uploader url="http://服务器地址"></Uploader>
```
### 自定义上传样式
``` tsx
<Uploader url="http://服务器地址">
<Button type="primary" icon="uploader">上传文件</Button>
</Uploader>
```
### 直接调起摄像头(移动端生效)
``` tsx
<Uploader url="http://服务器地址" capture></Uploader>
```
### 限制上传数量5个
``` tsx
<Uploader url="http://服务器地址" multiple maximum="5"></Uploader>
```
### 限制上传大小(每个文件最大不超过 50kb,也可以在beforeupload中自行处理)
``` tsx
<Uploader url="http://服务器地址" multiple maximize={1024 * 50} beforeUpload={beforeUpload} oversize={onOversize}></Uploader>
```
``` javascript
const formData = {
custom: 'test'
};
const onOversize = (files: File[]) => {
console.log('oversize 触发 文件大小不能超过 50kb', files);
};
const beforeUpload = (files: File[]) => {
//自定义处理
return files;
}
```
### 自定义 FormData headers
``` tsx
<Uploader url="http://服务器地址" data={formData} headers={formData} withCredentials={true}></Uploader>
```
``` javascript
const formData = {
custom: 'test'
};
const onOversize = (files: File[]) => {
console.log('oversize 触发 文件大小不能超过 50kb', files);
};
const beforeUpload = (files: File[]) => {
//自定义处理
return files;
}
```
### 禁用状态
``` tsx
<Uploader disabled></Uploader>
```
### Prop
| 字段 | 说明 | 类型 | 默认值 |
|-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------|------------------|
| name | `input` 标签 `name` 的名称,发到后台的文件参数名 | String | "file" |
| url | 上传服务器的接口地址 | String | - |
| isPreview | 是否上传成功后展示预览图 | Boolean | true |
| isDeletable | 是否展示删除按钮 | Boolean | true |
| method | 上传请求的 http method | String | "post" |
| capture | 图片[选取模式](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/input#htmlattrdefcapture),直接调起摄像头 | String | false |
| maximize | 可以设定最大上传文件的大小(字节) | Number丨String | Number.MAX_VALUE |
| maximum | 文件上传数量限制 | Number丨String | 1 |
| clearInput | 是否需要清空`input`内容,设为`true`支持重复选择上传同一个文件 | Boolean | false |
| accept | 允许上传的文件类型,[详细说明](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/Input/file#%E9%99%90%E5%88%B6%E5%85%81%E8%AE%B8%E7%9A%84%E6%96%87%E4%BB%B6%E7%B1%BB%E5%9E%8B) | String | * |
| headers | 设置上传的请求头部 | Object | {} |
| data | 附加上传的信息 formData | Object | {} |
| uploadIcon | 上传区域[图标名称](#/zh-CN/icon)或图片链接 | String | "photograph" |
| xhrState | 接口响应的成功状态(status)值 | Number | 200 |
| withCredentials | 支持发送 cookie 凭证信息 | Boolean | fasle |
| multiple | 是否支持文件多选 | Boolean | fasle |
| disabled | 是否禁用文件上传 | Boolean | fasle |
| timeout | 超时时间,单位为毫秒 | Number丨String | 1000 * 30 |
| beforeUpload | 上传前的函数需要返回一个`Promise`对象 | Function | null |
| beforeDelete | 除文件时的回调,返回值为 false 时不移除。支持返回一个 `Promise` 对象,`Promise` 对象 resolve(false) 或 reject 时不移除 | Function(file): boolean 丨Promise | - |
### FileItem
| 名称 | 说明 | 默认值 |
|----------|---------------------------------------------------------|---------------------------------|
| status | 文件状态值,可选'ready,uploading,success,error,removed' | "ready" |
| uid | 文件的唯一标识 | new Date().getTime().toString() |
| name | 文件名称 | "" |
| url | 文件路径 | "" |
| type | 文件类型 | "image/jpeg" |
| formData | 上传所需的data | new FormData() |
### Event
| 名称 | 说明 | 回调参数 |
|----------|------------------------|----------------------|
| start | 文件上传开始 | options |
| progress | 文件上传的进度 | event,options |
| oversize | 文件大小超过限制时触发 | files |
| success | 上传成功 | responseText,options |
| failure | 上传失败 | responseText,options |
| change | 上传文件改变时的状态 | fileList,event |
| removeImage | 文件删除之前的状态 | files,fileList |
import { Uploader } from './uploader'
export default Uploader
export class UploadOptions {
url = ''
formData?: FormData
method = 'post'
xhrState: string | number = 200
timeout: number = 30 * 1000
headers = {}
withCredentials = false
onStart?: Function
onProgress?: Function
onSuccess?: Function
onFailure?: Function
}
export class Upload {
options: UploadOptions
constructor(options: UploadOptions) {
this.options = options
}
upload() {
const options = this.options
const xhr = new XMLHttpRequest()
xhr.timeout = options.timeout
if (xhr.upload) {
xhr.upload.addEventListener(
'progress',
(e: ProgressEvent<XMLHttpRequestEventTarget>) => {
options.onProgress?.(e, options)
},
false
)
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === options.xhrState) {
options.onSuccess?.(xhr.responseText, options)
} else {
options.onFailure?.(xhr.responseText, options)
}
}
}
xhr.withCredentials = options.withCredentials
xhr.open(options.method, options.url, true)
// headers
for (const [key, value] of Object.entries(options.headers)) {
xhr.setRequestHeader(key, value as string)
}
options.onStart?.(options)
xhr.send(options.formData)
} else {
console.warn('浏览器不支持 XMLHttpRequest')
}
}
}
.nut-uploader {
position: relative;
display: flex;
flex-wrap: wrap;
&__slot {
position: relative;
}
&__upload {
position: relative;
background: $uploader-background;
width: $uploader-width;
height: $uploader-height;
display: flex;
align-items: center;
justify-content: center;
}
&__input {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
cursor: pointer;
opacity: 0;
&:disabled {
cursor: not-allowed;
}
}
&__preview {
width: $uploader-width;
height: $uploader-height;
display: flex;
align-items: center;
justify-content: center;
margin-right: 10px;
margin-bottom: 10px;
&-img {
position: relative;
width: 100%;
height: 100%;
.close {
position: absolute;
right: 0;
top: 0;
transform: translate(50%, -50%);
}
.tips {
position: absolute;
bottom: 0;
left: 0;
right: 0;
text-align: center;
font-size: 12px;
color: $white;
height: 30px;
line-height: 30px;
text-align: c;
background: rgba(0, 0, 0, 0.54);
}
&__c {
height: 100%;
width: 100%;
}
}
}
}
import React, { useState, FunctionComponent } from 'react'
import Icon from '@/packages/icon'
import { Upload, UploadOptions } from './upload'
import bem from '@/utils/bem'
import './uploader.scss'
export interface UploaderProps {
url: string
maximum: string | number
maximize: number
uploadIcon: string
name: string
accept: string
disabled: boolean
multiple: boolean
timeout: number
data: object
method: string
xhrState: number | string
headers: object
withCredentials: boolean
clearInput: boolean
isPreview: boolean
isDeletable: boolean
capture: boolean
start?: (option: UploadOptions) => void
removeImage?: (file: FileItem, fileList: FileItem[]) => void
success?: (param: { responseText: XMLHttpRequest['responseText']; option: UploadOptions }) => void
progress?: (param: { e: ProgressEvent<XMLHttpRequestEventTarget>; option: UploadOptions }) => void
failure?: (param: { responseText: XMLHttpRequest['responseText']; option: UploadOptions }) => void
update?: (fileList: any[]) => void
oversize?: (file: File[]) => void
change?: (param: { fileList: any[]; event: React.ChangeEvent<HTMLInputElement> }) => void
beforeUpload?: (file: File[]) => Promise<File[]>
beforeDelete?: (file: FileItem, files: FileItem[]) => boolean
}
export type FileItemStatus = 'ready' | 'uploading' | 'success' | 'error' | 'removed'
const defaultProps: UploaderProps = {
url: '',
maximum: 1,
uploadIcon: 'photograph',
name: 'file',
accept: '*',
disabled: false,
multiple: false,
maximize: Number.MAX_VALUE,
data: {},
headers: {},
method: 'post',
xhrState: 200,
timeout: 1000 * 30,
withCredentials: false,
clearInput: false,
isPreview: true,
isDeletable: true,
capture: false,
beforeDelete: (file: FileItem, files: FileItem[]) => {
return true
},
}
export class FileItem {
status: FileItemStatus = 'ready'
uid: string = new Date().getTime().toString()
name?: string
url?: string
type?: string
formData: FormData = new FormData()
}
export const Uploader: FunctionComponent<
Partial<UploaderProps> & React.HTMLAttributes<HTMLDivElement>
> = (props) => {
const {
children,
uploadIcon,
name,
accept,
disabled,
multiple,
url,
headers,
timeout,
method,
xhrState,
withCredentials,
data,
isPreview,
isDeletable,
maximum,
capture,
maximize,
start,
removeImage,
progress,
success,
update,
failure,
beforeDelete,
} = { ...defaultProps, ...props }
const [fileList, setFileList] = useState<any>([])
const b = bem('uploader')
const clearInput = (el: HTMLInputElement) => {
el.value = ''
}
const executeUpload = (fileItem: FileItem) => {
const uploadOption = new UploadOptions()
uploadOption.url = url
for (const [key, value] of Object.entries(data)) {
fileItem.formData.append(key, value)
}
uploadOption.formData = fileItem.formData
uploadOption.timeout = timeout * 1
uploadOption.method = method
uploadOption.xhrState = xhrState
uploadOption.headers = headers
uploadOption.withCredentials = withCredentials
uploadOption.onStart = (option: UploadOptions) => {
setFileList((fileList: FileItem[]) => {
fileList.map((item) => {
if (item.uid === fileItem.uid) {
item.status = 'ready'
}
})
return [...fileList]
})
start && start(option)
}
uploadOption.onProgress = (
e: ProgressEvent<XMLHttpRequestEventTarget>,
option: UploadOptions
) => {
console.log('progress', e, option)
setFileList((fileList: FileItem[]) => {
fileList.map((item) => {
if (item.uid === fileItem.uid) {
item.status = 'uploading'
}
})
return [...fileList]
})
progress && progress({ e, option })
}
uploadOption.onSuccess = (
responseText: XMLHttpRequest['responseText'],
option: UploadOptions
) => {
setFileList((fileList: FileItem[]) => {
update && update(fileList)
fileList.map((item) => {
if (item.uid === fileItem.uid) {
item.status = 'success'
}
})
return [...fileList]
})
success &&
success({
responseText,
option,
})
}
uploadOption.onFailure = (
responseText: XMLHttpRequest['responseText'],
option: UploadOptions
) => {
setFileList((fileList: FileItem[]) => {
fileList.map((item) => {
if (item.uid === fileItem.uid) {
item.status = 'error'
}
})
return [...fileList]
})
failure &&
failure({
responseText,
option,
})
}
new Upload(uploadOption).upload()
}
const readFile = (files: File[]) => {
files.forEach((file: File) => {
const formData = new FormData()
formData.append(name, file)
const fileItem = new FileItem()
fileItem.name = file.name
fileItem.status = 'uploading'
fileItem.type = file.type
fileItem.formData = formData
executeUpload(fileItem)
if (isPreview && file.type.includes('image')) {
const reader = new FileReader()
reader.onload = (event: ProgressEvent<FileReader>) => {
fileItem.url = (event.target as FileReader).result as string
fileList.push(fileItem)
setFileList([...fileList])
}
reader.readAsDataURL(file)
} else {
fileList.push(fileItem)
setFileList([...fileList])
}
})
}
const filterFiles = (files: File[]) => {
const maximum = (props.maximum as number) * 1
const oversizes = new Array<File>()
const filterFile = files.filter((file: File) => {
if (file.size > maximize) {
oversizes.push(file)
return false
} else {
return true
}
})
if (oversizes.length) {
props.oversize && props.oversize(files)
}
if (filterFile.length > maximum) {
filterFile.splice(maximum - 1, filterFile.length - maximum)
}
return filterFile
}
const onDelete = (file: FileItem, index: number) => {
if (beforeDelete && beforeDelete(file, fileList)) {
fileList.splice(index, 1)
removeImage && removeImage(file, fileList)
setFileList([...fileList])
} else {
console.log('用户阻止了删除!')
}
}
const fileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (disabled) {
return
}
const $el = event.target
let { files } = $el
if (props.beforeUpload) {
props.beforeUpload(new Array<File>().slice.call(files)).then((f: Array<File>) => {
const _files: File[] = filterFiles(new Array<File>().slice.call(f))
readFile(_files)
})
} else {
const _files = filterFiles(new Array<File>().slice.call(files))
readFile(_files)
}
props.change && props.change({ fileList, event })
if (props.clearInput) {
clearInput($el)
}
}
return (
<div className={`${b()}`}>
{children ? (
<div className="nut-uploader__slot">
{
<>
{children}
{maximum > fileList.length && (
<>
{capture ? (
<input
className="nut-uploader__input"
type="file"
capture="camera"
name={name}
accept={accept}
disabled={disabled}
multiple={multiple}
onChange={fileChange}
/>
) : (
<input
className="nut-uploader__input"
type="file"
name={name}
accept={accept}
disabled={disabled}
multiple={multiple}
onChange={fileChange}
/>
)}
</>
)}
</>
}
</div>
) : (
<>
{fileList.length !== 0 &&
fileList.map((item: any, index: number) => {
return (
<div className="nut-uploader__preview" key={item.uid}>
<div className="nut-uploader__preview-img">
{isDeletable && (
<Icon
color="rgba(0,0,0,0.6)"
className="close"
name="circle-close"
click={() => onDelete(item, index)}
/>
)}
{item.type.includes('image') && item.url && (
<img className="nut-uploader__preview-img__c" src={item.url} />
)}
{item.status !== 'success' && <div className="tips">{item.status}</div>}
</div>
</div>
)
})}
{maximum > fileList.length && (
<div className="nut-uploader__upload">
<Icon color="#808080" name={uploadIcon} />
{capture ? (
<input
className="nut-uploader__input"
type="file"
capture="camera"
name={name}
accept={accept}
disabled={disabled}
multiple={multiple}
onChange={fileChange}
/>
) : (
<input
className="nut-uploader__input"
type="file"
name={name}
accept={accept}
disabled={disabled}
multiple={multiple}
onChange={fileChange}
/>
)}
</div>
)}
</>
)}
</div>
)
}
Uploader.defaultProps = defaultProps
Uploader.displayName = 'NutUploader'
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册