初步实现文件上传
This commit is contained in:
parent
6600a6e697
commit
45bccc6ba0
@ -25,6 +25,12 @@
|
|||||||
<!-- <groupId>org.springframework.boot</groupId>-->
|
<!-- <groupId>org.springframework.boot</groupId>-->
|
||||||
<!-- <artifactId>spring-boot-starter-amqp</artifactId>-->
|
<!-- <artifactId>spring-boot-starter-amqp</artifactId>-->
|
||||||
<!-- </dependency>-->
|
<!-- </dependency>-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.coobird</groupId>
|
||||||
|
<artifactId>thumbnailator</artifactId>
|
||||||
|
<version>0.4.17</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
<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 String hash;
|
||||||
private Long folderId;
|
private Long folderId;
|
||||||
private String type;
|
private String type;
|
||||||
private Integer size;
|
private Long size;
|
||||||
private String path;
|
private String path;
|
||||||
private Date createTime;
|
private Date createTime;
|
||||||
private Date updateTime;
|
private Date updateTime;
|
||||||
|
@ -12,15 +12,23 @@ import java.util.List;
|
|||||||
public class FileService extends ServiceImpl<FileInfoMapper, FileInfo> {
|
public class FileService extends ServiceImpl<FileInfoMapper, FileInfo> {
|
||||||
/**
|
/**
|
||||||
* 查询了目录下的文件列表集合
|
* 查询了目录下的文件列表集合
|
||||||
|
*
|
||||||
* @param uid
|
* @param uid
|
||||||
* @param folderId
|
* @param folderId
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public List<FileInfo> listByFolderId(int uid,long folderId){
|
public List<FileInfo> listByFolderId(int uid, long folderId) {
|
||||||
QueryWrapper q = new QueryWrapper();
|
QueryWrapper q = new QueryWrapper();
|
||||||
q.eq("uid",uid);
|
q.eq("uid", uid);
|
||||||
q.eq("folder_id",folderId);
|
q.eq("folder_id", folderId);
|
||||||
q.eq("status",1); // TODO 枚举
|
q.eq("status", 1); // TODO 枚举
|
||||||
return this.list(q);
|
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.setUid(uid);
|
||||||
fNew.setName(name);
|
fNew.setName(name);
|
||||||
fNew.setParentId(parentId);
|
fNew.setParentId(parentId);
|
||||||
fNew.setPath(parent + "/" + name);
|
fNew.setPath((parent.equals("/")?"":parent) + "/" + name);
|
||||||
;
|
|
||||||
return this.save(fNew) ? fNew : null;
|
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:
|
pathmatch:
|
||||||
# 因为Springfox使用的路径匹配是基于AntPathMatcher的,而Spring Boot 2.6.X使用的是PathPatternMatcher。
|
# 因为Springfox使用的路径匹配是基于AntPathMatcher的,而Spring Boot 2.6.X使用的是PathPatternMatcher。
|
||||||
matching-strategy: ant_path_matcher
|
matching-strategy: ant_path_matcher
|
||||||
|
|
||||||
|
# 上传
|
||||||
|
servlet:
|
||||||
|
multipart:
|
||||||
|
max-file-size: 100MB
|
||||||
|
max-request-size: 100MB
|
||||||
springfox:
|
springfox:
|
||||||
documentation:
|
documentation:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
51
web/package-lock.json
generated
51
web/package-lock.json
generated
@ -9,13 +9,16 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^1.1.4",
|
"@element-plus/icons-vue": "^1.1.4",
|
||||||
|
"browser-md5-file": "^1.1.1",
|
||||||
"element-plus": "^2.1.11",
|
"element-plus": "^2.1.11",
|
||||||
|
"fetch-progress": "^1.3.0",
|
||||||
"less": "^4.1.2",
|
"less": "^4.1.2",
|
||||||
"less-loader": "^10.2.0",
|
"less-loader": "^10.2.0",
|
||||||
"qs": "^6.10.3",
|
"qs": "^6.10.3",
|
||||||
"vue": "^3.2.25",
|
"vue": "^3.2.25",
|
||||||
"vue-router": "^4.0.15",
|
"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": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^2.3.1",
|
"@vitejs/plugin-vue": "^2.3.1",
|
||||||
@ -491,6 +494,14 @@
|
|||||||
"resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.1.1.tgz",
|
"resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.1.1.tgz",
|
||||||
"integrity": "sha512-p4DO/JXwjs8klJyJL8Q2oM4ks5fUTze/h5k10oPPKMiLe1fj3G1QMzPHNmN1Py4ycOk7WlO2DcGXv1qiESJCZA=="
|
"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": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.20.3",
|
"version": "4.20.3",
|
||||||
"resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.20.3.tgz",
|
"resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.20.3.tgz",
|
||||||
@ -1081,6 +1092,11 @@
|
|||||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||||
"peer": true
|
"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": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.2",
|
"version": "2.3.2",
|
||||||
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz",
|
"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",
|
"resolved": "https://registry.npmmirror.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
|
||||||
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="
|
"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": {
|
"node_modules/supports-color": {
|
||||||
"version": "8.1.1",
|
"version": "8.1.1",
|
||||||
"resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz",
|
"resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz",
|
||||||
@ -1819,6 +1840,11 @@
|
|||||||
"vue": "^3.2.31"
|
"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": {
|
"node_modules/watchpack": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmmirror.com/watchpack/-/watchpack-2.3.1.tgz",
|
"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",
|
"resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.1.1.tgz",
|
||||||
"integrity": "sha512-p4DO/JXwjs8klJyJL8Q2oM4ks5fUTze/h5k10oPPKMiLe1fj3G1QMzPHNmN1Py4ycOk7WlO2DcGXv1qiESJCZA=="
|
"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": {
|
"browserslist": {
|
||||||
"version": "4.20.3",
|
"version": "4.20.3",
|
||||||
"resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.20.3.tgz",
|
"resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.20.3.tgz",
|
||||||
@ -2683,6 +2717,11 @@
|
|||||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||||
"peer": true
|
"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": {
|
"fsevents": {
|
||||||
"version": "2.3.2",
|
"version": "2.3.2",
|
||||||
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz",
|
"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",
|
"resolved": "https://registry.npmmirror.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
|
||||||
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="
|
"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": {
|
"supports-color": {
|
||||||
"version": "8.1.1",
|
"version": "8.1.1",
|
||||||
"resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz",
|
"resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz",
|
||||||
@ -3226,6 +3270,11 @@
|
|||||||
"click-outside-vue3": "^4.0.1"
|
"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": {
|
"watchpack": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmmirror.com/watchpack/-/watchpack-2.3.1.tgz",
|
"resolved": "https://registry.npmmirror.com/watchpack/-/watchpack-2.3.1.tgz",
|
||||||
|
@ -9,13 +9,16 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^1.1.4",
|
"@element-plus/icons-vue": "^1.1.4",
|
||||||
|
"browser-md5-file": "^1.1.1",
|
||||||
"element-plus": "^2.1.11",
|
"element-plus": "^2.1.11",
|
||||||
|
"fetch-progress": "^1.3.0",
|
||||||
"less": "^4.1.2",
|
"less": "^4.1.2",
|
||||||
"less-loader": "^10.2.0",
|
"less-loader": "^10.2.0",
|
||||||
"qs": "^6.10.3",
|
"qs": "^6.10.3",
|
||||||
"vue": "^3.2.25",
|
"vue": "^3.2.25",
|
||||||
"vue-router": "^4.0.15",
|
"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": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^2.3.1",
|
"@vitejs/plugin-vue": "^2.3.1",
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
<script>
|
<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 FileIcon from "./components/FileIcon.vue";
|
||||||
import {dayjs, ElMessage, ElMessageBox} from 'element-plus'
|
import {dayjs, ElMessage, ElMessageBox} from 'element-plus'
|
||||||
import api from "./service/api";
|
import api from "./service/api";
|
||||||
import qs from "qs";
|
import qs from "qs";
|
||||||
|
import FileUploader from "./components/file-uploader/Index.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
setup(){
|
||||||
|
return {Search}
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
currentActiveIndex: "all",
|
currentActiveIndex: "all",
|
||||||
@ -58,6 +61,9 @@ export default {
|
|||||||
loading: false,
|
loading: false,
|
||||||
message: ''
|
message: ''
|
||||||
},
|
},
|
||||||
|
search:{
|
||||||
|
value:''
|
||||||
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -74,7 +80,7 @@ export default {
|
|||||||
return pathList;
|
return pathList;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {FileIcon, ArrowDown, Grid},
|
components: {FileUploader, FileIcon, ArrowDown, Grid, FolderAdd},
|
||||||
mounted() {
|
mounted() {
|
||||||
// window.addEventListener('popstate',()=>console.log(location.href))
|
// window.addEventListener('popstate',()=>console.log(location.href))
|
||||||
window.addEventListener('hashchange', this.handleHashChange) // 添加监听
|
window.addEventListener('hashchange', this.handleHashChange) // 添加监听
|
||||||
@ -115,6 +121,13 @@ export default {
|
|||||||
f = Math.floor(Math.log(a) / Math.log(c));
|
f = Math.floor(Math.log(a) / Math.log(c));
|
||||||
return parseFloat((a / Math.pow(c, f)).toFixed(d)) + " " + e[f];
|
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) {
|
showFile(file, e) {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
if (file.type != 'folder') {
|
if (file.type != 'folder') {
|
||||||
@ -158,12 +171,12 @@ export default {
|
|||||||
// 开始新建
|
// 开始新建
|
||||||
this.createFolder.loading = true;
|
this.createFolder.loading = true;
|
||||||
try {
|
try {
|
||||||
await api.folder.create(this.getCurrentPath(),this.createFolder.value);
|
await api.folder.create(this.getCurrentPath(), this.createFolder.value);
|
||||||
this.createFolder.visible = false;
|
this.createFolder.visible = false;
|
||||||
this.handleHashChange();
|
this.handleHashChange();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.createFolder.message = e.message;
|
this.createFolder.message = e.message;
|
||||||
}finally {
|
} finally {
|
||||||
this.createFolder.loading = false // 取消loading
|
this.createFolder.loading = false // 取消loading
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,7 +218,28 @@ export default {
|
|||||||
</el-aside>
|
</el-aside>
|
||||||
<el-container>
|
<el-container>
|
||||||
<el-header class="pan-header">
|
<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">
|
<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>帮助</li>
|
||||||
<li>
|
<li>
|
||||||
<el-dropdown>
|
<el-dropdown>
|
||||||
@ -227,27 +261,10 @@ export default {
|
|||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
</el-header>
|
</el-header>
|
||||||
<el-main>
|
<el-main>
|
||||||
<div class="d-flex">
|
<div style="line-height: 30px;" 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="flex:1;display: flex">
|
<div style="flex:1;display: flex">
|
||||||
<div v-if="currentPath === '/'" style="font-weight: 700;font-size: 14px;">全部文件</div>
|
<div v-if="currentPath === '/'" style="font-weight: 700;font-size: 14px;">全部文件</div>
|
||||||
@ -299,7 +316,7 @@ export default {
|
|||||||
<!-- 自定义单元格内容 -->
|
<!-- 自定义单元格内容 -->
|
||||||
<template #default="file">
|
<template #default="file">
|
||||||
<div class="list-file-info">
|
<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"/>
|
:ext="file.row.type"/>
|
||||||
<span class="list-file-name">{{ file.row.name }}</span>
|
<span class="list-file-name">{{ file.row.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -323,7 +340,7 @@ export default {
|
|||||||
<div class="file-image">
|
<div class="file-image">
|
||||||
<FileIcon :file="file" style="width:90%"/>
|
<FileIcon :file="file" style="width:90%"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="file-name">{{ file.name }}</div>
|
<div class="file-name">{{ formatName(file.name) }}</div>
|
||||||
<div class="file-info">
|
<div class="file-info">
|
||||||
{{
|
{{
|
||||||
file.type == 'folder' ? formatDate(file.createTime) : formatSize(file.size)
|
file.type == 'folder' ? formatDate(file.createTime) : formatSize(file.size)
|
||||||
@ -356,28 +373,6 @@ export default {
|
|||||||
</template>
|
</template>
|
||||||
<style lang="less">
|
<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 {
|
.file-block-item {
|
||||||
|
@ -29,6 +29,14 @@
|
|||||||
.d-flex{
|
.d-flex{
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
.flex-1{
|
||||||
|
flex:1;
|
||||||
|
}
|
||||||
|
.search-box{
|
||||||
|
.el-input__wrapper{
|
||||||
|
border-radius: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
.logo-icon{
|
.logo-icon{
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
@ -38,6 +46,28 @@
|
|||||||
margin-right: 10px;
|
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 {
|
.pan-header-user-right {
|
||||||
float: right;
|
float: right;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<span style="display:inline-block">
|
<span style="display:inline-block">
|
||||||
<el-image v-if="type == 'picture'"
|
<el-image v-if="type == 'picture'"
|
||||||
:src="currentSrc" :previewSrcList="[currentSrc]"
|
:src="currentSrc" :previewSrcList="[currentPreview]"
|
||||||
:initial-index="4" fit="cover" :hide-on-click-modal="true"
|
:initial-index="4" fit="cover" :hide-on-click-modal="true"
|
||||||
/>
|
/>
|
||||||
<img v-else class="file-icon" :src="currentSrc"/>
|
<img v-else class="file-icon" :src="currentSrc"/>
|
||||||
@ -18,6 +18,9 @@ import {ElMessage} from "element-plus";
|
|||||||
export default {
|
export default {
|
||||||
// 定义组件所需的属性
|
// 定义组件所需的属性
|
||||||
props: {
|
props: {
|
||||||
|
/**
|
||||||
|
*@property {FileItem}
|
||||||
|
*/
|
||||||
file: {
|
file: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
@ -29,13 +32,15 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
currentSrc: FolderIcon,
|
currentSrc: FolderIcon,
|
||||||
|
currentPreview: FolderIcon,
|
||||||
type: 'icon'
|
type: 'icon'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
const type = this.file.type.toLowerCase(); // 拿到文件类型
|
const type = this.file.type.toLowerCase(); // 拿到文件类型
|
||||||
if (['png', 'jpg', 'jpeg', 'gif', 'webp'].includes(type)) {
|
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';
|
this.type = 'picture';
|
||||||
} else if (type == 'exe') {
|
} else if (type == 'exe') {
|
||||||
this.currentSrc = ExeIcon; // 图片
|
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 api
|
||||||
* @param {'GET'|'POST'|string} method
|
* @param {'GET'|'POST'|'FILE'|string} method
|
||||||
* @param postData
|
* @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());
|
//return fetch(API_PATH + api).then(res => res.json());
|
||||||
let options = {
|
let options = {
|
||||||
method
|
method
|
||||||
@ -18,6 +18,11 @@ function request(api, method = 'GET', postData = {}) {
|
|||||||
params.push(`${key}=${postData[key]}`);
|
params.push(`${key}=${postData[key]}`);
|
||||||
}
|
}
|
||||||
api += (api.includes('?') ? '&' : '?') + params.join('&');
|
api += (api.includes('?') ? '&' : '?') + params.join('&');
|
||||||
|
} else if (method.toUpperCase() == 'FILE') {
|
||||||
|
options = {
|
||||||
|
method: 'POST',
|
||||||
|
body: postData // 参数
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
options = {
|
options = {
|
||||||
method,
|
method,
|
||||||
@ -40,7 +45,8 @@ function request(api, method = 'GET', postData = {}) {
|
|||||||
reject(new Error(result.message))
|
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)
|
.then(processResult)
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
reject(e);
|
reject(e);
|
||||||
@ -67,5 +73,41 @@ export default {
|
|||||||
create(parent, name) {
|
create(parent, name) {
|
||||||
return request(`/api/folder/create`, 'POST', {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,
|
id: number,
|
||||||
name: string,
|
name: string,
|
||||||
path: string,
|
path: string,
|
||||||
|
/**
|
||||||
|
* 缩略图
|
||||||
|
*/
|
||||||
|
thumb: string,
|
||||||
size: number,
|
size: number,
|
||||||
type: 'folder' | string,
|
type: 'folder' | string,
|
||||||
updateTime: string
|
updateTime: string
|
||||||
|
Loading…
x
Reference in New Issue
Block a user