1.效果展示
20240218_162533
2.前端代码
前端使用的vue框架, 要求会vue2和elementui的基本使用,步骤如下
1. 准备一个表单
注意:表单数据提交类型必须是multipart/form-data,这样上传文件才没问题给表单绑定一个提交事件
<form enctype="multipart/form-data" class="myAvatar" method="post" @submit="handlerSubmit" > <input type="file" name="image" accept="image/*" required @change="validateFile" ref="file" style="margin-bottom: 30px" /> <div class="buttom"> <input v-model="avatar.name" name="name" style="display: none" /> <el-button type="info" @click="resetFile" icon="el-icon-refresh" >重置文件</el-button > <el-input type="submit" style="width: auto" prefix-icon="el-icon-position" value="提 交" ></el-input> </div> </form>
2.判断文件的大小是否合法
// 判断文件大小合法 validateFile(event) { const file = event.target.files[0]; this.avatar.img = file; console.log(this.avatar.img); const maxSizeInBytes = 1024 * 1024; // 设置最大文件大小为 1MB if (file && file.size > maxSizeInBytes) { // 文件大小超过限制,进行相应的处理 this.$message.error("文件大小超过1MB"); event.target.value = null; // 清空文件输入框的值 this.avatar.img = null; } else { // 打开 this.dialogVisible = true; } },
3.el-dialog+cropperjs完成图片的裁剪
3.1 先安装cropperjs,执行指令`npm i cropperjs`
3.2 使用elementui的组件
先初始化// 初始化 initCropper() { const createCropper = () => { return new Promise((resolve, reject) => { this.cropper = new Cropper(this.$refs.image, { // 在这里设置Cropper.js的选项 aspectRatio: 1, // 裁剪框的宽高比 viewMode: 1, // 显示模式:0 - 图像自由裁剪,1 - 图像按比例缩放裁剪,2 - 图像按比例缩放填充裁剪区域,3 - 图像完全填充裁剪区域 dragMode: "move", // 拖动模式:'crop' - 创建裁剪框,'move' - 移动图像,'none' - 禁用拖动 cropBoxResizable: false, // 是否允许修改裁剪框大小 minContainerWidth: 200, // 最小容器宽度 minContainerHeight: 200, // 最小容器高度 minCropBoxWidth: 100, // 最小裁剪框宽度 minCropBoxHeight: 100, // 最小裁剪框高度 zoomable: true, // 是否允许缩放图像 guides: true, // 是否显示裁剪框辅助线 background: false, // 是否显示裁剪框外的背景遮罩 autoCrop: true, // 是否自动创建裁剪框 autoCropArea: 0.6, // 自动创建裁剪框时的默认裁剪区域占比 toggleDragModeOnDblclick: false, // 是否允许双击切换拖动模式 responsive: true, // 是否启用响应式布局 }); resolve("ok"); }); }; createCropper(); },
截取时候调用,getCroppedCanvas方法执行截取拿到截取后的图片的base64编码。
cropImage() { const croppedCanvas = this.cropper.getCroppedCanvas({ imageSmoothingQuality: "high", }); // 在这里你可以将裁剪后的canvas转换为DataURL或Blob,并发送到后端进行保存或进一步处理 const croppedDataUrl = croppedCanvas.toDataURL("image/jpg"); const prefixIndex = croppedDataUrl.indexOf(",") + 1; this.afterImg = croppedDataUrl.slice(prefixIndex); this.dialogVisible = false; },
重置文件,这里先关闭对话框,然后移除Blob生成的缓存url,然后再清空数据模型
// 重置文件 resetFile() { this.dialogVisible = false; URL.revokeObjectURL(this.selectFileURL); const file = this.$refs.file; file.value = null; this.avatar.img = null; this.afterImg = null; },
base64编码转file的工具类
export default function base64ToFile(base64Data, fileName) { const extendName = fileName.split(".").at(-1); const byteCharacters = atob(base64Data); // 解码Base64字符串 const byteArrays = []; for (let offset = 0; offset < byteCharacters.length; offset += 512) { const slice = byteCharacters.slice(offset, offset + 512); // 每次处理512个字符 const byteNumbers = new Array(slice.length); for (let i = 0; i < slice.length; i++) { byteNumbers[i] = slice.charCodeAt(i); } const byteArray = new Uint8Array(byteNumbers); byteArrays.push(byteArray); } const blob = new Blob(byteArrays, { type: `image/${extendName}` }); // 创建Blob对象 const file = new File([blob], fileName, { type: `image/${extendName}` }); // 创建File对象 return file; }
3. 提交事件的处理先阻止表单的默认提交,因为表单默认提交会跳转url
我们这里发送ajax请求// 提交头像 handlerSubmit(event) { event.preventDefault(); // 如果头像为空 if (this.avatar.img == "" || this.avatar.img == null) { this.$message.warning("请先选择文件"); } else { const { name } = this.avatar.img; const extendName = name.split(".").at(-1); this.avatar.img = base64ToFile(this.afterImg, "file." + extendName); this.$store .dispatch("changeAvatar", this.avatar) .then(() => { this.resetFile(); this.$message.success("提交成功!"); // 请求一下头像路径,更新仓库里的url this.$store.dispatch("getAvatarImg", "Kingah"); }) .catch((error) => { this.$message.error("添加失败"); }); } },
3.后端代码
1.编写Controller@PostMapping("/upload") public Result uploadAvatar(String name, MultipartFile image) { log.info("{}头像上传,头像大小为{}MB", name, String.format("%.2f", 1.0 * image.getSize() / 1024 / 1024)); FileUpLoadUtil.transferTo(name, image); return Result.success(); }
2.图片上传的工具类
/** * @Time : 2024/2/12 16:18 * @Author : kinGah * @File : FileUpLoadUtil.java * @Weekday : 星期一 * @Description : 文件上传,一般是头像上传 **/ @Slf4j @Component public class FileUpLoadUtil { private static String location; private static EmpService empService; @Value("${employee.avatar.location}") public void setLocation(String location) { FileUpLoadUtil.location = location; FileUpLoadUtil.location = FileUpLoadUtil.location.substring(5); } public String getLocation() { return FileUpLoadUtil.location; } // // @Autowired public void setResourceLoader(ResourceLoader resourceLoader) { FileUpLoadUtil.resourceLoader = resourceLoader; } @Autowired public void setEmployeeDao(EmpService empService) { FileUpLoadUtil.empService = empService; } /** * 删除文件 * * @param name * @return */ private static Boolean removeOriginImage(String name) { Employee emp = empService.lambdaQuery().eq(Employee::getName, name).one(); // 如果文件存在 if (ObjUtil.isNotEmpty(emp)) { try { String imgUrl = emp.getImgUrl(); String[] arr = imgUrl.split("/"); String fileName = arr[arr.length - 1]; FileUtil.del(location + fileName); } catch (Exception e) { log.error(e.getMessage()); } } return true; } public static void transferTo(String name, MultipartFile image) { log.info("文件上传..."); // 获取原始文件名 String originalFilename = image.getOriginalFilename(); String extName = FileNameUtil.extName(originalFilename); if (removeOriginImage(name)) { // 生成新的文件名 String newFilename = UUID.randomUUID().toString() + "." + extName; try { // 存储在本地 image.transferTo(new File(location + newFilename)); // 更新数据库imgUrl LambdaUpdateWrapper<Employee> wrapper = new LambdaUpdateWrapper<>(); wrapper.eq(Employee::getName, name) .set(Employee::getImgUrl, "/images/" + newFilename); empService.update(wrapper); } catch (IOException e) { throw new RuntimeException(e); } } } }
3.图片访问路径
public String concatAvatarUrl(String name) throws MalformedURLException, doBusinessException { Employee emp = query().eq("name", name).one(); if (ObjectUtil.isEmpty(emp)) throw new doBusinessException(Code.AVATAR_NOT_EXIST); String uri = emp.getImgUrl(); return "/api/" + uri; }
4.开放静态资源访问路径
/** * @Time : 2024/2/11 21:35 * @Author : kinGah * @File : SpringMvcConfig.java * @Weekday : 星期日 * @Descripton : 添加MP拦截器、静态资源映射 **/ @RequiredArgsConstructor @Configuration public class SpringMvcConfig implements WebMvcConfigurer { @Value("${employee.avatar.location}") private String location; private LoginInterceptor loginInterceptor; /** * token * @param registry */ @Override public void addInterceptors(InterceptorRegistry registry) { // registry.addInterceptor(loginInterceptor) // .addPathPatterns("/**") // .excludePathPatterns("/login/**"); } /** * 添加静态资源访问路径 * @param registry */ @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 访问路径为/images/*映射到classpath:/static/images/下 registry.addResourceHandler("/images/*") .addResourceLocations(location); } }
4.总体思路
前端设置提交文件的按钮,选中文件后进行裁剪,然后把裁剪后的文件替换为选中的文件,最后提交表单。
后端接收请求,MultipartFile类型的文件有一个transferTo(new File(...))方法将缓存的文件保存到本地,然后把地址存到数据库里,查询的时候再发送请求拿。
5.仓库地址
kingah_sph_admin: 后台管理系统 前端vue,具体代码在views/person/adminview