文章说明
简单模拟拖拽文件夹和选择文件的进度条效果
<template>
<link rel="stylesheet" href="/style/css/iconfont.css">
<div class="drop-area" @drop="getDropItems" @click="showFilePicker">
<div>
<i class="iconfont icon-upload"/>
<div class="tip-text">
Drop file here or
<em>click to upload</em>
</div>
</div>
</div>
<div class="file-list">
<div v-for="(item, index) in data.fileList" :key="index" class="single-file">
<MyProgress :percentage="item.percentage" :content="item.name"/>
</div>
</div>
</template>
<script>
import {onBeforeMount, reactive} from "vue";
import MyProgress from "@/MyProgress.vue";
import {message} from "@/util";
export default {
name: "App",
components: {MyProgress},
setup() {
const data = reactive({
fileList: [],
isUploading: false,
});
onBeforeMount(() => {
onload = function () {
document.addEventListener("drop", function (e) {
//拖离
e.preventDefault();
});
document.addEventListener("dragleave", function (e) {
//拖后放
e.preventDefault();
});
document.addEventListener("dragenter", function (e) {
//拖进
e.preventDefault();
});
document.addEventListener("dragover", function (e) {
//拖来拖去
e.preventDefault();
});
};
});
function getFileFromEntryRecursively(entry) {
if (entry.isFile) {
data.fileList.push({
name: entry.fullPath.substring(entry.fullPath.lastIndexOf("/") + 1, entry.fullPath.length),
percentage: 0
});
} else {
let reader = entry.createReader();
reader.readEntries((entries) => {
entries.forEach((entry) => {
getFileFromEntryRecursively(entry);
});
});
}
}
function getDropItems(event) {
if (data.isUploading) {
message("正在上传...", "info");
return;
}
data.fileList = [];
data.isUploading = true;
const items = event.dataTransfer.items;
for (let i = 0; i <= items.length - 1; i++) {
const item = items[i];
if (item.kind === "file") {
const reader = new FileReader();
reader.readAsArrayBuffer(item.getAsFile());
console.log(reader)
const entry = item.webkitGetAsEntry();
getFileFromEntryRecursively(entry);
}
}
const timer = setInterval(() => {
upload();
closeTimer(timer);
}, 100);
}
function upload() {
for (let i = 0; i < data.fileList.length; i++) {
data.fileList[i].percentage += 1;
}
}
function closeTimer(timer) {
let isOver = true;
for (let i = 0; i < data.fileList.length; i++) {
if (data.fileList[i].percentage !== 100) {
isOver = false;
break;
}
}
if (isOver) {
clearInterval(timer);
data.isUploading = false;
}
}
const pickerOpts = {
excludeAcceptAllOption: false,
multiple: true,
};
async function showFilePicker() {
if (data.isUploading) {
message("正在上传...", "info");
return;
}
let fileHandle;
try {
fileHandle = await window.showOpenFilePicker(pickerOpts);
data.fileList = [];
data.isUploading = true;
} catch (e) {
if (e.name === 'AbortError' && e.message === 'The user aborted a request.') {
message("用户没有选择文件", "info");
return;
} else {
throw e;
}
}
for (let i = 0; i < fileHandle.length; i++) {
data.fileList.push({
name: fileHandle[i].name,
percentage: 0
});
const arrayBuffer = (await fileHandle[i].getFile()).arrayBuffer();
console.log(arrayBuffer)
let formData = new FormData();
formData.append("file", arrayBuffer);
console.log(formData)
}
const timer = setInterval(() => {
upload();
closeTimer(timer);
}, 100);
}
return {
data,
getDropItems,
showFilePicker,
};
},
};
</script>
<style>
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
.drop-area {
margin: 100px auto 0;
width: 800px;
height: 180px;
border: 1px dashed #dcdfe6;
display: flex;
align-items: center;
justify-content: center;
}
.drop-area:hover {
border-color: #409eff;
cursor: pointer;
}
.icon-upload::before {
display: flex;
justify-content: center;
font-size: 40px;
margin: 10px 0;
}
.tip-text {
color: #606266;
font-size: 14px;
text-align: center;
}
.tip-text em {
color: #409eff;
font-style: normal;
}
.file-list {
margin: 0 auto;
width: 800px;
}
.single-file {
margin: 10px 0;
}
</style>
<template>
<link rel="stylesheet" href="/style/css/iconfont.css">
<div class="progress-container">
<div class="bar">
<div class="percentage" :style="{'width': props.percentage + '%'}">
<span class="text-inside">{{ props.content + " " + props.percentage + "%" }}</span>
</div>
</div>
<div class="tip-content">
<span v-show="props.percentage !== 100">{{ props.percentage + "%" }}</span>
<i class="iconfont icon-over" v-show="props.percentage === 100"/>
</div>
</div>
</template>
<script>
export default {
props: ["percentage", "content"],
setup(props) {
return {
props
}
}
}
</script>
<style scoped>
.progress-container {
display: flex;
height: 30px;
cursor: pointer;
border: 1px dashed #dcdfe6;
padding: 0 10px;
}
.bar {
color: white;
font-weight: 500;
line-height: 30px;
font-size: 14px;
flex: 1;
}
.percentage {
border-radius: 30px;
background-color: #67c23a;
white-space: nowrap;
word-break: break-all;
overflow: hidden;
transition: width 0.2s linear;
}
.text-inside {
padding-right: 10px;
padding-left: 15px;
float: right;
}
.tip-content {
padding: 0 10px;
font-size: 16px;
line-height: 30px;
width: 40px;
}
.icon-over::before {
font-size: 24px;
color: #67c23a;
}
</style>
效果展示
结合后端实现文件上传
package com.boot.controller;
import com.boot.entity.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
/**
* <p>
* 前端控制器
* </p>
*
* @author bbyh
* @since 2023-12-27
*/
@Slf4j
@RestController
@RequestMapping("/fragment-info")
public class FragmentInfoController {
@PostMapping("/upload")
public Result upload(@RequestBody MultipartFile file) {
log.info(file.getOriginalFilename());
return Result.success("文件上传成功", null);
}
}
import {ElMessage} from "element-plus";
import axios from "axios";
const baseUrl = "http://127.0.0.1:8080"
export function message(msg, type) {
ElMessage({
message: msg,
type: type,
center: true,
showClose: true,
})
}
export const postFileRequest = (url, data, onUploadProgress) => {
return axios({
method: 'post',
url: baseUrl + url,
data: data,
onUploadProgress: onUploadProgress,
})
}
<template>
<link rel="stylesheet" href="/style/css/iconfont.css">
<div class="drop-area" @drop="getDropItems" @click="showFilePicker">
<div>
<i class="iconfont icon-upload"/>
<div class="tip-text">
Drop file here or
<em>click to upload</em>
</div>
</div>
</div>
<div class="file-list">
<div v-for="(item, index) in data.fileList" :key="index" class="single-file">
<MyProgress :percentage="item.percentage" :content="item.name"/>
</div>
</div>
</template>
<script>
import {onBeforeMount, reactive} from "vue";
import MyProgress from "@/MyProgress.vue";
import {message, postFileRequest} from "@/util";
export default {
name: "App",
components: {MyProgress},
setup() {
const data = reactive({
fileList: [],
isUploading: false,
});
onBeforeMount(() => {
onload = function () {
document.addEventListener("drop", function (e) {
//拖离
e.preventDefault();
});
document.addEventListener("dragleave", function (e) {
//拖后放
e.preventDefault();
});
document.addEventListener("dragenter", function (e) {
//拖进
e.preventDefault();
});
document.addEventListener("dragover", function (e) {
//拖来拖去
e.preventDefault();
});
};
});
function getFileFromEntryRecursively(entry) {
return new Promise((resolve) => {
if (entry.isFile) {
entry.file((file) => {
data.fileList.push({
name: entry.fullPath.substring(entry.fullPath.lastIndexOf("/") + 1, entry.fullPath.length),
percentage: 0,
file: file
});
resolve();
});
} else {
let reader = entry.createReader();
reader.readEntries((entries) => {
Promise.all(entries.map(entry => getFileFromEntryRecursively(entry))).then(() => {
resolve();
});
});
}
});
}
async function getDropItems(event) {
if (data.isUploading) {
message("正在上传...", "info");
return;
}
data.fileList = [];
data.isUploading = true;
const items = event.dataTransfer.items;
const promises = [];
for (const item of items) {
if (item.kind === "file") {
const entry = item.webkitGetAsEntry();
promises.push(getFileFromEntryRecursively(entry));
}
}
await Promise.all(promises);
upload();
}
function upload() {
for (let i = 0; i < data.fileList.length; i++) {
const onUploadProgress = (progressEvent) => {
data.fileList[i].percentage = parseInt(Number(((progressEvent.loaded / progressEvent.total) * 100)).toFixed(0));
};
const formData = new FormData();
formData.append("file", data.fileList[i].file);
postFileRequest("/fragment-info/upload", formData, onUploadProgress).then((res) => {
if (res.data.code === "200") {
message(res.data.msg, "success");
} else if (res.data.code === "500") {
message(res.data.msg, "error");
}
});
}
}
const pickerOpts = {
excludeAcceptAllOption: false,
multiple: true,
};
async function showFilePicker() {
if (data.isUploading) {
message("正在上传...", "info");
return;
}
let fileHandle;
try {
fileHandle = await window.showOpenFilePicker(pickerOpts);
data.fileList = [];
data.isUploading = true;
} catch (e) {
if (e.name === 'AbortError' && e.message === 'The user aborted a request.') {
message("用户没有选择文件", "info");
return;
} else {
throw e;
}
}
for (let i = 0; i < fileHandle.length; i++) {
const file = await fileHandle[i].getFile();
data.fileList.push({
name: fileHandle[i].name,
percentage: 0,
file: file
});
}
upload();
}
return {
data,
getDropItems,
showFilePicker,
};
},
};
</script>
<style>
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
.drop-area {
margin: 100px auto 0;
width: 800px;
height: 180px;
border: 1px dashed #dcdfe6;
display: flex;
align-items: center;
justify-content: center;
}
.drop-area:hover {
border-color: #409eff;
cursor: pointer;
}
.icon-upload::before {
display: flex;
justify-content: center;
font-size: 40px;
margin: 10px 0;
}
.tip-text {
color: #606266;
font-size: 14px;
text-align: center;
}
.tip-text em {
color: #409eff;
font-style: normal;
}
.file-list {
margin: 0 auto;
width: 800px;
}
.single-file {
margin: 10px 0;
}
</style>
效果展示
spring.servlet.multipart.enabled=true
spring.servlet.multipart.max-file-size=100MB
spring.servlet.multipart.max-request-size=200MB
加上分片的效果
<template>
<link rel="stylesheet" href="/style/css/iconfont.css">
<div class="drop-area" @drop="getDropItems" @click="showFilePicker">
<div>
<i class="iconfont icon-upload"/>
<div class="tip-text">
Drop file here or
<em>click to upload</em>
</div>
</div>
</div>
<div class="file-list">
<div v-for="(item, index) in data.fileList" :key="index" class="single-file" @click="showFragmentInfo(item)">
<MyProgress :percentage="item.percentage" :content="item.name"/>
</div>
</div>
<el-dialog v-model="data.fragmentDialogVisible" title="分片详情查看" width="80%">
<div v-for="(item, index) in data.showFragmentList" :key="index" class="single-file">
<MyProgress :percentage="item.percentage" :content="item.name"/>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="data.fragmentDialogVisible = false">关闭</el-button>
</span>
</template>
</el-dialog>
</template>
<script>
import {onBeforeMount, reactive} from "vue";
import MyProgress from "@/MyProgress.vue";
import {EACH_FILE, message, postFileRequest} from "@/util";
export default {
name: "App",
components: {MyProgress},
setup() {
const data = reactive({
fileList: [],
isUploading: false,
fragmentDialogVisible: false,
showFragmentList: []
});
onBeforeMount(() => {
onload = function () {
document.addEventListener("drop", function (e) {
//拖离
e.preventDefault();
});
document.addEventListener("dragleave", function (e) {
//拖后放
e.preventDefault();
});
document.addEventListener("dragenter", function (e) {
//拖进
e.preventDefault();
});
document.addEventListener("dragover", function (e) {
//拖来拖去
e.preventDefault();
});
};
});
function getFileFromEntryRecursively(entry) {
return new Promise((resolve) => {
if (entry.isFile) {
entry.file((file) => {
data.fileList.push({
name: entry.fullPath.substring(entry.fullPath.lastIndexOf("/") + 1, entry.fullPath.length),
percentage: 0,
file: file,
totalSize: file.size,
totalCompleteSize: 0
});
resolve();
});
} else {
let reader = entry.createReader();
reader.readEntries((entries) => {
Promise.all(entries.map(entry => getFileFromEntryRecursively(entry))).then(() => {
resolve();
});
});
}
});
}
async function getDropItems(event) {
if (data.isUploading) {
message("正在上传...", "info");
return;
}
data.fileList = [];
data.isUploading = true;
const items = event.dataTransfer.items;
const promises = [];
for (const item of items) {
if (item.kind === "file") {
const entry = item.webkitGetAsEntry();
promises.push(getFileFromEntryRecursively(entry));
}
}
await Promise.all(promises);
upload();
}
function upload() {
for (let i = 0; i < data.fileList.length; i++) {
const fragmentCount = Math.floor(data.fileList[i].file.size / EACH_FILE) + 1;
const fragmentList = [];
for (let j = 0; j < fragmentCount; j++) {
fragmentList.push({
id: j,
fragmentFile: data.fileList[i].file.slice(j * EACH_FILE, (j + 1) * EACH_FILE),
completeSize: 0,
name: data.fileList[i].name + "分片" + (j + 1),
percentage: 0,
});
}
data.fileList[i].fragmentList = fragmentList;
for (let j = 0; j < data.fileList[i].fragmentList.length; j++) {
const onUploadProgress = (progressEvent) => {
data.fileList[i].fragmentList[j].completeSize = progressEvent.loaded;
data.fileList[i].fragmentList[j].percentage = parseInt(Number(((progressEvent.loaded / progressEvent.total) * 100)).toFixed(0));
updateTotalPercentage(i);
};
const formData = new FormData();
formData.append("file", data.fileList[i].fragmentList[j].fragmentFile, data.fileList[i].fragmentList[j].name);
postFileRequest("/fragment-info/upload", formData, onUploadProgress).then((res) => {
if (res.data.code === "200") {
message(res.data.msg, "success");
} else if (res.data.code === "500") {
message(res.data.msg, "error");
}
});
}
}
}
function updateTotalPercentage(i) {
let totalCompleteSize = 0;
for (let j = 0; j < data.fileList[i].fragmentList.length; j++) {
totalCompleteSize += data.fileList[i].fragmentList[j].completeSize;
}
data.fileList[i].totalCompleteSize = totalCompleteSize;
data.fileList[i].percentage = parseInt(Number(data.fileList[i].totalCompleteSize / data.fileList[i].totalSize * 100).toFixed(0));
}
const pickerOpts = {
excludeAcceptAllOption: false,
multiple: true,
};
async function showFilePicker() {
if (data.isUploading) {
message("正在上传...", "info");
return;
}
let fileHandle;
try {
fileHandle = await window.showOpenFilePicker(pickerOpts);
data.fileList = [];
data.isUploading = true;
} catch (e) {
if (e.name === 'AbortError' && e.message === 'The user aborted a request.') {
message("用户没有选择文件", "info");
return;
} else {
throw e;
}
}
for (let i = 0; i < fileHandle.length; i++) {
const file = await fileHandle[i].getFile();
data.fileList.push({
name: fileHandle[i].name,
percentage: 0,
file: file,
totalSize: file.size,
totalCompleteSize: 0
});
}
upload();
}
function showFragmentInfo(item) {
data.showFragmentList = item.fragmentList;
data.fragmentDialogVisible = true;
}
return {
data,
getDropItems,
showFilePicker,
showFragmentInfo,
};
},
};
</script>
<style>
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
.drop-area {
margin: 100px auto 0;
width: 800px;
height: 180px;
border: 1px dashed #dcdfe6;
display: flex;
align-items: center;
justify-content: center;
}
.drop-area:hover {
border-color: #409eff;
cursor: pointer;
}
.icon-upload::before {
display: flex;
justify-content: center;
font-size: 40px;
margin: 10px 0;
}
.tip-text {
color: #606266;
font-size: 14px;
text-align: center;
}
.tip-text em {
color: #409eff;
font-style: normal;
}
.file-list {
margin: 0 auto;
width: 800px;
}
.single-file {
margin: 10px 0;
}
</style>
效果展示
加上MD5的校验,实现秒传和分片的效果
CREATE TABLE `file_info` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`file_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '文件名称',
`MD5` char(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '文件的MD5值',
`path` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '文件的路径',
`create_time` datetime NOT NULL COMMENT '文件创建时间',
`delete_state` bit(1) NOT NULL COMMENT '文件删除状态',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
CREATE TABLE `fragment_info` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`fragment_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '分片文件名称',
`fragment_order` int(11) NOT NULL COMMENT '分片文件序号',
`md5` char(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '分片文件的MD5值,采用转为16字节的数字存储',
`path` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '分片文件存储路径',
`create_time` datetime NOT NULL COMMENT '分片文件创建时间',
`delete_state` bit(1) NOT NULL COMMENT '删除状态(0表示未删除,1表示删除)',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 58 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
package com.boot.controller;
import cn.hutool.core.collection.ListUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.boot.entity.FileInfo;
import com.boot.entity.Result;
import com.boot.service.IFileInfoService;
import com.boot.util.FileUtil;
import com.boot.util.GetCurrentTime;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.io.File;
import java.util.List;
import java.util.Map;
/**
* <p>
* 前端控制器
* </p>
*
* @author bbyh
* @since 2023-12-27
*/
@RestController
@RequestMapping("/file-info")
public class FileInfoController {
@Resource
private IFileInfoService fileInfoService;
@PostMapping("/generateFile")
public Result generateFile(@RequestBody Map<String, Object> map) {
String name = (String) map.get("name");
String md5 = (String) map.get("md5");
List<String> fragmentMd5List = ListUtil.toList(map.get("fragmentMd5List").toString());
FileInfo fileInfo = new FileInfo();
fileInfo.setFileName(name);
fileInfo.setMd5(md5);
fileInfo.setPath(FileUtil.ROOT_PATH + md5 + File.separator + name);
fileInfo.setCreateTime(GetCurrentTime.getCurrentTimeBySecond());
fileInfo.setDeleteState(false);
fileInfoService.save(fileInfo);
return Result.success("文件:" + name + "上传成功", null);
}
@GetMapping("/checkMd5")
public Result checkMd5(@RequestParam String md5) {
QueryWrapper<FileInfo> wrapper = new QueryWrapper<>();
wrapper.eq("md5", md5).eq("delete_state", "0");
FileInfo fileInfo = fileInfoService.getOne(wrapper);
if (fileInfo != null) {
return Result.success("MD5已存在", null);
} else {
fileInfoService.remove(wrapper);
return Result.error("MD5不存在", null);
}
}
}
package com.boot.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.boot.entity.FragmentInfo;
import com.boot.entity.Result;
import com.boot.service.IFragmentInfoService;
import com.boot.util.GetCurrentTime;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.File;
import static com.boot.util.FileUtil.FRAGMENT_SPLIT;
import static com.boot.util.FileUtil.ROOT_PATH;
/**
* <p>
* 前端控制器
* </p>
*
* @author bbyh
* @since 2023-12-27
*/
@Slf4j
@RestController
@RequestMapping("/fragment-info")
public class FragmentInfoController {
@Resource
private IFragmentInfoService fragmentInfoService;
@PostMapping("/upload")
public Result upload(@RequestBody MultipartFile file, @RequestParam String md5) {
String originalFilename = file.getOriginalFilename();
assert originalFilename != null;
int lastIndexOf = originalFilename.lastIndexOf(FRAGMENT_SPLIT);
FragmentInfo fragmentInfo = new FragmentInfo();
fragmentInfo.setFragmentName(originalFilename);
fragmentInfo.setFragmentOrder(Integer.parseInt(originalFilename.substring(lastIndexOf + FRAGMENT_SPLIT.length())));
fragmentInfo.setPath(ROOT_PATH + md5 + File.separator + originalFilename);
fragmentInfo.setMd5(md5);
fragmentInfo.setCreateTime(GetCurrentTime.getCurrentTimeBySecond());
fragmentInfo.setDeleteState(false);
fragmentInfoService.save(fragmentInfo);
return Result.success("分片文件:" + originalFilename + "上传成功", null);
}
@GetMapping("/checkMd5")
public Result checkMd5(@RequestParam String md5) {
QueryWrapper<FragmentInfo> wrapper = new QueryWrapper<>();
wrapper.eq("md5", md5).eq("delete_state", "0");
FragmentInfo fragmentInfo = fragmentInfoService.getOne(wrapper);
if (fragmentInfo != null) {
return Result.success("MD5已存在", null);
} else {
fragmentInfoService.remove(wrapper);
return Result.error("MD5不存在", null);
}
}
}
<template>
<link rel="stylesheet" href="/style/css/iconfont.css">
<div class="drop-area" @drop="getDropItems" @click="showFilePicker">
<div>
<i class="iconfont icon-upload"/>
<div class="tip-text">
Drop file here or
<em>click to upload</em>
</div>
</div>
</div>
<div class="file-list">
<div v-for="(item, index) in data.fileList" :key="index" class="single-file" @click="showFragmentInfo(item)">
<MyProgress :percentage="item.percentage" :content="item.name" :transition="item.transition"/>
</div>
</div>
<el-dialog v-model="data.fragmentDialogVisible" title="分片详情查看" width="80%">
<div v-for="(item, index) in data.showFragmentList" :key="index" class="single-file">
<MyProgress :percentage="item.percentage" :content="item.name"/>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="data.fragmentDialogVisible = false">关闭</el-button>
</span>
</template>
</el-dialog>
</template>
<script>
import {onBeforeMount, reactive} from "vue";
import MyProgress from "@/MyProgress.vue";
import {calculateMD5, EACH_FILE, getRequest, message, postFileRequest, postRequest} from "@/util";
export default {
name: "App",
components: {MyProgress},
setup() {
const data = reactive({
fileList: [],
isUploading: false,
fragmentDialogVisible: false,
showFragmentList: []
});
onBeforeMount(() => {
onload = function () {
document.addEventListener("drop", function (e) {
//拖离
e.preventDefault();
});
document.addEventListener("dragleave", function (e) {
//拖后放
e.preventDefault();
});
document.addEventListener("dragenter", function (e) {
//拖进
e.preventDefault();
});
document.addEventListener("dragover", function (e) {
//拖来拖去
e.preventDefault();
});
};
});
function getFileFromEntryRecursively(entry) {
return new Promise((resolve) => {
if (entry.isFile) {
entry.file((file) => {
data.fileList.push({
name: entry.fullPath.substring(entry.fullPath.lastIndexOf("/") + 1, entry.fullPath.length),
percentage: 0,
file: file,
totalSize: file.size,
totalCompleteSize: 0,
isUpload: false
});
resolve();
});
} else {
let reader = entry.createReader();
reader.readEntries((entries) => {
Promise.all(entries.map(entry => getFileFromEntryRecursively(entry))).then(() => {
resolve();
});
});
}
});
}
async function getDropItems(event) {
if (data.isUploading) {
message("正在上传...", "info");
return;
}
data.fileList = [];
data.isUploading = true;
const items = event.dataTransfer.items;
const promises = [];
for (const item of items) {
if (item.kind === "file") {
const entry = item.webkitGetAsEntry();
promises.push(getFileFromEntryRecursively(entry));
}
}
await Promise.all(promises);
await upload();
}
async function upload() {
const checkMd5Tip = message("正在校验文件的md5,请稍候", "info");
await checkMd5(checkMd5Tip);
sliceFile();
const checkFragmentMd5Tip = message("正在校验分片文件的md5,请稍候", "info");
await checkFragmentMd5(checkFragmentMd5Tip);
for (let i = 0; i < data.fileList.length; i++) {
if (data.fileList[i].isUpload) {
continue;
}
for (let j = 0; j < data.fileList[i].fragmentList.length; j++) {
if (data.fileList[i].fragmentList[j].percentage !== 100) {
const onUploadProgress = (progressEvent) => {
data.fileList[i].fragmentList[j].percentage = parseInt(Number(((progressEvent.loaded / progressEvent.total) * 100)).toFixed(0));
data.fileList[i].fragmentList[j].completeSize = progressEvent.loaded / progressEvent.total * data.fileList[i].fragmentList[j].fragmentFile.size;
updateTotalPercentage(i);
};
const formData = new FormData();
formData.append("file", data.fileList[i].fragmentList[j].fragmentFile, data.fileList[i].fragmentList[j].name);
postFileRequest("/fragment-info/upload?md5=" + data.fileList[i].fragmentList[j].md5, formData, onUploadProgress).then((res) => {
if (res.data.code === 500) {
message(res.data.msg, "error");
}
});
}
}
}
}
async function checkMd5(checkMd5Tip) {
const promises = [];
const promisesCheckMd5 = [];
for (let i = 0; i < data.fileList.length; i++) {
promises.push(calculateMD5(data.fileList[i].file).then(md5 => {
data.fileList[i].md5 = md5;
promisesCheckMd5.push(getRequest("/file-info/checkMd5?md5=" + md5).then((res) => {
if (res.data.code === 200) {
data.fileList[i].percentage = 100;
data.fileList[i].isUpload = true;
data.fileList[i].transition = "none";
data.fileList[i].totalCompleteSize = data.fileList[i].file.size;
message(data.fileList[i].name + "文件上传完成", "success");
checkUploadOver(i);
}
}));
}));
}
await Promise.all(promises);
await Promise.all(promisesCheckMd5);
checkMd5Tip.close();
}
function sliceFile() {
for (let i = 0; i < data.fileList.length; i++) {
if (data.fileList[i].isUpload) {
continue;
}
const fragmentCount = Math.floor(data.fileList[i].file.size / EACH_FILE) + 1;
const fragmentList = [];
for (let j = 0; j < fragmentCount; j++) {
fragmentList.push({
id: j,
fragmentFile: data.fileList[i].file.slice(j * EACH_FILE, (j + 1) * EACH_FILE),
completeSize: 0,
name: data.fileList[i].name + "--分片" + (j + 1),
percentage: 0,
});
}
data.fileList[i].fragmentList = fragmentList;
}
}
async function checkFragmentMd5(checkFragmentMd5Tip) {
const promises = [];
const promisesCheckMd5 = [];
for (let i = 0; i < data.fileList.length; i++) {
if (data.fileList[i].isUpload) {
continue;
}
for (let j = 0; j < data.fileList[i].fragmentList.length; j++) {
promises.push(calculateMD5(data.fileList[i].fragmentList[j].fragmentFile).then(md5 => {
data.fileList[i].fragmentList[j].md5 = md5;
promisesCheckMd5.push(getRequest("/fragment-info/checkMd5?md5=" + md5).then((res) => {
if (res.data.code === 200) {
data.fileList[i].fragmentList[j].percentage = 100;
data.fileList[i].fragmentList[j].completeSize = data.fileList[i].fragmentList[j].fragmentFile.size;
}
}));
}));
}
}
await Promise.all(promises);
await Promise.all(promisesCheckMd5);
checkFragmentMd5Tip.close();
}
async function updateTotalPercentage(i) {
let totalCompleteSize = 0;
for (let j = 0; j < data.fileList[i].fragmentList.length; j++) {
totalCompleteSize += data.fileList[i].fragmentList[j].completeSize;
}
data.fileList[i].totalCompleteSize = totalCompleteSize;
data.fileList[i].percentage = parseInt(Number(data.fileList[i].totalCompleteSize / data.fileList[i].totalSize * 100).toFixed(0));
if (data.fileList[i].percentage === 100) {
if (!data.fileList[i].isUpload) {
data.fileList[i].isUpload = true;
message(data.fileList[i].name + "文件上传完成", "success");
await generateFile(i);
checkUploadOver(i);
}
}
}
async function generateFile(i) {
const fragmentMd5List = [];
for (let j = 0; j < data.fileList[i].fragmentList.length; j++) {
fragmentMd5List.push(data.fileList[i].fragmentList[j].md5);
}
await postRequest("/file-info/generateFile", {
name: data.fileList[i].name,
md5: data.fileList[i].md5,
fragmentMd5List: fragmentMd5List
}).then((res) => {
if (res.data.code === 500) {
message(res.data.msg, "error");
}
});
}
function checkUploadOver() {
let isOver = true;
for (let i = 0; i < data.fileList.length; i++) {
if (data.fileList[i].percentage !== 100) {
isOver = false;
break
}
}
if (isOver) {
data.isUploading = false;
}
}
const pickerOpts = {
excludeAcceptAllOption: false,
multiple: true,
};
async function showFilePicker() {
if (data.isUploading) {
message("正在上传...", "info");
return;
}
let fileHandle;
try {
fileHandle = await window.showOpenFilePicker(pickerOpts);
data.fileList = [];
data.isUploading = true;
} catch (e) {
if (e.name === 'AbortError' && e.message === 'The user aborted a request.') {
message("用户没有选择文件", "info");
return;
} else {
throw e;
}
}
for (let i = 0; i < fileHandle.length; i++) {
const file = await fileHandle[i].getFile();
data.fileList.push({
name: fileHandle[i].name,
percentage: 0,
file: file,
totalSize: file.size,
totalCompleteSize: 0,
isUpload: false,
});
}
await upload();
}
function showFragmentInfo(item) {
data.showFragmentList = item.fragmentList;
data.fragmentDialogVisible = true;
}
return {
data,
getDropItems,
showFilePicker,
showFragmentInfo,
};
},
};
</script>
<style>
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
.drop-area {
margin: 100px auto 0;
width: 800px;
height: 180px;
border: 1px dashed #dcdfe6;
display: flex;
align-items: center;
justify-content: center;
}
.drop-area:hover {
border-color: #409eff;
cursor: pointer;
}
.icon-upload::before {
display: flex;
justify-content: center;
font-size: 40px;
margin: 10px 0;
}
.tip-text {
color: #606266;
font-size: 14px;
text-align: center;
}
.tip-text em {
color: #409eff;
font-style: normal;
}
.file-list {
margin: 0 auto;
width: 800px;
}
.single-file {
margin: 10px 0;
}
</style>
<template>
<link rel="stylesheet" href="/style/css/iconfont.css">
<div class="progress-container">
<div class="bar">
<div class="percentage" :style="{'width': props.percentage + '%', 'transition' : props.transition ? props.transition : 'width 0.2s linear'}">
<span class="text-inside">{{ props.content + " " + props.percentage + "%" }}</span>
</div>
</div>
<div class="tip-content">
<span v-show="props.percentage !== 100">{{ props.percentage + "%" }}</span>
<i class="iconfont icon-over" v-show="props.percentage === 100"/>
</div>
</div>
</template>
<script>
export default {
props: ["percentage", "content", "transition"],
setup(props) {
return {
props
}
}
}
</script>
<style scoped>
.progress-container {
display: flex;
height: 30px;
cursor: pointer;
border: 1px dashed #dcdfe6;
padding: 0 10px;
}
.bar {
color: white;
font-weight: 500;
line-height: 30px;
font-size: 14px;
flex: 1;
}
.percentage {
border-radius: 30px;
background-color: #67c23a;
white-space: nowrap;
word-break: break-all;
overflow: hidden;
}
.text-inside {
padding-right: 10px;
padding-left: 15px;
float: right;
}
.tip-content {
padding: 0 10px;
font-size: 16px;
line-height: 30px;
width: 40px;
}
.icon-over::before {
font-size: 24px;
color: #67c23a;
}
</style>
import {ElMessage} from "element-plus";
import axios from "axios";
import {MD5} from 'crypto-js';
const baseUrl = "http://127.0.0.1:8080"
export function message(msg, type) {
return ElMessage({
message: msg,
type: type,
center: true,
showClose: true,
})
}
export const getRequest = (url) => {
return axios({
method: 'get',
url: baseUrl + url
})
}
export const postRequest = (url, data) => {
return axios({
method: 'post',
url: baseUrl + url,
data: data,
})
}
export const postFileRequest = (url, data, onUploadProgress) => {
return axios({
method: 'post',
url: baseUrl + url,
data: data,
onUploadProgress: onUploadProgress,
})
}
export const calculateMD5 = (file) => {
return new Promise(resolve => {
const fileReader = new FileReader();
fileReader.readAsBinaryString(file);
fileReader.onloadend = event => {
resolve(MD5(event.target.result).toString());
}
});
}
export const EACH_FILE = 1024 * 1024 * 2;
后续开发说明
源码下载