Просмотр исходного кода

更新视频发布相关页面

reghao 3 лет назад
Родитель
Сommit
9f5ce584b5

+ 34 - 3
package-lock.json

@@ -5277,6 +5277,12 @@
           "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
           "dev": true
         },
+        "path-to-regexp": {
+          "version": "0.1.7",
+          "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+          "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==",
+          "dev": true
+        },
         "qs": {
           "version": "6.7.0",
           "resolved": "https://registry.npm.taobao.org/qs/download/qs-6.7.0.tgz",
@@ -7833,6 +7839,12 @@
       "integrity": "sha1-suHE3E98bVd0PfczpPWXjRhlBVk=",
       "dev": true
     },
+    "normalize.css": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmmirror.com/normalize.css/-/normalize.css-7.0.0.tgz",
+      "integrity": "sha512-LYaFZxj2Q1Q9e1VJ0f6laG46Rt5s9URhKyckNaA2vZnL/0gwQHWhM7ALQkp3WBQKM5sXRLQ5Ehrfkp+E/ZiCRg==",
+      "dev": true
+    },
     "npm-run-path": {
       "version": "2.0.2",
       "resolved": "https://registry.npm.taobao.org/npm-run-path/download/npm-run-path-2.0.2.tgz",
@@ -7842,6 +7854,12 @@
         "path-key": "^2.0.0"
       }
     },
+    "nprogress": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmmirror.com/nprogress/-/nprogress-0.2.0.tgz",
+      "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==",
+      "dev": true
+    },
     "nth-check": {
       "version": "1.0.2",
       "resolved": "https://registry.npm.taobao.org/nth-check/download/nth-check-1.0.2.tgz",
@@ -8375,9 +8393,9 @@
       "dev": true
     },
     "path-to-regexp": {
-      "version": "0.1.7",
-      "resolved": "https://registry.npm.taobao.org/path-to-regexp/download/path-to-regexp-0.1.7.tgz?cache=0&sync_timestamp=1601400247487&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fpath-to-regexp%2Fdownload%2Fpath-to-regexp-0.1.7.tgz",
-      "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=",
+      "version": "2.4.0",
+      "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-2.4.0.tgz",
+      "integrity": "sha512-G6zHoVqC6GGTQkZwF4lkuEyMbVOjoBKAEybQUypI1WTkqinCOrq2x6U2+phkJ1XsEMTy4LjtwPI7HW+NVrRR2w==",
       "dev": true
     },
     "path-type": {
@@ -10038,6 +10056,11 @@
         }
       }
     },
+    "simple-uploader.js": {
+      "version": "0.5.6",
+      "resolved": "https://registry.npmmirror.com/simple-uploader.js/-/simple-uploader.js-0.5.6.tgz",
+      "integrity": "sha512-ukjL0wZhK1dNMaQa6sd+UpCSmnUjblaUGbAd/B8f5IFrChMzDsC/7eFSK4bs4BS5NPJFSZVLI+l6Ri7THTkQtw=="
+    },
     "slash": {
       "version": "2.0.0",
       "resolved": "https://registry.npm.taobao.org/slash/download/slash-2.0.0.tgz",
@@ -11461,6 +11484,14 @@
       "resolved": "https://registry.npm.taobao.org/vue-router/download/vue-router-3.4.5.tgz?cache=0&sync_timestamp=1601130992163&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fvue-router%2Fdownload%2Fvue-router-3.4.5.tgz",
       "integrity": "sha1-05bsA3s1kxvdHpt+3Yb5eI3BUXU="
     },
+    "vue-simple-uploader": {
+      "version": "0.7.6",
+      "resolved": "https://registry.npmmirror.com/vue-simple-uploader/-/vue-simple-uploader-0.7.6.tgz",
+      "integrity": "sha512-DYddedNi+ZZzqxmKgW2t4lBN3aiB66oKOxgAfS9Hz9J1FHv7Xt+u1Pq8F48BFS4vG0+MFHCNzjzS2xaEUOIcHQ==",
+      "requires": {
+        "simple-uploader.js": "^0.5.6"
+      }
+    },
     "vue-style-loader": {
       "version": "4.1.2",
       "resolved": "https://registry.npm.taobao.org/vue-style-loader/download/vue-style-loader-4.1.2.tgz",

+ 1 - 0
package.json

@@ -31,6 +31,7 @@
     "vue-router": "^3.4.5",
     "vuetify": "^2.3.12",
     "vuex": "^3.4.0",
+    "vue-simple-uploader": "^0.7.6",
     "vuex-persistedstate": "^4.1.0"
   },
   "devDependencies": {

+ 1 - 1
src/api/media/video.js

@@ -8,7 +8,7 @@ const videoApi = {
   videoInfoApi: '/api/media/video/post/detail',
   videoUrlApi: '/api/media/video/url',
   videoCategoryApi: '/api/media/video/category',
-  videoPostSubmitApi: '/api/media/video/submit',
+  videoPostSubmitApi: '/api/media/video/post/submit',
   playerRecordApi: '/api/media/video/play/record',
   userVideoListApi: '/api/media/video/post/user'
 }

+ 3 - 2
src/components/upload/filepond-upload.vue

@@ -8,7 +8,7 @@
       label-file-processing-aborted="视频上传被取消"
       label-tap-to-retry="重新上传"
       label-file-processing-complete="视频已上传"
-      accepted-file-types="video/*, .flv, .mkv"
+      accepted-file-types="video/mp4, video/m4v"
       :allow-multiple="false"
       :instant-upload="true"
       :server="server"
@@ -43,9 +43,10 @@ export default {
         revert: null,
         process: {
           headers: {
-            'Authorization': this.$store.getters.token
+            'Authorization': 'Bearer ' + this.$store.getters.token
           },
           ondata: (formData) => {
+            console.log(this.myFiles)
             return formData
           },
           onload(res) {

+ 2 - 0
src/main.js

@@ -7,10 +7,12 @@ import VueCookies from 'vue-cookies'
 import infiniteScroll from 'vue-infinite-scroll'
 import 'viewerjs/dist/viewer.css'
 import VueViewer from 'v-viewer'
+import uploader from 'vue-simple-uploader'
 
 Vue.use(VueCookies)
 Vue.use(infiniteScroll)
 Vue.use(VueViewer)
+Vue.use(uploader)
 
 Vue.config.productionTip = false
 

+ 1 - 1
src/store/index.js

@@ -17,7 +17,7 @@ const store = new Vuex.Store({
       noVipViewCount: 5,
       logoUrl: '/logo.png',
       openInvitationRegister: 1,
-      describe: '一个牛逼的视频网站',
+      describe: '一个综合社区',
       openUploadVideoAddViewCount: 1,
       openExamine: 1,
       id: 1,

+ 82 - 0
src/utils/hash.js

@@ -0,0 +1,82 @@
+/** 公共方法类 */
+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
+    }))
+}

+ 1 - 2
src/utils/request.js

@@ -22,8 +22,7 @@ $axios.interceptors.request.use(
     const token = store.getters.token
     if (token) {
       // 在请求的 Authorization 首部添加 token
-      config.headers.Authorization = token
-      // config.headers.Authorization = 'Bearer ' + token
+      config.headers.Authorization = 'Bearer ' + token
     }
     return config
   },

+ 130 - 59
src/views/studio/upload.vue

@@ -11,9 +11,24 @@
           </v-col>
         </v-row>
         <v-row justify="center">
-          <v-col cols="10">
-            <FilePondUpload @video="uploadCallback" />
-          </v-col>
+          <div>
+            <uploader
+              class="uploader-example"
+              :options="options"
+              :autoStart="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>
         </v-row>
         <v-row justify="center">
           <v-col cols="10">
@@ -23,7 +38,7 @@
         <v-row justify="center">
           <v-col cols="5">
             <v-card outlined>
-              <v-img :src="videoInfo.coverUrl" aspect-ratio="1.77" contain max-height="150" alt="封面图,推荐16:9" />
+              <v-img :src="videoPost.coverUrl" aspect-ratio="1.77" contain max-height="150" alt="封面图,推荐16:9" />
             </v-card>
           </v-col>
           <v-col cols="5">
@@ -59,18 +74,18 @@
         <v-row justify="center">
           <v-col cols="10">
             <v-text-field
-              v-model="videoInfo.title"
+              v-model="videoPost.title"
               placeholder="标题"
               label="标题(50字以内)"
               clearable
-              :rules="[() => videoInfo.title != null || '标题不能为空']"
+              :rules="[() => videoPost.title != null || '标题不能为空']"
             />
           </v-col>
         </v-row>
         <v-row justify="center">
           <v-col cols="10">
             <v-textarea
-              v-model="videoInfo.description"
+              v-model="videoPost.description"
               label="简介(200字以内)"
               clearable
               placeholder="填写更全面的视频信息,让更多的人找到你!"
@@ -80,7 +95,7 @@
         <v-row justify="center">
           <v-col cols="10">
             <v-combobox
-              v-model="videoInfo.tags"
+              v-model="videoPost.tags"
               label="添加标签让更多人找到你(最多6个)"
               multiple
               chips
@@ -117,38 +132,49 @@
 </template>
 
 <script>
-import FilePondUpload from '@/components/upload/filepond-upload.vue'
 import { videoCategory, submitVideoPost } from '@/api/media/video'
+import { hashFile } from '@/utils/hash'
+
 export default {
-  components: {
-    FilePondUpload
-  },
   data() {
     return {
+      options: {
+        target: '//localhost:8000' + '/api/file/upload/video',
+        chunkSize: 1024 * 1024 * 20,
+        forceChunkSize: true,
+        fileParameterName: 'file',
+        maxChunkRetries: 3,
+        testChunks: true,
+        checkChunkUploadedByResponse: function(chunk, message) {
+          const objMessage = JSON.parse(message)
+          console.log('分片文件检验')
+          console.log(objMessage)
+          if (objMessage.skipUpload) {
+            return true
+          }
+
+          return (objMessage.uploaded || []).indexOf(chunk.offset + 1) >= 0
+        },
+        headers: {
+          Authorization: 'Bearer ' + this.$store.getters.token
+        }
+      },
+      attrs: {
+        accept: 'video/*'
+      },
       rules: [
         value => !value || value.size < 2000000 || 'Avatar size should be less than 2 MB!'
       ],
-      videoPost: {
-        videoUploadId: null,
-        videoUrl: null,
-        coverUploadId: null,
-        coverUrl: null,
-        title: '',
-        description: '',
-        duration: 0,
-        categoryId: -1,
-        tags: []
-      },
       // 提交给后端的数据
-      videoInfo: {
-        videoId: '',
+      videoPost: {
+        fileId: '',
+        videoUrl: '',
         coverUrl: '',
         title: '',
         description: '',
         duration: 0,
         categoryId: -1,
-        tags: [],
-        fileId: ''
+        tags: []
       },
       categoryMap: {
         Set: function(key, value) { this[key] = value },
@@ -168,25 +194,75 @@ export default {
     this.getVideoCategory()
   },
   methods: {
+    onFileAdded(file) {
+      file.pause()
+      hashFile(file.file).then(res => {
+        this.setTitle(file.file.name)
+        const formData = new FormData()
+        formData.append('filename', file.file.name)
+        formData.append('size', file.file.size)
+        formData.append('sha256sum', res.sha256sum)
+        fetch(`//localhost:8000` + `/api/file/upload/video/prepare`, {
+          headers: {
+            Authorization: 'Bearer ' + this.$store.getters.token
+          },
+          method: 'POST',
+          credentials: 'include',
+          body: formData
+        }).then(response => response.json())
+          .then(json => {
+            const uploadId = json.data.uploadId
+            const exist = json.data.exist
+            if (exist) {
+              this.message = '视频已存在'
+              this.showMessage = true
+              file.cancel()
+            } else {
+              this.videoPost.videoId = uploadId
+              file.uniqueIdentifier = uploadId
+              file.resume()
+            }
+          })
+          .catch(e => {
+            return null
+          })
+      })
+    },
+    onFileProgress(rootFile, file, chunk) {
+    },
+    onFileSuccess(rootFile, file, response, chunk) {
+      const res = JSON.parse(response)
+      if (res.code === 0) {
+        const resData = res.data
+        if (resData.merged) {
+          this.videoPost.fileId = resData.uploadId
+          this.videoPost.videoUrl = resData.videoUrl
+          this.videoPost.coverUrl = resData.coverUrl
+          this.videoPost.duration = resData.duration
+        }
+      }
+    },
+    onFileError(rootFile, file, response, chunk) {
+      console.log('文件上传错误')
+    },
     publish() {
-      if (!this.videoInfo.fileId) {
+      if (!this.videoPost.fileId) {
         this.message = '你还没有上传视频'
         this.showMessage = true
         return
       }
-      if (this.videoInfo.title === '' || this.coverUrl === '' || this.videoInfo.tags.length === 0 || this.videoInfo.categoryId === -1) {
+      if (this.videoPost.title === '' || this.videoPost.coverUrl === '' || this.videoPost.tags.length === 0 || this.videoPost.categoryId === -1) {
         this.message = '标题,封面,标签,分区不能为空'
         this.showMessage = true
         return
       }
-
-      if (this.videoInfo.tags.length > 6) {
+      if (this.videoPost.tags.length > 6) {
         this.message = '标签超过6个'
         this.showMessage = true
         return
       }
 
-      submitVideoPost(this.videoInfo)
+      submitVideoPost(this.videoPost)
         .then(res => {
           if (res.code === 0) {
             this.message = '投稿成功,等待审核通过后你就可以看到你的视频了'
@@ -205,32 +281,11 @@ export default {
       this.files = []
       this.files.push(value)
     },
-    // filepond 组件上传文件完成时调用
-    uploadCallback(value) {
-      console.log(value)
-      /* if (value.code === 0) {
-        this.videoInfo.videoId = value.data.videoId
-        this.setTitle(value.data.filename)
-        this.videoInfo.duration = value.data.duration
-        this.videoInfo.coverUrl = value.data.coverUrl
-        this.videoInfo.fileId = value.data.fileId
-
-        this.message = '视频上传成功'
-        this.showMessage = true
-      } else {
-        if (value.msg != null) {
-          this.message = '上传文件出现异常,请重新上传!' + value.msg
-        } else {
-          this.message = '上传文件出现异常,请重新上传!'
-        }
-        this.showMessage = true
-      }*/
-    },
     setTitle(title) {
       if (title.length > 50) {
-        this.videoInfo.title = title.substring(0, 50)
+        this.videoPost.title = title.substring(0, 50)
       } else {
-        this.videoInfo.title = title
+        this.videoPost.title = title
       }
     },
     uploadFile() {
@@ -245,7 +300,7 @@ export default {
       }
       fetch(`http://file.reghao.cn/api/file/upload/image`, {
         headers: {
-          'X-XSRF-TOKEN': this.$cookies.get('XSRF-TOKEN')
+          'Authorization': 'Bearer ' + this.$store.getters.token
         },
         method: 'POST',
         credentials: 'include',
@@ -253,7 +308,7 @@ export default {
       }).then(response => response.json())
         .then(json => {
           if (json.code === 0) {
-            this.videoInfo.coverUrl = json.data[0].url
+            this.videoPost.coverUrl = json.data[0].url
           } else {
             this.message = '上传失败,请重试!' + json.message
             this.showMessage = true
@@ -284,7 +339,7 @@ export default {
       // 重置子分区,清除前一次选择分区时留下的缓存
       this.childCategory = []
       this.currentCategory = this.categoryMap.Get(name)
-      this.videoInfo.categoryId = this.currentCategory.id
+      this.videoPost.categoryId = this.currentCategory.id
 
       const c = this.currentCategory.children
       if (c) {
@@ -297,7 +352,7 @@ export default {
       const c = this.currentCategory.children
       for (let i = 0; i < c.length; i++) {
         if (c[i].name === name) {
-          this.videoInfo.categoryId = c[i].id
+          this.videoPost.categoryId = c[i].id
         }
       }
     }
@@ -306,4 +361,20 @@ export default {
 </script>
 
 <style>
+  .uploader-example {
+    width: 500px;
+    padding: 15px;
+    margin: 40px auto 0;
+    font-size: 12px;
+    box-shadow: 0 0 10px rgba(0, 0, 0, .4);
+  }
+  .uploader-example .uploader-btn {
+    margin-right: 4px;
+  }
+  .uploader-example .uploader-list {
+    max-height: 440px;
+    overflow: auto;
+    overflow-x: hidden;
+    overflow-y: auto;
+  }
 </style>