目录
一、效果展示
二、前端代码
三、后端代码及核心解释
四、进阶开发与思路
一、效果展示
1.1读取文件夹内的文件
1.2删除功能
1.3 上传文件
1.4 文件下载
对应的网盘实际地址与对应下载内容:
二、前端代码
2.1 创建vue项目(需要有vuex与router)并引入elementUi
npm i element-ui -S
2.2设置 VUEX(index.js):
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
// store/index.js
export default new Vuex.Store({
state: {
selectedFiles: []
},
mutations: {
ADD_TO_SELECTED(state, fileName) {
state.selectedFiles.push(fileName);
},
REMOVE_FROM_SELECTED(state, fileName) {
const index = state.selectedFiles.indexOf(fileName);
if (index !== -1) {
state.selectedFiles.splice(index, 1);
}
},
REMOVE_ALL(state) {
state.selectedFiles = [];
}
},
// ...
});
组件:FileCard Component:
<template>
<div class="file-cards" style="line-height: normal;">
<div v-for="(file, index) in fileList" :key="index" class="file-card" @click="toggleControlsAndSelect(index)">
<i :class="[file.isDir ? 'el-icon-folder-opened' : 'el-icon-document', 'file-icon']"></i>
<div class="file-name">{{ file.name }}</div>
<!-- 添加勾选框 -->
<el-checkbox-group v-model="selectedFiles" @change="handleGroupChange">
<el-checkbox :label="file.name" class="checkbox"></el-checkbox>
</el-checkbox-group>
</div>
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex';
export default {
props: {
fileList: {
type: Array,
required: true,
},
},
computed:{
...mapState(['selectedFiles']),
},
data() {
return {
// selectedFiles: [], // 用于存储被选中的文件名
};
},
methods: {
...mapMutations(['ADD_TO_SELECTED', 'REMOVE_FROM_SELECTED']),
handleGroupChange(values) {
values.forEach(value => this.ADD_TO_SELECTED(value));
this.fileList.filter(file => !values.includes(file.name)).forEach(file =>
this.REMOVE_FROM_SELECTED(file.name)
);
},
toggleControlsAndSelect(index) {
const fileName = this.fileList[index].name;
if (this.selectedFiles.includes(fileName)) {
this.REMOVE_FROM_SELECTED(fileName);
} else {
this.ADD_TO_SELECTED(fileName);
}
},
},
};
</script>
<style scoped>
.file-cards {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.file-card {
background-color: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
margin: 10px;
padding: 20px;
width: 200px;
cursor: pointer;
transition: all 0.3s;
}
.file-card:hover {
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.12);
}
.file-icon {
font-size: 50px;
color: #409eff;
}
.file-name {
text-align: center;
margin-top: 10px;
}
</style>
根组件:App.vue
Html:
<template>
<div id="Pan" style="border: 1px solid black; min-height: 90%; background-color: rgb(250, 250, 250);">
<!-- 操作板块 -->
<div id="Operate"
style="height: 50px; line-height: normal; border-top: 1px solid rgb(250, 250, 250); margin-top: 25px; ">
<el-upload class="upload-demo" action="/api/file/upload" :on-change="handleChange" :file-list="fileList" :show-file-list="showFileList"
style=" display: inline-block;">
<el-button type="primary">
<i class="el-icon-upload2"></i>
上传</el-button>
</el-upload>
<el-button type="success" style="margin-left: 10px;" @click="downloadSelectedFiles">
<i class="el-icon-download"></i>
下载</el-button>
<el-button type="primary" plain>
<i class="el-icon-share"></i>
分享</el-button>
<el-button type="danger" plain @click="deleteFile">
<i class="el-icon-delete"></i>
删除</el-button>
</div>
<!-- 导航板块 -->
<div id="navigation">
<el-breadcrumb separator-class="el-icon-arrow-right" style="padding-left: 10%; line-height: normal;">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item :to="{ path: '/' }">我的网盘</el-breadcrumb-item>
</el-breadcrumb>
</div>
<!-- 全选文件 -->
<div style="height: 35px; background-color: white; border: 1px solid rgb(230, 230, 230);">
<el-checkbox :indeterminate="isIndeterminate" v-model="checkAll" @change="handleCheckAllChange"
style="float: left; line-height: 35px; padding-left: 20%;">全选文件</el-checkbox>
</div>
<div id="FileList">
<file-cards :file-list="sampleFiles" @update-file-selection="handleFileSelectionUpdate"></file-cards>
</div>
</div>
</template>
Javascript:
<script>
import FileCards from '@/components/FileCards.vue';
import Cookies from 'js-cookie';
import { mapState, mapMutations } from 'vuex';
import axios from 'axios';
export default {
computed: {
...mapState(['selectedFiles']),
},
components: {
FileCards,
},
data() {
return {
sampleFiles: [
],
// 文件全选
isIndeterminate: false,
checkAll: false,
fileList: [],
showFileList:false,
};
},
methods: {
...mapMutations(['ADD_TO_SELECTED', 'REMOVE_FROM_SELECTED', 'SET_ALL_SELECTED', 'REMOVE_ALL']),
ListUserFiles() {
const id = Cookies.get("userId");
if (id === null) {
this.$notify({
title: '警告',
message: '请还未登录,无法使用本功能',
type: 'warning'
});
return;
}
},
addToSelected(fileName) {
if (!this.selectedFiles.includes(fileName)) {
this.selectedFiles.push(fileName);
}
},
removeFromSelected(fileName) {
const index = this.selectedFiles.indexOf(fileName);
if (index !== -1) {
this.selectedFiles.splice(index, 1);
}
},
handleFileSelectionUpdate(fileName, isChecked) {
if (isChecked) {
this.addToSelected(fileName);
} else {
this.removeFromSelected(fileName);
}
},
// 全选文件
handleCheckAllChange() {
if (this.selectedFiles.length === this.sampleFiles.length) {
this.REMOVE_ALL();
return;
}
this.REMOVE_ALL();
this.sampleFiles.forEach(file => {
this.ADD_TO_SELECTED(file.name);
});
},
// 上传文件
handleChange() {
console.log(this.fileList);
this.$message.success('上传成功');
this.getPanlist();
},
// 获取网盘文件列表
getPanlist() {
axios.get('api/file/list').then((Response) => {
this.sampleFiles = Response.data.data;
})
},
// 删除文件
deleteFile() {
axios.delete('api/file', {
data: {
fileNames: this.selectedFiles
}
}).then((response) => {
this.REMOVE_ALL();
this.getPanlist();
console.log(response);
});
},
downloadSelectedFiles() {
console.log(this.selectedFiles);
// 确保有文件被选中
if (this.selectedFiles.length === 0) {
alert("请选择要下载的文件!");
return;
}
axios({
url: 'api/file/download',
method: 'POST',
responseType: 'blob', // 告诉axios我们希望接收的数据类型是二进制流
data: {
fileNames: this.selectedFiles
}
}).then(response => {
// 创建一个a标签用于触发下载
let url = window.URL.createObjectURL(new Blob([response.data]));
let link = document.createElement('a');
link.href = url;
// 如果你知道文件名,可以设置下载文件名
link.setAttribute('download', 'download.zip');
document.body.appendChild(link);
link.click();
// 清理
document.body.removeChild(link);
});
},
},
mounted() {
this.ListUserFiles();
this.getPanlist();
},
};
</script>
css:
<style scoped>
#FileList {
margin-top: 20px;
}
#upload {
float: left;
}
</style>
三、后端代码及核心解释
额外的依赖:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
用以构造
3.1 返回类
//结果类
public class Result<T> {
// 状态码常量
public static final int SUCCESS = 200;
public static final int ERROR = 500;
private int code; // 状态码
private String message; // 消息
private T data; // 数据
// 构造函数,用于创建成功的结果对象
private Result(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
// 成功结果的静态方法
public static <T> Result<T> success(T data) {
return new Result<>(SUCCESS, "Success", data);
}
// 错误结果的静态方法
public static <T> Result<T> error(String message) {
return new Result<>(ERROR, message, null);
}
// 错误结果的静态方法,可以传入自定义的状态码
public static <T> Result<T> error(int code, String message) {
return new Result<>(code, message, null);
}
// 获取状态码
public int getCode() {
return code;
}
// 设置状态码
public void setCode(int code) {
this.code = code;
}
// 获取消息
public String getMessage() {
return message;
}
// 设置消息
public void setMessage(String message) {
this.message = message;
}
// 获取数据
public T getData() {
return data;
}
// 设置数据
public void setData(T data) {
this.data = data;
}
// 用于转换为Map类型的方法,方便序列化为JSON
public Map<String, Object> toMap() {
Map<String, Object> map = new HashMap<>();
map.put("code", code);
map.put("message", message);
map.put("data", data);
return map;
}
}
规范化后端返回Response的数据
由于本次上传都是小文件,后端限制在10MB以内.
@Configuration
public class servletMultipartConfigElement {
@Bean
public javax.servlet.MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
// 设置单个文件的最大大小
factory.setMaxFileSize(DataSize.ofMegabytes(10));
// 设置整个请求的最大大小
factory.setMaxRequestSize(DataSize.ofMegabytes(100));
return factory.createMultipartConfig();
}
}
3.2 获取用户的文件内容
// 获取文件内容
@GetMapping("/list")
public Result getListByUserId() {
// TODO:后期以JWT鉴权方式,获取Token中的USerID
int id = 8;
File directory = new File("E:\\ProjectReal\\AI WIth WEB SHell\\Pan" + File.separator + id);
if (!directory.exists()) {
boolean mkdirs = directory.mkdirs();
if (mkdirs){
Result.success("网盘创建成功");
}else {
Result.error("网盘创建失败");
}
return Result.error("异常");
}
// 直接将 fileList 转换为 JSONArray
JSONArray jsonArray = new JSONArray();
File[] files = directory.listFiles();
if (files != null) {
for (File file : files) {
JSONObject fileObj = new JSONObject();
fileObj.put("name", file.getName());
fileObj.put("isDir", file.isDirectory());
fileObj.put("selected", false);
jsonArray.add(fileObj);
}
}
return Result.success(jsonArray);
}
关键点在于通过java的IO与fastjson依赖构造出对应的JSON格式并返回
3.3 下载功能
@PostMapping("/download")
public ResponseEntity<?> downloadSelectedFiles(@RequestBody FileNamesDto fileNamesDto) throws IOException {
List<String> fileNames = fileNamesDto.getFileNames();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ZipOutputStream zos = new ZipOutputStream(baos);
for (String fileName : fileNames) {
File file = new File("E:\\ProjectReal\\AI WIth WEB SHell\\Pan" + File.separator + "8" + File.separator + fileName);
if (file.exists()) {
try (FileInputStream fis = new FileInputStream(file)) {
ZipEntry zipEntry = new ZipEntry(fileName);
zos.putNextEntry(zipEntry);
byte[] bytes = new byte[1024];
int length;
while ((length = fis.read(bytes)) >= 0) {
zos.write(bytes, 0, length);
}
zos.closeEntry();
}
catch (Exception e){
e.printStackTrace();
}
}
}
zos.close();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
headers.setContentDispositionFormData("attachment", "download.zip");
return new ResponseEntity<>(baos.toByteArray(), headers, HttpStatus.OK);
}
关键点在于,下载时候,不同的文件对应的请求头的MIME是不一样的,所以将文件先压缩后下载时候就只有一个文件格式为zip格式。
3.4 删除功能
@DeleteMapping()
public Result deleteFile(@RequestBody FileNamesDto fileNamesDto) {
List<String> fileNames = fileNamesDto.getFileNames();
int id = 8;
for (String fileName : fileNames) {
File file = new File("E:\\ProjectReal\\AI WIth WEB SHell\\Pan" + File.separator + id + File.separator + fileName);
if (!file.exists()) {
return Result.error("文件不存在");
}
if (file.isDirectory()){
deleteDirectory(file);
}else {
boolean delete = file.delete();
}
}
return Result.success("删除完成");
}
public static void deleteDirectory(File directory) {
if (directory.exists()) {
File[] entries = directory.listFiles();
if (entries != null) {
for (File entry : entries) {
if (entry.isDirectory()) {
deleteDirectory(entry);
} else {
entry.delete();
}
}
}
}
directory.delete();
}
注意:对于非空的directory是无法直接进行删除的,所以通过isDir判断如果是目录时候,则进行递归删除。将所有子文件都删除后再对目录进行删除.
3.5 上传功能
@PostMapping("/upload")
public ResponseEntity<?> upload(@RequestParam("file") MultipartFile file) {
try {
// 检查文件是否为空
if (file.isEmpty()) {
return ResponseEntity.badRequest().body("文件为空");
}
// 获取上传文件的原始文件名
String originalFileName = file.getOriginalFilename();
// 创建目录(如果不存在)
File directory = new File("E:\\ProjectReal\\AI WIth WEB SHell\\Pan\\8");
if (!directory.exists()) {
directory.mkdirs();
}
// 文件保存路径
Path targetLocation = Path.of(directory.getAbsolutePath(), originalFileName);
try (InputStream inputStream = file.getInputStream()) {
Files.copy(inputStream, targetLocation, StandardCopyOption.REPLACE_EXISTING);
}
return ResponseEntity.ok("上传成功");
} catch (IOException e) {
return ResponseEntity.status(500).body("上传失败:" + e.getMessage());
}
}
由于前端上传的格式是multipartFIle 格式,所以后端也需要相应类型的进行接收对其进行接收
四、进阶开发与思路
4.1 前端
1.可以通过设置拖拽区域实现,当拖拽文件到网盘内容区时,自动执行上传函数的功能。
2.对于大文件,可以单独写一个对应的大文件上传页面,并展示上传进度条。
4.2 后端
1.大文件上传,首先前端进行判断文件的大小,如果超过一定的大小,则调用大文件上传功能。这时候就需要实现分片上传与断点续传功能。
2.云盘网站用户的独立性,这次演示的是一个固定用户的网盘内容。在实现真正项目时候,可以通过jwt鉴权的方式,获取token中的userId,使得获取到每一个用户自己的网盘。
3.云盘存量的设置,可以在遍历用户文件时候计算总大小,并返回给前端展示。