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)
1.文件上传功能开发:springboot +vue基本的文件上传功能,上传讲师头像
teacher.vue
type=file,文件上传控件
触发文件上传动作方法一:增加一个新的按钮“开始上传”
触发文件上传动作方法二:在文件上传组件上增加change事件
teacher.vue
UploadController.java
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
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("");?
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;
}
}
1.文件上传功能开发:文件上传成功后返回值处理
UploadController.java
teacher.vue
1.文件上传功能开发:课程管理和小节管理使用文件组件
course.vue
准备了几张图片
?section.vue
springboot有默认上传文件大小的限制,可通过配置修改
max-file-size:单个文件的大小,max-request-size:请求的大小。比如一次请求可以上传多个文件,这时两个值就不一样了。
section.vue
1.文件上传功能开发:自动获取视频时长
section.vue
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>
<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,所以点击插入图片时,弹出的也是一个模态框