فهرست منبع

更新视频发布和文件上传页面

reghao 2 سال پیش
والد
کامیت
347555fa4b

+ 1 - 0
package.json

@@ -10,6 +10,7 @@
   "dependencies": {
     "axios": "^0.19.2",
     "core-js": "^3.6.4",
+    "crypto-js": "^4.1.1",
     "element-ui": "^2.13.0",
     "videojs-contrib-hls": "^5.15.0",
     "videojs-flash": "^2.2.1",

+ 3 - 4
src/api/file.js

@@ -1,10 +1,9 @@
 import { post } from '@/utils/request'
 
 const fileApi = {
-  videoIdApi: '/api/file/id/video'
+  ossPolicyApi: '/api/file/oss/policy'
 }
 
-// 获取视频文件 ID 和 URL ID
-export function getVideoId() {
-  return post(fileApi.videoIdApi)
+export function getOssPolicy() {
+  return post(fileApi.ossPolicyApi)
 }

+ 4 - 4
src/api/video.js

@@ -7,11 +7,11 @@ const videoApi = {
   videoCategoryApi: '/api/content/video/category',
   videoTagApi: '/api/content/video/tag',
   videoUrlApi: '/api/content/video/url',
+  videoSubmitApi: '/api/content/video/submit',
+  userVideoListApi: '/api/content/video/user',
 
   videoTimelineApi: '/api/media/video/post/timeline',
-  videoPostSubmitApi: '/api/media/video/post/submit',
   playerRecordApi: '/api/media/video/play/record',
-  userVideoListApi: '/api/media/video/post/user',
   userRecentlyVideoListApi: '/api/media/video/post/user/recently',
   testVideoApi: '/api/media/video/post/display'
 }
@@ -45,8 +45,8 @@ export function videoCategory() {
   return get(videoApi.videoCategoryApi)
 }
 
-export function submitVideoPost(videoPost) {
-  return post(videoApi.videoPostSubmitApi, videoPost)
+export function submitVideo(video) {
+  return post(videoApi.videoSubmitApi, video)
 }
 
 // 播放记录

+ 6 - 302
src/components/upload/PublishAudio.vue

@@ -15,8 +15,8 @@
           >
             <uploader-unsupport />
             <uploader-drop>
-              <p>拖动频文件到此处或</p>
-              <uploader-btn :attrs="attrs">选择频文件</uploader-btn>
+              <p>拖动频文件到此处或</p>
+              <uploader-btn :attrs="attrs">选择频文件</uploader-btn>
             </uploader-drop>
             <uploader-list />
           </uploader>
@@ -46,16 +46,6 @@
           <el-form-item label="描述">
             <el-input v-model="form.desc" type="textarea" />
           </el-form-item>
-          <el-form-item label="分区">
-            <el-select v-model="form.region" placeholder="请选择分区">
-              <el-option label="区域一" value="shanghai" />
-              <el-option label="区域二" value="beijing" />
-            </el-select>
-            <el-select v-model="form.region" placeholder="请选择子分区">
-              <el-option label="区域一" value="shanghai" />
-              <el-option label="区域二" value="beijing" />
-            </el-select>
-          </el-form-item>
           <el-form-item label="发布时间">
             <el-col :span="11">
               <el-date-picker v-model="form.date1" type="date" placeholder="选择日期" style="width: 100%;" />
@@ -64,23 +54,6 @@
               <el-time-picker v-model="form.date2" placeholder="选择时间" style="width: 100%;" />
             </el-col>
           </el-form-item>
-          <el-form-item label="即时配送">
-            <el-switch v-model="form.delivery" />
-          </el-form-item>
-          <el-form-item label="活动性质">
-            <el-checkbox-group v-model="form.type">
-              <el-checkbox label="美食/餐厅线上活动" name="type" />
-              <el-checkbox label="地推活动" name="type" />
-              <el-checkbox label="线下主题活动" name="type" />
-              <el-checkbox label="单纯品牌曝光" name="type" />
-            </el-checkbox-group>
-          </el-form-item>
-          <el-form-item label="特殊资源">
-            <el-radio-group v-model="form.resource">
-              <el-radio label="线上品牌商赞助" />
-              <el-radio label="线下场地免费" />
-            </el-radio-group>
-          </el-form-item>
           <el-form-item>
             <el-button type="primary" @click="onSubmit">立即投稿</el-button>
             <el-button>取消</el-button>
@@ -92,8 +65,6 @@
 </template>
 
 <script>
-import { videoCategory, submitVideoPost } from '@/api/video'
-import { getVideoId } from '@/api/file'
 
 export default {
   name: 'PublishAudio',
@@ -115,46 +86,7 @@ export default {
       attrs: {
         accept: 'video/*'
       },
-      rules: [
-        value => !value || value.size < 2000000 || 'Avatar size should be less than 2 MB!'
-      ],
-      coverUrl: null,
-      videoUrlId: null,
-      // 提交给后端的数据
-      videoPost: {
-        videoFileId: null,
-        urlObjectName: null,
-        coverFileId: null,
-        title: null,
-        description: null,
-        categoryId: null,
-        tags: [],
-        scope: 1,
-        width: null,
-        height: null,
-        duration: null
-      },
-      categoryMap: {
-        Set: function(key, value) { this[key] = value },
-        Get: function(key) { return this[key] },
-        Contains: function(key) { return this.Get(key) !== null },
-        Remove: function(key) { delete this[key] }
-      },
-      category: [],
-      childCategory: [],
-      scope: [
-        '所有人可见',
-        '验证码可见',
-        'VIP 可见',
-        '仅自己可见'
-      ],
-      nowCategory: {},
       coverFile: null,
-      showMessage: false,
-      message: '',
-      dialogImageUrl: '',
-      dialogVisible: false,
-      disabled: false,
       imageList: [],
       imageUrl: '',
       form: {
@@ -185,92 +117,10 @@ export default {
     },
     handleAvatarSuccess(res, file) {
       this.imageList.push(file)
-      console.log(this.imageList)
       this.imageUrl = URL.createObjectURL(file.raw)
     },
-    // 选择视频后获取视频的分辨率和时长, 并截取第一秒的内容作为封面
-    processVideo(file) {
-      return new Promise((resolve, reject) => {
-        const canvas = document.createElement('canvas')
-        const canvasCtx = canvas.getContext('2d')
-
-        const videoElem = document.createElement('video')
-        const dataUrl = window.URL.createObjectURL(file)
-        // 当前帧的数据是可用的
-        videoElem.onloadeddata = function() {
-          resolve(videoElem)
-        }
-        videoElem.onerror = function() {
-          reject('video 后台加载失败')
-        }
-        // 设置 auto 预加载数据, 否则会出现截图为黑色图片的情况
-        videoElem.setAttribute('preload', 'auto')
-        videoElem.src = dataUrl
-        // 预加载完成后才会获取到视频的宽高和时长数据
-        videoElem.addEventListener('canplay', this.onCanPlay(videoElem, canvas, canvasCtx))
-      })
-    },
-    onCanPlay(videoElem, canvas, canvasCtx) {
-      setTimeout(() => {
-        // 视频视频分辨率
-        const videoWidth = videoElem.videoWidth
-        const videoHeight = videoElem.videoHeight
-        this.videoPost.width = videoWidth
-        this.videoPost.height = videoHeight
-        this.videoPost.duration = videoElem.duration
-
-        videoElem.pause()
-        /* const ratio = window.devicePixelRatio || 1
-        canvasCtx.scale(ratio, ratio)*/
-        // 设置画布尺寸
-        canvas.width = videoWidth
-        canvas.height = videoHeight
-        canvasCtx.drawImage(videoElem, 0, 0, canvas.width, canvas.height)
-        // 把图标base64编码后变成一段url字符串
-        const urlData = canvas.toDataURL('image/jpeg')
-        if (typeof urlData !== 'string') {
-          alert('urlData不是字符串')
-          return
-        }
-
-        var arr = urlData.split(',')
-        var bstr = atob(arr[1])
-        var n = bstr.length
-        var u8arr = new Uint8Array(n)
-        while (n--) {
-          u8arr[n] = bstr.charCodeAt(n)
-        }
-        const coverFile = new File([u8arr], 'cover.jpg', { type: 'image/jpeg' })
-
-        if (coverFile instanceof File) {
-          const formData = new FormData()
-          formData.append('file', coverFile)
-          fetch(`//api.reghao.cn/api/file/upload/image`, {
-            headers: {},
-            method: 'POST',
-            credentials: 'include',
-            body: formData
-          }).then(response => response.json())
-            .then(json => {
-              if (json.code === 0) {
-                this.videoPost.coverFileId = json.data.imageFileId
-                this.coverUrl = json.data.imageUrl
-              } else {
-                this.message = '视频封面上传失败,请重试!' + json.message
-                this.showMessage = true
-              }
-            })
-            .catch(e => {
-              return null
-            })
-        }
-      }, 1000) // 1000毫秒,就是截取第一秒,2000毫秒就是截取第2秒,视频1秒通常24帧,也可以换算成截取第几帧。
-      // 防止拖动进度条的时候重复触发
-      // videoElem.removeEventListener('canplay', arguments.callee)
-    },
     onFileAdded(file) {
       this.setTitle(file.file.name)
-      this.processVideo(file.file)
       /* file.pause()
       hashFile(file.file).then(res => {
         const formData = new FormData()
@@ -314,52 +164,6 @@ export default {
     onFileError(rootFile, file, response, chunk) {
       console.log('文件上传错误')
     },
-    publish() {
-      if (!this.videoPost.videoFileId) {
-        this.message = '你还没有上传视频'
-        this.showMessage = true
-        return
-      }
-
-      if (!this.videoPost.coverFileId) {
-        this.message = '你还没有上传视频封面'
-        this.showMessage = true
-        return
-      }
-
-      if (this.videoPost.title === '' || this.videoPost.categoryId === -1) {
-        this.message = '分区和稿件标题不能为空'
-        this.showMessage = true
-        return
-      }
-
-      /* if (this.videoPost.scope === null) {
-        this.message = '稿件可见范围不能为空'
-        this.showMessage = true
-        return
-      }*/
-
-      if (this.videoPost.tags.length === 0 || this.videoPost.tags.length > 10) {
-        this.message = '标签最少 1 个, 最多 10 个'
-        this.showMessage = true
-        return
-      }
-
-      submitVideoPost(this.videoPost)
-        .then(res => {
-          if (res.code === 0) {
-            this.message = '投稿成功,等待审核通过后其他人就可以看到你的视频了'
-            this.showMessage = true
-            this.$router.push('/studio')
-          } else {
-            this.message = res.msg
-            this.showMessage = true
-          }
-        })
-        .catch(error => {
-          console.error(error.message)
-        })
-    },
     setFile(value) {
       this.coverFile = value
     },
@@ -371,112 +175,12 @@ export default {
         this.videoPost.title = title
       }
     },
-    /* uploadVideoCover() {
-      if (this.coverFile === null) {
-        this.message = '请先选择视频封面,然后上传!'
-        this.showMessage = true
-        return
-      }
-
-      if (this.videoPost.videoFileId === null) {
-        this.message = '等待视频上传完成后再上传封面!'
+    onSubmit() {
+      /* if (this.videoPost.scope === null) {
+        this.message = '稿件可见范围不能为空'
         this.showMessage = true
         return
-      }
-
-      const formData = new FormData()
-      formData.append('videoFileId', this.videoPost.videoFileId)
-      formData.append('file', this.coverFile)
-      fetch(`//api.reghao.cn/api/file/upload/video/cover`, {
-        headers: {
-          'Authorization': 'Bearer ' + this.$store.getters.token
-        },
-        method: 'POST',
-        credentials: 'include',
-        body: formData
-      }).then(response => response.json())
-        .then(json => {
-          if (json.code === 0) {
-            this.message = '封面已上传'
-            this.showMessage = true
-            this.videoPost.coverFileId = json.data.imageFileId
-            this.videoPost.imageUrl = json.data.imageUrl
-          } else {
-            this.message = '上传失败,请重试!' + json.message
-            this.showMessage = true
-          }
-        })
-        .catch(e => {
-          return null
-        })
-    },*/
-    getVideoCategory() {
-      videoCategory()
-        .then(res => {
-          if (res.code === 0) {
-            for (let i = 0; i < res.data.length; i++) {
-              const name = res.data[i].name
-              this.category.push(name)
-              this.categoryMap.Set(name, res.data[i])
-            }
-          } else {
-            console.error(res.msg)
-          }
-        })
-        .catch(error => {
-          console.error(error.message)
-        })
-    },
-    getCategory(name) {
-      // 重置子分区,清除前一次选择分区时留下的缓存
-      this.childCategory = []
-      this.currentCategory = this.categoryMap.Get(name)
-      this.videoPost.categoryId = this.currentCategory.id
-
-      const c = this.currentCategory.children
-      if (c) {
-        for (let i = 0; i < c.length; i++) {
-          this.childCategory.push(c[i].name)
-        }
-      }
-    },
-    getChildCategory(name) {
-      const c = this.currentCategory.children
-      for (let i = 0; i < c.length; i++) {
-        if (c[i].name === name) {
-          this.videoPost.categoryId = c[i].id
-        }
-      }
-    },
-    setVideoScope(scope) {
-      if (scope === '所有人可见') {
-        this.videoPost.scope = 1
-      } else if (scope === '验证码可见') {
-        this.videoPost.scope = 2
-      } else if (scope === 'VIP 可见') {
-        this.videoPost.scope = 3
-      } else if (scope === '仅自己可见') {
-        this.videoPost.scope = 4
-      }
-    },
-    getVideoIdWrapper() {
-      getVideoId(this.videoPost)
-        .then(res => {
-          if (res.code === 0) {
-            console.log(res.data)
-            this.videoPost.videoFileId = res.data.videoFileId
-            this.videoUrlId = res.data.videoUrlId
-          } else {
-            this.message = res.msg
-            this.showMessage = true
-          }
-        })
-        .catch(error => {
-          console.error(error.message)
-        })
-    },
-    onSubmit() {
-      console.log('submit!')
+      }*/
     }
   }
 }

+ 224 - 226
src/components/upload/PublishVideo.vue

@@ -40,28 +40,30 @@
       </el-row>
       <el-form ref="form" :model="form" label-width="80px">
         <el-form-item label="标题">
-          <el-input v-model="form.name" style="width: 70%; padding-right: 2px" placeholder="标题不能超过 50 个字符" />
+          <el-input v-model="form.title" style="width: 70%; padding-right: 2px" placeholder="标题不能超过 50 个字符" />
         </el-form-item>
         <el-form-item label="描述">
-          <el-input v-model="form.desc" type="textarea" style="width: 70%; padding-right: 2px" />
+          <el-input v-model="form.description" type="textarea" style="width: 70%; padding-right: 2px" />
         </el-form-item>
         <el-form-item label="分区">
           <el-select v-model="category" placeholder="请选择分区">
-            <el-option label="区域一" value="shanghai" />
-            <el-option label="区域二" value="beijing" />
+            <el-option label="新闻" value="shanghai" />
+            <el-option label="教育" value="beijing" />
           </el-select>
           <el-select v-model="childCategory" placeholder="请选择子分区">
-            <el-option label="区域一" value="shanghai" />
-            <el-option label="区域二" value="beijing" />
+            <el-option label="历史" value="shanghai" />
+            <el-option label="计算机" value="beijing" />
           </el-select>
         </el-form-item>
         <el-form-item label="标签">
           <el-input v-model="form.tags" style="width: 70%; padding-right: 2px" placeholder="多个标签之间使用英文逗号分隔" />
         </el-form-item>
         <el-form-item label="可见范围">
-          <el-select v-model="scope" placeholder="选择可见范围">
-            <el-option label="区域一" value="shanghai" />
-            <el-option label="区域二" value="beijing" />
+          <el-select v-model="form.scope" placeholder="选择可见范围">
+            <el-option label="所有人可见" value="1" />
+            <el-option label="验证码可见" value="2" />
+            <el-option label="VIP 可见" value="3" />
+            <el-option label="仅自己可见" value="4" />
           </el-select>
         </el-form-item>
         <el-form-item>
@@ -74,22 +76,34 @@
 </template>
 
 <script>
-import { videoCategory, submitVideoPost } from '@/api/video'
-import { getVideoId } from '@/api/file'
+import { videoCategory, submitVideo } from '@/api/video'
+import { getOssPolicy } from '@/api/file'
 
 export default {
   name: 'PublishVideo',
   data() {
     return {
-      activeName: 'first',
+      /***********************************************************************/
       options: {
-        target: '//api.reghao.cn/api/file/upload/video',
+        target: '//oss.reghao.cn/',
         chunkSize: 1024 * 1024 * 1024 * 5, // 5GiB
         fileParameterName: 'file',
         testChunks: false,
         query: (file, chunk) => {
-          this.videoPost.urlObjectName = 'video/playback/' + this.videoUrlId
-          return { key: this.videoPost.urlObjectName }
+          return {
+            key: this.form.videoObjectName,
+            acl: '',
+            policy: '',
+            'content-type': '',
+            'success_action_redirect': '',
+            'x-amz-meta-uuid': '',
+            'x-amz-server-side-encryption': '',
+            'X-amz-credential': '',
+            'x-amz-algorithm': '',
+            'x-amz-date': '',
+            'x-amz-meta-tag': '',
+            'x-amz-signature': ''
+          }
         },
         headers: {
         }
@@ -97,25 +111,8 @@ export default {
       attrs: {
         accept: 'video/*'
       },
-      rules: [
-        value => !value || value.size < 2000000 || 'Avatar size should be less than 2 MB!'
-      ],
+      /***********************************************************************/
       coverUrl: null,
-      videoUrlId: null,
-      // 提交给后端的数据
-      videoPost: {
-        videoFileId: null,
-        urlObjectName: null,
-        coverFileId: null,
-        title: null,
-        description: null,
-        categoryId: null,
-        tags: [],
-        scope: 1,
-        width: null,
-        height: null,
-        duration: null
-      },
       categoryMap: {
         Set: function(key, value) { this[key] = value },
         Get: function(key) { return this[key] },
@@ -124,30 +121,18 @@ export default {
       },
       category: [],
       childCategory: [],
-      scope: [
-        '所有人可见',
-        '验证码可见',
-        'VIP 可见',
-        '仅自己可见'
-      ],
-      nowCategory: {},
       coverFile: null,
-      showMessage: false,
-      message: '',
-      dialogImageUrl: '',
-      dialogVisible: false,
-      disabled: false,
       imageList: [],
       imageUrl: '',
+      // 提交给后端的数据
       form: {
-        videoFileId: null,
-        urlObjectName: null,
+        videoObjectName: null,
         coverFileId: null,
         title: null,
-        desc: null,
+        description: null,
         categoryId: 0,
         tags: null,
-        scope: 1,
+        scope: null,
         width: 0,
         height: 0,
         duration: 0
@@ -155,10 +140,53 @@ export default {
     }
   },
   created() {
-    this.getVideoIdWrapper()
+    this.getOssPolicyWrapper()
     this.getVideoCategory()
   },
   methods: {
+    /***********************************************************************/
+    onFileAdded(file) {
+      if (file.file.size > 1024*1024*1024*5) {
+        file.cancel()
+        this.$notify(
+          {
+            title: '提示',
+            message: '视频文件应小于 5GiB',
+            type: 'warning',
+            duration: 3000
+          }
+        )
+        return
+      }
+      this.setTitle(file.file.name)
+      this.processVideo(file.file)
+    },
+    onFileProgress(rootFile, file, chunk) {
+    },
+    onFileSuccess(rootFile, file, response, chunk) {
+      const res = JSON.parse(response)
+      if (res.code === 0) {
+        this.$notify(
+          {
+            title: '提示',
+            message: '视频已上传',
+            type: 'warning',
+            duration: 3000
+          }
+        )
+      }
+    },
+    onFileError(rootFile, file, response, chunk) {
+      this.$notify(
+        {
+          title: '提示',
+          message: '文件上传错误',
+          type: 'warning',
+          duration: 3000
+        }
+      )
+    },
+    /***********************************************************************/
     beforeAvatarUpload(file) {
       const isJPG = file.type === 'image/jpeg'
       const isLt2M = file.size / 1024 / 1024 < 2
@@ -172,7 +200,6 @@ export default {
     },
     handleAvatarSuccess(res, file) {
       this.imageList.push(file)
-      console.log(this.imageList)
       this.imageUrl = URL.createObjectURL(file.raw)
     },
     // 选择视频后获取视频的分辨率和时长, 并截取第一秒的内容作为封面
@@ -202,13 +229,11 @@ export default {
         // 视频视频分辨率
         const videoWidth = videoElem.videoWidth
         const videoHeight = videoElem.videoHeight
-        this.videoPost.width = videoWidth
-        this.videoPost.height = videoHeight
-        this.videoPost.duration = videoElem.duration
+        this.form.width = videoWidth
+        this.form.height = videoHeight
+        this.form.duration = videoElem.duration
 
         videoElem.pause()
-        /* const ratio = window.devicePixelRatio || 1
-        canvasCtx.scale(ratio, ratio)*/
         // 设置画布尺寸
         canvas.width = videoWidth
         canvas.height = videoHeight
@@ -228,7 +253,6 @@ export default {
           u8arr[n] = bstr.charCodeAt(n)
         }
         const coverFile = new File([u8arr], 'cover.jpg', { type: 'image/jpeg' })
-
         if (coverFile instanceof File) {
           const formData = new FormData()
           formData.append('file', coverFile)
@@ -240,11 +264,17 @@ export default {
           }).then(response => response.json())
             .then(json => {
               if (json.code === 0) {
-                this.videoPost.coverFileId = json.data.imageFileId
+                this.form.coverFileId = json.data.imageFileId
                 this.imageUrl = json.data.imageUrl
               } else {
-                this.message = '视频封面上传失败,请重试!' + json.message
-                this.showMessage = true
+                this.$notify(
+                  {
+                    title: '提示',
+                    message: '视频封面上传失败,请重试!' + json.message,
+                    type: 'warning',
+                    duration: 3000
+                  }
+                )
               }
             })
             .catch(e => {
@@ -255,170 +285,49 @@ export default {
       // 防止拖动进度条的时候重复触发
       // videoElem.removeEventListener('canplay', arguments.callee)
     },
-    onFileAdded(file) {
-      this.setTitle(file.file.name)
-      this.processVideo(file.file)
-      /* file.pause()
-      hashFile(file.file).then(res => {
-        const formData = new FormData()
-        formData.append('filename', file.file.name)
-        formData.append('size', file.file.size)
-        formData.append('sha256sum', res.sha256sum)
-        fetch(`//file.reghao.cn` + `/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 {
-              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) {
-        this.message = '视频已上传'
-        this.showMessage = true
-      }
-    },
-    onFileError(rootFile, file, response, chunk) {
-      console.log('文件上传错误')
-    },
-    publish() {
-      if (!this.videoPost.videoFileId) {
-        this.message = '你还没有上传视频'
-        this.showMessage = true
-        return
-      }
-
-      if (!this.videoPost.coverFileId) {
-        this.message = '你还没有上传视频封面'
-        this.showMessage = true
-        return
-      }
-
-      if (this.videoPost.title === '' || this.videoPost.categoryId === -1) {
-        this.message = '分区和稿件标题不能为空'
-        this.showMessage = true
-        return
-      }
-
-      /* if (this.videoPost.scope === null) {
-        this.message = '稿件可见范围不能为空'
-        this.showMessage = true
-        return
-      }*/
-
-      if (this.videoPost.tags.length === 0 || this.videoPost.tags.length > 10) {
-        this.message = '标签最少 1 个, 最多 10 个'
-        this.showMessage = true
-        return
-      }
-
-      submitVideoPost(this.videoPost)
-        .then(res => {
-          if (res.code === 0) {
-            this.message = '投稿成功,等待审核通过后其他人就可以看到你的视频了'
-            this.showMessage = true
-            this.$router.push('/studio')
-          } else {
-            this.message = res.msg
-            this.showMessage = true
-          }
-        })
-        .catch(error => {
-          console.error(error.message)
-        })
-    },
-    setFile(value) {
-      this.coverFile = value
-    },
     setTitle(title) {
       if (title.length > 50) {
-        this.videoPost.title = title.substring(0, 50)
-        this.videoPost.description = title
+        this.form.title = title.substring(0, 50)
+        this.form.description = title
       } else {
-        this.videoPost.title = title
+        this.form.title = title
       }
     },
-    /* uploadVideoCover() {
-      if (this.coverFile === null) {
-        this.message = '请先选择视频封面,然后上传!'
-        this.showMessage = true
-        return
-      }
-
-      if (this.videoPost.videoFileId === null) {
-        this.message = '等待视频上传完成后再上传封面!'
-        this.showMessage = true
-        return
-      }
-
-      const formData = new FormData()
-      formData.append('videoFileId', this.videoPost.videoFileId)
-      formData.append('file', this.coverFile)
-      fetch(`//api.reghao.cn/api/file/upload/video/cover`, {
-        headers: {
-          'Authorization': 'Bearer ' + this.$store.getters.token
-        },
-        method: 'POST',
-        credentials: 'include',
-        body: formData
-      }).then(response => response.json())
-        .then(json => {
-          if (json.code === 0) {
-            this.message = '封面已上传'
-            this.showMessage = true
-            this.videoPost.coverFileId = json.data.imageFileId
-            this.videoPost.imageUrl = json.data.imageUrl
-          } else {
-            this.message = '上传失败,请重试!' + json.message
-            this.showMessage = true
-          }
-        })
-        .catch(e => {
-          return null
-        })
-    },*/
     getVideoCategory() {
       videoCategory()
         .then(res => {
           if (res.code === 0) {
             for (let i = 0; i < res.data.length; i++) {
               const name = res.data[i].name
-              this.category.push(name)
+              //this.category.push(name)
               this.categoryMap.Set(name, res.data[i])
             }
           } else {
-            console.error(res.msg)
+            this.$notify(
+              {
+                title: '提示',
+                message: res.msg,
+                type: 'warning',
+                duration: 3000
+              }
+            )
           }
-        })
-        .catch(error => {
-          console.error(error.message)
+        }).catch(error => {
+          this.$notify(
+            {
+              title: '提示',
+              message: error.message,
+              type: 'error',
+              duration: 3000
+            }
+          )
         })
     },
     getCategory(name) {
       // 重置子分区,清除前一次选择分区时留下的缓存
       this.childCategory = []
       this.currentCategory = this.categoryMap.Get(name)
-      this.videoPost.categoryId = this.currentCategory.id
+      this.form.categoryId = this.currentCategory.id
 
       const c = this.currentCategory.children
       if (c) {
@@ -431,39 +340,128 @@ export default {
       const c = this.currentCategory.children
       for (let i = 0; i < c.length; i++) {
         if (c[i].name === name) {
-          this.videoPost.categoryId = c[i].id
+          this.form.categoryId = c[i].id
         }
       }
     },
-    setVideoScope(scope) {
-      if (scope === '所有人可见') {
-        this.videoPost.scope = 1
-      } else if (scope === '验证码可见') {
-        this.videoPost.scope = 2
-      } else if (scope === 'VIP 可见') {
-        this.videoPost.scope = 3
-      } else if (scope === '仅自己可见') {
-        this.videoPost.scope = 4
-      }
-    },
-    getVideoIdWrapper() {
-      getVideoId(this.videoPost)
+    getOssPolicyWrapper() {
+      getOssPolicy(this.videoPost)
         .then(res => {
           if (res.code === 0) {
-            console.log(res.data)
-            this.videoPost.videoFileId = res.data.videoFileId
-            this.videoUrlId = res.data.videoUrlId
+            this.form.videoObjectName = res.data.videoObjectName
           } else {
-            this.message = res.msg
-            this.showMessage = true
+            this.$notify(
+              {
+                title: '提示',
+                message: res.msg,
+                type: 'warning',
+                duration: 3000
+              }
+            )
           }
-        })
-        .catch(error => {
-          console.error(error.message)
+        }).catch(error => {
+        this.$notify(
+          {
+            title: '提示',
+            message: error.message,
+            type: 'warning',
+            duration: 3000
+          }
+        )
         })
     },
     onSubmit() {
-      console.log('submit!')
+      if (!this.form.videoObjectName) {
+        this.$notify(
+          {
+            title: '提示',
+            message: '你还没有上传视频',
+            type: 'warning',
+            duration: 3000
+          }
+        )
+        return
+      }
+
+      if (!this.form.coverFileId) {
+        this.$notify(
+          {
+            title: '提示',
+            message: '你还没有上传视频封面',
+            type: 'warning',
+            duration: 3000
+          }
+        )
+        return
+      }
+
+      if (this.form.title === '' || this.form.categoryId === -1) {
+        this.$notify(
+          {
+            title: '提示',
+            message: '分区和稿件标题不能为空',
+            type: 'warning',
+            duration: 3000
+          }
+        )
+        return
+      }
+
+       if (this.form.scope === null) {
+        this.$notify(
+          {
+            title: '提示',
+            message: '稿件可见范围不能为空',
+            type: 'warning',
+            duration: 3000
+          }
+        )
+        return
+      }
+
+      /*if (this.form.tags.length === 0 || this.videoPost.tags.length > 10) {
+        this.$notify(
+          {
+            title: '提示',
+            message: '标签最少 1 个, 最多 10 个',
+            type: 'warning',
+            duration: 3000
+          }
+        )
+        return
+      }*/
+      submitVideo(this.form)
+        .then(res => {
+          if (res.code === 0) {
+            this.$notify(
+              {
+                title: '提示',
+                message: '投稿成功,等待审核通过后其他人就可以看到你的视频了',
+                type: 'warning',
+                duration: 3000
+              }
+            )
+            this.$router.push('/user/post/list')
+          } else {
+            this.$notify(
+              {
+                title: '提示',
+                message: res.msg,
+                type: 'warning',
+                duration: 3000
+              }
+            )
+          }
+        }).catch(error => {
+        this.$notify(
+          {
+            title: '提示',
+            message: error.message,
+            type: 'warning',
+            duration: 3000
+          }
+        )
+        })
     }
   }
 }

+ 323 - 0
src/components/upload/UploadFile.vue

@@ -0,0 +1,323 @@
+<template>
+  <el-col>
+    <el-card>
+      <el-row>
+        <h2>上传文件</h2>
+        <uploader
+          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>
+      </el-row>
+      <el-divider />
+      <el-row>
+        <h2>文件信息</h2>
+      </el-row>
+      <el-form ref="form" :model="form" label-width="80px">
+        <el-form-item label="标题">
+          <el-input v-model="form.title" style="width: 70%; padding-right: 2px" placeholder="标题不能超过 50 个字符" />
+        </el-form-item>
+        <el-form-item label="描述">
+          <el-input v-model="form.description" type="textarea" style="width: 70%; padding-right: 2px" />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="onSubmit">立即投稿</el-button>
+          <el-button>取消</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+  </el-col>
+</template>
+
+<script>
+import { submitVideo } from '@/api/video'
+import { getOssPolicy } from '@/api/file'
+import { hashFile } from '@/utils/hash'
+
+export default {
+  name: 'UploadFile',
+  data() {
+    return {
+      /***********************************************************************/
+      options: {
+        target: '//oss.reghao.cn/?multipart',
+        chunkSize: 1024 * 1024 * 10, // 10MiB
+        //target: '//oss.reghao.cn/',
+        //chunkSize: 1024 * 1024 * 1024 * 5, // 5GiB
+        fileParameterName: 'file',
+        testChunks: true,
+        // 服务器分片校验函数,秒传及断点续传基础
+        checkChunkUploadedByResponse: function (chunk, message) {
+          const resData = JSON.parse(message);
+          let objMessage = resData.data;
+          if (objMessage.skipUpload) {
+            return true;
+          } else {
+            return (objMessage.uploaded || []).indexOf(chunk.offset + 1) >= 0
+          }
+        },
+        query: (file, chunk) => {
+          return {
+            key: this.form.objectName,
+            acl: '',
+            policy: '',
+            'content-type': '',
+            'success_action_redirect': '',
+            'x-amz-meta-uuid': '',
+            'x-amz-server-side-encryption': '',
+            'X-amz-credential': '',
+            'x-amz-algorithm': '',
+            'x-amz-date': '',
+            'x-amz-meta-tag': '',
+            'x-amz-signature': ''
+          }
+        },
+        headers: {
+        }
+      },
+      attrs: {
+        accept: 'video/*'
+      },
+      /***********************************************************************/
+      form: {
+        title: null,
+        description: null,
+      }
+    }
+  },
+  created() {
+    this.getOssPolicyWrapper()
+  },
+  methods: {
+    /***********************************************************************/
+    onFileAdded(file) {
+      this.setTitle(file.file.name)
+       file.pause()
+      hashFile(file.file).then(res => {
+        const formData = new FormData()
+        formData.append('filename', file.file.name)
+        formData.append('size', file.file.size)
+        formData.append('sha256sum', res.sha256sum)
+        fetch(`//oss.reghao.cn/?create`, {
+          /*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 {
+              file.uniqueIdentifier = res.sha256sum
+              file.resume()
+            }
+          }).catch(e => {
+            return null
+          })
+      })
+    },
+    onFileProgress(rootFile, file, chunk) {
+    },
+    onFileSuccess(rootFile, file, response, chunk) {
+      const res = JSON.parse(response)
+      if (res.code === 0) {
+        console.log(res)
+        this.$notify(
+          {
+            title: '提示',
+            message: '文件已上传',
+            type: 'warning',
+            duration: 3000
+          }
+        )
+      }
+    },
+    onFileError(rootFile, file, response, chunk) {
+      this.$notify(
+        {
+          title: '提示',
+          message: '文件上传错误',
+          type: 'warning',
+          duration: 3000
+        }
+      )
+    },
+    /***********************************************************************/
+    setTitle(title) {
+      if (title.length > 50) {
+        this.form.title = title.substring(0, 50)
+        this.form.description = title
+      } else {
+        this.form.title = title
+      }
+    },
+    getOssPolicyWrapper() {
+      getOssPolicy(this.videoPost)
+        .then(res => {
+          if (res.code === 0) {
+            this.form.objectName = res.data.objectName
+          } else {
+            this.$notify(
+              {
+                title: '提示',
+                message: res.msg,
+                type: 'warning',
+                duration: 3000
+              }
+            )
+          }
+        }).catch(error => {
+        this.$notify(
+          {
+            title: '提示',
+            message: error.message,
+            type: 'warning',
+            duration: 3000
+          }
+        )
+      })
+    },
+    onSubmit() {
+      if (!this.form.objectName) {
+        this.$notify(
+          {
+            title: '提示',
+            message: '你还没有上传视频',
+            type: 'warning',
+            duration: 3000
+          }
+        )
+        return
+      }
+
+      if (!this.form.coverFileId) {
+        this.$notify(
+          {
+            title: '提示',
+            message: '你还没有上传视频封面',
+            type: 'warning',
+            duration: 3000
+          }
+        )
+        return
+      }
+
+      if (this.form.title === '' || this.form.categoryId === -1) {
+        this.$notify(
+          {
+            title: '提示',
+            message: '分区和稿件标题不能为空',
+            type: 'warning',
+            duration: 3000
+          }
+        )
+        return
+      }
+
+       if (this.form.scope === null) {
+        this.$notify(
+          {
+            title: '提示',
+            message: '稿件可见范围不能为空',
+            type: 'warning',
+            duration: 3000
+          }
+        )
+        return
+      }
+      submitVideo(this.form)
+        .then(res => {
+          if (res.code === 0) {
+            this.$notify(
+              {
+                title: '提示',
+                message: '投稿成功,等待审核通过后其他人就可以看到你的视频了',
+                type: 'warning',
+                duration: 3000
+              }
+            )
+            this.$router.push('/user/post/list')
+          } else {
+            this.$notify(
+              {
+                title: '提示',
+                message: res.msg,
+                type: 'warning',
+                duration: 3000
+              }
+            )
+          }
+        }).catch(error => {
+        this.$notify(
+          {
+            title: '提示',
+            message: error.message,
+            type: 'warning',
+            duration: 3000
+          }
+        )
+        })
+    }
+  }
+}
+</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;
+}
+
+.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>

+ 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
+    }))
+}

+ 5 - 0
src/views/post/PostList.vue

@@ -21,6 +21,8 @@
 </template>
 
 <script>
+import { userVideoList } from '@/api/video'
+
 export default {
   name: 'PostList',
   data() {
@@ -33,6 +35,9 @@ export default {
     }
   },
   created() {
+    userVideoList(1, 10000).then(res => {
+      console.log(res)
+    })
   },
   methods: {
   }

+ 5 - 1
src/views/post/PostPublish.vue

@@ -27,6 +27,9 @@
           <el-tab-pane label="文章" name="fourth">
             <publish-article v-if="activeName === 'fourth'" />
           </el-tab-pane>
+          <el-tab-pane label="文件" name="fifth">
+            <upload-file v-if="activeName === 'fifth'" />
+          </el-tab-pane>
         </el-tabs>
       </el-col>
     </el-row>
@@ -38,10 +41,11 @@ import PublishVideo from '@/components/upload/PublishVideo'
 import PublishAudio from '@/components/upload/PublishAudio'
 import PublishImage from '@/components/upload/PublishImage'
 import PublishArticle from '@/components/upload/PublishArticle'
+import UploadFile from '@/components/upload/UploadFile'
 
 export default {
   name: 'Publish',
-  components: { PublishVideo, PublishAudio, PublishImage, PublishArticle },
+  components: { PublishVideo, PublishAudio, PublishImage, PublishArticle, UploadFile },
   data() {
     return {
       navList: [