Layui+Layer 弹出层实现 CRUD:AJAX 无刷新新增修改删除后台实战

一、业务效果说明

列表页面不跳转,点击【添加流派】,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 的方式。

相关推荐
sunphp开发者1 小时前
制作手绘地图瓦片切图
前端·uni-app
尤水就下1 小时前
聊聊 Prompt 是怎么一路进化到 Harness 的
前端·人工智能·ai编程
李溪白1 小时前
深入 JavaScript 作用域:从变量提升到执行上下文的底层逻辑
前端
想风1 小时前
Claude Code 实用技巧工作坊 —— Boris Cherny
前端·后端·github
梦想blog1 小时前
Claude Code + Chrome MCP:让浏览器自动化测试更简单
前端·chrome·自动化·mcp
YHHLAI1 小时前
React `useState` 深入浅出
前端·javascript·react.js
夜猫子小强1 小时前
手搓一个「本地邮件摘要」工具:零依赖、数据不出电脑
前端
PBitW1 小时前
Element plus 自定义列顺序 —— 求指点
前端·element
绿岛之北1 小时前
Electron 安全第五章:网络请求安全
前端·electron