【前后端的那些事】15min快速实现图片上传,预览功能(ElementPlus+Springboot)

news2024/12/27 3:30:26

文章目录

    • Element Plus + SpringBoot实现图片上传,预览,删除
        • 效果展示
      • 1. 后端代码
        • 1.1 controller
        • 1.2 service
      • 2. 前端代码
        • 2.1 路由创建
        • 2.2 api接口
        • 2.2 文件创建
      • 3. 前端上传组件封装

前言:最近写项目,发现了一些很有意思的功能,想写文章,录视频把这些内容记录下。但这些功能太零碎,如果为每个功能都单独搭建一个项目,这明显不合适。于是我想,就搭建一个项目,把那些我想将的小功能全部整合到一起。实现 搭一次环境,处处使用。

本文主要实现以下功能

  1. 图片上传

环境搭建
文章链接

已录制视频
视频链接

仓库地址
https://github.com/xuhuafeifei/fgbg-font-and-back.git

Element Plus + SpringBoot实现图片上传,预览,删除

效果展示
  • 提交样式
    在这里插入图片描述

  • 放大预览

在这里插入图片描述

  • 成功提交后端
    在这里插入图片描述

  • 访问url

在这里插入图片描述

  • 后端存储
    在这里插入图片描述

  • 根据url下载/访问图片

在这里插入图片描述

1. 后端代码

1.1 controller
import com.fgbg.common.utils.R;
import com.fgbg.demo.service.FileService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;

@RestController
@RequestMapping("/common/file")
public class FileController {
    @Autowired
    @Qualifier("localFileService")
    private FileService fileService;

    /**
     * 上传接口
     */
    @RequestMapping("/upload")
    public R upload(@RequestParam("image") MultipartFile file) throws IOException {
        String url = fileService.uploadFile(file, UUID.randomUUID().toString().substring(0, 10)
                + "-" + file.getOriginalFilename());
        return R.ok().put("data", url);
    }

    /**
     * 下载接口
     */
    @RequestMapping("/download/{fileName}")
    public void download(@PathVariable("fileName") String fileName, HttpServletRequest request, HttpServletResponse response) {
        fileService.downloadFile(fileName, request, response);
    }

    /**
     * 删除接口
     */
    @RequestMapping("/delete")
    public R deleteFile(@RequestParam String fileName) {
        boolean flag = fileService.deleteFile(fileName);
        return R.ok().put("data", flag);
    }
}

1.2 service

tip: 文件上传存储有多种解决方案,比如minio,阿里云…

笔者考虑到编写容易程度与文章核心解决问题,采用了最原始的存储方法,即本地存储。以后端所在服务器为存储容器,将前端上传的图片以FileIO的形式进行存储。

考虑到有多种存储方式,读者可以实现FileService接口,自行编写impl类,以达到不同的文件存储的具体实现方式

import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public interface FileService {
    /**
     * 上传图片, 返回url
     */
    String uploadFile(MultipartFile file, String fileName) throws IOException;

    /**
     * 下载图片
     */
    void downloadFile(String fileName, HttpServletRequest request, HttpServletResponse response);

    /**
     * 删除图片
     */
    boolean deleteFile(String fileName);
}

impl

import com.fgbg.demo.service.FileService;
import org.apache.commons.io.FileUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;

/**
 * 基于本地的文件管理服务
 */
@Service("localFileService")
public class LocalFileServiceImpl implements FileService {
    /**
     * 文件访问域名(请求下载的接口)
     */
    private static final String DOMAIN = "http://localhost:9005/api_demo/common/file/download/";

    /**
     * 文件物理存储位置
     */
    private static final String STORE_DIR = "E:\\B站视频创作\\前后端项目构建-小功能实现\\代码\\backend\\src\\main\\resources\\pict\\";

    /**
     * 上传图片, 返回url
     *
     * @param file
     * @param fileName
     */
    @Override
    public String uploadFile(MultipartFile file, String fileName) throws IOException {
        // 获取文件流
        InputStream is = file.getInputStream();
        // 在服务器中存储文件
        FileUtils.copyInputStreamToFile(is, new File(STORE_DIR + fileName));
        // 返回图片url
        String url = DOMAIN + fileName;
        System.out.println("文件url: " + url);
        return url;
    }

    /**
     * 下载图片
     *
     * @param fileName
     */
    @Override
    public void downloadFile(String fileName, HttpServletRequest request, HttpServletResponse response) {
        // 获取真实的文件路径
        String filePath = STORE_DIR + fileName;
        System.out.println("++++完整路径为:"+filePath);

        try {
            // 下载文件
            // 设置响应头
            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
            response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + fileName);

            // 读取文件内容并写入输出流
            Files.copy(Paths.get(filePath), response.getOutputStream());
            response.getOutputStream().flush();
        } catch (IOException e) {
            response.setStatus(404);
        }
    }

    /**
     * 删除图片
     *
     * @param fileName
     */
    @Override
    public boolean deleteFile(String fileName) {
        // 获取真实的文件路径
        String filePath = STORE_DIR + fileName;
        System.out.println("++++完整路径为:"+filePath);

        File file = new File(filePath);
        return file.delete();
    }
}

2. 前端代码

2.1 路由创建

/src/router/modules/file.ts

const { VITE_HIDE_HOME } = import.meta.env;
const Layout = () => import("@/layout/index.vue");

export default {
  path: "/file",
  name: "file",
  component: Layout,
  redirect: "/pict",
  meta: {
    icon: "homeFilled",
    title: "文件",
    rank: 0
  },
  children: [
    {
      path: "/pict",
      name: "pict",
      component: () => import("@/views/file/pict.vue"),
      meta: {
        title: "图片",
        showLink: VITE_HIDE_HOME === "true" ? false : true
      }
    }
  ]
} as RouteConfigsTable;
2.2 api接口

tip:

  • 文件上传只能用post
  • 前端部分图片封装为FormData对象
  • 请求头标明"Content-Type": "multipart/form-data"
import { http } from "@/utils/http";
import { R, baseUrlApi } from "./utils";

/** upload batch */
export const uploadBatch = (data: FormData) => {
  return http.request<R<any>>("post", baseUrlApi("common/file/uploadList"), {
    data,
    headers: {
      "Content-Type": "multipart/form-data"
    }
  });
};

/** upload */
export const upload = (data: FormData) => {
  return http.request<R<any>>("post", baseUrlApi("common/file/upload"), {
    data,
    headers: {
      "Content-Type": "multipart/form-data"
    }
  });
};
2.2 文件创建

/src/views/file/pict.vue

tip:

  • 图片封装为FormData

  • formdata添加图片信息时,使用的是append()方法. append(name: string, value: string | Blob)

  • append的第一个参数,对应的是后端@RequestParam("xxx") MultipartFile file中xxx的值,本文中后端批量上传接口,xxx值为’imageList’

  • Element Plus上传图片,图片数据中都会有一个新的字段数据raw,这个数据我们就理解成文件本身。像后端提交数据提交的也是raw本身,而非其余额外数据

    在这里插入图片描述

  • append第二个参数,提交的是fileList中每个文件元素的raw属性s数据

<template>
  <el-upload
    v-model:file-list="fileList"
    list-type="picture-card"
    multiple
    :auto-upload="false"
    :on-preview="handlePictureCardPreview"
    :on-remove="handleRemove"
  >
    <el-icon><Plus /></el-icon>
  </el-upload>

  <el-dialog v-model="dialogVisible">
    <img w-full :src="dialogImageUrl" alt="Preview Image" />
  </el-dialog>
  <el-button @click="submit">提交</el-button>
</template>

<script lang="ts" setup>
import { ref } from "vue";
import { Plus } from "@element-plus/icons-vue";
import { uploadBatch } from "/src/api/file.ts";
import type { UploadProps } from "element-plus";
import { ElMessage } from "element-plus";

const submit = () => {
  console.log(fileList.value);
  // 封装formData
  const data = new FormData();
  // forEach遍历的时fileList.value, 所有element不需要.value去除代理
  fileList.value.forEach(element => {
    data.append("imageList", element.raw);
  });
  uploadBatch(data).then(res => {
    console.log(res);
    if (res.code === 0) {
      ElMessage.success("上传成功");
    } else {
      ElMessage.error("上传失败: " + res.msg);
    }
  });
};

const fileList = ref();

const dialogImageUrl = ref("");
const dialogVisible = ref(false);

const handleRemove: UploadProps["onRemove"] = (uploadFile, uploadFiles) => {
  console.log(uploadFile, uploadFiles);
};

const handlePictureCardPreview: UploadProps["onPreview"] = uploadFile => {
  dialogImageUrl.value = uploadFile.url!;
  dialogVisible.value = true;
};
</script>

3. 前端上传组件封装

如果没有组件封装需求,那就不需要修改代码。
组件封装视频链接

tip: 提交逻辑交由父组件实现

child.vue

<template>
  <el-upload
    v-model:file-list="localFileList"
    list-type="picture-card"
    multiple
    :auto-upload="false"
    :on-preview="handlePictureCardPreview"
    :on-remove="handleRemove"
  >
    <el-icon><Plus /></el-icon>
  </el-upload>

  <el-dialog v-model="dialogVisible">
    <img w-full :src="dialogImageUrl" alt="Preview Image" />
  </el-dialog>
</template>

<script lang="ts" setup>
import { ref, watch } from "vue";
import { Plus } from "@element-plus/icons-vue";
import type { UploadProps } from "element-plus";

// 定义数据
const props = defineProps({
  fileList: {
    type: Array,
    default: () => []
  }
});

// 将父组件的数据拆解为子组件数据
const localFileList = ref(props.fileList);

// 监听localFileList, 跟新父组件数据
watch(
  localFileList,
  newValue => {
    emits("update:fileList", newValue);
  },
  {
    deep: true
  }
);

// 定义组件事件, 跟新fileList
const emits = defineEmits(["update:fileList"]);

const dialogImageUrl = ref("");
const dialogVisible = ref(false);

const handleRemove: UploadProps["onRemove"] = (uploadFile, uploadFiles) => {
  console.log(uploadFile, uploadFiles);
};

const handlePictureCardPreview: UploadProps["onPreview"] = uploadFile => {
  dialogImageUrl.value = uploadFile.url!;
  dialogVisible.value = true;
};
</script>

父组件

<script setup lang="ts">
import Child from "./component/child.vue";
import { ref } from "vue";
import { ElMessage } from "element-plus";
import { uploadBatch } from "/src/api/file.ts";

const fileList = ref();

const submit = () => {
  console.log(fileList.value);
  // 封装formData
  const data = new FormData();
  // forEach遍历的时fileList.value, 所有element不需要.value去除代理
  fileList.value.forEach(element => {
    data.append("imageList", element.raw);
  });
  uploadBatch(data).then(res => {
    console.log(res);
    if (res.code === 0) {
      ElMessage.success("上传成功");
    } else {
      ElMessage.error("上传失败: " + res.msg);
    }
  });
};
</script>

<template>
  <Child v-model:fileList="fileList" />
  <el-button @click="submit">提交</el-button>
</template>

<style lang="scss" scoped></style>

效果
在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SD-WAN组网设计原则:灵活、安全、高效

在实现按需、灵活和安全的SD-WAN组网方案中&#xff0c;我们必须遵循一系列关键的设计原则&#xff0c;以确保网络的可靠性和效率。通过以下几点设计原则&#xff0c;SD-WAN能够满足企业对灵活性、安全性和高效性的迫切需求。 灵活的Overlay网络互联 SD-WAN通过IP地址在站点之间…

appium之联动pycharm

前置条件&#xff1a; 1.java环境安装好了 2.android-sdk安装好&#xff08;uiautomatorviewer 也可以把这个启动起来&#xff09; 3.appium安装好 4.adb devices查看下设备是否连接 pycharm入门代码--固定写法 from appium import webdriver# 定义字典变量 desired_caps …

SpringMVC下半篇之异常处理器及日期转换器

3.异常处理器 如果不加以异常处理&#xff0c;错误信息肯定会抛在浏览器页面上&#xff0c;这样很不友好&#xff0c;所以必须进行异常处理。 3.1.异常处理思路 系统的dao、service、controller出现都通过throws Exception向上抛出&#xff0c;最后由springmvc前端控制器交由…

线性代数基础【5】特征值和特征向量

第五章 特征值和特征向量 第一节、特征值和特征向量的基本概念 一、特征值和特征向量的理论背景 在一个多项式中,未知数的个数为任意多个,且每一项次数都是2的多项式称为二次型,二次型分为两种类型:即非标准二次型及标准二次型 注意: ①二次型X^T AX为非标准二次型的充分必…

在线扒站网PHP源码-在线扒站工具网站源码

源码介绍 这是一款在线的网站模板下载程序&#xff0c;也就是我们常说的扒站工具&#xff0c;利用它我们可以很轻松的将别人的网站模板样式下载下来&#xff0c;这样就可以大大提高我们编写前端的速度了&#xff01;注&#xff1a;扒取的任何站点不得用于商业、违法用途&#…

02 MyBatisPlus核心功能之基于Mapper接口CRUD+基于Service接口实现CRUD

项目结构&#xff1a; 1.1 Insert方法 // 插入一条记录 // T 就是要插入的实体对象 // 默认主键生成策略为雪花算法&#xff08;后面讲解&#xff09; //返回值是影响条数 int insert(T entity);1.2 Delete方法 // 根据 entity 条件&#xff0c;删除记录 int delete(Param(…

【PAT甲级】1178 File Path(25分)[文件树,模拟,unordered_map]

问题思路&#xff1a; 在不断输入的过程中&#xff0c;可以通过层深和一个二维的vector数组来建立一棵树。即每输入一个节点&#xff0c;应当作为该节点上一层的最后一个节点的子孩子。用一个哈希value来指定节点的id&#xff0c;通过一个last记录每个节点id&#xff08;此时作…

2023年移远车载全面开花,智能座舱加速进击

作为汽车智能化的关键组件&#xff0c;车载模组正发挥着越来越重要的作用。 移远通信进入车载模组领域近十年&#xff0c;已形成了完善的车载产品队列&#xff0c;不但在5G/4G车载通信、智能座舱、C-V2X车路协同等领域打造了一枝独秀的产品线&#xff0c;也推出了车规级Wi-Fi/蓝…

Verilog基础:强度建模(一)

相关阅读 Verilog基础https://blog.csdn.net/weixin_45791458/category_12263729.html?spm1001.2014.3001.5482 一、强度建模基础 Verilog HDL提供了针对线网信号0、1、x、z的精准强度建模方式&#xff0c;这样可以允许将两个线网信号进行线与操作从而更加精确地描述出硬件行…

【论文阅读】Deep Graph Contrastive Representation Learning

目录 0、基本信息1、研究动机2、创新点3、方法论3.1、整体框架及算法流程3.2、Corruption函数的具体实现3.2.1、删除边&#xff08;RE&#xff09;3.2.2、特征掩盖&#xff08;MF&#xff09; 3.3、[编码器](https://blog.csdn.net/qq_44426403/article/details/135443921)的设…

[python]裁剪文件夹中所有pdf文档并按名称保存到指定的文件夹

最近在写论文的实验部分&#xff0c;由于latex需要pdf格式的文档&#xff0c;审稿专家需要对pdf图片进行裁剪放大&#xff0c;以保证图片质量。 原图&#xff1a; 裁剪后的图像&#xff1a; 代码粘贴如下。将input_folder和output_folder替换即可。(x1, y1)&#xff0c; (x2…

【Go面试向】rune和byte类型的认识与使用

【Go】rune和byte类型的认识与使用 大家好 我是寸铁&#x1f44a; 总结了一篇rune和byte类型的认识与使用的文章✨ 喜欢的小伙伴可以点点关注 &#x1f49d; byte和rune类型定义 byte,占用1个字节&#xff0c;共8个比特位&#xff0c;所以它实际上和uint8没什么本质区别,它表示…

一文了解【完全合作关系】下的【多智能体强化学习】

处于完全合作关系的多智能体的利益一致&#xff0c;获得的奖励相同&#xff0c;有共同的目标。比如多个工业机器人协同装配汽车&#xff0c;他们的目标是相同的&#xff0c;都希望把汽车装好。 在多智能体系统中&#xff0c;一个智能体未必能观测到全局状态 S。设第 i 号智能体…

Nginx前后端分离部署springboot和vue项目

Nginx前后端分离部署springboot和vue项目&#xff0c;其实用的比较多&#xff0c;有的小伙伴对其原理和配置还一知半解&#xff0c;现在就科普一下&#xff1a; 1、准备后端项目 后端工程无论是微服务还是单体&#xff0c;一般最终都是jar启动&#xff0c;关键点就是把后端服…

vivado RTL运行方法检查、分析方法报告、报告DRC

运行方法检查 Vivado Design Suite提供基于超快设计的自动化方法检查使用“报告方法论”命令的FPGA和SoC&#xff08;UG949&#xff09;方法论指南。您可以生成关于打开、详细阐述、综合或实现的方法论报告设计对于详细设计&#xff0c;方法报告会检查XDC和RTL文件。对于有关使…

CVE重要通用漏洞复现java php

在进行漏洞复现之前我们需要在linux虚拟机上进行docker的安装 我不喜欢win上安因为不知道为什么总是和我的vmware冲突 然后我的kali内核版本太低 我需要重新安装一个新的linux 并且配置网络 我相信这会话费我不少时间 查看版本 uname -a 需要5.5或以上的版本 看错了浪…

滚动菜单+图片ListView

目录 Fruit.java FruitAdapter MainActivity activity_main.xml fruit.xml 整体结构 Fruit.java public class Fruit {private String name;private int imageId;public Fruit(String name, int imageId) {this.name name;this.imageId imageId;}public String getNam…

AR与AI融合加速,医疗护理更便捷

根据Reports and Data的AR市场发展报告&#xff0c;到2026年&#xff0c;预计医疗保健市场中的AR/VR行业规模将达到70.5亿美元。这一趋势主要受到对创新诊断技术、神经系统疾病和疾病意识不断增长的需求驱动。信息技术领域的进步&#xff0c;包括笔记本电脑、计算机、互联网连接…

用 Python 制作可视化 GUI 界面,一键实现自动分类管理文件!

经常杂乱无章的文件夹会让我们找不到所想要的文件&#xff0c;因此小编特意制作了一个可视化GUI界面&#xff0c;通过输入路径一键点击实现文件分门别类的归档。 不同的文件后缀归类为不同的类别 我们先罗列一下大致有几类文件&#xff0c;根据文件的后缀来设定&#xff0c;大…