LayUI+SSM实现一个简单的后台管理系统
该后台管理系统是用于管理视频网站数据的,目前分5个菜单项,这篇博客主要讲述【影片管理】的具体功能和实现
后台代码结构和【影片管理】的界面如下图
该界面分为上下2部分,【搜索条件】和【影片列表】,2部分所用到的字段都在【Video】实体类中,具体代码如下
package me.xiaomaju.entity; import lombok.Data;
@Data public class Video { //表sys_video字段 private String id_video; private String name_video; private String describe; private String image; private int id_state; private String id_area; private String dt_show; //关联sys_type查询,拼接 private String type; //关联sys_state查询 private String name_state; //关联sys_area查询 private String area; //查询条件 page和limit要和LayUI的数据表格参数一致 private Integer page;//当前页码 private Integer limit;//每页显示数量 private String dt_begin; private String dt_end; private String id_type; }
视频表【sys_video】、状态表【sys_state】、地区表【sys_area】、类型表【sys_type】的表结构如下图
LayUI官网于2021年10月左右关闭,查看LayUI相关文档可以到 https://lln.kim/layui/index.html
【影片管理】明显用到LayUI的表单模块和数据表格模块,前端页面代码如下
<div style="width: 80% "> <!-- 搜索表单 --> <div> <fieldset class="layui-elem-field layui-field-title" style="margin-top: 10px;"> <legend>搜索条件</legend> </fieldset> <form class="layui-form layui-form-pane" method="post" style="text-align: center"> <div class="layui-inline"> <label class="layui-form-label">名称</label> <div class="layui-input-inline"> <input type="text" name="name_video" required placeholder="请输入名称" class="layui-input"> </div> </div> <div class="layui-inline"> <label class="layui-form-label">类型</label> <div class="layui-input-inline"> <select name="id_type" id="select_type" class="layui-input"> <option value="">请选择类型</option> </select> </div> </div> <div class="layui-inline"> <label class="layui-form-label">上映时间</label> <div class="layui-input-inline"> <input type="text" name="dt_begin" id="dt_begin" required placeholder="" class="layui-input"> </div> </div> <div class="layui-inline"> <label class="layui-form-label">至</label> <div class="layui-input-inline"> <input type="text" name="dt_end" id="dt_end" required placeholder="" class="layui-input"> </div> </div> <div class="layui-inline"> <button type="submit" class="layui-btn" lay-submit lay-filter="doSearch">搜索</button> </div> </form> </div> <!-- 搜索表单end --> <!-- 数据表格 --> <div> <fieldset class="layui-elem-field site-demo-button"> <legend>影片列表</legend> <div> <!-- 表格 --> <table class="layui-hide" id="dataTable" lay-filter="dataTable"></table> </div> </fieldset> <!-- 头部工具栏 --> <script type="text/html" id="topToolbar"> <div class="layui-btn-container"> <button class="layui-btn layui-btn-sm" lay-event="add"><i class="layui-icon layui-icon-add-circle"></i>添加 </button> <button class="layui-btn layui-btn-sm layui-btn-danger" lay-event="batchDelete"><i class="layui-icon layui-icon-delete"></i>批量删除 </button> </div> </script> <!-- 行工具栏 --> <script type="text/html" id="rowToolbar"> <button class="layui-btn layui-btn-xs" lay-event="edit">编辑</button> <button class="layui-btn layui-btn-danger layui-btn-xs" lay-event="delete">删除</button> </script> </div> <!-- 数据表格end --> </div>
开始写js代码,后面所有的js代码写在layui.use的 { } 里面。
<script> layui.use(['form', 'table', 'jquery', 'layer', 'laydate', 'upload'], function () { var form = layui.form; var table = layui.table; var $ = layui.jquery; var layer = layui.layer; var laydate = layui.laydate; var upload = layui.upload; //渲染表格组件 var tableIns = table.render({ elem: "#dataTable",//绑定表格元素,推荐使用ID选择器 url: "${pageContext.request.contextPath}/video/video-list",//异步请求地址,加入分页后,默认使用page(当前页码)和limit(每页显示数量)作为参数名称 page: true,//开启分页 toolbar: "#topToolbar", cols: [[ {type: "checkbox", fixed: "left", width: 80, align: "center"} , {field: 'id_video', title: 'ID', align: "center", width: 170} , {field: 'name_video', title: '名称', align: "center", width: 150} , {field: 'describe', title: '描述', align: "left", width: 255} , {field: 'image', title: '封面', align: "center", width: 150} , {field: 'name_state', title: '状态', align: "center", width: 60} , {field: 'type', title: '类型', align: "center", width: 140} , {field: 'area', title: '地区', align: "center", width: 90} , {field: 'dt_show', title: '上映时间', align: "center", width: 120} , {title: "操作", toolbar: "#rowToolbar", align: "center", width: 150} ]] });
}) </script>
通过url获取后台数据渲染数据表格,数据表格的数据要求4个属性,先新建该实体,代码如下
package me.xiaomaju.entity; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor public class LayUIDataGridView { private Integer code=0; private String msg=""; private Long count; private Object data; /** * 封装数据表格 * @param count * @param data */ public LayUIDataGridView(Long count, Object data) { this.count = count; this.data = data; } }
接下来通过 url: "${pageContext.request.contextPath}/video/video-list" 写后台代码
controller层代码如下
@Controller @RequestMapping("/video") public class VideoController { private static final Logger logger = org.apache.log4j.Logger.getLogger(VideoController.class); @Autowired private VideoService videoService; @RequestMapping(value = "/video-list") @ResponseBody public LayUIDataGridView findAll(Video video) { //设置分页信息(当前页码,每页条数) PageHelper.startPage(video.getPage(), video.getLimit()); //查询数据 List<Video> videoList = videoService.findByAttributes(video); //创建分页对象 PageInfo<Video> pageInfo = new PageInfo<Video>(videoList); //返回数据 return new LayUIDataGridView(pageInfo.getTotal(), pageInfo.getList()); } }
pagehelper是一个分页插件,在mybatis.xml配置一下即可,配置如下
<plugins> <plugin interceptor="com.github.pagehelper.PageInterceptor"> <!-- 分页合理化参数,默认false,设置为true是,pageNum<=0查第一页,pageNum>pages查最后一页--> <property name="reasonable" value="true"/> </plugin> </plugins>
service层代码如下
@Service public class VideoServiceImpl implements VideoService { private static final Logger logger = org.apache.log4j.Logger.getLogger(VideoServiceImpl.class); @Autowired private IVideoMapper videoMapper; @Override public List<Video> findByAttributes(Video video) { return videoMapper.findByAttributes(video);
}
}
mapper层代码如下
package me.xiaomaju.mapper; import me.xiaomaju.entity.Area; import me.xiaomaju.entity.Type; import me.xiaomaju.entity.Video; import org.apache.ibatis.annotations.*; import java.util.List; public interface IVideoMapper { @Select("<script> " + "select v.id_video, v.name_video,v.describe,v.image,v.id_state,v.id_area, v.dt_show, s.name_state,a.area from sys_video v " + "left join sys_state s on v.id_state=s.id_state " + "left join sys_area a on v.id_area=a.id_area " + " </script> ") public List<Video> findByAttributes(Video video); }
这样查出了【sys_video】表的所有字段,关联状态和地区表查出具体中文状态和地区。视频与类型则是多对多关系,这种多对多关系的数据用一张中间表来存储。
中间表【sys_video_type】只有2列,id_video和id_type,id_type又是类型表【sys_type】的主键。
所以可以在service层用id_video为入参,查询类型的中文,并将多个类型用逗号拼接在一起,代码如下
@Override public List<Video> findByAttributes(Video video) { List<Video> videoList = videoMapper.findByAttributes(video); if (videoList.size() < 1) return null; //封装类型 type for (Video _video : videoList) { List<String> typeList = videoMapper.findVideoType(_video.getId_video()); String type = StringUtil.getListStrs(typeList); _video.setType(type); } return videoList; }
mapper层查询类型代码如下
@Select("select t.name_type from sys_type t left join sys_video_type vt ON t.id_type= vt.id_type where vt.id_video=#{id} ") public List<String> findVideoType(String id);
通过上述表关系在那几张表造一些数据,页面数据表格就有对应数据显示了。
接下来实现上方【搜索条件】的查询功能,效果如下图
其中2个时间项用到了LayUI的日期与时间选择模块,需要进行组件渲染。搜索表单中【类型】下拉框的数据是动态的,后面介绍新增与编辑功能时会将对应js代码写出来。
//渲染日期组件 laydate.render({ elem: "#dt_begin", }); laydate.render({ elem: "#dt_end" });
//监听搜索按钮提交事件 form.on("submit(doSearch)", function (data) { tableIns.reload({ where: data.field,//查询条件 page: { curr: 1 } }); //禁止页面刷新 return false; });
需要注意的是,搜索和一开始的数据表格渲染走的是相同的后台代码,即"${pageContext.request.contextPath}/video/video-list"。
搜索表单中的字段都在Video实体中,所以直接在mapper层的代码中新增过滤条件,将原方法修改后的代码如下
@Select("<script> " + "select distinct v.id_video, v.name_video,v.describe,v.image,v.id_state,v.id_area, v.dt_show, s.name_state,a.area from sys_video v " + "left join sys_state s on v.id_state=s.id_state " + "left join sys_area a on v.id_area=a.id_area " + "left join sys_video_type vt on v.id_video=vt.id_video " + " <where> " + " <if test=\"name_video != null and name_video !='' \"> v.name_video=#{name_video} </if> " + " <if test=\" id_type != null and id_type !='' \"> and vt.id_type=#{id_type} </if> " + " <if test=\"dt_begin != null and dt_begin!='' \"> <![CDATA[ and v.dt_show >= #{dt_begin} ]]> </if> " + " <if test=\"dt_end != null and dt_end!='' \"> <![CDATA[ and v.dt_show <= #{dt_end} ]]> </if> " + " <if test=\"id_state != 0\"> AND v.id_state=#{id_state}</if> " + " </where> " + " </script> ") public List<Video> findByAttributes(Video video);
接下来实现新增和编辑的功能,新增和编辑用的是同一个表单,效果如下图
首先写这个表单的前台代码
<!-- 弹出层 --> <div id="addOrUpdateCardView" style="display: none;margin: 10px"> <form id="dataForm" method="post" class="layui-form layui-form-pane" lay-filter="dataForm"> <!-- 隐藏域,保存当前ID --> <input type="hidden" name="id_video"> <div class="layui-form-item"> <label class="layui-form-label">名称</label> <div class="layui-input-block"> <input type="text" name="name_video" required lay-verify="required|name_video" placeholder="请输入" autocomplete="off" class="layui-input"> </div> </div> <div class="layui-form-item layui-form-text"> <label class="layui-form-label">描述</label> <div class="layui-input-block"> <textarea name="describe" lay-verify="required|describe" class="layui-textarea"></textarea> </div> </div> <div class="layui-form-item"> <label class="layui-form-label">封面</label> <div class="layui-input-block"> <button type="button" class="layui-btn" id="btn-img">上传图片</button> <img class="layui-upload-img" width="50" height="50"> <!-- 隐藏域,保存后台传回来的图片保存路径 --> <input type="hidden" id="imgPath" name="image" value=""> </div> </div> <div class="layui-form-item"> <label class="layui-form-label">地区</label> <div class="layui-input-block"> <select name="id_area" class="layui-input"> <option value="">请选择地区</option> </select> </div> </div> <div class="layui-form-item"> <label class="layui-form-label">类型</label> <div class="layui-input-block" id="chkBox_type"> </div> </div> <div class="layui-form-item"> <label class="layui-form-label">上映时间</label> <div class="layui-input-block"> <input type="text" name="dt_show" id="dt_show" lay-verify="required|dt_show" placeholder="请输入" class="layui-input"> </div> </div> <div class="layui-form-item"> <label class="layui-form-label">状态</label> <div class="layui-input-block" id="sexRadio"> <input type="radio" name="id_state" value="1" title="禁用"> <input type="radio" name="id_state" value="2" title="正常" checked> <input type="radio" name="id_state" value="3" title="热播"> </div> </div> <div class="layui-form-item"> <div class="layui-input-block"> <button class="layui-btn" lay-submit lay-filter="doSubmit" id="formSubmit">立即提交</button> <button type="reset" class="layui-btn layui-btn-primary">重置</button> </div> </div> </form> </div> <!-- 弹出层end-->
新增和编辑对应的js代码,【上映日期】需要进行日期组件渲染
laydate.render({ elem: "#dt_show", type: "date"//默认 });
//监听表格头部工具栏事件 table.on("toolbar(dataTable)", function (obj) { switch (obj.event) { //添加 case 'add': openAddWindow(); break; } }); //监听表格行工具栏事件 table.on("tool(dataTable)", function (obj) { switch (obj.event) { //编辑 case 'edit': openUpdateWindow(obj.data); break; } });
var url;//提交地址 var mainIndex;//窗口索引 //打开编辑窗口 function openUpdateWindow(data) { //清空表单数据 $("#dataForm")[0].reset(); //将该行的类型给表单勾选上 var types = data.type.split(","); var typeCheckbox = $("input:checkbox[name='id_types']"); for (var j = 0; j < types.length; j++) { for (var i = 0; i < typeCheckbox.length; i++) { if (typeCheckbox[i].title == types[j]) { typeCheckbox[i].checked = true; } } form.render(); //更新渲染 } mainIndex = layer.open({ type: 1,//弹出层类型 title: "影片信息修改", area: ['800px', '600px'], content: $("#addOrUpdateCardView"),//引用的窗口代码 success: function () { //表单数据回显 // console.log(data); form.val("dataForm", data); //修改请求 url = "${pageContext.request.contextPath}/video/update"; } }); } //打开添加窗口 function openAddWindow() { mainIndex = layer.open({ type: 1,//弹出层类型 title: "添加影片", area: ['800px', '600px'], content: $("#addOrUpdateCardView"),//引用的窗口代码 success: function () { //清空表单数据 $("#dataForm")[0].reset(); url = "${pageContext.request.contextPath}/video/add"; } }); } //监听弹出层的表单提交事件 form.on("submit(doSubmit)", function (data) { if ($("input:checkbox[name='id_types']:checked").length > 3 || $("input:checkbox[name='id_types']:checked").length < 1) { layer.msg('类型为必填项,且最多只能选3项!', {icon: 5}); return false; } //获取checkbox[name='id_types']的值,获取所有选中的复选框,并将其值放入数组中 var arr = new Array(); $("input:checkbox[name='id_types']:checked").each(function (i) { arr[i] = $(this).val(); }); // 替换 data.field.id_types的数据为拼接后的字符串 data.field.id_types = arr.join(","); $.post(url, data.field, function (result) { if (result.success) { layer.alert(result.message, {icon: 1}); //关闭窗口 layer.close(mainIndex); //刷新数据表格 tableIns.reload(); } else { layer.alert(result.message, {icon: 2}); } }, "json"); return false; });
编辑功能的controller层代码如下
@RequestMapping(value = "/update", produces = "text/html;charset=UTF-8") @ResponseBody public String update(Video video, String[] id_types, HttpSession session) { Map<String, Object> map = new HashMap<String, Object>(); int result = videoService.update(video); // 先删除类型再新增 int types1 = videoService.deleteVideoType(video.getId_video()); int types = videoService.addVideoType(video.getId_video(), id_types); if (result > 0 && types > 0) { map.put("success", true); map.put("message", "修改成功"); Admin admin = (Admin) session.getAttribute("loginAdmin"); logger.info("修改影片的人员为:" + admin.getName_admin() + ";影片ID为:" + video.getId_video()); } else { map.put("success", false); map.put("message", "修改失败"); } return JSON.toJSONString(map); }
新增功能的controller层代码与编辑类似,新增的是直接新增类型,即往【sys_video_video】插入数据;而编辑需要先删除类型再新增类型,因为【id_types】的长度(表单中类型复选框勾选的数量)不是固定的。
而直接在新增功能的controller层代码中【video.getId_video()】是为空的,需要在mapper层加一个注解,详情见我下一篇博客。
搜索表单中的下拉框【类型】,新增功能和编辑功能的弹出层中的下拉框【地区】和复选框【类型】的数据是动态的,需要向后台发送Ajax请求,js代码如下
//发送Ajax请求查询类型 $.get("${pageContext.request.contextPath}/video/query-type", function (result) { var duoxuan = ""; var xiala = ""; //循环遍历集合 for (let i = 0; i < result.length; i++) { duoxuan += "<input type=\"checkbox\" name=\"id_types\" title='" + result[i].name_type + "' value='" + result[i].id_type + "' lay-skin=\"primary\" lay-verify=\"fuxuan\"> " xiala += "<option value='" + result[i].id_type + "'>" + result[i].name_type + "</option>" } //将网页代码追加到下拉列表和多选框中 $("[id='chkBox_type']").append(duoxuan); $("[id='select_type']").append(xiala); //更新渲染select下拉框 form.render("select"); }, "json"); //发送Ajax请求查询地区 $.get("${pageContext.request.contextPath}/video/query-area", function (result) { var html = ""; //循环遍历集合 for (let i = 0; i < result.length; i++) { html += "<option value='" + result[i].id_area + "'>" + result[i].area + "</option>" } //将网页代码追加到下拉列表中 $("[name='id_area']").append(html); //更新渲染select下拉框 form.render("select"); }, "json");
后台controller调用service,service 调用mapper查询地区和类型,返回对应实体的List集合即可。
如果你的前端页面和我一样是用jsp写的,不出意料的话,你的复选框显示可能有问题,在<html>标签上面加上 <!DOCTYPE html> 即可。
下面实现弹出层表单中的图片上传功能,js代码如下
//图片上传 var uploadInst = upload.render({ elem: "#btn-img" /*根据绑定id,打开本地图片*/ , url: "${pageContext.request.contextPath}/video/upload-img" /*上传后台接受接口*/ , done: function (res) { if (res.success) { $('#imgPath').attr('value', res.message); } else { layer.alert(res.message, {icon: 2}); } } });
后台controller层代码如下
@RequestMapping(value = "/upload-img", produces = "text/html;charset=UTF-8") @ResponseBody public String uploadImg(@RequestParam(value = "file", required = false) MultipartFile file, HttpServletRequest request) { Map<String, Object> map = new HashMap<String, Object>(); String originalFilename = file.getOriginalFilename(); // 获取文件上传名 String path = request.getServletContext().getRealPath("/") +"static\\img\\"+ originalFilename; logger.info("上传路径:"+path ); try { file.transferTo(new File(path)); map.put("success", true); map.put("message", path); } catch (IOException e) { map.put("success", false); map.put("message", "图片上传失败"); } return JSON.toJSONString(map); }
如果上传成功,将图片路径传到前台,放到弹出层表单中【封面】后的隐藏域中。
接下来实现行删除和批量删除,效果如下图
新增和批量删除在表格头部工具栏事件监听到,而行删除和编辑在表格行工具栏事件监听到,js代码如下
//监听表格头部工具栏事件 table.on("toolbar(dataTable)", function (obj) { switch (obj.event) { //添加 case 'add': openAddWindow(); break; //批量删除 case 'batchDelete': batchDelete(); break; } }); //监听表格行工具栏事件 table.on("tool(dataTable)", function (obj) { switch (obj.event) { //编辑 case 'edit': openUpdateWindow(obj.data); break; //删除 case 'delete': deleteById(obj.data); break; } });
//单个删除 function deleteById(data) { //提示用户确认是否删除 layer.confirm('真的要删除吗?', {icon: 3, title: '提示'}, function (index) { //发送ajax请求 $.post("${pageContext.request.contextPath}/video/deleteById", {"id_video": data.id_video}, function (result) { if (result.success) { layer.alert(result.message, {icon: 1}); //刷新数据表格 tableIns.reload(); } else { layer.alert(result.message, {icon: 2}); } }, "json"); //关闭提示框 layer.close(index); }); } //批量删除 function batchDelete() { //获取表格对象 var checkStatus = table.checkStatus('dataTable'); //判断是否有选中行 if (checkStatus.data.length > 0) { //定义数组,保存选中行的ID var idArr = []; //循环遍历获取选中行(目的是获取选中的每一行的ID值) for (let i = 0; i < checkStatus.data.length; i++) { //将选中的ID值添加到数组的末尾 idArr.push(checkStatus.data[i].id_video); } //将数组转成字符串 var ids = idArr.join(","); //提示用户是否删除 layer.confirm('真的要删除吗?', {icon: 3, title: "提示"}, function (index) { //发送ajax请求 $.post("${pageContext.request.contextPath}/video/batch-delete", {"ids": ids}, function (result) { if (result.success) { layer.alert(result.message, {icon: 1}); //刷新数据表格 tableIns.reload(); } else { layer.alert(result.message, {icon: 2}); } }, "json"); //关闭提示框 layer.close(index); }); } else { layer.msg("请选择要删除的数据"); } }
单个删除和批量删除的controller层代码如下
@RequestMapping(value = "/deleteById", produces = "text/html;charset=UTF-8") @ResponseBody public String deleteById(String id_video) { Map<String, Object> map = new HashMap<String, Object>(); int result = videoService.deleteById(id_video); if (result > 0) { map.put("success", true); map.put("message", "删除成功"); } else { map.put("success", false); map.put("message", "删除失败"); } return JSON.toJSONString(map); } @RequestMapping(value = "/batch-delete", produces = "text/html;charset=UTF-8") @ResponseBody public String batchDelte(String ids) { Map<String, Object> map = new HashMap<String, Object>(); int result = videoService.batchDelte(ids); if (result > 0) { map.put("success", true); map.put("message", "删除成功"); } else { map.put("success", false); map.put("message", "删除失败"); } return JSON.toJSONString(map); }
批量删除需要在service层中将ids按逗号分隔成数组再删除。
完毕