<template>
    <div class="artfess-upload">
        <div @click.stop="handleDargUpload" class="artfess-upload-content">
            <div
                v-if="drag"
                class="artfess-upload-drap"
                :class="disabled ? 'disable-drap' : ''"
                @dragover.prevent="onDragover"
                @drop.prevent="onDrop"
                @dragleave.prevent="dragover = false"
            >
                <slot v-if="this.$slots.default" name="default"></slot>
                <div v-else>
                    <div class="upload-icon">
                        <i class="el-icon-upload"></i>
                    </div>
                    <span>将文件拖到此处，或<em>点击上传</em></span>
                </div>
            </div>
            <template v-else>
                <slot v-if="this.$slots.default" name="default"></slot>
                <el-button
                    v-else
                    :disabled="disabled"
                    icon="el-icon-upload"
                    type="primary"
                    @click.stop="handleUpload"
                    >点击上传</el-button
                >
            </template>
            <input
                ref="input"
                type="file"
                class="upload-input"
                :accept="accept"
                :multiple="isMultiple"
                :directory="directory"
                :webkitdirectory="directory"
                @change="handleFileChange"
            />
        </div>
        <slot name="tip"></slot>
        <div class="upload-list" :style="`height: ${listHeight}`">
            <el-scrollbar class="editScroll">
                <div
                    class="upload-list-item"
                    v-for="(value, key) in fileListData"
                    :key="key"
                    @click="preview(value, key)"
                >
                    <div class="file-image">
                        <img :src="bindImg(value.name)" />
                    </div>
                    <div class="file-info">
                        <div class="file-name-con">
                            <span class="file-name">{{ value.name }}</span>
                            <span class="file-op">
                                <template v-if="value.state == 'success'">
                                    <i
                                        class="
                                            icon-success
                                            el-icon-circle-check
                                        "
                                    ></i>
                                    <i
                                        v-if="!disabled"
                                        class="icon-danger el-icon-circle-close"
                                        @click.stop.prevent="removeFile(key)"
                                    ></i>
                                </template>
                                <template
                                    v-if="
                                        value.state !== 'success' && !disabled
                                    "
                                >
                                    <i
                                        class="icon-play el-icon-video-play"
                                        v-if="value.state == 'pause'"
                                        @click.stop.prevent="proceed(key)"
                                    ></i>
                                    <i
                                        class="icon-pause el-icon-video-pause"
                                        v-if="value.state == 'pending'"
                                        @click.stop.prevent="pause(key)"
                                    ></i>
                                    <i
                                        class="
                                            icon-error
                                            el-icon-warning-outline
                                        "
                                        v-if="value.state == 'error'"
                                        @click.stop.prevent="pause(key)"
                                    ></i>
                                </template>
                            </span>
                        </div>
                        <el-progress
                            v-if="
                                value.percentage == -1 ||
                                value.percentage == 100
                                    ? false
                                    : true
                            "
                            :percentage="value.percentage"
                        ></el-progress>
                    </div>
                </div>
            </el-scrollbar>
        </div>
    </div>
</template>

<script>
import utils from '@/hotent-ui-util'
import {createFileChunk, calculateFileHash} from './utils'
import {MAX_REQUEST_NUM, MAX_RETRY_NUM, fileType} from './config'
import {VERIFY_FILE_API} from './api'
import {ajax4Upload, ajax} from './requset.js'
export default {
    props: {
        url: {
            type: String,
            default: '',
        },
        // 多文件
        isMultiple: {
            type: Boolean,
            default: false,
        },
        // 切片大小 单位M
        chunkSize: {
            type: Number,
            default: 10,
        },
        // 选择文件夹
        directory: {
            type: Boolean,
            default: false,
        },
        // 拖拽
        drag: {
            type: Boolean,
            default: false,
        },
        // 禁用
        disabled: {
            type: Boolean,
            default: false,
        },
        // 文件数量
        limit: Number,
        // 文件类型
        accept: {
            type: String,
            default: '',
        },
        // 文件类型不匹配时提示语
        notAcceptMessage: String,
        // 上传之前的钩子
        beforeUpload: Function,
        // 文件数量超出limit限制时提示语
        onExceedMessage: String,
        // 上传时附带的业务参数
        excessData: Object,
        // 文件数据
        filesData: Object,
        // 列表高度
        listHeight: String,
    },
    data() {
        return {
            // 上传地址
            tempIndex: 1,
            uploadUrl: window.context.uc + '/file/v1/uploadZone',
            enableSlice: true,
            // 文件数据
            fileListData: {
                // 列表中所有的文件
                // key: { // 组件内部文件唯一标识  ，建议设置为负数，防止和内部产生的 uid 冲突
                //     id: '', // 已上传文件id，可用户预览
                //     uid: uid, // 同key
                //     name: 'xx.png', // 文件名
                //     status: 'success', // 状态 pending success pause error
                //     chunk: [], // 分片数据，无需设置
                //     size: '', // 文件大小，无需设置
                //     percentage: -1, // 上传进度，无需设置
                //     requestList: [], // 请求的列表用于暂停操作，无需设置，
                // },
            },
            // 当前上传数据
            uploadListData: {},
            // 是否可拖拽触发
            dragover: false,
            // uploadTime: 0,
        }
    },
    watch: {
        filesData: {
            immediate: true,
            handler(fileList) {
                let obj = {}
                Object.keys(fileList).forEach((key) => {
                    obj[key] = {}
                    obj[key].uid =
                        fileList[key].uid || Date.now() + this.tempIndex++
                    obj[key].name = fileList[key].name || obj[key].uid
                    obj[key].percentage = -1
                    obj[key].state = fileList[key].state || 'success'
                    return obj
                })
                this.fileListData = obj
            },
        },
    },
    computed: {
        // 切片大小
        chunkSizeFull() {
            return this.chunkSize * 1024 * 1024 //为size加上单位M
        },
        headers() {
            const currentUser = JSON.parse(
                sessionStorage.getItem('currentUser')
            )
            return {Authorization: `Bearer ${currentUser.token}`}
        },
    },
    methods: {
        handleUpload() {
            if (this.disabled) {
                return
            }
            this.$refs.input.click()
        },
        handleDargUpload() {
            if (this.disabled) {
                return
            }
            this.$refs.input.click()
        },
        onDragover(e) {
            e.preventDefault()
            if (!this.disabled) {
                this.dragover = true
            }
        },
        async onDrop(e) {
            e.preventDefault()
            if (this.disabled) return
            this.dragover = false
            // 拖拽未开启文件夹模式
            let files = []
            // 拖拽开启文件夹模式
            let directoryEntryList = []
            let fileList = []
       
            // 允许上传文件夹
            if (this.directory) {
                if (e.dataTransfer.items && e.dataTransfer.items.length) {
                    let items = Array.from(e.dataTransfer.items)
                    for (let i = 0; i < items.length; i++) {
                        let item = items[i]
                        let entry = null
                        //兼容不同内核的浏览器
                        if (item.webkitGetAsEntry) {
                            entry = item.webkitGetAsEntry()
                        } else if (item.getAsEntry) {
                            entry = item.getAsEntry()
                        } else {
                            this.$message.warning('浏览器不支持拖拽上传')
                            return
                        }
                        directoryEntryList.push(entry)
                        console.log(entry, directoryEntryList)
                    }
                    if (directoryEntryList.length) {
                        for (
                            let index = 0;
                            index < directoryEntryList.length;
                            index++
                        ) {
                            let dirEntry = directoryEntryList[index]
                            if (dirEntry) {
                                //获取文件夹目录
                                let FileTree = await this.getFileTree(dirEntry)
                                // 拿到目录下的所有文件
                                if (Array.isArray(FileTree)) {
                                    //展平文件夹
                                    this.flattenArray(FileTree, fileList)
                                } else {
                                    //方便后续处理，单文件时也包装成数组
                                    fileList.push(FileTree)
                                }
                            }
                        }
                    }
                    this.formatFilesData(fileList)
                }
            } else {
                if (this.isMultiple) {
                    files = Array.from(e.dataTransfer.files)
                } else {
                    files = Array.from([e.dataTransfer.files[0]])
                }
            }

            this.formatFilesData(files)
        },
        async getFileTree(item) {
            let that = this
            var path = item.fullPath || ''
            let dir = new Array()
            if (item.isFile) {
                let resFile = await this.fileSync(item)
                if (path.split('/').length > 2) {
                    resFile.path = path
                }
                return resFile
                // item为文件夹时
            } else if (item.isDirectory) {
                var dirReader = item.createReader()
                let entries = await that.readEntriesSync(dirReader)
                for (let i = 0; i < entries.length; i++) {
                    let proItem = await that.getFileTree(entries[i])
                    dir.push(proItem)
                }
                return dir
            }
        },
        // 读取文件
        fileSync(item) {
            return new Promise((resolve) => {
                item.file((res) => {
                    resolve(res)
                })
            })
        },
        // 读取文件夹
        readEntriesSync(dirReader) {
            return new Promise((resolve) => {
                dirReader.readEntries((res) => {
                    resolve(res)
                })
            })
        },
        // 拍平数组
        flattenArray(array, result) {
            for (let i = 0; i < array.length; i++) {
                if (Array.isArray(array[i])) {
                    this.flattenArray(array[i], result)
                } else {
                    result.push(array[i])
                }
            }
        },
        // 文件数据过滤
        formatFilesData(files) {
            // 限制数量
            if (
                this.limit &&
                Object.keys(this.fileListData).length + files.length >
                    this.limit
            ) {
                if (this.onExceedMessage) {
                    this.$message.warning(this.onExceedMessage)
                } else {
                    this.$message.warning(`最多上传${this.limit}个文件!`)
                }
                return
            }
            let newFiles
            // 限制类型
            if (this.accept) {
                let acceptFiles = this.checkAccept(files)
                if (files.length !== acceptFiles.length) {
                    if (this.notAcceptMessage) {
                        this.$message.warning(this.notAcceptMessage)
                    } else {
                        this.$message.warning(`上传文件类型不匹配`)
                    }
                }
                newFiles = Array.from(acceptFiles)
            } else {
                newFiles = Array.from(files)
            }
            if (newFiles.length) {
                this.uploadListData = this.createChunk(newFiles)
                if (!this.beforeUpload) {
                    this.startUpload()
                    return
                }
                // 上传前钩子 返回true 继续上传请求，false 停止上传
                let before = this.beforeUpload(this.uploadListData)
                if (before) {
                    // 通过了在执行上传
                    this.startUpload()
                }
            }
        },
        // 文件类型验证
        checkAccept(files) {
            return [].slice.call(files).filter((file) => {
                const {type, name} = file
                const extension =
                    name.indexOf('.') > -1 ? `.${name.split('.').pop()}` : ''
                const baseType = type.replace(/\/.*$/, '')
                return this.accept
                    .split(',')
                    .map((type) => type.trim())
                    .filter((type) => type)
                    .some((acceptedType) => {
                        if (/\..+$/.test(acceptedType)) {
                            return extension === acceptedType
                        }
                        if (/\/\*$/.test(acceptedType)) {
                            return (
                                baseType === acceptedType.replace(/\/\*$/, '')
                            )
                        }
                        if (/^[^\/]+\/[^\/]+$/.test(acceptedType)) {
                            return type === acceptedType
                        }
                        return false
                    })
            })
        },
        // 将当次上传数据和列表数据合并，开始上传
        startUpload() {
            Object.keys(this.uploadListData).forEach((key) => {
                this.$set(this.fileListData, key, this.uploadListData[key])
            })
            console.log(this.fileListData, 'this.fileListData')
            this.verifyAndupload()
        },
        // input 触发文件上传
        handleFileChange(e) {
            // 切片 放入data对象
            const files = e.target.files
            if (!this.disabled) {
                this.formatFilesData(files)
            }
        },
        //将多文件分装进data，安照分片大小切割
        createChunk(files = []) {
            console.log(files, 'file开始切片')
            let filesObj = files.reduce((pre, cur, index, ary) => {
                let key = Date.now() + this.tempIndex++
                if (this.fileListData[key]) return
                pre[key] = {}
                pre[key].uid = key
                pre[key].requestList = []
                pre[key].size = cur.size
                pre[key].name = cur.name
                pre[key].type = cur.type
                pre[key].percentage = -1
                pre[key].state = 'pending'
                pre[key].folder = cur.webkitRelativePath || cur.path || ''
                // 单文件切割
                pre[key].chunk = createFileChunk(cur, this.chunkSizeFull)

                return pre
            }, {})
            return filesObj
        },
        // 切片上传
        verifyAndupload() {
            Object.keys(this.uploadListData).forEach((key) => {
                // 单文件size
                let totalSize = this.fileListData[key].size
                // 文件生成 Hash
                calculateFileHash(this.fileListData[key].chunk).then((hash) => {
                    // 判断后端是或已经存在该文件
                    this.verifyHash(totalSize, hash).then((res) => {
                        // console.log(res)
                        const response = JSON.parse(res)
                        const {uploaded, uploadedChunkList} = response

                        if (uploaded) {
                            // 秒传
                            this.secondUpload(this.fileListData[key], uploaded)
                        } else {
                            // 标记已经完成上传的
                            this.fileListData[key].chunk.forEach((chunk) => {
                                chunk.fileHash = hash
                                uploadedChunkList.forEach((uploadedChunk) => {
                                    if (
                                        uploadedChunk.zoneName ===
                                        chunk.chunkName
                                    ) {
                                        this.markAsSuccess(chunk)
                                    }
                                })
                            })
                            if (uploadedChunkList.length) {
                                // 计算文件上传进度百分比
                                this.calcProcessInSliceMode(key)
                            } else {
                                this.fileListData[key].percentage = 0
                            }
                            //开始上传切片
                            this.uploadChunks(this.fileListData[key].chunk, key)
                        }
                    })
                })
            })
        },
        // 秒传
        secondUpload(file, uploaded) {
            file.chunk.forEach((fileChunk) => {
                this.markAsSuccess(fileChunk)
            })
            file.percentage = 100
            file.id = uploaded
            file.state = 'success'
            // 成功回调
            let filesData = utils.deepClone(this.fileListData)
            let successFile = utils.deepClone(file)
            this.$emit('success', filesData, successFile)
        },
        // 切片标记为上传完成
        markAsSuccess(fileChunk) {
            fileChunk.status = 'SUCCESS'
            const chunkSize = fileChunk.chunk.size
            fileChunk.progress = {
                percentage: 100,
                loaded: chunkSize,
                total: chunkSize,
            }
        },
        // 标记ready
        markAsReady(fileChunk) {
            fileChunk.status = 'READY'
            if (fileChunk.progress) {
                fileChunk.progress.loaded = 0
                fileChunk.progress.percentage = 0
            }
        },
        // 文件上传与否和进度验证
        verifyHash(fileSize, contentHash) {
            const formData = new FormData()
            formData.append('fileSize', fileSize)
            formData.append('md5Value', contentHash)
            return ajax({
                url: VERIFY_FILE_API,
                data: formData,
                headers: this.headers,
            })
        },
        // 上传切片
        uploadChunks(fileChunkList, fileKey) {
            // const startTime = new Date().getTime()
            // 限制 request 数量
            this.requestWithLimit(fileChunkList, fileKey)
                .then(() => {
                    // const endTime = new Date().getTime()
                    // this.uploadTime =
                    //     parseInt(String((endTime - startTime) / 10)) / 100
                })
                .catch(() => {
                    // 有部分请求失败，将请求停掉
                    this.pause(fileKey, 'error')
                })
        },
        // 点击文件
        preview(value, key) {
            if (value.id || value.percentage == 100) {
                let filesData = utils.deepClone(this.fileListData)
                let file = utils.deepClone(this.fileListData[key])
                this.$emit('preview', filesData, file)
            }
        },
        // 删除文件
        removeFile(key) {
            if (this.disabled) {
                return
            }
            let filesData = utils.deepClone(this.fileListData)
            let removeFile = utils.deepClone(this.fileListData[key])
            this.$emit('remove', filesData, removeFile)
            this.$delete(this.fileListData, key)
        },
        // 取消并清空所有正在上传的切片
        pause(key, error) {
            this.fileListData[key].requestList.forEach((xhr) => xhr?.abort())
            this.fileListData[key].requestList = []
            if (error) {
                this.fileListData[key].state = 'error'
                let filesData = utils.deepClone(this.fileListData)
                let errorFile = utils.deepClone(this.fileListData[key])
                this.$emit('error', filesData, errorFile)
            } else {
                this.fileListData[key].state = 'pause'
            }
        },
        // 继续上传
        proceed(key) {
            this.fileListData[key].state = 'pending'
            this.uploadChunks(this.fileListData[key].chunk, key)
            this.calcProcessInSliceMode(key)
        },
        // 限制请求并发数
        requestWithLimit(
            fileChunkList,
            fileKey,
            max = MAX_REQUEST_NUM,
            retry = MAX_RETRY_NUM
        ) {
            return new Promise((resolve, reject) => {
                const requestNum = fileChunkList.filter((fileChunk) => {
                    return (
                        fileChunk.status === 'ERROR' ||
                        fileChunk.status === 'READY'
                    )
                }).length

                // 请求成功数量
                let counter = 0

                // 记录文件上传失败的次数
                const retryArr = []
                const request = () => {
                    // max 限制了最大并发数
                    while (counter < requestNum && max > 0) {
                        max--

                        // 等待或者error
                        const fileChunk = fileChunkList.find((chunk) => {
                            return (
                                chunk.status === 'ERROR' ||
                                chunk.status === 'READY'
                            )
                        })

                        if (!fileChunk) {
                            return
                        }

                        const formData = fileChunk.toFormData(this.excessData)
                        fileChunk.status = 'UPLOADING'

                        ajax4Upload({
                            method: 'POST',
                            url: this.url ? this.url : this.uploadUrl,
                            data: formData,
                            headers: this.headers,
                            onProgress: this.onProgressHandler.bind(
                                this,
                                fileChunk,
                                fileKey
                            ),
                            requestList: this.fileListData[fileKey].requestList,
                        })
                            .then((res) => {
                                console.log(res)
                                if (res.flag && res.id) {
                                    this.fileListData[fileKey].state = 'success'
                                    this.fileListData[fileKey].id = res.id
                                    let filesData = utils.deepClone(
                                        this.fileListData
                                    )
                                    let successFile = utils.deepClone(
                                        this.fileListData[fileKey]
                                    )
                                    this.$emit(
                                        'success',
                                        filesData,
                                        successFile
                                    )
                                }
                                fileChunk.status = 'SUCCESS'

                                // 释放通道
                                max++
                                counter++
                                if (counter === requestNum) {
                                    resolve()
                                } else {
                                    request()
                                }
                            })
                            .catch((e) => {
                                console.log(e)
                                if (e === 'Cancel') {
                                    this.markAsReady(fileChunk)
                                    this.calcProcessInSliceMode(fileKey)
                                } else {
                                    fileChunk.status = 'ERROR'
                                    // 触发重试机制
                                    if (
                                        typeof retryArr[fileChunk.index] !==
                                        'number'
                                    ) {
                                        retryArr[fileChunk.index] = 0
                                    }

                                    // 次数累加
                                    retryArr[fileChunk.index]++

                                    // 一个请求报错超过最大重试次数
                                    if (retryArr[fileChunk.index] >= retry) {
                                        return reject()
                                    }

                                    // 清空进度条
                                    fileChunk.progress = {}
                                    // 释放当前占用的通道，但是counter不累加
                                    max++
                                    request()
                                }
                            })
                    }
                }
                request()
            })
        },
        //  请求返回进度计算
        onProgressHandler(chunkFile, fileKey, e) {
            // 百分比先绑定到单独的一个文件上
            chunkFile.progress = {
                percentage: parseInt(String((e.loaded / e.total) * 100)),
                loaded: e.loaded,
                total: e.total,
            }
            this.calcProcessInSliceMode(fileKey)
        },
        // 计算一个总的百分比
        calcProcessInSliceMode(fileKey) {
            let loadedSum = 0
            let file = this.fileListData[fileKey]
            file.chunk.forEach(({progress = null}) => {
                if (progress) {
                    loadedSum += progress.loaded
                }
            })
            let percentage = parseInt(
                ((loadedSum / file.size) * 100).toFixed(2)
            )
            // 进度条 只有大于才更新,遇到取消或失败时不更新，避免进度条倒退
            if (file.percentage <= percentage) {
                file.percentage = percentage > 100 ? 100 : percentage
            }
            if (percentage >= 100) {
                this.fileListData[fileKey].state = 'success'
            }
            this.$forceUpdate()
        },
        // 根据文件名后缀匹配文件类型图片
        bindImg(name) {
            const extension = // 扩展名
                name.indexOf('.') > -1 ? `.${name.split('.').pop()}` : ''
            if (name) {
                if (fileType.imageType.includes(extension)) {
                    return require('@/assets/img/fileType/img.png')
                } else if (fileType.excelType.includes(extension)) {
                    return require('@/assets/img/fileType/excel.png')
                } else if (fileType.wordType.includes(extension)) {
                    return require('@/assets/img/fileType/word.png')
                } else if (fileType.pptType.includes(extension)) {
                    return require('@/assets/img/fileType/ppt.png')
                } else if (fileType.pdfType.includes(extension)) {
                    return require('@/assets/img/fileType/pdf.png')
                } else if (fileType.textType.includes(extension)) {
                    return require('@/assets/img/fileType/text.png')
                } else if (fileType.compressType.includes(extension)) {
                    return require('@/assets/img/fileType/zip.png')
                } else if (fileType.videoType.includes(extension)) {
                    return require('@/assets/img/fileType/video.png')
                } else if (fileType.audioType.includes(extension)) {
                    return require('@/assets/img/fileType/audio.png')
                } else {
                    return require('@/assets/img/fileType/unkown.png')
                }
            } else {
                return require('@/assets/img/fileType/unkown.png')
            }
        },
    },
}
</script>

<style lang="scss" scoped>
@import '@/assets/css/element-variables.scss';
.artfess-upload {
    .artfess-upload-content {
        display: inline-block;
        text-align: center;
        cursor: pointer;
        outline: none;
        margin-bottom: 6px;
    }
    .artfess-upload-drap {
        width: 360px;
        height: 160px;
        border: 1px dashed #d9d9d9;
        border-radius: 10px;
        display: flex;
        justify-content: center;
        align-items: center;
        cursor: pointer;
        .upload-icon {
            text-align: center;
            color: $--color-info;
            i {
                font-size: 40px;
            }
        }
        em {
            font-style: normal;
            color: $--color-primary;
        }
    }

    .artfess-upload-drap:hover {
        border: 1px dashed $--color-primary;
    }
    .artfess-upload-drap.disable-drap {
        background-color: #d9d9d9;
    }
    .artfess-upload-drap:hover.disable-drap {
        border: 1px dashed #d9d9d9;
        cursor: not-allowed;
    }
    .upload-input {
        display: none;
    }
    .upload-list {
        width: 100%;
        box-sizing: border-box;
        margin-top: 6px;
    }
    .upload-list-item {
        display: flex;
        align-items: center;
        margin-bottom: 4px;
    }
    .upload-list-item:hover {
        background-color: #ededee;
        i.icon-success {
            display: none;
        }
        i.icon-danger {
            display: inline-block;
        }
    }
    .upload-list-item:nth-last-of-type() {
        margin-bottom: 0;
    }
    .file-image {
        width: 22px;
        margin-right: 6px;
        img {
            width: 100%;
        }
    }
    .file-info {
        width: calc(100% - 26px);
    }
    .file-name-con {
        display: flex;
        justify-content: space-between;
    }
    .file-name {
        width: calc(100% - 30px);
        white-space: nowrap;
        text-overflow: ellipsis;
        overflow: hidden;
    }
    .file-op {
        width: 20px;
        margin-left: 10px;
        i {
            font-size: 16px;
        }
        i.icon-success {
            color: $--color-success;
        }
        i.icon-danger {
            cursor: pointer;
            color: $--color-danger;
            display: none;
        }
        i.icon-pause {
            cursor: pointer;
            color: $--color-warning;
        }
        i.icon-error {
            color: $--color-danger;
        }
        i.icon-play {
            cursor: pointer;
            color: $--color-success;
        }
    }
    >>> .el-scrollbar__wrap {
        overflow-x: hidden;
        margin-bottom: 0px !important;
    }
    .editScroll {
        height: 100% !important;
        overflow-x: hidden;
    }
}
</style>