From bfdfeaa65ae93fbd48612e42d5e971f2195db894 Mon Sep 17 00:00:00 2001
From: liuyijun <3476078473@qq.com>
Date: Mon, 9 Aug 2021 18:19:56 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8F=90=E4=BA=A4uploader=E7=BB=84?=
=?UTF-8?q?=E4=BB=B6=E7=AC=AC=E4=B8=80=E7=89=88?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/config.json | 13 +-
src/packages/avatar/avatar.scss | 4 +
src/packages/avatar/demo.scss | 3 +
src/packages/avatar/demo.tsx | 9 +-
src/packages/uploader/demo.scss | 3 +
src/packages/uploader/demo.tsx | 94 ++++++++
src/packages/uploader/doc.md | 133 +++++++++++
src/packages/uploader/index.ts | 2 +
src/packages/uploader/upload.ts | 52 ++++
src/packages/uploader/uploader.scss | 71 ++++++
src/packages/uploader/uploader.tsx | 358 ++++++++++++++++++++++++++++
11 files changed, 737 insertions(+), 5 deletions(-)
create mode 100644 src/packages/avatar/demo.scss
create mode 100644 src/packages/uploader/demo.scss
create mode 100644 src/packages/uploader/demo.tsx
create mode 100644 src/packages/uploader/doc.md
create mode 100644 src/packages/uploader/index.ts
create mode 100644 src/packages/uploader/upload.ts
create mode 100644 src/packages/uploader/uploader.scss
create mode 100644 src/packages/uploader/uploader.tsx
diff --git a/src/config.json b/src/config.json
index 01ba3d0..e36afda 100644
--- a/src/config.json
+++ b/src/config.json
@@ -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": "业务组件",
diff --git a/src/packages/avatar/avatar.scss b/src/packages/avatar/avatar.scss
index 26f4126..0199c64 100644
--- a/src/packages/avatar/avatar.scss
+++ b/src/packages/avatar/avatar.scss
@@ -13,6 +13,10 @@
left: 50%;
transform: translate(-50%, -50%);
}
+ .nut-icon__img {
+ width: 100%;
+ height: 100%;
+ }
.text {
display: inline-block;
width: 100%;
diff --git a/src/packages/avatar/demo.scss b/src/packages/avatar/demo.scss
new file mode 100644
index 0000000..4f3962c
--- /dev/null
+++ b/src/packages/avatar/demo.scss
@@ -0,0 +1,3 @@
+.demo-avatar {
+ color: #fff;
+}
diff --git a/src/packages/avatar/demo.tsx b/src/packages/avatar/demo.tsx
index 8157511..cb604b1 100644
--- a/src/packages/avatar/demo.tsx
+++ b/src/packages/avatar/demo.tsx
@@ -1,6 +1,7 @@
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 = () => {
修改形状
-
-
+
+
|
修改背景色
-
+
|
修改背景图片
-
+
|
可以修改头像的内容
diff --git a/src/packages/uploader/demo.scss b/src/packages/uploader/demo.scss
new file mode 100644
index 0000000..3246a56
--- /dev/null
+++ b/src/packages/uploader/demo.scss
@@ -0,0 +1,3 @@
+.demo.bg-w {
+ background: #fff;
+}
diff --git a/src/packages/uploader/demo.tsx b/src/packages/uploader/demo.tsx
new file mode 100644
index 0000000..32af7a6
--- /dev/null
+++ b/src/packages/uploader/demo.tsx
@@ -0,0 +1,94 @@
+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 => {
+ 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 => {
+ return new Promise((resolve) => {
+ const img = new Image()
+ img.onload = () => resolve(img)
+ img.src = dataURL
+ })
+ }
+ const canvastoFile = (
+ canvas: HTMLCanvasElement,
+ type: string,
+ quality: number
+ ): Promise => {
+ 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 (
+ <>
+
+ 基础用法
+
+ 自定义上传样式
+
+
+
+ 直接调起摄像头(移动端生效)
+
+ 上传状态
+
+ 限制上传数量5个
+
+ 限制上传大小(每个文件最大不超过 50kb)
+
+ 限制上传大小(在beforeupload钩子中处理)
+
+ 自定义数据 FormData 、 headers
+
+ 禁用状态
+
+
+ >
+ )
+}
+
+export default UploaderDemo
diff --git a/src/packages/uploader/doc.md b/src/packages/uploader/doc.md
new file mode 100644
index 0000000..59938e7
--- /dev/null
+++ b/src/packages/uploader/doc.md
@@ -0,0 +1,133 @@
+# Uploader 上传
+
+### 介绍
+
+用于将本地的图片或文件上传至服务器。
+
+### 安装
+
+``` javascript
+import { Uploader } from '@nutui/nutui';
+```
+
+## 代码示例
+
+### 基本用法
+
+``` tsx
+
+```
+
+
+### 自定义上传样式
+
+``` tsx
+
+
+
+```
+
+### 直接调起摄像头(移动端生效)
+
+``` tsx
+
+```
+### 限制上传数量5个
+
+``` tsx
+
+```
+### 限制上传大小(每个文件最大不超过 50kb,也可以在beforeupload中自行处理)
+
+``` tsx
+
+```
+
+``` javascript
+const formData = {
+ custom: 'test'
+};
+const onOversize = (files: File[]) => {
+ console.log('oversize 触发 文件大小不能超过 50kb', files);
+};
+const beforeUpload = (files: File[]) => {
+ //自定义处理
+ return files;
+}
+```
+
+### 自定义 FormData headers
+
+``` tsx
+
+```
+
+``` javascript
+const formData = {
+ custom: 'test'
+};
+const onOversize = (files: File[]) => {
+ console.log('oversize 触发 文件大小不能超过 50kb', files);
+};
+const beforeUpload = (files: File[]) => {
+ //自定义处理
+ return files;
+}
+```
+
+### 禁用状态
+
+``` tsx
+
+```
+
+### 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 |
+
diff --git a/src/packages/uploader/index.ts b/src/packages/uploader/index.ts
new file mode 100644
index 0000000..4885a19
--- /dev/null
+++ b/src/packages/uploader/index.ts
@@ -0,0 +1,2 @@
+import { Uploader } from './uploader'
+export default Uploader
diff --git a/src/packages/uploader/upload.ts b/src/packages/uploader/upload.ts
new file mode 100644
index 0000000..abba12c
--- /dev/null
+++ b/src/packages/uploader/upload.ts
@@ -0,0 +1,52 @@
+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) => {
+ 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')
+ }
+ }
+}
diff --git a/src/packages/uploader/uploader.scss b/src/packages/uploader/uploader.scss
new file mode 100644
index 0000000..9aaecd0
--- /dev/null
+++ b/src/packages/uploader/uploader.scss
@@ -0,0 +1,71 @@
+.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%;
+ }
+ }
+ }
+}
diff --git a/src/packages/uploader/uploader.tsx b/src/packages/uploader/uploader.tsx
new file mode 100644
index 0000000..0cf5294
--- /dev/null
+++ b/src/packages/uploader/uploader.tsx
@@ -0,0 +1,358 @@
+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; 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 }) => void
+ beforeUpload?: (file: File[]) => Promise
+ 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 & React.HTMLAttributes
+> = (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([])
+ 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,
+ 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) => {
+ 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()
+ 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) => {
+ if (disabled) {
+ return
+ }
+ const $el = event.target
+ let { files } = $el
+
+ if (props.beforeUpload) {
+ props.beforeUpload(new Array().slice.call(files)).then((f: Array) => {
+ const _files: File[] = filterFiles(new Array().slice.call(f))
+ readFile(_files)
+ })
+ } else {
+ const _files = filterFiles(new Array().slice.call(files))
+ readFile(_files)
+ }
+
+ props.change && props.change({ fileList, event })
+
+ if (props.clearInput) {
+ clearInput($el)
+ }
+ }
+
+ return (
+
+ )
+}
+
+Uploader.defaultProps = defaultProps
+Uploader.displayName = 'NutUploader'
--
GitLab
|