Browse Source

PublishFile.vue 中实现 vue-simple-uploader 大文件分片上传

reghao 1 năm trước cách đây
mục cha
commit
57eef280f3

+ 2 - 2
src/components/upload/EditVideo.vue

@@ -217,12 +217,12 @@ export default {
   methods: {
     // ****************************************************************************************************************
     onFileAdded(file) {
-      if (file.file.size > 1024 * 1024 * 1024 * 5) {
+      if (file.file.size > 1024 * 1024 * 1024 * 10) {
         file.cancel()
         this.$notify(
           {
             title: '提示',
-            message: '视频文件应小于 5GiB',
+            message: '视频文件应小于 10GB',
             type: 'warning',
             duration: 3000
           }

+ 122 - 112
src/components/upload/PublishFile.vue

@@ -1,42 +1,29 @@
 <template>
   <el-row class="movie-list">
-    <el-col :md="12" style="padding-right: 5px; padding-left: 5px; padding-bottom: 5px">
+    <el-col :md="16" style="padding-right: 5px; padding-left: 5px; padding-bottom: 5px">
       <el-row style="padding-right: 5px; padding-left: 5px; padding-bottom: 5px">
         <el-card class="box-card">
           <div slot="header" class="clearfix">
             <span>上传文件</span>
           </div>
           <div class="text item">
-            <el-tooltip class="item" effect="dark" content="点击上传文件" placement="top-end">
-              <el-upload
-                class=""
-                action=""
-                :show-file-list="false"
-                :http-request="fnUploadRequest"
-                :on-success="handleAvatarSuccess"
-                :before-upload="beforeAvatarUpload"
-              >
-                <img v-if="imageUrl" :src="imageUrl" class="avatar" alt="">
-                <i v-else class="el-icon-plus avatar-uploader-icon" />
-              </el-upload>
-            </el-tooltip>
-          </div>
-        </el-card>
-      </el-row>
-    </el-col>
-    <el-col :md="12" style="padding-right: 5px; padding-left: 5px; padding-bottom: 5px">
-      <el-row style="padding-right: 5px; padding-left: 5px; padding-bottom: 5px">
-        <el-card class="box-card">
-          <div slot="header" class="clearfix">
-            <span>OSS 地址</span>
-          </div>
-          <div class="text item">
-            <span>{{ imageUrl }}</span>
-            <!--            <el-form ref="form" :model="imageUrl" label-width="80px">
-              <el-form-item label="文件地址">
-                <el-input v-model="imageUrl" style="padding-right: 1px" readonly />
-              </el-form-item>
-            </el-form>-->
+            <uploader
+              v-if="options !== null"
+              class="uploader-example"
+              :options="options"
+              :auto-start="true"
+              @file-added="onFileAdded"
+              @file-success="onFileSuccess"
+              @file-progress="onFileProgress"
+              @file-error="onFileError"
+            >
+              <uploader-unsupport />
+              <uploader-drop>
+                <p>拖动文件到此处或</p>
+                <uploader-btn :attrs="attrs">选择文件</uploader-btn>
+              </uploader-drop>
+              <uploader-list />
+            </uploader>
           </div>
         </el-card>
       </el-row>
@@ -45,79 +32,126 @@
 </template>
 
 <script>
-import OSS from 'ali-oss'
-import { getSignedUrl, getStsToken } from '@/api/file'
+import { getVideoChannelInfo } from '@/api/file'
+import { hashFile } from '@/utils/functions'
 
 export default {
   name: 'PublishFile',
   data() {
     return {
-      imageUrl: '',
-      ossUrl: ''
+      // ****************************************************************************************************************
+      options: null,
+      attrs: {
+        accept: '*'
+      }
     }
   },
   created() {
+    getVideoChannelInfo().then(res => {
+      if (res.code === 0) {
+        const resData = res.data
+        this.options = {
+          target: resData.ossUrl,
+          // 分块大小 10MB
+          chunkSize: 1024 * 1024 * 10,
+          // 失败自动重传次数
+          maxChunkRetries: 3,
+          fileParameterName: 'file',
+          testChunks: true,
+          // 服务器分片校验函数, 秒传及断点续传基础
+          checkChunkUploadedByResponse: function(chunk, message) {
+            const objMessage = JSON.parse(message)
+            if (objMessage.skipUpload) {
+              return true
+            }
+
+            return (objMessage.uploaded || []).indexOf(chunk.offset + 1) >= 0
+          },
+          query: (file, chunk) => {
+            return {
+              channelId: resData.channelId,
+              multiparts: ''
+            }
+          },
+          headers: {
+            Authorization: 'Bearer ' + resData.token
+          },
+          withCredentials: false
+        }
+      } else {
+        this.$notify({
+          title: '提示',
+          message: '获取 OSS 服务器地址失败, 暂时无法上传文件',
+          type: 'error',
+          duration: 3000
+        })
+      }
+    }).catch(error => {
+      this.$notify({
+        title: '提示',
+        message: error.message,
+        type: 'warning',
+        duration: 3000
+      })
+    })
+  },
+  mounted() {
   },
   methods: {
-    // 图片上传成功回调
-    handleAvatarSuccess(res) {
-      if (res) {
-        const objectName = res.url.replace(this.ossUrl, '')
-        const payload = {}
-        payload.objectName = objectName
-        getSignedUrl(payload).then(resp => {
-          if (resp.code === 0) {
-            this.imageUrl = resp.data
-            // this.imageUrl = res.url
-          }
+    // ****************************************************************************************************************
+    onFileAdded(file) {
+      if (file.file.size > 1024 * 1024 * 1024 * 20) {
+        file.cancel()
+        this.$notify({
+          title: '提示',
+          message: '文件应小于 20GB',
+          type: 'warning',
+          duration: 3000
         })
+        return
       }
-    },
-    beforeAvatarUpload(file) {
-      const isJPG = file.type === 'image/jpeg'
-      const isLt2M = file.size / 1024 / 1024 < 2
 
-      if (!isJPG) {
-        this.$message.error('上传头像图片只能是 JPG 格式!')
-      }
-      if (!isLt2M) {
-        this.$message.error('上传头像图片大小不能超过 2MB!')
-      }
-      return isJPG && isLt2M
+      file.pause()
+      hashFile(file.file).then(result => {
+        console.log(result.sha256sum)
+        this.startUpload(result.sha256sum, file)
+      })
     },
-    async fnUploadRequest(options) {
-      try {
-        const file = options.file // 拿到 file
-        const index = file.name.lastIndexOf('.')
-        const suffix = file.name.substr(index)
-        getStsToken().then(resp => {
-          if (resp.code === 0) {
-            const credentials = resp.data
-            this.ossUrl = credentials.ossUrl
-            const client = new OSS({
-              region: credentials.region,
-              bucket: credentials.bucket,
-              accessKeyId: credentials.accessKeyId,
-              accessKeySecret: credentials.accessKeySecret,
-              stsToken: credentials.securityToken
-            })
-
-            const objectId = credentials.objectId
-            const objectName = 'image/i/' + objectId + suffix
-            client.put(objectName, file).then(resp1 => {
-              if (resp1.res.statusCode === 200) {
-                console.log(resp1)
-                options.onSuccess(resp1)
-              } else {
-                console.log(resp1)
-                options.onError('上传失败')
-              }
-            })
-          }
+    startUpload(sha256sum, file) {
+      file.uniqueIdentifier = sha256sum
+      file.resume()
+      // this.statusRemove(file.id)
+    },
+    onFileProgress(rootFile, file, chunk) {
+    },
+    onFileSuccess(rootFile, file, response, chunk) {
+      const resp = JSON.parse(response)
+      if (resp.code === 0) {
+        console.log(resp.data)
+        this.$notify({
+          title: '提示',
+          message: '文件已上传',
+          type: 'warning',
+          duration: 3000
+        })
+      } else {
+        this.$notify({
+          title: '提示',
+          message: resp.msg,
+          type: 'warning',
+          duration: 3000
         })
-      } catch (e) {
-        options.onError('上传失败')
       }
+    },
+    onFileError(rootFile, file, response, chunk) {
+      console.log(response)
+      // const res = JSON.parse(response)
+      this.$notify({
+        title: '提示',
+        message: '文件上传错误',
+        type: 'warning',
+        duration: 3000
+      })
     }
   }
 }
@@ -140,28 +174,4 @@ export default {
   overflow-x: hidden;
   overflow-y: auto;
 }
-
-.avatar-uploader .el-upload {
-  border: 1px dashed #d9d9d9;
-  border-radius: 6px;
-  cursor: pointer;
-  position: relative;
-  overflow: hidden;
-}
-.avatar-uploader .el-upload:hover {
-  border-color: #409EFF;
-}
-.avatar-uploader-icon {
-  font-size: 28px;
-  color: #8c939d;
-  width: 320px;
-  height: 240px;
-  line-height: 178px;
-  text-align: center;
-}
-.avatar {
-  width: 320px;
-  height: 240px;
-  display: block;
-}
 </style>

+ 1 - 1
src/components/upload/PublishVideo.vue

@@ -236,7 +236,7 @@ export default {
         file.cancel()
         this.$notify({
           title: '提示',
-          message: '视频文件应小于 10GiB',
+          message: '视频文件应小于 10GB',
           type: 'warning',
           duration: 3000
         })

+ 2 - 1
src/router/vod.js

@@ -4,7 +4,8 @@ const History = () => import('views/post/History')
 const FavVideo = () => import('views/post/FavlistVideo')
 const FavImage = () => import('views/post/FavlistImage')
 const PostAnalysis = () => import('views/post/PostAnalysis')
-const PostPublishVideo = () => import('components/upload/PublishVideo')
+const PostPublishVideo1 = () => import('components/upload/PublishVideo')
+const PostPublishVideo = () => import('components/upload/PublishFile')
 const UserPostVideo = () => import('views/post/VideoPost')
 const PostEditVideo = () => import('components/upload/EditVideo')
 const PostPublishAudio = () => import('components/upload/PublishAudio')

+ 60 - 55
src/utils/functions.js

@@ -8,11 +8,11 @@ import encHex from 'crypto-js/enc-hex'
  * @param {Object} datetime
  */
 export function formateTime(datetime) {
-  if (datetime == null) return ''
+  if (datetime === null) return ''
 
   datetime = datetime.replace(/-/g, '/')
 
-  let time = new Date()
+  const time = new Date()
   let outTime = new Date(datetime)
   if (/^[1-9]\d*$/.test(datetime)) {
     outTime = new Date(parseInt(datetime) * 1000)
@@ -20,36 +20,36 @@ export function formateTime(datetime) {
 
   if (
     time.getTime() < outTime.getTime() ||
-    time.getFullYear() != outTime.getFullYear()
+    time.getFullYear() === outTime.getFullYear()
   ) {
     return parseTime(outTime, '{y}-{m}-{d} {h}:{i}')
   }
 
-  if (time.getMonth() != outTime.getMonth()) {
+  if (time.getMonth() !== outTime.getMonth()) {
     return parseTime(outTime, '{m}-{d} {h}:{i}')
   }
 
-  if (time.getDate() != outTime.getDate()) {
-    let day = outTime.getDate() - time.getDate()
-    if (day == -1) {
+  if (time.getDate() !== outTime.getDate()) {
+    const day = outTime.getDate() - time.getDate()
+    if (day === -1) {
       return parseTime(outTime, '昨天 {h}:{i}')
     }
 
-    if (day == -2) {
+    if (day === -2) {
       return parseTime(outTime, '前天 {h}:{i}')
     }
 
     return parseTime(outTime, '{m}-{d} {h}:{i}')
   }
 
-  let diff = time.getTime() - outTime.getTime()
+  const diff = time.getTime() - outTime.getTime()
 
-  if (time.getHours() != outTime.getHours() || diff > 30 * 60 * 1000) {
+  if (time.getHours() !== outTime.getHours() || diff > 30 * 60 * 1000) {
     return parseTime(outTime, '{h}:{i}')
   }
 
   let minutes = outTime.getMinutes() - time.getMinutes()
-  if (minutes == 0) {
+  if (minutes === 0) {
     return '刚刚'
   }
 
@@ -63,15 +63,15 @@ export function formateTime(datetime) {
  * @param {String} value 文件大小(字节)
  */
 export function formateSize(value) {
-  if (null == value || value == '') {
+  if (value === null || value === '') {
     return '0'
   }
-  let unitArr = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
+  const unitArr = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
   let index = 0
-  let srcsize = parseFloat(value)
+  const srcsize = parseFloat(value)
   index = Math.floor(Math.log(srcsize) / Math.log(1000))
   let size = srcsize / Math.pow(1000, index)
-  size = size.toFixed(2) //保留的小数位数
+  size = size.toFixed(2) // 保留的小数位数
   return size + unitArr[index]
 }
 /**
@@ -91,19 +91,19 @@ export function getFileExt(fileName) {
  * @param {String} name
  */
 export function downloadIamge(imgsrc, name) {
-  //下载图片地址和图片名
-  let image = new Image()
+  // 下载图片地址和图片名
+  const image = new Image()
   // 解决跨域 Canvas 污染问题
   image.setAttribute('crossOrigin', 'anonymous')
   image.onload = function() {
-    let canvas = document.createElement('canvas')
+    const canvas = document.createElement('canvas')
     canvas.width = image.width
     canvas.height = image.height
-    let context = canvas.getContext('2d')
+    const context = canvas.getContext('2d')
     context.drawImage(image, 0, 0, image.width, image.height)
-    let url = canvas.toDataURL('image/png') //得到图片的base64编码数据
-    let a = document.createElement('a') // 生成一个a元素
-    let event = new MouseEvent('click') // 创建一个单击事件
+    const url = canvas.toDataURL('image/png') // 得到图片的base64编码数据
+    const a = document.createElement('a') // 生成一个a元素
+    const event = new MouseEvent('click') // 创建一个单击事件
     a.download = name || 'photo' // 设置图片名称
     a.href = url // 将生成的URL设置为a.href属性
     a.dispatchEvent(event) // 触发a的单击事件
@@ -117,21 +117,21 @@ export function downloadIamge(imgsrc, name) {
  * @param {String} imgsrc 例如图片名: D8x5f13a53dbc4b9_350x345.png
  */
 export function getImageInfo(imgsrc) {
-  let data = {
+  const data = {
     width: 0,
-    height: 0,
+    height: 0
   }
 
-  let arr = imgsrc.split('_')
-  if (arr.length == 1) return data
+  const arr = imgsrc.split('_')
+  if (arr.length === 1) return data
 
   let info = arr[arr.length - 1].match(/\d+x\d+/g)
-  if (info == null) return data
+  if (info === null) return data
 
   info = info[0].split('x')
   return {
     width: parseInt(info[0]),
-    height: parseInt(info[1]),
+    height: parseInt(info[1])
   }
 }
 
@@ -170,7 +170,7 @@ export function parseTime(time, cFormat) {
     h: date.getHours(),
     i: date.getMinutes(),
     s: date.getSeconds(),
-    a: date.getDay(),
+    a: date.getDay()
   }
 
   const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => {
@@ -194,7 +194,7 @@ export function parseTime(time, cFormat) {
 export function trim(str, type = null) {
   if (type) {
     return str.replace(/(^\s*)|(\s*$)/g, '')
-  } else if (type == 'l') {
+  } else if (type === 'l') {
     return str.replace(/(^\s*)/g, '')
   } else {
     return str.replace(/(\s*$)/g, '')
@@ -263,7 +263,7 @@ export function toggleClass(element, className) {
   }
 
   let classString = element.className
-  let nameIndex = classString.indexOf(className)
+  const nameIndex = classString.indexOf(className)
   if (nameIndex === -1) {
     classString += '' + className
   } else {
@@ -320,13 +320,13 @@ export function imgZoom(src, width = 200) {
   if (info.width < width) {
     return {
       width: `${info.width}px`,
-      height: `${info.height}px`,
+      height: `${info.height}px`
     }
   }
 
   return {
     width: width + 'px',
-    height: parseInt(info.height / (info.width / width)) + 'px',
+    height: parseInt(info.height / (info.width / width)) + 'px'
   }
 }
 
@@ -349,7 +349,7 @@ export function getSelection() {
  * @param {Function} callback 复制成功回调方法
  */
 export const copyTextToClipboard = (value, callback) => {
-  let textArea = document.createElement('textarea')
+  const textArea = document.createElement('textarea')
   textArea.style.background = 'transparent'
   textArea.value = value
 
@@ -382,13 +382,13 @@ export function hidePhone(phone) {
  * @param {Object} datetime
  */
 export function beautifyTime(datetime = '') {
-  if (datetime == null) {
+  if (datetime === null) {
     return ''
   }
 
   datetime = datetime.replace(/-/g, '/')
 
-  let time = new Date()
+  const time = new Date()
   let outTime = new Date(datetime)
   if (/^[1-9]\d*$/.test(datetime)) {
     outTime = new Date(parseInt(datetime) * 1000)
@@ -398,33 +398,33 @@ export function beautifyTime(datetime = '') {
     return parseTime(outTime, '{y}/{m}/{d}')
   }
 
-  if (time.getFullYear() != outTime.getFullYear()) {
+  if (time.getFullYear() !== outTime.getFullYear()) {
     return parseTime(outTime, '{y}/{m}/{d}')
   }
 
-  if (time.getMonth() != outTime.getMonth()) {
+  if (time.getMonth() !== outTime.getMonth()) {
     return parseTime(outTime, '{m}/{d}')
   }
 
-  if (time.getDate() != outTime.getDate()) {
-    let day = outTime.getDate() - time.getDate()
-    if (day == -1) {
+  if (time.getDate() !== outTime.getDate()) {
+    const day = outTime.getDate() - time.getDate()
+    if (day === -1) {
       return parseTime(outTime, '昨天 {h}:{i}')
     }
 
-    if (day == -2) {
+    if (day === -2) {
       return parseTime(outTime, '前天 {h}:{i}')
     }
 
     return parseTime(outTime, '{m}-{d}')
   }
 
-  if (time.getHours() != outTime.getHours()) {
+  if (time.getHours() !== outTime.getHours()) {
     return parseTime(outTime, '{h}:{i}')
   }
 
   let minutes = outTime.getMinutes() - time.getMinutes()
-  if (minutes == 0) {
+  if (minutes === 0) {
     return '刚刚'
   }
 
@@ -458,7 +458,7 @@ export function getMutipSort(arr) {
 
     do {
       tmp = arr[i++](a, b)
-    } while (tmp == 0 && i < arr.length)
+    } while (tmp === 0 && i < arr.length)
 
     return tmp
   }
@@ -471,7 +471,7 @@ export function getMutipSort(arr) {
  * @param {String} color 超链接颜色
  */
 export function textReplaceLink(text, color = '#409eff') {
-  let exp = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gi
+  const exp = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gi
   return text.replace(
     exp,
     `<a href='$1' target="_blank" style="color:${color};text-decoration: revert;">$1</a >`
@@ -490,12 +490,12 @@ export function debounce(func, wait, immediate) {
   let timeout
 
   return function() {
-    let context = this
-    let args = arguments
+    const context = this
+    const args = arguments
 
     if (timeout) clearTimeout(timeout) // timeout 不为null
     if (immediate) {
-      let callNow = !timeout // 第一次会立即执行,以后只有事件执行后才会再次触发
+      const callNow = !timeout // 第一次会立即执行,以后只有事件执行后才会再次触发
       timeout = setTimeout(function() {
         timeout = null
       }, wait)
@@ -521,7 +521,7 @@ export function sha256sum(file) {
   })
 }
 
-export function hashFile (file) {
+export function hashFile(file) {
   /**
    * 使用指定的算法计算hash值
    */
@@ -537,7 +537,7 @@ export function hashFile (file) {
     /**
      * 更新文件块的hash值
      */
-    function hashBlob (blob) {
+    function hashBlob(blob) {
       return new Promise((resolve, reject) => {
         const reader = new FileReader()
         reader.onload = ({ target }) => {
@@ -555,10 +555,15 @@ export function hashFile (file) {
   }
 
   // 同时计算文件的sha256和md5,并使用promise返回
+  /* return Promise.all([hashFileInternal(file, CryptoJs.algo.SHA256.create()),
+    hashFileInternal(file, CryptoJs.algo.MD5.create())])
+    .then(([sha256sum, md5sum]) => ({
+      sha256sum,
+      md5sum
+    }))*/
   return Promise.all([hashFileInternal(file, CryptoJs.algo.SHA256.create()),
     hashFileInternal(file, CryptoJs.algo.MD5.create())])
-      .then(([sha256sum, md5sum]) => ({
-        sha256sum,
-        md5sum
-      }))
+    .then(([sha256sum]) => ({
+      sha256sum
+    }))
 }

+ 0 - 82
src/utils/hash.js

@@ -1,82 +0,0 @@
-/** 公共方法类 */
-import CryptoJs from 'crypto-js'
-import encHex from 'crypto-js/enc-hex'
-
-export function hashBlob(blob) {
-  /**
-   * 使用指定的算法计算hash值
-   */
-  function hashFileInternal(blob, alog) {
-    let promise = Promise.resolve()
-    promise = promise.then(() => hashBlob(blob))
-
-    /**
-     * 更新文件块的hash值
-     */
-    function hashBlob(blob) {
-      return new Promise((resolve, reject) => {
-        const reader = new FileReader()
-        reader.onload = ({ target }) => {
-          const wordArray = CryptoJs.lib.WordArray.create(target.result)
-          // 增量更新计算结果
-          alog.update(wordArray)
-          resolve()
-        }
-        reader.readAsArrayBuffer(blob)
-      })
-    }
-
-    // 使用promise返回最终的计算结果
-    return promise.then(() => encHex.stringify(alog.finalize()))
-  }
-
-  // 同时计算文件的sha256和md5,并使用promise返回
-  return Promise.all([hashFileInternal(blob, CryptoJs.algo.SHA256.create()),
-    hashFileInternal(blob, CryptoJs.algo.MD5.create())])
-    .then(([sha256sum, md5sum]) => ({
-      sha256sum,
-      md5sum
-    }))
-}
-
-export function hashFile(file) {
-  /**
-   * 使用指定的算法计算hash值
-   */
-  function hashFileInternal(file, alog) {
-    // 指定块的大小,这里设置为20MB,可以根据实际情况进行配置
-    const chunkSize = 20 * 1024 * 1024
-    let promise = Promise.resolve()
-    // 使用promise来串联hash计算的顺序。因为FileReader是在事件中处理文件内容的,必须要通过某种机制来保证update的顺序是文件正确的顺序
-    for (let index = 0; index < file.size; index += chunkSize) {
-      promise = promise.then(() => hashBlob(file.slice(index, index + chunkSize)))
-    }
-
-    /**
-     * 更新文件块的hash值
-     */
-    function hashBlob(blob) {
-      return new Promise((resolve, reject) => {
-        const reader = new FileReader()
-        reader.onload = ({ target }) => {
-          const wordArray = CryptoJs.lib.WordArray.create(target.result)
-          // 增量更新计算结果
-          alog.update(wordArray)
-          resolve()
-        }
-        reader.readAsArrayBuffer(blob)
-      })
-    }
-
-    // 使用promise返回最终的计算结果
-    return promise.then(() => encHex.stringify(alog.finalize()))
-  }
-
-  // 同时计算文件的sha256和md5,并使用promise返回
-  return Promise.all([hashFileInternal(file, CryptoJs.algo.SHA256.create()),
-    hashFileInternal(file, CryptoJs.algo.MD5.create())])
-    .then(([sha256sum, md5sum]) => ({
-      sha256sum,
-      md5sum
-    }))
-}