第 11 章 · jQuery + Layui 后台 CRUD 全流程
本章目标:串联列表页 table + 搜索 form、新增/编辑 layer 弹窗 + form Ajax、删除 confirm + POST/DELETE;实现权限菜单、loading 与统一错误提示;完成 admin-demo 商品管理模块完整 walkthrough,对标行业常见 Layui 商户后台。
学时建议:5 小时(含 2 小时跟练)
前置:ch07~ch10 全部内容;ch04 Ajax;建议对照 admin-demo 的 product-list.html。
11.1 后台 CRUD 页面模式
示例电商后台 admin-demo 一条业务线(商品)典型拆分为:
商品管理
├── product-list.html 列表 + 搜索 + table + 行操作
├── product-edit.html 表单(iframe 子页或独立页)
├── product-list.js 列表逻辑
├── api/product.js 接口函数(可选)
└── mock/products.json 联调前数据
CRUD 与 Layui 组件映射:
| 操作 | Layui / jQuery |
|---|---|
| C reate | toolbar「新增」→ layer.open 空表单 |
| R ead | table.render + 详情 open |
| U pdate | 行「编辑」→ 带 id 打开表单 → PUT |
| D elete | layer.confirm → DELETE |
| 搜索 | form.on('submit') → table.reload |
行业常见做法无独立 api.js,逻辑写在页面内联脚本或模板 {% block JS %};admin-demo 建议逐步抽到 js/ 便于 ch12 迁移对照。
11.2 列表页:table + 搜索 form
11.2.1 HTML 骨架
product-list.html:页头说明 + layui-form 搜索(keyword、status)+ #productTable;模板 #productToolbar(新增)、#productBar(编辑/删除)。布局同 ch09 订单列表,字段换为商品维度。
11.2.2 初始化 table
var searchCache = {};
function initProductTable() {
table.render({
id: 'productTable',
elem: '#productTable',
url: API_BASE + '/api/products',
method: 'get',
height: 'full-140',
toolbar: '#productToolbar',
defaultToolbar: ['filter', 'exports'],
where: searchCache,
request: { pageName: 'page', limitName: 'limit' },
parseData: parseListResponse,
cols: [[
{ type: 'numbers', title: '序号', width: 60 },
{ field: 'id', title: 'ID', width: 80 },
{ field: 'name', title: '商品名', minWidth: 180 },
{ field: 'price', title: '价格', width: 100,
templet: function (d) { return '¥' + d.price; }
},
{ field: 'stock', title: '库存', width: 80 },
{ field: 'status', title: '状态', width: 90, templet: '#statusTpl' },
{ fixed: 'right', title: '操作', toolbar: '#productBar', width: 140 }
]],
page: { limit: 10, limits: [10, 20, 50] },
done: onTableDone
});
}
form.on('submit(searchProduct)', function (data) {
searchCache = data.field;
table.reload('productTable', {
where: searchCache,
page: { curr: 1 }
});
return false;
});
与 product-list.html 的搜索表单、table.reload 完全一致。
11.3 新增/编辑:layer 弹窗 + form 提交
11.3.1 打开弹窗
function openProductForm(id) {
var title = id ? '编辑商品' : '新增商品';
var url = 'product-edit.html' + (id ? '?id=' + id : '');
layer.open({
type: 2,
title: title,
shade: 0.3,
maxmin: true,
area: ['1000px', '750px'],
content: url,
end: function () {
table.reload('productTable');
}
});
}
table.on('toolbar(productTable)', function (obj) {
if (obj.event === 'add') openProductForm();
});
table.on('tool(productTable)', function (obj) {
if (obj.event === 'edit') openProductForm(obj.data.id);
});
11.3.2 子页提交(ch08 扩展)
// product-edit.html 内
form.on('submit(saveProduct)', function (data) {
var field = normalizeProductField(data.field);
var isEdit = !!field.id;
$.ajax({
url: API_BASE + '/api/products' + (isEdit ? '/' + field.id : ''),
type: isEdit ? 'PUT' : 'POST',
contentType: 'application/json',
data: JSON.stringify(field),
headers: authHeaders(),
success: function (res) {
if (res.code === 0) {
XZ.ok('保存成功', function () {
closeSelfLayer();
});
} else {
XZ.fail(res.msg);
}
},
error: XZ.onHttpError
});
return false;
});
父页传参替代 URL 查询(行业常见:弹窗内操作 iframe DOM):
success: function (layero, index) {
var iframe = layero.find('iframe')[0];
var child = iframe.contentWindow;
if (child && child.setProductId) child.setProductId(rowData);
}
两种任选,团队统一即可。
11.4 删除:confirm + Ajax
table.on('tool(productTable)', function (obj) {
if (obj.event === 'del') {
XZ.confirm({
title: '删除商品',
html: '<p>确定删除 <b>' + obj.data.name + '</b>?</p>' +
'<p style="color:#999;font-size:12px;">有未完成订单时请勿删除</p>',
onOk: function () {
var load = layer.load(1);
$.ajax({
url: API_BASE + '/api/products/' + obj.data.id,
type: 'DELETE',
headers: authHeaders(),
success: function (res) {
layer.close(load);
if (res.code === 0) {
XZ.ok('已删除');
obj.del();
} else {
XZ.fail(res.msg);
}
},
error: function () {
layer.close(load);
XZ.onHttpError();
}
});
}
});
}
});
行业常见删除后 location.reload();教学项目优先 obj.del() 或 table.reload 减少闪烁。
11.5 权限菜单
11.5.1 菜单配置
// js/menu-config.js