UploaderCard.vue 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. <template>
  2. <el-card class="box-card upload-card shadow-box">
  3. <div slot="header" class="clearfix">
  4. <i class="el-icon-upload2" />
  5. <span style="margin-left: 8px">{{ uploadConfig.title }}</span>
  6. <el-tag v-if="uploadStatus" :type="statusTagType" size="mini" style="float: right">
  7. {{ uploadStatus }}
  8. </el-tag>
  9. </div>
  10. <uploader
  11. v-if="uploaderOptions"
  12. ref="uploader"
  13. :options="uploaderOptions"
  14. :auto-start="false"
  15. class="uploader-app"
  16. @file-added="onFileAdded"
  17. @file-success="onFileSuccess"
  18. @file-error="onFileError"
  19. @file-progress="onFileProgress"
  20. >
  21. <uploader-unsupport />
  22. <uploader-drop>
  23. <div class="drop-area">
  24. <i class="el-icon-video-camera" />
  25. <p>将文件拖到此处,或 <uploader-btn :attrs="uploadConfig.fileAttrs" class="uploader-btn">点击上传</uploader-btn></p>
  26. <div class="upload-tip">文件最大不超过 10GB</div>
  27. </div>
  28. </uploader-drop>
  29. <uploader-list />
  30. </uploader>
  31. </el-card>
  32. </template>
  33. <script>
  34. import SparkMD5 from 'spark-md5'
  35. import { prepareUpload, checkSample } from '@/api/file'
  36. import { hashFile } from '@/utils/functions'
  37. export default {
  38. name: 'UploaderCard',
  39. props: {
  40. // 接收父组件传入的配置对象
  41. uploadConfig: {
  42. type: Object,
  43. required: true
  44. }
  45. },
  46. data() {
  47. return {
  48. uploaderOptions: null,
  49. uploadStatus: '', // 计算Hash, 预校验, 采样校验, 上传中, 已完成, 错误
  50. videoChannelCode: null
  51. }
  52. },
  53. computed: {
  54. statusTagType() {
  55. const map = { '上传中': '', '已完成': 'success', '错误': 'danger', '预校验': 'warning', '采样校验': 'warning' }
  56. return map[this.uploadStatus] || 'info'
  57. }
  58. },
  59. watch: {
  60. // 确保在数据传入后初始化
  61. uploadConfig: {
  62. immediate: true,
  63. handler(newVal) {
  64. if (newVal) {
  65. this.initUploader(newVal)
  66. }
  67. }
  68. }
  69. },
  70. created() {
  71. },
  72. methods: {
  73. initUploader(uploadConfig) {
  74. this.videoChannelCode = uploadConfig.channelCode
  75. var ossUrl = uploadConfig.ossUrl
  76. var token = uploadConfig.token
  77. this.uploaderOptions = {
  78. target: ossUrl,
  79. chunkSize: 1024 * 1024 * 10, // 初始分片大小,后续会被 prepare 接口返回的 splitSize 覆盖
  80. fileParameterName: 'file',
  81. testChunks: false,
  82. processParams: (params, file, chunk) => ({
  83. identifier: file.uniqueIdentifier,
  84. chunkSize: params.chunkSize,
  85. totalChunks: params.totalChunks,
  86. chunkNumber: params.chunkNumber,
  87. filename: file.name,
  88. sha256sum: file.sha256sum
  89. }),
  90. query: () => ({ channelCode: this.videoChannelCode }),
  91. checkChunkUploadedByResponse: (chunk, message) => {
  92. // 上传文件到 oss 后的响应
  93. const obj = JSON.parse(message)
  94. return obj.data.uploaded
  95. // return (obj.data.uploaded || []).indexOf(chunk.offset + 1) >= 0
  96. },
  97. headers: { Authorization: 'Bearer ' + token }
  98. }
  99. },
  100. onFileAdded: async function(file) {
  101. this.$emit('before-upload', file.name)
  102. try {
  103. // 1. 全文件 SHA256 计算
  104. this.uploadStatus = '计算Hash'
  105. const hashResult = await hashFile(file.file)
  106. file.sha256sum = hashResult.sha256sum
  107. // 2. /upload/prepare 预校验
  108. this.uploadStatus = '预校验'
  109. const prepareRes = await prepareUpload({
  110. channelCode: this.videoChannelCode,
  111. filename: file.name,
  112. size: file.size,
  113. sha256sum: file.sha256sum
  114. })
  115. if (prepareRes.code !== 0) throw new Error(prepareRes.msg)
  116. const { uploadId, splitSize, exist, offset, length } = prepareRes.data
  117. file.uniqueIdentifier = uploadId
  118. // 动态调整分片大小(如果后端有要求)
  119. if (splitSize) this.uploaderOptions.chunkSize = splitSize
  120. // 3. /upload/check_sample 采样校验
  121. let isFastUpload = false
  122. if (exist) {
  123. this.uploadStatus = '采样校验'
  124. const sampleMd5 = await this.calculateSampleMd5(file.file, offset, length)
  125. const checkRes = await checkSample({
  126. uploadId: uploadId,
  127. sampleMd5: sampleMd5
  128. })
  129. if (checkRes.code === 0 && checkRes.data.fastUpload) {
  130. isFastUpload = true
  131. // 1. 直接取消该文件的上传任务(从 uploader-list 中移除)
  132. file.cancel()
  133. // 2. 更新全局状态
  134. this.uploadStatus = '已完成'
  135. // 2. 延迟一小会儿执行成功回调,让用户看到进度条拉满的快感
  136. setTimeout(() => {
  137. this.handleUploadSuccess(file, checkRes.data)
  138. }, 1000)
  139. return
  140. }
  141. }
  142. // 4. 如果没能秒传,启动分片上传
  143. if (!isFastUpload) {
  144. this.uploadStatus = '上传中'
  145. file.resume()
  146. }
  147. } catch (e) {
  148. this.uploadStatus = '错误'
  149. this.$notify.error({ title: '校验失败', message: e.message })
  150. file.cancel()
  151. }
  152. },
  153. // 核心工具:计算文件指定位置的 MD5
  154. calculateSampleMd5(file, offset, length) {
  155. return new Promise((resolve, reject) => {
  156. const spark = new SparkMD5.ArrayBuffer()
  157. const reader = new FileReader()
  158. // 只读取后端要求的片段 [offset, offset + length]
  159. const blob = file.slice(offset, offset + length)
  160. reader.onload = (e) => {
  161. spark.append(e.target.result)
  162. resolve(spark.end())
  163. }
  164. reader.onerror = () => reject('采样读取失败')
  165. reader.readAsArrayBuffer(blob)
  166. })
  167. },
  168. handleUploadSuccess(file, resData) {
  169. this.uploadStatus = '已完成'
  170. this.$emit('success', {
  171. uploadId: resData.uploadId,
  172. file: file,
  173. channelCode: this.videoChannelCode
  174. })
  175. },
  176. onFileSuccess(rootFile, file, response) {
  177. const res = JSON.parse(response)
  178. if (res.code === 0) {
  179. this.handleUploadSuccess(file, res.data)
  180. } else {
  181. this.uploadStatus = '错误'
  182. this.$message.error(res.msg || '上传失败')
  183. }
  184. },
  185. onFileError() {
  186. this.uploadStatus = '错误'
  187. },
  188. onFileProgress() {
  189. this.uploadStatus = '上传中'
  190. }
  191. }
  192. }
  193. </script>