前后端分离 -- 深入浅出 Spring Boot + Vue + ElementUI 实现相册管理系统【文件上传 分页 】 文件上传也不过如此~

简介: 前后端分离 -- 深入浅出 Spring Boot + Vue + ElementUI 实现相册管理系统【文件上传 分页 】 文件上传也不过如此~

前后端分离 -- 深入浅出系列 Spring Boot + Vue + ElementUI 实现相册管理系统【文件上传 分页 】 文件上传也不过如此~

引言

Hello,我是Bug终结者,一名热爱后端Java的风趣且幽默的程序员~ 终于等到幸运的你~

快来发现我的宇宙哦~

博文系列

深入浅出前后端分离系列第一篇:前后端分离 --- 深入浅出Spring Boot + Vue实现员工管理系统 Vue如此简单~

项目简介

开发一个基于==Spring Boot + Vue==的前后端分离相册管理系统项目,完成增、删、改、列表、多条件、分页的功能

功能细节:

  • 实现照片的新增、删除、修改、列表、多条件、分页
  • 图片上传使用 el-upload 控件,建议设置 auto-upload 为 false。
  • 控制图片最大不能超过5M
  • 上传的必须是图片,或者约束后缀为 png/jpg/gif/bmp等图片。
  • 建议先保存标题信息到数据库,并返回得到noid的值,然后再上传图片,并更新 relative_path字段。
  • 图片保存文件命名为 yyyyMMdd/HHmmssXXXX.后缀这样的图片,例如图片为 仓鼠.png,保存后类似为 20220107/1115232832.png, XXXX表示4位的随机数字。
  • 图片在列表上显示为 200X200大小
  • 列表要支持分页,分页控件使用 el-pagination
  • 删除的时候,要将图片从硬盘上删除掉,然后再删除数据库中的记录。
  • 列表页显示如下

在这里插入图片描述

效果图

在这里插入图片描述

开发环境

后端:Spring Boot + MyBatis + Maven

前端:Vue + ElementUI

工具:前端使用WebStorm,后端使用IDEA

放松一下~

在这里插入图片描述

适度适度~

在这里插入图片描述

挺解乏的哦~🥰

==言归正传~==

项目风格

请求方式统一为Post请求,目的是保证安全性

规范了接口层统一返回格式为ResultBean,详细如:

统一返回格式风格类,ResultBean

状态码,详细信息,数据

数据表

t_picture

CREATE TABLE `t_picture` (
  `noid` int(11) NOT NULL AUTO_INCREMENT,
  `picture_name` varchar(64) NOT NULL COMMENT '照片名称',
  `relative_path` varchar(128) DEFAULT NULL COMMENT '存储相对路径',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`noid`)
) ENGINE=InnoDB AUTO_INCREMENT=71 DEFAULT CHARSET=utf8mb4

项目结构

前端结构

在这里插入图片描述

后端结构

在这里插入图片描述

项目难点

前端部分难点:

上传文件采用elementui控件的el-upload,要求不可自动上传,所以设置自动上传为false

其中,要先保存数据到数据库在进行保存图片路径存入数据库,通过存入的数据id去修改当前的数据

显示图片时采用了provide / inject组合控制的显示

因为上传成功后需要再次刷新,如果不刷新,图片不出来,那就要重新刷新页面,但刷新页面对用户来说极不友好,而且时间慢,效率低,所以采用该显示方式,完美解决了二次加载的问题

介绍provide / inject组合控制的显示到底怎么用

首先在app.vue中加入如下代码

app.vue

<template>
  <div id="app">
    <router-view v-if="isRouteAlive"/>
  </div>
</template>

<script>

  export default {
    name: 'App',
    computed: {},
    watch: {},
    //加入provide
    provide() {
      return{
        //返回reload方法,该方法为全局方法
        reload: this.reload,
      }
    },
    data() {
      return {
        isRouteAlive:true
      };
    },
    methods: {
      //刷新页面
      reload() {
        this.isRouteAlive = false;
        this.$nextTick(function () {
          this.isRouteAlive = true;
        })
      }
    }
  };
</script>

<style>
  @import '//at.alicdn.com/t/font_2048717_67mh6lqgbh.css';
</style>

在要二次加载的vue文件中加入provide

在这里插入图片描述

执行完上传图片代码后,进行二次加载
在这里插入图片描述

完美解决二次加载

核心源码

前端源码

PictureList.vue

<template xmlns:c="http://www.w3.org/1999/html">
  <div class="box-main2 box-heigt100">
    <div class="box-search">
      <div>
        <el-input type="text" size="mini" @keydown.native.enter="clkBtnSearch" @clear="clkBtnSearch" v-model="searchInfo.searchKey" clearable placeholder="请输入标题"></el-input>
      </div>
      <div class="m1">
        <el-button type="primary" size="mini"  @click="clkBtnSearch">搜索</el-button>
      </div>
      <div class="m1">
        <el-button type="warning" size="mini" @click="clkBtnAdd">新增</el-button>
      </div>
    </div>
    <div class="box-table2">
      <el-table :data="picturePage.list" border style="width: 100%;">
        <el-table-column type="index" label="序号" width="120"></el-table-column>
        <el-table-column width="150" label="照片">
          <template slot-scope="scope">
            <el-image :src="src_url+scope.row.relative_path"></el-image>
          </template>
        </el-table-column>
        <el-table-column prop="picture_name" label="标题"></el-table-column>
        <el-table-column prop="strCreateTime" label="上传日期"></el-table-column>
        <el-table-column label="操作">
          <template slot-scope="scope">
            <el-button type="warning" @click="clkBtnEdit(scope.row)">修改</el-button>
            <el-button type="danger" @click="clkBtnDelete(scope.row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <div class="box-page1">
      <el-pagination background :current-page="picturePage.pageNum" :page-size="picturePage.pageSize" :total="picturePage.total"
                     layout="total,prev,pager,next"
                     @current-change="chgPageNum">
      </el-pagination>
    </div>
    <el-dialog :visible.sync="showPictureDigLog" title="新增/编辑">
      <el-form label-width="120px">
        <el-form-item label="标题">
          <el-input type="text" size="mini" placeholder="请输入标题" v-model="picture.picture_name"></el-input>
        </el-form-item>
        <el-form-item label="照片">
          <el-image v-if="editPicture" style="width: 200px" :src="src_url+picture.relative_path"></el-image>
          <el-upload
            class="upload-demo"
            ref="upload"
            :action="uploadUrl"
            :data="uploadDataParam"
            :before-upload="checkFileType"
            accept=".png,.jpg,.gif, .bmp"
            :auto-upload="false">
            <el-button slot="trigger" size="small" type="primary">选取文件</el-button>
            <div slot="tip" class="el-upload__tip">只能上传png/jpg/gif/bmp文件,且不超过5M</div>
          </el-upload>
        </el-form-item>
      </el-form>
      <span slot="footer" class="dialog-footer">
        <el-button type="primary" size="mini" @click="clkBtnSave">确定</el-button>
        <el-button type="warning" size="mini" @click="showPictureDigLog = false">取消</el-button>
      </span>
    </el-dialog>


  </div>
</template>

<script>
  import request from '@/common/utils/request';

  export default {
    computed:{
    },
    inject: ['reload'],
    data() {
      return {
        searchInfo: {searchKey:''},
        picturePage:{pageNum: 1, pageSize: 2, list: []},
        showPictureDigLog:false,
        picture:{},
        uploadUrl:'',
        uploadDataParam:{},
        src_url:'http://127.0.0.1:8345/springbootajax/',
        editPicture:false,
      };
    },
    watch:{
      '$route.path':{
        handler:function(newVal){
          if(newVal == '/picture-list'){
            this.initData();
          }
        }, immediate: true
      },
    },
    mounted() {
      this.uploadUrl = this.settings.apiUrl + "/picture/upload";
      this.initData();
    },
    methods: {
      initData() {
        this.getPictureList();
      },
      getPictureList() {
        let url = this.settings.apiUrl + "/picture/page";
        let data = {};
        data.searchKey = this.searchInfo.searchKey;
        data.pageNum = this.picturePage.pageNum;
        data.pageSize = this.picturePage.pageSize;
        console.log(data)
        request.post(url, data).then((res) => {
          if (res.code === 0) {
            this.picturePage = res.data;
          }
        })
      },
      clkBtnAdd(){
        this.picture = {};
        this.showPictureDigLog = true;
      },
      clkBtnEdit(row) {
        this.picture = JSON.parse(JSON.stringify(row));
        this.editPicture = true;
        this.showPictureDigLog = true;
      },
      checkFileType(file) {
        let imgSize = file.size / 1024 / 1024;
        console.log("文件类型:"+file.type);
        console.log("文件大小:" + imgSize)
        if (imgSize > 5) {
          this.$message("文件超出规定上传大小,请重新上传文件!")
          return false;
        }
        //png/jpg/gif/bmp
      },
      clkBtnSave() {
        let url = this.settings.apiUrl + "/picture/save";
        let data = this.picture;

        request.post(url, data).then((res) => {
          if (res.code === 0) {
            this.uploadDataParam.noid = res.data;
            this.submitUpload();
            this.showPictureDigLog = false;
            this.getPictureList();
            this.$message("保存成功~")
          }
        })
      },

      //保存表单提交
      submitUpload() {
        this.$refs.upload.submit();
        this.reload();
      },
      clkBtnDelete(row) {
        this.$confirm("您确信要删除吗?", "提示").then(() => {
          let url = this.settings.apiUrl + "/picture/delete";
          let data = {noid: row.noid};
          request.post(url, data).then((res) => {
            if (res.code === 0) {
              this.getPictureList();
              this.$message("删除成功~");
            }
          })
        }).catch(() => {
          this.$message("取消删除~");
        })
      },
      chgPageNum(pageNum) {
        this.picturePage.pageNum = pageNum;
        this.getPictureList();
      },
      clkBtnSearch() {
        this.getPictureList();
      }
    },
  };
</script>

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

  .box-main2 {
    width: 90%;
    margin: 10px;
    box-table2{
      height: auto;
    }
    .box-search{
      display: flex;
      margin: 10px;
      .m1{
        padding-left: 10px;
      }
    }
    .box-page1{
      margin-top: 10px;
    }
  }

</style>

添加路由

route.js

{
    path: '/picture-list',
        meta: {pageTitle: '相册管理', leftMenuIndex: 'picture-list'},
            component: () => import('@/views/PictureList')
},

添加左侧菜单显示

LeftMenu.vue

<el-menu-item  index="picture-list" @click.native="clkItem('/picture-list')">
    <i class="menuitem-icon iconfont icon-logging"></i>
    <span class="menuitem-text" slot="title">相册列表</span>
</el-menu-item>

后端源码

PictureController

package com.wanshi.controller;

import com.github.pagehelper.PageInfo;
import com.wanshi.bean.Picture;
import com.wanshi.bean.ResultBean;
import com.wanshi.service.PictureService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;
import java.util.Map;

@RestController
@CrossOrigin
@RequestMapping("/picture")
public class PictureController {

    @Autowired
    private PictureService pictureService;

    /**
     * 相册分页
     * @param param
     * @return
     */
    @PostMapping("/page")
    public ResultBean<PageInfo<Picture>> page(@RequestBody Picture param) {
        ResultBean<PageInfo<Picture>> res = pictureService.page(param);
        return res;
    }

    /**
     * 保存,将新增和修改合成了一个方法,根据id判断是新增或修改
     * @param param
     * @return
     */
    @PostMapping("/save")
    public ResultBean<Integer> insert(@RequestBody Picture param) {
        ResultBean<Integer> res = pictureService.save(param);
        return res;
    }

    /**
     * 删除图片
     * @param param
     * @return
     */
    @PostMapping("/delete")
    public ResultBean<Integer> delete(@RequestBody Picture param) {
        ResultBean<Integer> res = pictureService.delete(param);
        return res;
    }

    /**
     * 上传图片,与上传信息分开,改为手动上传,点击确定后,先保存数据,再保存图片相关信息至数据库
     * @param map
     * @param file
     * @return
     */
    @PostMapping("/upload")
    public ResultBean<Integer> upload(@RequestParam Map<String, Object> map, MultipartFile file) {
        ResultBean<Integer> res = pictureService.upload(map, file);
        return res;
    }

}

PictureService

package com.wanshi.service;

import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.wanshi.bean.Picture;
import com.wanshi.bean.ResultBean;
import com.wanshi.config.GlobalSet;
import com.wanshi.mapper.PictureMapper;
import com.wanshi.utils.PbFileUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Map;

@Service
public class PictureService {

    @Autowired
    private GlobalSet globalSet;

    @Autowired
    private PictureMapper pictureMapper;

    private SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日");

    public ResultBean<PageInfo<Picture>> page(Picture param) {
        PageHelper.clearPage();
        PageHelper.startPage(param.getPageNum(), param.getPageSize());
        List<Picture> pictureList = pictureMapper.list(param);
        for (Picture picture : pictureList) {
            if (picture != null) {
                picture.setStrCreateTime(sdf.format(picture.getCreate_time()));
            }
        }
        PageInfo<Picture> pageInfo = new PageInfo<>(pictureList);
        return ResultBean.create(0, "success", pageInfo);
    }

    public ResultBean<Integer> save(Picture param) {
        if (param.getNoid() != null ) {
            pictureMapper.update(param);
        } else {
            pictureMapper.insert(param);
        }
        return ResultBean.create(0, "success", param.getNoid());
    }

    public ResultBean<Integer> delete(Picture param) {
        //获取要删除的照片存储相对地址
        Picture picture = pictureMapper.get(param);
        //拼接绝对路径并删除
        new File(globalSet.getUploadPath() + picture.getRelative_path()).delete();
        pictureMapper.delete(param);
        return ResultBean.create(0, "success", null);
    }

    public ResultBean<Integer> upload(Map<String, Object> map, MultipartFile file) {
        try {
            //查询当前id是否存在,并上传过图片
            Picture picture2 = new Picture();
            picture2.setNoid(Integer.valueOf((String)map.get("noid")));
            Picture picture3 = pictureMapper.get(picture2);
            if (picture3.getRelative_path() != null) {
                //拼接绝对路径并删除
                new File(globalSet.getUploadPath() + picture3.getRelative_path()).delete();
            }
            //上传文件,获得上传后的文件名称
            String uploadName = PbFileUtils.upload(file, globalSet.getUploadPath());

            if (!StringUtils.isEmpty((String)map.get("noid"))) {
                Picture picture = new Picture();
                picture.setNoid((Integer.valueOf((String) map.get("noid"))));
                picture.setRelative_path(uploadName);
                pictureMapper.updateImg(picture);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ResultBean.create(0, "success", null);
    }
}

工具类PbFileUtils

package com.wanshi.utils;

import com.wanshi.config.GlobalSet;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;

/**
 * 图片上传类
 */
public class PbFileUtils {

    /**
     * 文件上传,
     * @param file 上传文件
     * @param uploadPath 要上传到的路径
     * @return
     * @throws IOException
     */
    public static String upload(MultipartFile file, String uploadPath) throws IOException {

        String finalFileName = "";
        if (file.getSize() > 0) {
            String originalFilename = file.getOriginalFilename();
            //获取源文件的后缀名
            String extName = originalFilename.substring(originalFilename.lastIndexOf("."));


            //获取年月
            SimpleDateFormat sdfYyyyMMdd = new SimpleDateFormat("yyyy-MM");
            finalFileName = sdfYyyyMMdd.format(new Date()) + "/";

            //获取日
            SimpleDateFormat sdfdd = new SimpleDateFormat("dd");
            finalFileName += sdfdd.format(new Date()) + "/";

            //获取时分秒
            SimpleDateFormat sdfHHmmss = new SimpleDateFormat("HHmmss");
            finalFileName += sdfHHmmss.format(new Date());

            //生成4位随机数字
            Integer rndNum = new Random().nextInt(1000)+9000;

            //拼接随机数字和后缀名
            finalFileName += rndNum + extName;

            //目标文件
            File f1 = new File(uploadPath+finalFileName);
            if (!f1.exists()) {
                f1.mkdirs();
            }
            //开始上传
            file.transferTo(f1);
        }
        return finalFileName;
    }

}

博主寄语

至此,系统到此完美结束,该案例通俗易懂,详细一步步带入,通过本案例,可提高你的学习能力以及训练自己的逻辑思维能力,认真学习的你很耀眼,相信你的技术一定会有一个质的飞跃,加油,努力练习,祝你成为你想要成为的人!

若在本项目中遇到技术难题,可在下方评论区留言或私信我,授人以鱼不如授人以渔

如果你觉得博主写的不错的话,不妨给个一键三连,点击下方小拳头即可一键三连。

点击拳头哦

相关文章
|
存储 Java 数据库
Spring Boot 注册登录系统:问题总结与优化实践
在Spring Boot开发中,注册登录模块常面临数据库设计、密码加密、权限配置及用户体验等问题。本文以便利店销售系统为例,详细解析四大类问题:数据库字段约束(如默认值缺失)、密码加密(明文存储风险)、Spring Security配置(路径权限不当)以及表单交互(数据丢失与提示不足)。通过优化数据库结构、引入BCrypt加密、完善安全配置和改进用户交互,提供了一套全面的解决方案,助力开发者构建更 robust 的系统。
525 0
|
存储 人工智能 Java
Spring AI与DeepSeek实战四:系统API调用
在AI应用开发中,工具调用是增强大模型能力的核心技术,通过让模型与外部API或工具交互,可实现实时信息检索(如天气查询、新闻获取)、系统操作(如创建任务、发送邮件)等功能;本文结合Spring AI与大模型,演示如何通过Tool Calling实现系统API调用,同时处理多轮对话中的会话记忆。
2988 57
|
存储 人工智能 自然语言处理
用Spring AI搭建本地RAG系统:让AI成为你的私人文档助手
想让AI帮你读懂PDF文档吗?本文教你用Spring AI和Ollama搭建一个本地RAG系统,让AI成为你的私人文档助手。无需GPU,无需云端API,只需几行代码,你的文档就能开口说话了!
2206 2
|
消息中间件 存储 Java
📨 Spring Boot 3 整合 MQ 构建聊天消息存储系统
本文详细介绍了如何使用Spring Boot 3结合RabbitMQ构建高效可靠的聊天消息存储系统。通过引入消息队列,实现了聊天功能与消息存储的解耦,解决了高并发场景下直接写入数据库带来的性能瓶颈问题。文章首先分析了不同MQ产品的特点及适用场景,最终选择RabbitMQ作为解决方案,因其成熟稳定、灵活路由和易于集成等优势。接着,通过Docker快速部署RabbitMQ,并完成Spring Boot项目的配置与代码实现,包括生产者发送消息、消费者接收并处理消息等功能。最后,通过异步存储机制,既保证了消息的即时性,又实现了可靠持久化。
1027 0
📨 Spring Boot 3 整合 MQ 构建聊天消息存储系统
|
Java 调度 流计算
基于Java 17 + Spring Boot 3.2 + Flink 1.18的智慧实验室管理系统核心代码
这是一套基于Java 17、Spring Boot 3.2和Flink 1.18开发的智慧实验室管理系统核心代码。系统涵盖多协议设备接入(支持OPC UA、MQTT等12种工业协议)、实时异常检测(Flink流处理引擎实现设备状态监控)、强化学习调度(Q-Learning算法优化资源分配)、三维可视化(JavaFX与WebGL渲染实验室空间)、微服务架构(Spring Cloud构建分布式体系)及数据湖建设(Spark构建实验室数据仓库)。实际应用中,该系统显著提升了设备调度效率(响应时间从46分钟降至9秒)、设备利用率(从41%提升至89%),并大幅减少实验准备时间和维护成本。
621 0
|
人工智能 自然语言处理 Java
对话即服务:Spring Boot整合MCP让你的CRUD系统秒变AI助手
本文介绍了如何通过Model Context Protocol (MCP) 协议将传统Spring Boot服务改造为支持AI交互的智能系统。MCP作为“万能适配器”,让AI以统一方式与多种服务和数据源交互,降低开发复杂度。文章以图书管理服务为例,详细说明了引入依赖、配置MCP服务器、改造服务方法(注解方式或函数Bean方式)及接口测试的全流程。最终实现用户通过自然语言查询数据库的功能,展示了MCP在简化AI集成、提升系统易用性方面的价值。未来,“对话即服务”有望成为主流开发范式。
10210 7
|
SQL 前端开发 Java
深入理解 Spring Boot 项目中的分页与排序功能
本文深入讲解了在Spring Boot项目中实现分页与排序功能的完整流程。通过实际案例,从Service层接口设计到Mapper层SQL动态生成,再到Controller层参数传递及前端页面交互,逐一剖析每个环节的核心逻辑与实现细节。重点包括分页计算、排序参数校验、动态SQL处理以及前后端联动,确保数据展示高效且安全。适合希望掌握分页排序实现原理的开发者参考学习。
897 4
|
存储 JavaScript 前端开发
如何高效实现 vue 文件批量下载及相关操作技巧
在Vue项目中,实现文件批量下载是常见需求。例如文档管理系统或图片库应用中,用户可能需要一次性下载多个文件。本文介绍了三种技术方案:1) 使用`file-saver`和`jszip`插件在前端打包文件为ZIP并下载;2) 借助后端接口完成文件压缩与传输;3) 使用`StreamSaver`解决大文件下载问题。同时,通过在线教育平台的实例详细说明了前后端的具体实现步骤,帮助开发者根据项目需求选择合适方案。
1108 0
|
JSON Java 数据格式
微服务——SpringBoot使用归纳——Spring Boot中的全局异常处理——处理系统异常
本文介绍了在Spring Boot项目中如何通过创建`GlobalExceptionHandler`类来全局处理系统异常。通过使用`@ControllerAdvice`注解,可以拦截项目中的各种异常,并结合`@ExceptionHandler`注解针对特定异常(如参数缺失、空指针等)进行定制化处理。文中详细展示了处理参数缺失异常和空指针异常的示例代码,并说明了通过拦截`Exception`父类实现统一异常处理的方法。虽然拦截`Exception`可一劳永逸,但为便于问题排查,建议优先处理常见异常,最后再兜底处理未知异常,确保返回给调用方的信息友好且明确。
1581 0
微服务——SpringBoot使用归纳——Spring Boot中的全局异常处理——处理系统异常

热门文章

最新文章