初步实现文件上传

This commit is contained in:
LittleBoy 2022-05-10 08:33:08 +08:00
parent 6600a6e697
commit 45bccc6ba0
20 changed files with 2516 additions and 83 deletions

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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
View File

@ -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",

View File

@ -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",

View File

@ -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 {

View File

@ -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;

View File

@ -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; //

File diff suppressed because it is too large Load Diff

View 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', // inputid
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>

View 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>

View 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
}
}

View 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)
}
}

View 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)
}

View File

@ -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)
}
}

View File

@ -4,6 +4,10 @@ declare type FileItem = {
id: number,
name: string,
path: string,
/**
*
*/
thumb: string,
size: number,
type: 'folder' | string,
updateTime: string