技术栈
- 前端
- DOM操作:jQuery
- 模板插件:Art-template
- 分页插件:Pagination
- 后端
- 服务器:Apache
- 交互逻辑:PHP
- 数据库
- Mysql
- 可视化工具:navicat
page-index 后台首页

布局与展示
主要是左边的nav导航栏,然后最重要的就是数据统计和展示,如上图,这里会把评论comment和post发表的文章做一个简单的统计,这里我用了一个基于HTML5的一个插件工具chart.js把统计结果美观且直观的展示在页面上,这里也是对canvas的一个深入学习和使用。
var ctx = document.getElementById('myChart').getContext('2d');
var chart = new Chart(ctx, {
// The type of chart we want to create
type: 'line',
// The data for our dataset
data: {
labels: ["文章", "评论", "已发表的文章", , ],
datasets: [{
label: "数据整理",
backgroundColor: 'rgb(255, 99, 132)',
borderColor: 'rgb(255, 99, 132)',
data: [<?php echo $posts_count ?>, <?php echo $comments_count ?>, <?php echo $posts_count_published ?>],
}]
},
});
page-login 登录页面

登录校验
- 先判断用户有没有输入邮箱密码,格式对不对
- 然后再用户点击登录后,连接Mysql数据,看看数据库里面有没有这个用户的信息,再判断密码是否正确
- 全部正确后,在跳转回首页之前,为了把用户的信息放在首页上,先把当前的用户信息保存到
session上,方面后面直接提取使用
// 载入配置文件
// 给用户找一个箱子(如果你之前有就用之前的,没有就给个新的)
session_start();
require_once '../config.php';
function login () {
// 1. 接受并校验
// 2. 持久化
// 3. 响应
if (empty($_POST['email'])) {
$GLOBALS['message'] = '请填写邮箱';
return;
}
if (empty($_POST['password'])) {
$GLOBALS['message'] = '请填写密码';
return;
}
$email = $_POST['email'];
$password = $_POST['password'];
//当客户端提交过来了完整的表单信息就应该开始对其进行校验
$conn = mysqli_connect(BAIXIU_DB_HOST, BAIXIU_DB_USER, BAIXIU_DB_PASS, BAIXIU_DB_NAME);
if (!$conn) {
exit('<h1>连接数据库失败</h1>');
}
$query = mysqli_query($conn, "select * from users where email = '{$email}' limit 1 ;");
if (!$query) {
$GLOBALS['message'] = '登陆失败,请重试';
return;
}
$user = mysqli_fetch_assoc($query);
if (!$user) {
// 用户名不存在
$GLOBALS['message'] = '邮箱与密码不匹配';
return;
}
// 一般密码是加密存储的,
if ($user['password'] !== $password) {
$GLOBALS['message'] = '邮箱与密码不匹配';
return;
}
// if ($email !== 'admin') {
// $GLOBALS['message'] = '邮箱或者密码错误';
// return;
// }
// if ($password !== '123') {
// $GLOBALS['message'] = '邮箱或者密码错误';
// return;
// }
//存一个登陆标识
// $_SESION['is_logged_in'] = true;
// 为了后续可以直接获取当前登陆用户的信息,这里直接将用户信息放发到 session
$_SESSION['current_login_user'] = $user;
// 一切ok,可以跳转
header('Location: /admin/index.php');
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
login();
}
if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['action']) && $_GET['action'] === 'logout') {
unset($_SESSION['current_login_user']);
}
- 这里还有个亮点设计是在用户输入完邮箱后,在头像框应该出现这个邮箱账号的头像,这样不仅让用户有意识这个邮箱是自己的账号没有填错,也让用户体验更好,这点
QQ和TIM在之前就已经做到,体验很好,我们也可以做一下 - 代码如下
$(function ($){
// 1. 单独作用域
// 2. 确保页面加载后执行
// TODO:在用户输入自己的邮箱过后,页面上展示这个邮箱对应的头像
// -时机:邮箱文本框失去焦点,并且能够拿到文本框中填写的邮箱时,
// -事件:获取这个文本框中填写的邮箱对应的头像地址,展示到上面的img元素上
var emailFormat = /^[a-zA-Z0-9]+@[a-zA-Z0-9]+\.[a-zA-Z0-9]+$/
// 测试字符串是否符合正则对象的方法 emailFormat.test('string')
$('#email').on('blur', function (){
var value = $(this).val()
// 忽略文本框为空或者不是一个邮箱
if (!value || !emailFormat.test(value)) return
// 用户输入了一个合理的邮箱地址
// 获取这个邮箱对应的头像地址,展示到上面的img元素上
// 因为客户端的 JS 无法操作数据库、应该通过 js 发送AJAX 请求告诉服务端的某个接口
// 让这个接口帮助客户端获取头像地址
$.get('/admin/api/avatar.php', { email: value }, function (res){
// 希望 res 能返回用户邮箱的头像地址
if (!res) return
// 展示到上面的 img 元素上
// $('.avatar').fadeOut().attr('src', res).fadeIn()
//空白框图像显示谈谈的消失
$('.avatar').fadeOut(function () {
// 下面的其实时 $(this).on('load').attr
// 加一个 .on('load'). 当前的参数加入后再执行onload里面的淡淡出现
$(this).on('load', function (){
$(this).fadeIn()
}).attr('src', res);
})
})
})
})
page-posts 所有文章页面

模板渲染与数据展示
- 这里最有意思的点是直接用
PHP代码手动搓出了分页的设置和交互逻辑,判断当页面为1的时候,查询数据库的前6条数据,在当前页面为2的时候,发送Ajax请求获取第7-12条数据,并通过回调函数覆盖掉之前的数据。同时分页代码最左边出现个<<图标表示回到第一页。 - 最难的就是处理数据最后的三页
- 分页代码的
active样式要如何设置?当前页>=3页后如何保持居中? - 删掉最后一页的最后一个数据页面应该会怎么变化?
- 如何用
PHP代码把这种变化写出来? - 解决方案核心代码如下
- 分页代码的
// 处理分页参数=====================================
$page = empty($_GET['page']) ? 1 : (int)$_GET['page'];
$page = $page < 1 ? 1 : $page;
$size = 6;
$total_count = (int)baixiu_fetch_one("
select count(1) as count from posts
inner join categories on posts.category_id = categories.id
inner join users on posts.user_id = users.id;")['count'];
$total_pages = (int)ceil($total_count / $size);
$page = $page > $total_pages ? $total_pages : $page;
// 接受筛选参数==================================
$where = '1 = 1';
$search = '';
//分类筛选
if (isset($_GET['category']) && $_GET['category'] !== 'all') {
$where .= ' and posts.category_id = ' . $_GET['category'];
$search .= '&category=' . $_GET['category'];
}
if (isset($_GET['status']) && $_GET['status'] !== 'all') {
$where .= " and posts.status = '{$_GET['status']}'";
$search .= '&status=' . $_GET['status'];
}
// where => " 1=1 and postsss.category_id = 1 and posts.status = 'published'"
// search => "&category=1&status"
// 计算越过多少条
$offset = ($page - 1) * $size;
// 获取全部数据展示====================================
$posts = baixiu_fetch_all("select
posts.id,
posts.title,
posts.created,
posts.status,
categories.name as categories_name,
users.nickname as users_nickname
from posts
inner join categories on posts.category_id = categories.id
inner join users on posts.user_id = users.id
where {$where}
order by posts.created desc
limit {$offset}, {$size};
");
$categories = baixiu_fetch_all("select * from categories");
// 处理分页页码===================================
// 求出最大页码
// 最大页数等于 数据总数 / 每页展示多少条
// 最大页数 $tatal_page = ceil($total / $size)
$visiables = 5;
// 计算最大和最小的展示的页码
$begin = $page - ($visiables - 1) / 2;
$end = $begin + $visiables - 1;
// 重点考虑合理性的问题
// begin > 0 end <= total_pages
// -2 -1 0 1 2 当pages=1的时候 begin=-1,end=3
$begin = $begin < 1 ? 1 : $begin;//确保begin不会小于1
$end = $begin + $visiables - 1; // 因为 1-2=-1 范围会在-1 0 1 2 3 ,但是begin=1了,end不能还是3 ,这样就成了 1 2 3 了
// 48 49 50 51 52 当pages=51的时候,begin=49,end=53
$end = $end > $total_pages ? $total_pages : $end; //确保了 end 不会大于total_pages
$begin = $end - ($visiables - 1); // 因为
//-1 0 1 2 3,当peges=2的时候,begin先=0 ,然后小于1,就等于1,end就=5,但是最大页为3,所以end就=3,然后到上面这条代码,begin又-4变回-1,所以最后要把begin变回1
$begin = $begin < 1 ? 1 : $begin;
page-comments 评论页面

数据展示
- 这里还是使用模板引擎把数据渲染到页面上,在
posts页面中,我们手搓了分页代码,仔细看实在是太麻烦和复杂了,这次我们用更加方便的插件pagination分页插件,可以让我们的分页设置更加简单和轻松
var current_page = 1
function loadPageData (page) {
// 发送 AJAX 请求获取列表所需数据
$.getJSON('/admin/api/comments.php', { page: page}, function (res) {
if (page > res.total_pages) {
loadPageData(res.total_pages)
return
}
$('.pagination').twbsPagination('destroy')
$('.pagination').twbsPagination({
first: '«',
last: '»',
prev: '<',
next: '>',
startPage: page,
totalPages: res.total_pages,
visiablePages: 5,
initiateStartPageClick: false,
// 下面是给页码注册点击事件
onPageClick: function (e,page) {
loadPageData(page)
}
})
// 拿过来的 res 现在是一个数组了
// 请求发送出去啦,不知道什么什么时候回来,回来的话就执行下面的代码,这样就不用等了
console.log(res)
var html = $('#comments_tmpl').render({
suibian: res.comments
})
$('tbody').html(html).fadeIn()
// 将数据渲染到页面上
current_page = page
})
}
loadPageData(current_page)
- 删除的功能主要用到jQuery的on事件绑定,给每一个删除按钮绑定一个删除的事件
$('tbody').on('click', '.btn-delete', function (){
// 删除单条数据的时机
// 1. 拿到需要删除的数据 ID
var id = $(this).parent().parent().data('id')
// 2. 发送一个 AJAX 请求 告诉服务端要删除哪一条具体的数据
$.get('/admin/api/comment-delete.php', {id:id}, function (res) {
if (!res) return
loadPageData(current_page)
})
// 3. 根据服务端返回的删除是否成功决定是否再界面上移除这个元素
})
page-categories 分类页面

逻辑判断
- 这个页面还是对表单操作的细节化
- 左边是普通的表单,右边是把数据库的表单查询渲染出来
- 编辑按钮在渲染的时候就把id值赋予了这个按钮,点击就是把默认值放到左边的表单中,且下面的添加按钮会变成保存和取消,如下图
- 这里要小心的是用
PHP写的时候,添加新分类的页面也是这个,编辑的界面也是这个,里面的路由请求要设置好-
- GET请求,默认界面,如上图,用户可以填写新分类并添加。
-
- GET请求带?id=2 参数, 编辑按钮,用户想要修改这条分类数据,把id=2的分类的值传入到左边的表单中,同时按钮变化,如下图
-
- POST请求,默认界面,用户提交表单,把提交的表单保存到数据库,
-
- POST请求带?id=2参数,用户修改完了id=2的数据,把表单中的内容通过SQL语句进行update数据库,并同步渲染到右边列表中
-

- 具体代码:
function add_category () {
if (empty($_POST['name']) || empty($_POST['slug'])) {
$GLOBALS['message'] = '请完整填写表单';
return;
}
// 接受并保存
$name = $_POST['name'];
$slug = $_POST['slug'];
$rows = baixiu_execute("insert into categories values (null, '{$slug}', '{$name}');");
$GLOBALS['message'] = $rows <= 0 ? '添加失败' : '添加成功';
}
function edit_category () {
global $current_edit_category;
// if (empty($_POST['name']) || empty($_POST['slug'])) {
// $GLOBALS['message'] = '请完整填写表单';
// return;
// }
// 接受并保存
$id = $current_edit_category['id'];
$name = empty($_POST['name']) ? $current_edit_category['name'] : $_POST['name'];
$current_edit_category['name'] = $name;
$slug = empty($_POST['slug']) ? $current_edit_category['slug'] : $_POST['slug'];
$current_edit_category['slug'] = $slug;
$rows = baixiu_execute("update categories set slug = '{$slug}', name = '{$name}' where id = '{$id}';");
$GLOBALS['message'] = $rows <= 0 ? '编辑失败' : '编辑成功';
}
if (empty($_GET['id'])) {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
add_category();
}
} else {
// 客户端通过 URL 传递了一个 ID
// => 客户端是要来拿一个修改数据的表单
// => 需要拿到用户想要修改的数据
$current_edit_category = baixiu_fetch_one('select * from categories where id = ' . $_GET['id']);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
edit_category();
}
}
批量删除功能

- 这里用到很多细节
- 只有当用户选择多选框之后,批量删除按钮才出来
- 用户选择了哪几个数据,批量删除的按钮带的参数?id=2,3,4等就可以跟右边的删除按钮走同一个删除模块删除
- 全选/全不选 有没有更方便的代码实现
- 所有数据左边的框用户都手动勾上后,全选框也要同步勾上(这里没做到) 具体看代码
// 不要重复使用无意义的选择操作,应该采用变量去本地化
$(function ($) {
// 在表格中的任意一个 checkbox 选中状态变化时
var $tbodyCheckboxs = $('tbody input')
var $btnDelete = $('#btn_delete')
var allCheckeds = []
$tbodyCheckboxs.on('change', function () {
// this.dateset['id']
// console.log($(this).attr('data-id'))
// console.log($(this).data('id'))
var id = $(this).data('id')
if ($(this).prop('checked')) {
allCheckeds.includes(id) || allCheckeds.push(id)
} else {
allCheckeds.splice(allCheckeds.indexOf(id), 1)
}
// console.log(allCheckeds)
allCheckeds.length ? $btnDelete.fadeIn() : $btnDelete.fadeOut()
$btnDelete.prop('search', '?id=' + allCheckeds)
})
// # version 1 ============================
// $tbodyCheckboxs.on('change', function () {
// // 有任意个 checkbox 选中就显示,反之隐藏
// var flag = false
// $tbodyCheckboxs.each(function (i,item) {
// // attr 和 prop 的区别
// // attr 访问的是 元素属性,就是HTML页面标签能看到的属性
// // prop 访问的是 元素对应的 DOM 对象的属性,比如 a 元素的里面的 DOM 对象还有很多 host属性,hostname属性等等我们看不到的
// // console.log($(item).prop('checked'))
// if ($(this).prop('checked')) {
// flag =true
// }
// })
// flag ? $btnDelete.fadeIn() : $btnDelete.fadeOut()
// })
// 找一个合适的时机 做一件合适的事情
// 全选和全不选============================
$('thead input').on('change', function () {
// 1. 获取当前选中状态
var checked = $(this).prop('checked')
// 2. 设置给标题中的每一个
// console.log($tbodyCheckboxs)
$tbodyCheckboxs.prop('checked', checked)
})
})