跳转到内容

文件分片上传

<template>
    <div id="global-uploader">
        <!-- 上传 -->
        <uploader
                ref="uploader"
                :options="options"
                :autoStart="false"
                @file-added="onFileAdded"
                @file-success="onFileSuccess"
                @file-progress="onFileProgress"
                @file-error="onFileError"
                class="uploader-app">
            <uploader-unsupport></uploader-unsupport>

            <div :class="{'video-upload': isResources}">
                <uploader-btn id="global-uploader-btn" ref="uploadBtn" :class="{'video-upload-btn': isResources}">
                    <img src="@/assets/portfolio.png" class="global-btn-img" v-if="!isResources">
                    <img src="@/assets/videoUpload.png" class="global-btn-img" v-if="isResources">
                    <span v-if="!isResources">点击上传附件</span>
                    <span v-if="isResources">点击上传视频</span>
                </uploader-btn>
                <div class="video-upload-desc" v-if="isResources">
                    视频格式为mp4,大小不大于2GB
                </div>
            </div>

            <!-- <div v-for="(item, index) in myFileListWatch" :key="item.id" class="simple-uploader-cont" v-if="!isResources"> -->
            <div v-for="(item, index) in myFileList" :key="item.id" class="simple-uploader-cont" v-if="!isResources">
                <div class="simple-uploader-btn">
                    <div class="simple-uploader-item">
                        <div class="simple-uploader-fileName">
                            {{ item.name }}
                        </div>
                        <div @click="deleteUpload(item, index)">
                            <i class="el-icon-close"></i>
                        </div>
                    </div>
                    <div class="simple-uploader-progress">
                        <el-progress :percentage="parseFloat((item._prevUploadedSize / item.size * 100).toFixed(2))" :format="((percentage) => {format(percentage, index)})" color="#5EAF8A"></el-progress>
                    </div>
                </div>
                <div class="simple-uploader-form">
                    <el-form label-position="top" :inline="true" class="demo-form-inline">
                        <el-form-item label="积分:" v-if="isAppendixPrice">
                            <el-input v-model="item.integral" placeholder="积分" size="mini" class="uploader-form-input"></el-input>
                        </el-form-item>
                        <el-form-item label="文件格式:" v-if="isWireframe">
                            <el-select v-model="item.fileFormat" placeholder="文件格式" size="mini" class="uploader-form-input">
                                <el-option :label="item.name" :value="item.id" v-for="item in fileFormatList" :key="item.id"></el-option>
                            </el-select>
                        </el-form-item>
                        <el-form-item label="版本:" v-if="item.needVersion == 1">
                            <el-input v-model="item.fileFormatVersion" placeholder="版本" size="mini" class="uploader-form-input"></el-input>
                        </el-form-item>
                        <el-form-item label="渲染器:" v-if="item.renderList && (item.renderList.length != 0)">
                            <el-select v-model="item.renderId" placeholder="渲染器" size="mini" class="uploader-form-input">
                                <el-option :label="item.name" :value="item.id" v-for="item in item.renderList" :key="item.id"></el-option>
                            </el-select>
                        </el-form-item>
                        <el-form-item label="渲染器版本:" v-if="item.needRenderVersion== 1">
                            <el-input v-model="item.renderVersion" placeholder="渲染器版本" size="mini" class="uploader-form-input"></el-input>
                        </el-form-item>
                    </el-form>
                </div>
            </div>
            <div v-if="isResources" class="base-input">
                <div class="resources-title">
                    上传合集:
                </div>
                <div class="resources-cont">
                    <el-select v-model="videoId" size="mini" placeholder="请选择上传合集" style="width:193px;margin-right:10px">
                        <el-option :label="item.title" :value="item.id" v-for="(item, index) in videoCollectionList" :key="item.id"></el-option>
                    </el-select>
                    <video-collection :subNavOneList="subNavOneList"/>
                </div>
                <div class="resources-title" v-if="videoId">
                    视频所属分类:
                </div>
                <div class="resources-cont" v-if="videoId">
                    <div v-for="(item, index) in videoClassifyList" :key="item">
                        <el-input v-model="videoClassifyList[index]" size="mini" disabled style="width:138px;margin-right:20px"></el-input>
                    </div>
                </div>
            </div>
            <div class="resources-title" v-if="isResources && (myFileList.length != 0)">
                上传列表:
            </div>
            <div class="simple-uploader-videoCont" v-for="(item, index) in myFileList" :key="item.id" v-if="isResources">
                <div class="simple-uploader-videoBtn">
                    <div class="simple-uploader-videoDesc">
                        <div class="simple-uploader-videoTitle">
                            <div>
                                {{ item.name }}
                            </div>
                            <div>
                                {{ item.size | computerByte }}
                            </div>
                            <div>
                                取消上传
                            </div>
                        </div>
                        <div class="simple-uploader-videoProgress">
                            <el-progress :percentage="parseFloat((item._prevUploadedSize / item.size * 100).toFixed(2))" :format="((percentage) => {format(percentage, index)})" color="#5EAF8A"></el-progress>
                        </div>
                        <div class="resources-cont">
                            <div class="video-title">
                                标题:
                            </div>
                            <div class="video-input">
                                <el-input v-model="item.videoTitle" size="mini" style="width:906px;" placeholder="请输入标题"></el-input>
                            </div>
                        </div>
                        <div class="resources-cont">
                            <div class="video-title">
                                简介:
                            </div>
                            <div class="video-form-desc">
                                <el-input v-model="item.remark" type="textarea" size="mini" style="width:906px;" placeholder="请输入简介" maxlength="5000" show-word-limit class="video-form-desc-textarea"></el-input>
                            </div>
                        </div>
                    </div>
                    <div>
                        <i class="el-icon-close delete-icon-video" @click="deleteUpload(item, index)"></i>
                    </div>
                </div>
            </div>
        </uploader>
    </div>
</template>

<script>

import SparkMD5 from "spark-md5"
import { getToken } from "@/utils/auth"
import { fileCusFilePartCancel, fileCusFilePartMerge, modelCusModelFileDelKey, modelCusFileFormatMenuCollect, modelCusModelListMine, modelCusModelTypeCollectModelId } from "@/api/upload"
import { mapGetters } from "vuex"
import VideoCollection from '@/components/VideoCollection/index'

export default {
    props: {
        fileType: {
            type: String
        },
        isWireframe: {
            type: Boolean
        },
        isAppendixPrice: {
            type: Boolean
        },
        isResources: {
            type: Boolean
        },
        isClean: {
            type: Boolean
        },
        subNavOneList: {
            type: Array
        }
    },
    components: {
        VideoCollection
    },
    filters: {
        computerByte(value) {
            var byte = (value / 1024).toFixed(2)
            if (byte >= 1024) {
                var M = (byte / 1024).toFixed(2)
                if (M >= 1024) {
                    var T = (M / 1024).toFixed(2)
                    if (T >= 1024) {
                        var P = (T / 1024).toFixed(2)
                        return P + 'P'
                    }
                    return T + 'T'
                }
                return M + 'M'
            }
            return byte + 'kb'
        }
    },
    data() {
        return {
            hasFileObjs: [],
            options: {
                // collapseFlag: true,
                target: "/app/file/cus/file/part/upload",
                chunkSize: "5242880",
                fileParameterName: "file",
                maxChunkRetries: 3,
                testChunks: true,   //是否开启服务器分片校验
                headers: { Authorization: `Bearer ${getToken()}` },
                // 服务器分片校验函数,秒传及断点续传基础
                checkChunkUploadedByResponse: (chunk, message) => {
                    var res = JSON.parse(message).data
                    if (res.hasFile) {
                        this.hasFileObjs.push(res.identifier)
                        return true
                    }
                    return (res.uploaded || []).indexOf(chunk.offset + 1) >= 0
                },
                query: {
                    bucketName: "xrcymodel"
                },
                simultaneousUploads: 5,
            },
            collapse: false,
            files: [],
            handleContextOptions: {},
            myFileList: [],
            mergeList: [],
            fileFormatList: [],
            checkForm: false,
            videoCollectionList: [],
            videoId: null,
            videoClassifyList: []
        }
    },
    computed: {
        ...mapGetters([
            "videoCollectionId"
        ]),
        //Uploader实例
        uploader() {
            return this.$refs.uploader.uploader
        },
        myFileListWatch(){
            return JSON.parse(JSON.stringify(this.myFileList))
        }
    },
    watch: {
        videoCollectionId() {
            modelCusModelListMine({mainTabId: 6}).then(response => {
                this.videoCollectionList = response.data
                this.videoId = this.videoCollectionId
            })
        },
        videoId(newId, oldId) {
            if ((newId != oldId) && newId) {
                modelCusModelTypeCollectModelId({modelId: this.videoId}).then(response => {
                    this.videoClassifyList = response.data
                })
                this.$store.commit('upload/SET_VIDEOCOLLECTIONID', this.videoId)
            }
            if (!newId) {
                this.videoClassifyList = []
            }
        },
        myFileListWatch: {
            handler(newFileList, oldFileList) {
                if (!this.isResources) {
                    for (var i = 0, len = this.myFileList.length; i < len; i++) {
                        if (oldFileList.length != 0 && newFileList.length != 0 && oldFileList[i] && (newFileList[i].fileFormat != oldFileList[i].fileFormat)) {
                            this.$set(this.myFileList[i], 'fileFormatVersion', null)
                            this.$set(this.myFileList[i], 'renderId', null)
                            this.$set(this.myFileList[i], 'renderVersion', null)
                        }
                        if (this.myFileList[i].fileFormat) {
                            this.myFileList[i].needRenderVersion = 0
                            for (var j = 0, jlen = this.fileFormatList.length; j < jlen; j++) {
                                if (this.myFileList[i].fileFormat == this.fileFormatList[j].id) {
                                    this.myFileList[i].renderList = this.fileFormatList[j].vo.render
                                    this.myFileList[i].needVersion = this.fileFormatList[j].needVersion
                                    if (this.fileFormatList[j].vo.render.length != 0 && this.myFileList[i].renderId) {
                                        for (var n = 0, nlen = this.fileFormatList[j].vo.render.length; n < nlen; n++) {
                                            if (this.myFileList[i].renderId == this.fileFormatList[j].vo.render[n].id) {
                                                this.myFileList[i].needRenderVersion = this.fileFormatList[j].vo.render[n].needVersion
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            },
            deep: true
        },
        isClean() {
            this.checkForm = true
            for (var w = 0; w < this.$refs.uploader.fileList.length; w++) {
                this.$refs.uploader.fileList[w].cancel()
                w--
            }
            this.checkForm = false
        },
        isResources() {
            document.getElementById('global-uploader-btn').getElementsByTagName('input')[0].accept = this.fileType
        },
    },
    mounted() {
        this.$nextTick(() => {
            document.getElementById('global-uploader-btn').getElementsByTagName('input')[0].accept = this.fileType
        })
        modelCusFileFormatMenuCollect().then(response => {
            this.fileFormatList = response.data
        })
        modelCusModelListMine({mainTabId: 6}).then(response => {
            this.videoCollectionList = response.data
        })
    },
    methods: {
        deleteUpload(file, index) {
            var fileKey = this.getFileKeyByProcessFile(file)
            file.cancel()
            if (file.size == file._prevUploadedSize) {
                modelCusModelFileDelKey({fileKey: fileKey}).then(response => {
                    this.$message({ message: '成功删除', type: 'success' })
                })
            } else {
                fileCusFilePartCancel({ fileKey, offline: false }).then(() => {
                    this.$message({ message: '成功删除', type: 'success' })
                })
            }
            this.myFileList.splice(index, 1)
            for (var j = 0, jlen = this.mergeList.length; j < jlen; j++) {
                if (this.mergeList[j].fileKey == fileKey) {
                    this.mergeList.splice(j, 1)
                    break
                }
            }
        },
        format(percentage, index) {
            var percentages = document.getElementsByClassName('el-progress__text')
            if (percentage == 100) {
                this.$nextTick(() => {
                    percentages[index].innerHTML = '合并中'
                })
                var myFileFileKey = this.getFileKeyByProcessFile(this.myFileList[index])
                for (var n = 0, nlen = this.mergeList.length; n < nlen; n++) {
                    if (this.mergeList[n].fileKey == myFileFileKey) {
                        this.$nextTick(() => {
                            percentages[index].innerHTML = '完成'
                        })
                        break
                    }
                }
            } else {
                this.$nextTick(() => {
                    percentages[index].innerHTML = `${percentage}%`
                })
            }
        },
        onFileAdded(file) {
            if (this.checkForm) {
                return
            }
            this.computeMD5(file)
        },
        onFileProgress(rootFile, file, chunk) {
            if (this.checkForm) {
                return
            }
            for (var i = 0, len = this.$refs.uploader.fileList.length; i < len; i++) {
                this.myFileList.splice(i, 1, this.$refs.uploader.fileList[i])
            }
            console.log(`上传中 ${file.name},chunk:${chunk.startByte / 1024 / 1024} ~ ${chunk.endByte / 1024 / 1024}`)
        },
        getFileKeyByProcessFile(file) {
            var md5 = file.uniqueIdentifier
            var extName = file.name.substring(file.name.lastIndexOf("."))
            return md5 + extName
        },
        onFileSuccess(rootFile, file, response, chunk) {
            var res = JSON.parse(response)

            if (res.code == 200) {
                if (this.checkForm) {
                    return
                }
                var fileKey = this.getFileKeyByProcessFile(file)
                for (var j = 0, jlen = this.mergeList.length; j < jlen; j++) {
                    if (this.mergeList[j].fileKey == fileKey) {
                        return
                    }
                }
                fileCusFilePartMerge({ fileKey, offline: false }).then(response => {
                    if (response.code == 200) {
                        this.mergeList.push({fileKey: response.data})
                        this.$message({ message: '上传成功', type: 'success' })
                        if (this.isResources) {
                            var url = URL.createObjectURL(file.file)
                            var audioElement = new Audio(url)
                            audioElement.addEventListener("loadedmetadata", (_event) => {
                                var duration = audioElement.duration
                                this.mergeList[this.mergeList.length - 1].duration = audioElement.duration
                                for (var i = 0, len = this.myFileList.length; i < len; i++) {
                                    if (this.getFileKeyByProcessFile(this.myFileList[i]) == response.data) {
                                        this.myFileList[i].duration = duration
                                        break
                                    }
                                }
                            })
                        }
                        file.hasUploaded = true
                        for (var m = 0, mlen = this.myFileList.length; m < mlen; m++) {
                            this.myFileList[m].uploadSuccess = false
                            for (var n = 0, nlen = this.mergeList.length; n < nlen; n++) {
                                var myFileFileKey = this.getFileKeyByProcessFile(this.myFileList[m])
                                if (this.mergeList[n].fileKey == myFileFileKey) {
                                    this.myFileList[m].uploadSuccess = true
                                    this.myFileList[m].key = response.data
                                    break
                                }
                            }
                        }
                    } else {
                        this.$message({ message: response.message, type: "error" })
                    }
                })
            } else {
                this.$message({ message: response.message, type: "error" })
                return
            }
        },
        onFileError(rootFile, file, response, chunk) {
            this.$message({
                message: response,
                type: 'error'
            })
        },
        computeMD5(file) {
            for (var j = 0, jlen = this.$refs.uploader.fileList.length - 1; j < jlen; j++) {
                if (this.$refs.uploader.fileList[j].name == this.$refs.uploader.fileList[this.$refs.uploader.fileList.length - 1].name) {
                    this.$message({ message: '同名文件已存在', type: "error" })
                    this.$refs.uploader.fileList[this.$refs.uploader.fileList.length - 1].cancel()
                    break
                }
            }
            for (var i = 0, len = this.$refs.uploader.fileList.length; i < len; i++) {
                this.myFileList.splice(i, 1, this.$refs.uploader.fileList[i])
            }
            var fileReader = new FileReader()
            var time = new Date().getTime()
            var blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
            var currentChunk = 0
            const chunkSize = 10 * 1024 * 1000
            var chunks = Math.ceil(file.size / chunkSize)
            var spark = new SparkMD5.ArrayBuffer()

            file.pause()

            loadNext()

            fileReader.onload = (e => {
                spark.append(e.target.result)

                if (currentChunk < chunks) {
                    currentChunk++
                    loadNext()
                } else {
                    var md5 = spark.end()
                    this.computeMD5Success(md5, file)
                    console.log(`MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${file.size} 用时:${new Date().getTime() - time} ms`)
                }
            })

            fileReader.onerror = () => {
                this.error(`文件${file.name}读取出错,请检查该文件`)
                file.cancel()
            }

            function loadNext() {
                var start = currentChunk * chunkSize
                var end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize

                fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end))
            }
        },
        computeMD5Success(md5, file) {
            file.uniqueIdentifier = md5
            file.resume()
        },
        error(msg) {
            this.$notify({
                title: "错误",
                message: msg,
                type: "error",
                duration: 2000
            });
        }
    }
}
</script>

<style scoped>
#global-uploader {
    width: 152px;
}
#global-uploader-btn {
    width: 152px;
    height: 44px;
    text-align: center;
    line-height: 44px;
    color: #fff;
    background-color: #55A1E8;
    padding: 0;
    border: none;
    font-size: 14px;
    letter-spacing: 1px;
}
.global-btn-img {
    vertical-align: middle;
}
.simple-uploader-cont {
    display: flex;
    width: 1000px;
}
.simple-uploader-btn {
    width: 152px;
    margin-right: 30px;
}
.simple-uploader-item {
    display: flex;
    justify-content: space-between;
    width: 152px;
    margin-top: 40px;
    height: 44px;
    color: #fff;
    background-color: #55A1E8;
    font-size: 12px;
    line-height: 44px;
    text-align: center;
    padding: 0 9px;
}
.simple-uploader-form {
    margin-top: 40px;
}
.simple-uploader-fileName {
    width: 125px;
    white-space: nowrap;
    text-overflow: ellipsis;
    overflow: hidden;
}
.el-icon-close {
    font-size: 18px;
    cursor: pointer;
    vertical-align: middle;
}
.simple-uploader-progress {
    width: 152px;
}
.uploader-form-input {
    width: 140px;
}
.simple-uploader-videoCont {
    width: 1030px;
    margin-top: 18px;
}
.simple-uploader-videoBtn {
    display: flex;
    justify-content: space-between;
    width: 1030px;
    height: 240px;
    background-color: #3F4954;
    margin-bottom: 13px;
    padding: 15px 13px;
}
.delete-icon-video {
    font-size: 28px;
    color: #fff;
    padding-top: 6px;
}
.simple-uploader-videoDesc {
    width: 960px;
    height: 41px;
}
.simple-uploader-videoTitle {
    display: flex;
    justify-content: space-between;
    width: 910px;
    font-size: 14px;
    height: 20px;
    line-height: 20px;
    color: #fff;
    margin-bottom: 4px;
}
.simple-uploader-videoProgress {
    width: 960px;
}
.video-collection-cont {
    display: flex;
    width: 100%;
}
.uploader-video-collection {
    width: 193px;
    margin-right: 13px;
}
.video-collection-input {
    display: flex;
}
.collection-svg {
    font-size: 20px;
    margin-left: 13px;
    cursor: pointer;
    margin-top: 4px;
}
.video-upload {
    width: 1028px;
    height: 147px;
    border: 1px dashed #1890FF;
}
.video-upload-btn {
    margin: 40px 0 15px 438px;
}
.video-upload-desc {
    width: 1028px;
    text-align: center;
    font-size: 12px;
    color: rgba(255, 255, 255, .63);
}
.resources-title {
    font-size: 14px;
    color: #dcdcdc;
    margin-top: 30px;
    margin-bottom: 13px;
}
.resources-cont {
    display: flex;
}
.base-input {
    width: 1080px;
}
.video-title {
    width: 50px;
    font-size: 14px;
    color: #dcdcdc;
    line-height: 67px;
}
.video-input {
    margin-top: 20px;
}
.video-form-desc {
    margin-top: 20px;
}
.video-form-desc-textarea {
    width: 906px;
}
</style>

<style>
.simple-uploader-progress .el-progress__text {
    color: #fff;
    padding-left: 13px;
}
.simple-uploader-progress .el-progress {
    white-space: nowrap;
}
.simple-uploader-videoProgress .el-progress__text {
    color: #fff;
    padding-left: 10px;
}
.simple-uploader-videoProgress .el-progress {
    white-space: nowrap;
}
.simple-uploader-form .el-form-item__label {
    line-height: 0;
    color: #fff;
}
.simple-uploader-form .el-input--mini .el-input__inner {
    background: #3F4953;
    border: none;
    color: #fff;
}
.uploader-video-collection .el-input--mini .el-input__inner {
    background: #3F4953;
    border: none;
    color: #fff;
}
.base-input .el-form-item__label {
    color: #fff !important;
}
.base-input .el-input--mini .el-input__inner {
    background: #3F4953;
    border: none;
    color: #fff;
}
.video-input .el-form-item__label {
    color: #fff !important;
}
.video-input .el-input--mini .el-input__inner {
    background: #223040;
    border: none;
    color: #fff;
}
.video-form-desc .el-form-item__label {
    padding: 0;
    color: #fff;
}
.video-form-desc .el-textarea__inner {
    background-color: #223040;
    border: none;
    color: #fff;
    height: 74px;
}
.video-form-desc .el-textarea .el-input__count {
    color: #fff;
    background-color: #223040;
    letter-spacing: 2px;
}
</style>