SpringCloud + Vue 视频管理系统第五章(大章的开发)
第一节、大章查询功能开发(前端和后端)
1、新建business模块,建立chapter表
新增maven子项目business,和以前一样把system模块下的依赖全部导入到business模块中。
新建com.course.business包,然后把system中的配置文件以及类文件都复制到business中,修改相应的内容(端口号改成了9002)。
大章开发在数据库中加入大章表(chapter),在all.sql中执行如下代码。
drop table if exists `chapter`;
create table `chapter` (
`id` char(8) not null comment 'ID',
`course_id` char(8) comment '课程ID',
`name` varchar(50) comment '名称',
primary key (`id`)
) engine=innodb default charset=utf8mb4 comment='大章';
2、使用代码生成器生成持久层代码
以前在server模块中创建过generatorConfig.xml文件,这次还是使用这个文件,修改tableName字段为Chapter,注意为了方便以后复用,最好不要直接在原位置修改,建议复制一行把原来的注释掉。运行mybatis-generator。
<table tableName="chapter" domainObjectName="Chapter"/>
3、编写其他层代码
service层先写,因为business中引入了TestService,那么把TestService和TestController的Test和test,替换为Chapter和chapter即可(当然需要复制再替换,保留原来的TestXXXX的文件)。
再看一下controller,这里的controller是给控台用的,但是business中的controller有可能是给控台使用,也可能是给网站使用,所以最好再加一层目录。
在controller包下创建admin(给控台使用的)目录,把ChapterController放入。这时还要注意一个问题,一般创建一个项目,请求的访问地址,应该和包目录要一致,所以要在请求中再加一层/admin才行。
@RestController
@RequestMapping("/admin")
public class ChapterController {
@Resource
private ChapterService chapterService;
@RequestMapping("/chapter")
public List<Chapter> chapter(){
return chapterService.list();
}
}
访问127.0.0.1:9002/business/admin/chapte可以访问到相应数据。
数据传输也有点问题,一般开发中数据库到持久层和业务层到视图层采用的实体类不一样,因为可能在传递的过程中要加上其他数据。所以普遍要给业务层和视图层之间加一层dto类。这里就采用dto的方式,创建dto目录,把Chapter类复制到dto包中,命名为ChapterDto。这里还要修改service和controller层,不再使用以前的实体类传输,而是dto进行传输。
ChapterService类代码:也就是把chapter转成chapterDto。
@Service
public class ChapterService {
@Resource
private ChapterMapper chapterMapper;
public List<ChapterDto> list(){
ChapterExample chapterExample = new ChapterExample();
List<Chapter> chapterList = chapterMapper.selectByExample(chapterExample);
List<ChapterDto> chapterDtoList = new ArrayList<>();
for(int i = 0,l = chapterList.size();i < l;i++){
Chapter chapter = chapterList.get(i);
ChapterDto chapterDto = new ChapterDto();
BeanUtils.copyProperties(chapter,chapterDto);
chapterDtoList.add(chapterDto);
}
return chapterDtoList;
}
}
ChapterController类代码:
@RestController
@RequestMapping("/admin")
public class ChapterController {
@Resource
private ChapterService chapterService;
@RequestMapping("/chapter")
public List<ChapterDto> chapter(){
return chapterService.list();
}
}
4、前端页面开发(使用假数据)
这里为了方便展示查询出来的数据,使用ace模板中的tables.html页面。
现在admin目录下的src下的views下的admin目录创建chapter.vue。这里为了方便绑定路由,先简单写一个模板。
<template>
<div>
<h1>大章</h1>
</div>
</template>
在router下加上子路由。
import Vue from 'vue'
import Router from 'vue-router'
import Login from './views/login.vue'
import Admin from './views/admin.vue'
import Welcome from './views/admin/welcome.vue'
import Chapter from './views/admin/chapter.vue'
Vue.use(Router);
export default new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: [{
path: '*',
redirect: "/login",
}, {
path: '/login',
component: Login
}, {
path: '/admin',
component: Admin,
children: [{
path: 'welcome',
component: Welcome,
}, {
path: 'chapter',
component: Chapter,
}]
}]
})
访问http://localhost:8080/admin/chapter可以访问到。这时就需要tables.html中的代码了。
在该页面的代码中搜索PAGE BEGIN就能找到,收起代码,复制到chapter.vue中。代码太多了…
这时自动刷新可以看到页面效果。
因为这个是router-vue引入的,router-vue外部已经有两行样式了。
<div class="row">
<div class="col-xs-12">
<router-view/>
</div>
</div>
而刚才引入的代码前两行也有这个样式,那么就得去掉刚才引入重复的两行,还有结尾两行。这样假数据页面就完成了。
5、点击sidebar菜单实现页面跳转
找到系统管理代码,合并然后复制粘贴一份,改成业务管理。其中某个子模块改成大章管理。也就是变成了下面这样。
<li class="active open">
<a href="#" class="dropdown-toggle">
<i class="menu-icon fa fa-list"></i>
<span class="menu-text"> 业务管理 </span>
<b class="arrow fa fa-angle-down"></b>
</a>
<b class="arrow"></b>
<ul class="submenu">
<li class="">
<a href="tables.html">
<i class="menu-icon fa fa-caret-right"></i>
大章管理
</a>
<b class="arrow"></b>
</li>
</ul>
</li>
系统管理因为目前也没啥用,所以把它的active open删除掉,不让他默认打开。也就变成了下面这样。
当前访问的路径是http://localhost:8080/admin/chapter,为了更好的展示,最好让它也是访问/business路径找到的。访问http://localhost:8080/admin/business/chapter路径。那么就需要修改router.js。
{
path: 'business/chapter',
component: Chapter,
}
为了让chapter页面sidebar默认开启状态,可以修改它的二级状态,也就是加上active样式。这样就变为下面这个样式。
下面就该实现怎么点击欢迎跳到欢迎页,点击大章按钮跳到大章管理,并且还关闭或激活相应的一级、二级样式。
这里需要加入方法,为了更好的追踪到相应的dom,这里加上了一些id,这些id和路由的路径是相互对应的,方便后面通过id匹配相应的请求。
- 欢迎页面:id=“welcome-sidebar”;
- 大章管理:id=“business-chapter-sidebar”;
activeSidebar: function (id) {
$("#" + id).siblings().removeClass("active");
$("#" + id).siblings().find("li").removeClass("active");
$("#" + id).addClass("active");
let parentLi = $("#" + id).parents("li");
if (parentLi) {
parentLi.siblings().removeClass("open active");
parentLi.addClass("open active");
}
}
上面是admin.vue定义的activeSidebar方法,可以用在其他页面上。
chapter.vue:
<script>
export default {
name: "chapter",
mounted: function() {
this.$parent.activeSidebar("business-chapter-sidebar");
},
methods: {
}
}
</script>
welcome.vue:
<script>
export default {
name: "welcome",
mounted: function() {
this.$parent.activeSidebar("welcome-sidebar");
},
methods: {
}
}
</script>
样式变化的方法有了,怎么实现跳转呢?需要使用router-link来实现。vue的router-link相当于跳转,to哪个路由就跳哪个页面。
<li class="" id="welcome-sidebar">
<router-link to="/admin/welcome">
<i class="menu-icon fa fa-tachometer"></i>
<span class="menu-text"> 欢迎 </span>
</router-link>
<b class="arrow"></b>
</li>
----------------------------------------------------
<ul class="submenu">
<li class="active" id="business-chapter-sidebar">
<router-link to="/admin/business/chapter">
<i class="menu-icon fa fa-caret-right"></i>
大章管理
</router-link>
<b class="arrow"></b>
</li>
</ul>
这样就实现了点击跳转。
这里还需要调整请求的路由,刚登录直接就进入业务管理了,实际上登陆进去,应该进入welcome页面,而且/admin好像有点多余,因为进入了以后哪个网址都是以它为开头的。那就不如不加了。
首先login页面跳转到admin页面时,代码修改成这样:
login(){
router.js也是:
path: "/",
name: "admin",
component: Admin,
admin下的欢迎页和大章页面也是:
<router-link to="/welcome">
<i class="menu-icon fa fa-tachometer"></i>
<span class="menu-text"> 欢迎 </span>
</router-link>
<router-link to="/business/chapter">
<i class="menu-icon fa fa-caret-right"></i>
大章管理
</router-link>
为了后面方便router.js给每个子路由加个名字:
import Vue from "vue"
import Router from "vue-router"
import Login from "./views/login.vue"
import Admin from "./views/admin.vue"
import Welcome from "./views/admin/welcome.vue"
import Chapter from "./views/admin/chapter.vue"
Vue.use(Router);
export default new Router({
mode: "history",
base: process.env.BASE_URL,
routes: [{
path: "*",
redirect: "/login",
}, {
path: "/login",
component: Login
}, {
path: "/",
name: "admin",
component: Admin,
children: [{
path: "welcome",
name: "welcome",
component: Welcome,
}, {
path: "business/chapter",
name: "business/chapter",
component: Chapter,
}]
}]
})
6、通用sidebar激活方法
当前这个sidebar,每创建一个siderbar,就需要修改很多代码,不如直接写个通用的方法。
admin下写了个watch函数,监听的时$route路由的变化,只要变化就进行相应的响应。根据刚才设置的路由名称,修改它的路由把它变为id,通过activeSidebar方法激活样式,这下每个页面的激活方法就不再需要了。
<script>
export default {
name: "admin",
mounted: function() {
let _this = this;
$("body").removeClass("login-layout light-login");
$("body").attr("class", "no-skin");
_this.activeSidebar(_this.$route.name.replace("/", "-") + "-sidebar");
},
watch: {
$route: {
handler:function(val, oldVal){
console.log("---->页面跳转:", val, oldVal);
let _this = this;
_this.$nextTick(function(){
_this.activeSidebar(_this.$route.name.replace("/", "-") + "-sidebar");
})
}
}
},
methods: {
login () {
this.$router.push("/admin")
},
activeSidebar: function (id) {
$("#" + id).siblings().removeClass("active");
$("#" + id).siblings().find("li").removeClass("active");
$("#" + id).addClass("active");
let parentLi = $("#" + id).parents("li");
if (parentLi) {
parentLi.siblings().removeClass("open active");
parentLi.addClass("open active");
}
}
}
}
</script>
第二节、大章查询功能开发(前后端交互)
1、集成axios
terminal窗口,跳到admin目录下。
执行如下指令:
npm install axios --save
引入到main.js中。加上这两句:
import axios from 'axios'
Vue.prototype.$ajax = axios;
使用前后端交互:
list() { let _this = this; _this.$ajax.get('http://127.0.0.1:9002/business/admin/chapter/list').then((response)=>{ console.log("查询大章列表结果: ",response); })}
这个方法应该在页面初始化的时候执行。
mounted: function() { let _this = this; _this.list();},
看前端的路径时/business/admin/chapter/list,但controller路径不对,修改一下。
@RestController@RequestMapping("/admin/chapter")public class ChapterController { @Resource private ChapterService chapterService; @RequestMapping("/list") public List<ChapterDto> list() { return chapterService.list(); }}
2、解决跨域问题
我们发现,网页报错了,主要时CORS跨域问题。前端后端交互就容易产生跨域问题。
增加跨域配置,在server下的config包下,创建这个CorsConfig类,把下面代码放入:
@Configurationpublic class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedHeaders(CorsConfiguration.ALL) .allowedMethods(CorsConfiguration.ALL) .allowCredentials(true) .maxAge(3600);
这个时候发现console控制台能够接收到数据了。
实际上更好的方式是在gateway中启动跨域配置。在GatewayApplication中配置代码:
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(Boolean.TRUE);
config.addAllowedMethod("*");
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
取消config中的CorsConfig取消掉。
3、接收数据并显示
首先调整chapter.vue中的表格,只留最基本的即可,其他的到时候使用循环显示。这个比较困难到时候直接拷代码即可。
这是修改后的。
<tr>
<td>123</td>
<td>test</td>
<td>1111</td>
<td>
<div class="hidden-sm hidden-xs btn-group">
<button class="btn btn-xs btn-success">
<i class="ace-icon fa fa-check bigger-120"></i>
</button>
<button class="btn btn-xs btn-info">
<i class="ace-icon fa fa-pencil bigger-120"></i>
</button>
<button class="btn btn-xs btn-danger">
<i class="ace-icon fa fa-trash-o bigger-120"></i>
</button>
<button class="btn btn-xs btn-warning">
<i class="ace-icon fa fa-flag bigger-120"></i>
</button>
</div>
<div class="hidden-md hidden-lg">
<div class="inline pos-rel">
<button class="btn btn-minier btn-primary dropdown-toggle" data-toggle="dropdown" data-position="auto">
<i class="ace-icon fa fa-cog icon-only bigger-110"></i>
</button>
<ul class="dropdown-menu dropdown-only-icon dropdown-yellow dropdown-menu-right dropdown-caret dropdown-close">
<li>
<a href="#" class="tooltip-info" data-rel="tooltip" title="View">
<span class="blue">
<i class="ace-icon fa fa-search-plus bigger-120"></i>
</span>
</a>
</li>
<li>
<a href="#" class="tooltip-success" data-rel="tooltip" title="Edit">
<span class="green">
<i class="ace-icon fa fa-pencil-square-o bigger-120"></i>
</span>
</a>
</li>
<li>
<a href="#" class="tooltip-error" data-rel="tooltip" title="Delete">
<span class="red">
<i class="ace-icon fa fa-trash-o bigger-120"></i>
</span>
</a>
</li>
</ul>
</div>
</div>
</td>
</tr>
这时复制一行,作为一个副本。先写前端script。定义一个chapters变量,使用方法list()来调用前后端交互。
<script>
export default {
name: "chapter",
data: function(){
return {
chapters: []
}
},
mounted: function() {
let _this = this;
_this.list();
},
methods: {
list() {
let _this = this;
_this.$ajax.get('http://127.0.0.1:9002/business/admin/chapter/list').then((response)=>{
console.log("查询大章列表结果: ",response);
_this.chapters = response.data;
})
}
}
}
</script>
页面展示使用v-for循环显示数据。
<tr v-for="chapter in chapters">
<td>{{chapter.id}}</td>
<td>{{chapter.name}}</td>
<td>{{chapter.courseId}}</td>
......
</tr>
这回发现显示了代码。
那么可以把假数据删除了。
4、使用gateway实现路由转发
我们希望网页访问的都是一个路径,而不是一会儿9001、一会儿9002的。所以需要使用gateway,进入配置文件,添加下面代码。
spring.cloud.gateway.routes[1].id=business
spring.cloud.gateway.routes[1].uri=http://127.0.0.1:9002
spring.cloud.gateway.routes[1].predicates[0].name=Path
spring.cloud.gateway.routes[1].predicates[0].args[0]=/business/**
这时就可以把请求的地址改成9000了。重启gatewayApplication。
list() {
let _this = this;
_this.$ajax.get('http://127.0.0.1:9000/business/admin/chapter/list').then((response)=>{
console.log("查询大章列表结果: ",response);
_this.chapters = response.data;
})
}
这时发现确实可以查到。
再思考一个问题,生产环境和测试环境下,如果每次都要修改下面的路径太麻烦了。
_this.$ajax.get('http://127.0.0.1:9000/business/admin/chapter/list')
所以采用SpringCloud的loadBalance功能:
spring.cloud.gateway.routes[1].id=business
#spring.cloud.gateway.routes[1].uri=http://127.0.0.1:9002
spring.cloud.gateway.routes[1].uri=lb://business
spring.cloud.gateway.routes[1].predicates[0].name=Path
spring.cloud.gateway.routes[1].predicates[0].args[0]=/business/**
第三节、大章分页功能开发
1、集成PageHelper(后端分页)
course的pom文件引入依赖,server下引入不带版本的依赖。
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.10</version>
</dependency>
可以测试一下:直接用PageHelper查询哪页。
PageHelper.startPage(1,1);
2、分页参数前后端交互
前后端数据交互是个问题,这里采用加一层dto的方式,加入PageDto,页码和每行数量由前端获取,总条数和总数据由后端获取到,最终形成了PageDto。为了方便每种数据都能取到,采用了泛型。
package com.course.server.dto;
import java.util.List;
public class PageDto<T> {
protected int page;
protected int size;
protected long total;
protected List<T> list;
public int getPage() {
return page;
}
public void setPage(int page) {
this.page = page;
}
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
public long getTotal() {
return total;
}
public void setTotal(long total) {
this.total = total;
}
public List<T> getList() {
return list;
}
public void setList(List<T> list) {
this.list = list;
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("PageDto{");
sb.append("page=").append(page);
sb.append(", size=").append(size);
sb.append(", total=").append(total);
sb.append(", list=").append(list);
sb.append('}');
return sb.toString();
}
}
chapterService代码:
public void list(PageDto pageDto){
PageHelper.startPage(pageDto.getPage(),pageDto.getSize());
ChapterExample chapterExample = new ChapterExample();
List<Chapter> chapterList = chapterMapper.selectByExample(chapterExample);
PageInfo<Chapter> pageInfo = new PageInfo<>(chapterList);
pageDto.setTotal(pageInfo.getTotal());
List<ChapterDto> chapterDtoList = new ArrayList<>();
for(int i = 0,l = chapterList.size();i < l;i++){
Chapter chapter = chapterList.get(i);
ChapterDto chapterDto = new ChapterDto();
BeanUtils.copyProperties(chapter,chapterDto);
chapterDtoList.add(chapterDto);
}
pageDto.setList(chapterDtoList);
}
把相关数据直接放入PageDto对象中。这样就拿到了全部的数据。
chaperMapper代码:
@RequestMapping("/list")
public PageDto list(PageDto pageDto) {
chapterService.list(pageDto);
return pageDto;
}
这时发现查出的数据有问题,前端找不到数据。是因为是data下的list。所以需要在chapter.vue文件中的list方法修改一下。
修改成下面这样:
list() {
let _this = this;
_this.$ajax.get('http://127.0.0.1:9000/business/admin/chapter/list').then((response)=>{
console.log("查询大章列表结果: ",response);
_this.chapters = response.data.list;
})
}
这样就查到了,但是还是有问题,请求传递参数一般都是post请求这里get请求不太合适,那么先测试一下简单的。
list() {
let _this = this;
_this.$ajax.post('http://127.0.0.1:9000/business/admin/chapter/list',{
page: 1,
size: 1
}).then((response)=>{
console.log("查询大章列表结果: ",response);
_this.chapters = response.data.list;
})
}
先测试传递page=1和size=1看能不能查到,发现不能。经过检查发现这样传递,controller是接收不到的。post常见的传递方式有两种,一种以表单的方式,一种是以流的方式(json),默认是流的方式,前端的数据传递给后端,但后端没接收到,说明controller这样写有点问题。其实只需要在方法参数前加上@RequestBody即可。
@RequestMapping("/list")
public PageDto list(@RequestBody PageDto pageDto) {
chapterService.list(pageDto);
return pageDto;
}
这次测试发现分页添加就生效了。
3、添加分页组件
首先添加个刷新按钮。在chapter.vue前面添加如下代码。不过注意下只能有一个父标签。点击后会执行list方法,进行刷新。
<p>
<button v-on:click="list()" class="btn btn-white btn-default btn-round">
<i class="ace-icon fa fa-refresh"></i>
刷新
</button>
</p>
添加分页组件pagination.vue。因为是组件,要放在components目录下。
<template>
<div class="pagination" role="group" aria-label="分页">
<button type="button" class="btn btn-default btn-white btn-round"
v-bind:disabled="page === 1"
v-on:click="selectPage(1)">
1
</button>
<button type="button" class="btn btn-default btn-white btn-round"
v-bind:disabled="page === 1"
v-on:click="selectPage(page - 1)">
上一页
</button>
<button v-for="p in pages" v-bind:id="'page-' + p"
type="button" class="btn btn-default btn-white btn-round"
v-bind:class="{'btn-primary active':page == p}"
v-on:click="selectPage(p)">
{{p}}
</button>
<button type="button" class="btn btn-default btn-white btn-round"
v-bind:disabled="page === pageTotal"
v-on:click="selectPage(page + 1)">
下一页
</button>
<button type="button" class="btn btn-default btn-white btn-round"
v-bind:disabled="page === pageTotal"
v-on:click="selectPage(pageTotal)">
{{pageTotal||1}}
</button>
<span class="m--padding-10">
每页
<select v-model="size">
<option value="1">1</option>
<option value="5">5</option>
<option value="10">10</option>
<option value="20">20</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
条,共【{{total}}】条
</span>
</div>
</template>
<script>
export default {
name: 'pagination',
props: {
list: {
type: Function,
default: null
},
itemCount: Number
},
data: function () {
return {
total: 0,
size: 10,
page: 0,
pageTotal: 0,
pages: [],
}
},
methods: {
render(page, total) {
let _this = this;
_this.page = page;
_this.total = total;
_this.pageTotal = Math.ceil(total / _this.size);
_this.pages = _this.getPageItems(_this.pageTotal, page, _this.itemCount || 5);
},
selectPage(page) {
let _this = this;
if (page < 1) {
page = 1;
}
if (page > _this.pageTotal) {
page = _this.pageTotal;
}
if (this.page !== page) {
_this.page = page;
if (_this.list) {
_this.list(page);
}
}
},
getPageItems(total, current, length) {
let items = [];
if (length >= total) {
for (let i = 1; i <= total; i++) {
items.push(i);
}
} else {
let base = 0;
if (current - 0 > Math.floor((length - 1) / 2)) {
base = Math.min(total, current - 0 + Math.ceil((length - 1) / 2)) - length;
}
for (let i = 1; i <= length; i++) {
items.push(base + i);
}
}
return items;
}
}
}
</script>
<style scoped>
.pagination {
vertical-align: middle !important;
font-size: 16px;
margin-top: 0;
margin-bottom: 10px;
}
.pagination button {
margin-right: 5px;
}
.btn-primary.active {
background-color: #2f7bba !important;
border-color: #27689d !important;
color: white !important;
font-weight: 600;
}
</style>
在chapter中表格上,刷新按钮下放至这个分页的组件。
然后在chapter.vue中要引入pagination.vue组件。然后使用它。
<script>
import Pagination from "../../components/pagination";
export default {
components: {Pagination},
}
这里首先要注意,添加分页组件以后,list就不是查询所有了,肯定是按页查询,那么list()中肯定要加上页码参数。
mounted: function() {
let _this = this;
_this.$refs.pagination.size = 5;
_this.list(1);
},
methods: {
list(page) {
let _this = this;
_this.$ajax.post('http://127.0.0.1:9000/business/admin/chapter/list', {
page: page,
size: _this.$refs.pagination.size,
}).then((response)=>{
console.log("查询大章列表结果:", response);
_this.chapters = response.data.list;
_this.$refs.pagination.render(page, response.data.total);
})
}
}
这样就完成了分页组件的开发。
第四节、大章新增功能
1、使用模态框
使用BootStrap模态框。加入如下代码:
<div class="modal fade" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span> </button>
<h4 class="modal-title">表单</h4>
</div>
<div class="modal-body">
<form class="form-horizontal">
<div class="form-group">
<label class="col-sm-2 control-label">名称</label>
<div class="col-sm-10">
<input class="form-control" placeholder="名称">
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">课程ID</label>
<div class="col-sm-10">
<input class="form-control" placeholder="课程ID">
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary">保存</button>
</div>
</div>
</div>
</div>
然后复制上一节chapter.vue中的刷新按钮,把它改成新增。
<p>
<button v-on:click="add()" class="btn btn-white btn-default btn-round">
<i class="ace-icon fa fa-edit"></i>
新增
</button>
<button v-on:click="list()" class="btn btn-white btn-default btn-round">
<i class="ace-icon fa fa-refresh"></i>
刷新
</button>
</p>
这时就需要新增一个add()方法。添加时希望弹出模态框。
add() {
let _this = this;
$(".modal").modal("show");
},
但是这时又弹出一个错误,说_this错误。这还是上面说的那种自动检查错误。
还是在.eslintrc.js中修改,在rules下新增一个检验关闭:
'no-unused-vars': 'off'
代码如下:
module.exports = {
root: true,
env: {
node: true
},
'extends': [
'plugin:vue/essential',
'eslint:recommended'
],
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-undef': 'off',
'vue/no-unused-vars': 'off',
'vue/require-v-for-key': 'off',
'no-unused-vars': 'off'
},
parserOptions: {
parser: 'babel-eslint'
}
};
点击新增弹出模态框。
2、数据库id的选用
数据库id最好选择Uuid,不要选用自增id,因为自增id容易破解。
UuidUtil的代码如下:
import java.util.UUID;
public class UuidUtil {
public static String[] chars = new String[] { "a", "b", "c", "d", "e", "f",
"g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s",
"t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5",
"6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I",
"J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V",
"W", "X", "Y", "Z" };
public static String getShortUuid() {
StringBuffer shortBuffer = new StringBuffer();
String uuid = UuidUtil.getUuid();
for (int i = 0; i < 8; i++) {
String str = uuid.substring(i * 4, i * 4 + 4);
int x = Integer.parseInt(str, 16);
shortBuffer.append(chars[x % 0x3E]);
}
return shortBuffer.toString();
}
public static String getUuid(){
String uuid = UUID.randomUUID().toString();
return uuid.replaceAll("-", "");
}
public static void main(String[] args) {
System.out.println(getShortUuid());
}
}
那么先在controller和service层添加save方法,这里需要使用uuid。
service层:
public void save(ChapterDto chapterDto){
Chapter chapter = new Chapter();
chapterDto.setId(UuidUtil.getShortUuid());
BeanUtils.copyProperties(chapterDto,chapter);
chapterMapper.insert(chapter);
}
controller层:
@RequestMapping("/save")
public ChapterDto save(@RequestBody ChapterDto chapterDto) {
LOG.info("chapterDto:{}",chapterDto);
chapterService.save(chapterDto);
return chapterDto;
}
然后需要给前端设置save()方法。data中设置一个参数chapter,然后把这个chapter保存到后端。
data: function(){
return {
chapter: {},
chapters: []
},
save() {
let _this = this;
_this.$ajax.post('http://127.0.0.1:9000/business/admin/chapter/save', this.chapter).then((response)=>{
console.log("保存大章列表结果:", response);
})
}
在chapter.vue中还要给div设置响应的对应参数,以保证可以和后端保存。
<div class="modal-body">
<form class="form-horizontal">
<div class="form-group">
<label class="col-sm-2 control-label">名称</label>
<div class="col-sm-10">
<input v-model="chapter.name" class="form-control" placeholder="名称">
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">课程ID</label>
<div class="col-sm-10">
<input v-model="chapter.courseId" class="form-control" placeholder="课程ID">
</div>
</div>
</form>
</div>
还需要在保存按钮位置加上点击保存事件:
<button v-on:click="save" type="button" class="btn btn-primary">保存</button>
3、CopyUtil的使用
我们之前用了大量的代码来写两个类对象的复制,那么不如写一个通用的CopyUtil类,以简化后续的代码:
package com.course.server.util;
import org.springframework.beans.BeanUtils;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
public class CopyUtil {
public static <T> List<T> copyList(List source, Class<T> clazz) {
List<T> target = new ArrayList<>();
if (!CollectionUtils.isEmpty(source)){
if (!CollectionUtils.isEmpty(source)){
for (Object c: source) {
T obj = copy(c, clazz);
target.add(obj);
}
}
}
return target;
}
public static <T> T copy(Object source, Class<T> clazz) {
if (source == null) {
return null;
}
T obj = null;
try {
obj = clazz.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
BeanUtils.copyProperties(source, obj);
return obj;
}
}
service层使用CopyUtil代码:
public void list(PageDto pageDto){
PageHelper.startPage(pageDto.getPage(),pageDto.getSize());
ChapterExample chapterExample = new ChapterExample();
List<Chapter> chapterList = chapterMapper.selectByExample(chapterExample);
PageInfo<Chapter> pageInfo = new PageInfo<>(chapterList);
pageDto.setTotal(pageInfo.getTotal());
//List<ChapterDto> chapterDtoList = new ArrayList<>();
//for(int i = 0,l = chapterList.size();i < l;i++){
//Chapter chapter = chapterList.get(i);
//ChapterDto chapterDto = new ChapterDto();
//BeanUtils.copyProperties(chapter,chapterDto);
//chapterDtoList.add(chapterDto);
// }
List<ChapterDto> chapterDtoList = CopyUtil.copyList(chapterList, ChapterDto.class);
pageDto.setList(chapterDtoList);
}
public void save(ChapterDto chapterDto){
//Chapter chapter = new Chapter();
chapterDto.setId(UuidUtil.getShortUuid());
//BeanUtils.copyProperties(chapterDto,chapter);
Chapter chapter = CopyUtil.copy(chapterDto, Chapter.class);
chapterMapper.insert(chapter);
}
4、增加统一返回的ResponseDto
看上面这两个方法一个list一个save,他们的返回参数不相同,看着很别扭,而且前后端应用最好返回一样的参数。那么创建一个ResponseDto类来接收所有类型的对象。
ResponseDto类:
package com.course.server.dto;
public class ResponseDto<T> {
private boolean success = true;
private String code;
private String message;
private T content;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public boolean getSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getContent() {
return content;
}
public void setContent(T content) {
this.content = content;
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("ResponseDto{");
sb.append("success=").append(success);
sb.append(", code='").append(code).append('\'');
sb.append(", message='").append(message).append('\'');
sb.append(", content=").append(content);
sb.append('}');
return sb.toString();
}
}
修改controller类。
@RequestMapping("/list")
public ResponseDto list(@RequestBody PageDto pageDto) {
ResponseDto responseDto = new ResponseDto();
chapterService.list(pageDto);
responseDto.setContent(pageDto);
return responseDto;
}
@RequestMapping("/save")
public ResponseDto save(@RequestBody ChapterDto chapterDto) {
LOG.info("chapterDto:{}",chapterDto);
ResponseDto responseDto = new ResponseDto();
chapterService.save(chapterDto);
responseDto.setContent(chapterDto);
return responseDto;
}
前端注意修改:
list(page) {
let _this = this;
_this.$ajax.post('http://127.0.0.1:9000/business/admin/chapter/list', {
page: page,
size: _this.$refs.pagination.size,
}).then((response)=>{
console.log("查询大章列表结果:", response);
let resp = response.data;
_this.chapters = resp.content.list;
_this.$refs.pagination.render(page, resp.content.total);
})
},
response.data现在不能查到结果了,其中还有一层content,那么就加上这层。
save方法有可能成功也有可能不成功,先判断。如果成功了关闭模态框,并且从第一页刷新list()。
save() {
let _this = this;
_this.$ajax.post('http://127.0.0.1:9000/business/admin/chapter/save', this.chapter).then((response)=>{
console.log("保存大章列表结果:", response);
if (resp.success){
$(".modal").modal("hide");
_this.list(1);
}
})
}
这样就可以新增了。
修改大章功能也可以使用save方法,但是修改是包含id字段值的。
public void save(ChapterDto chapterDto) {
Chapter chapter = CopyUtil.copy(chapterDto, Chapter.class);
if (StringUtils.isEmpty(chapterDto.getId())) {
this.insert(chapter);
} else {
this.update(chapter);
}
}
private void insert(Chapter chapter) {
chapter.setId(UuidUtil.getShortUuid());
chapterMapper.insert(chapter);
}
private void update(Chapter chapter) {
chapterMapper.updateByPrimaryKey(chapter);
}
第五节、大章删除功能
1、删除代码
先在删除按键位置写个删除点击事件。
del(id) {
let _this = this;
_this.$ajax.delete('http://127.0.0.1:9000/business/admin/chapter/delete/' + id).then((response)=>{
console.log("删除大章列表结果:", response);
let resp = response.data;
if (resp.success) {
_this.list(1);
}
})
}
controller和service层。
@RequestMapping("/delete/{id}")
public ResponseDto delete(@PathVariable String id) {
LOG.info("id:{}",id);
ResponseDto responseDto = new ResponseDto();
chapterService.delete(id);
return responseDto;
}
public void delete(String id) {
chapterMapper.deleteByPrimaryKey(id);
}
确实删除了。
2、引入sweetalert和消息提示框
如果没有提示框,删除没有任何提示,是不是有误删我们也不知道。
那么先在index.html中引入sweetalert所需要的js文件。
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@9"></script>
在del方法中引入sweetalert提示消息:
del(id) {
let _this = this;
Swal.fire({
title: '确认删除?',
text: "删除后不可恢复,确认删除?",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: '确认!'
}).then((result) => {
if (result.value) {
_this.$ajax.delete('http://127.0.0.1:9000/business/admin/chapter/delete/' + id).then((response)=>{
console.log("删除大章列表结果:", response);
let resp = response.data;
if (resp.success) {
_this.list(1);
Swal.fire(
'删除成功!',
'删除成功!',
'success'
)
}
})
}
})
}
删除提示和成功提示见上面。
这个消息提示框需要点击ok,这个提示框才能关闭,我们需要一个可以自动关闭的。
需要引入js到index.html中。
<script src="<%= BASE_URL %>static/js/toast.js"></script>
而且为了方便,避免过程中大量重复的代码,在public/static/js下创建一个toast.js当作通用组件。
const Toast = Swal.mixin({
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 3000,
timerProgressBar: true,
onOpen: (toast) => {
toast.addEventListener('mouseenter', Swal.stopTimer)
toast.addEventListener('mouseleave', Swal.resumeTimer)
}
});
toast = {
success: function (message) {
Toast.fire({
icon: 'success',
title: message
});
},
error: function (message) {
Toast.fire({
icon: 'error',
title: message
});
},
warning: function (message) {
Toast.fire({
icon: 'warning',
title: message
});
}
};
这时在执行完方法后启动这个代码即可。如:
save(page) {
let _this = this;
_this.$ajax.post('http://127.0.0.1:9000/business/admin/chapter/save', _this.chapter).then((response)=>{
console.log("保存大章列表结果:", response);
let resp = response.data;
if (resp.success) {
$("#form-modal").modal("hide");
_this.list(1);
toast.success("保存成功!");
}
})
},
del(id) {
let _this = this;
Swal.fire({
title: '确认删除?',
text: "删除后不可恢复,确认删除?",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: '确认!'
}).then((result) => {
if (result.value) {
_this.$ajax.delete('http://127.0.0.1:9000/business/admin/chapter/delete/' + id).then((response)=>{
console.log("删除大章列表结果:", response);
let resp = response.data;
if (resp.success) {
_this.list(1);
toast.success("删除成功!");
}
})
}
})
}
这里确实提示了保存成功的消息提示框。
3、等待框的使用
等待框会让页面看上去更舒服,尤其是保存删除等操作,有这个等待框可以更好的了解什么时候代码结束了。
先引入js。
<script src="https://cdn.bootcss.com/jquery.blockUI/2.70.0-2014.11.23/jquery.blockUI.min.js"></script>
<script src="<%= BASE_URL %>static/js/loading.js"></script>
增加个自定义的loading.js,保存在static/js目录下。
Loading = {
show: function () {
$.blockUI({
message: '<img src="/static/image/loading.gif" />',
css: {
padding: "10px",
left: "50%",
width: "80px",
marginLeft: "-40px",
}
});
},
hide: function () {
setTimeout(function () {
$.unblockUI();
}, 500)
}
};
在保存和删除前端代码最关键的代码前后添加开启和隐藏等待框的代码。
list(page) {
let _this = this;
Loading.show();
_this.$ajax.post('http://127.0.0.1:9000/business/admin/chapter/list', {
page: page,
size: _this.$refs.pagination.size,
}).then((response)=>{
Loading.hide();
console.log("查询大章列表结果:", response);
let resp = response.data;
_this.chapters = resp.content.list;
_this.$refs.pagination.render(page, resp.content.total);
})
},
save(page) {
let _this = this;
Loading.show();
_this.$ajax.post('http://127.0.0.1:9000/business/admin/chapter/save', _this.chapter).then((response)=>{
Loading.hide();
console.log("保存大章列表结果:", response);
let resp = response.data;
if (resp.success) {
$("#form-modal").modal("hide");
_this.list(1);
toast.success("保存成功!");
}
})
},
del(id) {
let _this = this;
Swal.fire({
title: '确认删除?',
text: "删除后不可恢复,确认删除?",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: '确认!'
}).then((result) => {
if (result.value) {
Loading.show();
_this.$ajax.delete('http://127.0.0.1:9000/business/admin/chapter/delete/' + id).then((response)=>{
Loading.hide();
console.log("删除大章列表结果:", response);
let resp = response.data;
if (resp.success) {
_this.list(1);
toast.success("删除成功!");
}
})
}
})
}
}
换一个消息提示框,之前的太小了。引入confirm.js
<script src="<%= BASE_URL %>static/js/confirm.js"></script>
Confirm = {
show: function (message, callback) {
Swal.fire({
title: '确认?',
text: message,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: '确认!'
}).then((result) => {
if (result.value) {
if (callback) {
callback()
}
}
})
}
}
修改toast.js。
Toast = {
success: function (message) {
Swal.fire({
position: 'top-end',
icon: 'success',
title: message,
showConfirmButton: false,
timer: 3000
})
},
error: function (message) {
Swal.fire({
position: 'top-end',
icon: 'error',
title: message,
showConfirmButton: false,
timer: 3000
})
},
warning: function (message) {
Swal.fire({
position: 'top-end',
icon: 'warning',
title: message,
showConfirmButton: false,
timer: 3000
})
}
};
4、校验代码
前端代码最好还是校验一下,因为传入为空或者本身值不符合格式最好是不要保存到数据库的。
这里先写两个js,一个tool.js,一个validator.js。都存在了static/js/下了。
Tool = {
isEmpty: function (obj) {
if ((typeof obj == 'string')) {
return !obj || obj.replace(/\s+/g, "") == ""
} else {
return (!obj || JSON.stringify(obj) === "{}" || obj.length === 0);
}
},
isNotEmpty: function (obj) {
return !this.isEmpty();
},
isLength: function (str, min, max) {
return $.trim(str).length >= min && $.trim(str).length <= max;
}
};
Validator = {
require: function (value, text) {
if (Tool.isEmpty(value)) {
Toast.warning(text + "不能为空");
return false;
} else {
return true
}
},
length: function (value, text, min, max) {
if (!Tool.isLength(value, min, max)) {
Toast.warning(text + "长度" + min + "~" + max + "位");
return false;
} else {
return true
}
}
};
然后引入到index.html。
<script src="<%= BASE_URL %>static/js/tool.js"></script>
<script src="<%= BASE_URL %>static/js/validator.js"></script>
chapter.vue只需要在方法前写上这段代码即可。
if (!Validator.require(_this.chapter.name, "名称")
|| !Validator.require(_this.chapter.courseId, "课程ID")
|| !Validator.length(_this.chapter.courseId, "课程ID", 1, 8)) {
return;
}
然后开始后端校验。
后端需要创建一个ValidatorUtil工具类。
package com.course.server.util;
import com.course.server.exception.ValidatorException;
import org.springframework.util.StringUtils;
public class ValidatorUtil {
public static void require(String str, String fieldName) {
if (StringUtils.isEmpty(str)) {
throw new ValidatorException(fieldName + "不能为空");
}
}
public static void length(String str, String fieldName, int min, int max) {
int length = 0;
if (!StringUtils.isEmpty(str)) {
length = str.length();
}
if (length < min || length > max) {
throw new ValidatorException(fieldName + "长度" + min + "~" + max + "位");
}
}
}
如果执行有问题可以抛出异常,那么自定义一个异常类。
package com.course.server.exception;
public class ValidatorException extends RuntimeException{
public ValidatorException(String message) {
super(message);
}
}
controller的save方法添加校验。
ValidatorUtil.require(chapterDto.getName(), "名称");
ValidatorUtil.require(chapterDto.getCourseId(), "课程ID");
ValidatorUtil.length(chapterDto.getCourseId(), "课程ID", 1, 8);
还需要一个处理异常的Handler。可以省的每次都要写大量代码,直接Handler看到抛异常就处理就行。
package com.course.business.controller;
import com.course.server.dto.ResponseDto;
import com.course.server.exception.ValidatorException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@ControllerAdvice
public class ControllerExceptionHandler {
private static final Logger LOG = LoggerFactory.getLogger(ControllerExceptionHandler.class);
@ExceptionHandler(value = ValidatorException.class)
@ResponseBody
public ResponseDto validatorExceptionHandler(ValidatorException e) {
ResponseDto responseDto = new ResponseDto();
responseDto.setSuccess(false);
LOG.warn(e.getMessage());
responseDto.setMessage("请求参数异常!");
return responseDto;
}
}
前端如果有问题也可以在控制台提示。
} else {
Toast.warning(resp.message)
5、优化功能
当前控制台还有网页日志输出不太好,优化日志输出可以更好的开发。
父项目pom引入依赖。
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.60</version>
</dependency>
server下引入依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
server目录下创建LogAspect类。然后所有controller都作为切点使用当前这个方法。
package com.course.server.config;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.support.spring.PropertyPreFilters;
import com.course.server.util.UuidUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Field;
@Aspect
@Component
public class LogAspect {
private final static Logger LOG = LoggerFactory.getLogger(LogAspect.class);
@Pointcut("execution(public * com.course.*.controller..*Controller.*(..))")
public void controllerPointcut() {}
@Before("controllerPointcut()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
MDC.put("UUID", UuidUtil.getShortUuid());
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
Signature signature = joinPoint.getSignature();
String name = signature.getName();
String nameCn = "";
if (name.contains("list") || name.contains("query")) {
nameCn = "查询";
} else if (name.contains("save")) {
nameCn = "保存";
} else if (name.contains("delete")) {
nameCn = "删除";
} else {
nameCn = "操作";
}
Class clazz = signature.getDeclaringType();
Field field;
String businessName = "";
try {
field = clazz.getField("BUSINESS_NAME");
if (!StringUtils.isEmpty(field)) {
businessName = (String) field.get(clazz);
}
} catch (NoSuchFieldException e) {
LOG.error("未获取到业务名称");
} catch (SecurityException e) {
LOG.error("获取业务名称失败", e);
}
LOG.info("------------- 【{}】{}开始 -------------", businessName, nameCn);
LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
LOG.info("类名方法: {}.{}", signature.getDeclaringTypeName(), name);
LOG.info("远程地址: {}", request.getRemoteAddr());
Object[] args = joinPoint.getArgs();
Object[] arguments = new Object[args.length];
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof ServletRequest
|| args[i] instanceof ServletResponse
|| args[i] instanceof MultipartFile) {
continue;
}
arguments[i] = args[i];
}
String[] excludeProperties = {};
PropertyPreFilters filters = new PropertyPreFilters();
PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
excludefilter.addExcludes(excludeProperties);
LOG.info("请求参数: {}", JSONObject.toJSONString(arguments, excludefilter));
}
@Around("controllerPointcut()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = proceedingJoinPoint.proceed();
String[] excludeProperties = {"password"};
PropertyPreFilters filters = new PropertyPreFilters();
PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
excludefilter.addExcludes(excludeProperties);
LOG.info("返回结果: {}", JSONObject.toJSONString(result, excludefilter));
LOG.info("------------- 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
return result;
}
}
前端日志输出。
axios.interceptors.request.use(function (config) {
console.log("请求:", config);
return config;
}, error => {});
axios.interceptors.response.use(function (response) {
console.log("返回结果:", response);
return response;
}, error => {});
|