PHP+Apache+Mysql+jQuery+Bootsrap开发博客后台管理系统----阿里百秀项目

1,781 阅读5分钟

技术栈

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

page-index 后台首页

布局与展示

主要是左边的nav导航栏,然后最重要的就是数据统计和展示,如上图,这里会把评论commentpost发表的文章做一个简单的统计,这里我用了一个基于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']);
}
  • 这里还有个亮点设计是在用户输入完邮箱后,在头像框应该出现这个邮箱账号的头像,这样不仅让用户有意识这个邮箱是自己的账号没有填错,也让用户体验更好,这点QQTIM在之前就已经做到,体验很好,我们也可以做一下
  • 代码如下
$(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: '&laquo;',
              last: '&raquo;',
              prev: '&lt;',
              next: '&gt;',
              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写的时候,添加新分类的页面也是这个,编辑的界面也是这个,里面的路由请求要设置好
      1. GET请求,默认界面,如上图,用户可以填写新分类并添加。
      1. GET请求带?id=2 参数, 编辑按钮,用户想要修改这条分类数据,把id=2的分类的值传入到左边的表单中,同时按钮变化,如下图
      1. POST请求,默认界面,用户提交表单,把提交的表单保存到数据库,
      1. 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)
        })
})

源码:github.com/wubo49494/B…