Vue+SpringBoot实现仿网盘项目

发布于:2024-07-11 ⋅ 阅读:(24) ⋅ 点赞:(0)

目录

一、效果展示

二、前端代码

三、后端代码及核心解释

四、进阶开发与思路


一、效果展示

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.云盘存量的设置,可以在遍历用户文件时候计算总大小,并返回给前端展示。