前端页面的设计
前端设计使用ElementUI进行编写,
在这里整体的项目就简单的实现,前端代码也就不在此展示了,基本功能也就是实现一套增删改查等系列功能。看一下效果图:
在这里简单的对前端的设计做一下解释
数据库的设计(MySql)
建一个springboot的数据库,在数据库当中新建一个info信息表。并且插入一条数据用于测试
CREATE TABLE `info` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` char(5) NOT NULL,
`birthday` char(10) NOT NULL,
`sex` char(1) NOT NULL,
`address` char(20) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
后端项目的搭建
1.项目搭建,使用spring快速初始化进行构建一个spring的项目。
下一步,进行设置项目名、包名等
进行添加需要使用到的其余依赖
依赖添加完成之后,选择项目的地址,直接finish即可
项目配置:首先打开pom.xml文件,添加mysql和druid依赖
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.0.20version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.19version>
<scope>compilescope>
dependency>
在resource下的application.properties文件当中添加配置,配置内容如下:
server.port=8989
spring.mvc.static-path-pattern=/**
server.servlet.context-path=/
spring.applicantion.name=elementuser
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/springboot?characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=root
mybatis.mapper-locations=classpath:com/lzq/mapper/*.xml
mybatis.type-aliases-package=com.lzq.entity
之后就是编写所对应的java文件了,在这里包名统一使用的是com.lzq
+不同的包名,进行命名。首先就是实体类User,放在com.lzq.entity
包下面
package com.lzq.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.Accessors;
@Data
@AllArgsConstructor //有参构造
@NoArgsConstructor //无参构造
@ToString //tostring方法
@Accessors(chain = true)
public class User {
private int id;
private String name;
@JsonFormat(pattern = "yyyy-MM-dd") //时间格式化
private String birthday;
private String sex;
private String address;
}
之后添加一个dao接口,放在com.lzq.dao
包下,添加一个返回所有数据的一个方法(接口)。
package com.lzq.dao;
import com.lzq.entity.User;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface UserDao {
List<User> findAll();
}
服务类,放在com.lzq.Service
下,使用一个服务接口定义一个方法,
package com.lzq.Service;
import com.lzq.entity.User;
import java.util.List;
public interface UserService {
List<User> findAll();
}
接口定义好之后使用一个类UserServiceImpl继承接口实现方法,
package com.lzq.Service;
import com.lzq.dao.UserDao;
import com.lzq.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userdao;
@Override
public List<User> findAll() {
return userdao.findAll();
}
}
之后我们添加一个controller进行配置接口即可,
package com.lzq.controller;
import com.lzq.Service.UserService;
import com.lzq.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@CrossOrigin //前端接口为8080,后端接口为8989,这个参数使用可进行跨域访问
@RequestMapping("/user") //类路由
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/findAll") //设置请求方式和路由地址
public List<User> findAll(){
return userService.findAll();
}
}
到这里java代码就编写完了,之后我们只需要添加mapper.xml配置文件,配置好查询语句即可,在java同级目录下rescource下,新建一个包com.lzq.mapper
添加一个UserDaoMapper.xml配置文件
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.lzq.dao.UserDao">
<select id="findAll" resultType="com.lzq.entity.User">
select * from info
</select>
</mapper>
到这里我们就完成了后端代码的编写,整体的项目路径如下所示,之后我们只需要运行这个springboot项目即可,在java目录下,有一个ElementuserApplication文件,右键run即可。
在项目跑起来之后我们需要对这个路由进行测试(也就是controller当中定义的路由),可以使用postman对接口进行测试,如下图所示,
或者直接在浏览器当中访问路由地址即可。http://localhost:8989/user/findAll
到这里就实现了一个接口的开发。接下来就是通过前端获取后端的数据了
通过前端请求获取后端数据
1.查询功能的开发
在前面的前端当中写到安装axios库,用于发送请求。axios库的安装:直接在命令行当中进行安装,键入命令:npm install axios
安装完成之后需要使用这个axios网络库,在main.js文件当中进行使用,使用以下命令。
import axios from 'axios'
Vue.config.productionTip = false
Vue.prototype.$http = axios
到这里就安装好了axios库,之后进行使用即可,使用axios发送一个get请求,获取数据,这里使用tableData进行获取返回数据,将返回的数据渲染到前端的一个表格当中即可。
this.$http.get("http://localhost:8989/user/findAll").then(res => {
this.tableData = res.data;
})
2.添加功能的开发
在前端页面添加一个添加按钮,按钮触发后弹出一个dialog框,用于录入信息,输入完成之后进行提交到后台数据库。先对后台进行开发。在前面的代码段的基础上,先在mapper文件当中添加一个sql语句。
<insert id="save" parameterType="com.lzq.entity.User" useGeneratedKeys="true" keyProperty="id">
insert into info values (#{id},#{name},#{birthday},#{sex},#{address})
</insert>
在这里的id对应dao层的代码,在dao和service当中添加一个save方法。dao对应mapper.xml文件,
int save(User user);
再在service的实现类当中调用dao进行方法实现:
@Override
public int save(User user) {
return userdao.save(user);
}
添加的代码编写完成之后,在控制器controller当中添加一个接口用于测试。使用post请求发送数据,数据是一个对象所以要使用@RequestBody
@PostMapping("/save")
public int save(@RequestBody User user){
return userService.save(user);
}
运行项目使用Postman进行测试:返回值为1表示插入成功,也就是这个接口是没有问题的。
前端代码实现,这里使用到一个elementui组件的一个from表单,表单验证的这个组件,(不做过多解释,单击前往) 之后在点击添加提交之后,返送一条post请求,在最后面添加完成之后需要对这个表单的值置为空,并且重新查询数据进行显示。当然这个dialog弹出框当然也要隐藏,置为false即可
3.删除功能的开发
在数据进行显示的表格也是使用elementUI当中的一个组件,自定义列模板,在后面包含了编辑和删除操作的按钮,(不做过多解释),这样也就是说,我们不需要自己去定义其他的按钮了,前端页面不需要做大规模的重写,并且还绑定了编辑和删除触发的方法,方法的输出值就是当前行这个对象,因此我们可以获取到删除这一行的id值,通过id值进行删除数据。
前端简单的说完了,就是后端接口的开发了,和添加方法同理,现在xml文件当中定义好删除语句.
<delete id="remove" parameterType="Integer">
delete from info where id = #{id}
delete>
在dao和service当中添加删除的方法
int remove(Integer id);
在service接口的实现类对方法进行实现:
@Override
public int remove(Integer id) {
return userdao.remove(id);
}
最后在控制器类当中实现一个接口,在这里传入的是一个Integer类型的id值,使用@RequestParams即可,
@GetMapping("/remove")
public int delete(@RequestParam Integer id){
return userService.remove(id);
}
在postman当中对接口测试,在这里发get请求,并且添加一个参数,也就是通常的使用?进行添加。
接口是没有错的,所以在这里就可以在前端发送请求了,在前面有说道,他自己给绑定了一个方法,并且这个方法是可以直接获取到id的,所以我们直接在这个方法当中发送请求就可以实现删除了。
在这里是直接按下删除这个按钮之后,数据就被直接删除了,这样就存在一个问题,无法避免误触,所以在这里我们可以添加一个组件,气泡提示框:单击前往,避免误触,将原先帮顶给删除的单击事件,重新绑定给气泡提示框的确定删除按钮。
<el-popconfirm confirm-button-text='好的' cancel-button-text='不用了' icon="el-icon-info"
icon-color="red" title="确定删除?" @confirm="handleDelete(scope.$index, scope.row)">
<el-button slot="reference" size="mini" type="danger">删除</el-button>
</el-popconfirm>
4.编辑功能的开发
编辑功能,其实也就是对数据进行更新,大致和删除同理,在组件上本就绑定了一个编辑按钮,所以我们不需要做过多的改变,1. 添加查询语句
<update id="update" parameterType="com.lzq.entity.User">
update info set name = #{name} ,birthday = #{birthday} ,sex=#{sex},
address = #{address} where id =#{id}
update>
在dao和service当中添加更新的方法
int update(User user);
在service接口的实现类对方法进行实现:
@Override
public int update(User user) {
return userdao.update(user);
}
在添加一个接口,与添加方法同理
@PostMapping("/update")
public int update (@RequestBody User user){
return userService.update(user);
}
在这里进行更新数据的时候id是一定有的,而在添加的时候是不需要提供id的,并且在进行修改的时候,嗯同样使用的是添加数据的那个dialog框,所以在这里我们的接口路由可以使用一个,在里面进行判断id是否存在,存在则执行update,不存在则执行save。(使用前面save的这个方法)
@PostMapping("/save")
public int save(@RequestBody User user){
if(user.getId()==0){
return userService.save(user);
}else {
return userService.update(user);
}
}
前端请求不需要改变,只需要在点击编辑按钮的时候,获取到这个行对象给到ruleFrom即可。
到这里增删改查的基本方法就实现了,也没多少,好好理解前面写的。
5.分页功能的开发
分页功能的开发,我们使用到Mybatis提供的一个组件PageHelper,在这里做简单的解释和使用。在github上可以查看这个组件:前往github,在里面的readme.md文件就是简介和使用教程
1、 安装分页组件PageHelper组件,在pom文件当中添加依赖即可
<dependency>
<groupId>com.github.pagehelpergroupId>
<artifactId>pagehelper-spring-boot-starterartifactId>
<version>1.2.3version>
dependency>
2、在springboot当中的application.properties配置文件添加关于PageHelper组件的配置
pagehelper.helper-dialect=mysql
pagehelper.reasonable=true
pagehelper.support-methods-arguments=true
3、进行使用,在service层当中添加一个方法,pageInfo是一个组件提供的类进行导入即可。如下:在这里只设置当前的页,每一页我这是给了一个固定值5,也可以根据前端传过来的值进行设置。
import com.github.pagehelper.PageInfo;
...
PageInfo<User> findAllUserByPageS(int pageNum);
之后就是在实现类当中进行实现:这里如果使用前端传过来的值,只需要接收一个Integer类型的pageSize代替这个5即可。
@Override
public PageInfo<User> findAllUserByPageS(int pageNum) {
PageHelper.startPage(pageNum, 5);
List<User> lists = userdao.findAll();
PageInfo<User> pageInfo = new PageInfo<User>(lists);
return pageInfo;
}
之后在控制器类当中添加一个接口:
@GetMapping("/PageHelper")
public PageInfo<User> testPageHelper(@RequestParam Integer pageNum) {
PageInfo<User> queryResult = userService.findAllUserByPageS(pageNum);
return queryResult;
}
接口也有了,对这个接口进行测试,如下:可以看到有返回数据,不单单是这一页的user对象,还包括了其余页码有关信息。
在前端,使用分页组件进行实现分页:分页组件,这里使用带背景颜色的分页,如下所示:
<el-pagination background layout="prev, pager, next" :total="total" class="page" :page-size="5"
@current-change="pageChange">
</el-pagination>
绑定的方法:这个val的值就是当前页,把这个页给到分页请求。
pageChange(val) {
this.page = val
this.show()
},
所以在原本的查询所有findAll就需要修改一下,修改为这个分页的请求。如下所示:
到这里分页的功能也就实现了。整个项目实战差不多也就告一段落了,
最后如果需要这个小的项目也可以私信我,也可以用来学习一下。
其余开发问题总结
1、对表格的某一列数据统计和显示问题
在使用表格组件的时候,通常会对当前的这一个表格的数据进行统计,在elementui里面的表格组件当中,存在一个表尾合计行Demo,可以进行参考,使用其: show-summary
属性就可以对表格进行统计了(这里会对所有int数字进行累加统计),但是我们可以添加一个方法summary-method="getSummaries"
指定统计的方法(显示列)如下所示:
getSummaries(param) {
const {
columns,
data
} = param;
const sums = [];
columns.forEach((column, index) => {
if (index === 0) {
sums[index] = '总价';
return;
}
const values = data.map(item => Number(item[column.property]));
sums[index] = values.reduce((prev, curr) => {
const value = Number(curr);
if (!isNaN(value)) {
return prev + curr;
}
});
//在这里可以判断 column.property 这个的值是否是需要进行统计的那一列,
//如果是则进行累加统计,反之将置为空
//也就是只统计这一列amountOfMoney的值。
column.property == "amountOfMoney" ? (sums[index] += '元') : sums[index] = '';
});
return sums;
},
在这里,如果你代码块已经是叠加好的,不难发现,在运行代码的时候是看不到总计这最后一行的效果的。但是当你打开F12进行调试的时候,ctrl+F 查找这个总价时,又是会显示出来,这是因为原本的高度是将这个进行下移了,所以在最开始是看不到的。
如何解决这个问题呢?我们只需要在Vue的生命周期的Updated当中进行设置高度即可。
updated() {
this.$nextTick(() => {
this.$refs['table'].doLayout();
})
},
2、前端参数传递的问题
在前端传递一个查询条件给后台进行查询,通常都是会以表的某一列作为查询条件进行传递,这里我们在后台可以直接使用@RequestBody 实体类 实体类对象
或者 @RequestParam 包装数据类型 变量名
进行接收,接收到这个数据之后进行处理即可。但是在这里如果传入的是一个时间段呢,在实体类当中只有一个变量对应,这个时候如何处理呢?
在这里可以使用@RequestBody HashMap 对象名
进行接收,接收到之后使用HashMap的get方法获取到这其中的值,并且我们还需要判断是否为空这个判断,保证逻辑的正常,避免出现错误,如下图所示:
3、关于List集合处理的问题
在进行查询之后会有多条数据,当现在存在一个需求,需要对这多条数据的name相同的进行合并,并且其金额进行累加合并,也就是一个人的多条数据合并为一条数据。
在进行数据合并我们使用双重for循环进行判断,name是否相同,相同就把后面的一条List数据进行remove删除操作。
在list集合当中是不支持对集合当中的某一个元素的值进行修改。那如何进行累加合并呢?
如下代码所示:可能有些多了,简单的说一下,我们需要将需要修改的这一条list集合使用一个实体类的对象进行保存,对这个实体类对象进行修改,修改某个属性,使用实体类的setter方法即可,在对实体类操作完成之后,使用集合的set方法将实体类的这条数据插入到原先的List集合当中即可
//得到List集合,对集合进行操作
for (int i = 0; i < list.size() - 1; i++) {
for (int j = list.size() - 1; j > i; j--) {
if (list.get(i).getOrderingPerson().equals(list.get(j).getOrderingPerson())) {
int sum = list.get(i).getAmountOfMoney() + list.get(j).getAmountOfMoney();
OvertimemealTaxiregistration over = list.get(i);
over.setAmountOfMoney(sum);
list.set(i, over);
list.remove(j);
}
}
}
4、关于表单验证的问题
在前端进行添加的时候不免的会使用到表单验证,在这里简单的说一下表单验证规则。
required: true,//是否必填
message: '请输入',//错误提示信息
trigger: 'blur'//检验方式(blur为鼠标点击其他地方,)
type: 'email',//要检验的类型(number,email,date等)
pattern: /^1[34578]\d{
9}$/,//可以写正则表达式
在这里遇到的一个问题是,在进行验证是否是数字的时候,即使输入的是一个数字也会出现验证不通过,可能是在绑定的时候数字被转换成了字符串,所以在这里需要对v-model
进行设置,在Vue文档当中对v-model
的修饰符提出了数据类型的限制,如下图所示:Vue文档地址,单击前往
所以在绑定的时候加上这个number修饰符即可。v-model.number="ruleForm.amountOfMoney"
第二点就是:在进行表单验证的时候,有些选项的验证条件比较多,就会导致这个.vue文件过于冗长,为了保持代码的简洁性和高复用性,我们可以将其进行抽离出来,在这里抽离出一个是否为数字的验证为例,
在js文件当中进行编写代码,如下:
//rule 规则 value 传递的参数,输入的参数 callback 回调函数
export const isNumber = (rule, value, callback) => {
if (isNaN(value)) {
callback(new Error('这不是一个数字'))
} else {
callback()
}
}
之后我们在 vue 的 script 标签当中进行引入这个方法:
import {
isNumber} from '@/utils/validate' //导入这个js文件
修改这个表单验证代码:
amountOfMoney: [{
required: true,
trigger: 'blur',
validator: isNumber
}],
5、关于Dialog表单数据的问题
在进行一个dialog弹框的时候,会使用一个form表单进行数据验证,如上一个问题所示,在这里存在一个问题,在进行录入数据之后,点击取消按钮,这个我们可以通过一个单击事件将表单刚才的数据清空,但是当我们单击右上角的×的时候,会发现表单的数据不会进行清空,这该如何将其清空呢?
在这里我们可以给dialog绑定一个before-close属性,在官方文档当中有提示,是表示关闭前的回调,会暂停 Dialog 的关闭,参数是一个function,我们给这个属性绑定一个function。就绑定我们原先取消按钮的那个function,这样当我们按取消或者按右上角的×的时候都会将数据进行清空
注意:这里的取消按钮绑定的单击事件是需要加()的,而before-close的属性值不需要加(),代码如下所示:
<el-dialog title="详细信息" :visible.sync="dialogVisible" :before-close="cance">
<el-button @click="cance()" size="mini">取消</el-button>
6、关于v-if 是否显示的问题
在Vue文档当中对 v-if 是否进行显示只做了简单的介绍,但是现存一个需求如下,在使用 v-for 遍历数据进行显示的时候,我们需要根据这个数组当中的某一个属性对其进行是否展示的功能,这该如何下手呢?到这里多少有点懵。
处理方法其实也很简单,在要遍历的数组当中包含了一个元素,我们需要根据这个元素的值进行是否显示的判断,我们只需要对这个CradForm 添加一个元素(元素的类型为Boolean类型)就可以了。在获取完这个list集合之后,使用一个for循环获取到每一个子集合,进行if判断,添加这个show进去。
到这里我们就可以直接在v-if当中传入这个show做判断了,v-if="item.show"
7、关于高度自适应的问题
在前端页面进行显示的时候,在放大缩小之后会导致页面的整体发生变化,这样原本设置好的页面布局都会发生改变,在这个时候我们就需要获取实时的高度给到某一个div块,使得整个 html 页面能够撑起来,而获取这个高度该如何获取呢?
在这里我们可以直接通过style进行设置,这里100vh是整个 html 页面的高度,之后进行减去这个页面当中固定不变的高度(其他组件的高度,在这里也就是178px) 得到这个高度赋给当前这个div块,这样也就实现了高度自适应。
style="height: calc(100vh - 178px);"
8、关于时间处理的问题
在前端下古墓当中,难免会使用到选取时间这一个功能,使用到TimePicker 时间选择器这个组件,本着是挺简单的一个组件,但是在最后的项目上线(部署到本地服务器)之后,却出现了一个小问题,在选择时间的时候进行提交给后端之后再查询出来会发现时间少了两天。
1、先排查后端问题,在发现使用@JsonFormat这个注释的时候,直接就给定一个时间格式,但是在前端进行传递的时候也没有进行处理,就可能会导致时间的小时分钟也传递过来,而进行接收的时候就会对这小时的时间进行后移一天。所以为了避免这个问题,我们对时间再进行处理一下,
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date payDate;
这样之后进行还是存在问题,这样就只好去修改前端了,对组件获取到的时间进行处理成年月日的格式:
this.ruleForm.orderDate = this.ruleForm.orderDate.getFullYear() + "-"
+ (this.ruleForm.orderDate.getMonth() +1) + "-"
+ this.ruleForm.orderDate.getDate()
在这里对进行从前端传递给后端的时间进行修改之后,在这里还发现传递过去之后写入数据库这都是没有问题的,但是在进行更新操作的时候还是会少一天,我们只需要在进行更新的时候也对时间进行处理即可,同样使用上述代码。
9、关于elementui组件原生样式的问题
在开发的过程当中,进行图片叠加显示的时候,出现了一个问题,当使用一个 el-image 标签作为最下层的图片显示的时候,再使用一个div块(一个 el-image 图片块 加 一个div文字块)进行浮动显示,浮动在最开始的 el-image上,到这里会发发现,对div添加浮动,图片是可以浮动上去的,而和图片在同一个div下的文字却无法浮动上去,最开始我也很懵13,这是什么情况,如下图所示:文字却显示不出来
在进行一顿排查和请教前辈之后,发现在最下方的el-image标签,使用到了一个相对定位,在进行取消这个相对定位的时候,会发现我们想要显示的文字和按钮都显示出来了。这个问题确实是有点小离谱,也就在这记录下来。
那在代码当中如何修改呢?我们只需要使用html原生提供的 img 标签来代替 el-image 标签即可。