springboot+vue2 实现文件上传,vue表单实现上传多张照片或视频回显
vue
vuejs/vue: 是一个用于构建用户界面的 JavaScript 框架,具有简洁的语法和丰富的组件库,可以用于开发单页面应用程序和多页面应用程序。
项目地址:https://gitcode.com/gh_mirrors/vu/vue
免费下载资源
·
此教程可以实现对上传的文件管理,并加入了我的功能需求,大家可以自行进行修改。
文件管理:
功能需求视频:
文件上传视频(图片和视频)
先来简单介绍下功能实现,实现此功能分为数据库、后端java、前端vue共同实现;
业务流程:
- vue页面点击上传后,通过调用后端接口,进行上传,后端进行判断,并设置上传位置,大小等。
- 把文件上传后,数据库设计了文件表,对上传的文件进行记录。
- 后端进行上传后,返回前端一个文件路径,通过文件位置进行回显。
- 当点击确定后,把文件位置根据业务要求保存到数据库。
- 因为我的需求是只上传图片和视频,在后端判断时进行了判断文件是什么类型,并进行了筛选,不是图片和视频都不保存。
下面直接上代码:
mysql
文件管理的数据表:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for files
-- ----------------------------
DROP TABLE IF EXISTS `files`;
CREATE TABLE `files` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '文件名称',
`type` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '文件类型',
`size` bigint NULL DEFAULT NULL COMMENT '文件大小',
`url` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '链接',
`is_delete` tinyint(1) NULL DEFAULT 0 COMMENT '是否删除',
`enable` tinyint NULL DEFAULT 1 COMMENT '是否禁用',
`creat_time` datetime NULL DEFAULT CURRENT_TIMESTAMP,
`md5` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '文件md5',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 444 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
此数据表是文件上传后文件的信息。
springboot
配置文件(application.yml)
spring:
# 文件大小
servlet:
multipart:
max-file-size: 5MB
# 请求大小
max-request-size: 10MB
server:
port: 端口
ip: 地址
files:
upload:
path: 文件地址
文件地址:服务器或者本地的文件夹地址。
pom.xml文件
<!-- 上传文件验证 -->
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
<version>1.24.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.51</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.20</version>
</dependency>
<!-- mybatis plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.1-jre</version>
</dependency>
entity类
package com.maintenanceDispatch.entity.files;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import io.swagger.annotations.ApiModel;
import java.io.Serializable;
import java.util.Date;
import lombok.Getter;
import lombok.Setter;
/**
* <p>
*
* </p>
*
* @author ysdysyn
* @since 2022-11-09
*/
@Getter
@Setter
@ApiModel("文件")
public class Files implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**
* 文件名称
*/
private String name;
/**
* 文件类型
*/
private String type;
/**
* 文件大小
*/
private Long size;
/**
* 链接
*/
private String url;
private String md5;
/**
* 是否删除
*/
private Boolean isDelete;
/**
* 是否禁用
*/
private Boolean enable;
private Date creatTime;
}
mapper类
package com.maintenanceDispatch.mapper.files;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.maintenanceDispatch.entity.files.Files;
public interface FileMapper extends BaseMapper<Files> {
}
封装类
Result
package com.example.yungongju.common;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 接口统一返回包装类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
private String code;
private String msg;
private Object data;
public static Result success() {
return new Result(Constants.CODE_200, "", null);
}
public static Result success(Object data) {
return new Result(Constants.CODE_200, "", data);
}
public static Result error(String code, String msg) {
return new Result(code, msg, null);
}
public static Result error() {
return new Result(Constants.CODE_500, "系统错误", null);
}
}
Constants
package com.example.yungongju.common;
public interface Constants {
String CODE_200 = "200"; //成功
String CODE_401 = "401"; // 权限不足
String CODE_400 = "400"; // 参数错误
String CODE_500 = "500"; // 系统错误
String CODE_600 = "600"; // 其他业务异常
String DICT_TYPE_ICON = "icon";
String FILES_KEY = "FILES_FRONT_ALL";
}
controller类
package com.maintenanceDispatch.controller.files;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.maintenanceDispatch.common.Constants;
import com.maintenanceDispatch.common.Result;
import com.maintenanceDispatch.entity.files.Files;
import com.maintenanceDispatch.mapper.files.FileMapper;
import org.apache.tika.metadata.HttpHeaders;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.TikaMetadataKeys;
import org.apache.tika.mime.MediaType;
import org.apache.tika.parser.AutoDetectParser;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.Parser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.xml.sax.helpers.DefaultHandler;
import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
/**
* 文件上传相关接口
*/
@RestController
@RequestMapping("/file")
public class FileController {
@Value("${files.upload.path}")
private String fileUploadPath;
@Value("${server.ip}")
private String serverIp;
@Resource
private FileMapper fileMapper;
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 文件上传接口
*
* @param file 前端传递过来的文件
* @return
* @throws IOException
*/
@PostMapping("/upload")
public String[] upload(@RequestParam MultipartFile file) throws IOException {
String originalFilename = file.getOriginalFilename();
//文件类型
String type = FileUtil.extName(originalFilename);
long size = file.getSize();
String url = null;
AutoDetectParser parser = new AutoDetectParser();
parser.setParsers(new HashMap<MediaType, Parser>());
Metadata metadata = new Metadata();
metadata.add(TikaMetadataKeys.RESOURCE_NAME_KEY, file.getName());
try (InputStream stream = file.getInputStream()) {
parser.parse(stream, new DefaultHandler(), metadata, new ParseContext());
}catch (Exception e){
throw new RuntimeException();
}
String suffix= metadata.get(HttpHeaders.CONTENT_TYPE);
//对比对应的文件类型的mime就好了至于不知道对应的是什么的话就百度,百度一定会知道
String[] text =suffix.split("/");
System.out.println(text[0]);
// 定义一个文件唯一的标识码
String fileUUID = IdUtil.fastSimpleUUID() + StrUtil.DOT + type;
File uploadFile = new File(fileUploadPath + fileUUID);
// 判断配置的文件目录是否存在,若不存在则创建一个新的文件目录
File parentFile = uploadFile.getParentFile();
//判断文件类型是否属于图片或者视频
if (Objects.equals(text[0], "image")){
if(!parentFile.exists()) {
parentFile.mkdirs();
}
// 获取文件的md5
String md5 = SecureUtil.md5(file.getInputStream());
// 从数据库查询是否存在相同的记录
Files dbFiles = getFileByMd5(md5);
if (dbFiles != null) {
url = dbFiles.getUrl();
} else {
// 上传文件到磁盘
file.transferTo(uploadFile);
// 数据库若不存在重复文件,则不删除刚才上传的文件
url = "http://" + serverIp + ":7777/file/" + fileUUID;
}
// 存储数据库
Files saveFile = new Files();
saveFile.setName(originalFilename);
saveFile.setType(type);
saveFile.setSize(size/1024); // 单位 kb
saveFile.setUrl(url);
saveFile.setMd5(md5);
fileMapper.insert(saveFile);
// 最简单的方式:直接清空缓存
flushRedis(Constants.FILES_KEY);
}else if (Objects.equals(text[0], "video")){
if(!parentFile.exists()) {
parentFile.mkdirs();
}
// 获取文件的md5
String md5 = SecureUtil.md5(file.getInputStream());
// 从数据库查询是否存在相同的记录
Files dbFiles = getFileByMd5(md5);
if (dbFiles != null) {
url = dbFiles.getUrl();
} else {
// 上传文件到磁盘
file.transferTo(uploadFile);
// 数据库若不存在重复文件,则不删除刚才上传的文件
url = "http://" + serverIp + ":7777/file/" + fileUUID;
}
// 存储数据库
Files saveFile = new Files();
saveFile.setName(originalFilename);
saveFile.setType(type);
saveFile.setSize(size/1024); // 单位 kb
saveFile.setUrl(url);
saveFile.setMd5(md5);
fileMapper.insert(saveFile);
// 最简单的方式:直接清空缓存
flushRedis(Constants.FILES_KEY);
}else {
url = "请上传图片或视频";
}
System.out.println(suffix);
// 从redis取出数据,操作完,再设置(不用查询数据库)
// String json = stringRedisTemplate.opsForValue().get(Constants.FILES_KEY);
// List<Files> files1 = JSONUtil.toBean(json, new TypeReference<List<Files>>() {
// }, true);
// files1.add(saveFile);
// setCache(Constants.FILES_KEY, JSONUtil.toJsonStr(files1));
// 从数据库查出数据
// List<Files> files = fileMapper.selectList(null);
// // 设置最新的缓存
// setCache(Constants.FILES_KEY, JSONUtil.toJsonStr(files));
String[] a={url,text[0]};
return a;
}
/**
* 文件下载接口 http://localhost:9090/file/{fileUUID}
* @param fileUUID
* @param response
* @throws IOException
*/
@GetMapping("/{fileUUID}")
public void download(@PathVariable String fileUUID, HttpServletResponse response) throws IOException {
// 根据文件的唯一标识码获取文件
File uploadFile = new File(fileUploadPath + fileUUID);
// 设置输出流的格式
ServletOutputStream os = response.getOutputStream();
response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileUUID, "UTF-8"));
response.setContentType("application/octet-stream");
// 读取文件的字节流
os.write(FileUtil.readBytes(uploadFile));
os.flush();
os.close();
}
/**
* 通过文件的md5查询文件
* @param md5
* @return
*/
private Files getFileByMd5(String md5) {
// 查询文件的md5是否存在
QueryWrapper<Files> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("md5", md5);
List<Files> filesList = fileMapper.selectList(queryWrapper);
return filesList.size() == 0 ? null : filesList.get(0);
}
// @CachePut(value = "files", key = "'frontAll'")
@PostMapping("/update")
public Result update(@RequestBody Files files) {
fileMapper.updateById(files);
flushRedis(Constants.FILES_KEY);
return Result.success();
}
@GetMapping("/detail/{id}")
public Result getById(@PathVariable Integer id) {
return Result.success(fileMapper.selectById(id));
}
//清除一条缓存,key为要清空的数据
// @CacheEvict(value="files",key="'frontAll'")
@DeleteMapping("/{id}")
public Result delete(@PathVariable Integer id) {
Files files = fileMapper.selectById(id);
files.setIsDelete(true);
fileMapper.updateById(files);
flushRedis(Constants.FILES_KEY);
return Result.success();
}
@PostMapping("/del/batch")
public Result deleteBatch(@RequestBody List<Integer> ids) {
// select * from sys_file where id in (id,id,id...)
QueryWrapper<Files> queryWrapper = new QueryWrapper<>();
queryWrapper.in("id", ids);
List<Files> files = fileMapper.selectList(queryWrapper);
for (Files file : files) {
file.setIsDelete(true);
fileMapper.updateById(file);
}
return Result.success();
}
/**
* 分页查询接口
* @param pageNum
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public Result findPage(@RequestParam Integer pageNum,
@RequestParam Integer pageSize,
@RequestParam(defaultValue = "") String name) {
QueryWrapper<Files> queryWrapper = new QueryWrapper<>();
// 查询未删除的记录
queryWrapper.eq("is_delete", false);
queryWrapper.orderByDesc("id");
if (!"".equals(name)) {
queryWrapper.like("name", name);
}
return Result.success(fileMapper.selectPage(new Page<>(pageNum, pageSize), queryWrapper));
}
// 设置缓存
private void setCache(String key, String value) {
stringRedisTemplate.opsForValue().set(key, value);
}
// 删除缓存
private void flushRedis(String key) {
stringRedisTemplate.delete(key);
}
}
upload类里判断了上传文件的类型,可以根据需求自行更改。
其他类为了在vue的文件页面提供接口。
vue
直接来第一张照片的效果图的代码:
<template>
<div>
<div style="margin: 10px 0">
<el-input v-model="name" placeholder="请输入名称" style="width: 200px" suffix-icon="el-icon-search"></el-input>
<el-button class="ml-5" type="primary" @click="load">搜索</el-button>
<el-button type="warning" @click="reset">重置</el-button>
</div>
<div style="margin: 10px 0">
<el-upload :action="'http://localhost:9090/file/upload'" :on-success="handleFileUploadSuccess"
:show-file-list="false" style="display: inline-block">
<el-button class="ml-5" type="primary">上传文件 <i class="el-icon-top"></i></el-button>
</el-upload>
<el-popconfirm
cancel-button-text='我再想想'
class="ml-5"
confirm-button-text='确定'
icon="el-icon-info"
icon-color="red"
title="您确定批量删除这些数据吗?"
@confirm="delBatch"
>
<el-button slot="reference" type="danger">批量删除 <i class="el-icon-remove-outline"></i></el-button>
</el-popconfirm>
</div>
<el-table :data="tableData" :header-cell-class-name="'headerBg'" border stripe
@selection-change="handleSelectionChange">
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column label="ID" prop="id" width="80"></el-table-column>
<el-table-column label="文件名称" prop="name"></el-table-column>
<el-table-column label="文件类型" prop="type"></el-table-column>
<el-table-column label="文件大小(kb)" prop="size"></el-table-column>
<el-table-column label="预览">
<template slot-scope="scope">
<el-button type="primary" @click="preview(scope.row.url)">预览</el-button>
</template>
</el-table-column>
<el-table-column label="下载">
<template slot-scope="scope">
<el-button type="primary" @click="download(scope.row.url)">下载</el-button>
</template>
</el-table-column>
<el-table-column label="启用">
<template slot-scope="scope">
<el-switch v-model="scope.row.enable" active-color="#13ce66" inactive-color="#ccc"
@change="changeEnable(scope.row)"></el-switch>
</template>
</el-table-column>
<el-table-column align="center" label="操作" width="200">
<template slot-scope="scope">
<el-popconfirm
cancel-button-text='我再想想'
class="ml-5"
confirm-button-text='确定'
icon="el-icon-info"
icon-color="red"
title="您确定删除吗?"
@confirm="del(scope.row.id)"
>
<el-button slot="reference" type="danger">删除 <i class="el-icon-remove-outline"></i></el-button>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<div style="padding: 10px 0">
<el-pagination
:current-page="pageNum"
:page-size="pageSize"
:page-sizes="[2, 5, 10, 20]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange">
</el-pagination>
</div>
</div>
</template>
<script>
import {serverIp} from "../../public/config";
export default {
name: "File",
data() {
return {
serverIp: serverIp,
tableData: [],
name: '',
multipleSelection: [],
pageNum: 1,
pageSize: 10,
total: 0
}
},
created() {
this.load()
},
methods: {
load() {
this.request.get("/file/page", {
params: {
pageNum: this.pageNum,
pageSize: this.pageSize,
name: this.name,
}
}).then(res => {
this.tableData = res.data.records
this.total = res.data.total
})
},
changeEnable(row) {
this.request.post("/file/update", row).then(res => {
if (res.code === '200') {
this.$message.success("操作成功")
}
})
},
del(id) {
this.request.delete("/file/" + id).then(res => {
if (res.code === '200') {
this.$message.success("删除成功")
this.load()
} else {
this.$message.error("删除失败")
}
})
},
handleSelectionChange(val) {
console.log(val)
this.multipleSelection = val
},
delBatch() {
let ids = this.multipleSelection.map(v => v.id) // [{}, {}, {}] => [1,2,3]
this.request.post("/file/del/batch", ids).then(res => {
if (res.code === '200') {
this.$message.success("批量删除成功")
this.load()
} else {
this.$message.error("批量删除失败")
}
})
},
reset() {
this.name = ""
this.load()
},
handleSizeChange(pageSize) {
console.log(pageSize)
this.pageSize = pageSize
this.load()
},
handleCurrentChange(pageNum) {
console.log(pageNum)
this.pageNum = pageNum
this.load()
},
handleFileUploadSuccess(res) {
console.log(res)
this.$message.success("上传成功")
this.load()
},
download(url) {
window.open(url)
},
preview(url) {
window.open('https://file.keking.cn/onlinePreview?url=' + encodeURIComponent(window.btoa((url))))
},
}
}
</script>
<style scoped>
</style>
下面是实现多张图片和视频显示的vue代码:
** template 部分**
<div v-for="testImgList in voucherId.testImgList">
<div @click="uplo(testImgList.id)">
<el-upload
:action="baseURL+'/file/upload'"
:on-success="handleAvatarSuccess3"
:show-file-list="false"
class="avatar-uploader"
v-text=""
>
<div v-if="testImgList.url" style="float: left">
<img v-show="testImgList.type" :src="testImgList.url" class="avatar">
<div v-show="!testImgList.type" style="height: 178px;width: 178px; float: left" >
<video style="" :src="testImgList.url"
class="avatar"
loop="loop" autoplay="autoplay"/>
</div>
</div>
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
</div>
</div>
<i style="font-size: 50px" class="el-icon-folder-add" @click="addUrlNLXc(voucherId.id)"></i>
<br>
<el-button style="float: right;margin: 10px 10px " type="success" @click="voucherAdd()">提交</el-button>
<br>
- 注意好v-for部分,是要循环的数据,
- baseURL是上传文件的接口,比如值为:localhost:8080
- handleAvatarSuccess3方法可以拿到后端返回的值。也就说文件地址。
js部分
uplo(id){
console.log(id)
this.upid=id
},
handleAvatarSuccess3(res) {
console.log(this.upid)
if (res[0] != "请上传图片或视频") {
if (res[1]=="image"){
this.voucherId.testImgList[this.upid-1].type=true
}else {
this.voucherId.testImgList[this.upid-1].type=false
}
this.voucherId.testImgList[this.upid-1].url=res[0]
this.$message.success("上传成功")
console.log(res)
} else {
this.$message.error("请上传图片或视频")
}
console.log(this.voucherId)
},
addUrlNLXc(){
console.log(this.voucherId);
var arr={
id: this.voucherId.testImgList[this.voucherId.testImgList.length-1].id + 1,
url:'',
category:'测试凭证',
inventoryId:this.voucherId.id,
type:true
}
this.voucherId.testImgList.push(arr)
console.log(this.voucherId);
},
voucherAdd(){
console.log(this.voucherId)
this.voucherId.testImg=this.testImg
this.request.post("/main-file/save",this.voucherId.testImgList).then(res => {
this.$message.success("提交成功")
this.request.get("/inventory/findId/" + this.maintainViewid).then(res => {
console.log(res)
this.tableDatamaintain = res.data
})
this.dialogvoucherView=false
})
},
addUrlNLXc方法是添加一行,对数据进行添加。
有些变量未定义,需要再data return中定义
css部分
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .el-upload:hover {
border-color: #409EFF;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
line-height: 178px;
text-align: center;
}
.avatar {
width: 178px;
height: 178px;
display: block;
float: left;
}
因为有些功能涉及其他功能块,需要理解后加入自己的需求进行修改。
如有不解与疑问,请尽情留言。
附:表单实现上传多张照片或视频回显也可以做成这样,欢迎沟通。
GitHub 加速计划 / vu / vue
80
16
下载
vuejs/vue: 是一个用于构建用户界面的 JavaScript 框架,具有简洁的语法和丰富的组件库,可以用于开发单页面应用程序和多页面应用程序。
最近提交(Master分支:4 个月前 )
73486cb5
* chore: fix link broken
Signed-off-by: snoppy <michaleli@foxmail.com>
* Update packages/template-compiler/README.md [skip ci]
---------
Signed-off-by: snoppy <michaleli@foxmail.com>
Co-authored-by: Eduardo San Martin Morote <posva@users.noreply.github.com> 6 个月前
e428d891
Updated Browser Compatibility reference. The previous currently returns HTTP 404. 6 个月前
更多推荐
已为社区贡献1条内容
所有评论(0)