大事件后台管理系统开发实战(下)
文章目录
- 续前篇:大事件后台管理系统开发实战(中)
- 1. 文章类别
- 1.1 点击编辑按钮展示修改文章分类的弹出层
- 1.2 为修改文章分类的弹出层填充表单数据
- 1.3 更新文章分类的数据
- 1.4 删除文章分类
- 2. 文章列表
- 2.1 创建文章列表页面
- 2.2 定义查询参数对象q
- 2.3 请求文章列表数据并使用模板引擎渲染列表结构
- 2.3.1 定义获取文章列表数据
- 2.4 定义美化时间格式的过滤器
- 2.5 绘制筛选区域的UI结构
- 2.6 发起请求获取并渲染文章分类的下拉选择框
- 2.7 实现筛选的功能
- 3. 分页
- 3.1 定义渲染分页的 renderPage 方法
- 3.2 调用 laypage.render 方法渲染分页的基本结构
- 3.3 在jump回调函数中通过obj.curr获取到最新的页码值
- 3.4 解决 jump 回调函数发生死循环的问题
- 3.5 自定义分页的功能项
- 3.6 实现切换每页展示多少条数据的功能
- 4. 删除文章
- 4.1 实现删除文章的功能
- 4.2 解决删除文章时的小 Bug
- 5. 发布文章
- 5.1 创建文章发布页面的基本结构
- 5.2 新建基本的表单结构
- 5.3 渲染文章类别对应的下拉选择框结构
- 5.4 渲染富文本编辑器
- 5.5 渲染封面裁剪区域
- 5.6 渲染提交按钮区域
- 5.7 点击选择封面按钮打开文件选择框
- 5.8 将选择的图片设置到裁剪区域中
- 小结 - 渲染封面裁剪区域
- 5.9 分析发布文章的实现步骤
- 5.10 基于Form表单快速创建FormData对象
- 5.11 将裁剪后的封面追加到FormData对象中
- 5.12 发起Ajax请求实现发布文章的功能
- 6. 编辑文章
- 6.1 创建文章编辑页面基本结构
- 6.2 给编辑按钮添加点击事件
- 6.3 新建基本的表单结构
- 6.4 文章类别对应的下拉选择框结构
- 6.5 渲染富文本编辑器
- 6.6 封面裁剪区域结构
- 6.7 提交按钮区域
- 6.8 通过 URLSearchParams 对象,获取 URL 传递的参数
- 6.9 发起请求获取文章详情
- 6.10 选择文章封面
- 6.11 监听文件(文章封面图片)选择框的 change 事件
- 6.12 设置文章的发布状态
- 6.13 发布文章
- 6.14 完整html结构
- 6.15 完整css 样式代码
- 6.16 编辑功能完整 JS 代码
- 7. 将开发完成的项目代码推送到GitHub
续前篇:大事件后台管理系统开发实战(中)
1. 文章类别
1.1 点击编辑按钮展示修改文章分类的弹出层
为编辑按钮添加
btn-edit
类名如下:<button type="button" class="layui-btn layui-btn-xs btn-edit" data-id="{{$value.Id}}">编辑</button>
定义
修改分类
的弹出层:<script type="text/html" id="dialog-edit"><form class="layui-form" id="form-edit" lay-filter="form-edit"><!-- 隐藏域,保存 Id 的值 --><input type="hidden" name="Id"><div class="layui-form-item"><label class="layui-form-label">分类名称</label><div class="layui-input-block"><input type="text" name="name" required lay-verify="required" placeholder="请输入分类名称" autocomplete="off" class="layui-input"></div></div><div class="layui-form-item"><label class="layui-form-label">分类别名</label><div class="layui-input-block"><input type="text" name="alias" required lay-verify="required" placeholder="请输入分类别名" autocomplete="off" class="layui-input"></div></div><div class="layui-form-item"><div class="layui-input-block"><button class="layui-btn" lay-submit lay-filter="formDemo">确认修改</button></div></div></form> </script>
快速为一个表单填充数据:先为form表单添加 lay-filter
属性,再调用form
方法(前提是要先导入form:var form=layui.form
)。同时,为表单创建一个隐藏域(input)用于保存id
值
3. 通过 代理
的形式,为 btn-edit
按钮绑定点击事件:
var indexEdit = null$('tbody').on('click', '.btn-edit', function() {// 弹出一个修改文章分类信息的层indexEdit = layer.open({type: 1,area: ['500px', '250px'],title: '修改文章分类',content: $('#dialog-edit').html()})})
1.2 为修改文章分类的弹出层填充表单数据
为编辑按钮绑定
data-id
自定义属性:<button type="button" class="layui-btn layui-btn-xs btn-edit" data-id="{{$value.Id}}">编辑</button>
在展示弹出层之后,根据 id 的值发起请求获取文章分类的数据,并填充到表单中:
var id = $(this).attr('data-id') // 发起请求获取对应分类的数据 $.ajax({method: 'GET',url: '/my/article/cates/' + id,success: function(res) {form.val('form-edit', res.data)} })
1.3 更新文章分类的数据
通过代理的形式,为修改分类的表单绑定 submit 事件:
$('body').on('submit', '#form-edit', function(e) {e.preventDefault()$.ajax({method: 'POST',url: '/my/article/updatecate',data: $(this).serialize(),success: function(res) {if (res.status !== 0) {return layer.msg('更新分类数据失败!')}layer.msg('更新分类数据成功!')layer.close(indexEdit)initArtCateList()}}) })
1.4 删除文章分类
为删除按钮绑定
btn-delete
类名,并添加data-id
自定义属性:<button type="button" class="layui-btn layui-btn-danger layui-btn-xs btn-delete" data-id="{{$value.Id}}">删除</button>
通过代理的形式,为删除按钮绑定点击事件:
$('tbody').on('click', '.btn-delete', function() {var id = $(this).attr('data-id') // 获取 id 的值// 提示用户是否要删除layer.confirm('确认删除?', { icon: 3, title: '提示' }, function(index) {$.ajax({method: 'GET',url: '/my/article/deletecate/' + id,success: function(res) {if (res.status !== 0) {return layer.msg('删除分类失败!')}layer.msg('删除分类成功!')layer.close(index)initArtCateList()}})}) })
2. 文章列表
共分为3个区域,依卡片面板上的布局设计,从上到下依次是:
- 筛选区域;
- 列表区域;
- 分页区域。
2.1 创建文章列表页面
首先,绘制卡片区域,打开layui官网,复制卡片面板的HTML结构代码
新建
/article/art_list.html
页面结构如下:<!DOCTYPE html> <html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><link rel="stylesheet" href="/assets/lib/layui/css/layui.css" /><link rel="stylesheet" href="/assets/css/article/art_list.css" /></head><body><!-- 卡片区域 --><div class="layui-card"><div class="layui-card-header">文章列表</div><div class="layui-card-body"></div></div><!-- 导入第三方的 JS 插件 --><script src="/assets/lib/layui/layui.all.js"></script><script src="/assets/lib/jquery.js"></script><script src="/assets/js/baseAPI.js"></script><!-- 导入自己的 JS 脚本 --><script src="/assets/js/article/art_list.js"></script></body> </html>
新建
/assets/css/article/art_list.css
样式表如下:html, body {margin: 0;padding: 0; }body {padding: 15px;background-color: #f2f3f5; }
新建
/assets/js/article/art_list.js
脚本文件。
2.2 定义查询参数对象q
定义一个查询的参数对象如下:
// 定义一个查询的参数对象,将来请求数据的时候,// 需要将请求参数对象提交到服务器var q = {pagenum: 1, // 页码值,默认请求第一页的数据pagesize: 2, // 每页显示几条数据,默认每页显示2条cate_id: '', // 文章分类的 Idstate: '' // 文章的发布状态}
2.3 请求文章列表数据并使用模板引擎渲染列表结构
API 接口如图(注意,请求时需携带 4 个 参数):
2.3.1 定义获取文章列表数据
1)定义查询的参数对象q
, 将来请求数据时,需要将请求参数对象提交到服务器。
var q = {pagenum: 1, // 默认值,默认请求第一页的数据 pagesize: 2, // 每页显示几条数据cate_id: '', // 文章分类的id,默认为空state: '' // 文章的发布状态}
2)定义一个获取文章列表数据的方法(自定义函数)
注: 要先导入模板引擎
initTable() // 获取文章列表数据的方法
function initTable() {$.ajax({method: 'GET',url: '/my/article/list',data: q,success: function(res) {if (res.status !== 0) {return layer.msg('获取文章列表失败!')}// 使用模板引擎渲染页面的数据var htmlStr = template('tpl-table', res)$('tbody').html(htmlStr)}})
}
- 在页面中添加表格结构如下:
在layui官网找到 【页面元素】⇒\Rightarrow⇒【表格】⇒\Rightarrow⇒【常规用法】,复制html结构到代码编辑器中。
修改后的表格结构如下:
<!-- 列表区域 -->
<table class="layui-table"><colgroup><col /><col width="150" /><col width="180" /><col width="150" /><col width="150" /></colgroup><thead><tr><th>文章标题</th><th>分类</th><th>发表时间</th><th>状态</th><th>操作</th></tr></thead><tbody></tbody>
</table>
定义列表数据的模板结构:
<script type="text/html" id="tpl-table">{{each data}}<tr><td>{{$value.title}}</td><td>{{$value.cate_name}}</td><td>{{$value.pub_date|dataFormat}}</td><td>{{$value.state}}</td><td><button type="button" class="layui-btn layui-btn-xs">编辑</button><button type="button" class="layui-btn layui-btn-danger layui-btn-xs">删除</button></td></tr>{{/each}} </script>
2.4 定义美化时间格式的过滤器
通过
template.defaults.imports
定义过滤器(此属性模板引擎独有,需先导入模板引擎):// 定义美化时间的过滤器template.defaults.imports.dataFormat = function(date) {const dt = new Date(date)var y = dt.getFullYear()var m = padZero(dt.getMonth() + 1)var d = padZero(dt.getDate())var hh = padZero(dt.getHours())var mm = padZero(dt.getMinutes())var ss = padZero(dt.getSeconds())return y + '-' + m + '-' + d + ' ' + hh + ':' + mm + ':' + ss}// 定义补零的函数function padZero(n) {return n > 9 ? n : '0' + n}
在模板引擎中使用过滤器:
<td>{{$value.pub_date|dataFormat}}</td>
2.5 绘制筛选区域的UI结构
绘制 UI 结构:
<!-- 筛选区域 --> <form class="layui-form" id="form-search"><div class="layui-form-item layui-inline"><select name="cate_id"></select></div><div class="layui-form-item layui-inline"><select name="state"><option value="">所有状态</option><option value="已发布">已发布</option><option value="草稿">草稿</option></select></div><div class="layui-form-item layui-inline"><button class="layui-btn" lay-submit lay-filter="formDemo">筛选</button></div> </form>
2.6 发起请求获取并渲染文章分类的下拉选择框
定义
initCate
函数请求文章分类的列表数据:initCate()// 初始化文章分类的方法function initCate() {$.ajax({method: 'GET',url: '/my/article/cates',success: function(res) {if (res.status !== 0) {return layer.msg('获取分类数据失败!')}// 调用模板引擎渲染分类的可选项var htmlStr = template('tpl-cate', res)$('[name=cate_id]').html(htmlStr)// 通过 layui 重新渲染表单区域的UI结构form.render()}})}
定义分类可选项的模板结构:
<script type="text/html" id="tpl-cate"><option value="">所有分类</option>{{each data}}<option value="{{$value.Id}}">{{$value.name}}</option>{{/each}} </script>
2.7 实现筛选的功能
为筛选表单绑定 submit 事件:
$('#form-search').on('submit', function(e) {e.preventDefault()// 获取表单中选中项的值var cate_id = $('[name=cate_id]').val()var state = $('[name=state]').val()// 为查询参数对象 q 中对应的属性赋值q.cate_id = cate_idq.state = state// 根据最新的筛选条件,重新渲染表格的数据initTable() })
3. 分页
在 layui 官方文档中找到 【内置模块】⇒\Rightarrow⇒【分页】,
1)在自己 vs code 中 art_list.html 分页区域的位置 ,添加一个 id
名为 pageBox
的 div 容器,用于存放分页组件
2)初始化分页区:从 layui 中导入 laypage 对象,即复制上图上的代码 var laypage=layui.laypage
,并在 art_list.js 头部导入此句代码。
3)js 中调用 laypage.render 方法渲染分页的结构。注意要传入 4 个参数
参数选项 | 说明 | 类型 | 默认值 |
---|---|---|---|
elem |
指向存放分页的容器,值可以是容器ID、DOM对象。如: 1. elem: ‘id’ 注意:这里不能加 # 号 2. elem: document.getElementById(‘id’) |
String/Object | - |
count | 数据总数。一般通过服务端得到 | Number | - |
limit | 每页显示的条数。laypage将会借助 count 和 limit 计算出分页数。 | Number | 10 |
curr | 起始页。一般用于刷新类型的跳页以及HASH跳页。 | Number | 1 |
// 导入 laypage 对象
var laypage = layui.laypage;
// 渲染底部分分页区域的方法function renderPage(total) {laypage.render({elem: 'pageBox', //分页容器 Idcount: total, // 分页条数limit: q.pagesize, // 每页显示几条数据curr: q.pagenum // 指定默认选中哪一页})}
保存并刷新网页后,效果如下:
3.1 定义渲染分页的 renderPage 方法
定义渲染分页的方法:
function renderPage(total) {console.log(total) }
在
initTable
中调用renderPage
方法:function initTable() {$.ajax({method: 'GET',url: '/my/article/list',data: q,success: function(res) {if (res.status !== 0) {return layer.msg('获取文章列表失败!')}// 使用模板引擎渲染页面的数据var htmlStr = template('tpl-table', res)$('tbody').html(htmlStr)// 调用渲染分页的方法renderPage(res.total)}}) }
3.2 调用 laypage.render 方法渲染分页的基本结构
在页面中定义分页的区域:
<!-- 分页区域 --> <div id="pageBox"></div>
调用 laypage.render() 方法来渲染分页的结构:
// 定义渲染分页的方法 function renderPage(total) {// 调用 laypage.render() 方法来渲染分页的结构laypage.render({elem: 'pageBox', // 分页容器的 Idcount: total, // 总数据条数limit: q.pagesize, // 每页显示几条数据curr: q.pagenum // 设置默认被选中的分页}) }
3.3 在jump回调函数中通过obj.curr获取到最新的页码值
// 定义渲染分页的方法
function renderPage(total) {// 调用 laypage.render() 方法来渲染分页的结构laypage.render({elem: 'pageBox', // 分页容器的 Idcount: total, // 总数据条数limit: q.pagesize, // 每页显示几条数据curr: q.pagenum, // 设置默认被选中的分页// 分页发生切换的时候,触发 jump 回调jump: function(obj) {console.log(obj.curr)// 把最新的页码值,赋值到 q 这个查询参数对象中q.pagenum = obj.curr}})
}
3.4 解决 jump 回调函数发生死循环的问题
分页里面有个 jump
回调函数(包括两个参数,obj
和first
),通过这个回调就可以拿到当前的页码值。只要点击页码或者调用了 laypage.render() 方法,就会触发 jump 回调。
jump 中,能监听到分页切换事件
// 定义渲染分页的方法function renderPage(total) {// 调用 laypage.render() 方法来渲染分页的结构laypage.render({elem: 'pageBox', // 分页容器的 Idcount: total, // 总数据条数limit: q.pagesize, // 每页显示几条数据curr: q.pagenum, // 设置默认被选中的分页// 分页发生切换的时候,触发 jump 回调// 触发 jump 回调的方式有两种:// 1. 点击页码的时候,会触发 jump 回调// 2. 只要调用了 laypage.render() 方法,就会触发 jump 回调jump: function(obj, first) {// 可以通过 first 的值,来判断是通过哪种方式,触发的 jump 回调// 如果 first 的值为 true,证明是方式2触发的// 否则就是方式1触发的console.log(first)console.log(obj.curr)// 把最新的页码值,赋值到 q 这个查询参数对象中q.pagenum = obj.curr// 根据最新的 q 获取对应的数据列表,并渲染表格// initTable() // 不能直接在这里调用,会形成死循环。if (!first) {initTable()}}})}
3.5 自定义分页的功能项
// 定义渲染分页的方法
function renderPage(total) {// 调用 laypage.render() 方法来渲染分页的结构laypage.render({elem: 'pageBox', // 分页容器的 Idcount: total, // 总数据条数limit: q.pagesize, // 每页显示几条数据curr: q.pagenum, // 设置默认被选中的分页layout: ['count', 'limit', 'prev', 'page', 'next', 'skip'],limits: [2, 3, 5, 10],// 分页发生切换的时候,触发 jump 回调// 触发 jump 回调的方式有两种:// 1. 点击页码的时候,会触发 jump 回调// 2. 只要调用了 laypage.render() 方法,就会触发 jump 回调jump: function(obj, first) {// 可以通过 first 的值,来判断是通过哪种方式,触发的 jump 回调// 如果 first 的值为 true,证明是方式2触发的// 否则就是方式1触发的console.log(first)console.log(obj.curr)// 把最新的页码值,赋值到 q 这个查询参数对象中q.pagenum = obj.curr// 根据最新的 q 获取对应的数据列表,并渲染表格// initTable()if (!first) {initTable()}}})
}
3.6 实现切换每页展示多少条数据的功能
// 定义渲染分页的方法
function renderPage(total) {// 调用 laypage.render() 方法来渲染分页的结构laypage.render({elem: 'pageBox', // 分页容器的 Idcount: total, // 总数据条数limit: q.pagesize, // 每页显示几条数据curr: q.pagenum, // 设置默认被选中的分页layout: ['count', 'limit', 'prev', 'page', 'next', 'skip'],limits: [2, 3, 5, 10],// 分页发生切换的时候,触发 jump 回调// 触发 jump 回调的方式有两种:// 1. 点击页码的时候,会触发 jump 回调// 2. 只要调用了 laypage.render() 方法,就会触发 jump 回调jump: function(obj, first) {// 可以通过 first 的值,来判断是通过哪种方式,触发的 jump 回调// 如果 first 的值为 true,证明是方式2触发的// 否则就是方式1触发的console.log(first)console.log(obj.curr)// 把最新的页码值,赋值到 q 这个查询参数对象中q.pagenum = obj.curr// 把最新的条目数,赋值到 q 这个查询参数对象的 pagesize 属性中q.pagesize = obj.limit// 根据最新的 q 获取对应的数据列表,并渲染表格// initTable()if (!first) {initTable()}}})
}
4. 删除文章
4.1 实现删除文章的功能
为删除按钮绑定
btn-delete
类名和 添加data-id
自定义属性:<button type="button" class="layui-btn layui-btn-danger layui-btn-xs btn-delete" data-id="{{$value.Id}}">删除</button>
通过代理的形式,为删除按钮绑定点击事件处理函数:
$('tbody').on('click', '.btn-delete', function() {// 获取到文章的 idvar id = $(this).attr('data-id') // 这句一定要放在询问提示框之前,否则会删除失败!// 询问用户是否要删除数据layer.confirm('确认删除?', { icon: 3, title: '提示' }, function(index) {$.ajax({method: 'GET',url: '/my/article/delete/' + id,success: function(res) {if (res.status !== 0) {return layer.msg('删除文章失败!')}layer.msg('删除文章成功!')initTable()}})layer.close(index)}) })
4.2 解决删除文章时的小 Bug
$('tbody').on('click', '.btn-delete', function() {// 获取删除按钮的个数var len = $('.btn-delete').length// 获取到文章的 idvar id = $(this).attr('data-id')// 询问用户是否要删除数据layer.confirm('确认删除?', { icon: 3, title: '提示' }, function(index) {$.ajax({method: 'GET',url: '/my/article/delete/' + id,success: function(res) {if (res.status !== 0) {return layer.msg('删除文章失败!')}layer.msg('删除文章成功!')// 当数据删除完成后,需要判断当前这一页中,是否还有剩余的数据// 如果没有剩余的数据了,则让页码值 -1 之后,// 再重新调用 initTable 方法// 4if (len === 1) {// 如果 len 的值等于1,证明删除完毕之后,页面上就没有任何数据了// 页码值最小必须是 1q.pagenum = q.pagenum === 1 ? 1 : q.pagenum - 1}initTable()}})layer.close(index)})
})
5. 发布文章
5.1 创建文章发布页面的基本结构
新建
/article/art_pub.html
页面结构如下:<!DOCTYPE html> <html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><link rel="stylesheet" href="/assets/lib/layui/css/layui.css" /><link rel="stylesheet" href="/assets/css/article/art_pub.css" /></head><body><!-- 卡片区域 --><div class="layui-card"><div class="layui-card-header">写文章</div><div class="layui-card-body">卡片式面板面板通常用于非白色背景色的主体内<br />从而映衬出边框投影</div></div><!-- 导入第三方的 JS 插件 --><script src="/assets/lib/layui/layui.all.js"></script><script src="/assets/lib/jquery.js"></script><script src="/assets/js/baseAPI.js"></script><!-- 导入自己的 JS --><script src="/assets/js/article/art_pub.js"></script></body> </html>
新建
/assets/css/article/art_pub.css
样式文件如下:html, body {margin: 0;padding: 0; }body {padding: 15px;background-color: #f2f3f5; }
新建
/assets/js/article/art_pub.js
脚本文件如下:$(function() { })
5.2 新建基本的表单结构
<!-- 发布文章的表单 -->
<form class="layui-form"><div class="layui-form-item"><label class="layui-form-label">文章标题</label><div class="layui-input-block"><input type="text" name="title" required lay-verify="required" placeholder="请输入标题" autocomplete="off" class="layui-input" /></div></div>
</form>
5.3 渲染文章类别对应的下拉选择框结构
定义 UI 结构:
<!-- 第二行 --><div class="layui-form-item"><label class="layui-form-label">文章类别</label><div class="layui-input-block"><select name="cate_id" lay-verify="required"></select></div></div>
导入 art-template:
<script src="/assets/lib/template-web.js"></script>
定义模板结构:
<script type="text/html" id="tpl-cate"><option value="">请选择文章类别</option>{{each data}}<option value="{{$value.Id}}">{{$value.name}}</option>{{/each}} </script>
定义
initCate
方法:$(function() {var layer = layui.layervar form = layui.forminitCate()// 定义加载文章分类的方法function initCate() {$.ajax({method: 'GET',url: '/my/article/cates',success: function(res) {if (res.status !== 0) {return layer.msg('初始化文章分类失败!')}// 调用模板引擎,渲染分类的下拉菜单var htmlStr = template('tpl-cate', res)$('[name=cate_id]').html(htmlStr)// 一定要记得调用 form.render() 方法 // 由于layui不知道动态生成了分类form.render()}})} })
5.4 渲染富文本编辑器
参考我的另一篇文章: 基于 Layui 的富文本编辑器和封面图片剪裁的实现方案
与编辑器 & 图片裁剪实现方案相关的素材文件:
网盘下载链接:https://pan.baidu.com/s/1k9IBXKKQfD7pf9a6NNyQkQ
提取码:qw6o
5.5 渲染封面裁剪区域
参考 基于 Layui 的富文本编辑器和封面图片剪裁的实现方案
5.6 渲染提交按钮区域
<!-- 第五行 --><div class="layui-form-item"><div class="layui-input-block"><button class="layui-btn" lay-submit>发布</button><button class="layui-btn layui-btn-primary" lay-submit>存为草稿</button></div></div>
5.7 点击选择封面按钮打开文件选择框
修改 UI 结构,为
选择封面
按钮添加id
,并且在按钮后面添加文件选择框
:<!-- 选择封面按钮 --> <button type="button" class="layui-btn layui-btn-danger" id="btnChooseImage">选择封面</button> <!-- 隐藏的文件选择框 --> <input type="file" id="coverFile" style="display: none;" accept="image/png,image/jpeg,image/gif" />
为选择封面的按钮,绑定点击事件处理函数:
$('#btnChooseImage').on('click', function() {$('#coverFile').click() })
5.8 将选择的图片设置到裁剪区域中
监听
coverFile
的change
事件,获取用户选择的文件列表:// 监听 coverFile 的 change 事件,获取用户选择的文件列表$('#coverFile').on('change', function(e) {// 获取到文件的列表数组var files = e.target.files// 判断用户是否选择了文件if (files.length === 0) {return}// 根据文件,创建对应的 URL 地址var newImgURL = URL.createObjectURL(files[0])// 为裁剪区域重新设置图片$image.cropper('destroy') // 销毁旧的裁剪区域.attr('src', newImgURL) // 重新设置图片路径.cropper(options) // 重新初始化裁剪区域})
小结 - 渲染封面裁剪区域
第一步:为文件选择框绑定 change
事件;
第二步:在事件对象 e
中,获取到用户选择的文件列表files
;
第三步:判断用户是否选择了文件,如果没有选择文件,就直接return
出去,不再执行后面代码;
第四步:用户选择了文件,我们需要将文件创建为对应的新的url
地址;
第五步:重新为裁剪区域设置图片。
5.9 分析发布文章的实现步骤
API 接口如图所示:
为
存为草稿
按钮添加id
属性:<button class="layui-btn layui-btn-primary" lay-submit id="btnSave2">存为草稿</button>
定义文章的发布状态:
var art_state = '已发布'
为存为草稿按钮,绑定点击事件处理函数:
$('#btnSave2').on('click', function() {art_state = '草稿' })
5.10 基于Form表单快速创建FormData对象
为发布文章的 Form 表单添加
id
属性:<form class="layui-form" id="form-pub"></form>
为表单绑定 submit 提交事件:
$('#form-pub').on('submit', function(e) {// 1. 阻止表单的默认提交行为e.preventDefault()// 2. 基于 form 表单,快速创建一个 FormData 对象var fd = new FormData($(this)[0])// 3. 将文章的发布状态,追加到 fd 中fd.append('state', art_state)})
快速创建 FormData 对象:new FormData($(this)[0])
代码注释:
在new
FormData 期间,通过把 jQuery 对象$(this)
以[0]
的形式转换为一个原生的 DOM 对象,然后把这个 DOM 对象传递给new FormData()
,就能够创建出一个 FormData 对象。
为了证实这个fd 对象中确实存了一些数据,我们可以用forEach
将 FormData 中存储的每一个键循环打印到控制台查看,方法如下:
// 测试创建 FormData 是否成功
fd.forEach(function(v, k) { // 用v 接收FormData中存的值,k 接收键console.log(k, v);
})
提交事件触发后,控制台打印如下:
5.11 将裁剪后的封面追加到FormData对象中
这里仍然参照文章 基于 Layui 的富文本编辑器和封面图片剪裁的实现方案 一文中 “4. 将裁剪后的图片,输出为文件” 的代码。
代码如图:
// 为表单绑定 submit 提交事件$('#form-pub').on('submit', function(e) {// 1. 阻止表单的默认提交行为e.preventDefault()// 2. 基于 form 表单,快速创建一个 FormData 对象var fd = new FormData($(this)[0])// 3. 将文章的发布状态,存到 fd 中fd.append('state', art_state)// 4. 将封面裁剪过后的图片,输出为一个文件对象$image.cropper('getCroppedCanvas', {// 创建一个 Canvas 画布width: 400,height: 280}).toBlob(function(blob) {// 将 Canvas 画布上的内容,转化为文件对象// 得到文件对象后,进行后续的操作// 5. 将文件对象,存储到 fd 中fd.append('cover_img', blob)// 6. 发起 ajax 数据请求})})
只要调用了toBlob
,就能将裁剪之后的图片输出为文件了,.toBlob(function(blob)
中的blob
就是图片文件。再将 blob
存到 FormData(也就是fd) 里。
5.12 发起Ajax请求实现发布文章的功能
定义一个发布文章的方法:
function publishArticle(fd) {$.ajax({method: 'POST',url: '/my/article/add',data: fd,// 注意:如果向服务器提交的是 FormData 格式的数据,// 必须添加以下两个配置项contentType: false,processData: false,success: function(res) {if (res.status !== 0) {return layer.msg('发布文章失败!')}layer.msg('发布文章成功!')// 发布文章成功后,跳转到文章列表页面location.href = '/article/art_list.html'}}) }
注意:
如果向服务器提交的是 FormData 格式的数据,必须添加以下两个配置项:
contentType: false,
processData: false,
否则,请求就会失败。
把裁剪的图片追加到
FormData
对象中之后,调用publishArticle
方法:// 为表单绑定 submit 提交事件 $('#form-pub').on('submit', function(e) {// 1. 阻止表单的默认提交行为e.preventDefault()// 2. 基于 form 表单,快速创建一个 FormData 对象var fd = new FormData($(this)[0])// 3. 将文章的发布状态,存到 fd 中fd.append('state', art_state)// 4. 将封面裁剪过后的图片,输出为一个文件对象$image.cropper('getCroppedCanvas', {// 创建一个 Canvas 画布width: 400,height: 280}).toBlob(function(blob) {// 将 Canvas 画布上的内容,转化为文件对象// 得到文件对象后,进行后续的操作// 5. 将文件对象,存储到 fd 中fd.append('cover_img', blob)// 6. 发起 ajax 数据请求publishArticle(fd)}) })
6. 编辑文章
6.1 创建文章编辑页面基本结构
- 新建
/article/art_edit.html
页面结构如下:
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><link rel="stylesheet" href="/assets/lib/layui/css/layui.css" /><link rel="stylesheet" href="/assets/css/article/art_pub.css" /></head><body><!-- 卡片区域 --><div class="layui-card"><div class="layui-card-header">写文章</div><div class="layui-card-body">卡片式面板面板通常用于非白色背景色的主体内<br />从而映衬出边框投影</div></div><!-- 导入第三方的 JS 插件 --><script src="/assets/lib/layui/layui.all.js"></script><script src="/assets/lib/jquery.js"></script><script src="/assets/js/baseAPI.js"></script><!-- 导入自己的 JS --><script src="/assets/js/article/art_edit.js"></script></body></html>
- 新建
/assets/css/article/art_edit.css
样式文件(初始化页面)如下:
html,
body {margin: 0;padding: 0;
}body {padding: 15px;background-color: #f2f3f5;
}
- 新建
/assets/js/article/art_edit.js
脚本文件如下:
$(function() { })
6.2 给编辑按钮添加点击事件
- 修改文章列表的模板引擎
将/article/art.list.html
文件打开,给模板引擎中的“编辑”按钮添加类名btn_edit
、自定义属性data-index
和data-id
<button type="button" data-index='{{$value.Id}}' class="layui-btn layui-btn-xs btn-edit" data-id="{{$value.Id}}">编辑</button>
- 给【编辑】按钮绑定点击事件
// 给文章编辑按钮绑定 click 单击事件$('body').on('click', '.btn-edit', function() {location.href = '/article/art_edit.html?id=' + $(this).attr('data-id')})
6.3 新建基本的表单结构
在上一步创建的html结构的卡片区域中,将类名为 layui-card-body 的 div 盒子内的文字替换为如下表单(在Layui官方文档中复制输入框html结构),并为form表单添加id="formAddArticle"
<!-- 修改文章的表单 -->
<form class="layui-form" lay-filter="addArticle" id="formAddArticle" enctype="multipart/form-data"><!-- 文章标题 --><div class="layui-form-item"><label class="layui-form-label">文章标题</label><div class="layui-input-block"><input type="text" name="title" required lay-verify="required" placeholder="请输入标题" autocomplete="off" class="layui-input" /></div></div>
</form>
6.4 文章类别对应的下拉选择框结构
定义 UI 结构:
<!-- 文章类别 --><div class="layui-form-item"><label class="layui-form-label">文章类别</label><div class="layui-input-block" id="art_cate"></div></div>
导入 art-template:
在html文件底部,在导入的 layui.js 后面导入template 模板引擎脚本文件。<script src="/assets/lib/template-web.js"></script>
定义文章分类列表模板结构:
给模板结构添加 id属性:id="selectArtCates"
,方便后续渲染。<!-- 文章分类列表的模板 --><script type="text/html" id="selectArtCates"><select name="cate_id" lay-verify="required"><option value="">请选择文章类别</option>{{each data}}<option value="{{$value.Id}}">{{$value.name}}</option>{{/each}}</select></script>
定义
renderArticleCates
方法,渲染文章分类列表:$(function() {// 获取需要的 layui 对象var form = layui.form// 1. 渲染文章分类列表renderArticleCates()function renderArticleCates() {$.get('/my/article/cates', function(res) {if (res.status !== 0) {return layer.msg('获取文章分类列表失败!')}var htmlStr = template('selectArtCates', res)$('#art_cate').html(htmlStr)// 一定要记得调用 form.render() 方法 // 由于layui不知道动态生成了分类form.render()getArticleById()})}
6.5 渲染富文本编辑器
参考 基于 Layui 的富文本编辑器和封面图片剪裁的实现方案 完成。
6.6 封面裁剪区域结构
- 剪裁区域html
<!-- 文章封面 -->
<div class="layui-form-item"><label class="layui-form-label">文章封面</label><div class="layui-input-block cropper-container"><!-- 文件选择框 --><input type="file" id="fileCover" accept="image/jpeg,image/png,image/gif,image/bmp" style="display: none;" /><div class="cropper-box"><img id="image" src=""></div><div class="right-box"><div class="img-preview"></div><button class="layui-btn layui-btn-danger" id="btnChooseCoverImage">选择封面</button></div></div>
</div>
art_edit.css
文件中添加剪裁区域 css 样式
.cropper-box {width: 400px;height: 280px;
}#image {max-width: 400px;/* 这个设置很重要 */max-height: 280px;
}.img-preview {width: 200px;height: 140px;overflow: hidden;margin-bottom: 20px;
}.cropper-container {display: flex;
}
6.7 提交按钮区域
<!-- 按钮区域 -->
<div class="layui-form-item"><div class="layui-input-block"><button class="layui-btn" lay-submit id="btnPublish">发布</button><button class="layui-btn layui-btn-primary" lay-submit id="btnSave">存为草稿</button></div>
</div>
给表单添加隐藏域,用于获取用户选择的文章的Id。
<input type="hidden" name="Id" />
6.8 通过 URLSearchParams 对象,获取 URL 传递的参数
// 通过 URLSearchParams 对象,获取 URL 传递过来的参数var params = new URLSearchParams(location.search)var artId = params.get('id')// 文章的发布状态var pubState = ''
解释:
首先声明变量params
,并通过URLSearchParams对象
赋值,再通过params.get()
的方式输入键、拿到值(这里赋值给artId
),就很简单地 拿到了前一个页面(文章列表页点击“编辑”时)传过来的文章id
值。
Location对象
提供以下属性。
属性 | 说明 |
---|---|
Location.href
|
整个 URL |
Location.protocol
|
当前 URL 的协议,包括冒号(: )
|
Location.host
|
主机,包括冒号(: )和端口(默认的80端口和443端口会省略)。
|
Location.hostname
|
主机名,不包括端口。 |
Location.port
|
端口号。 |
Location.pathname
|
URL 的路径部分,从根路径/ 开始。
|
Location.search
|
查询字符串部分,从问号? 开始。
|
Location.hash
|
片段字符串部分,从# 开始。
|
Location.username
|
域名前面的用户名。 |
Location.password
|
域名前面的密码。 |
Location.origin
|
URL 的协议、主机名和端口。 |
更多关于URLSearchParams(location.search)
,可参考MDN文档 和后面这篇简书文章 《JS Location对象,URL对象,URLSearchParams对象》
6.9 发起请求获取文章详情
根据文章的 Id,获取文章的详情,并初始化表单的数据内容:
function getArticleById() {// 发起请求,获取文章详情$.get('/my/article/' + artId, function(res) {// 获取数据失败if (res.status !== 0) {return layer.msg('获取文章失败!')}// 获取数据成功var art = res.data// 为 form 表单赋初始值form.val('addArticle', {Id: art.Id,title: art.title,cate_id: art.cate_id,content: art.content})// 手动初始化富文本编辑器initEditor() // cropper插件自带方法,导入插件即可使用// 初始化图片裁剪器var $image = $('#image')// 设置图片路径$image.attr('src', 'http://ajax.frontend.itheima.net' + art.cover_img)// 裁剪选项var cropperOption = {aspectRatio: 400 / 280,preview: '.img-preview',// 初始化图片裁剪框的大小autoCropArea: 1}// 初始化裁剪区域$image.cropper(cropperOption)})}
form.val
:携带有值就给表单赋值,未携带值则获取表单中的值。
语法:
form.val('filter', object)
;
用于给指定表单集合的元素赋值和取值。如果 object 参数存在,则为赋值;如果 object 参数不存在,则为取值。其中「取值」功能为 layui 2.2.5 开始新增
attr()
方法:设置被选元素(这里是image
标签)的属性和值。详情参阅 W3School文档。
裁剪选项
var cropperOption={}
,参见 基于Layui 的富文本编辑器与封面的实现方案
6.10 选择文章封面
// 选择封面$('#btnChooseCoverImage').on('click', function(e) e.preventDefault() // 阻止默认提交行为$('#fileCover').click()})
6.11 监听文件(文章封面图片)选择框的 change 事件
$('#fileCover').on('change', function(e) {var files = e.target.files// 没有选择文件if (files.length === 0) {return}// 重新为裁剪区域设置图片$('#image').cropper('destroy').attr('src', URL.createObjectURL(files[0])).cropper({aspectRatio: 400 / 280,preview: '.img-preview'})
})
6.12 设置文章的发布状态
$('#btnPublish').on('click', function() {pubState = '已发布'
})
$('#btnSave').on('click', function() {pubState = '草稿'
})
6.13 发布文章
绑定【发布】按钮的submit
提交事件,同样的需参照 基于Layui 的富文本编辑器与封面的实现方案,完成后的代码如下:
$('#formAddArticle').on('submit', function(e) {e.preventDefault() // 阻止默认提交事件// 创建一个 Canvas 画布$('#image').cropper('getCroppedCanvas', {width: 400,height: 280})// 将 Canvas 画布上的内容,转化为文件对象.toBlob(function(blob) { // 5.1 组织参数对象 FormDatavar fd = new FormData($('#formAddArticle')[0])// 5.2 添加封面fd.append('cover_img', blob)// 5.3 添加文章的发表状态fd.append('state', pubState)// 5.4 发起请求$.ajax({method: 'POST',url: '/my/article/edit',data: fd,contentType: false,processData: false,success: function(res) {if (res.status !== 0) {return layer.msg('编辑文章失败!')}location.href = '/article/art_list.html'}})})
})
toBlob()
方法的语法:canvas.toBlob(callback, type, encoderOptions)
;
用以展示canvas上的图片;这个图片文件可以被缓存或保存到本地,由用户代理端自行决定。如不特别指明,图片的类型默认为 image/png,分辨率为 96dpi。
第三个参数用于针对image/jpeg格式的图片进行输出图片的质量设置(值在0与1之间)。
更多详情请参阅 MDN文档
附< 文件在线转 Base64 地址>: https://www.css-js.com/tools/base64.html
至此,文章编辑功能的开发已算完成。
6.14 完整html结构
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><link rel="stylesheet" href="/assets/lib/layui/css/layui.css"><!-- 富文本样式 --><link rel="stylesheet" href="/assets/lib/cropper/cropper.css" /><link rel="stylesheet" href="/assets/css/article/art_edit.css">
</head><body><div class="layui-card"><div class="layui-card-header">修改文章</div><div class="layui-card-body"><!-- 修改文章的表单 --><form class="layui-form" lay-filter="addArticle" id="formAddArticle" enctype="multipart/form-data"><!-- 文章标题 --><div class="layui-form-item"><label class="layui-form-label">文章标题</label><div class="layui-input-block"><input type="text" name="title" required lay-verify="required" placeholder="请输入标题" autocomplete="off" class="layui-input" /></div></div><!-- 文章类别 --><div class="layui-form-item"><label class="layui-form-label">文章类别</label><div class="layui-input-block" id="art_cate"></div></div><!-- 文章内容 --><div class="layui-form-item"><label class="layui-form-label">文章内容</label><div class="layui-input-block" style="height: 400px;"><textarea name="content" id="content"></textarea></div></div><!-- 文章封面 --><div class="layui-form-item"><label class="layui-form-label">文章封面</label><div class="layui-input-block cropper-container"><!-- 文件选择框 --><input type="file" id="fileCover" accept="image/jpeg,image/png,image/gif,image/bmp" style="display: none;" /><div class="cropper-box"><img id="image" src=""></div><div class="right-box"><div class="img-preview"></div><button class="layui-btn layui-btn-danger" id="btnChooseCoverImage">选择封面</button></div></div></div><!-- 按钮区域 --><div class="layui-form-item"><div class="layui-input-block"><button class="layui-btn" lay-submit id="btnPublish">发布</button><button class="layui-btn layui-btn-primary" lay-submit id="btnSave">存为草稿</button></div></div><input type="hidden" name="Id" /></form></div></div><!-- 文章分类列表的模板 --><script type="text/html" id="selectArtCates"><select name="cate_id" lay-verify="required"><option value="">请选择文章类别</option>{{each data}}<option value="{{$value.Id}}">{{$value.name}}</option>{{/each}}</select></script><script src="/assets/lib/layui/layui.all.js"></script><script src="/assets/lib/template-web.js"></script><script src="/assets/lib/jquery.js"></script><script src="/assets/js/baseAPI.js"></script><!-- 富文本编辑器 --><script src="../../assets/lib/tinymce/tinymce.min.js"></script><script src="../../assets/lib/tinymce/tinymce_setup.js"></script><!-- 图片裁剪 --><script src="../../assets/lib/cropper/Cropper.js"></script><script src="../../assets/lib/cropper/jquery-cropper.js"></script><script src="/assets/js/article/art_edit.js"></script>
</body></html>
6.15 完整css 样式代码
html,body {margin: 0;padding: 0;
}body {padding: 15px;background-color: #f2f3f5;
}.cropper-box {width: 400px;height: 280px;
}#image {max-width: 400px;/* 这个设置很重要! */max-height: 280px;
}.img-preview {width: 200px;height: 140px;overflow: hidden;margin-bottom: 20px;
}.cropper-container {display: flex;
}.cropper-box {margin-right: 15px;
}.right-box {text-align: center;
}
6.16 编辑功能完整 JS 代码
$(function() {// 通过 URLSearchParams 对象,获取 前一个页面URL 传递过来的参数var params = new URLSearchParams(location.search)var artId = params.get('id')// 文章的发布状态var pubState = ''// 获取需要的 layui 对象var form = layui.form// 1. 渲染文章分类列表renderArticleCates()function renderArticleCates() {$.get('/my/article/cates', function(res) {if (res.status !== 0) {return layer.msg('获取文章分类列表失败!')}var htmlStr = template('selectArtCates', res)$('#art_cate').html(htmlStr)form.render()getArticleById()})}// 2. 根据文章的 Id,获取文章的详情,并初始化表单的数据内容function getArticleById() {// 发起请求,获取文章详情$.get('/my/article/' + artId, function(res) {// 获取数据失败if (res.status !== 0) {return layer.msg('获取文章失败!')}// 获取数据成功var art = res.data// 为 form 表单赋初始值form.val('addArticle', {Id: art.Id,title: art.title,cate_id: art.cate_id,content: art.content})// 手动初始化富文本编辑器initEditor()// 初始化图片裁剪器var $image = $('#image')$image.attr('src', 'http://ajax.frontend.itheima.net' + art.cover_img)// $image.attr('src', 'http://www.liulongbin.top:3007' + art.cover_img)// 裁剪选项var cropperOption = {aspectRatio: 400 / 280,preview: '.img-preview',// 初始化图片裁剪框的大小autoCropArea: 1}// 初始化裁剪区域$image.cropper(cropperOption)})}// 3. 选择封面$('#btnChooseCoverImage').on('click', function(e) {e.preventDefault()$('#fileCover').click()})// 4. 监听文件选择框的 change 事件$('#fileCover').on('change', function(e) {var files = e.target.files// 没有选择文件if (files.length === 0) {return}// 重新为裁剪区域设置图片$('#image').cropper('destroy').attr('src', URL.createObjectURL(files[0])).cropper({aspectRatio: 400 / 280,preview: '.img-preview'})})// 设置文章的发布状态$('#btnPublish').on('click', function() {pubState = '已发布'})$('#btnSave').on('click', function() {pubState = '草稿'})// 5. 发布文章$('#formAddArticle').on('submit', function(e) {e.preventDefault()$('#image').cropper('getCroppedCanvas', {width: 400,height: 280}).toBlob(function(blob) {// 5.1 组织参数对象var fd = new FormData($('#formAddArticle')[0])// 5.2 添加封面fd.append('cover_img', blob)// 5.3 添加文章的发表状态fd.append('state', pubState)// 5.4 发起请求$.ajax({method: 'POST',url: '/my/article/edit',data: fd,contentType: false,processData: false,success: function(res) {if (res.status !== 0) {return layer.msg('编辑文章失败!')}location.href = '/article/art_list.html'}})})})
})
7. 将开发完成的项目代码推送到GitHub
1、检查当前所在分支:
git branch
2、本地提交
git add .
3、检查所有文件状态
git status
4、运行 git commit -m
git commit -m "完成文章管理相关功能的开发"
5、将本地article
分支提交到 Github 存储
git push -u origin article
6、将本地article
分支里最新的代码合并到master
主分支
1) 切换到 master 主分支
git checkout master
2)合并article里最新的代码
git merge article
7、将本地 master最新代码推送到Github中
git push
大事件后台管理系统开发实战(下)相关推荐
- 大事件后台管理系统开发实战(上)
文章目录 前言 0. 资源地址 1. 项目前期的准备工作 1.1 初始化项目结构 1.2 使用GitHub管理大事件的项目 1.3 安装VSCode的Live Server插件辅助开发 2. 登录注册 ...
- 大事件后台管理系统开发实战(中)
文章目录 3. 后台主页 3.8 获取用户的基本信息 3.9 渲染用户头像 3.10为有权限的接口统一设置headers请求头 3.11 实现退出功能 3.12 控制用户的访问权限 3.13 优化权限 ...
- 视频教程-SSM后台管理系统开发实战-Java
SSM后台管理系统开发实战 5年IT从业经验,目前职位是Java高级工程师.架构师,在gitchat发布有<SSM博客系统开发实战>达人课,CSDN博客专家,博客专栏作者,梦境网项目独立开 ...
- SSM后台管理系统开发实战
一.简介: 通过这个课程带大家从零开发一款功能全面的后台管理系统,包括项目搭建.功能实现到最后的Linux系统部署全过程.本课程使用SpringMVC + Spring + Mybatis作为主体框架 ...
- 前端基础第三天项目 大事件后台管理系统
0. 资源地址 线上 DEMO 项目地址:http://www.escook.cn:8086/ 项目的 API 接口地址: https://www.showdoc.cc/escook?page_id= ...
- 大事件后台管理系统——个人中心
1.创建验证规则函数 个人中心--基本资料:用来修改用户信息 针对表单不同类型的数据需要设定验证规则函数,只有输入正确才能提交表单 form.verify({nickname: function(va ...
- 大事件后台管理系统——文章管理
1.文章类别模块 1.1获取文章分类的列表 利用模版引擎快速渲染表格数据 <!-- 表格数据的模板 --><script type="text/html" id= ...
- 《伴我汽车》后台管理系统开发笔记 下
ok,欢迎来到后半部分, 如果你是直接访问到我的第二部分的话,可以到第一部分下载源码:https://blog.csdn.net/qq_44850489/article/details/1095027 ...
- Vue 2.x 实战之后台管理系统开发(二)
1. 导语 承接上文:Vue 2.x 实战之后台管理系统开发(一) 在上一篇文章中,我详细叙述了如何创建项目框架和引入各种后台常用插件,做好这些准备工作后,我们就可以着手进行页面的开发了.在开发过程中 ...
最新文章
- 【 C 】const 学习笔记
- android realm删除对象,Android Realm-从服务访问Realm对象
- 洛谷P1456 Monkey King
- PowerDesigner设计数据库
- UML建模之部署图(Deployment Diagram)
- mysql索引抽密度_使用python脚本从abaqus输出数据库获取元素密度
- linux sysstat rpm包下载,[20141201]SYSSTAT软件包.txt
- 打开python环境_windows下切换Python运行环境。
- python基础:数据类型一
- 第一百八十四节,jQuery-UI,验证注册表单
- 计算机网络的各层及其协议,计算机网络的体系结构 (architecture) 是计算机网络的各层及其协议的集合...
- SPSS异常值处理(图文+数据集)【SPSS 010期】
- android开发案例1---拦截电话,拯救史迪仔,有序广播
- 如何通过a链接实现图片下载
- 计算机关机键是,电脑关机快捷键是什么
- 弹丸论破2 中文攻略
- Qt教程(新手入门级)
- java imageio_java-ImageIO.write()方法和png
- vs编译错误:在查找预编译头时遇到意外的文件结尾。是否忘记了向源中添加“#include stdafx.h”?
- 疯狂动物消消乐html5游戏在线玩,疯狂动物消消乐
热门文章
- An Energy-Efficient Ant-Based Routing Algorithm for Wireless Sensor Networks (无线传感网中一种基于蚁群算法的能量有效路由)
- Win7虚拟无线AP以及Android手机抓包
- 共享内存:mmap函数实现
- zookeeper结构和命令详解
- Problem E: 校庆
- 如何缩小码农和高手的差距
- Java 调用 Impala - JDBC 调用Impala
- 「技术人生」第2篇:学会分析事物的本质
- 资深技术专家崮德在阿里管理研发团队的实践和思考
- 蚂蚁金服王旭:开源的意义是把社区往前推进一步