Browse Source

添加全局上传组件

zhangyu 5 years ago
parent
commit
3c07e973b5
5 changed files with 431 additions and 1 deletions
  1. 2 0
      package.json
  2. 7 1
      src/App.vue
  3. BIN
      src/assets/house.jpg
  4. 420 0
      src/components/globaluploader/index.vue
  5. 2 0
      src/main.js

+ 2 - 0
package.json

@@ -63,11 +63,13 @@
         "sass-loader": "^7.1.0",
         "semver": "^5.3.0",
         "shelljs": "^0.7.6",
+        "spark-md5": "^3.0.0",
         "stompjs": "^2.3.3",
         "swiper": "^4.0.0",
         "uglifyjs-webpack-plugin": "^1.1.1",
         "url-loader": "^0.5.8",
         "vue-loader": "^13.3.0",
+        "vue-simple-uploader": "^0.7.2",
         "vue-style-loader": "^3.0.1",
         "vue-template-compiler": "^2.5.2",
         "webpack": "^3.6.0",

+ 7 - 1
src/App.vue

@@ -1,11 +1,17 @@
 <template>
     <div id='app'>
         <router-view/>
+        <global-uploader></global-uploader>
     </div>
 </template>
 
 <script>
+import globalUploader from '@/components/globaluploader'
+
 export default {
-    name: 'App'
+    name: 'App',
+    components: {
+        globalUploader
+    },
 }
 </script>

BIN
src/assets/house.jpg


+ 420 - 0
src/components/globaluploader/index.vue

@@ -0,0 +1,420 @@
+<template>
+  <div id="global-uploader">
+
+    <!-- 上传 -->
+    <uploader
+        ref="uploader"
+        :options="options"
+        :autoStart="false"
+        @file-added="onFileAdded"
+        @file-success="onFileSuccess"
+        @file-progress="onFileProgress"
+        @file-error="onFileError"
+        class="uploader-app">
+      <uploader-unsupport></uploader-unsupport>
+
+      <uploader-btn id="global-uploader-btn" :attrs="attrs" ref="uploadBtn">选择文件</uploader-btn>
+
+      <uploader-list v-show="panelShow">
+        <div class="file-panel" slot-scope="props" :class="{'collapse': collapse}">
+          <div class="file-title">
+            <h2>文件列表</h2>
+            <div class="operate">
+              <el-button @click="fileListShow" style="color:#999;" type="text" :title="collapse ? '展开':'折叠' ">
+                <i class="iconfont" :class="collapse ? 'icon-shenglvehao': 'icon-shenglvehao'"></i>
+              </el-button>
+              <el-button @click="close" style="color:#999;" type="text" title="关闭">
+                <i class="iconfont icon-guanbi"></i>
+              </el-button>
+            </div>
+          </div>
+
+          <ul class="file-list" v-show="collapse">
+            <el-scrollbar style="height:100%;">
+              <li v-for="file in props.fileList" :key="file.id">
+                <uploader-file :class="'file_' + file.id" ref="files" :file="file" :list="true"></uploader-file>
+              </li>
+              <div class="no-file" v-if="!props.fileList.length"><i class="iconfont icon-wushuju"></i> 暂无待上传文件</div>
+            </el-scrollbar>
+          </ul>
+        </div>
+      </uploader-list>
+
+    </uploader>
+
+  </div>
+</template>
+
+<script>
+/**
+ *   全局上传插件
+ *   调用方法:Bus.$emit('openUploader', {}) 打开文件选择框,参数为需要传递的额外参数
+ *   监听函数:Bus.$on('fileAdded', fn); 文件选择后的回调
+ *            Bus.$on('fileSuccess', fn); 文件上传成功的回调
+ */
+
+// import {ACCEPT_CONFIG} from './js/config';
+import Bus from '@/utils/bus.js'
+import SparkMD5 from 'spark-md5';
+
+// 这两个是我自己项目中用的,请忽略
+// import {Ticket} from '@/assets/js/utils';
+// import api from '@/api';
+
+export default {
+  data() {
+    return {
+      options: {
+        target: '',
+        chunkSize: 2*1024*1024,
+        fileParameterName: 'upfile',
+        maxChunkRetries: 3,
+        testChunks: false,   //是否开启服务器分片校验
+        // 服务器分片校验函数,秒传及断点续传基础
+        // checkChunkUploadedByResponse: function (chunk, message) {
+        //   let objMessage = JSON.parse(message);
+        //   if (objMessage.skipUpload) {
+        //     return true;
+        //   }
+
+        //   return (objMessage.uploaded || []).indexOf(chunk.offset + 1) >= 0
+        // },
+        // headers: {
+          // Authorization: Ticket.get() && "Bearer " + Ticket.get().access_token
+        // },
+        query() {}
+      },
+      attrs: {
+        // accept: ACCEPT_CONFIG.getAll()
+      },
+      panelShow: false,   //选择文件后,展示上传panel
+      collapse: false,
+    }
+  },   
+  mounted() {
+    Bus.$on('openUploader', query => {
+      this.params = query || {};
+
+      if (this.$refs.uploadBtn) {
+        document.getElementById('global-uploader-btn').click();
+        // $('#global-uploader-btn').click();
+      }
+    });
+  },
+  computed: {
+    //Uploader实例
+    uploader() {
+      return this.$refs.uploader.uploader;
+    }
+  },
+  methods: {
+    onFileAdded(file) {
+      this.panelShow = true;
+      this.computeMD5(file);
+
+      Bus.$emit('fileAdded');
+    },
+    onFileProgress(rootFile, file, chunk) {
+      console.log(`上传中 ${file.name},chunk:${chunk.startByte / 1024 / 1024} ~ ${chunk.endByte / 1024 / 1024}`)
+    },
+    onFileSuccess(rootFile, file, response, chunk) {
+      let res = JSON.parse(response);
+
+      // 服务器自定义的错误(即虽返回200,但是是错误的情况),这种错误是Uploader无法拦截的
+      if (!res.result) {
+        this.$message({ message: res.message, type: 'error' });
+        // 文件状态设为“失败”
+        this.statusSet(file.id, 'failed');
+        return
+      }
+
+      // 如果服务端返回需要合并
+      if (res.needMerge) {
+        // 文件状态设为“合并中”
+        this.statusSet(file.id, 'merging');
+
+        api.mergeSimpleUpload({
+          tempName: res.tempName,
+          fileName: file.name,
+          ...this.params,
+        }).then(res => {
+          // 文件合并成功
+          Bus.$emit('fileSuccess');
+
+          this.statusRemove(file.id);
+        }).catch(e => {});
+
+      // 不需要合并
+      } else {
+        Bus.$emit('fileSuccess');
+        console.log('上传成功');
+      }
+    },
+    onFileError(rootFile, file, response, chunk) {
+      this.$message({
+        message: response,
+        type: 'error'
+      })
+    },
+
+    /**
+     * 计算md5,实现断点续传及秒传
+     * @param file
+     */
+    computeMD5(file) {
+      let fileReader = new FileReader();
+      let time = new Date().getTime();
+      let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
+      let currentChunk = 0;
+      const chunkSize = 2 * 1024 * 1024;
+      let chunks = Math.ceil(file.size / chunkSize);
+      let spark = new SparkMD5.ArrayBuffer();
+
+      // 文件状态设为"计算MD5"
+      this.statusSet(file.id, 'md5');
+      file.pause();
+
+      loadNext();
+
+      fileReader.onload = (e => {
+        spark.append(e.target.result);
+
+        if (currentChunk < chunks) {
+          currentChunk++;
+          loadNext();
+
+          // 实时展示MD5的计算进度
+          this.$nextTick(() => {
+            document.getElementsByClassName(`myStatus_${file.id}`)[0].innerText = '校验MD5 '+ ((currentChunk/chunks)*100).toFixed(0)+'%'
+            // $(`.myStatus_${file.id}`).text('校验MD5 '+ ((currentChunk/chunks)*100).toFixed(0)+'%')
+          })
+        } else {
+          let md5 = spark.end();
+          this.computeMD5Success(md5, file);
+          console.log(`MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${file.size} 用时:${new Date().getTime() - time} ms`);
+        }
+      });
+
+      fileReader.onerror = function () {
+        this.error(`文件${file.name}读取出错,请检查该文件`)
+        file.cancel();
+      };
+
+      function loadNext() {
+        let start = currentChunk * chunkSize;
+        let end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
+
+        fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end));
+      }
+    },
+
+    computeMD5Success(md5, file) {
+      // 将自定义参数直接加载uploader实例的opts上
+      Object.assign(this.uploader.opts, {
+        query: {
+            ...this.params,
+        }
+      })
+
+      file.uniqueIdentifier = md5;
+      file.resume();
+      this.statusRemove(file.id);
+    },
+
+    fileListShow() {
+      this.collapse = this.collapse? false: true;
+
+      // let $list = $('#global-uploader .file-list');
+      // let $list = document.querySelector('#global-uploader .file-list')
+
+      // if ($list.style.display == '' && $list.style.display == 'none') {
+      //   $list.style.display == 'block';//显示
+      //   this.collapse = false;
+      //   // $list.slideUp();//隐藏
+      //   // this.collapse = true;
+      // } else {
+      //   $list.style.display == 'none';//隐藏
+      //   this.collapse = true;
+      //   // $list.slideDown();//显示
+      //   // this.collapse = false;
+      // }
+    },
+    close() {
+      this.uploader.cancel();
+
+      this.panelShow = false;
+    },
+
+    /**
+     * 新增的自定义的状态: 'md5'、'transcoding'、'failed'
+     * @param id
+     * @param status
+     */
+    statusSet(id, status) {
+      let statusMap = {
+        md5: {
+          text: '校验MD5',
+          bgc: '#fff'
+        },
+        merging: {
+          text: '合并中',
+          bgc: '#e2eeff'
+        },
+        transcoding: {
+          text: '转码中',
+          bgc: '#e2eeff'
+        },
+        failed: {
+          text: '上传失败',
+          bgc: '#e2eeff'
+        }
+      }
+
+      this.$nextTick(() => {
+        let node = document.createElement("P");
+        let att = document.createAttribute("class");
+        att.value = `status_style myStatus_${id}`;
+        let text = document.createTextNode(`${statusMap[status].text}`);
+	      node.appendChild(text);
+        node.setAttributeNode(att);
+        node.style.backgroundColor = `${statusMap[status].bgc}`
+        document.querySelector(`.file_${id} .uploader-file-status`).appendChild(node);
+
+        // $(`<p class="myStatus_${id}"></p>`).appendTo(`.file_${id} .uploader-file-status`).css({
+        //   'position': 'absolute',
+        //   'top': '0',
+        //   'left': '0',
+        //   'right': '0',
+        //   'bottom': '0',
+        //   'zIndex': '1',
+        //   'backgroundColor': statusMap[status].bgc
+        // }).text(statusMap[status].text);
+      })
+    },
+    statusRemove(id) {
+      this.$nextTick(() => {
+        let dom = document.getElementsByClassName(`myStatus_${id}`)[0];
+        dom.parentNode.removeChild(dom);
+      })
+    },
+
+    error(msg) {
+      this.$notify({
+        title: '错误',
+        message: msg,
+        type: 'error',
+        duration: 2000
+      })
+    }
+  },
+  watch: {},
+  destroyed() {
+    Bus.$off('openUploader');
+  },
+  components: {}
+}
+</script>
+
+<style scoped lang="scss">
+#global-uploader {
+  position: fixed;
+  z-index: 20;
+  top: auto;
+  right: 15px;
+  bottom: 0;
+
+  .uploader-app {
+    width: 633px;
+  }
+
+  .file-panel {
+    background-color: #fff;
+    border: 1px solid #e2e2e2;
+    border-radius: 7px 7px 0 0;
+    box-shadow: 0 0 10px rgba(0, 0, 0, .2);
+
+    .file-title {
+      display: flex;
+      height: 40px;
+      line-height: 40px;
+      padding: 0 15px;
+      border-bottom: 1px solid #ddd;
+      h2{
+        font-size: 14px;
+        font-style: normal;
+        cursor: text;
+        font-weight: 200;
+        color: #666;
+      }
+
+      .operate {
+        flex: 1;
+        text-align: right;
+      }
+    }
+
+    .file-list {
+      position: relative;
+      height: 350px;
+      overflow-x: hidden;
+      overflow-y: auto;
+      background-color: #fff;
+      /deep/ .el-scrollbar__wrap{
+        overflow-x: hidden;
+      }
+
+      > li {
+        background-color: #fff;
+      }
+      /deep/ .status_style{
+        position: absolute;
+        top: 0;
+        left: 0;
+        right: 0;
+        bottom: 0;
+        z-index: 1;
+      }
+    }
+
+    // &.collapse {
+    //   .file-title {
+    //     background-color: #E7ECF2;
+    //   }
+    // }
+  }
+
+  .no-file {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    font-size: 16px;
+    text-align: center;
+  }
+
+  /deep/.uploader-file-icon {
+    &:before {
+      content: '' !important;
+    }
+
+    &[icon=image] {
+      // background: url(./images/image-icon.png);
+    }
+    &[icon=video] {
+      // background: url(./images/video-icon.png);
+    }
+    &[icon=document] {
+      // background: url(./images/text-icon.png);
+    }
+  }
+
+  /deep/ .uploader-file-actions > span {
+    margin-right: 6px;
+  }
+}
+/* 隐藏上传按钮 */
+#global-uploader-btn {
+  position: absolute;
+  clip: rect(0, 0, 0, 0);
+}
+</style>

+ 2 - 0
src/main.js

@@ -7,8 +7,10 @@ import store from './store'
 import 'element-ui/lib/theme-chalk/base.css';
 import CollapseTransition from 'element-ui/lib/transitions/collapse-transition';
 import ElementUI from 'element-ui'
+import uploader from 'vue-simple-uploader'
 import 'element-ui/lib/theme-chalk/index.css'
 import './framework/style/awesome.less'
+Vue.use(uploader)
 Vue.use(ElementUI, { size: "small", zIndex: 1000 })
 
 Vue.component(CollapseTransition.name, CollapseTransition)