vue+element+springboot实现多张图片上传

文章讲述了在动态模块中使用el-upload组件处理多张图片上传,包括禁用自动上传、控制选择数量、文件删除以及前端向服务端发送MultipartFile数组的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

     1.需求说明
     2.实现思路
     3.el-upload组件主要属性说明
     4.前端传递MultipartFile数组与服务端接收说明
     5.完整代码

1.需求说明

    动态模块新增添加动态功能,支持多张图片上传.实现过程中对el-upload组件不是很熟悉,踩了很多坑,当然也参考过别的文章,发现处理很复杂.这里记录最终实现结果,方便有同样问题的同学查看,避免浪费多余时间.下面是发布动态的页面,点击上传图片打开本地文件选择,点击确定完成动态发布功能.
在这里插入图片描述

2.实现思路

    多张图片上传逻辑(兼容单张图片上传,只需要修改最大上传图片数量为1):
    1.需要页面选择好所有图片之后调用服务端的图片上传接口,这样处理的原因是图片上传过程中会出现几种场景:

选择好图片之后要删除之前选择的某一张或某几张;
选择好图片之后要追加几张图片

如果按照选择一张就调用一次图片上传接口,不但会增加服务端调用次数,而且还会增加无效图片的存储成本.
    2.多张图片一次上传接口调用成功后返回多张图片的图片地址,再调用服务端动态发布接口,完成动态发布功能

3.el-upload组件主要属性说明

1.禁用选择图片自动上传功能
    action属性:图片上传服务端请求地址,在组件中属于必传,默认选择一张图片就要调用一次.按照上面梳理的逻辑需要禁用调用该功能,auto-upload设置为false即可.action由于是必传,所以此处设置为#
2.开启多选
    设置multiple设置为true,默认false,否则在出现的文件选择窗口中只能选择一个文件.
3.设置选择文件最大数量
    使用limit属性,超过最大数量的处理逻辑可以在on-exceed中实现,其中处理的逻辑是页面提示已超过最大数请重新选择.
4.显示已选择的图片列表
    设置file-list实现
5.选择好图片之后追加几张图片问题处理
    on-change可以监听选中的图片,一次性选择多个图片会执行多次,但是为保证业务处理逻辑执行成功,只需要最后一次on-change中添加业务处理,所以通过判断监听返回的fileList集合长度是否是最大来处理.自定义的fileList就是on-change中最后一次on-change监听返回的fileList集合信息.下文中自定义的imgUrlList为调用文件上传服务端组装的图片参数集合.服务端的file对象对应on-change监听file中的file.raw.

handleChange(file, fileList){
				let length = fileList.length	
				this.maxLength = Math.max(length, this.maxLength)
				setTimeout(() => {
					if(length !== this.maxLength) {
						return
					} else {
						this.fileList=fileList
					}
				})
			}

6.选择好图片之后删除已选中图片问题处理
    before-remove可以监听要进行删除的图片信息.每个图片中有一个唯一标识uid,通过唯一标识删除自定义fileList中的图片

handleRemove(file, fileList){
				this.fileList=this.fileList.filter(imgFile=>imgFile.uid != file.uid)
			},

4.前端传递MultipartFile[]与服务端接收说明

    服务端接口为post请求,请求方式为post表单提交.具体如下

@PostMapping("/uploadImg")
    public ResultVo uploadImg(@RequestParam(name = "multipartFiles") MultipartFile[] multipartFiles,
                               @RequestParam(name = "fileType") Integer fileType) {
        String url = adminDriftService.adminUploadImg(multipartFiles,fileType);
        return ResultVoUtil.success(url);
    }

前端页面需要按照FormData类型进行传递,注意一下参数拼接:

let formData = new FormData();
				this.imgUrlList.map(img=>{
					formData.append("multipartFiles", img)
				})
				formData.append("fileType", 2)

5.完整代码

    前端:
dynamic.js:

export function uploadImg(formData) {
	return axios({
		url: 'uploadImg',
		method: 'POST',	
		data: formData
	})
}

新增动态弹窗:

<el-dialog title="新增动态" :visible.sync="addDynamicVisible">
			  <el-form :model="addDynamicForm">
				  <el-form-item label="用户昵称" :label-width="formLabelWidth">
					<el-select v-model="addDynamicForm.userId" filterable placeholder="请选择">
					    <el-option
					      v-for="systemUser in systemUserList"
					      :key="systemUser.userId"
					      :label="systemUser.userName"
					      :value="systemUser.userId">
					    </el-option>
					  </el-select>
				  </el-form-item>
			    <el-form-item label="动态文本内容" :label-width="formLabelWidth">
			      <el-input v-model="addDynamicForm.contentText" autocomplete="off"></el-input>
			    </el-form-item>
				<el-form-item label="动态图片" :label-width="formLabelWidth">
				  <el-upload
					action="#"
				    :file-list="fileList"
					:before-remove="handleRemove"
					:auto-upload="false" 
					:multiple="true" 
					:on-change="handleChange"
					:limit="9"
					:on-exceed="handleExceed"
				    list-type="picture">
				    <el-button size="small" type="primary">点击上传</el-button>
				    <div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div>
				  </el-upload>
				</el-form-item>
			  </el-form>
			  <div slot="footer" class="dialog-footer">
			    <el-button @click="closeAddDynamic()">取 消</el-button>
			    <el-button type="primary" @click="addDynamicInfo()">确 定</el-button>
			  </div>
			</el-dialog>
			
<script>			
import {findSystemUserList,addDynamic,uploadImg} from "@/api/dynamic";
export default {
		data() {
			return {
				addDynamicVisible:false, // 是否显示新增动态弹窗
				addDynamicForm:{}, // 新增动态内容
				systemUserList:[],  // 系统用户列表信息
				formLabelWidth: '120px',
				imgUrlList:[], // 上传文件地址集合
				fileList:[], // 上传文件本地显示图片集合(非服务器图片地址)
				maxLength:0 // 上传文件最大值
			}
		}
	methods:{
		// 图片列表删除之后处理操作
		handleRemove(file, fileList){
				this.fileList=this.fileList.filter(imgFile=>imgFile.uid != file.uid)
			},	
	   // 每次打开本地文件选择窗口选择图片后处理操作		
		handleChange(file, fileList){
				let length = fileList.length	
				this.maxLength = Math.max(length, this.maxLength)
				setTimeout(() => {
					if(length !== this.maxLength) {
						return
					} else {
						this.fileList=fileList
					}
				})
			},	
			// 选择图片超过最大限制9张之后处理操作
			handleExceed(files, fileList){
				this.msgError("最多选取9张,请重新选择!")
				// 清空之前选择内容
				this.fileList=[]
			},
			// 添加动态操作
			addDynamicInfo(){
				// 参数校验
				if(!this.addDynamicForm.userId){
					this.msgError("请选择用户!")
					return
				}
				this.imgUrlList=this.fileList.map(file=>file.raw)
				this.serverUploadImg()
			},	  
			// 上传图片处理逻辑
			serverUploadImg() {
				let formData = new FormData();
				this.imgUrlList.map(img=>{
					formData.append("multipartFiles", img)
				})
				formData.append("fileType", 2)
				uploadImg(formData).then(res => {
					if (res.code === 200) {
						// 图片上传成功之后触发动态发布逻辑
						this.addDynamicForm.contentImg=res.data
						this.serverAddDynamic()
					}else {
						this.msgError(res.msg)
					}
				}).catch(() => {
					this.msgError("请求失败")
				})
			},	
			// 动态发布逻辑
			serverAddDynamic() {
				addDynamic(this.addDynamicForm).then(res => {
					if (res.code === 200) {
						this.msgSuccess('添加成功!');
						this.addDynamicVisible=false;
						this.addDynamicForm={};
						this.imgUrlList=[];
						this.fileList=[];
						this.serverFindDynamicInfoList(this.queryInfo)
					}else {
						this.msgError(res.msg)
					}
				}).catch(() => {
					this.msgError("请求失败")
				})
			},	  
		}
</script>		

服务端文件批量上传:

  @ApiImplicitParams({@ApiImplicitParam(name = "multipartFiles",value = "图片集合",required = true, dataType = "MultipartFile[]"),
            @ApiImplicitParam(name = "fileType",value = "文件类型:1.用户图片:头像以及背景图;2.动态图",required = true,
                    dataType = "Integer",paramType = "insert",example = "1")})
    @ApiOperation("上传图片")
    @PostMapping("/uploadImg")
    public ResultVo uploadImg(@NotNull(message = "文件对象不允许为空!") @RequestParam(name = "multipartFiles") MultipartFile[] multipartFiles,
                              @NotNull(message = "文件类型不允许为空!") @RequestParam(name = "fileType") Integer fileType) {
        String url = service.adminUploadImg(multipartFiles,fileType);
        return ResultVoUtil.success(url);
    }

多张文件上传实现逻辑:

   public String adminUploadImg(MultipartFile[] multipartFiles, Integer fileType) {
        String imgUrlStr ="";
        for (int i = 0; i < multipartFiles.length; i++) {
            String url = aliYunService.uploadImg(multipartFiles[i], fileType,"");
            imgUrlStr=imgUrlStr+url;
            if(multipartFiles.length > 1  && i < multipartFiles.length-1){
                imgUrlStr=imgUrlStr+",";
            }
        }

        return imgUrlStr;
    }

图片上传具体实现逻辑:

public String uploadImg(MultipartFile multipartFile,Integer fileType,String fileNameParam) {


        // 返回文件地址
        String fileUrl = "";

        // 文件校验
        checkFile(multipartFile,fileType);

        // 文件上传路径
        // modify by txm 2023/3/24 图片类型修改为枚举
        String filePath = FilePathEnum.getOSSFilePath(fileType);
        String fileName = StrUtil.isBlank(fileNameParam) ?  multipartFile.getOriginalFilename():fileNameParam;
        String pathKey = StrUtil.concat(true,filePath,"/",fileName);

        // meta设置请求头
        OSS ossClient = new OSSClientBuilder().build(driftConfig.getEndpoint(), driftConfig.getOssAccessKeyId(), driftConfig.getOssAccessKeySecret());
        try {
            // 上传至阿里OSS
            ossClient.putObject(driftConfig.getBucketName(), pathKey, new ByteArrayInputStream(multipartFile.getBytes()));
            fileUrl = driftConfig.getUrlPrefix() + pathKey;
        } catch (Exception e) {
            // modify by txm 2023/10/16 修改描述
            log.error("文件上传失败:{}", e.getMessage());
            throw new BusinessException("文件上传失败:请重试!");
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
        // 上传成功
        return fileUrl;
    }
<think>好的,我现在需要帮助用户解决如何使用Vue2、Element UI和Spring Boot实现图片上传和表单数据一起提交,并且图片要上传到指定的服务器,使用FTP服务。我需要分步骤思考这个问题,确保每个部分都正确。 首先,用户的需求是将表单数据和图片一起提交,图片通过FTP上传到指定服务器。这意味着前端需要处理表单数据和文件上传,后端需要接收表单数据,同时处理图片上传到FTP服务器。 前端部分:用户使用的是Vue2和Element UI。Element UI有一个上传组件el-upload,可以用来处理文件上传。通常,文件上传会通过multipart/form-data格式发送,但这里可能需要单独处理文件上传和表单提交,或者将它们合并成一个请求。不过用户希望一起提交,所以可能需要将表单数据和图片放在同一个请求体中,或者分步处理。但考虑到FTP上传可能需要不同的处理方式,可能需要先上传图片到FTP服务器,获取URL后再和表单数据一起提交到后端。 不过,常规做法可能是前端图片上传到后端,后端再上传到FTP。或者前端直接上传到FTP服务器,然后提交表单数据(包含图片路径)到后端。用户提到图片上传指定服务器,用FTP服务,可能是指后端使用FTP将接收到的图片上传到指定服务器,或者前端直接通过FTP上传。但前端直接通过FTP上传可能存在安全问题,因为FTP凭证会暴露在前端,所以更合理的做法是前端将图片发送到后端,后端再通过FTP上传到指定服务器。 所以步骤应该是: 1. 前端使用el-upload组件上传图片到后端Spring Boot的API。 2. 后端接收到图片后,使用FTP客户端将图片上传到指定的FTP服务器,并返回存储的路径或URL。 3. 前端在表单提交时,将其他表单数据和图片的路径一起提交到后端,保存到数据库。 或者,如果希望表单数据和图片一起提交,可能需要前端将文件和其他表单字段放在同一个FormData对象中,然后发送到后端。后端处理FormData,同时上传文件到FTP,然后保存数据。 可能需要调整流程,比如前端上传图片到后端,后端上传到FTP,保存路径,然后返回路径给前端前端再提交表单时带上这个路径。或者一步完成,前端将所有数据和文件发送到后端,后端处理文件上传到FTP,然后保存数据。 但考虑到用户要求“一起提交”,可能更倾向于后者,即前端在一个请求中同时发送表单数据和文件,后端处理文件上传到FTP,并保存表单数据。不过,如果文件较大,可能需要分步上传,但用户可能希望一次性提交。 接下来需要具体分析每个部分的实现前端部分: 使用Element UI的el-upload组件,设置自动上传为false,手动收集文件,然后在提交表单时,将文件和表单数据一起提交。需要将文件添加到FormData对象中,同时添加其他表单字段。 例如: 在Vue组件中,data里有一个form对象,包含其他字段,还有一个fileList数组存储上传的文件。 当用户点击提交时,使用FormData来构建请求体: const formData = new FormData(); formData.append('file', this.fileList[0].raw); // 假设只上传一个文件 formData.append('title', this.form.title); formData.append('description', this.form.description); 然后通过axios发送POST请求到后端接口。 后端部分: Spring Boot接收MultipartFile和表单数据。处理文件上传到FTP服务器,然后保存其他数据到数据库。 需要集成FTP客户端,比如使用Apache Commons Net库中的FTPClient。 后端处理步骤: 1. 接收MultipartFile和表单参数。 2. 将文件上传到FTP服务器,获取存储后的路径。 3. 将路径和其他表单数据存入数据库。 需要配置FTP的连接参数(主机、端口、用户名、密码、路径等),可能在application.properties中配置,通过@Value注入。 需要注意的是,文件上传到FTP可能需要进行被动模式,处理文件流,关闭连接等。 可能的问题点: - 文件名的处理,避免重复。 - FTP连接的建立和关闭,确保每次上传都正确连接和断开。 - 错误处理,比如FTP上传失败时的回滚或提示。 现在需要详细写出每个步骤的代码示例,并确保各部分正确。 前端代码: 在Vue组件中,使用el-upload组件,设置auto-upload为false,绑定on-change事件来更新fileList。 提交表单时,组装FormData,发送POST请求。 后端代码: 创建Spring Boot的Controller,接收MultipartFile和表单参数,调用FTP服务上传文件,保存数据。 FTP工具类:封装上传方法,使用FTPClient,处理文件流。 可能还需要处理文件名,例如使用UUID生成唯一文件名,避免冲突。 测试时需要注意跨域问题,前端开发服务器的代理配置,以及后端对MultipartFile的支持(可能需要配置MultipartResolver)。 总结步骤: 1. 前端使用el-upload收集文件,与其他表单数据一起通过FormData提交。 2. 后端接收请求,提取文件和表单数据。 3. 后端使用FTPClient上传文件到指定服务器,获取存储路径。 4. 后端将表单数据和文件路径存入数据库。 5. 返回操作结果给前端。 可能还需要处理多个文件上传的情况,但用户的问题中没有特别说明,可能只需处理单个文件。 现在需要将这些步骤转化为结构化的回答,确保每个部分代码正确,注意事项提到。</think>以下为分步解决方案,结合Vue2+Element UI+Spring Boot实现图片与表单数据联合提交,并通过FTP上传图片到指定服务器: --- ### 一、前端实现Vue2 + Element UI) #### 1. 文件上传组件配置 ```vue <template> <el-upload action="#" <!-- 禁用自动上传 --> :auto-upload="false" :on-change="handleFileChange" :file-list="fileList"> <el-button>选择图片</el-button> </el-upload> </template> <script> export default { data() { return { form: { title: '', description: '' }, // 表单数据 fileList: [] // 存储用户选择的文件 }; }, methods: { handleFileChange(file, fileList) { this.fileList = fileList.slice(-1); // 仅保留最新文件(单文件上传) }, async submitForm() { const formData = new FormData(); // 添加表单字段 Object.keys(this.form).forEach(key => { formData.append(key, this.form[key]); }); // 添加图片文件(Element UI的文件对象存储在raw属性) if (this.fileList.length > 0) { formData.append('file', this.fileList[0].raw); } try { const response = await this.$axios.post('/api/submit-form', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); this.$message.success('提交成功'); } catch (error) { this.$message.error('提交失败'); } } } }; </script> ``` --- ### 二、后端实现Spring Boot + FTP) #### 1. 添加依赖 ```xml <!-- pom.xml --> <dependency> <groupId>commons-net</groupId> <artifactId>commons-net</artifactId> <version>3.8.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> ``` #### 2. 配置FTP参数 ```properties # application.properties ftp.host=your.ftp.server.com ftp.port=21 ftp.username=your_username ftp.password=your_password ftp.base-path=/uploads/ ``` #### 3. 创建FTP工具类 ```java @Component public class FtpUtil { @Value("${ftp.host}") private String host; @Value("${ftp.port}") private int port; @Value("${ftp.username}") private String username; @Value("${ftp.password}") private String password; @Value("${ftp.base-path}") private String basePath; public String uploadFile(MultipartFile file) throws IOException { FTPClient ftp = new FTPClient(); try { ftp.connect(host, port); ftp.login(username, password); ftp.setFileType(FTP.BINARY_FILE_TYPE); ftp.enterLocalPassiveMode(); // 重要!解决防火墙阻塞问题 String filename = UUID.randomUUID() + "_" + file.getOriginalFilename(); InputStream inputStream = file.getInputStream(); boolean success = ftp.storeFile(basePath + filename, inputStream); inputStream.close(); if (!success) throw new IOException("FTP上传失败"); return filename; // 返回存储后的文件名 } finally { if (ftp.isConnected()) { ftp.logout(); ftp.disconnect(); } } } } ``` #### 4. 控制器处理请求 ```java @RestController @RequestMapping("/api") public class UploadController { @Autowired private FtpUtil ftpUtil; @PostMapping("/submit-form") public ResponseEntity<?> handleFormSubmit( @RequestParam("file") MultipartFile file, @RequestParam String title, @RequestParam String description) { try { // 1. 上传文件到FTP String storedFilename = ftpUtil.uploadFile(file); // 2. 保存到数据库(伪代码) // YourEntity entity = new YourEntity(title, description, storedFilename); // repository.save(entity); return ResponseEntity.ok().build(); } catch (IOException e) { return ResponseEntity.status(500).body("文件上传失败"); } } } ``` --- ### 三、关键注意事项 1. **前端安全**:不要在前端硬编码FTP凭据,必须通过后端中转 2. **文件重名处理**:使用UUID生成唯一文件名 3. **连接池优化**:频繁上传建议使用FTP连接池(如:Apache Commons Pool) 4. **异常处理**: - 前端捕获413错误(文件过大) - 后端添加`@ControllerAdvice`全局异常处理 5. **性能优化**: ```java // 大文件分块上传(伪代码) public void uploadLargeFile(InputStream inputStream) { byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { ftp.storeFileStream(remotePath); // 分块写入... } } ``` --- ### 四、扩展建议 - 文件类型验证:前端通过`before-upload`钩子限制格式,后端通过`file.getContentType()`二次验证 - 进度条显示:使用Element UI的`on-progress`事件 - 断点续传:通过前端分片+后端记录已上传片段实现 通过以上步骤,即可实现表单与图片的联合提交,并安全通过FTP存储到指定服务器。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卖柴火的小伙子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值