| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213 |
- <template>
- <el-card class="box-card upload-card shadow-box">
- <div slot="header" class="clearfix">
- <i class="el-icon-upload2" />
- <span style="margin-left: 8px">{{ uploadConfig.title }}</span>
- <el-tag v-if="uploadStatus" :type="statusTagType" size="mini" style="float: right">
- {{ uploadStatus }}
- </el-tag>
- </div>
- <uploader
- v-if="uploaderOptions"
- ref="uploader"
- :options="uploaderOptions"
- :auto-start="false"
- class="uploader-app"
- @file-added="onFileAdded"
- @file-success="onFileSuccess"
- @file-error="onFileError"
- @file-progress="onFileProgress"
- >
- <uploader-unsupport />
- <uploader-drop>
- <div class="drop-area">
- <i class="el-icon-video-camera" />
- <p>将文件拖到此处,或 <uploader-btn :attrs="uploadConfig.fileAttrs" class="uploader-btn">点击上传</uploader-btn></p>
- <div class="upload-tip">文件最大不超过 10GB</div>
- </div>
- </uploader-drop>
- <uploader-list />
- </uploader>
- </el-card>
- </template>
- <script>
- import SparkMD5 from 'spark-md5'
- import { prepareUpload, checkSample } from '@/api/file'
- import { hashFile } from '@/utils/functions'
- export default {
- name: 'UploaderCard',
- props: {
- // 接收父组件传入的配置对象
- uploadConfig: {
- type: Object,
- required: true
- }
- },
- data() {
- return {
- uploaderOptions: null,
- uploadStatus: '', // 计算Hash, 预校验, 采样校验, 上传中, 已完成, 错误
- videoChannelCode: null
- }
- },
- computed: {
- statusTagType() {
- const map = { '上传中': '', '已完成': 'success', '错误': 'danger', '预校验': 'warning', '采样校验': 'warning' }
- return map[this.uploadStatus] || 'info'
- }
- },
- watch: {
- // 确保在数据传入后初始化
- uploadConfig: {
- immediate: true,
- handler(newVal) {
- if (newVal) {
- this.initUploader(newVal)
- }
- }
- }
- },
- created() {
- },
- methods: {
- initUploader(uploadConfig) {
- this.videoChannelCode = uploadConfig.channelCode
- var ossUrl = uploadConfig.ossUrl
- var token = uploadConfig.token
- this.uploaderOptions = {
- target: ossUrl,
- chunkSize: 1024 * 1024 * 10, // 初始分片大小,后续会被 prepare 接口返回的 splitSize 覆盖
- fileParameterName: 'file',
- testChunks: false,
- processParams: (params, file, chunk) => ({
- identifier: file.uniqueIdentifier,
- chunkSize: params.chunkSize,
- totalChunks: params.totalChunks,
- chunkNumber: params.chunkNumber,
- filename: file.name,
- sha256sum: file.sha256sum
- }),
- query: () => ({ channelCode: this.videoChannelCode }),
- checkChunkUploadedByResponse: (chunk, message) => {
- // 上传文件到 oss 后的响应
- const obj = JSON.parse(message)
- return obj.data.uploaded
- // return (obj.data.uploaded || []).indexOf(chunk.offset + 1) >= 0
- },
- headers: { Authorization: 'Bearer ' + token }
- }
- },
- onFileAdded: async function(file) {
- this.$emit('before-upload', file.name)
- try {
- // 1. 全文件 SHA256 计算
- this.uploadStatus = '计算Hash'
- const hashResult = await hashFile(file.file)
- file.sha256sum = hashResult.sha256sum
- // 2. /upload/prepare 预校验
- this.uploadStatus = '预校验'
- const prepareRes = await prepareUpload({
- channelCode: this.videoChannelCode,
- filename: file.name,
- size: file.size,
- sha256sum: file.sha256sum
- })
- if (prepareRes.code !== 0) throw new Error(prepareRes.msg)
- const { uploadId, splitSize, exist, offset, length } = prepareRes.data
- file.uniqueIdentifier = uploadId
- // 动态调整分片大小(如果后端有要求)
- if (splitSize) this.uploaderOptions.chunkSize = splitSize
- // 3. /upload/check_sample 采样校验
- let isFastUpload = false
- if (exist) {
- this.uploadStatus = '采样校验'
- const sampleMd5 = await this.calculateSampleMd5(file.file, offset, length)
- const checkRes = await checkSample({
- uploadId: uploadId,
- sampleMd5: sampleMd5
- })
- if (checkRes.code === 0 && checkRes.data.fastUpload) {
- isFastUpload = true
- // 1. 直接取消该文件的上传任务(从 uploader-list 中移除)
- file.cancel()
- // 2. 更新全局状态
- this.uploadStatus = '已完成'
- // 2. 延迟一小会儿执行成功回调,让用户看到进度条拉满的快感
- setTimeout(() => {
- this.handleUploadSuccess(file, checkRes.data)
- }, 1000)
- return
- }
- }
- // 4. 如果没能秒传,启动分片上传
- if (!isFastUpload) {
- this.uploadStatus = '上传中'
- file.resume()
- }
- } catch (e) {
- this.uploadStatus = '错误'
- this.$notify.error({ title: '校验失败', message: e.message })
- file.cancel()
- }
- },
- // 核心工具:计算文件指定位置的 MD5
- calculateSampleMd5(file, offset, length) {
- return new Promise((resolve, reject) => {
- const spark = new SparkMD5.ArrayBuffer()
- const reader = new FileReader()
- // 只读取后端要求的片段 [offset, offset + length]
- const blob = file.slice(offset, offset + length)
- reader.onload = (e) => {
- spark.append(e.target.result)
- resolve(spark.end())
- }
- reader.onerror = () => reject('采样读取失败')
- reader.readAsArrayBuffer(blob)
- })
- },
- handleUploadSuccess(file, resData) {
- this.uploadStatus = '已完成'
- this.$emit('success', {
- uploadId: resData.uploadId,
- file: file,
- channelCode: this.videoChannelCode
- })
- },
- onFileSuccess(rootFile, file, response) {
- const res = JSON.parse(response)
- if (res.code === 0) {
- this.handleUploadSuccess(file, res.data)
- } else {
- this.uploadStatus = '错误'
- this.$message.error(res.msg || '上传失败')
- }
- },
- onFileError() {
- this.uploadStatus = '错误'
- },
- onFileProgress() {
- this.uploadStatus = '上传中'
- }
- }
- }
- </script>
|