初步实现文件上传
This commit is contained in:
parent
6600a6e697
commit
45bccc6ba0
@ -25,6 +25,12 @@
|
||||
<!-- <groupId>org.springframework.boot</groupId>-->
|
||||
<!-- <artifactId>spring-boot-starter-amqp</artifactId>-->
|
||||
<!-- </dependency>-->
|
||||
<dependency>
|
||||
<groupId>net.coobird</groupId>
|
||||
<artifactId>thumbnailator</artifactId>
|
||||
<version>0.4.17</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
|
@ -0,0 +1,80 @@
|
||||
package xyz.longicorn.driver.controller;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import org.springframework.util.DigestUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import xyz.longicorn.driver.config.BizException;
|
||||
import xyz.longicorn.driver.dto.ApiResult;
|
||||
import xyz.longicorn.driver.pojo.FileInfo;
|
||||
import xyz.longicorn.driver.pojo.FolderInfo;
|
||||
import xyz.longicorn.driver.service.FileService;
|
||||
import xyz.longicorn.driver.service.FolderService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class UploadController {
|
||||
private final static String SAVE_PATH = "D:\\tmp\\upload";
|
||||
private final static String ACCESS_PATH = "/upload";
|
||||
@Resource
|
||||
private FileService fileService;
|
||||
@Resource
|
||||
private FolderService folderService;
|
||||
private final static Map<String, String> FILE_MIME_TYPE = new HashMap<>();
|
||||
|
||||
public UploadController() {
|
||||
FILE_MIME_TYPE.put("image/png", "png");
|
||||
FILE_MIME_TYPE.put("image/jpeg", "jpg");
|
||||
FILE_MIME_TYPE.put("image/jpg", "jpg");
|
||||
FILE_MIME_TYPE.put("image/bmp", "bmp");
|
||||
FILE_MIME_TYPE.put("image/webp", "webp");
|
||||
FILE_MIME_TYPE.put("image/gif", "gif");
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@PostMapping("/upload")
|
||||
public ApiResult upload(@RequestParam("parent") String parent, @RequestPart("file") MultipartFile file) {
|
||||
//
|
||||
// 1.计算md5
|
||||
String md5 = DigestUtils.md5DigestAsHex(file.getInputStream());
|
||||
FileInfo f = fileService.getByMd5(md5); //
|
||||
//
|
||||
FileInfo fileInfo = new FileInfo();
|
||||
if (parent.equals("/")) {
|
||||
fileInfo.setFolderId(0l);
|
||||
} else {
|
||||
FolderInfo folder = folderService.getByPath(1, parent);
|
||||
if (folder == null) {
|
||||
throw BizException.create("保存目录不存在");
|
||||
}
|
||||
fileInfo.setFolderId(folder.getId());
|
||||
}
|
||||
fileInfo.setUid(1l);
|
||||
fileInfo.setHash(md5);//
|
||||
fileInfo.setName(file.getOriginalFilename());
|
||||
fileInfo.setSize(file.getSize());
|
||||
String type = file.getContentType().toLowerCase();
|
||||
if (FILE_MIME_TYPE.containsKey(type)) {
|
||||
type = FILE_MIME_TYPE.get(type);
|
||||
} else {
|
||||
type = "file";
|
||||
}
|
||||
fileInfo.setType(type);
|
||||
if (f != null) { // 系统已经存在了该文件
|
||||
// 不保存上传文件 直接copy数据
|
||||
fileInfo.setPath(f.getPath());
|
||||
} else {
|
||||
String path = SAVE_PATH + "\\" + md5;//文件的保存路径
|
||||
fileInfo.setPath(path);
|
||||
// 爆保存上传文件
|
||||
file.transferTo(new File(path));
|
||||
}
|
||||
fileService.save(fileInfo);//保存文件信息
|
||||
return ApiResult.success(null);
|
||||
}
|
||||
}
|
@ -24,7 +24,7 @@ public class FileInfo implements Serializable {
|
||||
private String hash;
|
||||
private Long folderId;
|
||||
private String type;
|
||||
private Integer size;
|
||||
private Long size;
|
||||
private String path;
|
||||
private Date createTime;
|
||||
private Date updateTime;
|
||||
|
@ -12,15 +12,23 @@ import java.util.List;
|
||||
public class FileService extends ServiceImpl<FileInfoMapper, FileInfo> {
|
||||
/**
|
||||
* 查询了目录下的文件列表集合
|
||||
*
|
||||
* @param uid
|
||||
* @param folderId
|
||||
* @return
|
||||
*/
|
||||
public List<FileInfo> listByFolderId(int uid,long folderId){
|
||||
public List<FileInfo> listByFolderId(int uid, long folderId) {
|
||||
QueryWrapper q = new QueryWrapper();
|
||||
q.eq("uid",uid);
|
||||
q.eq("folder_id",folderId);
|
||||
q.eq("status",1); // TODO 枚举
|
||||
q.eq("uid", uid);
|
||||
q.eq("folder_id", folderId);
|
||||
q.eq("status", 1); // TODO 枚举
|
||||
return this.list(q);
|
||||
}
|
||||
|
||||
public FileInfo getByMd5(String md5) {
|
||||
QueryWrapper q = new QueryWrapper();
|
||||
q.eq("hash", md5);//根据MD5查询是否已经存在改文件
|
||||
q.last("limit 1");
|
||||
return this.getOne(q);
|
||||
}
|
||||
}
|
||||
|
@ -85,8 +85,7 @@ public class FolderService extends ServiceImpl<FolderInfoMapper, FolderInfo> {
|
||||
fNew.setUid(uid);
|
||||
fNew.setName(name);
|
||||
fNew.setParentId(parentId);
|
||||
fNew.setPath(parent + "/" + name);
|
||||
;
|
||||
fNew.setPath((parent.equals("/")?"":parent) + "/" + name);
|
||||
return this.save(fNew) ? fNew : null;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,25 @@
|
||||
package xyz.longicorn.driver.util;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import net.coobird.thumbnailator.Thumbnails;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class ImageUtils {
|
||||
private static final List<String> imageTypes = Arrays.asList("png", "jpg", "jpeg", "gif", "bmp", "webp");
|
||||
|
||||
public static boolean isImage(String type) {
|
||||
return imageTypes.contains(type.toLowerCase());
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static void getPreview(String imagePath, OutputStream os) {
|
||||
Thumbnails.of(new File(imagePath))
|
||||
.size(256, 256)
|
||||
.outputFormat("jpg").toOutputStream(os);
|
||||
}
|
||||
}
|
@ -45,6 +45,12 @@ spring:
|
||||
pathmatch:
|
||||
# 因为Springfox使用的路径匹配是基于AntPathMatcher的,而Spring Boot 2.6.X使用的是PathPatternMatcher。
|
||||
matching-strategy: ant_path_matcher
|
||||
|
||||
# 上传
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 100MB
|
||||
max-request-size: 100MB
|
||||
springfox:
|
||||
documentation:
|
||||
enabled: true
|
||||
|
51
web/package-lock.json
generated
51
web/package-lock.json
generated
@ -9,13 +9,16 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^1.1.4",
|
||||
"browser-md5-file": "^1.1.1",
|
||||
"element-plus": "^2.1.11",
|
||||
"fetch-progress": "^1.3.0",
|
||||
"less": "^4.1.2",
|
||||
"less-loader": "^10.2.0",
|
||||
"qs": "^6.10.3",
|
||||
"vue": "^3.2.25",
|
||||
"vue-router": "^4.0.15",
|
||||
"vue-simple-context-menu": "^4.0.2"
|
||||
"vue-simple-context-menu": "^4.0.2",
|
||||
"vue-upload-component": "^2.8.22"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^2.3.1",
|
||||
@ -491,6 +494,14 @@
|
||||
"resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.1.1.tgz",
|
||||
"integrity": "sha512-p4DO/JXwjs8klJyJL8Q2oM4ks5fUTze/h5k10oPPKMiLe1fj3G1QMzPHNmN1Py4ycOk7WlO2DcGXv1qiESJCZA=="
|
||||
},
|
||||
"node_modules/browser-md5-file": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/browser-md5-file/-/browser-md5-file-1.1.1.tgz",
|
||||
"integrity": "sha512-9h2UViTtZPhBa7oHvp5mb7MvJaX5OKEPUsplDwJ800OIV+In7BOR3RXOMB78obn2iQVIiS3WkVLhG7Zu1EMwbw==",
|
||||
"dependencies": {
|
||||
"spark-md5": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.20.3",
|
||||
"resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.20.3.tgz",
|
||||
@ -1081,6 +1092,11 @@
|
||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/fetch-progress": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/fetch-progress/-/fetch-progress-1.3.0.tgz",
|
||||
"integrity": "sha512-BCeKkVRx0x4mk/ykGGJ9FA2oJgrSp/lQgMiy2Ub+S2SMipt+po2uULUBM3OMOM/5XiwPpM4QyYmbYv/e98NWng=="
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz",
|
||||
@ -1613,6 +1629,11 @@
|
||||
"resolved": "https://registry.npmmirror.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
|
||||
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="
|
||||
},
|
||||
"node_modules/spark-md5": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/spark-md5/-/spark-md5-2.0.2.tgz",
|
||||
"integrity": "sha512-9WfT+FYBEvlrOOBEs484/zmbtSX4BlGjzXih1qIEWA1yhHbcqgcMHkiwXoWk2Sq1aJjLpcs6ZKV7JxrDNjIlNg=="
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz",
|
||||
@ -1819,6 +1840,11 @@
|
||||
"vue": "^3.2.31"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-upload-component": {
|
||||
"version": "2.8.22",
|
||||
"resolved": "https://registry.npmmirror.com/vue-upload-component/-/vue-upload-component-2.8.22.tgz",
|
||||
"integrity": "sha512-AJpETqiZrgqs8bwJQpWTFrRg3i6s7cUodRRZVnb1f94Jvpd0YYfzGY4zluBqPmssNSkUaYu7EteXaK8aW17Osw=="
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/watchpack/-/watchpack-2.3.1.tgz",
|
||||
@ -2317,6 +2343,14 @@
|
||||
"resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.1.1.tgz",
|
||||
"integrity": "sha512-p4DO/JXwjs8klJyJL8Q2oM4ks5fUTze/h5k10oPPKMiLe1fj3G1QMzPHNmN1Py4ycOk7WlO2DcGXv1qiESJCZA=="
|
||||
},
|
||||
"browser-md5-file": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/browser-md5-file/-/browser-md5-file-1.1.1.tgz",
|
||||
"integrity": "sha512-9h2UViTtZPhBa7oHvp5mb7MvJaX5OKEPUsplDwJ800OIV+In7BOR3RXOMB78obn2iQVIiS3WkVLhG7Zu1EMwbw==",
|
||||
"requires": {
|
||||
"spark-md5": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"browserslist": {
|
||||
"version": "4.20.3",
|
||||
"resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.20.3.tgz",
|
||||
@ -2683,6 +2717,11 @@
|
||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||
"peer": true
|
||||
},
|
||||
"fetch-progress": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/fetch-progress/-/fetch-progress-1.3.0.tgz",
|
||||
"integrity": "sha512-BCeKkVRx0x4mk/ykGGJ9FA2oJgrSp/lQgMiy2Ub+S2SMipt+po2uULUBM3OMOM/5XiwPpM4QyYmbYv/e98NWng=="
|
||||
},
|
||||
"fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz",
|
||||
@ -3099,6 +3138,11 @@
|
||||
"resolved": "https://registry.npmmirror.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
|
||||
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="
|
||||
},
|
||||
"spark-md5": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/spark-md5/-/spark-md5-2.0.2.tgz",
|
||||
"integrity": "sha512-9WfT+FYBEvlrOOBEs484/zmbtSX4BlGjzXih1qIEWA1yhHbcqgcMHkiwXoWk2Sq1aJjLpcs6ZKV7JxrDNjIlNg=="
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz",
|
||||
@ -3226,6 +3270,11 @@
|
||||
"click-outside-vue3": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"vue-upload-component": {
|
||||
"version": "2.8.22",
|
||||
"resolved": "https://registry.npmmirror.com/vue-upload-component/-/vue-upload-component-2.8.22.tgz",
|
||||
"integrity": "sha512-AJpETqiZrgqs8bwJQpWTFrRg3i6s7cUodRRZVnb1f94Jvpd0YYfzGY4zluBqPmssNSkUaYu7EteXaK8aW17Osw=="
|
||||
},
|
||||
"watchpack": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/watchpack/-/watchpack-2.3.1.tgz",
|
||||
|
@ -9,13 +9,16 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^1.1.4",
|
||||
"browser-md5-file": "^1.1.1",
|
||||
"element-plus": "^2.1.11",
|
||||
"fetch-progress": "^1.3.0",
|
||||
"less": "^4.1.2",
|
||||
"less-loader": "^10.2.0",
|
||||
"qs": "^6.10.3",
|
||||
"vue": "^3.2.25",
|
||||
"vue-router": "^4.0.15",
|
||||
"vue-simple-context-menu": "^4.0.2"
|
||||
"vue-simple-context-menu": "^4.0.2",
|
||||
"vue-upload-component": "^2.8.22"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^2.3.1",
|
||||
|
@ -1,12 +1,15 @@
|
||||
<script>
|
||||
import {ArrowDown, Grid} from '@element-plus/icons-vue'
|
||||
import {ArrowDown, Grid, FolderAdd,Search} from '@element-plus/icons-vue'
|
||||
import FileIcon from "./components/FileIcon.vue";
|
||||
import {dayjs, ElMessage, ElMessageBox} from 'element-plus'
|
||||
import api from "./service/api";
|
||||
import qs from "qs";
|
||||
import FileUploader from "./components/file-uploader/Index.vue";
|
||||
|
||||
export default {
|
||||
|
||||
setup(){
|
||||
return {Search}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentActiveIndex: "all",
|
||||
@ -58,6 +61,9 @@ export default {
|
||||
loading: false,
|
||||
message: ''
|
||||
},
|
||||
search:{
|
||||
value:''
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -74,7 +80,7 @@ export default {
|
||||
return pathList;
|
||||
}
|
||||
},
|
||||
components: {FileIcon, ArrowDown, Grid},
|
||||
components: {FileUploader, FileIcon, ArrowDown, Grid, FolderAdd},
|
||||
mounted() {
|
||||
// window.addEventListener('popstate',()=>console.log(location.href))
|
||||
window.addEventListener('hashchange', this.handleHashChange) // 添加监听
|
||||
@ -115,6 +121,13 @@ export default {
|
||||
f = Math.floor(Math.log(a) / Math.log(c));
|
||||
return parseFloat((a / Math.pow(c, f)).toFixed(d)) + " " + e[f];
|
||||
},
|
||||
// 对文件名进行处理
|
||||
formatName(name) {
|
||||
if (!name || name.length < 15) return name;
|
||||
const extIndex = name.lastIndexOf('.'),
|
||||
ext = name.substr(extIndex);
|
||||
return name.substr(0, (15 - ext.length)) + "**" + ext;
|
||||
},
|
||||
showFile(file, e) {
|
||||
console.log(e)
|
||||
if (file.type != 'folder') {
|
||||
@ -158,12 +171,12 @@ export default {
|
||||
// 开始新建
|
||||
this.createFolder.loading = true;
|
||||
try {
|
||||
await api.folder.create(this.getCurrentPath(),this.createFolder.value);
|
||||
await api.folder.create(this.getCurrentPath(), this.createFolder.value);
|
||||
this.createFolder.visible = false;
|
||||
this.handleHashChange();
|
||||
} catch (e) {
|
||||
this.createFolder.message = e.message;
|
||||
}finally {
|
||||
} finally {
|
||||
this.createFolder.loading = false // 取消loading
|
||||
}
|
||||
|
||||
@ -205,7 +218,28 @@ export default {
|
||||
</el-aside>
|
||||
<el-container>
|
||||
<el-header class="pan-header">
|
||||
<div class="d-flex">
|
||||
<div class="d-flex flex-1">
|
||||
<div class="left-tool-bar" style="padding-top: 15px;">
|
||||
<file-uploader @upload-success="handleHashChange" :current-folder="currentPath"/>
|
||||
|
||||
<el-button @click="createFolder.visible = true" size="default"
|
||||
style="margin-left: 10px" round>
|
||||
<el-icon>
|
||||
<folder-add/>
|
||||
</el-icon>
|
||||
<span style="margin-left: 4px;">新建文件夹</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="pan-header-user-right">
|
||||
<li><el-input
|
||||
v-model="search.value"
|
||||
class="search-box" clearable
|
||||
style="border-radius:30px;"
|
||||
placeholder="搜索你的文件"
|
||||
:prefix-icon="Search"
|
||||
/></li>
|
||||
<li>帮助</li>
|
||||
<li>
|
||||
<el-dropdown>
|
||||
@ -227,27 +261,10 @@ export default {
|
||||
</el-dropdown>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</el-header>
|
||||
<el-main>
|
||||
<div class="d-flex">
|
||||
<div class="left-tool-bar">
|
||||
<el-dropdown>
|
||||
<el-button size="default" round>上传</el-button>
|
||||
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item>上传文件</el-dropdown-item>
|
||||
<el-dropdown-item>上传文件夹</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-button @click="createFolder.visible = true" style="margin-left: 10px" size="default"
|
||||
round
|
||||
type="primary">新建文件夹
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex;padding: 10px 0;line-height: 30px;">
|
||||
<div style="line-height: 30px;" class="d-flex">
|
||||
<!-- 当前路径 -->
|
||||
<div style="flex:1;display: flex">
|
||||
<div v-if="currentPath === '/'" style="font-weight: 700;font-size: 14px;">全部文件</div>
|
||||
@ -299,7 +316,7 @@ export default {
|
||||
<!-- 自定义单元格内容 -->
|
||||
<template #default="file">
|
||||
<div class="list-file-info">
|
||||
<FileIcon class="list-file-icon" :file="file.row"
|
||||
<FileIcon class="list-file-icon" style="width: 40px;" :file="file.row"
|
||||
:ext="file.row.type"/>
|
||||
<span class="list-file-name">{{ file.row.name }}</span>
|
||||
</div>
|
||||
@ -323,7 +340,7 @@ export default {
|
||||
<div class="file-image">
|
||||
<FileIcon :file="file" style="width:90%"/>
|
||||
</div>
|
||||
<div class="file-name">{{ file.name }}</div>
|
||||
<div class="file-name">{{ formatName(file.name) }}</div>
|
||||
<div class="file-info">
|
||||
{{
|
||||
file.type == 'folder' ? formatDate(file.createTime) : formatSize(file.size)
|
||||
@ -356,28 +373,6 @@ export default {
|
||||
</template>
|
||||
<style lang="less">
|
||||
|
||||
.logo-block {
|
||||
line-height: 60px;
|
||||
text-align: center;
|
||||
background-color: var(--el-color-primary);
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.pan-left-menu {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.pan-left-aside {
|
||||
border-right: solid 1px var(--el-border-color);
|
||||
height: 100vh;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.pan-header {
|
||||
background-color: #dedede;
|
||||
}
|
||||
|
||||
|
||||
// 平铺显示
|
||||
.file-block-item {
|
||||
|
@ -29,6 +29,14 @@
|
||||
.d-flex{
|
||||
display: flex;
|
||||
}
|
||||
.flex-1{
|
||||
flex:1;
|
||||
}
|
||||
.search-box{
|
||||
.el-input__wrapper{
|
||||
border-radius: 30px;
|
||||
}
|
||||
}
|
||||
.logo-icon{
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
@ -38,6 +46,28 @@
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.logo-block {
|
||||
line-height: 60px;
|
||||
text-align: center;
|
||||
background-color: var(--el-color-primary);
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.pan-left-menu {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.pan-left-aside {
|
||||
border-right: solid 1px var(--el-border-color);
|
||||
height: 100vh;
|
||||
width: 200px;
|
||||
box-shadow: 1px 0px 5px rgb(0 0 0 / 10%)
|
||||
}
|
||||
|
||||
.pan-header {
|
||||
box-shadow: 0 1px 5px rgb(0 0 0 / 10%)
|
||||
}
|
||||
|
||||
.pan-header-user-right {
|
||||
float: right;
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<span style="display:inline-block">
|
||||
<el-image v-if="type == 'picture'"
|
||||
:src="currentSrc" :previewSrcList="[currentSrc]"
|
||||
:src="currentSrc" :previewSrcList="[currentPreview]"
|
||||
:initial-index="4" fit="cover" :hide-on-click-modal="true"
|
||||
/>
|
||||
<img v-else class="file-icon" :src="currentSrc"/>
|
||||
@ -18,6 +18,9 @@ import {ElMessage} from "element-plus";
|
||||
export default {
|
||||
// 定义组件所需的属性
|
||||
props: {
|
||||
/**
|
||||
*@property {FileItem}
|
||||
*/
|
||||
file: {
|
||||
type: Object,
|
||||
required: true
|
||||
@ -29,13 +32,15 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
currentSrc: FolderIcon,
|
||||
currentPreview: FolderIcon,
|
||||
type: 'icon'
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const type = this.file.type.toLowerCase(); // 拿到文件类型
|
||||
if (['png', 'jpg', 'jpeg', 'gif', 'webp'].includes(type)) {
|
||||
this.currentSrc = this.file.path || UnknownIcon; // 图片
|
||||
this.currentSrc = this.file.thumb || UnknownIcon; // 图片
|
||||
this.currentPreview = this.file.path || UnknownIcon; // 图片
|
||||
this.type = 'picture';
|
||||
} else if (type == 'exe') {
|
||||
this.currentSrc = ExeIcon; // 图片
|
||||
|
1352
web/src/components/file-uploader/FileUpload.vue
Normal file
1352
web/src/components/file-uploader/FileUpload.vue
Normal file
File diff suppressed because it is too large
Load Diff
250
web/src/components/file-uploader/Index.vue
Normal file
250
web/src/components/file-uploader/Index.vue
Normal file
@ -0,0 +1,250 @@
|
||||
<template>
|
||||
<el-dropdown>
|
||||
<el-button type="primary" round>
|
||||
<label for="file_for_one">
|
||||
<el-icon class="el-icon--right">
|
||||
<Upload/>
|
||||
</el-icon>
|
||||
<span style="margin-left: 4px;">上传</span>
|
||||
</label>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu class="file-upload-dropdown">
|
||||
<el-dropdown-item>
|
||||
<label class="file-input-trigger" for="file_for_one">上传文件</label>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item>
|
||||
<label class="file-input-trigger" for="file_for_directory">上传文件夹</label>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<!-- 实现选择文件上传 -->
|
||||
<input type="file" multiple ref="fileOne" @change="selectFileChange" id="file_for_one"
|
||||
style="overflow: hidden;position: fixed;width: 1px;height: 1px;z-index: -1;opacity: 0;">
|
||||
<!-- 实现选择文件夹上传 -->
|
||||
<input type="file" multiple ref="fileDirectory" @change="selectDirectoryChange" id="file_for_directory"
|
||||
allowdirs="true" directory="true" webkitdirectory="true"
|
||||
style="overflow: hidden;position: fixed;width: 1px;height: 1px;z-index: -1;opacity: 0;">
|
||||
|
||||
<div class="upload-file-list" v-show="showUploadList && fileListData.length > 0">
|
||||
<div class="header d-flex">
|
||||
<div class="title" style="flex:1">上传列表</div>
|
||||
<div class="close-btn" @click="showUploadList=false">
|
||||
<el-icon>
|
||||
<arrow-down-bold/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="fileListData" style="width: 100%" empty-text="请选择要上传的文件">
|
||||
<el-table-column prop="name" label="名称"/>
|
||||
<el-table-column label="进度" width="80">
|
||||
<template #default="data">
|
||||
<span>{{ Math.ceil(data.row.progress) }}%</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="data">
|
||||
<span v-if="data.row.status == FileStatus.Ready">等待上传</span>
|
||||
<span v-if="data.row.status == FileStatus.Uploading">上传中</span>
|
||||
<span v-if="data.row.status == FileStatus.Processing">处理中</span>
|
||||
<span v-if="data.row.status == FileStatus.Error">上传错误</span>
|
||||
<span v-if="data.row.status == FileStatus.Success">上传完成</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column width="80">
|
||||
<template #default="data">
|
||||
<el-button v-if="data.row.status != FileStatus.Uploading" @click="removeFile(data.row)" size="small"
|
||||
circle type="primary"
|
||||
:icon="CloseBold"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<el-icon v-if="fileListData.length > 0" class="transfer-icon" @click="showUploadList = true">
|
||||
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M266.24 20.48a61.44 61.44 0 0 1 61.15328 55.54176L327.68 81.92v671.1296l52.71552-72.37632a61.44 61.44 0 0 1 102.64576 67.29728l-3.2768 4.95616-163.84 225.28a63.0784 63.0784 0 0 1-4.096 5.12l-1.80224 1.88416c-7.04512 7.20896-15.48288 12.288-24.45312 15.23712l-2.4576 0.73728a60.17024 60.17024 0 0 1-8.31488 1.76128l-4.21888 0.4096L266.24 1003.52c-2.8672 0-5.7344-0.2048-8.56064-0.57344l-4.17792-0.73728-6.5536-1.80224a60.416 60.416 0 0 1-24.49408-15.1552l-3.35872-3.76832-2.53952-3.2768-163.84-225.28a61.44 61.44 0 0 1 95.68256-76.88192l3.6864 4.62848L204.8 753.09056V81.92A61.44 61.44 0 0 1 266.24 20.48z m491.52 0c2.90816 0 5.7344 0.2048 8.56064 0.57344l4.17792 0.73728 6.5536 1.80224c9.0112 2.90816 17.408 7.9872 24.45312 15.1552l3.39968 3.76832 2.53952 3.2768 163.84 225.28a61.44 61.44 0 0 1-95.6416 76.88192l-3.72736-4.62848L819.2 270.90944V942.08a61.44 61.44 0 0 1-122.59328 5.89824L696.32 942.08V270.86848l-52.71552 72.4992a61.44 61.44 0 0 1-80.85504 16.7936l-4.95616-3.2768a61.44 61.44 0 0 1-16.7936-80.85504l3.23584-4.95616 163.84-225.28a63.0784 63.0784 0 0 1 4.13696-5.12l1.80224-1.92512c7.04512-7.168 15.44192-12.24704 24.41216-15.1552l2.4576-0.77824A60.17024 60.17024 0 0 1 757.76 20.48z"
|
||||
p-id="2005"></path>
|
||||
</svg>
|
||||
</el-icon>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FileUpload from "./FileUpload.vue";
|
||||
import {Upload, CircleCloseFilled, CloseBold, ArrowDownBold} from '@element-plus/icons-vue'
|
||||
import Hash from 'browser-md5-file';
|
||||
import api from "../../service/api";
|
||||
|
||||
const hash = new Hash();
|
||||
const FileStatus = {
|
||||
Processing: 0,
|
||||
Ready: 1,
|
||||
Uploading: 2,
|
||||
Success: 3,
|
||||
Error: 4
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "Index",
|
||||
emits:{
|
||||
/**
|
||||
* 文件上传后的触发此事件
|
||||
*/
|
||||
uploadSuccess:null
|
||||
},
|
||||
props: {
|
||||
/**
|
||||
* 当前目录
|
||||
*/
|
||||
currentFolder: {
|
||||
type: String,
|
||||
default: '/'
|
||||
}
|
||||
},
|
||||
components: {FileUpload, Upload, ArrowDownBold},
|
||||
data() {
|
||||
return {
|
||||
FileStatus,
|
||||
showUploadList: false,
|
||||
CircleCloseFilled, CloseBold,
|
||||
file_input_id: 'file-input', // input元素的id
|
||||
uploadDirectory: false, // 是否上传文件
|
||||
thread: 3, // 上传线程数
|
||||
maxSize: 100 * 1024 * 1024, // 最大允许大小
|
||||
fileListData: [],
|
||||
fileId: 0,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectFileChange(e) {
|
||||
// 当选择文件完成后
|
||||
this.add(this.$refs.fileOne.files)
|
||||
},
|
||||
selectDirectoryChange(e) {
|
||||
// 当选择文件夹完成后
|
||||
this.add(this.$refs.fileDirectory.files)
|
||||
},
|
||||
handleFileInput(files) {
|
||||
this.showUploadList = true;
|
||||
this.$refs.upload.active = true
|
||||
},
|
||||
removeFile(file) {
|
||||
const index = this.fileListData.indexOf(file);// 获取到要删除的下表
|
||||
this.fileListData.splice(index, 1)
|
||||
},
|
||||
add(files) {
|
||||
this.showUploadList = true;
|
||||
Array.from(files).forEach(file => {
|
||||
this.fileListData.push({
|
||||
id: this.fileId++,
|
||||
file,
|
||||
name: file.name,
|
||||
status: FileStatus.Ready,
|
||||
progress: 0,
|
||||
})
|
||||
})
|
||||
this.startUpload();//直接上传
|
||||
},
|
||||
startUpload() {
|
||||
this.fileListData.forEach(f => {
|
||||
if (f.status == FileStatus.Ready) {
|
||||
// 开始上传文件
|
||||
this.uploadFile(f)
|
||||
}
|
||||
})
|
||||
},
|
||||
uploadFile(f) {
|
||||
f.status = FileStatus.Uploading;
|
||||
api.upload(this.currentFolder, f.file, (p) => {
|
||||
f.progress = p.progress
|
||||
}).then(ret => {
|
||||
console.log(ret)
|
||||
f.status = FileStatus.Success; //
|
||||
this.$emit('upload-success', {
|
||||
result: ret.data,
|
||||
file: f
|
||||
})
|
||||
}).catch(e => console.log(e));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.file-upload-dropdown {
|
||||
.el-dropdown-menu__item {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.file-input-trigger {
|
||||
display: block;
|
||||
flex: 1;
|
||||
padding: 5px 16px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-dropdown-menuItem-hover-fill);
|
||||
color: var(--el-dropdown-menuItem-hover-color)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-file-list {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
width: 600px;
|
||||
min-height: 200px;
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
border-radius: 5px;
|
||||
z-index: 99;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 5px rgb(0 0 0 / 30%);
|
||||
|
||||
.header {
|
||||
line-height: 40px;
|
||||
background-color: var(--el-color-info);
|
||||
color: #fff;
|
||||
padding-left: 10px;
|
||||
//border-radius: ;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-color-info-dark-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.transfer-icon {
|
||||
background-color: var(--el-color-primary);
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
z-index: 98;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-color-primary-dark-2);
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
fill: #fff
|
||||
}
|
||||
}
|
||||
</style>
|
38
web/src/components/file-uploader/InputFile.vue
Normal file
38
web/src/components/file-uploader/InputFile.vue
Normal file
@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<input
|
||||
type="file"
|
||||
:name="$parent.name"
|
||||
:id="$parent.inputId || $parent.name"
|
||||
:accept="$parent.accept"
|
||||
:capture="$parent.capture"
|
||||
:disabled="$parent.disabled"
|
||||
@change="change"
|
||||
:webkitdirectory="$parent.directory && $parent.features.directory"
|
||||
:directory="$parent.directory && $parent.features.directory"
|
||||
:multiple="$parent.multiple && $parent.features.html5"
|
||||
/>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
methods: {
|
||||
change(e) {
|
||||
this.$parent.addInputFile(e.target)
|
||||
if (e.target.files) {
|
||||
e.target.value = ''
|
||||
if (e.target.files.length && !/safari/i.test(navigator.userAgent)) {
|
||||
e.target.type = ''
|
||||
e.target.type = 'file'
|
||||
}
|
||||
} else {
|
||||
// ie9 fix #219
|
||||
this.$destroy()
|
||||
// eslint-disable-next-line
|
||||
new this.constructor({
|
||||
parent: this.$parent,
|
||||
el: this.$el,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
395
web/src/components/file-uploader/chunk/ChunkUploadHandler.js
Normal file
395
web/src/components/file-uploader/chunk/ChunkUploadHandler.js
Normal file
@ -0,0 +1,395 @@
|
||||
import {
|
||||
default as request,
|
||||
createRequest,
|
||||
sendFormRequest
|
||||
} from '../utils/request'
|
||||
|
||||
export default class ChunkUploadHandler {
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {File} file
|
||||
* @param {Object} options
|
||||
*/
|
||||
constructor(file, options) {
|
||||
this.file = file
|
||||
this.options = options
|
||||
this.chunks = []
|
||||
this.sessionId = null
|
||||
this.chunkSize = null
|
||||
this.speedInterval = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the max retries from options
|
||||
*/
|
||||
get maxRetries() {
|
||||
return parseInt(this.options.maxRetries, 10)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the max number of active chunks being uploaded at once from options
|
||||
*/
|
||||
get maxActiveChunks() {
|
||||
return parseInt(this.options.maxActive, 10)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the file type
|
||||
*/
|
||||
get fileType() {
|
||||
return this.file.type
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the file size
|
||||
*/
|
||||
get fileSize() {
|
||||
return this.file.size
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the file name
|
||||
*/
|
||||
get fileName() {
|
||||
return this.file.name
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets action (url) to upload the file
|
||||
*/
|
||||
get action() {
|
||||
return this.options.action || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the body to be merged when sending the request in start phase
|
||||
*/
|
||||
get startBody() {
|
||||
return this.options.startBody || {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the body to be merged when sending the request in upload phase
|
||||
*/
|
||||
get uploadBody() {
|
||||
return this.options.uploadBody || {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the body to be merged when sending the request in finish phase
|
||||
*/
|
||||
get finishBody() {
|
||||
return this.options.finishBody || {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the headers of the requests from options
|
||||
*/
|
||||
get headers() {
|
||||
return this.options.headers || {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether it's ready to upload files or not
|
||||
*/
|
||||
get readyToUpload() {
|
||||
return !!this.chunks
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the progress of the chunk upload
|
||||
* - Gets all the completed chunks
|
||||
* - Gets the progress of all the chunks that are being uploaded
|
||||
*/
|
||||
get progress() {
|
||||
const completedProgress = (this.chunksUploaded.length / this.chunks.length) * 100
|
||||
const uploadingProgress = this.chunksUploading.reduce((progress, chunk) => {
|
||||
return progress + ((chunk.progress | 0) / this.chunks.length)
|
||||
}, 0)
|
||||
|
||||
return Math.min(completedProgress + uploadingProgress, 100)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the chunks that are pending to be uploaded
|
||||
*/
|
||||
get chunksToUpload() {
|
||||
return this.chunks.filter(chunk => {
|
||||
return !chunk.active && !chunk.uploaded
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether there are chunks to upload or not
|
||||
*/
|
||||
get hasChunksToUpload() {
|
||||
return this.chunksToUpload.length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the chunks that are uploading
|
||||
*/
|
||||
get chunksUploading() {
|
||||
return this.chunks.filter(chunk => {
|
||||
return !!chunk.xhr && !!chunk.active
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the chunks that have finished uploading
|
||||
*/
|
||||
get chunksUploaded() {
|
||||
return this.chunks.filter(chunk => {
|
||||
return !!chunk.uploaded
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates all the chunks in the initial state
|
||||
*/
|
||||
createChunks() {
|
||||
this.chunks = []
|
||||
|
||||
let start = 0
|
||||
let end = this.chunkSize
|
||||
while (start < this.fileSize) {
|
||||
this.chunks.push({
|
||||
blob: this.file.file.slice(start, end),
|
||||
startOffset: start,
|
||||
active: false,
|
||||
retries: this.maxRetries
|
||||
})
|
||||
start = end
|
||||
end = start + this.chunkSize
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the progress of the file with the handler's progress
|
||||
*/
|
||||
updateFileProgress() {
|
||||
this.file.progress = this.progress
|
||||
}
|
||||
|
||||
/**
|
||||
* Paues the upload process
|
||||
* - Stops all active requests
|
||||
* - Sets the file not active
|
||||
*/
|
||||
pause() {
|
||||
this.file.active = false
|
||||
this.stopChunks()
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops all the current chunks
|
||||
*/
|
||||
stopChunks() {
|
||||
this.chunksUploading.forEach(chunk => {
|
||||
chunk.xhr.abort()
|
||||
chunk.active = false
|
||||
})
|
||||
|
||||
this.stopSpeedCalc()
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumes the file upload
|
||||
* - Sets the file active
|
||||
* - Starts the following chunks
|
||||
*/
|
||||
resume() {
|
||||
this.file.active = true
|
||||
this.startChunking()
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the file upload
|
||||
*
|
||||
* @returns Promise
|
||||
* - resolve The file was uploaded
|
||||
* - reject The file upload failed
|
||||
*/
|
||||
upload() {
|
||||
this.promise = new Promise((resolve, reject) => {
|
||||
this.resolve = resolve
|
||||
this.reject = reject
|
||||
})
|
||||
this.start()
|
||||
|
||||
return this.promise
|
||||
}
|
||||
|
||||
/**
|
||||
* Start phase
|
||||
* Sends a request to the backend to initialise the chunks
|
||||
*/
|
||||
start() {
|
||||
request({
|
||||
method: 'POST',
|
||||
headers: Object.assign({}, this.headers, {
|
||||
'Content-Type': 'application/json'
|
||||
}),
|
||||
url: this.action,
|
||||
body: Object.assign(this.startBody, {
|
||||
phase: 'start',
|
||||
mime_type: this.fileType,
|
||||
size: this.fileSize,
|
||||
name: this.fileName
|
||||
})
|
||||
}).then(res => {
|
||||
if (res.status !== 'success') {
|
||||
this.file.response = res
|
||||
return this.reject('server')
|
||||
}
|
||||
|
||||
this.sessionId = res.data.session_id
|
||||
this.chunkSize = res.data.end_offset
|
||||
|
||||
this.createChunks()
|
||||
this.startChunking()
|
||||
}).catch(res => {
|
||||
this.file.response = res
|
||||
this.reject('server')
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts to upload chunks
|
||||
*/
|
||||
startChunking() {
|
||||
for (let i = 0; i < this.maxActiveChunks; i++) {
|
||||
this.uploadNextChunk()
|
||||
}
|
||||
|
||||
this.startSpeedCalc()
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads the next chunk
|
||||
* - Won't do anything if the process is paused
|
||||
* - Will start finish phase if there are no more chunks to upload
|
||||
*/
|
||||
uploadNextChunk() {
|
||||
if (this.file.active) {
|
||||
if (this.hasChunksToUpload) {
|
||||
return this.uploadChunk(this.chunksToUpload[0])
|
||||
}
|
||||
|
||||
if (this.chunksUploading.length === 0) {
|
||||
return this.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a chunk
|
||||
* - Sends the chunk to the backend
|
||||
* - Sets the chunk as uploaded if everything went well
|
||||
* - Decreases the number of retries if anything went wrong
|
||||
* - Fails if there are no more retries
|
||||
*
|
||||
* @param {Object} chunk
|
||||
*/
|
||||
uploadChunk(chunk) {
|
||||
chunk.progress = 0
|
||||
chunk.active = true
|
||||
this.updateFileProgress()
|
||||
chunk.xhr = createRequest({
|
||||
method: 'POST',
|
||||
headers: this.headers,
|
||||
url: this.action
|
||||
})
|
||||
|
||||
chunk.xhr.upload.addEventListener('progress', function (evt) {
|
||||
if (evt.lengthComputable) {
|
||||
chunk.progress = Math.round(evt.loaded / evt.total * 100)
|
||||
}
|
||||
}, false)
|
||||
|
||||
sendFormRequest(chunk.xhr, Object.assign(this.uploadBody, {
|
||||
phase: 'upload',
|
||||
session_id: this.sessionId,
|
||||
start_offset: chunk.startOffset,
|
||||
chunk: chunk.blob
|
||||
})).then(res => {
|
||||
chunk.active = false
|
||||
if (res.status === 'success') {
|
||||
chunk.uploaded = true
|
||||
} else {
|
||||
if (chunk.retries-- <= 0) {
|
||||
this.stopChunks()
|
||||
return this.reject('upload')
|
||||
}
|
||||
}
|
||||
|
||||
this.uploadNextChunk()
|
||||
}).catch(() => {
|
||||
chunk.active = false
|
||||
if (chunk.retries-- <= 0) {
|
||||
this.stopChunks()
|
||||
return this.reject('upload')
|
||||
}
|
||||
|
||||
this.uploadNextChunk()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish phase
|
||||
* Sends a request to the backend to finish the process
|
||||
*/
|
||||
finish() {
|
||||
this.updateFileProgress()
|
||||
this.stopSpeedCalc()
|
||||
|
||||
request({
|
||||
method: 'POST',
|
||||
headers: Object.assign({}, this.headers, {
|
||||
'Content-Type': 'application/json'
|
||||
}),
|
||||
url: this.action,
|
||||
body: Object.assign(this.finishBody, {
|
||||
phase: 'finish',
|
||||
session_id: this.sessionId
|
||||
})
|
||||
}).then(res => {
|
||||
this.file.response = res
|
||||
if (res.status !== 'success') {
|
||||
return this.reject('server')
|
||||
}
|
||||
|
||||
this.resolve(res)
|
||||
}).catch(res => {
|
||||
this.file.response = res
|
||||
this.reject('server')
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets an interval to calculate and
|
||||
* set upload speed every 3 seconds
|
||||
*/
|
||||
startSpeedCalc() {
|
||||
this.file.speed = 0
|
||||
let lastUploadedBytes = 0
|
||||
if (!this.speedInterval) {
|
||||
this.speedInterval = window.setInterval(() => {
|
||||
let uploadedBytes = (this.progress / 100) * this.fileSize
|
||||
this.file.speed = (uploadedBytes - lastUploadedBytes)
|
||||
lastUploadedBytes = uploadedBytes
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the upload speed interval
|
||||
*/
|
||||
stopSpeedCalc() {
|
||||
this.speedInterval && window.clearInterval(this.speedInterval)
|
||||
this.speedInterval = null
|
||||
this.file.speed = 0
|
||||
}
|
||||
}
|
59
web/src/components/file-uploader/utils/chunkUpload.js
Normal file
59
web/src/components/file-uploader/utils/chunkUpload.js
Normal file
@ -0,0 +1,59 @@
|
||||
const CHUNK_SIZE = 1048576
|
||||
const ChunkActiveUploads = {}
|
||||
|
||||
const chunkUploadStart = (req, res) => {
|
||||
const uuid = Math.floor((1 + Math.random()) * 0x10000)
|
||||
.toString(16)
|
||||
.substring(1)
|
||||
ChunkActiveUploads[uuid] = {}
|
||||
|
||||
return res.json({
|
||||
status: 'success',
|
||||
data: {
|
||||
session_id: uuid,
|
||||
start_offset: 0,
|
||||
end_offset: CHUNK_SIZE
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const chunkUploadPart = (req, res) => {
|
||||
setTimeout(() => {
|
||||
const rand = Math.random()
|
||||
if (rand <= 0.25) {
|
||||
res.status(500)
|
||||
res.json({ status: 'error', error: 'server' })
|
||||
} else {
|
||||
res.send({ status: 'success' })
|
||||
}
|
||||
}, 100 + parseInt(Math.random() * 2000, 10))
|
||||
}
|
||||
|
||||
const chunkUploadFinish = (req, res) => {
|
||||
setTimeout(() => {
|
||||
const rand = Math.random()
|
||||
if (rand <= 0.25) {
|
||||
res.status(500)
|
||||
res.json({ status: 'error', error: 'server' })
|
||||
} else {
|
||||
res.send({ status: 'success' })
|
||||
}
|
||||
}, 100 + parseInt(Math.random() * 2000, 10))
|
||||
}
|
||||
|
||||
module.exports = (req, res) => {
|
||||
if (!req.body.phase) {
|
||||
return chunkUploadPart(req, res)
|
||||
}
|
||||
|
||||
switch (req.body.phase) {
|
||||
case 'start':
|
||||
return chunkUploadStart(req, res)
|
||||
|
||||
case 'upload':
|
||||
return chunkUploadPart(req, res)
|
||||
|
||||
case 'finish':
|
||||
return chunkUploadFinish(req, res)
|
||||
}
|
||||
}
|
87
web/src/components/file-uploader/utils/request.js
Normal file
87
web/src/components/file-uploader/utils/request.js
Normal file
@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Creates a XHR request
|
||||
*
|
||||
* @param {Object} options
|
||||
*/
|
||||
export const createRequest = (options) => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.open(options.method || 'GET', options.url)
|
||||
xhr.responseType = 'json'
|
||||
if (options.headers) {
|
||||
Object.keys(options.headers).forEach(key => {
|
||||
xhr.setRequestHeader(key, options.headers[key])
|
||||
})
|
||||
}
|
||||
|
||||
return xhr
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a XHR request with certain body
|
||||
*
|
||||
* @param {XMLHttpRequest} xhr
|
||||
* @param {Object} body
|
||||
*/
|
||||
export const sendRequest = (xhr, body) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
var response
|
||||
try {
|
||||
response = JSON.parse(xhr.response)
|
||||
} catch (err) {
|
||||
response = xhr.response
|
||||
}
|
||||
resolve(response)
|
||||
} else {
|
||||
reject(xhr.response)
|
||||
}
|
||||
}
|
||||
xhr.onerror = () => reject(xhr.response)
|
||||
xhr.send(JSON.stringify(body))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a XHR request with certain form data
|
||||
*
|
||||
* @param {XMLHttpRequest} xhr
|
||||
* @param {Object} data
|
||||
*/
|
||||
export const sendFormRequest = (xhr, data) => {
|
||||
const body = new FormData()
|
||||
for (var name in data) {
|
||||
body.append(name, data[name])
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
var response
|
||||
try {
|
||||
response = JSON.parse(xhr.response)
|
||||
} catch (err) {
|
||||
response = xhr.response
|
||||
}
|
||||
resolve(response)
|
||||
} else {
|
||||
reject(xhr.response)
|
||||
}
|
||||
}
|
||||
xhr.onerror = () => reject(xhr.response)
|
||||
xhr.send(body)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and sends XHR request
|
||||
*
|
||||
* @param {Object} options
|
||||
*
|
||||
* @returns Promise
|
||||
*/
|
||||
export default function (options) {
|
||||
const xhr = createRequest(options)
|
||||
|
||||
return sendRequest(xhr, options.body)
|
||||
}
|
@ -3,10 +3,10 @@ const API_PATH = "http://localhost:8080"
|
||||
/**
|
||||
*
|
||||
* @param api
|
||||
* @param {'GET'|'POST'|string} method
|
||||
* @param {'GET'|'POST'|'FILE'|string} method
|
||||
* @param postData
|
||||
*/
|
||||
function request(api, method = 'GET', postData = {}) {
|
||||
function request(api, method = 'GET', postData = {}, progressChange = null) {
|
||||
//return fetch(API_PATH + api).then(res => res.json());
|
||||
let options = {
|
||||
method
|
||||
@ -18,6 +18,11 @@ function request(api, method = 'GET', postData = {}) {
|
||||
params.push(`${key}=${postData[key]}`);
|
||||
}
|
||||
api += (api.includes('?') ? '&' : '?') + params.join('&');
|
||||
} else if (method.toUpperCase() == 'FILE') {
|
||||
options = {
|
||||
method: 'POST',
|
||||
body: postData // 参数
|
||||
}
|
||||
} else {
|
||||
options = {
|
||||
method,
|
||||
@ -40,7 +45,8 @@ function request(api, method = 'GET', postData = {}) {
|
||||
reject(new Error(result.message))
|
||||
}
|
||||
}
|
||||
fetch(API_PATH + api, options).then(res => res.json())
|
||||
fetch(API_PATH + api, options)
|
||||
.then(res => res.json())
|
||||
.then(processResult)
|
||||
.catch(e => {
|
||||
reject(e);
|
||||
@ -67,5 +73,41 @@ export default {
|
||||
create(parent, name) {
|
||||
return request(`/api/folder/create`, 'POST', {parent, name});
|
||||
}
|
||||
},
|
||||
upload(parent, file, onProcess = null) {
|
||||
// 上传文件到某个目录
|
||||
const postData = new FormData(); // 将数据封装成form表单
|
||||
postData.append("parent", parent); // 父目录
|
||||
postData.append("file", file);// 文件
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = new XMLHttpRequest();
|
||||
request.open('POST', API_PATH + '/api/upload');
|
||||
request.upload.addEventListener('progress', function (e) {
|
||||
// upload progress as percentage
|
||||
let progress = (e.loaded / e.total) * 100;
|
||||
onProcess({
|
||||
uploaded: e.loaded,
|
||||
total: e.total,
|
||||
progress
|
||||
})
|
||||
});
|
||||
request.addEventListener('load', function (e) {
|
||||
// HTTP status message (200, 404 etc)
|
||||
let ret = {message: null}
|
||||
try {
|
||||
ret = JSON.parse(request.response)
|
||||
if (request.status == 200) {
|
||||
resolve(JSON.parse(request.response));
|
||||
} else {
|
||||
reject(Error(ret.message || '上传文件出错!'));
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
reject(Error('上传文件异常'))
|
||||
}
|
||||
});
|
||||
request.send(postData);
|
||||
})
|
||||
// return request('/api/upload', 'FILE', postData, onProcess)
|
||||
}
|
||||
}
|
4
web/src/service/type.d.ts
vendored
4
web/src/service/type.d.ts
vendored
@ -4,6 +4,10 @@ declare type FileItem = {
|
||||
id: number,
|
||||
name: string,
|
||||
path: string,
|
||||
/**
|
||||
* 缩略图
|
||||
*/
|
||||
thumb: string,
|
||||
size: number,
|
||||
type: 'folder' | string,
|
||||
updateTime: string
|
||||
|
Loading…
x
Reference in New Issue
Block a user