Browse Source

feat:新增uploader组件

Frans 6 years ago
parent
commit
136dbf37fa

+ 3 - 3
.babelrc.js

@@ -26,8 +26,8 @@ const plugins = [
 //     presets[0][1].modules = "commonjs";
 // }
 
-// if (process.env["NODE_ENV"] === "production" && process.env["PROD_TYPE"] === "disp") {
-//     presets[0][1].useBuiltIns = false;
-// }
+if (process.env["DOC_TYPE"] === "true") {
+    presets[0][1].modules = "commonjs";
+}
 
 module.exports = { presets, plugins };

+ 14 - 0
CHANGELOG.md

@@ -1,3 +1,17 @@
+## 2.1.0
+
+`2019-4-15`
+
+* :sparkles: feat: 新增上传组件`Uploader`
+* :sparkles: feat: 服务端渲染新增支持按需加载
+* :sparkles: chore: UMD包支持commonjs规范,服务端渲染不再需要引commonjs版的构建包了
+* :sparkles: chore: 大幅削减完整版构建包体积
+* :sparkles: chore: 单元测试功能优化
+* :sparkles: upd: `Scroller`组件优化
+* :sparkles: upd: `Picker`组件优化 
+* :bug: fix: 修复`Dialog`组件取消按钮触发事件错误的问题
+* :zap: doc: 文档内容修改完善
+
 ## 2.0.9
 
 `2019-3-29`

+ 3 - 3
package.json

@@ -1,6 +1,6 @@
 {
   "name": "@nutui/nutui",
-  "version": "2.0.9",
+  "version": "2.1.0",
   "description": "一套轻量级移动端Vue组件库",
   "typings": "dist/types/index.d.ts",
   "main": "dist/nutui.js",
@@ -14,11 +14,11 @@
   "scripts": {
     "dev:demo": "cross-env NODE_ENV=development webpack-dev-server -d --open --progress --config build/webpack.demo.dev.conf.js",
     "build:demo": "cross-env NODE_ENV=production webpack --hide-modules --progress --config build/webpack.demo.build.conf.js",
-    "dev:doc": "cross-env NODE_ENV=development webpack-dev-server -d --open -w --progress --config build/webpack.doc.dev.conf.js",
+    "dev:doc": "cross-env NODE_ENV=development DOC_TYPE=true webpack-dev-server -d --open -w --progress --config build/webpack.doc.dev.conf.js",
     "doc:clear": "node scripts/clearCache.js",
     "dev": "npm run dev:demo",
     "dev:carefree": "cross-env NODE_ENV=carefree carefree_env=dev webpack -w --colors --progress --config build/webpack.demo.dev.conf.js",
-    "build:doc": "cross-env NODE_ENV=production webpack --hide-modules --progress --config build/webpack.doc.build.conf.js",
+    "build:doc": "cross-env NODE_ENV=production DOC_TYPE=true webpack --hide-modules --progress --config build/webpack.doc.build.conf.js",
     "build:site": "npm run build:demo && npm run build:doc",
     "build:prod": "cross-env NODE_ENV=production webpack --hide-modules --progress --config build/webpack.prod.conf.js && node scripts/createIndexScss.js",
     "build:prodmin": "cross-env NODE_ENV=production webpack --hide-modules --progress --config build/webpack.prod.mini.conf.js",

+ 3 - 1
sites/doc/index.html

@@ -5,7 +5,9 @@
   <meta charset="utf-8" />
   <meta content="telephone=no" name="format-detection" />
   <link rel="shortcut icon" href="/favicon.ico">
-  <title>NutUI 2.0 - 移动端Vue组件库</title>
+  <title>NutUI - 移动端Vue组件库</title>
+  <meta name="description" content="移动端Vue组件库NutUI官网" />
+  <meta name="keywords" content="NutUI,NutUI 2.0,移动端Vue组件库,Vue components,JDC,京东用户体验设计部" />
   <script src="//misc.360buyimg.com/felibs/vue/2.5.16/vue.min.js"></script>
   <style>
     a[title=站长统计]{

+ 3 - 2
sites/doc/root.js

@@ -1,6 +1,6 @@
 import hide from './compents/hidden/hidden.vue';
 import vb from './asset/js/isVisibiliy.js';
-var myMixin ={
+const myMixin ={
     data(){
         return {
           content:'',
@@ -64,4 +64,5 @@ var myMixin ={
     });
   }
 }
-module.exports = myMixin;
+
+export default myMixin;

+ 10 - 0
src/config.json

@@ -432,6 +432,16 @@
       "star": 4,
       "showDemo": true,
       "author": "iris"
+    },
+    {
+      "version": "1.0.0",
+      "name": "Uploader",
+      "chnName": "上传",
+      "desc": "文件上传组件",
+      "type": "component",
+      "sort": "5",
+      "showDemo": true,
+      "author": "林如风"
     }
   ]
 }

+ 7 - 4
src/locales/lang/en-US.js

@@ -2,10 +2,8 @@ import Vue from 'vue'
 
 const lang = {
     lang: {
-        dialog: {
-            okBtnTxt: 'Ok',
-            cancelBtnTxt: 'Cancel'
-        },
+        okBtnTxt: 'Ok',
+        cancelBtnTxt: 'Cancel',
         calendar: {
             loadPrevMonth: 'Load the last month',
             noMoreMonth: 'No earlier month~',
@@ -30,6 +28,11 @@ const lang = {
             step: 'Step',
             stepDesc: 'Step description'
         },
+        uploader: {
+            xmlError: 'Sorry, your browser does not support this component!',
+            typeError: 'This type of file is not supported',
+            limitError: 'File size exceeded the limit'
+        }
         okBtnTxt: 'Ok',
         cancelText: 'Cancel'
     }

+ 7 - 6
src/locales/lang/zn-CH.js

@@ -3,10 +3,8 @@ import Vue from 'vue'
 
 const lang = {
     lang: {
-        dialog: {
-            okBtnTxt: '确 定',
-            cancelBtnTxt: '取 消'
-        },
+        okBtnTxt: '确 定',
+        cancelBtnTxt: '取 消',
         calendar: {
             loadPrevMonth: '加载上一个月',
             noMoreMonth: '没有更早月份~',
@@ -31,8 +29,11 @@ const lang = {
             step: '步骤',
             stepDesc: '步骤描述'
         },
-        okBtnTxt: '确定',
-        cancelText: '取消'
+        uploader:{
+            xmlError:'对不起,您的浏览器不支持本组件!',
+            typeError: '不支持上传该类型文件',
+            limitError: '文件大小超过限制'
+        }
     }
 }
 

+ 3 - 0
src/nutui.js

@@ -83,6 +83,8 @@ import CountDown from './packages/countdown/index.js';
 import './packages/countdown/countdown.scss';
 import InfiniteLoading from './packages/infiniteloading/index.js';
 import './packages/infiniteloading/infiniteloading.scss';
+import Uploader from "./packages/uploader/index.js";
+import "./packages/uploader/uploader.scss";
 
 const packages = {
   Cell,
@@ -126,6 +128,7 @@ const packages = {
   BackTop,
   CountDown,
   InfiniteLoading,
+  Uploader
 };
 
 const components = {};

+ 2 - 2
src/packages/dialog/dialog.vue

@@ -38,14 +38,14 @@
                 class="nut-dialog-btn nut-dialog-cancel"
                 v-if="!noCancelBtn"
                 @click="cancelBtnClick(cancelAutoClose)"
-              >{{cancelBtnTxt || nutTranslate('lang.dialog.cancelBtnTxt')}}</button>
+              >{{cancelBtnTxt || nutTranslate('lang.cancelBtnTxt')}}</button>
               <button
                 class="nut-dialog-btn nut-dialog-ok"
                 v-if="!noOkBtn"
                 :class="{'disabled':okBtnDisabled}"
                 :disabled="okBtnDisabled"
                 @click="okBtnClick"
-              >{{okBtnTxt || nutTranslate('lang.dialog.okBtnTxt')}}</button>
+              >{{okBtnTxt || nutTranslate('lang.okBtnTxt')}}</button>
             </div>
           </template>
         </div>

+ 83 - 0
src/packages/uploader/__test__/uploader.spec.js

@@ -0,0 +1,83 @@
+import { shallowMount, mount } from '@vue/test-utils'
+import Uploader from '../uploader.vue';
+import Vue from 'vue';
+
+
+describe('Uploader.vue', () => {
+    const wrapper = shallowMount(Uploader);   
+    const wrappers = mount(Uploader);
+    it('设置名称', () => {
+        wrapper.setProps({ name: 'lilinsen'});
+        return Vue.nextTick().then(function () {           
+            expect(wrapper.contains('[name="lilinsen"]')).toBe(true);;            
+        })
+    });
+    it('模拟设置 url isPreview clearInput maxSize acceptType 等,并点击上传',()=>{
+        wrappers.setProps({
+            url:'https://my-json-server.typicode.com/linrufeng/demo/posts',
+            isPreview:true,
+            clearInput:true,
+            maxSize:500,
+            acceptType:['application/zip']
+        })
+        return Vue.nextTick().then(()=>{
+            wrappers.trigger('click');
+            expect(true)
+        })
+    })
+    // it('隐藏', () => {
+    //     wrapper.setData({ msg: '测试文案', visible: false });
+
+    //     return Vue.nextTick().then(function () {
+    //         expect(wrapper.isVisible()).toBe(false);
+    //     });
+    // });
+
+    // it('文案展示', () => {
+    //     wrapper.setData({ msg: '测试文案', visible:true });
+    //     return Vue.nextTick().then(function () {
+    //         expect(wrapper.find('.nut-toast-text').text()).toBe('测试文案');
+    //     });
+    // });
+
+    // it('尺寸设置', () => {
+    //     wrapper.setData({ msg: '测试文案', visible: true, size: 'small' });
+
+    //     return Vue.nextTick().then(function () {
+    //         expect(wrapper.contains('.nut-toast-small')).toBe(true);
+    //     });
+    // });
+
+    // it('图标', () => {
+    //     wrapper.setData({ msg: '测试文案', visible: true, type:'success' });
+
+    //     return Vue.nextTick().then(function () {
+    //         expect(wrapper.contains('.nut-toast-has-icon')).toBe(true);
+    //     });
+    // });
+
+    // it('loading类型', () => {
+    //     wrapper.setData({ msg: '测试文案', visible: true, type: 'loading' });
+
+    //     return Vue.nextTick().then(function () {
+    //         expect(wrapper.contains('.nut-loading')).toBe(true);
+    //     });
+    // });
+
+    // it('loading图标不旋转', () => {
+    //     wrapper.setData({ msg: '测试文案', visible: true, type: 'loading', loadingRotate:false });
+
+    //     return Vue.nextTick().then(function () {
+    //         expect(wrapper.contains('.nut-toast-icon-rotate')).toBe(false);
+    //     });
+    // });
+
+    // it('设置ID', () => {
+    //     wrapper.setData({ id:'test', msg: '测试文案', visible: true, type: 'loading' });
+
+    //     return Vue.nextTick().then(function () {
+    //         expect(wrapper.attributes('id')).toBe('test');
+    //     });
+    // });
+
+});

+ 211 - 0
src/packages/uploader/demo.vue

@@ -0,0 +1,211 @@
+<template>
+  <div class="demo-list">
+    <h4>基本用法</h4>
+    <div>
+      <nut-cell>
+        <span slot="title">
+          <nut-uploader
+            :name="name"
+            :url="url"
+            :xhrState="stateNum"
+            :acceptType = "['image/jpeg', 'image/png', 'image/gif', 'image/bmp']"
+            @success="demo1Success"
+            @failure="demo1Fail"
+            @start="demo1UploadStart"
+            @showMsg="showMsg"
+          >{{demo1Name}}</nut-uploader>
+        </span>
+        <div slot="desc"></div>
+      </nut-cell>
+    </div>
+
+    <h4>高级用法</h4>
+    <p>结合Button组件和Toast组件使用</p>
+    <div>
+      <nut-cell>
+        <span slot="title">
+          <nut-uploader
+            :name="name"
+            :url="url"
+            :xhrState="stateNum"
+            @start="demo2UploadStart"
+            @success="demo2Success"
+            @failure="demo2Fail"
+            @showMsg="showMsg1"
+          >
+            <nut-button small>{{demo2Name}}</nut-button>
+          </nut-uploader>
+        </span>
+        <div slot="desc"></div>
+      </nut-cell>
+    </div>
+
+    <p>结合进度条Progress组件使用,展示上传进度</p>
+    <div>
+      <nut-cell>
+        <span slot="title">
+          <nut-uploader
+            :name="name"
+            :url="url"
+            :xhrState="stateNum"
+            @success="demoSuccess"
+            @fail="demoFail"
+            @progress="progress"
+            @showMsg="showMsg1"
+            :clearInput="true"
+          >
+            <nut-button small>上传</nut-button>
+          </nut-uploader>
+        </span>
+        <div slot="desc"></div>
+      </nut-cell>
+      <nut-cell>
+        <span slot="title">
+          <nut-progress
+            class="progress-style"
+            :percentage="progressNum"
+            :showText="true"
+            strokeWidth="12"
+          />
+        </span>
+        <div slot="desc"></div>
+      </nut-cell>
+    </div>
+
+    <p>预览上传图片</p>
+    <div>
+      <nut-cell>
+        <span slot="title">
+          <nut-uploader
+            :name="name"
+            :url="url"
+            :xhrState="stateNum"
+            :isPreview="true"
+            @success="demoSuccess"
+            @fail="demoFail"
+            @preview="preview"
+            @showMsg="showMsg1"
+          >
+            <nut-button small>上传</nut-button>
+          </nut-uploader>
+        </span>
+        <div slot="desc"></div>
+      </nut-cell>
+    </div>
+
+    <transition name="fade">
+      <div class="img-outbox">
+        <img class="img-box" v-if="previewImg" :src="previewImg" alt>
+      </div>
+    </transition>
+  </div>
+</template>
+
+<script>
+export default {
+  components: {},
+  data() {
+    return {
+      url: "https://my-json-server.typicode.com/linrufeng/demo/posts",
+      demo1Name: "点击选择文件",
+      demo2Name: "点击选择文件",
+      name: "test1",
+      stateNum: 201,
+      block: "block",
+      progressNum: 0,
+      previewImg: null,
+      previewImg2: null,
+      progressNum2: null,
+      upOver: false,
+      demo3Type: ["application/zip"]
+    };
+  },
+  methods: {
+    demo1UploadStart() {
+      this.demo1Name = "上传中...";
+      this.progressNum1 = 0;
+    },
+    demo1Success() {
+      this.demo1Name = "上传成功";
+    },
+    demo1Fail() {
+      this.demo1Name = "上传失败";
+    },
+    demo2UploadStart() {
+      this.demo2Name = "上传中...";
+    },
+    demo2Success() {
+      this.demo2Name = "选择文件";
+      this.$toast.success("上传成功");
+    },
+    demo2Fail() {
+      this.demo2Name = "选择文件";
+      this.$toast.fail("上传失败");
+    },
+    demoSuccess(file, res) {
+      this.$toast.success("上传成功");
+    },
+    demoFail() {
+      this.$toast.fail("上传失败");
+    },
+    progress(file, loaded, total) {
+      this.progressNum = parseInt((100 * loaded) / total);
+    },
+    preview(file) {
+      this.previewImg = file;
+    },
+    showMsg1(msg) {
+      this.$toast.text(msg);
+    },
+    showMsg(msg){
+      alert(msg);
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.img-outbox {
+  margin-top: 20px;
+  margin-left:20px;
+  width: 100px;
+  height: 100px;
+  border-radius: 6px;
+  position: relative;
+  border: 1px solid #f2f2f2;
+  line-height: 100px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  .img-box {
+    margin-top: 0;
+  }
+  .icon {
+    width: 20px;
+    height: 20px;
+    position: absolute;
+    z-index: 2;
+    top: 0;
+    right: 0;
+  }
+  .pr {
+    position: absolute;
+    z-index: 2;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    color: #fff;
+    font-size: 16px;
+    text-align: center;
+    line-height: 100px;
+    background: rgba(0, 0, 0, 0.2);
+  }
+}
+.img-box {
+  margin-top: 20px;
+  max-width: 100%;
+  max-height: 100%;
+  border-radius: 6px;
+}
+</style>

+ 140 - 0
src/packages/uploader/doc.md

@@ -0,0 +1,140 @@
+# Uploader 上传
+
+文件上传组件
+
+## 基本用法
+
+```html
+<nut-uploader
+    name="uploader-demo"
+    :url="url"
+    :isPreview="true"
+    :acceptType = "['image/jpeg', 'image/png', 'image/gif', 'image/bmp']"
+    @start="onStart"
+    @success="onSuccess"
+    @fail="onFail"
+    @progress="onProgress"
+    @preview="onPreview"
+    @showMsg="showMsgFn"
+    typeError="对不起,不支持上传该类型文件!"
+    limitError="对不起,文件大小超过限制!"
+>
+上传
+</nut-uploader>   
+```
+
+```javascript
+export default { 
+  data() {
+    return {
+      url:'https://my-json-server.typicode.com/linrufeng/demo/posts',    
+    };
+  },
+  methods:{
+      onStart(){
+        console.log('上传开始');
+      },
+      onSuccess(file,res){
+        alert('上传成功!');
+      },
+      onFail(file,res){
+        alert('上传失败!');
+      },
+      onProgress(file, loaded, total) {
+        console.log('上传进度:'+parseInt((100 * loaded) / total)+'%');
+      },
+      onPreview(file) {
+        this.previewImg = file;
+      },
+      showMsgFn(msg){
+        alert(msg);
+      },
+  }
+```
+
+## 高级用法
+
+与吐司 **Toast** 组件结合使用
+
+```html
+<nut-uploader
+    name="uploader-demo"
+    :url="url"
+    @success="onSuccess"
+    @fail="onFail"
+    @showMsg="showMsgFn"
+>
+上传
+</nut-uploader>   
+```
+
+```javascript
+export default { 
+  data() {
+    return {
+      url:'https://my-json-server.typicode.com/linrufeng/demo/posts',    
+    };
+  },
+  methods:{
+      onSuccess(file,res){
+        this.$toast.success('上传成功');
+      },
+      onFail(file,res){
+        this.$toast.fail('上传失败!');
+      },
+      showMsgFn(msg){
+        this.$toast.text(msg);
+      },
+  }
+
+```
+
+与按钮 **Button** 组件结合使用
+
+```html
+<nut-uploader
+  :name="name"
+  :url="url"    
+  >
+  <nut-button small>上传</nut-button>
+</nut-uploader>   
+```
+
+与进度条组件 **Progress** 结合使用
+
+```html
+<nut-uploader
+    :name="name"
+    :url="url"
+    @progress="progress"    
+    > 上传
+</nut-uploader>  
+<nut-progress :percentage="progressNum" :showText="false" strokeWidth="24"/>
+```
+
+## Prop
+
+| 字段 | 说明 | 类型 | 默认值
+|----- | ----- | ----- | ----- 
+| name | input name的名称 | String | ""
+| url | 上传服务器的接口地址 | String | -
+| isPreview | 是否需要预览 | Boolean | false
+| clearInput | 是否需要清空input内容,设为true支持重复选择上传同一个文件 | Boolean | false
+| maxSize | 可以设定最大上传文件的大小(字节) | Number | 5242880
+| acceptType | 可以上传文件的类型 | Array | ['image/jpeg', 'image/png', 'image/gif', 'image/bmp']
+| attach | 附加上传的信息 | Object | {}
+| xhrState | 接口响应的成功状态(status)值 | Number | 200
+| typeError | 文件类型错误提示文案 | String | "不支持上传该类型文件"
+| limitError | 文件大小超过限制提示文案 | String | "文件大小超过限制"
+| xmlError | 浏览器不支持本组件时的提示文案 | String | "对不起,您的浏览器不支持本组件!"
+
+## Event
+
+| 名称 | 说明 | 回调参数 
+|----- | ----- | ----- 
+| start | 文件上传开始 | -
+| progress | 文件上传的进度 | 上传文件、已上传数据量、总数据量
+| preview | isPreview为true时可通过此方法获文件的Base64编码,一般用于预览 | 文件的Base64编码
+| success | 上传成功 | 文件、responseText
+| failure | 上传失败 | 文件、responseText
+| showMsg | 组件抛出信息的处理函数 | 组件抛出的提示信息

+ 8 - 0
src/packages/uploader/index.js

@@ -0,0 +1,8 @@
+import Uploader from './uploader.vue';
+import './uploader.scss';
+
+Uploader.install = function(Vue) {
+  Vue.component(Uploader.name, Uploader);
+};
+
+export default Uploader

+ 13 - 0
src/packages/uploader/uploader.scss

@@ -0,0 +1,13 @@
+.nut-uploader{   
+    position: relative;
+    display: inline-block;
+    .uploader{
+        opacity: 0;
+        position: absolute;
+        top:0;
+        left:0;
+        width: 100%;
+        height: 100%;
+        font-size:14px;
+    }
+}

+ 151 - 0
src/packages/uploader/uploader.vue

@@ -0,0 +1,151 @@
+<template>
+  <div class="nut-uploader">
+    <slot></slot>
+    <input type="file" :name="name" @change="upload($event)" class="uploader" :multiple="multiple">
+  </div>
+</template>
+<script>
+import Uploader from "../../utils/uploader.js";
+import locale from "../../mixins/locale";
+export default {
+  name: "nut-uploader",
+  mixins: [locale],
+  props: {
+    name: {
+      type: String,
+      default: ""
+    },
+    url: {
+      type: String,
+      default: ""
+    },
+    multiple: {
+      type: Boolean,
+      default: false
+    },
+    isPreview: {
+      type: Boolean,
+      default: false
+    },
+    maxSize: {
+      type: Number,
+      default: 5242880
+    },
+    acceptType: {
+      type: Array,
+      default() {
+        return ["image/jpeg", "image/png", "image/gif", "image/bmp"];
+      }
+    },
+    attach: {
+      type: Object,
+      default() {
+        return {};
+      }
+    },
+    changeEvtCallback: {
+      type: Function
+    },
+    xhrState: {
+      type: Number,
+      default: 200
+    },
+    clearInput: {
+      type: Boolean,
+      default: false
+    },
+    xmlError: {
+      type: String,
+      default: ''
+    },
+    typeError: {
+      type: String,
+      default: ''
+    },
+    limitError: {
+      type: String,
+      default: ''
+    }
+  },
+  data() {
+    return {};
+  },
+  methods: {
+    createUploaderOpts() {
+      const _this = this;
+      return {
+        $el: {},
+        url: this.url, //图片上传地址
+        formData: null,
+        isPreview: this.isPreview, //是否开启本地预览
+        previewData: null,
+        maxSize: this.maxSize, //允许上传的文件最大字节
+        acceptType: this.acceptType, //允许上传的文件类型
+        xhrState: this.xhrState,
+        clearInput: this.clearInput,
+        xmlError: this.xmlError || this.nutTranslate('lang.uploader.xmlError'),
+        typeError: this.typeError || this.nutTranslate('lang.uploader.typeError'),
+        limitError: this.limitError || this.nutTranslate('lang.uploader.limitError'),
+        onStart() {
+          _this.$emit("start");
+        },
+        onProgress(file, loaded, total) {
+          _this.$emit("progress", file, loaded, total);
+        },
+        onPreview(previewFile) {
+          _this.$emit("preview", previewFile);
+        },
+        onSuccess(file, responseTxt) {
+          _this.$emit("success", file, responseTxt);
+        },
+        onFailure(file, responseTxt) {
+          _this.$emit("failure", file, responseTxt);
+        }
+      };
+    },
+    upload($event) {
+      const tar = $event.target;
+
+      if (!this.url) {
+        this.$emit("showMsg", "请先配置上传url");
+        this.$emit("afterChange", tar, $event);
+        return;
+      }
+      const formData = new FormData();
+      const opt = this.createUploaderOpts();
+      opt.$el = tar;
+      if (this.isPreview) {
+        opt.previewData = tar.files[0];
+      }
+      if (this.multiple) {
+        for (let i = 0; i < tar.files.length; i++) {
+          if (tar.files[i]) {
+            if (this.acceptType.indexOf(tar.files[i].type) == -1) {
+              this.$emit("showMsg", opt.typeError);
+              return;
+            }
+          }
+        }
+      } else {
+        if (tar.files[0]) {
+          if (this.acceptType.indexOf(tar.files[0].type) == -1) {
+            this.$emit("showMsg", opt.typeError);
+            return;
+          }
+        }
+      }
+      formData.append(tar.name, tar.files[0]);
+      for (let key of Object.keys(this.attach)) {
+        formData.append(key, this.attach[key]);
+      }
+      opt.formData = formData;
+      opt.showMsgFn = msg => {
+        this.$emit("showMsg", msg);
+      };
+      new Uploader(opt);
+
+      this.$emit("afterChange", tar, $event);
+    }
+  }
+};
+</script>

+ 104 - 0
src/utils/uploader.js

@@ -0,0 +1,104 @@
+
+class IdaUploader {
+   constructor (settings) {
+       this.options = {
+           url: '',
+           formData: null,
+           isPreview: true, //是否开启本地预览
+           previewData: null,
+           maxSize: 0, //允许上传的文件最大字节,0为不限制
+           acceptType: [], //允许上传的文件类型,如'image/jpeg'
+           showMsgFn: null,
+           onStart: null,
+           onProgress: null,
+           onPreview: null,
+           onSuccess: null,
+           onFailure: null,
+           xhrStatus:200, //默认上传成功是200
+           readyState:4,
+           xmlError:null, 
+           typeError:null, 
+           limitError:null 
+       };
+       Object.assign(this.options, settings);
+       this[this.options.isPreview ? 'preview' : 'uploader']()
+   }
+   triggerFunc(func) {
+        if (typeof(func)==='function') {
+            return func.bind(this);
+        } else {
+            console.warn(func + 'is not a function!');
+            return function() {};
+        }
+    }
+   showMsg (msg) {
+       if (typeof(this.options.showMsgFn)=='function') {
+           this.options.showMsgFn(msg);
+       } else {
+           console.log(msg);
+       }
+   }
+   check (file) {
+       if(Array.isArray(file)){           
+           for(let key in file){
+                if (this.options.maxSize && (file[key].size > this.options.maxSize)) {
+                    this.showMsg(this.limitError);
+                    return false;
+                }
+                if (this.options.acceptType.length && this.options.acceptType.indexOf(file[key].type) === -1) {           
+                    this.showMsg(this.typeError);
+                    return false;
+                }
+           }
+       }else{
+            if (this.options.maxSize && (file.size > this.options.maxSize)) {
+                this.showMsg(this.limitError);
+                return false;
+            }
+            if (this.options.acceptType.length && this.options.acceptType.indexOf(file.type) === -1) {           
+                this.showMsg(this.typeError);
+                return false;
+            }
+       }       
+       return true;
+   }
+   preview () {  
+       const file = this.options.previewData;    
+       if (!this.check(file)) return;
+       const reader = new FileReader();       
+       reader.onload = (e) => {
+        this.uploader();
+           this.triggerFunc.call(this.options, this.options.onPreview)(e.target.result);           
+       }
+       reader.readAsDataURL(file);       
+   }
+   uploader () {
+       const xhr = new XMLHttpRequest();
+       let options = this.options;
+       let formData = options.formData;       
+       if (xhr.upload) {    
+           xhr.upload.addEventListener('progress', (e) => {
+               this.triggerFunc.call(options, options.onProgress)(formData, e.loaded, e.total);
+           }, false);
+           xhr.onreadystatechange = (e) => {              
+               if (xhr.readyState === 4) {                  
+                   if (xhr.status === options.xhrState) {
+                        this.triggerFunc.call(options, options.onSuccess)(formData, xhr.responseText);
+                   } else {
+                        this.triggerFunc.call(options, options.onFailure)(formData, xhr.responseText);
+                   }
+               }
+           };
+           xhr.withCredentials = true;
+           xhr.open('POST', options.url, true);
+           this.triggerFunc.call(options, options.onStart)();          
+           xhr.send(formData);
+           if(options.clearInput){
+                options.$el.value = ''  ;
+           }           
+       } else {
+           this.showMsg(this.xmlError)
+       }
+   }
+}
+export default IdaUploader;

+ 1 - 0
types/nutui.d.ts

@@ -58,3 +58,4 @@ export declare class Toast extends UIComponent {}
 export declare class BackTop extends UIComponent {}
 export declare class Scroller extends UIComponent {}
 export declare class CountDown extends UIComponent {}
+export declare class Uploader extends UIComponent {}