目录
一、效果展示
二、前端代码
三、后端代码及核心解释
四、进阶开发与思路
一、效果展示
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类型的方法,方便序列化为JSONpublic 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 {@Beanpublic 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中的USerIDint 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 转换为 JSONArrayJSONArray 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.云盘存量的设置,可以在遍历用户文件时候计算总大小,并返回给前端展示。