一、业务效果说明
列表页面不跳转,点击【添加流派】,layer.open 弹出弹窗表单,填写数据 AJAX 提交,提交成功关闭弹窗,局部刷新表格列表。
点击每行【修改】按钮,发送 AJAX 请求查询单条数据,回显到弹窗表单,编辑提交更新数据库。
点击【删除】按钮,弹出 layer.confirm 确认提示框,确认之后执行删除,无刷新更新列表。
表单校验:正则校验输入内容,异步 AJAX 校验流派名称是否重复。
核心:所有操作停留在列表页,不发生页面跳转,用户体验更好。
二、项目引入 Layui 资源
把 layui 静态资源(css、js)放入 webapp,在 jsp 页面头部引入
<!--引入jQuery layui -->
<script src="/js/jquery-3.3.1.min.js"></script>
<link rel="stylesheet" href="/layui/css/layui.css">
<script src="/layui/layui.js"></script>
三、列表页面 HTML 片段
列表页面,隐藏新增弹窗 div、修改弹窗 div,默认 display:none,layer.open 动态渲染。
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%--列表表单,用于分页、搜索提交--%>
<form id="listForm" action="/mtype/list" method="get">
<input type="hidden" name="pageNo" id="pageNo" value="${page.pageNo}">
<input type="text" name="tname" value="${mq.tname}" placeholder="流派名称搜索">
<button type="submit">搜索</button>
</form>
<button id="addBtn" class="layui-btn layui-btn-normal">添加流派</button>
<table class="layui-table">
<thead>
<tr>
<th>编号</th>
<th>流派名称</th>
<th>流派描述</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<c:forEach items="${page.list}" var="item">
<tr>
<td>${item.tid}</td>
<td>${item.tname}</td>
<td>${item.tdesc}</td>
<td>
<button class="layui-btn layui-btn-xs edit-btn" data-tid="${item.tid}">修改</button>
<button class="layui-btn layui-btn-xs layui-btn-danger del-btn" data-tid="${item.tid}">删除</button>
</td>
</tr>
</c:forEach>
</tbody>
</table>
<%--新增弹窗 隐藏div--%>
<div id="addPop" style="display:none;padding:20px;">
<form class="layui-form" id="addForm" lay-filter="addFilter">
<div class="layui-form-item">
<label class="layui-form-label">流派名称</label>
<div class="layui-input-block">
<input type="text" name="tname" lay-verify="required|checkName" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">流派描述</label>
<div class="layui-input-block">
<textarea name="tdesc" class="layui-textarea"></textarea>
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit lay-filter="addSubmit">提交新增</button>
</div>
</div>
</form>
</div>
<%--修改弹窗 隐藏div--%>
<div id="updatePop" style="display:none;padding:20px;">
<form class="layui-form" id="updateForm" lay-filter="updateFilter">
<%--主键隐藏域,用于更新条件--%>
<input type="hidden" name="tid">
<div class="layui-form-item">
<label class="layui-form-label">流派名称</label>
<div class="layui-input-block">
<input type="text" name="tname" lay-verify="required" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">流派描述</label>
<div class="layui-input-block">
<textarea name="tdesc" class="layui-textarea"></textarea>
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit lay-filter="updateSubmit">保存修改</button>
</div>
</div>
</form>
</div>
重点注意:
两个弹窗 div 写在同一个列表页面,默认隐藏,layer 只是把 DOM 拿出来弹窗渲染,不会销毁 DOM,会产生数据缓存坑。
lay‑filter 的值必须和 js 监听的 filter 名字一一对应,名字对不上表单监听完全失效。
四、前端 JS 完整代码
<script>
var addLayerIndex; //新增弹窗索引,用于关闭弹窗
var updateLayerIndex; //修改弹窗索引
layui.use(['form','layer'],function () {
var form = layui.form;
var layer = layui.layer;
//自定义表单校验:正则 + 异步校验流派名称是否重复
form.verify({
checkName:function (value,item) {
//正则:只能中文、字母、数字,1‑20位
if(!/^[\u4e00‑\u9a5fa‑zA‑Z0‑9]{1,20}$/.test(value)){
return "名称只能是中英文、数字,长度1‑20";
}
var flag = true;
//异步校验必须设置 async:false 同步等待结果,校验器需要拿到返回结果
$.ajax({
url:"/mtype/checkName",
type:"post",
data:{tname:value},
async:false,
success:function (resp) {
if(resp === "exist"){
flag = false;
}
}
})
if(!flag){
return "该流派名称已经存在!";
}
}
});
//1、打开新增弹窗
$("#addBtn").click(function () {
//【重要】每次打开新增弹窗,重置表单,清除上次残留数据,解决DOM缓存旧数据
$("#addForm")[0].reset();
form.render('addFilter');
addLayerIndex = layer.open({
type:1,
title:"新增流派",
area:['520px','360px'],
content:$("#addPop")
})
});
//监听新增表单提交 lay‑filter="addFilter" lay‑submit="addSubmit"
form.on('submit(addSubmit)',function (data) {
$.ajax({
url:"/mtype/addMtype",
type:"post",
data:data.field,
dataType:"text",
success:function (res) {
if(res === "success"){
layer.msg("新增成功");
layer.close(addLayerIndex);
//提交列表表单,无刷新刷新页面列表数据
$("#listForm").submit();
}else {
layer.msg("新增失败");
}
}
})
return false; //阻止表单原生跳转提交!!!
});
//2、修改:点击修改按钮,回显数据打开弹窗
$(".edit-btn").click(function () {
var tid = $(this).data("tid");
//请求后端获取单条数据
$.ajax({
url:"/mtype/getMtype",
type:"post",
data:{tid:tid},
dataType:"json",
success:function (obj) {
//给修改表单赋值
form.val("updateFilter",obj);
updateLayerIndex = layer.open({
type:1,
title:"修改流派",
area:['520px','360px'],
content:$("#updatePop")
})
}
})
});
//监听修改表单提交
form.on('submit(updateSubmit)',function (data) {
$.ajax({
url:"/mtype/updateMtype",
type:"post",
data:data.field,
dataType:"text",
success:function (res) {
if(res === "success"){
layer.msg("修改成功");
layer.close(updateLayerIndex);
$("#listForm").submit();
}else{
layer.msg("修改失败");
}
}
})
return false;
});
//3、删除功能 layer.confirm确认弹窗
$(".del-btn").click(function () {
var tid = $(this).data("tid");
layer.confirm("确定删除该流派吗?删除后不可恢复!",{icon:3,title:"删除确认"},function (index) {
$.ajax({
url:"/mtype/delMtype",
type:"post",
data:{tid:tid},
dataType:"text",
success:function (res) {
if(res === "success"){
layer.msg("删除成功");
layer.close(index);
$("#listForm").submit();
}else{
layer.msg("删除失败");
}
}
})
})
})
})
</script>
关键点说明:
return false:阻止 form 表单原生跳转提交,完全交给 AJAX 异步处理。
form.render(filterName):layui 表单 DOM 改变后必须重新渲染,否则输入框、校验不生效。
异步校验名字重复,async:false,必须同步,校验函数要拿到 ajax 返回结果。
layer.open 拿到返回的 index,用于layer.close(index)关闭指定弹窗。
五、SSM Controller 后端代码
package cn.tx.controller;
import cn.tx.model.Mtype;
import cn.tx.query.MtypeQuery;
import cn.tx.service.MtypeService;
import cn.tx.util.Page;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping("/mtype")
public class MtypeController {
@Autowired
private MtypeService mtypeService;
//列表分页(上篇博客通用分页)
@RequestMapping("/list")
public String list(MtypeQuery mq, Model model){
if(mq.getPageNo() == null){
mq.setPageNo(1);
}
Page<Mtype> page = mtypeService.selectObjectByCondition(mq);
model.addAttribute("page",page);
model.addAttribute("mq",mq);
return "songtype";
}
//异步校验流派名称是否存在
@ResponseBody
@PostMapping("/checkName")
public String checkName(String tname){
MtypeQuery query = new MtypeQuery();
query.setTname(tname);
Page<Mtype> page = mtypeService.selectObjectByCondition(query);
if(page.getTotalCount()>0){
return "exist"; //名称已存在
}
return "ok";
}
//新增
@ResponseBody
@PostMapping("/addMtype")
public String addMtype(Mtype mtype){
mtypeService.insert(mtype);
return "success";
}
//根据id查询单条数据,给修改弹窗回显
@ResponseBody
@PostMapping("/getMtype")
public Mtype getMtype(Integer tid){
return mtypeService.selectByPrimaryKey(tid);
}
//修改
@ResponseBody
@PostMapping("/updateMtype")
public String updateMtype(Mtype mtype){
mtypeService.updateByPrimaryKeySelective(mtype);
return "success";
}
//删除
@ResponseBody
@PostMapping("/delMtype")
public String delMtype(Integer tid){
mtypeService.deleteByPrimaryKey(tid);
return "success";
}
}
六、高频踩坑汇总
坑 1:lay‑filter 名字不匹配,表单 submit 完全没有触发
现象:点击提交按钮毫无反应,js 里面的form.on('submit(xxx)'不会执行。
原因:form标签lay‑filter="addFilter"和 js 监听submit(addSubmit)里面的 filter 名称必须严格对应,大小写敏感。
排查:复制粘贴字符串,不要手敲写错。
坑 2:layer 弹窗关闭,第二次打开弹窗,还残留上次填写旧数据(DOM 缓存坑)
原因:layer.open 只是把页面中已有的 div 拿出来弹窗,关闭弹窗不会删除 DOM 节点,表单数据还留在内存 DOM 里面。
解决方案:每次打开弹窗前,手动$("#addForm")0.reset()重置表单,执行form.render("addFilter")重新渲染 layui 表单。
坑 3:AJAX 提交成功,但是页面列表不刷新
现象:弹窗关闭,数据库数据变了,表格还是旧数据。
错误做法:location.reload()整页刷新(体验差)。
本项目方案:$("#listForm").submit()提交列表查询表单,请求后台重新拉取列表数据,页面刷新列表。
坑 4:自定义校验做 AJAX 异步查重,直接 return 结果无效
现象:不管名字是否重复,校验永远通过。
根源:ajax 默认async:true异步,校验函数直接 return 的时候,AJAX 请求还没有返回。
解决:设置async:false同步等待 ajax 执行完毕,拿到布尔标记再 return 提示文字。
注意:同步 AJAX 会阻塞浏览器,少量后台管理场景可以使用;高并发前台不推荐。
坑 5:form.val () 赋值回显修改弹窗不生效
form.val 的第一个参数是lay‑filter的值,不是表单 id!form.val("updateFilter",obj)。
实体类属性名必须和表单name属性完全一致,大小写敏感。
赋值之后不需要手动给 input 设置 val,form.val 会自动填充全部表单元素。
坑 6:忘记写 return false,表单触发原生页面跳转
现象:点击提交弹窗一瞬间,页面直接跳转到/mtype/addMtype地址。
form.on('submit(xxx)',function(data){ ... return false; })必须写 return false,阻止表单原生提交行为,全部交给 AJAX 处理。
坑 7:layer.confirm 删除框,点取消也执行删除
现象:不管点确认还是取消,都会执行删除逻辑。
原因:删除逻辑写在 confirm 外面;删除逻辑必须写在layer.confirm回调函数内部,只有点击确定才会执行回调。
七、整体开发流程小结
在列表 JSP 页面写 2 份隐藏 div,分别对应新增表单、修改表单,设置 lay‑filter。
js 监听按钮,layer.open 打开弹窗,打开前重置表单解决 DOM 缓存。
layui form.verify 写自定义校验,异步查重设置async:false。
form.on 监听 lay‑submit 提交事件,AJAX 提交 data.field 表单数据,return false阻止原生提交。
Controller 接收参数完成新增 / 修改,返回success文本。
成功回调关闭 layer 弹窗,提交列表表单刷新页面表格。
删除使用layer.confirm确认弹窗,确认后执行 AJAX 删除。
核心思想:Layer 弹窗只是 UI 容器,真正表单 DOM 仍然在列表页面,因此带来缓存坑;AJAX 异步交互避免页面跳转,是后台管理系统主流交互方案。
八、拓展思考
问:除了把 div 写在当前页面隐藏,layer.open 还有什么方式加载弹窗?
答:
本项目方案:页面内置隐藏 div,弹窗渲染;优点速度快,不用额外请求;缺点存在 DOM 缓存问题,每次打开要重置表单。
type:2 iframe 方式,单独跳转一个 jsp 弹窗页面;优点 DOM 隔离无缓存,缺点需要额外发请求,弹窗页面不能直接操作父页面 js 变量。
后台管理系统小弹窗,优先采用内置 div 的方式。