Vue+SpringBoot实现仿网盘项目

目录

一、效果展示

二、前端代码

三、后端代码及核心解释

四、进阶开发与思路


一、效果展示

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


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/792235.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Spark SQL 概述

Spark SQL 概述 Spark SQL 是 Apache Spark 的一个模块&#xff0c;专门用于处理结构化数据。它集成了 SQL 查询和 Spark 编程的强大功能&#xff0c;使得处理大数据变得更加高效和简便。通过 Spark SQL&#xff0c;用户可以直接在 Spark 中使用 SQL 查询&#xff0c;或者使用 …

C++基础语法:链表和数据结构

前言 "打牢基础,万事不愁" .C的基础语法的学习 引入 链表是最基础的数据集合,对标数组.数组是固定长度,随机访问,链表是非固定长度,不能随机访问.数组查找快,插入慢;链表是插入快,查找慢. 前面推导过"数据结构算法数据集合".想建立一个数据集合,就要设计数…

Python-找客户软件

软件功能 请求代码&#xff1a; 填充表格&#xff1a; 可以search全国各个区县的所有企业信息&#xff0c;过滤手机号、查看是否续存/在业状态。方便找客户。 支持定-制-其他引-留-阮*件&#xff08;XHSS&#xff0c;DYY&#xff0c;KS&#xff0c;Bi-li*Bi-li&#xff09; V*…

嵌入式转行2个星期,一些真心话建议~

在开始前刚好我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「嵌入式的资料从专业入门到高级教程」&#xff0c; 点个关注在评论区回复“888”之后私信回复“888”&#xff0c;全部无偿共享给大家&#xff01;&#xff01;&#xff01; 嵌入式转行2个星期&…

【计算机方向】中科院三区,国人发文占比>50%,录用容易,认可度不低~

今天小编带来计算机领域SCI快刊的解读&#xff01; 如有相关领域作者有意投稿&#xff0c;可作为重点关注&#xff01; 期刊解析 01 期刊信息 出版商&#xff1a;Springer Singapore ISSN&#xff1a;1672-6529 E-ISSN&#xff1a;2543-2141 期刊官方网站: https://www.sprin…

市面上值得入手的骨传导耳机怎么选?一次给你搞定全方位的选购攻略

随着骨传导耳机市场的日益发展&#xff0c;有很多人使用了一些不合适的骨传导耳机导致听力损伤等问题&#xff0c;这些问题也引起很多人日益关注的。原因大致就是&#xff0c;市面上出现了大量由非专业品牌贴牌和有网红生产的骨传导耳机产品&#xff0c;他们的核心技术的研发和…

CSDN回顾与前行:我的创作纪念日——2048天的技术成长与感悟

CSDN回顾与前行&#xff1a;我的创作纪念日——2048天的技术成长与感悟 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 前言 时光荏苒&#xff0c;岁月如梭。转眼间&#xff0c;从我在CSDN上写下第一篇技术博客《2-6 带头结点的链式表操作…

寻找并可视化交互

「AI秘籍」系列课程&#xff1a; 人工智能应用数学基础 人工智能Python基础 人工智能基础核心知识 人工智能BI核心知识 人工智能CV核心知识 使用特征重要性、弗里德曼 H 统计量和 ICE 图分析相互作用 本文中的代码需要安装 R 语言包 药物的副作用可能取决于你的性别。吸入…

解决浏览器 CORS跨域问题

跨域问题其实就是不同源请求导致 解决跨域问题时&#xff0c;Chrome 插件 requestly 解决 1、Chrome 应用商店 &#xff1a;chrome://extensions/ 搜索 requestly 插件。并添加到扩展程序 2、打开扩展程序&#xff0c;为当前接口设置请求头 在response Header 中设置 Acce…

samba共享windows和ubuntu的文件

通过Samba服务器实现Windows与Ubuntu之间的文件共享是一个常见的需求&#xff0c;下面是实现这一目标的详细步骤&#xff1a; 一、Ubuntu开启Samba服务器 安装Samba&#xff1a; 打开终端&#xff0c;使用以下命令安装Samba服务&#xff1a; sudo apt update sudo apt install…

辐射神经场算法——Instant-NGP / Mipi-NeRF 360 / 3D Gaussian Splatting

辐射神经场算法——Instant-NGP / Mipi-NeRF 360 / 3D Gaussian Splatting 1. Instant-NGP1. MultiResolution Hash Encoding1.2 Accelerated Ray Marching1.3 实验结果 2. Mip-NeRF 3602.1 场景参数化2.2 在线蒸馏2.3 失真正则化2.4 实验结果 3. 3D Gaussian Splatting3.1 Dif…

Monaco 使用 DefinitionProvider

DefinitionProvider 可以弹出方法定义&#xff0c;效果如下&#xff0c;按住 command 鼠标左键&#xff0c;弹出方法说明。 点击时 Monaco Editor 会调用注册函数&#xff0c;注册函数返回文件地址和需要显示的位置&#xff0c;实现代码如下 return monaco.languages.register…

自主研发接口测试框架

测试任务&#xff1a;将以前完成的所有的脚本统一改写为unitest框架方式 1、需求原型 1.1 框架目录结构 V1.0&#xff1a;一般的设计思路分为配置层、脚本层、数据层、结果层&#xff0c;如下图所示 V 2.0&#xff1a;加入驱动层testdriver 1.2 框架各层需要完成的工作 1、配…

Swiper轮播图实现

如上图&#xff0c;列表左右滚动轮播&#xff0c;用户鼠标移动到轮播区域&#xff0c;动画停止&#xff0c;鼠标移开轮播继续。 此例子实现技术框架是用的ReactCSS。 主要用的是css的transform和transition来实现左右切换动画效果。 React代码&#xff1a; import React, { us…

WEB-INF 泄露-RoarCTF-2019-EasyJava(BUUCTF)

题目页面 点开help 这里存在文件下载漏洞&#xff0c;参数选择POST传参&#xff08;使用HackBar插件&#xff09; 查看文件内容 下载存有web信息的XML文件&#xff0c;这里补充一点知识点 WEB-INF主要包含一下文件或目录&#xff1a; /WEB-INF/web.xml&#xff1a;Web应用程序…

关于思维和智能体模型的思考(1)

思维的本质&#xff1a;它的能力似乎源自于那些智能体之间复杂的交错关联 --马文 明斯基 最近阅读美国马文 明斯基写的书《心智社会》&#xff0c;觉得忽然开朗。他对人类思维&#xff0c;智能&#xff0c;智能体等概念做了十分优雅的解读。 个人觉得&#xff0c;他利…

【银河麒麟高级服务器操作系统】数据中心系统异常卡死分析处理建议

了解银河麒麟操作系统更多全新产品&#xff0c;请点击访问&#xff1a;https://product.kylinos.cn 1.服务器环境以及配置 【机型】浪潮NF5280M5 处理器&#xff1a; Intel 内存&#xff1a; 1T 【内核版本】 4.19.90-24.4.v2101.ky10.x86_64 【OS镜像版本】 银河麒麟…

element UI时间组件两种使用方式

加油&#xff0c;新时代打工&#xff01; 组件官网&#xff1a;https://element.eleme.cn/#/zh-CN/component/date-picker 先上效果图&#xff0c;如下&#xff1a; 第一种实现方式 <div class"app-container"><el-formref"submitForm":model&q…

滑动窗口,最长子序列最好的选择 -> O(N)

最近在学校上短学期课程&#xff0c;做程序设计题&#xff0c;一下子回忆起了大一学数据结构与算法的日子&#xff01; 这十天我会记录一些做题的心得&#xff0c;今天带来的是对于最长子序列长度题型的解题框架&#xff1a;滑动窗口 本质就是双指针算法&#xff1a; 通过le…

VSCode神仙插件——通义灵码 (AI编程助手)

1、安装&登录插件 安装时,右下角会有弹窗,让你登录该软件 同意登录后,会跳转浏览器页面 VSCode右下角出现如下图标即登录成功 2、使用 (1)点击左侧栏中的如下图标,打开通义灵码,可以进行智能问答 (2) 选中代码,右键 但是,上述所有的操作会在左侧问答栏中提供答案,并无法直…