Spring Cloud + Vue前后端分离-第8章 文件上传功能开发

 源代码在GitHub - 629y/course: Spring Cloud + Vue前后端分离-在线课程

Spring Cloud + Vue前后端分离-第8章 文件上传功能开发

8-1 完成基本的文件上传功能

搭建文件模块- file

1.增加file模块,用于文件上传和存储

pom.xml(file)

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.course</groupId>
        <artifactId>course</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <artifactId>file</artifactId>
    <dependencies>
        <!-- 热部署DevTools -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>

        <!-- 集成mybatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.course</groupId>
            <artifactId>server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
</project>

FileApplication.java

application.properties

logback.xml

启动成功

application.properties(gateway)

springboot +vue文件上传功能

1.文件上传功能开发:springboot +vue基本的文件上传功能,上传讲师头像

teacher.vue

type=file,文件上传控件

触发文件上传动作方法一:增加一个新的按钮“开始上传”

触发文件上传动作方法二:在文件上传组件上增加change事件

teacher.vue

UploadController.java

8-2 讲师头像的保存与显示

文件访问配置

1.文件上传功能开发:增加springboot静态资源配置,配置文件访问路径

SpringBoot静态资源配置,静态资源包含图片、CSS、JS等

SpringMvcConfig.java

UploadController.java

 http://127.0.0.1:9003/file/f/teacher/6vIkHyAr-头像1.jpg

测试路由转发到file模块是否生效,测试的时候要注意浏览器是有静态资源缓存的,可以用ctrl+f5强制刷新,也可以用chrome无痕浏览来测试,不会有缓存。 

 http://127.0.0.1:9000/file/f/teacher/6vIkHyAr-头像1.jpg

文件上传实时显示

1.文件上传功能开发:文件上传实时显示与保存

UploadController.java

teacher.vue

img-responsive:bootstrap内置的样式,图片自适应

讲师头像显示优化

1.文件上传功能开发:讲师头像显示优化

利用bootstraps栅格系统,一个div是12格,可以让图片只占4格的宽度

teacher.vue

application.properties

Maven多环境配置,将开发环境和生存环境配置成不同的值 

@Value,注入属性值

UploadController.java

SpringMvcConfig.java

8-3 文件上传组件开发

使用单独的文件上传按钮

1.文件上传功能开发:使用单独的文件上传按钮

优化的点:用一个单独的按钮,来代替file控件,且操作的流程没有变化。

teacher.vue

增加上传文件类型的判断

1.文件上传功能开发:增加上传文件类型的判断

优化:使用vue的$refs来获取组件

teacher.vue

制作文件上传公共组件

1.文件上传功能开发:制作文件上传公共组件

2.修复不能连续上传同一个文件的BUG

一个项目中,很多地方都会用到文件上传功能,所以有必要把文件上传做成通用组件。这样以后也可以把文件组件直接拷贝到其它项目直接用。

file.vue

teacher.vue

为组件增加上传成功后的回调函数,和组件不相关的业务代码应该由外部通过回调函数传进来。

file.vue

teacher.vue

file.vue

公共代码的放到组件里,变化的代码做成可配置的属性,由外部传入。

如果一个页面放了两个文件上传组件,会出现两个input的id重复,可以把id 也做成可配置的。

 teacher.vue

 

BUG:连续选择同一个文件的时候,第二次会没反应。

$("#" + _this.inputId + "-input").val(""); 

8-4 增加文件管理功能

文件表设计与基本代码生成

1.文件上传功能开发:文件表设计与基本代码生成

all.sql

#文件
DROP TABLE IF EXISTS `file`;
CREATE TABLE `file`  (
  `id` char(8) not null default '' comment 'id',
  `path` varchar(100) not null comment '相对路径',
  `name` varchar(100) comment '文件名',
  `suffix` varchar(10)  comment '后缀',
  `size` int  comment '大小|字节B',
  `use` char(1) comment '用途|枚举[FileUseEnum]:COURSE("C", "讲师"),TEACHER("T", "课程")',
  `created_at` datetime(3) comment '创建时间',
  `updated_at` datetime(3) comment '修改时间',
  primary key (`id`),
  unique key `path_unique` (`path`)
)ENGINE = InnoDB default charset = utf8mb4 COMMENT = '文件';

FileUseEnum.java

生成generatorConfig.xml

会对关键字的表、字段,增加反引号

EnumGenerator.java 

ServerGenerator.java

 VueGenerator.java

admin.vue

router.js

测试

上传文件时保存文件记录

上传文件时保存文件记录

1.文件上传功能开发:上传文件时保存文件记录

统一文件名为8位uuid+ 文件后缀。下一章,我们还会对进一步规范文件名。

UploadController.java

application.properties

文件记录不允许编辑和删除,但是可以做一些统计功能,比如文件大小统计,文件类型统计。也可增加文件审核功能,对不合规的图片、视频做处理。

去掉文件管理的新增、修改、删除功能

1.文件上传功能开发:去掉文件管理的新增、修改、删除功能

file.vue

<template>
  <div>
    <p>
      <button v-on:click="list(1)" class="btn btn-white btn-default btn-round">
        <i class="ace-icon fa fa-refresh"></i>
        刷新
      </button>
    </p>
    <pagination ref="pagination" v-bind:list="list" v-bind:itemCount="8"></pagination>
    <!--  v-bind:list="list",前面的list,是分页组件暴露出来的一个回调方法,后面的list,是file组件的list方法  -->
    <table id="simple-table" class="table  table-bordered table-hover">
      <thead>
      <tr>
        <th>id</th>
        <th>相对路径</th>
        <th>文件名</th>
        <th>后缀</th>
        <th>大小</th>
        <th>用途</th>
      </tr>
      </thead>
      <tbody>
      <tr v-for="file in files">
        <td>{{file.id}}</td>
        <td>{{file.path}}</td>
        <td>{{file.name}}</td>
        <td>{{file.suffix}}</td>
        <td>{{file.size}}</td>
        <td>{{FILE_USE | optionKV(file.use)}}</td>
      </tr>
      </tbody>
    </table>
  </div>
</template>
<script>
  import Pagination from "../../components/pagination";

  export default {
    name: "file-file",
    components: {Pagination},
    data: function () {
      return {
        file:{},
        // file变量用于绑定form 表单的数据
        files: [],
        FILE_USE: FILE_USE,
      }
    },
    mounted:function () {
      let _this = this;
      _this.$refs.pagination.size = 5;
      _this.list(1);
    },
    methods:{
      /**
       * 列表查询
       */
      list(page) {
        let _this = this;
        Loading.show();
        _this.$refs.pagination.size = 5;
        // /admin 用于控台类的接口,/web 用于网站类的接口。接口设计中,用不同的请求前缀代表不同的入口,做接口隔离,方便做鉴权、统计、监控等
        _this.$ajax.post(process.env.VUE_APP_SERVER +"/file/admin/file/list",{
          page:page,
          size:_this.$refs.pagination.size,
        }).then((response) => {
          Loading.hide();
          let resp = response.data;
          _this.files = resp.content.list;
          //response.data 就相当于responseDto
          _this.$refs.pagination.render(page,resp.content.total);
        })
      },
    }
  }
</script>

FileController.java

删除了保存和删除两个接口 

增加文件大小格式化过滤器

1.文件上传功能开发:增加文件大小格式化过滤器

filter.js

小技巧:不用算出最终的数值大小,直接写表达式,让程序来计算,这种写法方便维护和修改,比如我写个5242880,看不出来是多少,但是写5*1024*1024,一看就是5M。

file.vue

文件按用途分类保存

1.文件上传功能开发:文件按用途分类保存

FileUseEnum.java

课程讲师弄错了,调换一下

改过之后,记得重新生成一遍 

file.vue

teacher.vue

UploadController.java

package com.course.file.controller.admin;

import com.course.server.dto.FileDto;
import com.course.server.dto.ResponseDto;
import com.course.server.enums.FileUseEnum;
import com.course.server.service.FileService;
import com.course.server.util.UuidUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;

@RequestMapping("/admin")
@RestController
public class UploadController {
    private static final Logger LOG = LoggerFactory.getLogger(UploadController.class);
    public static final String BUSINESS_NAME= "文件上传";
    //@Value,注入属性值
    @Value("${file.domain}")
    private String FILE_DOMAIN;

    @Value("${file.path}")
    private String FILE_PATH;

    @Resource
    private FileService fileService;

    @RequestMapping("/upload")
    public ResponseDto upload(@RequestParam MultipartFile file,String use) throws IOException {
        LOG.info("上传文件开始");
        LOG.info(file.getOriginalFilename());
        LOG.info(String.valueOf(file.getSize()));

        //保存文件到本地
        FileUseEnum useEnum = FileUseEnum.getByCode(use);
        String key = UuidUtil.getShortUuid();
        String fileName = file.getOriginalFilename();
        String suffix = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();

        //如果文件夹不存在则创建
        String dir = useEnum.name().toLowerCase();
        File fullDir = new File(FILE_PATH + dir);
        if(!fullDir.exists()){
            fullDir.mkdirs();
        }
        String path = dir + File.separator + key + "." + suffix;
        String fullPath =FILE_PATH + path;
        File dest = new File(fullPath);
        file.transferTo(dest);
        LOG.info(dest.getAbsolutePath());

        LOG.info("保存文件记录开始");
        FileDto fileDto = new FileDto();
        fileDto.setPath(path);
        fileDto.setName(fileName);
        fileDto.setSize(Math.toIntExact(file.getSize()));
        fileDto.setSuffix(suffix);
        fileDto.setUse(use);
        fileService.save(fileDto);

        ResponseDto responseDto = new ResponseDto();
        responseDto.setContent(FILE_DOMAIN + path);
        return responseDto;
    }
}

8-5 文件上传组件的使用

文件上传成功后返回值处理

1.文件上传功能开发:文件上传成功后返回值处理

UploadController.java

teacher.vue

课程管理和小节管理使用文件组件

1.文件上传功能开发:课程管理和小节管理使用文件组件

course.vue

准备了几张图片

 section.vue

springboot有默认上传文件大小的限制,可通过配置修改

max-file-size:单个文件的大小,max-request-size:请求的大小。比如一次请求可以上传多个文件,这时两个值就不一样了。

section.vue

自动获取视频时长

1.文件上传功能开发:自动获取视频时长

section.vue

8-6 课程内容中增加文件管理

1.文件上传功能开发:增加课程内容文件管理,用于富文本框中插入图片或视频

2.修复course,section,teacher页面中关于文件组件中inputId的赋值

3.修复file组件中selectFile,id的值改为变量

新增【课程内容文件表】,用于管理当前这篇内容用到的文件,支持新增,删除。上传文件后自动新增一条记录。

all.sql

ServerGenerator.java

generatorConfig.xml

只需要生成持久层和服务端的代码,不需要生成界面

CourseContentFileService.java

CourseContentFileController.java

Bug修复:这里的选择器应该用inputId变量

file.vue

Bug修复:可配置的变量名是inputId,所以应该用v-bind:input-id

teacher.vue

section.vue

course.vue

style="overflow:auto;" 

 流程:先使用文件上传组件,上传文件,并保存file表记录,前端拿到上传结果后,再调一次服务端,保存course_content_file表记录。

这里的删除只是删除course_content_file表的记录,并没有删除真正的文件,也没有删除file表的记录,看起来会导致脏数据,下一章我们会解决这个问题。

tool.js

问题:课程页面越来越复杂,课程内容模态框相关的代码越来越多,所以有必要把课程内容模态框相关的内容移到单独的页面

1.文件上传功能开发:将文件内容模态框做成单独的页面,代码更容易维护,内容编辑更方便

content.vue

<template>
  <div>
    <h4 class="lighter">
      <i class="ace-icon fa fa-hand-o-right icon-animated-hand-pointer blue"></i>
      <router-link to="/business/course" class="pink"> {{course.name}} </router-link>
    </h4>
    <hr>

    <file v-bind:input-id="'content-file-upload'"
          v-bind:text="'上传文件'"
          v-bind:suffixs="['jpg','jpeg', 'png','mp4']"
          v-bind:use="FILE_USE.COURSE.key"
          v-bind:after-upload="afterUploadContentFile"></file>
    <br>
    <table id="file-table" class="table table-bordered table-hover">
      <thead>
      <tr>
        <th>名称</th>
        <th>地址</th>
        <th>大小</th>
        <th>操作</th>
      </tr>
      </thead>

      <tbody>
      <tr v-for="(f,i) in files" v-bind:key="f.id">
        <td>{{f.name}}</td>
        <td>{{f.url}}</td>
        <td>{{f.size | formatFileSize}}</td>
        <td>
          <button v-on:click="delFile(f)" class="btn btn-white btn-xs btn-warning btn-round">
            <i class="ace-icon fa fa-times red2"></i>
            删除
          </button>
        </td>
      </tr>
      </tbody>
    </table>

    <form class="form-horizontal">
      <div class="form-group">
        <div class="col-lg-12">
          {{saveContentLabel}}
        </div>
      </div>
      <div class="form-group">
        <div class="col-lg-12">
          <div id="content"></div>
        </div>
      </div>
      <div class="form-group">
        <div class="col-lg-12">
          {{saveContentLabel}}
        </div>
      </div>
    </form>
    <p>
      <button v-on:click="saveContent()" type="button" class="btn btn-white btn-info btn-round">
        <i class="ace-icon fa fa-plus blue"></i>
        保存
      </button>&nbsp;
      <router-link to="/business/course" type="button" class="btn btn-white btn-info btn-round" data-dismiss="modal">
        <i class="ace-icon fa fa-times"></i>
        返回课程
      </router-link>
    </p>
  </div>
</template>
<script>
import File from "@/components/file.vue";
export default {
  name: "business-course-content",
  components: {File},
  data: function () {
    return {
      course: {},
      // course变量用于绑定form 表单的数据
      FILE_USE:FILE_USE,
      saveContentLabel:"",
      files:[],
      saveContentInterval:{},
    }
  },
  mounted: function () {
    let _this = this;
    let course = SessionStorage.get(SESSION_KEY_COURSE) || {};
    if (Tool.isEmpty(course)) {
      _this.$router.push("/welcome");
    }
    _this.course = course;
    _this.init();
    // sidebar激活样式方法一
    this.$parent.activeSidebar("business-course-sidebar");
  },
  destroyed: function() {
    let _this = this;
    console.log("组件销毁");
    clearInterval(_this.saveContentInterval);
  },
  methods: {
    /**
     * 打开内容编辑器
     */
    init() {
      let _this = this;
      let course = _this.course;
      let id = course.id;
      $("#content").summernote({
        focus: true,
        height: 300
      });
      //先清空历史文本
      $("#content").summernote('code', '');
      _this.saveContentLabel = "";

      //加载内容文件列表
      _this.listContentFiles();

      Loading.show();
      _this.$ajax.get(process.env.VUE_APP_SERVER + '/business/admin/course/find-content/'
          +id).then((response) => {
        Loading.hide();
        let resp = response.data;
        if (resp.success) {
          //调用modal方法时,增加backdrop:'static',则点击空白位置,模态框不会自动关闭。
          $("#course-content-modal").modal({backdrop: 'static', keyboard: false});
          if (resp.content) {
            $("#content").summernote('code', resp.content.content);
          }
          //定时自动保存
          //扩展:setInterval,重复的定时任务;setTimeout,只执行一次的定时任务
          _this.saveContentInterval = setInterval(function (){
            _this.saveContent();
          },5000);
        } else {
          Toast.warning(resp.message);
        }
      });
    },
    /**
     * 保存内容
     */
    saveContent() {
      let _this = this;
      let content = $("#content").summernote("code");
      _this.$ajax.post(process.env.VUE_APP_SERVER + '/business/admin/course/save-content/',
          {
            id: _this.course.id,
            content: content
          }).then((response) => {
        Loading.hide();
        let resp = response.data;
        if (resp.success) {
          // Toast.success("内容保存成功");
          // let now = Tool.dateFormat("yyyy-MM-dd hh:mm:ss");
          let now = Tool.dateFormat("mm:ss");
          _this.saveContentLabel = "最后保存时间:"+now;
        } else {
          Toast.warning(resp.message);
        }
      });
    },
    /**
     * 加载内容文件列表
     */
    listContentFiles(){
      let _this = this;
      _this.$ajax.get(process.env.VUE_APP_SERVER + "/business/admin/course-content-file/list/" + _this.course.id).then((response) => {
        let resp = response.data;
        if (resp.success) {
          _this.files = resp.content;
        }
      });
    },
    /**
     * 上传内容文件后,保存内容文件记录
     */
    afterUploadContentFile(response){
      let _this = this;
      console.log("开始保存文件记录");
      let file = response.content;
      file.courseId = _this.course.id;
      file.url = file.path;
      _this.$ajax.post(process.env.VUE_APP_SERVER + "/business/admin/course-content-file/save",file).then((response) => {
        let resp = response.data;
        if (resp.success) {
          Toast.success("上传文件成功");
          _this.files.push(resp.content);
        }
      });
    },
    /**
     * 删除内容文件
     */
    delFile(f){
      let _this = this;
      Confirm.show("删除课程后不可恢复,确认删除?",function (){
        _this.$ajax.delete(process.env.VUE_APP_SERVER + "/business/admin/course-content-file/delete/"+ f.id).then((response) => {
          let resp = response.data;
          if (resp.success) {
            Toast.success("删除文件成功");
            Tool.removeObj(_this.files,f);
          }
        });
      })
    }
  }
}
</script>

course.vue

原来是打开模态框时执行的代码,现在变成打开content页面就去执行

router.js

 原来的逻辑是打开模态框时,开始定时任务,关闭模态框时,清除定时任务。现在要改为打开页面组件初始化时(mounted),开始定时任务,页面组件销毁时(destroyed),清除定时任务

summernote富文本框,是基于bootstrap,所以点击插入图片时,弹出的也是一个模态框

  • 17
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值