个人技术博客——使用Spring Boot和Vue-Axios互传图片文件

222100133蓝有润 2024-06-06 23:55:20
这个作业属于哪个课程软件工程实践-2023学年-W班
这个作业要求在哪里软件工程实践总结&个人技术博客
这个作业的目标个人技术博客
其他参考文献-

目录

  • 使用Spring Boot和Vue-Axios互传图片文件
  • 技术概述
  • 技术详述
  • 获取图片的方式
  • Spring Boot后端配置
  • Axios前端调用
  • 示例代码
  • 图片上传——纯图片
  • 图片上传——上传复杂的数据结构
  • 获得图片——url
  • 获得图片——图片文件
  • 获得图片——多张图片文件

使用Spring Boot和Vue-Axios互传图片文件

技术概述

在现代Web应用中,图片文件的上传和下载是一个常见的需求。本人在进行福鱼的商品图片功能全栈开发时,遇到了上传图片的问题。在查阅了资料后,已经能够完成商品图片的上传和获取。本文将探讨如何使用Spring Boot作为后端框架,结合Axios作为前端库,实现高效的图片文件互传。

技术详述

获取图片的方式

在这里插入图片描述

Spring Boot后端配置

  1. 图片上传接口:使用@RestController@PostMapping注解创建一个图片上传接口。
  2. 图片存储:配置图片存储路径,使用MultipartFile接收上传的图片。
  3. 图片下载接口:创建一个图片下载接口,根据请求参数返回相应的图片流或者url。

Axios前端调用

  1. 上传图片:使用Axios的.post方法,配置FormData对象,将图片作为数据发送。
  2. 下载图片:使用Axios的.get方法,如果是获得图片文件,设置配置响应类型为blob,以便处理二进制图片流;如果是url,则直接发送url。

示例代码

图片上传——纯图片

前端

<template>
  <div>
    <form @submit.prevent="uploadImage">
      <input type="file" @change="onFileChange" />
      <button type="submit">上传图片</button>
    </form>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  methods: {
    onFileChange(event) {
      this.file = event.target.files[0];
    },
    uploadImage() {
      let formData = new FormData();
      formData.append('image', this.file);

      axios.post('/upload', formData, {
        headers: {
          'Content-Type': 'multipart/form-data'
        }
      }).then(response => {
        console.log('图片上传成功', response);
      }).catch(error => {
        console.error('图片上传失败', error);
      });
    }
  }
};
</script>

后端

@PostMapping("/upload")
public ResponseEntity<?> uploadImage(@RequestParam("image") MultipartFile file) {
    try {
        // 保存图片到服务器
        Path path = Files.createTempFile("images", file.getOriginalFilename());
        Files.write(path, file.getBytes());

        return ResponseEntity.ok("图片上传成功");
    } catch (IOException e) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("图片上传失败");
    }
}

技术简介

  • FormData 对象:用于构造复杂的JSON数据,可以包含JSON字符串、图片二进制内容,可以被发送到服务器。
  • Content-Type:请求头中的字段,用于指定发送的数据类型,multipart/form-data 表示表单数据。
  • @RequestParam:用于获取请求参数。
  • MultipartFile:Spring 提供的用于封装上传文件的接口。

流程说明

在这里插入图片描述

图片上传——上传复杂的数据结构

可以图片和JSON一起发送,这里使用了福鱼项目的CommodityWithImages数据结构作为示例

<template>
    <frontstage_layout>
        <router-link to="frontstage_ShopView" class="frontstage_review_results_close">
        <button type="button" class="frontstage_review_results_close_button" >退出</button>
    </router-link>
        <div class="container">
            <input type="text" v-model="name" placeholder="商品名称" style="margin-top: 10px;"></input>
            <textarea type="text" v-model="description" placeholder="描述一下宝贝的品牌型号、货品来源" rows="5" style="margin-top: 10px;"></textarea>
            <div class="image-upload-container">
                <el-upload
                action=""
                :multiple="true"
                :auto-upload="true"
                :accept="acceptTypes"
                :list-type="listType"
                :file-list="files"
                :before-upload="beforeUpload"
                :http-request="handlePost"
                :on-preview="handlePreview"
                :on-change="changeImage"
                :on-remove="handleRemove"
                :on-exceed="extendLength"
                >
                <i class="el-icon-plus"></i>
                <div slot="tip">最多可上传5张jpg/png图片,且每张图片大小不超过500kb</div>
                </el-upload>
            </div>

            <select v-model="category" id="select">
                <option value="book">书本</option>
                <option value="digital">数码</option>
                <option value="clothing">服饰</option>
                <option value="game">游戏</option>
                <option value="life">生活</option>
                <option value="appliance">电器</option>
            </select>

            <editableTable ref="editableTable"></editableTable>
            <div class="row">
                <input
                    type="number"
                    v-model.number="price"
                    placeholder="价格(元)"
                    :min="0"
                    id="price"
                    style="margin-bottom: 10px;"
                >
            </div>
            <div class="row2">
                <button  id="price" @click = "addCommodity()" style="background-color:#9e2121;color: white;">添加商品</button>
            </div>
        </div>
    </frontstage_layout>
</template>

<script>
    import frontstage_layout from '../../components/frontstage/frontstage_layout.vue';
    import editableTable from '../../components/editableTable.vue';
    import { ElLoading } from 'element-plus';
    export default {
        components: {
            frontstage_layout,
            editableTable,
        },     
        data() {
            return {
                description: '',
                file: null,
                category: 'book',
                price: null,
                name:null,
                formattedDate:null,
                acceptTypes: 'jpg, jpeg,png, PNG',
                listType: 'picture-card',
                files: [],
                maxUploadLimit : 5,
                forceDelete : false,
                maxSize : 500,
                loading : null,
            };
        }, 
        methods: {
            addCommodity() {
                if (!this.name || this.name.trim() === '') {
                    this.$message.error('商品名称不能为空');
                    return;
                }

                if (!this.description || this.description.trim() === '') {
                    this.$message.error('商品描述不能为空');
                    return;
                }

                // 图片检查
                if (this.files.length === 0) {
                    this.$message.error('请上传商品图片');
                    return;
                }

                // 价格检查
                if (this.price === null || this.price <= 0) {
                    this.$message.error('价格必须大于0');
                    return;
                }
                const userId = JSON.parse(localStorage.getItem('user')).userid;
                this.formattedDate = this.getTime();
                
                let formData = new FormData();
                // Add commodity data
                formData.append('commodity.shopId', userId);
                formData.append('commodity.name', this.name);
                formData.append('commodity.description', this.description);
                formData.append('commodity.price', this.price);
                formData.append('commodity.isActive', 1);
                formData.append('commodity.formattedDate', this.formattedDate);
                
                // Add images
                this.files.forEach((fileItem, index) => {
                    formData.append('images', fileItem.raw);
                });
                const keyValues = JSON.parse(JSON.stringify(this.$refs.editableTable.tableData));
                keyValues.forEach((item) => {
                    const keyValueString = `${item.key.replace(/:/g, '@')}:${item.value.replace(/:/g, '@')}`;
                    formData.append('keyValues', keyValueString);
                });
                this.loading = ElLoading.service({        
                    lock: true,
                    customClass: 'global-loading',
                })
                // Send the request
                this.$axios.post('commodity/information', formData, {
                    headers: {
                        'Content-Type': 'multipart/form-data'
                    }
                })
                .then(response => {
                    console.log(response);
                    this.$message.success('添加成功,请等待审核');
                    this.$router.push('/frontstage_ShopView');
                    this.loading.close();
                })
                .catch(error => {
                    console.error(error);
                    if (error.response) {
                        console.error(error.response.data);
                        console.error(error.response.status);
                        console.error(error.response.headers);
                    }
                    this.loading.close();
                });
            },
        } 
    }  
</script>

<style scoped>
</style>

后端
获取的请求体结构

package com.example.demo.requests;
import java.util.List;
import java.util.Map;

import org.springframework.web.multipart.MultipartFile;

import com.example.demo.models.Commodity;

public class CommodityWithImages {
    private Commodity commodity;
    public List<MultipartFile> images;
    private List<String> keyValues;
    //省略getter和setter
}

控制+服务层

    @PostMapping("/information")
    public ResponseEntity<Object> addCommodity(@ModelAttribute CommodityWithImages commodityWithImages) {
        Commodity commodity = commodityWithImages.getCommodity();
        if (commodity == null || commodity.getName() == null || commodity.getPrice() == null) {
            return new ResponseEntity<>("商品信息不完整", HttpStatus.BAD_REQUEST);
        }
        Map<String, Object> response = commodityService.addCommodity(commodityWithImages);
        return ResponseEntity.ok(response);
    }
     public Map<String, Object> addCommodity(CommodityWithImages commodityWithImages) {
        Map<String, Object> response = new HashMap<>();
        Commodity commodity = commodityWithImages.getCommodity();
        commodityMapper.insertCommodity(commodity);

        Long id = commodity.getId();

        List<CommodityKeyValue> keyValueMap = new ArrayList<>();
        if (commodityWithImages.getKeyValues() != null) {
            for (String keyValue : commodityWithImages.getKeyValues()) {
                String[] parts = keyValue.split(":");
                String key = parts[0].replace("@", ":");
                String value = parts[1].replace("@", ":");
                CommodityKeyValue temp = new CommodityKeyValue();
                temp.setCommodityId(id);
                temp.setKeyName(key);
                temp.setValue(value);
                keyValueMap.add(temp);
            }
            addCommodityKey(keyValueMap);
        }
        if (commodityWithImages.getImages() != null) {
            try {
                for (MultipartFile image : commodityWithImages.getImages()) {
                    if (!image.isEmpty()) {
                        Path directoryPath = Paths.get("images/commodity/" + id);
                        if (!Files.exists(directoryPath)) {
                            Files.createDirectories(directoryPath);
                        }
                        String fileName = image.getOriginalFilename();
                        byte[] bytes = image.getBytes();
                        Path directory = Paths.get("images/commodity/", String.valueOf(id));
                        Path path = Files.createTempFile(directory, "_", fileName);
                        Files.write(path, bytes, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE);
                    }
                }
            } catch (IOException e) {
                response.put("status", "error");
                response.put("message", "上传失败: " + e.getMessage());
                return response;
            }
            response.put("message", "图片上传成功");
        }
        response.put("status", "success");
        response.put("message", "商品上传成功");
        return response;
    }

技术简介

流程说明

在这里插入图片描述

获得图片——url

后端

@GetMapping("/get-image-url/{id}")
public ResponseEntity<?> getImageUrl(@PathVariable long id) {
    String imageUrl = "http://example.com/images/" + id + ".jpg";
    return ResponseEntity.ok(imageUrl);
}
<template>
  <div>
    <img :src="imageUrl" alt="商品图片">
  </div>
</template>

<script>
import axios from 'axios';

export default {
  data() {
    return {
      imageUrl: ''
    };
  },
  created() {
    this.fetchImageUrl();
  },
  methods: {
    fetchImageUrl() {
      axios.get('/get-image-url/1').then(response => {
        this.imageUrl = response.data;
      }).catch(error => {
        console.error('获取图片URL失败', error);
      });
    }
  }
};
</script>

流程说明

在这里插入图片描述

获得图片——图片文件

后端

@GetMapping("/download-image/{id}")
public ResponseEntity<Resource> downloadImage(@PathVariable long id) {
    // 假设根据id获取图片文件
    File file = new File("path/to/image.jpg");
    FileSystemResource fileSystemResource = new FileSystemResource(file);

    return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + file.getName())
            .contentType(MediaType.APPLICATION_OCTET_STREAM)
            .body(fileSystemResource);
}

前端

<template>
  <div>
    <a :href="downloadUrl" download>下载图片</a>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  data() {
    return {
      downloadUrl: ''
    };
  },
  created() {
    this.fetchDownloadUrl();
  },
  methods: {
    fetchDownloadUrl() {
      axios.get('/download-image/1', { responseType: 'blob' })
        .then(response => {
          this.downloadUrl = window.URL.createObjectURL(new Blob([response.data]));
        }).catch(error => {
          console.error('获取图片下载URL失败', error);
        });
    }
  }
};
</script>

流程说明

在这里插入图片描述

获得图片——多张图片文件

后端

public Resource getImageZip(long id) {
        List<Resource> imageResources = new ArrayList<>();
        List<File> imageFiles = getImageFiles(id); //获得所有的图片File

        for (File imageFile : imageFiles) {
            imageResources.add(new FileSystemResource(imageFile));
        }

        File tempZipFile;
        try {
            tempZipFile = File.createTempFile("images", ".zip");
            try (ZipOutputStream zipOutputStream = new ZipOutputStream(new FileOutputStream(tempZipFile))) {
                for (Resource imageResource : imageResources) {
                    try (InputStream imageInputStream = imageResource.getInputStream()) {
                        ZipEntry zipEntry = new ZipEntry(imageResource.getFilename());
                        zipOutputStream.putNextEntry(zipEntry);
                        byte[] buffer = new byte[1024];
                        int length;
                        // 读取InputStream并写入ZipOutputStream
                        while ((length = imageInputStream.read(buffer)) != -1) {
                            zipOutputStream.write(buffer, 0, length);
                        }
                        zipOutputStream.closeEntry();
                    }
                }
            }
            // 创建一个FileSystemResource来包装ZIP图片
            Resource zipResource = new FileSystemResource(tempZipFile);
            return zipResource;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

前端

<template>
      <div class="product-details">
        <el-carousel :interval="3000" arrow="always" class="product-slider" motion-blur=true height="90%" indicator-position="outside">
          <el-carousel-item v-for="(image, index) in productImages" :key="index">
            <img :src="image" alt="Product Image" class="product-image" />
          </el-carousel-item>
        </el-carousel>
          
          <div class="product-info">
            <h1 style="border-bottom: 3px solid #9e2121; border-top: 3px solid #9e2121; font-weight: bold; text-align: center; padding: 8% 8% 8% 0%;">{{ commodity.commodityName }}</h1>
      
              <div class="seller-info" style="display: flex; align-items: center; justify-content: center; padding:8% 8% 8% 0% ;">
                <p style="color: #9e2121; font-weight: bold; font-size: x-large;text-align: left;margin-right: 15%;">{{ commodity.price }}</p>
                <img :src="seller.avatar" alt="Seller Avatar" style="width: 10%; height: 10%; border-radius: 50%;">
                <div style="margin-left: 10px;">
                  <p style="font-size: large; font-weight: bold;">{{ commodity.userName }}</p>
                  <router-link 
                  :to="linkDestination" 
                  style="text-decoration: none; color: #9e2121;"
                >
                  访问商家
                </router-link>
                </div>
              </div>
        
            <p style="border-bottom: 3px solid #9e2121;border-top: 3px solid #9e2121; padding: 5% 8% 5% 0%; font-size: x-large; text-align: center;"> {{ commodity.commodityDescription }}</p>
          </div>
        </div>
        
        <div class="custom-table-container">
</div>


  </template>
  
<script>
  import JSZip from 'jszip';
  import { ElLoading, ElCarousel, ElCarouselItem } from 'element-plus'
  export default {
    components: {
      ElCarousel,
      ElCarouselItem,
    },
    emits: ['update-info'],
    data() {
      return {
        commodity:{},
        productImages: [
        ],
        currentIndex: 0 ,
        seller: {
          id:1,
          avatar: 'https://ts1.cn.mm.bing.net/th/id/R-C.67c70ed0eae200d69455a91b43a9f407?rik=%2bjNY%2bmbfVS8RWQ&riu=http%3a%2f%2fwww.sucaijishi.com%2fuploadfile%2f2018%2f0508%2f20180508023717621.png%3fimageMogr2%2fformat%2fjpg%2fblur%2f1x0%2fquality%2f60&ehk=8jAEJxDPBhcrPlaoFp5%2bI%2fI5rdr3YShGr2RMKGwIuhM%3d&risl=&pid=ImgRaw&r=0',
          name: '商家姓名',
          link: '商家链接',
          creditScore: '4.5', 
          friendliness: '4.7',
          loading : null,
        },
        keyValues : {},
      };
    },
  methods: {
     getCommodityMessage() {     
        //获得商品信息
      },
      fetchCommodityPicture(id) {
        //根据商品id获得对应的图片列表
        this.controlLoading(this.productImages);
        if (this.productImages.length > 0) return;
        this.$axios.get(`/commodity/picture/${id}`, { responseType: 'arraybuffer' })
          .then(response => {
            const zip = new JSZip();
            return zip.loadAsync(new Uint8Array(response.data));
          })
          .then(zip => {
            const promises = [];
            zip.forEach((relativePath, file) => {
              if (file.name.match(/\.(jpg|jpeg|png|gif)$/)) {
                promises.push(file.async('blob').then(content => {
                  this.productImages.push(
                    URL.createObjectURL(content)
                  );
                }));
              }
              this.controlLoading(this.productImages);
            });

            return Promise.all(promises);
          })
          .catch(error => {
            console.error('Error fetching and unzipping the picture:', error);
          });
      },
    },
    mounted() {
      this.getCommodityMessage();
      this.fetchCommodityPicture(this.$route.query.id);
    },
};
</script>
  
<style scoped>

  </style>

技术简介

  • ZipOutputStream:Java 中用于写入 ZIP 格式文件的输出流。
  • JSZip:JavaScript 库,用于创建、读取或编辑 .zip 文件和 unzip .zip 文件。
  • JSZip.loadAsync:JSZip 方法,用于异步加载 ZIP 文件。

流程说明

在这里插入图片描述

...全文
92 回复 打赏 收藏 转发到动态 举报
AI 作业
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

310

社区成员

发帖
与我相关
我的任务
社区描述
福州大学的软件工程实践-2023学年-W班
软件工程需求分析结对编程 高校 福建省·福州市
社区管理员
  • FZU_SE_teacherW
  • Pity·Monster
  • 助教张富源
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧