Browse Source

feat: taro uploader

richard1015 4 years ago
parent
commit
1f9dfd1d7d

+ 1 - 0
src/config.json

@@ -239,6 +239,7 @@
         },
         {
           "name": "Uploader",
+          "taro": true,
           "sort": 2,
           "cName": "上传组件",
           "type": "component",

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

@@ -121,6 +121,7 @@ setup() {
 | with-credentials  | 支持发送 cookie 凭证信息                                                                                                                                                               | Boolean                           | fasle            |
 | multiple          | 是否支持文件多选                                                                                                                                                                       | Boolean                           | fasle            |
 | disabled          | 是否禁用文件上传                                                                                                                                                                       | Boolean                           | fasle            |
+| timeout           | 超时时间,单位为毫秒                                                                                                   | Number丨String                    | 1000 * 30                 |
 | before-upload     | 上传前的函数需要返回一个`Promise`对象                                                                                                                                                  | Function                          | null             |
 | before-delete     | 除文件时的回调,返回值为 false 时不移除。支持返回一个 `Promise` 对象,`Promise` 对象 resolve(false) 或 reject 时不移除                                                                 | Function(file): boolean 丨Promise | -                |
 

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

@@ -0,0 +1,147 @@
+# Uploader 上传
+
+### 介绍
+
+用于将本地的图片或文件上传至服务器。
+
+### 安装
+
+``` javascript
+import { createApp } from 'vue';
+import { Uploader } from '@nutui/nutui';
+
+const app = createApp();
+app.use(Uploader);
+
+```
+
+## 代码示例
+
+### 基本用法
+
+``` html
+<nut-uploader url="http://服务器地址"></nut-uploader>
+```
+
+
+### 自定义上传样式
+
+``` html
+<nut-uploader url="http://服务器地址">
+  <nut-button type="primary" icon="uploader">上传文件</nut-button>
+</nut-uploader>
+```
+
+### 直接调起摄像头 camera
+    
+``` html
+<nut-uploader url="http://服务器地址" source-type="camera" ></nut-uploader>
+```
+### 限制上传数量5个
+
+``` html
+<nut-uploader url="http://服务器地址" multiple maximum="5"></nut-uploader>
+```
+### 限制上传大小(每个文件最大不超过 50kb,也可以在beforeupload中自行处理)
+
+``` html
+<nut-uploader url="http://服务器地址" multiple :maximize="1024 * 50" :before-upload="beforeUpload" @oversize="onOversize"></nut-uploader>
+```
+
+``` javascript
+setup() {
+    const formData = {
+      custom: 'test'
+    };
+    const onOversize = (files: File[]) => {
+      console.log('oversize 触发 文件大小不能超过 50kb', files);
+    };
+
+    const beforeUpload = (files: File[]) => {
+      //自定义处理
+      return files;
+    }
+   
+    return {
+      onOversize,
+      formData
+    };
+}
+```
+
+### 自定义 FormData headers
+
+``` html
+<nut-uploader url="http://服务器地址" :data="formData" :headers="formData"></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              | 发到后台的文件参数名                                                                                                   | String                            | "file"                    |
+| url               | 上传服务器的接口地址                                                                                                   | String                            | -                         |
+| v-model:file-list | 默认已经上传的文件列表                                                                                                 | FileItem[]                        | []                        |
+| is-preview        | 是否上传成功后展示预览图                                                                                               | Boolean                           | true                      |
+| is-deletable      | 是否展示删除按钮                                                                                                       | Boolean                           | true                      |
+| method            | 上传请求的 http method                                                                                                 | String                            | "post"                    |
+| source-type       | [选择图片的来源](https://developers.weixin.qq.com/miniprogram/dev/api/media/image/wx.chooseImage.html)                 | String                            | ['album','camera']        |
+| maximize          | 可以设定最大上传文件的大小(字节)                                                                                     | Number丨String                    | 9                         |
+| maximum           | 文件上传数量限制                                                                                                       | Number丨String                    | 1                         |
+| size-type         | 选择图片的来源,[详细说明](https://developers.weixin.qq.com/miniprogram/dev/api/media/image/wx.chooseImage.html)       | String                            | ['original','compressed'] |
+| headers           | 设置上传的请求头部                                                                                                     | Object                            | {}                        |
+| data              | 附加上传的信息 formData                                                                                                | Object                            | {}                        |
+| upload-icon       | 上传区域[图标名称](#/zh-CN/icon)或图片链接                                                                             | String                            | "photograph"              |
+| xhr-state         | 接口响应的成功状态(status)值                                                                                         | Number                            | 200                       |
+| disabled          | 是否禁用文件上传                                                                                                       | Boolean                           | fasle                     |
+| timeout           | 超时时间,单位为毫秒                                                                                                   | Number丨String                    | 1000 * 30                 |
+| before-delete     | 除文件时的回调,返回值为 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      | 文件路径                                                | ""                              |
+| formData | 上传所需的data                                          | {}                              |
+
+### Event
+
+| 名称     | 说明                   | 回调参数         |
+|----------|------------------------|------------------|
+| start    | 文件上传开始           | options          |
+| progress | 文件上传的进度         | event,options    |
+| oversize | 文件大小超过限制时触发 | files            |
+| success  | 上传成功               | response,options |
+| failure  | 上传失败               | response,options |
+| change   | 上传文件改变时的状态   | fileList,event   |
+| delete   | 文件删除之前的状态     | files,fileList   |
+

+ 4 - 4
src/packages/__VUE/uploader/index.scss

@@ -62,10 +62,10 @@
         text-align: c;
         background: rgba(0, 0, 0, 0.54);
       }
-    }
-    img {
-      height: 100%;
-      width: 100%;
+      &__c {
+        height: 100%;
+        width: 100%;
+      }
     }
   }
 }

+ 244 - 0
src/packages/__VUE/uploader/index.taro.vue

@@ -0,0 +1,244 @@
+<template>
+  <view :class="classes">
+    <view class="nut-uploader__slot" v-if="$slots.default">
+      <slot></slot>
+      <template v-if="maximum - fileList.length">
+        <nut-button class="nut-uploader__input" @click="chooseImage" />
+      </template>
+    </view>
+
+    <template v-else>
+      <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"
+            color="rgba(0,0,0,0.6)"
+            @click="onDelete(item, index)"
+            class="close"
+            name="mask-close"
+          ></nut-icon>
+          <image
+            class="nut-uploader__preview-img__c"
+            v-if="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 color="#808080" :name="uploadIcon"></nut-icon>
+        <nut-button class="nut-uploader__input" @click="chooseImage" />
+      </view>
+    </template>
+  </view>
+</template>
+
+<script lang="ts">
+import { computed, PropType, reactive } from 'vue';
+import { createComponent } from '@/packages/utils/create';
+import { Uploader, UploadOptions } from './uploader';
+const { componentName, create } = createComponent('uploader');
+import Taro from '@tarojs/taro';
+export type FileItemStatus =
+  | 'ready'
+  | 'uploading'
+  | 'success'
+  | 'error'
+  | 'removed';
+export class FileItem {
+  status: FileItemStatus = 'ready';
+  uid: string = new Date().getTime().toString();
+  url?: string;
+  path?: string;
+  type?: string;
+  formData: any = {};
+}
+export type SizeType = 'original' | 'compressed';
+export type SourceType = 'album' | 'camera' | 'user' | 'environment';
+export default create({
+  props: {
+    name: { type: String, default: 'file' },
+    url: { type: String, default: '' },
+    sizeType: {
+      type: Array as PropType<SizeType[]>,
+      default: () => ['original', 'compressed']
+    },
+    sourceType: {
+      type: Array as PropType<SourceType[]>,
+      default: () => ['album', 'camera']
+    },
+    timeout: { type: [Number, String], default: 1000 * 30 },
+    // 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: Boolean, default: false },
+    maximize: { type: [Number, String], default: Number.MAX_VALUE },
+    maximum: { type: [Number, String], default: 9 },
+    clearInput: { type: Boolean, default: false },
+    accept: { type: String, default: '*' },
+    headers: { type: Object, default: {} },
+    data: { type: Object, default: {} },
+    uploadIcon: { type: String, default: 'photograph' },
+    xhrState: { type: [Number, String], default: 200 },
+    disabled: { type: Boolean, default: false },
+    beforeDelete: {
+      type: Function,
+      default: (file: FileItem, files: FileItem[]) => {
+        return true;
+      }
+    },
+    onChange: { type: Function }
+  },
+  emits: [
+    'start',
+    'progress',
+    'oversize',
+    'success',
+    'failure',
+    'change',
+    'delete',
+    'update:fileList'
+  ],
+  setup(props, { emit }) {
+    const fileList = reactive(props.fileList) as Array<FileItem>;
+    const classes = computed(() => {
+      const prefixCls = componentName;
+      return {
+        [prefixCls]: true
+      };
+    });
+
+    const chooseImage = () => {
+      if (props.disabled) {
+        return;
+      }
+      Taro.chooseImage({
+        // 选择数量
+        count: (props.maximize as number) * 1 - props.fileList.length,
+        // 可以指定是原图还是压缩图,默认二者都有
+        sizeType: props.sizeType,
+        sourceType: props.sourceType,
+        success: onChange
+      });
+    };
+
+    const executeUpload = (fileItem: FileItem) => {
+      const uploadOption = new UploadOptions();
+      uploadOption.url = props.url;
+      for (const [key, value] of Object.entries(props.data)) {
+        fileItem.formData[key] = value;
+      }
+      uploadOption.formData = fileItem.formData;
+      uploadOption.method = props.method;
+      uploadOption.headers = props.headers;
+      uploadOption.onStart = (option: UploadOptions) => {
+        fileItem.status = 'ready';
+        emit('start', option);
+      };
+      uploadOption.onProgress = (e: any, option: UploadOptions) => {
+        fileItem.status = 'uploading';
+        emit('progress', { e, option });
+      };
+
+      uploadOption.onSuccess = (
+        data: Taro.uploadFile.SuccessCallbackResult,
+        option: UploadOptions
+      ) => {
+        fileItem.status = 'success';
+        emit('success', {
+          data,
+          option
+        });
+        emit('update:fileList', fileList);
+      };
+      uploadOption.onFailure = (
+        data: Taro.uploadFile.SuccessCallbackResult,
+        option: UploadOptions
+      ) => {
+        fileItem.status = 'error';
+        emit('failure', {
+          data,
+          option
+        });
+      };
+      new Uploader(uploadOption).uploadTaro(fileItem.path!, Taro);
+    };
+
+    const readFile = (files: Taro.chooseImage.ImageFile[]) => {
+      files.forEach((file: Taro.chooseImage.ImageFile) => {
+        const fileItem = reactive(new FileItem());
+        fileItem.path = file.path;
+        fileItem.status = 'uploading';
+        fileItem.type = file.type;
+        if (props.isPreview) {
+          fileItem.url = file.path;
+        }
+        fileList.push(fileItem);
+        executeUpload(fileItem);
+      });
+    };
+
+    const filterFiles = (files: Taro.chooseImage.ImageFile[]) => {
+      const maximum = (props.maximum as number) * 1;
+      const maximize = (props.maximize as number) * 1;
+      const oversizes = new Array<Taro.chooseImage.ImageFile>();
+      files = files.filter((file: Taro.chooseImage.ImageFile) => {
+        if (file.size > maximize) {
+          oversizes.push(file);
+          return false;
+        } else {
+          return true;
+        }
+      });
+      if (oversizes.length) {
+        emit('oversize', oversizes);
+      }
+      if (files.length > maximum) {
+        files.splice(maximum - 1, files.length - maximum);
+      }
+      return files;
+    };
+    const onDelete = (file: FileItem, index: number) => {
+      if (props.beforeDelete(file, fileList)) {
+        fileList.splice(index, 1);
+        emit('delete', {
+          file,
+          fileList
+        });
+      } else {
+        console.log('用户阻止了删除!');
+      }
+    };
+
+    const onChange = (res: Taro.chooseImage.SuccessCallbackResult) => {
+      // 返回选定照片的本地文件路径列表,tempFilePath可以作为img标签的src属性显示图片
+      const { tempFilePaths, tempFiles } = res;
+      const _files: Taro.chooseImage.ImageFile[] = filterFiles(tempFiles);
+      readFile(_files);
+
+      emit('change', {
+        fileList
+      });
+    };
+
+    return {
+      onDelete,
+      fileList,
+      classes,
+      chooseImage
+    };
+  }
+});
+</script>
+
+<style lang="scss">
+@import 'index.scss';
+</style>

+ 10 - 4
src/packages/__VUE/uploader/index.vue

@@ -30,7 +30,7 @@
     <template v-else>
       <view
         class="nut-uploader__preview"
-        v-for="item in fileList"
+        v-for="(item, index) in fileList"
         :key="item.uid"
       >
         <view class="nut-uploader__preview-img">
@@ -41,7 +41,11 @@
             class="close"
             name="mask-close"
           ></nut-icon>
-          <img v-if="item.type.includes('image') && item.url" :src="item.url" />
+          <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>
@@ -99,6 +103,7 @@ export default create({
     name: { type: String, default: 'file' },
     url: { type: String, default: '' },
     // defaultFileList: { type: Array, default: () => new Array<FileItem>() },
+    timeout: { type: [Number, String], default: 1000 * 30 },
     fileList: { type: Array, default: () => [] },
     isPreview: { type: Boolean, default: true },
     isDeletable: { type: Boolean, default: true },
@@ -158,6 +163,7 @@ export default create({
         fileItem.formData.append(key, value);
       }
       uploadOption.formData = fileItem.formData;
+      uploadOption.timeout = (props.timeout as number) * 1;
       uploadOption.method = props.method;
       uploadOption.xhrState = props.xhrState as number;
       uploadOption.headers = props.headers;
@@ -183,7 +189,7 @@ export default create({
           responseText,
           option
         });
-        emit('update:fileList', props.fileList);
+        emit('update:fileList', fileList);
       };
       uploadOption.onFailure = (
         responseText: XMLHttpRequest['responseText'],
@@ -203,7 +209,7 @@ export default create({
         const formData = new FormData();
         formData.append(props.name, file);
 
-        const fileItem = new FileItem();
+        const fileItem = reactive(new FileItem());
         fileItem.name = file.name;
         fileItem.status = 'uploading';
         fileItem.type = file.type;

+ 41 - 1
src/packages/__VUE/uploader/uploader.ts

@@ -1,8 +1,9 @@
 export class UploadOptions {
   url = '';
-  formData: FormData = new FormData();
+  formData?: FormData;
   method = 'post';
   xhrState = 200;
+  timeout = 30 * 1000;
   headers = {};
   withCredentials = false;
   onStart?: Function;
@@ -18,6 +19,7 @@ export class Uploader {
   upload() {
     const options = this.options;
     const xhr = new XMLHttpRequest();
+    xhr.timeout = options.timeout;
     if (xhr.upload) {
       xhr.upload.addEventListener(
         'progress',
@@ -47,4 +49,42 @@ export class Uploader {
       console.warn('浏览器不支持 XMLHttpRequest');
     }
   }
+  uploadTaro(filePath: string, Taro: any) {
+    const options = this.options;
+    const uploadTask = Taro.uploadFile({
+      url: options.url,
+      filePath,
+      header: {
+        'Content-Type': 'multipart/form-data',
+        ...options.headers
+      }, //
+      formData: options.formData,
+      name: 'files',
+      success(response: { errMsg: any; statusCode: number; data: string }) {
+        if (response.errMsg) {
+          options.onFailure?.(response, options);
+        } else if (options.xhrState === response.statusCode) {
+          options.onSuccess?.(response, options);
+        }
+      },
+      fail(e: any) {
+        options.onFailure?.(e, options);
+      }
+    });
+    options.onStart?.(options);
+    uploadTask.progress(
+      (res: {
+        progress: any;
+        totalBytesSent: any;
+        totalBytesExpectedToSend: any;
+      }) => {
+        options.onProgress?.(res, options);
+        // console.log('上传进度', res.progress);
+        // console.log('已经上传的数据长度', res.totalBytesSent);
+        // console.log('预期需要上传的数据总长度', res.totalBytesExpectedToSend);
+      }
+    );
+
+    // uploadTask.abort(); // 取消上传任务
+  }
 }

+ 1 - 0
src/sites/mobile-taro/vue/src/app.config.ts

@@ -1,5 +1,6 @@
 export default {
   pages: [
+    'pages/uploader/index',
     'pages/cell/index',
     'pages/rate/index',
     'pages/collapse/index',

+ 3 - 0
src/sites/mobile-taro/vue/src/pages/uploader/index.config.ts

@@ -0,0 +1,3 @@
+export default {
+  navigationBarTitleText: 'Uploader'
+};

+ 64 - 0
src/sites/mobile-taro/vue/src/pages/uploader/index.vue

@@ -0,0 +1,64 @@
+<template>
+  <div class="demo bg-w">
+    <h2>基础用法</h2>
+    <nut-uploader :url="uploadUrl" @start="start"></nut-uploader>
+    <h2>自定义上传样式</h2>
+    <nut-uploader :url="uploadUrl">
+      <nut-button type="primary" icon="uploader">上传文件</nut-button>
+    </nut-uploader>
+    <!-- 
+      album 从相册选图
+      camera 使用相机
+      user 使用前置摄像头(仅H5纯浏览器使用)
+      environment 使用后置摄像头(仅H5纯浏览器)
+      -->
+    <h2>直接调起摄像头 camera </h2>
+    <nut-uploader source-type="camera"></nut-uploader>
+    <h2>上传状态</h2>
+    <nut-uploader :url="uploadUrl" @delete="onDelete"></nut-uploader>
+    <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>
+    <h2>自定义数据 FormData 、 headers </h2>
+    <nut-uploader
+      :url="uploadUrl"
+      :data="formData"
+      :headers="formData"
+      :with-credentials="true"
+    ></nut-uploader>
+    <h2>禁用状态</h2>
+    <nut-uploader disabled></nut-uploader>
+  </div>
+</template>
+
+<script lang="ts">
+export default {
+  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: any, fileList: any[]) => {
+      console.log('delete 事件触发', file, fileList);
+    };
+
+    return {
+      onOversize,
+      onDelete,
+      uploadUrl,
+      formData
+    };
+  }
+};
+</script>
+
+<style lang="scss" scoped></style>