上传文件

289 阅读1分钟

简单版

<template>
    <div id="app">
        <input type="file" name="file" @change="handleFileChange" />
        <button @click="uploadFile">上传文件</button>
    </div>
</template>

<script>
import axios from 'axios';
export default {
    data() {
        return {
            file: null,
        };
    },
    methods: {
        uploadFile() {
            const form = new FormData();
            form.append('name', 'file');
            form.append('file', this.file);
            axios.post('/uploadFile', form);
        },
        handleFileChange(e) {
            const [file] = e.target.files;
            if (!file) return;
            this.file = file;
        },
    },
};
</script>

拖拽+进度条版

<template>
    <div id="app">
        <div ref="drag" id="drag">
            <input type="file" name="file" @change="handleFileChange" />
        </div>
        <button @click="uploadFile">上传文件</button>
        <!-- network 改成fast 3G 观察效果-->
        <el-progress :text-inside="true" :stroke-width="26" :percentage="uploadProgress"></el-progress>
    </div>
</template>

<script>
import axios from 'axios';
export default {
    name: 'App',
    data() {
        return {
            file: null,
            uploadProgress: 0,
        };
    },
    mounted() {
        this.bindEvent();
    },
    methods: {
        bindEvent() {
            const drag = this.$refs.drag;
            drag.addEventListener('dragover', e => {
                drag.style.borderColor = 'red';
                e.preventDefault();
            });
            drag.addEventListener('dragleave', e => {
                drag.style.borderColor = '#ccc';
                e.preventDefault();
            });
            drag.addEventListener('drop', e => {
                console.log(e.dataTransfer.files);
                const fileList = e.dataTransfer.files;
                drag.style.borderColor = '#ccc';
                this.file = fileList[0];
                e.preventDefault();
            });
        },
        uploadFile() {
            const form = new FormData();
            form.append('name', 'file');
            form.append('file', this.file);
            axios.post('/uploadFile', form, {
                onUploadProgress: progress => {
                    console.log(progress);
                    this.uploadProgress = Number(((progress.loaded / progress.total) * 100).toFixed(2));
                },
            });
        },
        handleFileChange(e) {
            const [file] = e.target.files;
            if (!file) return;
            this.file = file;
        },
    },
};
</script>

<style lang="less">
#drag {
    height: 100px;
    border: 1px dashed #ccc;
    display: flex;
    width: 100%;
    justify-content: center;
    align-items: center;
    margin-bottom: 20px;
}
</style>

判断文件类型

安装hexdump for VSCode 插件可以方便查看图片头信息

image.png 代码越来越多,从这里开始省略重复代码,只提供跟标题相关代码,最后会有完整版

blobToString(blob) {
    console.log('blob', blob);
    return new Promise(resolve => {
        const reader = new FileReader();
        reader.onload = function () {
            console.log('reader.result', reader.result)
            const ret = reader.result
                .split('')
                .map(v => v.charCodeAt())
                .map(v => v.toString(16).toUpperCase())
                .map(v => v.padStart(2, '0'))
                .join(' ');
            resolve(ret);
        };
        reader.readAsBinaryString(blob);
    });
},
async isGif(file) {
    // 前面6个十六进制'47 49 46 38 39 61''47 49 46 38 37 61'
    const ret = await this.blobToString(file.slice(0, 6));
    console.log(ret);
    const isGif = ret === '47 49 46 38 39 61' || ret === '47 49 46 38 37 61';
    return isGif;
},
async isPng(file) {
    const ret = await this.blobToString(file.slice(0, 8));
    console.log(ret);
    const isPng = ret === '89 50 4E 47 0D 0A 1A 0A';
    return isPng;
},
async isJpg(file) {
    const len = file.size;
    const start = await this.blobToString(file.slice(0, 2));
    const tail = await this.blobToString(file.slice(-2, len));
    console.log(start, tail);
    const isjpg = start === 'FF D8' && tail === 'FF D9';
    return isjpg;
},
async isImage(file) {
    return (await this.isGif(file)) || (await this.isPng(file)) || (await this.isJpg(file));
},
async uploadFile() {
    if (!(await this.isImage(this.file))) {
        console.log('文件格式不对');
        return;
    } else {
        console.log('文件格式正确')
    }
    const form = new FormData();
    form.append('name', 'file');
    form.append('file', this.file);
    axios.post('/uploadFile', form, {
        onUploadProgress: progress => {
            console.log(progress);
            this.uploadProgress = Number(((progress.loaded / progress.total) * 100).toFixed(2));
        },
    });
},

计算md5值

web-worker计算md5值
  • npm i --save spark-md5
  • 把安装好的spark-md5中的spark-md5.min.js拷贝到public目录中
  • public目录中新建文件hash.js
// hash.js
self.importScripts('spark-md5.min.js');

self.onmessage = e => {
    // 接受主线程传递的数据
    const {chunks} = e.data;
    const spark = new self.SparkMD5.ArrayBuffer();
    let progress = 0;
    let count = 0;

    const loadNext = index => {
        const reader = new FileReader();
        reader.readAsArrayBuffer(chunks[index].file)
        reader.onload = e => {
            count++;
            spark.append(e.target.result)
            if (count === chunks.length) {
                self.postMessage({
                    progress: 100,
                    hash: spark.end()
                })
            } else {
                progress += 100 / chunks.length;
                self.postMessage({
                    progress
                })
                loadNext(count)
            }
        }
    }
    loadNext(0)
}
<template>
    <div id="app">
        ......
        <div>
            <p>计算hash进度</p>
            <el-progress :text-inside="true" :stroke-width="26" :percentage="hashProgress"></el-progress>
        </div>
    </div>
</template>
<script>
import axios from 'axios';
const CHUNK_SIZE = 0.5 * 1024 * 1024;
export default {
    data() {
        return {
            ......
            hashProgress: 0,
        };
    },
    ......
    methods: {
    
        ......
        
        createFileChunk(file, size = CHUNK_SIZE) {
            const chunks = [];
            let cur = 0;
            while (cur < this.file.size) {
                chunks.push({
                    index: cur,
                    file: file.slice(cur, cur + size),
                });
                cur += size;
            }
            return chunks;
        },
        calculateHashWorker() {
            return new Promise(resolve => {
                this.worker = new Worker('/hash.js');
                this.worker.postMessage({
                    chunks: this.chunks,
                });
                this.worker.onmessage = e => {
                    const { progress, hash } = e.data;
                    this.hashProgress = Number(progress.toFixed(2));
                    if (hash) {
                        resolve(hash);
                    }
                };
            });
        },
        async uploadFile() {
            ......
            this.chunks = this.createFileChunk(this.file);
            const hash = await this.calculateHashWorker();
            console.log('hash', hash);
            const form = new FormData();
            form.append('name', 'file');
            form.append('file', this.file);
            axios.post('/uploadFile', form, {
                onUploadProgress: progress => {
                    this.uploadProgress = Number(((progress.loaded / progress.total) * 100).toFixed(2));
                },
            });
        },
    },
};
</script>
requestIdleCallback计算文件md5
calculateHashIdle() {
    return new Promise(resolve => {
        const spark = new sparkMD5.ArrayBuffer();
        let count = 0;

        const appendToSpark = async file => {
            return new Promise(resolve => {
                const reader = new FileReader();
                reader.readAsArrayBuffer(file)
                reader.onload = e => {
                    spark.append(e.target.result);
                    resolve()
                }
            })
        }
        const workLoop = async deadline => {
            // 空闲时间 且有任务
            while(count<this.chunks.length && deadline.timeRemaining() > 1) {
                await appendToSpark(this.chunks[count].file);
                count++;
                if (count<this.chunks.length) {
                    this.hashProgress = Number((100*count) / this.chunks.length.toFixed(2))
                } else {
                    this.hashProgress = 100;
                    resolve(spark.end())
                }
            }
            window.requestIdleCallback(workLoop)
        }
        window.requestIdleCallback(workLoop)
    })
}

抽样hash
  • 不算全量
  • 布隆过滤器 损失一部分的精度 换取效率
calculateHashSample() {
    return new Promise(resolve => {
        const spark = new sparkMD5.ArrayBuffer();
        const reader = new FileReader();

        const file = this.file;
        const size = file.size;
        const offset = 2 * 1024 * 1024;

        // 第一个区块2M,最后一个区块数据全要
        let chunks = [file.slice(0, offset)];
        // 中间的,取前中后各两个字节
        let cur = offset;
        while (cur < size) {
            if (cur + offset >= size) {
                chunks.push(file.slice(cur, cur + offset));
            } else {
                const mid = cur + offset / 2;
                const end = cur + offset;
                chunks.push(file.slice(cur, cur + 2));
                chunks.push(file.slice(mid, mid + 2));
                chunks.push(file.slice(end - 2, end));
            }
            cur += offset;
        }
        reader.readAsArrayBuffer(new Blob(chunks));
        reader.onload = e => {
            spark.append(e.target.result);
            this.hashProgress = 100;
            resolve(spark.end());
        };
    });
}

切片上传

<template>
    <div class="cube-container" :style="{ width: cubeWidth + 'px' }">
        <div class="cube" v-for="chunk in chunks" :key="chunk.name">
            <div
                :class="{
                    uploading: chunk.progress > 0 && chunk.progress < 100,
                    success: (chunk.progress == 100),
                    error: chunk.progress < 0,
                }"
                :style="{height: chunk.progress + '%'}"
            >
                <i
                    class="el-icon-loading"
                    style="color: #f56c6c"
                    v-if="chunk.progress < 100 && chunk.progress > 0"
                ></i>
            </div>
        </div>
    </div>
</template>
......
data() {
    return {
        file: null,
        hashProgress: 0,
        chunks: [],
        hash: ''
    };
},
computed: {
    uploadProgress() {
        if (!this.file || this.chunks.length) {
            return 0;
        }
        const loaded = this.chunks
            .map(item => {
                item.chunk.size * item.progress;
            })
            .reduce((acc, cur) => acc + cur, 0);
        return Number((loaded * 100) / this.file.size.toFixed(2));
    },
    cubeWidth() {
        return Math.ceil(Math.sqrt(this.chunks.length)) * 16;
    },
},
methods: {
    async uploadFile() {
        const chunks = this.createFileChunk(this.file);
        const hash = await this.calculateHashWorker(chunks);
        this.hash = hash;
        this.chunks = chunks.map((chunk, index) => {
            const name = hash + '-' + index;
            return {
                hash,
                name,
                index,
                chunk: chunk.file,
                progress: 0
            };
        });
        await this.uploadChunks();
    },
    async uploadChunks() {
        const requests = this.chunks
            .map(({ chunk, hash, name }) => {
                // 转成promise
                const form = new FormData();
                form.append('chunk', chunk);
                form.append('hash', hash);
                form.append('name', name);
                return form;
            })
            .map((form, index) => {
                axios.post('/uploadFile', form, {
                    onUploadProgress: progress => {
                        // 不是整体的进度条了,而是每个区块有自己的进度条,整体的进度条需要计算
                        this.chunks[index].progress = Number(((progress.loaded / progress.total) * 100).toFixed(2));
                    },
                });
            });
        // @todo 并发量控制
        await Promise.all(requests);
    },
}
......
<style>
.cube-container {
    .cube {
        width: 14px;
        height: 14px;
        line-height: 12px;
        border: 1px solid #000;
        background: #ccc;
        float: left;
        .success {
            background: green;
        }
        .uploading {
            background: blue;
        }
        .error {
            background: red;
        }
    }
}
</style>

文件合并

mergeRequest() {
    axios.post('/merge', {
        ext: this.file.name.split('.').pop(),
        size: CHUNK_SIZE,
        hash: this.hash
    }).then(res => {
        if (res.code === 200) {
            this.$message({
                type: 'success',
                message: '上传成功'
            })
        }
    })
},

秒传功能 & 断点续传

  • 问一下后端,文件是否上传过,如果没有,是否有存在的切片
  • 把不存在的切片上传,已经存在的切片不上传
async uploadFile() {
    const chunks = this.createFileChunk(this.file);
    const hash = await this.calculateHashWorker(chunks);
    this.hash = hash;

    const {
        data: { uploaded, uploadedList },
    } = axios.post('/checkFile', {
        hash: this.hash,
        ext: this.file.name.split('.').pop(),
    });
    if (uploaded) {
        // 秒传
        return this.$message({
            type: 'success',
            message: '秒传成功',
        });
    }
    this.chunks = chunks.map((chunk, index) => {
        const name = hash + '-' + index;
        return {
            hash,
            name,
            index,
            chunk: chunk.file,
            progress: uploadedList.indexOf(name) > -1 ? 100 : 0
        };
    });
    await this.uploadChunks(uploadedList);
},
async uploadChunks(uploadedList) {
    const requests = this.chunks
        .filter(chunk => uploadedList.indexOf(chunk.name) === -1)
        .map(({ chunk, hash, name, index }) => {
            // 转成promise
            const form = new FormData();
            form.append('chunk', chunk);
            form.append('hash', hash);
            form.append('name', name);
            return {
                form,
                index,
                error: 0
            };
        })
        .map(({form, index}) => {
            axios.post('/uploadFile', form, {
                onUploadProgress: progress => {
                    // 不是整体的进度条了,而是每个区块有自己的进度条,整体的进度条需要计算
                    this.chunks[index].progress = Number(((progress.loaded / progress.total) * 100).toFixed(2));
                },
            });
        });
    // @todo 并发量控制
    await Promise.all(requests);
    await this.mergeRequest();
},

并发数控制

  • 尝试申请tcp链接过多,也会造成卡顿
sendRequest(chunks, limit = 3) {
    return new Promise(resolve => {
        const len = chunks.length;
        let count = 0;
        const start = async() => {
            const task = chunks.shift()
            if (task) {
                const {form,index} = task;
                await axios.post('/uploadFile', form, {
                    onUploadProgress: progress => {
                        this.chunks[index].progress = Number(((progress.loaded / progress.total) * 100).toFixed(2));
                    },
                });
                if (count == len - 1) {
                    resolve()
                } else {
                    count ++
                    start()
                }
            }
        }

        while(limit > 0) {
            start()
            limit -= 1
        }
    })
}

报错重试 & 报错次数限制

  • 报错之后,进度条变红,开始重试
  • 一个切片重试失败三次后,整体全部终止
sendRequest(chunks, limit = 3) {
    return new Promise((resolve, reject) => {
        const len = chunks.length;
        let count = 0;
        let isStop = false;
        const start = async () => {
            if (isStop) return;
            const task = chunks.shift();
            if (task) {
                const { form, index } = task;
                try {
                    await axios.post('/uploadFile', form, {
                        onUploadProgress: progress => {
                            this.chunks[index].progress = Number(
                                ((progress.loaded / progress.total) * 100).toFixed(2)
                            );
                        },
                    });
                    if (count == len - 1) {
                        resolve();
                    } else {
                        count++;
                        start();
                    }
                } catch(e) {
                    this.chunks[index].progress = -1;
                    if(task.error < 3) {
                        task.error++;
                        chunks.unshift(task)
                        start()
                    } else {
                        isStop = true;
                        reject()
                    }
                }
            }
        };

        while (limit > 0) {
            start();
            limit -= 1;
        }
    });
}

完整版

<template>
    <div id="app">
        <div ref="drag" id="drag">
            <input type="file" name="file" @change="handleFileChange" />
        </div>
        <button @click="uploadFile">上传文件</button>
        <!-- network 改成fast 3G 观察效果-->
        <el-progress :text-inside="true" :stroke-width="26" :percentage="uploadProgress"></el-progress>
        <div>
            <p>计算hash进度</p>
            <el-progress :text-inside="true" :stroke-width="26" :percentage="hashProgress"></el-progress>
        </div>
        <div>
            <div class="cube-container" :style="{ width: cubeWidth + 'px' }">
                <div class="cube" v-for="chunk in chunks" :key="chunk.name">
                    <div
                        :class="{
                            uploading: chunk.progress > 0 && chunk.progress < 100,
                            success: chunk.progress == 100,
                            error: chunk.progress < 0,
                        }"
                        :style="{ height: chunk.progress + '%' }"
                    >
                        <i
                            class="el-icon-loading"
                            style="color: #f56c6c"
                            v-if="chunk.progress < 100 && chunk.progress > 0"
                        ></i>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
import axios from 'axios';
import sparkMD5 from 'spark-md5';
const CHUNK_SIZE = 0.5 * 1024 * 1024;
export default {
    data() {
        return {
            file: null,
            // uploadProgress: 0,
            hashProgress: 0,
            chunks: [],
            hash: '',
        };
    },
    mounted() {
        this.bindEvent();
    },
    computed: {
        uploadProgress() {
            if (!this.file || this.chunks.length) {
                return 0;
            }
            const loaded = this.chunks
                .map(item => {
                    item.chunk.size * item.progress;
                })
                .reduce((acc, cur) => acc + cur, 0);
            return Number((loaded * 100) / this.file.size.toFixed(2));
        },
        cubeWidth() {
            return Math.ceil(Math.sqrt(this.chunks.length)) * 16;
        },
    },
    methods: {
        blobToString(blob) {
            return new Promise(resolve => {
                const reader = new FileReader();
                reader.onload = function () {
                    console.log('reader.result', reader.result);
                    const ret = reader.result
                        .split('')
                        .map(v => v.charCodeAt()) // unicode编码
                        .map(v => v.toString(16).toUpperCase()) //十六进制的字符串
                        .map(v => v.padStart(2, '0')) //ES2017 引入了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部补全。
                        .join(' ');
                    resolve(ret);
                };
                reader.readAsBinaryString(blob);
            });
        },
        async isGif(file) {
            const ret = await this.blobToString(file.slice(0, 6));
            console.log(ret);
            const isGif = ret === '47 49 46 38 39 61' || ret === '47 49 46 38 37 61';
            return isGif;
        },
        async isPng(file) {
            const ret = await this.blobToString(file.slice(0, 8));
            console.log(ret);
            const isPng = ret === '89 50 4E 47 0D 0A 1A 0A';
            return isPng;
        },
        async isJpg(file) {
            const len = file.size;
            const start = await this.blobToString(file.slice(0, 2));
            const tail = await this.blobToString(file.slice(-2, len));
            console.log(start, tail);
            const isjpg = start === 'FF D8' && tail === 'FF D9';
            return isjpg;
        },
        async isImage(file) {
            return (await this.isGif(file)) || (await this.isPng(file)) || (await this.isJpg(file));
        },
        bindEvent() {
            const drag = this.$refs.drag;
            drag.addEventListener('dragover', e => {
                drag.style.borderColor = 'red';
                e.preventDefault();
            });
            drag.addEventListener('dragleave', e => {
                drag.style.borderColor = '#ccc';
                e.preventDefault();
            });
            drag.addEventListener('drop', e => {
                console.log(e.dataTransfer.files);
                const fileList = e.dataTransfer.files;
                drag.style.borderColor = '#ccc';
                this.file = fileList[0];
                e.preventDefault();
            });
        },
        createFileChunk(file, size = CHUNK_SIZE) {
            const chunks = [];
            let cur = 0;
            while (cur < this.file.size) {
                chunks.push({
                    index: cur,
                    file: file.slice(cur, cur + size),
                });
                cur += size;
            }
            return chunks;
        },
        calculateHashWorker(chunks) {
            return new Promise(resolve => {
                this.worker = new Worker('/hash.js');
                this.worker.postMessage({
                    chunks,
                });
                this.worker.onmessage = e => {
                    const { progress, hash } = e.data;
                    this.hashProgress = Number(progress.toFixed(2));
                    if (hash) {
                        resolve(hash);
                    }
                };
            });
        },
        calculateHashIdle(chunks) {
            return new Promise(resolve => {
                const spark = new sparkMD5.ArrayBuffer();
                let count = 0;

                const appendToSpark = async file => {
                    return new Promise(resolve => {
                        const reader = new FileReader();
                        reader.readAsArrayBuffer(file);
                        reader.onload = e => {
                            spark.append(e.target.result);
                            resolve();
                        };
                    });
                };
                const workLoop = async deadline => {
                    // 空闲时间 且有任务
                    while (count < chunks.length && deadline.timeRemaining() > 1) {
                        await appendToSpark(chunks[count].file);
                        count++;
                        if (count < chunks.length) {
                            this.hashProgress = Number((100 * count) / chunks.length.toFixed(2));
                        } else {
                            this.hashProgress = 100;
                            resolve(spark.end());
                        }
                    }
                    window.requestIdleCallback(workLoop);
                };
                window.requestIdleCallback(workLoop);
            });
        },
        calculateHashSample() {
            return new Promise(resolve => {
                const spark = new sparkMD5.ArrayBuffer();
                const reader = new FileReader();

                const file = this.file;
                const size = file.size;
                const offset = 2 * 1024 * 1024;

                // 第一个区块2M,最后一个区块数据全要
                let chunks = [file.slice(0, offset)];
                // 中间的,取前中后各两个字节
                let cur = offset;
                while (cur < size) {
                    if (cur + offset >= size) {
                        chunks.push(file.slice(cur, cur + offset));
                    } else {
                        const mid = cur + offset / 2;
                        const end = cur + offset;
                        chunks.push(file.slice(cur, cur + 2));
                        chunks.push(file.slice(mid, mid + 2));
                        chunks.push(file.slice(end - 2, end));
                    }
                    cur += offset;
                }
                reader.readAsArrayBuffer(new Blob(chunks));
                reader.onload = e => {
                    spark.append(e.target.result);
                    this.hashProgress = 100;
                    resolve(spark.end());
                };
            });
        },
        async uploadFile() {
            // if (!(await this.isImage(this.file))) {
            //     console.log('文件格式不对');
            //     return;
            // } else {
            //     console.log('文件格式正确')
            // }
            const chunks = this.createFileChunk(this.file);
            const hash = await this.calculateHashWorker(chunks);
            this.hash = hash;
            // console.log('hash', hash);
            // const hash1 = await this.calculateHashIdle();
            // console.log('hash1', hash1);
            // const hash2 = await this.calculateHashSample();
            // console.log('hash2', hash2);

            const {
                data: { uploaded, uploadedList },
            } = axios.post('/checkFile', {
                hash: this.hash,
                ext: this.file.name.split('.').pop(),
            });
            if (uploaded) {
                // 秒传
                return this.$message({
                    type: 'success',
                    message: '秒传成功',
                });
            }
            this.chunks = chunks.map((chunk, index) => {
                // 切片的名字 hash + index
                const name = hash + '-' + index;
                return {
                    hash,
                    name,
                    index,
                    chunk: chunk.file,
                    progress: uploadedList.indexOf(name) > -1 ? 100 : 0,
                };
            });
            await this.uploadChunks(uploadedList);
        },
        async uploadChunks(uploadedList) {
            const requests = this.chunks
                .filter(chunk => uploadedList.indexOf(chunk.name) === -1)
                .map(({ chunk, hash, name, index }) => {
                    // 转成promise
                    const form = new FormData();
                    form.append('chunk', chunk);
                    form.append('hash', hash);
                    form.append('name', name);
                    return {
                        form,
                        index,
                        error: 0
                    };
                });
            // .map(({form, index}) => {
            //     axios.post('/uploadFile', form, {
            //         onUploadProgress: progress => {
            //             // 不是整体的进度条了,而是每个区块有自己的进度条,整体的进度条需要计算
            //             this.chunks[index].progress = Number(((progress.loaded / progress.total) * 100).toFixed(2));
            //         },
            //     });
            // });
            await this.sendRequest(requests);
            // await Promise.all(requests);
            await this.mergeRequest();
            // const form = new FormData();
            // form.append('name', 'file');
            // form.append('file', this.file);
            // axios.post('/uploadFile', form, {
            //     onUploadProgress: progress => {
            //         this.uploadProgress = Number(((progress.loaded / progress.total) * 100).toFixed(2));
            //     },
            // });
        },
        sendRequest(chunks, limit = 3) {
            return new Promise((resolve, reject) => {
                const len = chunks.length;
                let count = 0;
                let isStop = false;
                const start = async () => {
                    if (isStop) return;
                    const task = chunks.shift();
                    if (task) {
                        const { form, index } = task;
                        try {
                            await axios.post('/uploadFile', form, {
                                onUploadProgress: progress => {
                                    this.chunks[index].progress = Number(
                                        ((progress.loaded / progress.total) * 100).toFixed(2)
                                    );
                                },
                            });
                            if (count == len - 1) {
                                resolve();
                            } else {
                                count++;
                                start();
                            }
                        } catch(e) {
                            this.chunks[index].progress = -1;
                            if(task.error < 3) {
                                task.error++;
                                chunks.unshift(task)
                                start()
                            } else {
                                isStop = true;
                                reject()
                            }
                        }
                    }
                };

                while (limit > 0) {
                    start();
                    limit -= 1;
                }
            });
        },
        mergeRequest() {
            axios
                .post('/merge', {
                    ext: this.file.name.split('.').pop(),
                    size: CHUNK_SIZE,
                    hash: this.hash,
                })
                .then(res => {
                    if (res.code === 200) {
                        this.$message({
                            type: 'success',
                            message: '上传成功',
                        });
                    }
                });
        },
        handleFileChange(e) {
            const [file] = e.target.files;
            if (!file) return;
            this.file = file;
        },
    },
};
</script>

<style lang="less">
#drag {
    height: 100px;
    border: 1px dashed #ccc;
    display: flex;
    width: 100%;
    justify-content: center;
    align-items: center;
    margin-bottom: 20px;
}
.cube-container {
    .cube {
        width: 14px;
        height: 14px;
        line-height: 12px;
        border: 1px solid #000;
        background: #ccc;
        float: left;
        .success {
            background: green;
        }
        .uploading {
            background: blue;
        }
        .error {
            background: red;
        }
    }
}
</style>