Browse Source

feat: uploader

richard1015 5 years ago
parent
commit
b88a9b5a79

+ 0 - 1
.prettierrc

@@ -3,7 +3,6 @@
   "semi": true,
   "bracketSpacing": true,
   "tabWidth": 2,
-  "printWidth": 300,
   "useTabs": false,
   "htmlWhitespaceSensitivity": "strict",
   "trailingComma": "none"

+ 46 - 2
src/packages/uploader/demo.vue

@@ -1,15 +1,59 @@
 <template>
   <div class="demo bg-w">
     <h2>基础用法</h2>
-    <nut-uploader></nut-uploader>
+    <nut-uploader :url="uploadUrl"></nut-uploader>
+    <h2>上传状态</h2>
+    <nut-uploader
+      :url="uploadUrl"
+      multiple
+      @on-delete="onDelete"
+    ></nut-uploader>
+    <h2>限制上传数量5个</h2>
+    <nut-uploader :url="uploadUrl" multiple max-count="5"></nut-uploader>
+    <h2>限制上传大小(每个文件最大不超过 50kb)</h2>
+    <nut-uploader
+      :url="uploadUrl"
+      multiple
+      :max-size="1024 * 50"
+      @oversize="onOversize"
+    ></nut-uploader>
+    <h2>自定义数据 FormData 、 headers </h2>
+    <nut-uploader
+      :url="uploadUrl"
+      :form-data="formData"
+      :headers="formData"
+      :with-Credentials="true"
+    ></nut-uploader>
+    <h2>禁用状态</h2>
+    <nut-uploader disabled></nut-uploader>
   </div>
 </template>
 
 <script lang="ts">
 import { createComponent } from '@/utils/create';
+import { FileItem } from './index.vue';
 const { createDemo } = createComponent('uploader');
 export default createDemo({
-  props: {}
+  setup() {
+    const uploadUrl =
+      'https://my-json-server.typicode.com/linrufeng/demo/posts';
+
+    const formData = {
+      custom: 'test'
+    };
+    const onOversize = (files: File[]) => {
+      console.log('oversize 触发 文件大小不能超过 50kb', files);
+    };
+    const onDelete = (file: FileItem, fileList: FileItem[]) => {
+      console.log('on-delete 事件触发', file, fileList);
+    };
+    return {
+      onOversize,
+      onDelete,
+      uploadUrl,
+      formData
+    };
+  }
 });
 </script>
 

+ 91 - 33
src/packages/uploader/doc.md

@@ -19,44 +19,102 @@ app.use(Uploader);
 
 ### 基本用法
 
+``` html
+<nut-uploader url="http://服务器地址"></nut-uploader>
+```
+### 限制上传数量5个
+
+``` html
+<nut-uploader url="http://服务器地址" multiple max-count="5"></nut-uploader>
+```
+### 限制上传大小(每个文件最大不超过 50kb)
+
+``` html
+<nut-uploader url="http://服务器地址" multiple :max-size="1024 * 50" @oversize="onOversize"></nut-uploader>
+```
+
+``` javascript
+setup() {
+    const formData = {
+      custom: 'test'
+    };
+    const onOversize = (files: File[]) => {
+      console.log('oversize 触发 文件大小不能超过 50kb', files);
+    };
+   
+    return {
+      onOversize,
+      formData
+    };
+}
+```
+
+### 自定义 FormData headers
+
+``` html
+<nut-uploader url="http://服务器地址" :form-data="formData" :headers="formData" :with-Credentials="true"></nut-uploader>
+```
+
+``` javascript
+setup() {
+    const formData = {
+      custom: 'test'
+    };
+    const onOversize = (files: File[]) => {
+      console.log('oversize 触发 文件大小不能超过 50kb', files);
+    };
+   
+    return {
+      onOversize,
+      formData
+    };
+}
+```
+
+### 禁用状态
+
+``` html
+<nut-uploader disabled></nut-uploader>
+```
+
 ## API
 
 ### Prop
 
-| 字段 | 说明 | 类型 | 默认值
-|----- | ----- | ----- | ----- 
-| name | `input` 标签 `name` 的名称,发到后台的文件参数名 | String | "file"
-| url | 上传服务器的接口地址 | String | -
-| default-file-list | 默认已经上传的文件列表 | object[] | -
-| file-list | 默认已经上传的文件列表 | object[] | -
-| custom-request | 通过覆盖默认的上传行为,可以自定义自己的上传实现 | Function | -
-| is-preview | 是否上传成功后展示预览图 | Boolean | true
-| is-deletable | 是否展示删除按钮 | Boolean | true
-| method | 上传请求的 http method | String | "post"
-| capture | 图片[选取模式](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/input#htmlattrdefcapture),可选值为 camera (直接调起摄像头) | String | "camera"
-| max-size | 可以设定最大上传文件的大小(字节) | Number丨String | 5242880 (5M)
-| max-count | 文件上传数量限制 | Number丨String | 1
-| clear-input | 是否需要清空`input`内容,设为`true`支持重复选择上传同一个文件 | Boolean | false
-| accept-type | 允许上传的文件类型,[详细说明](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 | {}
-| form-data | 附加上传的信息 formData | Object | {}
-| upload-icon | 上传区域[图标名称](#/zh-CN/icon)或图片链接 | String | photograph
-| xhr-state | 接口响应的成功状态(status)值 | Number | 200
-| with-credentials | 支持发送 cookie 凭证信息 | Boolean | fasle
-| multiple | 是否支持文件多选 | Boolean | fasle
-| disabled | 是否禁用文件上传 | Boolean | fasle
-| before-upload | 上传前的函数需要返回一个对象  | Function | {event:$event} $event为点击事件必传
-| before-delete | 除文件时的回调,返回值为 false 时不移除。支持返回一个 Promise 对象,Promise 对象 resolve(false) 或 reject 时不移除      | Function(file): boolean 丨Promise | -
-| on-change | 上传文件改变时的状态,详见     | Function(fileList) 丨 Promise | -
-| custom-request | 通过覆盖默认的上传行为,可以自定义自己的上传实现     | Function  | -
+| 字段              | 说明                                                                                                                                                                                   | 类型                              | 默认值      |
+|-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------|-------------|
+| name              | `input` 标签 `name` 的名称,发到后台的文件参数名                                                                                                                                       | String                            | "file"      |
+| url               | 上传服务器的接口地址                                                                                                                                                                   | String                            | -           |
+| default-file-list | 默认已经上传的文件列表                                                                                                                                                                 | object[]                          | -           |
+| file-list         | 默认已经上传的文件列表                                                                                                                                                                 | object[]                          | -           |
+| custom-request    | 通过覆盖默认的上传行为,可以自定义自己的上传实现                                                                                                                                       | Function                          | -           |
+| is-preview        | 是否上传成功后展示预览图                                                                                                                                                               | Boolean                           | true        |
+| is-deletable      | 是否展示删除按钮                                                                                                                                                                       | Boolean                           | true        |
+| method            | 上传请求的 http method                                                                                                                                                                 | String                            | "post"      |
+| capture           | 图片[选取模式](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/input#htmlattrdefcapture),可选值为 camera (直接调起摄像头)                                                   | String                            | "camera"    |
+| max-size          | 可以设定最大上传文件的大小(字节)                                                                                                                                                     | Number丨String                    | -           |
+| max-count         | 文件上传数量限制                                                                                                                                                                       | Number丨String                    | 1           |
+| clear-input       | 是否需要清空`input`内容,设为`true`支持重复选择上传同一个文件                                                                                                                          | Boolean                           | false       |
+| accept-type       | 允许上传的文件类型,[详细说明](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                            | {}          |
+| form-data         | 附加上传的信息 formData                                                                                                                                                                | Object                            | {}          |
+| upload-icon       | 上传区域[图标名称](#/zh-CN/icon)或图片链接                                                                                                                                             | String                            | photograph  |
+| xhr-state         | 接口响应的成功状态(status)值                                                                                                                                                         | Number                            | 200         |
+| with-credentials  | 支持发送 cookie 凭证信息                                                                                                                                                               | Boolean                           | fasle       |
+| multiple          | 是否支持文件多选                                                                                                                                                                       | Boolean                           | fasle       |
+| disabled          | 是否禁用文件上传                                                                                                                                                                       | Boolean                           | fasle       |
+| before-upload     | 上传前的函数需要返回一个对象                                                                                                                                                           | Function                          | input files |
+| before-delete     | 除文件时的回调,返回值为 false 时不移除。支持返回一个 Promise 对象,Promise 对象 resolve(false) 或 reject 时不移除                                                                     | Function(file): boolean 丨Promise | -           |
+| on-change         | 上传文件改变时的状态,详见                                                                                                                                                             | Function(fileList) 丨 Promise     | -           |
+| custom-request    | 通过覆盖默认的上传行为,可以自定义自己的上传实现                                                                                                                                       | Function                          | -           |
 
 ### Event
 
-| 名称 | 说明 | 回调参数 
-|----- | ----- | ----- 
-| start | 文件上传开始 | -
-| progress | 文件上传的进度 | 上传文件、已上传数据量、总数据量
-| oversize | 	文件大小超过限制时触发 | fileItem
-| success | 上传成功 | fileList
-| failure | 上传失败 | fileList
+| 名称     | 说明                   | 回调参数                         |
+|----------|------------------------|----------------------------------|
+| start    | 文件上传开始           | -                                |
+| progress | 文件上传的进度         | 上传文件、已上传数据量、总数据量 |
+| oversize | 文件大小超过限制时触发 | files                            |
+| success  | 上传成功               | fileList                         |
+| failure  | 上传失败               | fileList                         |
 

+ 60 - 16
src/packages/uploader/index.scss

@@ -1,20 +1,64 @@
 .nut-uploader {
-  display: flex;
-  align-items: center;
-  justify-content: center;
   position: relative;
-  overflow: hidden;
-  background: $uploader-babackground;
-  width: $uploader-width;
-  height: $uploader-height;
-  input {
-    position: absolute;
-    top: 0;
-    left: 0;
-    width: 100%;
-    height: 100%;
-    overflow: hidden;
-    cursor: pointer;
-    opacity: 0;
+  display: flex;
+  flex-wrap: wrap;
+
+  .upload {
+    position: relative;
+    background: $uploader-babackground;
+    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;
+        font-size: 12px;
+        color: $white;
+        height: 30px;
+        line-height: 30px;
+        text-align: c;
+        background: rgba(0, 0, 0, 0.54);
+      }
+    }
+    img {
+      height: 100%;
+      width: 100%;
+    }
   }
 }

+ 201 - 14
src/packages/uploader/index.vue

@@ -1,30 +1,69 @@
 <template>
   <view :class="classes">
-    <nut-icon color="#808080" :name="uploadIcon"></nut-icon>
-    <input type="file" :name="name" @change="onChange" />
+    <view class="preview" v-for="item in fileList" :key="item.uid">
+      <view class="preview-img">
+        <nut-icon
+          v-if="isDeletable"
+          color="rgba(0,0,0,0.6)"
+          @click="onDelete(item, index)"
+          class="close"
+          name="mask-close"
+        ></nut-icon>
+        <img v-if="item.type.includes('image') && item.url" :src="item.url" />
+        <view class="tips" v-if="item.status != 'success'">{{
+          item.status
+        }}</view>
+      </view>
+    </view>
+    <view class="upload" v-if="maxCount - fileList.length">
+      <nut-icon color="#808080" :name="uploadIcon"></nut-icon>
+      <input
+        type="file"
+        :capture="capture"
+        :accept="acceptType"
+        :multiple="multiple"
+        :name="name"
+        :disabled="disabled"
+        @change="onChange"
+      />
+    </view>
   </view>
 </template>
 
 <script lang="ts">
-import { computed } from 'vue';
+import { computed, reactive } from 'vue';
 import { createComponent } from '@/utils/create';
 import Icon from '@/packages/icon/index.vue';
+import { Uploader, UploadOptions } from './uploader';
 const { componentName, create } = createComponent('uploader');
-
+export type FileItemStatus =
+  | 'ready'
+  | 'uploading'
+  | 'success'
+  | 'error'
+  | 'removed';
+export class FileItem {
+  status: FileItemStatus = 'ready';
+  uid: string = new Date().getTime().toString();
+  name?: string;
+  url?: string;
+  type?: string;
+  formData: FormData = new FormData();
+}
 export default create({
   props: {
     name: { type: String, default: 'file' },
     url: { type: String, default: '' },
-    defaultFileList: { type: Array, default: [] },
-    fileList: { type: Array, default: [] },
+    defaultFileList: { type: Array, default: () => new Array<FileItem>() },
+    fileList: { type: Array, default: () => [] },
     isPreview: { type: Boolean, default: true },
     isDeletable: { type: Boolean, default: true },
     method: { type: String, default: 'post' },
     capture: { type: String, default: '' },
-    maxSize: { type: [Number, String], default: 1024 * 1024 * 5 },
+    maxSize: { type: [Number, String], default: Number.MAX_VALUE },
     maxCount: { type: [Number, String], default: 1 },
     clearInput: { type: Boolean, default: false },
-    acceptType: { type: String, default: '' },
+    acceptType: { type: String, default: '*' },
     headers: { type: Object, default: {} },
     formData: { type: Object, default: {} },
     uploadIcon: { type: String, default: 'photograph' },
@@ -32,17 +71,35 @@ export default create({
     withCredentials: { type: Boolean, default: false },
     multiple: { type: Boolean, default: false },
     disabled: { type: Boolean, default: false },
-    beforeUpload: { type: Function },
-    beforeDelete: { type: Function },
+    beforeUpload: {
+      type: Function,
+      default: (files: FileList) => {
+        return files;
+      }
+    },
+    beforeDelete: {
+      type: Function,
+      default: (file: FileItem, files: FileItem[]) => {
+        return true;
+      }
+    },
     onChange: { type: Function },
     customRequest: { type: Function }
   },
   components: {
     [Icon.name]: Icon
   },
-  emits: ['start', 'progress', 'oversize', 'success', 'failure', 'on-change'],
+  emits: [
+    'start',
+    'progress',
+    'oversize',
+    'success',
+    'failure',
+    'on-change',
+    'on-delete'
+  ],
   setup(props, { emit }) {
-    console.log(props);
+    const fileList = reactive(props.fileList) as Array<FileItem>;
     const classes = computed(() => {
       const prefixCls = componentName;
       return {
@@ -50,12 +107,142 @@ export default create({
       };
     });
 
-    const onChange = (event: Event) => {
-      emit('on-change', event);
+    const clearInput = (el: HTMLInputElement) => {
+      el.value = '';
+    };
+
+    const executeUpload = (fileItem: FileItem) => {
+      const uploadOption = new UploadOptions();
+      uploadOption.url = props.url;
+      for (const [key, value] of Object.entries(props.formData)) {
+        fileItem.formData.append(key, value);
+      }
+      uploadOption.formData = fileItem.formData;
+      uploadOption.method = props.method;
+      uploadOption.xhrState = props.xhrState as number;
+      uploadOption.headers = props.headers;
+      uploadOption.withCredentials = props.withCredentials;
+      uploadOption.onStart = (option: UploadOptions) => {
+        fileItem.status = 'ready';
+        emit('start', option);
+      };
+      uploadOption.onProgress = (
+        e: ProgressEvent<XMLHttpRequestEventTarget>,
+        option: UploadOptions
+      ) => {
+        fileItem.status = 'uploading';
+        emit('progress', { e, option });
+      };
+
+      uploadOption.onSuccess = (
+        responseText: XMLHttpRequest['responseText'],
+        option: UploadOptions
+      ) => {
+        fileItem.status = 'success';
+        emit('success', {
+          responseText,
+          option
+        });
+      };
+      uploadOption.onFailure = (
+        responseText: XMLHttpRequest['responseText'],
+        option: UploadOptions
+      ) => {
+        fileItem.status = 'error';
+        emit('failure', {
+          responseText,
+          option
+        });
+      };
+      new Uploader(uploadOption).upload();
+    };
+
+    const readFile = (files: File[]) => {
+      files.forEach((file: File) => {
+        const formData = new FormData();
+        formData.append(props.name, file);
+
+        const fileItem = new FileItem();
+        fileItem.name = file.name;
+        fileItem.status = 'uploading';
+        fileItem.type = file.type;
+        fileItem.formData = formData;
+        executeUpload(fileItem);
+
+        if (props.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);
+          };
+          reader.readAsDataURL(file);
+        } else {
+          fileList.push(fileItem);
+        }
+      });
+    };
+
+    const filterFiles = (files: File[]) => {
+      const maxCount = (props.maxCount as number) * 1;
+      const maxSize = (props.maxSize as number) * 1;
+      const oversizes = new Array<File>();
+      files = files.filter((file: File) => {
+        if (file.size > maxSize) {
+          oversizes.push(file);
+          return false;
+        } else {
+          return true;
+        }
+      });
+      if (oversizes.length) {
+        emit('oversize', oversizes);
+      }
+      if (files.length > maxCount) {
+        files.splice(maxCount - 1, files.length - maxCount);
+      }
+      return files;
+    };
+    const onDelete = (file: FileItem, index: number) => {
+      if (props.beforeDelete(file, fileList)) {
+        fileList.splice(index, 1);
+        emit('on-delete', {
+          file,
+          fileList
+        });
+      } else {
+        console.log('用户阻止了删除!');
+      }
+    };
+
+    const onChange = (event: InputEvent) => {
+      if (props.disabled) {
+        return;
+      }
+      const $el = event.target as HTMLInputElement;
+      let { files } = $el;
+
+      if (props.beforeUpload) {
+        files = props.beforeUpload(files);
+      }
+
+      const _files: File[] = filterFiles(new Array<File>().slice.call(files));
+
+      readFile(_files);
+
+      if (props.clearInput) {
+        clearInput($el);
+      }
+
+      emit('on-change', {
+        fileList,
+        event
+      });
     };
 
     return {
       onChange,
+      onDelete,
+      fileList,
       classes
     };
   }

+ 50 - 0
src/packages/uploader/uploader.ts

@@ -0,0 +1,50 @@
+export class UploadOptions {
+  url = '';
+  formData: FormData = new FormData();
+  method = 'post';
+  xhrState = 200;
+  headers = {};
+  withCredentials = false;
+  onStart?: Function;
+  onProgress?: Function;
+  onSuccess?: Function;
+  onFailure?: Function;
+}
+export class Uploader {
+  options: UploadOptions;
+  constructor(options: UploadOptions) {
+    this.options = options;
+  }
+  upload() {
+    const options = this.options;
+    const xhr = new XMLHttpRequest();
+    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');
+    }
+  }
+}