浏览代码

feat(uploader): auto-upload false #688

richard1015 4 年之前
父节点
当前提交
de0f7010c3

+ 18 - 30
src/packages/__VUE/uploader/demo.vue

@@ -13,41 +13,29 @@
     <h2>限制上传数量5个</h2>
     <nut-uploader :url="uploadUrl" multiple maximum="5"></nut-uploader>
     <h2>限制上传大小(每个文件最大不超过 50kb)</h2>
-    <nut-uploader
-      :url="uploadUrl"
-      multiple
-      :maximize="1024 * 50"
-      @oversize="onOversize"
-    ></nut-uploader>
+    <nut-uploader :url="uploadUrl" multiple :maximize="1024 * 50" @oversize="onOversize"></nut-uploader>
     <h2>限制上传大小(在beforeupload钩子中处理)</h2>
-    <nut-uploader
-      :url="uploadUrl"
-      multiple
-      :before-upload="beforeUpload"
-      :maximize="1024 * 50"
-      @oversize="onOversize"
-    >
+    <nut-uploader :url="uploadUrl" multiple :before-upload="beforeUpload" :maximize="1024 * 50" @oversize="onOversize">
     </nut-uploader>
     <h2>自定义数据 FormData 、 headers </h2>
-    <nut-uploader
-      :url="uploadUrl"
-      :data="formData"
-      :headers="formData"
-      :with-credentials="true"
-    ></nut-uploader>
+    <nut-uploader :url="uploadUrl" :data="formData" :headers="formData" :with-credentials="true"></nut-uploader>
+    <h2>手动上传 </h2>
+    <nut-uploader :url="uploadUrl" maximum="5" :auto-upload="false" ref="uploadRef"></nut-uploader>
+    <br />
+    <nut-button type="success" size="small" @click="submitUpload">执行上传</nut-button>
     <h2>禁用状态</h2>
     <nut-uploader disabled></nut-uploader>
   </div>
 </template>
 
 <script lang="ts">
+import { ref } from 'vue';
 import { createComponent } from '../../utils/create';
 import { FileItem } from './index.vue';
 const { createDemo } = createComponent('uploader');
 export default createDemo({
   setup() {
-    const uploadUrl =
-      'https://my-json-server.typicode.com/linrufeng/demo/posts';
+    const uploadUrl = 'https://my-json-server.typicode.com/linrufeng/demo/posts';
     const formData = {
       custom: 'test'
     };
@@ -65,14 +53,8 @@ export default createDemo({
         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 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);
@@ -95,12 +77,18 @@ export default createDemo({
       const f = await new File([blob], file[0].name);
       return [f];
     };
+    const uploadRef = ref<any>(null);
+    const submitUpload = () => {
+      uploadRef.value.submit();
+    };
     return {
       onOversize,
       beforeUpload,
       onDelete,
       uploadUrl,
-      formData
+      formData,
+      uploadRef,
+      submitUpload
     };
   }
 });

+ 31 - 0
src/packages/__VUE/uploader/doc.md

@@ -91,6 +91,28 @@ setup() {
 }
 ```
 
+### 手动上传
+    
+``` html 
+<nut-uploader url="http://服务器地址" maximum="5" :auto-upload="false" ref="uploadRef"></nut-uploader>
+<nut-button type="success" size="small" @click="submitUpload">执行上传</nut-button>
+```
+
+``` javascript
+import { ref } from 'vue';
+setup() {
+    const uploadRef = ref<any>(null);
+    const submitUpload = () => {
+      uploadRef.value.submit();
+    };
+    return {
+      uploadRef,
+      submitUpload
+    };
+}
+```
+
+
 ### 禁用状态
 
 ``` html
@@ -101,6 +123,7 @@ setup() {
 
 | 字段              | 说明                                                                                                                                                                                   | 类型                              | 默认值           |
 |-------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------|------------------|
+| auto-upload       | 是否在选取文件后立即进行上传,false 时需要手动执行 ref submit 方法进行上传                                                                                                             | Boolean                           | true             |
 | name              | `input` 标签 `name` 的名称,发到后台的文件参数名                                                                                                                                       | String                            | "file"           |
 | url               | 上传服务器的接口地址                                                                                                                                                                   | String                            | -                |
 | v-model:file-list | 默认已经上传的文件列表                                                                                                                                                                 | FileItem[]                        | []               |
@@ -149,3 +172,11 @@ setup() {
 | change   | 上传文件改变时的状态   | fileList,event       |
 | delete   | 文件删除之前的状态     | files,fileList       |
 
+### Methods
+
+通过 [ref](https://v3.cn.vuejs.org/api/special-attributes.html#key) 可以获取到 Uploader 实例并调用实例方法
+
+| 方法名           | 说明                                                       | 参数 | 返回值 |
+|------------------|------------------------------------------------------------|------|--------|
+| submit           | 手动上传模式,执行上传操作                                 | -    | -      |
+| clearUploadQueue | 清空已选择的文件队列(该方法一般配合在手动模式上传时使用) | -    | -      |

+ 31 - 0
src/packages/__VUE/uploader/doc.taro.md

@@ -87,6 +87,27 @@ setup() {
 }
 ```
 
+### 手动上传
+    
+``` html 
+<nut-uploader url="http://服务器地址" maximum="5" :auto-upload="false" ref="uploadRef"></nut-uploader>
+<nut-button type="success" size="small" @click="submitUpload">执行上传</nut-button>
+```
+
+``` javascript
+import { ref } from 'vue';
+setup() {
+    const uploadRef = ref<any>(null);
+    const submitUpload = () => {
+      uploadRef.value.submit();
+    };
+    return {
+      uploadRef,
+      submitUpload
+    };
+}
+```
+
 ### 禁用状态
 
 ``` html
@@ -97,6 +118,7 @@ setup() {
 
 | 字段              | 说明                                                                                                                   | 类型                              | 默认值                    |
 |-------------------|------------------------------------------------------------------------------------------------------------------------|-----------------------------------|---------------------------|
+| auto-upload       | 是否在选取文件后立即进行上传,false 时需要手动执行 ref submit 方法进行上传                                             | Boolean                           | true                      |
 | name              | 发到后台的文件参数名                                                                                                   | String                            | "file"                    |
 | url               | 上传服务器的接口地址                                                                                                   | String                            | -                         |
 | v-model:file-list | 默认已经上传的文件列表                                                                                                 | FileItem[]                        | []                        |
@@ -140,3 +162,12 @@ setup() {
 | change   | 上传文件改变时的状态   | fileList,event   |
 | delete   | 文件删除之前的状态     | files,fileList   |
 
+
+### Methods
+
+通过 [ref](https://v3.cn.vuejs.org/api/special-attributes.html#key) 可以获取到 Uploader 实例并调用实例方法
+
+| 方法名           | 说明                                                       | 参数 | 返回值 |
+|------------------|------------------------------------------------------------|------|--------|
+| submit           | 手动上传模式,执行上传操作                                 | -    | -      |
+| clearUploadQueue | 清空已选择的文件队列(该方法一般配合在手动模式上传时使用) | -    | -      |

+ 35 - 6
src/packages/__VUE/uploader/index.taro.vue

@@ -75,6 +75,7 @@ export default create({
     uploadIconSize: { type: [String, Number], default: '' },
     xhrState: { type: [Number, String], default: 200 },
     disabled: { type: Boolean, default: false },
+    autoUpload: { type: Boolean, default: true },
     beforeDelete: {
       type: Function,
       default: (file: FileItem, files: FileItem[]) => {
@@ -86,6 +87,8 @@ export default create({
   emits: ['start', 'progress', 'oversize', 'success', 'failure', 'change', 'delete', 'update:fileList'],
   setup(props, { emit }) {
     const fileList = reactive(props.fileList) as Array<FileItem>;
+    let uploadQueue: Promise<Uploader>[] = [];
+
     const classes = computed(() => {
       const prefixCls = componentName;
       return {
@@ -107,7 +110,7 @@ export default create({
       });
     };
 
-    const executeUpload = (fileItem: FileItem) => {
+    const executeUpload = (fileItem: FileItem, index: number) => {
       const uploadOption = new UploadOptions();
       uploadOption.name = props.name;
       uploadOption.url = props.url;
@@ -117,7 +120,9 @@ export default create({
       uploadOption.formData = fileItem.formData;
       uploadOption.method = props.method;
       uploadOption.headers = props.headers;
+      uploadOption.taroFilePath = fileItem.path;
       uploadOption.onStart = (option: UploadOptions) => {
+        clearUploadQueue(index);
         fileItem.status = 'ready';
         emit('start', option);
       };
@@ -141,21 +146,42 @@ export default create({
           option
         });
       };
+      let task = new Uploader(uploadOption);
+      if (props.autoUpload) {
+        task.uploadTaro(Taro.uploadFile);
+      } else {
+        uploadQueue.push(
+          new Promise((resolve, reject) => {
+            resolve(task);
+          })
+        );
+      }
+    };
 
-      new Uploader(uploadOption).uploadTaro(fileItem.path!, Taro.uploadFile);
+    const clearUploadQueue = (index = -1) => {
+      if (index > -1) {
+        uploadQueue.splice(index, 1);
+      } else {
+        uploadQueue = [];
+      }
+    };
+    const submit = () => {
+      Promise.all(uploadQueue).then((res) => {
+        res.forEach((i) => i.uploadTaro(Taro.uploadFile));
+      });
     };
 
     const readFile = (files: Taro.chooseImage.ImageFile[]) => {
-      files.forEach((file: Taro.chooseImage.ImageFile) => {
+      files.forEach((file: Taro.chooseImage.ImageFile, index: number) => {
         const fileItem = reactive(new FileItem());
         fileItem.path = file.path;
-        fileItem.status = 'uploading';
+        fileItem.status = 'ready';
         fileItem.type = file.type;
         if (props.isPreview) {
           fileItem.url = file.path;
         }
         fileList.push(fileItem);
-        executeUpload(fileItem);
+        executeUpload(fileItem, index);
       });
     };
 
@@ -180,6 +206,7 @@ export default create({
       return files;
     };
     const onDelete = (file: FileItem, index: number) => {
+      clearUploadQueue(index);
       if (props.beforeDelete(file, fileList)) {
         fileList.splice(index, 1);
         emit('delete', {
@@ -206,7 +233,9 @@ export default create({
       onDelete,
       fileList,
       classes,
-      chooseImage
+      chooseImage,
+      clearUploadQueue,
+      submit
     };
   }
 });

+ 43 - 52
src/packages/__VUE/uploader/index.vue

@@ -28,11 +28,7 @@
     </view>
 
     <template v-else>
-      <view
-        class="nut-uploader__preview"
-        v-for="(item, index) in fileList"
-        :key="item.uid"
-      >
+      <view class="nut-uploader__preview" v-for="(item, index) in fileList" :key="item.uid">
         <view class="nut-uploader__preview-img">
           <nut-icon
             v-if="isDeletable"
@@ -41,22 +37,12 @@
             class="close"
             name="circle-close"
           ></nut-icon>
-          <img
-            class="nut-uploader__preview-img__c"
-            v-if="item.type.includes('image') && item.url"
-            :src="item.url"
-          />
-          <view class="tips" v-if="item.status != 'success'">{{
-            item.status
-          }}</view>
+          <img class="nut-uploader__preview-img__c" 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="nut-uploader__upload" v-if="maximum - fileList.length">
-        <nut-icon
-          :size="uploadIconSize"
-          color="#808080"
-          :name="uploadIcon"
-        ></nut-icon>
+        <nut-icon :size="uploadIconSize" color="#808080" :name="uploadIcon"></nut-icon>
         <input
           class="nut-uploader__input"
           v-if="capture"
@@ -88,12 +74,7 @@ import { computed, reactive } from 'vue';
 import { createComponent } from '../../utils/create';
 import { Uploader, UploadOptions } from './uploader';
 const { componentName, create } = createComponent('uploader');
-export type FileItemStatus =
-  | 'ready'
-  | 'uploading'
-  | 'success'
-  | 'error'
-  | 'removed';
+export type FileItemStatus = 'ready' | 'uploading' | 'success' | 'error' | 'removed';
 export class FileItem {
   status: FileItemStatus = 'ready';
   uid: string = new Date().getTime().toString();
@@ -125,6 +106,7 @@ export default create({
     withCredentials: { type: Boolean, default: false },
     multiple: { type: Boolean, default: false },
     disabled: { type: Boolean, default: false },
+    autoUpload: { type: Boolean, default: true },
     beforeUpload: {
       type: Function,
       default: null
@@ -138,18 +120,11 @@ export default create({
     onChange: { type: Function }
     // customRequest: { type: Function }
   },
-  emits: [
-    'start',
-    'progress',
-    'oversize',
-    'success',
-    'failure',
-    'change',
-    'delete',
-    'update:fileList'
-  ],
+  emits: ['start', 'progress', 'oversize', 'success', 'failure', 'change', 'delete', 'update:fileList'],
   setup(props, { emit }) {
     const fileList = reactive(props.fileList) as Array<FileItem>;
+    let uploadQueue: Promise<Uploader>[] = [];
+
     const classes = computed(() => {
       const prefixCls = componentName;
       return {
@@ -161,7 +136,7 @@ export default create({
       el.value = '';
     };
 
-    const executeUpload = (fileItem: FileItem) => {
+    const executeUpload = (fileItem: FileItem, index: number) => {
       const uploadOption = new UploadOptions();
       uploadOption.url = props.url;
       for (const [key, value] of Object.entries(props.data)) {
@@ -175,20 +150,15 @@ export default create({
       uploadOption.withCredentials = props.withCredentials;
       uploadOption.onStart = (option: UploadOptions) => {
         fileItem.status = 'ready';
+        clearUploadQueue(index);
         emit('start', option);
       };
-      uploadOption.onProgress = (
-        e: ProgressEvent<XMLHttpRequestEventTarget>,
-        option: UploadOptions
-      ) => {
+      uploadOption.onProgress = (e: ProgressEvent<XMLHttpRequestEventTarget>, option: UploadOptions) => {
         fileItem.status = 'uploading';
         emit('progress', { e, option });
       };
 
-      uploadOption.onSuccess = (
-        responseText: XMLHttpRequest['responseText'],
-        option: UploadOptions
-      ) => {
+      uploadOption.onSuccess = (responseText: XMLHttpRequest['responseText'], option: UploadOptions) => {
         fileItem.status = 'success';
         emit('success', {
           responseText,
@@ -196,30 +166,48 @@ export default create({
         });
         emit('update:fileList', fileList);
       };
-      uploadOption.onFailure = (
-        responseText: XMLHttpRequest['responseText'],
-        option: UploadOptions
-      ) => {
+      uploadOption.onFailure = (responseText: XMLHttpRequest['responseText'], option: UploadOptions) => {
         fileItem.status = 'error';
         emit('failure', {
           responseText,
           option
         });
       };
-      new Uploader(uploadOption).upload();
+      let task = new Uploader(uploadOption);
+      if (props.autoUpload) {
+        task.upload();
+      } else {
+        uploadQueue.push(
+          new Promise((resolve, reject) => {
+            resolve(task);
+          })
+        );
+      }
+    };
+    const clearUploadQueue = (index = -1) => {
+      if (index > -1) {
+        uploadQueue.splice(index, 1);
+      } else {
+        uploadQueue = [];
+      }
+    };
+    const submit = () => {
+      Promise.all(uploadQueue).then((res) => {
+        res.forEach((i) => i.upload());
+      });
     };
 
     const readFile = (files: File[]) => {
-      files.forEach((file: File) => {
+      files.forEach((file: File, index: number) => {
         const formData = new FormData();
         formData.append(props.name, file);
 
         const fileItem = reactive(new FileItem());
         fileItem.name = file.name;
-        fileItem.status = 'uploading';
+        fileItem.status = 'ready';
         fileItem.type = file.type;
         fileItem.formData = formData;
-        executeUpload(fileItem);
+        executeUpload(fileItem, index);
 
         if (props.isPreview && file.type.includes('image')) {
           const reader = new FileReader();
@@ -255,6 +243,7 @@ export default create({
       return files;
     };
     const onDelete = (file: FileItem, index: number) => {
+      clearUploadQueue(index);
       if (props.beforeDelete(file, fileList)) {
         fileList.splice(index, 1);
         emit('delete', {
@@ -297,7 +286,9 @@ export default create({
       onChange,
       onDelete,
       fileList,
-      classes
+      classes,
+      clearUploadQueue,
+      submit
     };
   }
 });

+ 3 - 2
src/packages/__VUE/uploader/uploader.ts

@@ -8,6 +8,7 @@ export class UploadOptions {
   headers = {};
   withCredentials = false;
   onStart?: Function;
+  taroFilePath?: string;
   onProgress?: Function;
   onSuccess?: Function;
   onFailure?: Function;
@@ -50,11 +51,11 @@ export class Uploader {
       console.warn('浏览器不支持 XMLHttpRequest');
     }
   }
-  uploadTaro(filePath: string, uploadFile: Function) {
+  uploadTaro(uploadFile: Function) {
     const options = this.options;
     const uploadTask = uploadFile({
       url: options.url,
-      filePath,
+      filePath: options.taroFilePath,
       header: {
         'Content-Type': 'multipart/form-data',
         ...options.headers

+ 1 - 0
src/sites/doc/components/Header.vue

@@ -197,6 +197,7 @@ export default defineComponent({
         line-height: 63px;
         text-align: center;
         cursor: pointer;
+        flex-shrink: 0;
         a {
           display: inline-block;
           line-height: 64px;

+ 15 - 14
src/sites/mobile-taro/vue/src/dentry/pages/uploader/index.vue

@@ -19,28 +19,23 @@
     <h2>限制上传数量5个</h2>
     <nut-uploader :url="uploadUrl" maximum="5"></nut-uploader>
     <h2>限制上传大小(每个文件最大不超过 50kb)</h2>
-    <nut-uploader
-      :url="uploadUrl"
-      :maximize="1024 * 50"
-      @oversize="onOversize"
-    ></nut-uploader>
+    <nut-uploader :url="uploadUrl" :maximize="1024 * 50" @oversize="onOversize"></nut-uploader>
     <h2>自定义数据 FormData 、 headers </h2>
-    <nut-uploader
-      :url="uploadUrl"
-      :data="formData"
-      :headers="formData"
-      :with-credentials="true"
-    ></nut-uploader>
+    <nut-uploader :url="uploadUrl" :data="formData" :headers="formData" :with-credentials="true"></nut-uploader>
+    <h2>手动上传 </h2>
+    <nut-uploader :url="uploadUrl" maximum="5" :auto-upload="false" ref="uploadRef"></nut-uploader>
+    <br />
+    <nut-button type="success" size="small" @click="submitUpload">执行上传</nut-button>
     <h2>禁用状态</h2>
     <nut-uploader disabled></nut-uploader>
   </div>
 </template>
 
 <script lang="ts">
+import { ref } from 'vue';
 export default {
   setup() {
-    const uploadUrl =
-      'https://my-json-server.typicode.com/linrufeng/demo/posts';
+    const uploadUrl = 'https://my-json-server.typicode.com/linrufeng/demo/posts';
     const formData = {
       custom: 'test'
     };
@@ -50,12 +45,18 @@ export default {
     const onDelete = (file: any, fileList: any[]) => {
       console.log('delete 事件触发', file, fileList);
     };
+    const uploadRef = ref<any>(null);
+    const submitUpload = () => {
+      uploadRef.value.submit();
+    };
 
     return {
       onOversize,
       onDelete,
       uploadUrl,
-      formData
+      formData,
+      uploadRef,
+      submitUpload
     };
   }
 };