Node项目实战开发-博客系统

Nodejs项目实战开发-博客系统(已完结)

个人博客系统
欢迎访问我的博客~
MaXiaoYu’s Bolg

前言:

开发技术

技术 版本
Node ^14.3.0
ejs ^3.1.3
express ^4.17.1
cookie-session ^1.4.0
mysql ^2.18.1

开发工具

技术 版本
VSCode ^1.47.3.0
MySql ^8.0.12
PhpStudy ^8.1.0.7

第一阶段 博客前台

第二阶段 博客后台

博客前台 项目环境搭建

首先初始化项目

在终端输入命令

npm init

会生成一个 package.json 文件

安装一系列第三方npm模块

npm i express
npm i ejs
npm i mysql

会生成一个 package-lock.json 文件

创建一系列文件

新建index.js文件

/**
 * 入口函数
 */
const express = require("express");
// 创建主应用
const app = express();

// 模板引擎的设置
app.set("view engine", "html");
app.set("views", `${
      
      __dirname}/views`);
app.engine("html", require("ejs").renderFile); // 用ejs模板渲染html

// 静态资源配置
app.use(express.static("static"));

// 监听服务器
app.listen(3000);

修改 package.json 文件

"scripts": {
    
    
  "test": "echo \"Error: no test specified\" && exit 1"
},

自定义启动命令

把上面的代码改成以下代码

"scripts": {
    
    
  "start": "node index.js"
},

新建static目录

静态资源目录

里面存放 css js img 等等的静态资源

新建views目录

模板目录

里面存放模板文件

新建router目录

子应用目录

将我们的程序进行模块化管理,每一个模块都将视为一个子应用

新建middleware目录

中间件目录

与路由器配套的一些操作方法,在路由加载页面之前获取所需要的一些数据

新建model目录

数据模型目录

操作数据库的一些文件

这些文件之间的一些关系

从数据库中进行增删改查,把得到的一些数据返回给中间件,由中间件返回给路由,由路由进行页面的渲染和加载,最后返回给客户端

博客前台 模板文件导入

在views目录和static目录下导入文件

导入完成之后可以把重复的代码提取出来,进行简化处理

<%- include('header.html') -%>
html代码不重复的部分
<%- include('footer.html') -%>

在router目录下新建index.js文件

/**
 * 首页子应用(首页路由)
 */

const express = require("express");
// 首页子应用
const indexApp = express();

indexApp.get("/", (req, res) => {
    
    
  res.render("index");
});

// 把这个子应用导出去
module.exports = indexApp;

然后在myblog下的 index.js 中调用首页子应用

// 调用首页子应用
app.use(/\/(index)?/, require("./router/index"));

启动服务

node index.js

浏览器输入127.0.0.1:3000 出现以下页面说明搭建成功

a6aqIg.png

博客前台 封装数据库操作

在model目录下新建 model.js 文件

const mysql = require("mysql");

/**
 * 数据模型的基类
 * 封装了数据库操作
 */
module.exports = class Model {
    
    
  // 连接对象
  static conn = null;

  /**
   * 数据库连接方法
   */
  static connection() {
    
    
    Model.conn = mysql.createConnection({
    
    
      host: "127.0.0.1",
      user: "root",
      password: "lijiazhao123",
      database: "blog",
    });
    Model.conn.connect((err) => {
    
    
      if (err) {
    
    
        console.log(`数据库连接失败:${
      
      err.message}`);
      }
    });
  }

  /**
   * 关闭数据库连接
   */
  static end() {
    
    
    if (null != Model.conn) {
    
    
      Model.conn.end();
    }
  }

  /**
   * 通用查询方法
   * @param {string} sql 要执行的SQL语句
   * @param {Array} params 给SQL语句的占位符进行赋值的参数数组
   */
  static query(sql, params = []) {
    
    
    return new Promise((resolve, reject) => {
    
    
      this.connection();

      Model.conn.query(sql, params, (err, results) => {
    
    
        if (err) {
    
    
          reject(err);
        } else {
    
    
          resolve(results);
        }
      });

      this.end();
    });
  }
};

在model目录下新建 article.js 文件

/**
 * 文章数据模型
 */
module.exports = class Article extends require("./model") {
    
    
  /**
   *
   * @param {integer} num 条目数
   */
  static getHot(num) {
    
    
    return new Promise((resolve, reject) => {
    
    
      let sql =
        "SELECT id,title,content,`time` FROM article WHERE hot = 1 LIMIT ?";
      this.query(sql, num)
        .then((results) => {
    
    
          resolve(results);
        })
        .catch((err) => {
    
    
          console.log(`获取热门文章失败:${
      
      err.message}`);
          reject(err);
        });
    });
  }
};

在middleware目录下新建 article.js 文件

const Article = require("../model/article");

/**
 * 文章中间件
 */
module.exports = {
    
    
  /**
   * 获取热门文章
   */
  getHot: (req, res, next) => {
    
    
    Article.getHot(3)
      .then((results) => {
    
    
        req.hots = results;
        next();
      })
      .catch((err) => {
    
    
        next(err);
      });
  },
};

再由中间件返回给路由

将router目录下的 index.js 进行修改

/**
 * 首页子应用(首页路由)
 */

const express = require("express");
const article = require("../middleware/article");

// 首页子应用
const indexApp = express();

indexApp.get("/", [article.getHot], (req, res) => {
    
    
  res.render("index", {
    
     hots: req.hots });
});

// 把这个子应用导出去
module.exports = indexApp;

启动服务显示以下页面

acXJzD.png

发现这个时间的格式有点别扭,修改为本地时间

<%= hot.time.toLocaleString() %>

发现这个内容有点别扭

<%= hot.content.replace(/<[^>]+>/g,"").substring(0,100) %>

修改后如下图:

agNIp9.png

博客前台 首页最新博文

数据库查询

在model目录下的 article.js 文件中新增以下方法

/**
  * 获取文章列表
  */
  static getList() {
    
    
    return new Promise((resolve, reject) => {
    
    
      let sql =
        "SELECT id,title,content,`time` FROM article ORDER BY time DESC";
      this.query(sql)
        .then((results) => {
    
    
          resolve(results);
        })
        .catch((err) => {
    
    
          console.log(`获取文章列表失败:${
      
      err.message}`);
          reject(err);
        });
    });
  }

把查询到的数据交给中间件

在middleware目录下的 article.js 文件中新增以下方法

/**
  * 获取最新文章
  */
  getList: (req, res, next) => {
    
    
    Article.getList()
      .then((results) => {
    
    
        req.articles = results;
        next();
      })
      .catch((err) => {
    
    
        next(err);
      });
  },

在由中间件交给路由渲染到页面中

在router目录下的 index.js 文件中修改以下代码

indexApp.get("/", [article.getHot, article.getList], (req, res) => {
    
    
  let {
    
     hots, articles } = req;
  res.render("index", {
    
     hots, articles });
});

开启服务,出现以下页面

a2sUVs.png

博客前台 导航条

数据库查询

在model目录下新建 category.js 文件

/**
 * 文章类目数据模型
 */
module.exports = class Category extends require("./model") {
    
    
  /**
   * 获取文章类目列表
   */
  static getList() {
    
    
    return new Promise((resolve, reject) => {
    
    
      let sql = "SELECT id,`name` FROM category ORDER BY `index` DESC";
      this.query(sql)
        .then((results) => {
    
    
          resolve(results);
        })
        .catch((err) => {
    
    
          console.log(`获取文章类目列表失败:${
      
      err.message}`);
          reject(err);
        });
    });
  }
};

把查询到的数据交给中间件

在middlewar目录下新建 category.js 文件

const Category = require("../model/category");

/**
 * 文章类目中间件
 */
module.exports = {
    
    
  /**
   * 获取文章类目列表
   */
  getList: (req, res, next) => {
    
    
    Category.getList()
      .then((results) => {
    
    
        req.categories = results;
        next();
      })
      .catch((err) => {
    
    
        next(err);
      });
  },
};

在由中间件交给路由渲染到页面中

修改router目录下的 index.js 文件

/**
 * 首页子应用(首页路由)
 */

const express = require("express");
const article = require("../middleware/article");
const category = require("../middleware/category");

// 首页子应用
const indexApp = express();

indexApp.get(
  "/",
  [article.getHot, article.getList, category.getList],
  (req, res) => {
    
    
    let {
    
     hots, articles, categories } = req;
    res.render("index", {
    
     hots, articles, categories });
  }
);

// 把这个子应用导出去
module.exports = indexApp;

修改views下的 header.html 文件

用ejs模板来显示路由渲染过来的数据

<% categories.forEach(category => { %>
    <li class="nav-item">
        <a class="nav-link" href="#"><%= category.name %> </a>
    </li>
<% }) %>

启动服务。显示页面如下

a2XRwd.png

博客前台 文章列表

在model目录下的 article.js 文件中新增方法

/**
   * 获取指定类目下的文章列表
   * @param {integer} id 类目编号
   */
  static getListByCategoryId(id) {
    
    
    return new Promise((resolve, reject) => {
    
    
      let sql =
        "SELECT id,title,content,`time` FROM article WHERE category_id = ? ORDER BY time DESC";
      this.query(sql, id)
        .then((results) => {
    
    
          resolve(results);
        })
        .catch((err) => {
    
    
          console.log(`获取指定类目下的文章列表失败:${
      
      err.message}`);
          reject(err);
        });
    });
  }

在middleware目录下的 article.js 文件新增属性

/**
   * 获取指定类目下的文章
   */
  getListByCategoryId: (req, res, next) => {
    
    
    /**
     * http://127.0.0.1:3000/article/list/1
     * 获取路由下的id - 此时id为1
     */
    let id = req.params.id;
    Article.getListByCategoryId(id)
      .then((results) => {
    
    
        req.articles = results;
        next();
      })
      .catch((err) => {
    
    
        next(err);
      });
  },

在router目录下新建 article.js 文件

/**
 * 文章子应用
 */
const express = require("express");
const article = require("../middleware/article");
const category = require("../middleware/category");

// 文章子应用
const articleApp = express();

articleApp.get(
  "/list/:id",
  [article.getListByCategoryId, category.getList],
  (req, res) => {
    
    
    let {
    
     articles, categories } = req;
    res.render("list", {
    
     articles, categories });
  }
);

module.exports = articleApp;

修改views目录下的 list.html 文件

<% articles.forEach(article => { %>
    <div class="col my-3">
      <div class="card h-100">
        <img src="holder.js/100px150" class="card-img-top" alt="..." />
        <div class="card-body">
          <h5 class="card-title"><%= article.title %></h5>
          <p class="card-text">
            <small class="text-muted"
              ><%= article.time.toLocaleString() %></small
            >
          </p>
          <p class="card-text">
            <%= article.content.replace(/<[^>]+>/g,"").substring(0,100) %>...
          </p>
          <a href="#" class="stretched-link"></a>
        </div>
      </div>
    </div>
<% }) %>

最后在myblog目录下的 index.js 入口函数中新增

app.use("/article", require("./router/article"));

启动服务,浏览器访问 http://127.0.0.1:3000/article/list/1 ,出现以下页面

aRnmCV.png

虽然现在输入url网址能够访问到指定类目列表,但是点击类目列表却不能访问

这时候应该去 header.html 模板文件中修改 href 路由

<% categories.forEach(category => { %>
    <li class="nav-item">
        <a class="nav-link" href="/article/list/<%= category.id %> "
           ><%= category.name %>
        </a>
    </li>
<% }) %>

然后就是修改当前栏目后面显示的数据

通过id查找数据库中的name,然后进行数据显示

在model目录下的 category.js 文件中新增方法

/**
   * 获取指定编号的类目详情
   * @param {integer} id 类目编号
   */
  static getCategoryById(id) {
    
    
    return new Promise((resolve, reject) => {
    
    
      let sql = "SELECT id,`name`,`index` FROM category WHERE id = ?";
      this.query(sql, id)
        .then((results) => {
    
    
          resolve(results[0]);
        })
        .catch((err) => {
    
    
          console.log(`获取指定编号的类目详情失败:${
      
      err.message}`);
          reject(err);
        });
    });
  }

在middleware目录下新增属性

/**
   * 获取指定的类目详情
   */
  getCategoryById: (req, res, next) => {
    
    
    let id = req.params.id;
    Category.getCategoryById(id)
      .then((results) => {
    
    
        req.category = results;
        next();
      })
      .catch((err) => {
    
    
        next(err);
      });
  },

修改router目录下的 article.js 文件

articleApp.get(
  "/list/:id",
  [article.getListByCategoryId, category.getList, category.getCategoryById],
  (req, res) => {
    
    
    let {
    
     articles, categories, category } = req;
    res.render("list", {
    
     articles, categories, category });
  }
);

修改views目录下的 list.html 模板文件

<h2 class="mb-4">当前栏目:<%= category.name %></h2>

启动服务,出现以下页面

aR3ffA.png

博客前台 文章搜索

在model目录下的 article.js 文件下新增方法

/**
   * 获取指定关键词的文章列表
   * @param {integer} keyword 搜索内容
   */
  static getListByKeyword(keyword) {
    
    
    return new Promise((resolve, reject) => {
    
    
      let sql =
        "SELECT id,title,content,`time` FROM article WHERE title LIKE ? ORDER BY time DESC";
      this.query(sql, `%${
      
      keyword}%`)
        .then((results) => {
    
    
          resolve(results);
        })
        .catch((err) => {
    
    
          console.log(`获取指定关键词的文章列表失败:${
      
      err.message}`);
          reject(err);
        });
    });
  }

在middleware目录下的 article.js 文件中新增属性

/**
   * 获取指定类目下的文章
   */
  getListByKeyword: (req, res, next) => {
    
    
    /**
     * 要获取表单的keyword
     */
    let keyword = req.query.keyword;
    Article.getListByKeyword(keyword)
      .then((results) => {
    
    
        req.articles = results;
        next();
      })
      .catch((err) => {
    
    
        next(err);
      });
  },

在router目录下新建文件 search.js

/**
 * 搜索子应用
 */
const express = require("express");
const article = require("../middleware/article");
const category = require("../middleware/category");

// 文章子应用
const searchApp = express();

searchApp.get("/", [article.getListByKeyword, category.getList], (req, res) => {
    
    
  let {
    
     articles, categories } = req;
  res.render("search", {
    
     articles, categories, keyword: req.query.keyword });
});

module.exports = searchApp;

修改views目录下的 search.html 模板文件

<%- include("header.html") -%>

<div class="container mt-5">
  <h2 class="mb-4">搜索结果(搜索词:<%= keyword %> )</h2>
  <div class="row row-cols-4">
    <% articles.forEach(article => { %>
    <div class="col my-3">
      <div class="card h-100">
        <img src="holder.js/100px150" class="card-img-top" alt="..." />
        <div class="card-body">
          <h5 class="card-title"><%= article.title %></h5>
          <p class="card-text">
            <small class="text-muted"
              ><%= article.time.toLocaleString() %></small
            >
          </p>
          <p class="card-text">
            <%= article.content.replace(/<[^>]+>/g,"").substring(0,100) %>...
          </p>
          <a href="#" class="stretched-link"></a>
        </div>
      </div>
    </div>
    <% }) %>
  </div>
</div>

<%- include('footer.html') -%>

修改views目录下的 header.html 文件的form表单

<form
class="form-inline my-2 my-lg-0 ml-5"
method="get"
action="/search"
>
<input
class="form-control mr-sm-2"
name="keyword"
type="search"
placeholder="请输入关键词..."
/>
<button class="btn btn-outline-success my-2 my-sm-0" type="submit">
搜索
</button>
</form>

启动服务,搜索css出现以下页面

aRwkNV.png

博客前台 文章详情

在router目录下新建 article.js 文件

// 文章详情页
articleApp.get("/:id", (req, res) => {
    
    
  let {
    
     categories } = req;
  res.render("article", {
    
     categories });
});

启动服务,在浏览器输入url网址 http://127.0.0.1:3000/article/1 会出现以下页面

af4OZq.png

在model目录下的 article.js 文件中新增方法

/**
   * 获取指定文章的详情
   * @param {integer} id 文章编号
   */
  static getArticleById(id) {
    
    
    return new Promise((resolve, reject) => {
    
    
      let sql =
        "SELECT a.id,a.title,a.content,a.`time`,a.hits,a.category_id,c.`name` FROM article a,category c WHERE a.id = ? AND a.category_id = c.id";
      this.query(sql, id)
        .then((results) => {
    
    
          resolve(results[0]);
        })
        .catch((err) => {
    
    
          console.log(`获取指定文章的详情失败:${
      
      err.message}`);
          reject(err);
        });
    });
  }

在middleware目录下的 article.js 文件中新增属性

/**
   * 获取指定文章的详情
   */
  getArticleById: (req, res, next) => {
    
    
    let id = req.params.id;
    Article.getArticleById(id)
      .then((results) => {
    
    
        req.article = results;
        next();
      })
      .catch((err) => {
    
    
        next(err);
      });
  },

修改router目录下的 article.js 文件

// 文章详情页
articleApp.get("/:id", [article.getArticleById], (req, res) => {
    
    
  let {
    
     categories, article } = req;
  res.render("article", {
    
     categories, article });
});

修改views目录下的 article.html 文件

<%- include("header.html") -%>
<div class="container mt-4">
  <nav>
    <ol class="breadcrumb bg-white">
      <li class="breadcrumb-item"><a href="/">首页</a></li>
      <li class="breadcrumb-item">
        <a href="/article/list/<%= article.category_id %>"
          ><%= article.name %></a
        >
      </li>
      <li class="breadcrumb-item active"><%= article.title %></li>
    </ol>
  </nav>
  <div class="card">
    <div class="card-header bg-white">
      <h3 class="card-title m-0"><%= article.title %></h3>
      <p class="text-muted small mt-2 m-0">
        <span class="mr-3">发表时间:<%= article.time.toLocaleString() %></span>
        <span class="mr-1">点击:<%= article.hits %></span>
      </p>
    </div>
    <!-- 注意:下面用的是 '-' 而不是'=' '-'可以解析HTML代码  -->
    <div class="card-body"><%- article.content %></div>
    <div class="card-footer bg-white border-0">
      <span class="badge badge-pill">标签1</span>
      <span class="badge badge-pill">标签2</span>
      <span class="badge badge-pill">标签3</span>
      <span class="badge badge-pill">标签4</span>
      <span class="badge badge-pill">标签5</span>
    </div>
  </div>
  <nav>
    <ul class="pagination mt-3">
      <li class="page-item">
        <a class="page-link" href="#">上一篇:文章标题</a>
      </li>
      <li class="page-item ml-auto">
        <a class="page-link" href="#">下一篇:文章标题</a>
      </li>
    </ul>
  </nav>
</div>

<%- include('footer.html') -%>

注意:<%- article.content %> 用 ‘-’ 可以解析HTML代码

然后更改各文件路由

按需求来更改路由即可

参考路由

href="/article/<%= article.id %>"

启动服务,基本完成文章跳转功能

博客前台 文章标签显示

在model目录下新建 tab.js 文件

/**
 * 标签数据模型
 */
module.exports = class Tab extends require("./model") {
    
    
  /**
   * 获取指定文章的标签列表
   * @param {integer} id 文章编号
   */
  static getListByArticleId(id) {
    
    
    return new Promise((resolve, reject) => {
    
    
      let sql = "SELECT id,`name` FROM tabs WHERE article_id = ?";
      this.query(sql, id)
        .then((results) => {
    
    
          resolve(results);
        })
        .catch((err) => {
    
    
          console.log(`获取指定文章的标签列表失败:${
      
      err.message}`);
          reject(err);
        });
    });
  }
};

在middleware目录下的 article.js 文件下新增属性

/**
   * 获取指定文章的标签列表
   */
  getListByArticleId: (req, res, next) => {
    
    
    let id = req.params.id;
    Tab.getListByArticleId(id)
      .then((results) => {
    
    
        req.tabs = results;
        next();
      })
      .catch((err) => {
    
    
        next(err);
      });
  },

修改router目录下的 article.js 文件

// 文章详情页
articleApp.get(
  "/:id",
  [article.getArticleById, article.getListByArticleId],
  (req, res) => {
    
    
    let {
    
     categories, article, tabs } = req;
    res.render("article", {
    
     categories, article, tabs });
  }
);

修改views目录下的 article.js 文件

<div class="card-footer bg-white border-0">
    <% tabs.forEach(tab => {
    
     %>
    <span class="badge badge-pill"><%= tab.name %> </span>
    <% }) %>
</div>

开启服务,出现以下页面

ahm6LF.png

博客前台 上一篇下一篇

在model目录下的 article.js 文件中新建两个方法

/**
   * 获取上一篇文章
   * @param {integer} id 当前文章编号
   */
  static getPrevArticle(id) {
    
    
    return new Promise((resolve, reject) => {
    
    
      let sql =
        "SELECT id,title FROM article WHERE id < ? ORDER BY id DESC LIMIT 1";
      this.query(sql, id)
        .then((results) => {
    
    
          resolve(results[0]);
        })
        .catch((err) => {
    
    
          console.log(`获取上一篇文章失败:${
      
      err.message}`);
          reject(err);
        });
    });
  }

  /**
   * 获取下一篇文章
   * @param {integer} id 当前文章编号
   */
  static getNextArticle(id) {
    
    
    return new Promise((resolve, reject) => {
    
    
      let sql =
        "SELECT id,title FROM article WHERE id > ? ORDER BY id ASC LIMIT 1";
      this.query(sql, id)
        .then((results) => {
    
    
          resolve(results[0]);
        })
        .catch((err) => {
    
    
          console.log(`获取下一篇文章失败:${
      
      err.message}`);
          reject(err);
        });
    });
  }

在middleware目录下的 article.js 文件中新增两个属性

/**
   * 获取上一篇文章
   */
  getPrevArticle: (req, res, next) => {
    
    
    let id = req.params.id;
    Article.getPrevArticle(id)
      .then((results) => {
    
    
        req.prev = results;
        next();
      })
      .catch((err) => {
    
    
        next(err);
      });
  },
  /**
   * 获取下一篇文章
   */
  getNextArticle: (req, res, next) => {
    
    
    let id = req.params.id;
    Article.getNextArticle(id)
      .then((results) => {
    
    
        req.next = results;
        next();
      })
      .catch((err) => {
    
    
        next(err);
      });
  },

修改router目录下的 article.js 文件

// 文章详情页
articleApp.get(
  "/:id",
  [
    article.getArticleById,
    article.getListByArticleId,
    article.getPrevArticle,
    article.getNextArticle,
  ],
  (req, res) => {
    
    
    let {
    
     categories, article, tabs, prev, next } = req;
    res.render("article", {
    
     categories, article, tabs, prev, next });
  }
);

修改views目录下的 article.html 文件

<nav>
    <ul class="pagination mt-3">
      <% if (prev) { %>
      <!-- 如果上一篇文章存在才显示上一篇文章 -->
      <li class="page-item">
        <a class="page-link" href="/article/<%= prev.id %>"
          >上一篇:<%= prev.title %></a
        >
      </li>
      <% } %>
      <!-- 如果下一篇文章存在才显示下一篇文章 -->
      <% if (next) { %>
      <li class="page-item ml-auto">
        <a class="page-link" href="/article/<%= next.id %>"
          >下一篇:<%= next.title %></a
        >
      </li>
      <% } %>
    </ul>
  </nav>

开启服务,页面完成以下效果

ah8kRJ.gif

博客前台 用户登录

首先在router目录下新建 login.js 文件

/**
 * 登录子应用(首页路由)
 */

const express = require("express");

// 登录子应用
const loginApp = express();

// 加载登录页面
loginApp.get("/", (req, res) => {
    
    
  res.render("login");
});

// 把这个子应用导出去
module.exports = loginApp;

在myblog目录下的 index.js 文件中调用登录子应用

// 调用登录子应用
app.use("/login", require("./router/login"));

在model目录下新建 user.js 文件

/**
 * 用户数据模型
 */
module.exports = class User extends require("./model") {
    
    
  /**
   * 用户登录
   * @param {string} username 登录账号
   * @param {string} password 登录密码
   */
  static login(username, password) {
    
    
    return new Promise((resolve, reject) => {
    
    
      let sql =
        "SELECT id,username FROM `user` WHERE username=? AND `password`=?";
      this.query(sql, [username, password])
        .then((results) => {
    
    
          resolve(results[0]);
        })
        .catch((err) => {
    
    
          console.log(`登录失败:${
      
      err.message}`);
          reject(err);
        });
    });
  }
};

因为用户登录不用中间件,所以直接在router目录下的 login.js 文件中实现登录操作

// 实现登录操作
loginApp.post("/", (req, res, next) => {
    
    
  let {
    
     username, password } = req.body;
  User.login(username, password)
    .then((results) => {
    
    
      if (results) {
    
    
        res.redirect("/");
      } else {
    
    
        res.render("login", {
    
     msg: "登录失败!用户名或密码错误" });
      }
    })
    .catch((err) => {
    
    
      next(err);
    });
});

把登录失败的数据映射到页面上

修改在views目录下的 login.html 文件

<div class="text-danger col offset-2"><%= msg %></div>

在myblog下的 index.js 文件中进行post请求处理

// POST请求处理
app.use(express.urlencoded({
    
     extended: true }));

开启服务,出现以下效果

[ahcP2R.gif

博客前台 Session与退出

安装cookie-session模块

npm i cookie-session

在myblog下的 index.js 文件中引入cookie-session

const session = require("cookie-session");

配置session

// SESSION配置
app.use(
  session({
    
    
    keys: ["secret"],
    maxAge: 1000 * 60 * 30, // cookie的生命周期
  })
);

修改router目录下的 login.js 文件的登录操作功能

用户登录后会把从数据库查询到的结果results保存到session的user中

// 实现登录操作
loginApp.post("/", (req, res, next) => {
    
    
  let {
    
     username, password } = req.body;
  User.login(username, password)
    .then((results) => {
    
    
      if (results) {
    
    
        // session存储(key=value)
        req.session.user = results;
        res.redirect("/");
      } else {
    
    
        res.render("login", {
    
     msg: "登录失败!用户名或密码错误" });
      }
    })
    .catch((err) => {
    
    
      next(err);
    });
});

在middleware目录下新建 auth.js 文件

/**
 * 权限中间件
 */
module.exports = {
    
    
  /**
   * 从session中读取用户
   */
  getUser: (req, res, next) => {
    
    
    // 从session中读取数据
    req.user = req.session.user;
    next();
  },
};

article.jsindex.jssearch.js 文件中都加入user,目的是为了使未登录的用户不能访问这些页面。

const auth = require("../middleware/auth");

articleApp.use(category.getList, auth.getUser);

// 文章列表页
articleApp.get(
  "/list/:id",
  [article.getListByCategoryId, category.getCategoryById],
  (req, res) => {
    
    
    let {
    
     articles, categories, category, user } = req;
    res.render("list", {
    
     articles, categories, category, user });
  }
);

// 文章详情页
articleApp.get(
  "/:id",
  [
    article.getArticleById,
    article.getListByArticleId,
    article.getPrevArticle,
    article.getNextArticle,
  ],
  (req, res) => {
    
    
    let {
    
     categories, article, tabs, prev, next, user } = req;
    res.render("article", {
    
     categories, article, tabs, prev, next, user });
  }
);
const auth = require("../middleware/auth");

indexApp.use(auth.getUser);

// 加载首页页面
indexApp.get(
  "/",
  [article.getHot, article.getList, category.getList],
  (req, res) => {
    
    
    let {
    
     hots, articles, categories, user } = req;
    res.render("index", {
    
     hots, articles, categories, user });
  }
);
const auth = require("../middleware/auth");

searchApp.use(auth.getUser);

searchApp.get("/", [article.getListByKeyword, category.getList], (req, res) => {
    
    
  let {
    
     articles, categories, user } = req;
  res.render("search", {
    
    
    articles,
    categories,
    keyword: req.query.keyword,
    user,
  });
});

修改views目录下的 header.html 文件,加入判断

如果user存在,说明已登录,则显示用户数据

如果user不存在,说明未登录,则显示登录

<ul class="navbar-nav ml-auto">
    <% if (user) { %>
        <li class="nav-item dropdown">
            <a
               class="nav-link dropdown-toggle"
               href="#"
               id="navbarDropdown"
               data-toggle="dropdown"
               >
                <%= user.username %>
                    </a>
                <div class="dropdown-menu">
                    <a class="dropdown-item" href="/user/logout">退出</a>
                </div>
                </li>
            <% } else { %>
                <li class="nav-item">
                    <a href="/login" class="nav-link">登录</a>
        </li>
   <% } %>
</ul>

启动服务,用户未登录显示以下效果

aho8pT.png

用户已登录显示以下效果

ahowA1.png

在myblog目录下的 index.js 文件中实现退出登录

// 退出登录
app.get("/user/logout", (req, res) => {
    
    
  req.session.user = null;
  res.render("login", {
    
     msg: "退出成功" });
});

启动服务,实现以下效果

ah7SII.gif

博客后台 功能介绍与模板整合

在views目录下新建admin后台管理目录

把需要的资源复制进来

同样,在static目录下新建admin目录

把需要的资源复制进来

pv.json 文件复制到myblog目录下

博客后台 模板管理

为了方便,把代码重复的部分提取到 header.html 模板中

<nav class="navbar navbar-expand-sm navbar-dark bg-dark">
  <a class="navbar-brand" href="#">
    <img src="./static/img/logo.png" width="30" height="30" alt="" />
    个人中心
  </a>
  <div class="navbar-nav">
    <a href="../index.html" class="nav-link">博客首页</a>
  </div>

  <ul class="navbar-nav ml-auto">
    <li class="nav-item dropdown">
      <a
        class="nav-link dropdown-toggle"
        href="#"
        id="navbarDropdown"
        data-toggle="dropdown"
      >
        admin
      </a>
      <div class="dropdown-menu dropdown-menu-right">
        <a class="dropdown-item" href="#">个人中心</a>
        <a class="dropdown-item" href="/user/logout">退出</a>
      </div>
    </li>
  </ul>
</nav>

然后用ejs模板引入即可

在router目录下新建admin目录,在router/admin目录下新建 index.js 文件

/**
 * 后台首页
 */
const express = require("express");
const indexApp = express();

indexApp.get("/", (req, res) => {
    
    
  res.render("admin/index");
});

module.exports = indexApp;

在myblog目录下的 index.js 文件中调用后台首页

// 调用后台首页
app.use(/\/admin\/(index)?/, require("./router/admin/index"));

在middleware目录下的 auth.js 文件中新增属性

/**
   * 是否允许用户进入后台管理页
   */
  allowToAdmin: (req, res, next) => {
    
    
    let user = req.session.user;
    if (user) {
    
    
      req.user = user;
      next();
    } else {
    
    
      res.redirect("/login");
    }
  },

myblog/index.js 中加上进入后台的权限验证

// 进入后台的权限验证
app.use("/admin/?*", require("./middleware/auth").allowToAdmin);

这样写就可以使所有的后台页面都需要登录之后才能访问

修改 views/admin/header.html 文件

<nav class="navbar navbar-expand-sm navbar-dark bg-dark">
  <a class="navbar-brand" href="#">
    <img src="/admin/img/logo.png" width="30" height="30" alt="" />
    个人中心
  </a>
  <div class="navbar-nav">
    <a href="/" class="nav-link">博客首页</a>
  </div>

  <ul class="navbar-nav ml-auto">
    <li class="nav-item dropdown">
      <a
        class="nav-link dropdown-toggle"
        href="#"
        id="navbarDropdown"
        data-toggle="dropdown"
      >
        <%= user.username %>
      </a>
      <div class="dropdown-menu dropdown-menu-right">
        <a class="dropdown-item" href="/admin">个人中心</a>
        <a class="dropdown-item" href="/user/logout">退出</a>
      </div>
    </li>
  </ul>
</nav>

views/header.html 中新增个人中心

<div class="dropdown-menu">
    <a class="dropdown-item" href="/admin">个人中心</a>
    <a class="dropdown-item" href="/user/logout">退出</a>
</div>

博客后台 获取最后登录时间

model/user.js 文件中新增方法

/**
   * 最后一次登陆的时间
   */
  static lastLoginTime() {
    
    
    return new Promise((resolve, reject) => {
    
    
      let sql =
        "SELECT time FROM log WHERE handle = '登录' ORDER BY time DESC LIMIT 1";
      this.query(sql)
        .then((results) => {
    
    
          resolve(results[0]);
        })
        .catch((err) => {
    
    
          console.log(`登录失败:${
      
      err.message}`);
          reject(err);
        });
    });
  }

在middleware目录下新建 user.js 文件

/**
 * 用户中间件
 */
const User = require("../model/user");

module.exports = {
    
    
  /**
   * 最后一次登录时间
   */
  lastLoginTime: (req, res, next) => {
    
    
    User.lastLoginTime()
      .then((results) => {
    
    
        req.lastLoginTime = results;
        next();
      })
      .catch((err) => {
    
    
        next(err);
      });
  },
};

修改 router/admin/index.js 文件

/**
 * 后台首页
 */
const express = require("express");
const user = require("../../middleware/user");

const indexApp = express();

indexApp.get("/", [user.lastLoginTime], (req, res) => {
    
    
  let {
    
     user, lastLoginTime } = req;
  res.render("admin/index", {
    
     user, lastLoginTime });
});

module.exports = indexApp;

修改 views/admin/index.html 文件

<span>上次登录时间:<%= lastLoginTime.time.toLocaleString() %></span>

博客后台 访问量、博文和类目的统计

在model目录下新建 pv.js 文件

/**
 * 访问量数据模型
 */
module.exports = class PV extends require("./model") {
    
    
  /**
   * 获取总访问量
   */
  static getTotal() {
    
    
    return new Promise((resolve, reject) => {
    
    
      let sql = "SELECT SUM(hits) AS total FROM pv";
      this.query(sql)
        .then((results) => {
    
    
          resolve(results[0].total);
        })
        .catch((err) => {
    
    
          console.log(`获取总访问量失败:${
      
      err.message}`);
          reject(err);
        });
    });
  }
};

model/article.js 文件中新增方法

/**
   * 总博文数
   */
  static getCount() {
    
    
    return new Promise((resolve, reject) => {
    
    
      let sql = "SELECT COUNT(1) AS count FROM article";
      this.query(sql)
        .then((results) => {
    
    
          resolve(results[0].count);
        })
        .catch((err) => {
    
    
          console.log(`获取总博文数失败:${
      
      err.message}`);
          reject(err);
        });
    });
  }

model/category.js 文件中新增方法

/**
   * 总类目数
   */
  static getCount() {
    
    
    return new Promise((resolve, reject) => {
    
    
      let sql = "SELECT COUNT(1) AS count FROM category";
      this.query(sql)
        .then((results) => {
    
    
          resolve(results[0].count);
        })
        .catch((err) => {
    
    
          console.log(`获取总类目数失败:${
      
      err.message}`);
          reject(err);
        });
    });
  }

在middleware目录下新建 pv.js 文件

/**
 * 访问量中间件
 */
const Pv = require("../model/pv");

module.exports = {
    
    
  /**
   * 获取总访问量
   */
  getTotal: (req, res, next) => {
    
    
    Pv.getTotal()
      .then((results) => {
    
    
        req.pvTotal = results;
        next();
      })
      .catch((err) => {
    
    
        next(err);
      });
  },
};

middleware/article.js 文件中新建属性

/**
   * 获取总博文数
   */
  getCount: (req, res, next) => {
    
    
    Article.getCount()
      .then((results) => {
    
    
        req.articleCount = results;
        next();
      })
      .catch((err) => {
    
    
        next(err);
      });
  },

middleware/category.js 文件中新建属性

/**
   * 获取总类目数
   */
  getCount: (req, res, next) => {
    
    
    Category.getCount()
      .then((results) => {
    
    
        req.categoryCount = results;
        next();
      })
      .catch((err) => {
    
    
        next(err);
      });
  },

修改 router/admin/index.js 文件

/**
 * 后台首页
 */
const express = require("express");
const user = require("../../middleware/user");
const pv = require("../../middleware/pv");
const category = require("../../middleware/category");
const article = require("../../middleware/article");

const indexApp = express();

indexApp.get(
  "/",
  [user.lastLoginTime, pv.getTotal, category.getCount, article.getCount],
  (req, res) => {
    
    
    let {
    
     user, lastLoginTime, pvTotal, categoryCount, articleCount } = req;
    res.render("admin/index", {
    
    
      user,
      lastLoginTime,
      pvTotal,
      categoryCount,
      articleCount,
    });
  }
);

module.exports = indexApp;

修改 views/admin/index.html 文件

<div class="card-deck">
    <div class="card text-center">
        <div class="card-header bg-success text-white">总访问量</div>
        <div class="card-body"><%= pvTotal %></div>
        </div>
        <div class="card text-center">
            <div class="card-header bg-info text-white">总博文数</div>
            <div class="card-body"><%= articleCount %></div>
        </div>
        <div class="card text-center">
        <div class="card-header bg-warning text-white">总类目数</div>
        <div class="card-body"><%= categoryCount %></div>
    </div>
</div>

博客后台 获取访问量趋势图数据

model/pv.js 文件中新建获取全部记录的方法

/**
   * 获取全部记录
   */
  static getAll() {
    
    
    return new Promise((resolve, reject) => {
    
    
      let sql = "SELECT time,hits FROM pv ORDER BY time ASC";
      this.query(sql)
        .then((results) => {
    
    
          resolve(results);
        })
        .catch((err) => {
    
    
          console.log(`获取全部记录失败:${
      
      err.message}`);
          reject(err);
        });
    });
  }

middleware/pv.js 文件中新建获取全部记录的属性

/**
   * 获取全部记录
   */
  getAll: (req, res, next) => {
    
    
    Pv.getAll()
      .then((results) => {
    
    
        req.pvs = results;
        next();
      })
      .catch((err) => {
    
    
        next(err);
      });
  },

router/admin/index.js 文件中新建访问量接口

/**
 * 访问量接口
 */
indexApp.get("/pvs", [pv.getAll], (req, res) => {
    
    
  let {
    
     pvs } = req;
  let data = {
    
    };
  data.data = pvs;
  data.start = pvs[0].time;
  data.end = pvs[pvs.length - 1].time;
  res.json(data);
});

static/admin/js/public.js 文件中的url改为 /admin/index/pvs

let url = "/admin/index/pvs";

博客后台 管理导航处理

views/admin 目录下新建 navs.html 文件,是重复代码通用模板

<div class="col-2 bg-white border-right nav-left p-0">
  <div class="row text-center mt-3 px-5">
    <a href="./article/add.html" class="btn btn-primary rounded-0 col"
      >发表博文</a
    >
  </div>
  <div
    class="list-group list-group-flush border-top border-bottom mt-3 text-center"
  >
    <li class="list-group-item list-group-item-action active">
      <a href="./index.html" class="text-decoration-none text-secondary">
        <i class="iconfont icon-home pr-1"></i>个人中心
      </a>
    </li>
    <li class="list-group-item list-group-item-action">
      <a
        href="./article/index.html"
        class="text-decoration-none text-secondary"
      >
        <i class="iconfont icon-neirongguanli pr-1"></i>文章管理
      </a>
    </li>
    <li class="list-group-item list-group-item-action">
      <a
        href="./category/index.html"
        class="text-decoration-none text-secondary"
      >
        <i class="iconfont icon-leimuguanli pr-1"></i>类目管理
      </a>
    </li>
    <li class="list-group-item list-group-item-action">
      <a href="./log/index.html" class="text-decoration-none text-secondary">
        <i class="iconfont icon-log pr-1"></i>查看日志
      </a>
    </li>
    <li class="list-group-item list-group-item-action">
      <a
        href="./account/index.html"
        class="text-decoration-none text-secondary"
      >
        <i class="iconfont icon-zhanghuguanli pr-1"></i>账户管理
      </a>
    </li>
  </div>
</div>

然后按需替换即可

替换语法:

<%- include("navs.html") -%>

router/admin 目录下新建文件

category.js - 后台类目管理

/**
 * 后台类目管理
 */
const express = require("express");
const categoryApp = express();

categoryApp.get("/", (req, res) => {
    
    
  res.render("admin/category/index", {
    
     user: req.user });
});

module.exports = categoryApp;

article.js - 后台文章管理

/**
 * 后台文章管理
 */
const express = require("express");
const articleApp = express();

articleApp.get("/", (req, res) => {
    
    
  res.render("admin/article/index", {
    
     user: req.user });
});

module.exports = articleApp;

log.js - 后台日志管理

/**
 * 后台日志管理
 */
const express = require("express");
const logApp = express();

logApp.get("/", (req, res) => {
    
    
  res.render("admin/log/index", {
    
     user: req.user });
});

module.exports = logApp;

account.js - 后台账户管理

/**
 * 后台账户管理
 */
const express = require("express");
const accountApp = express();

accountApp.get("/", (req, res) => {
    
    
  res.render("admin/account/index", {
    
     user: req.user });
});

module.exports = accountApp;

myblog/index.js 中调用路由

// 调用后台文章管理
app.use("/admin/article", require("./router/admin/article"));
// 调用后台类目管理
app.use("/admin/category", require("./router/admin/category"));
// 调用后台日志管理
app.use("/admin/log", require("./router/admin/log"));
// 调用后台账户管理
app.use("/admin/account", require("./router/admin/account"));

修改 views/admin 目录下的 navs.html 文件中的路由

<div class="col-2 bg-white border-right nav-left p-0">
  <div class="row text-center mt-3 px-5">
    <a href="./article/add.html" class="btn btn-primary rounded-0 col"
      >发表博文</a
    >
  </div>
  <div
    class="list-group list-group-flush border-top border-bottom mt-3 text-center"
  >
    <li class="list-group-item list-group-item-action">
      <a href="/admin/" class="text-decoration-none text-secondary">
        <i class="iconfont icon-home pr-1"></i>个人中心
      </a>
    </li>
    <li class="list-group-item list-group-item-action">
      <a href="/admin/article/" class="text-decoration-none text-secondary">
        <i class="iconfont icon-neirongguanli pr-1"></i>文章管理
      </a>
    </li>
    <li class="list-group-item list-group-item-action">
      <a href="/admin/category/" class="text-decoration-none text-secondary">
        <i class="iconfont icon-leimuguanli pr-1"></i>类目管理
      </a>
    </li>
    <li class="list-group-item list-group-item-action">
      <a href="/admin/log/" class="text-decoration-none text-secondary">
        <i class="iconfont icon-log pr-1"></i>查看日志
      </a>
    </li>
    <li class="list-group-item list-group-item-action">
      <a href="/admin/account/" class="text-decoration-none text-secondary">
        <i class="iconfont icon-zhanghuguanli pr-1"></i>账户管理
      </a>
    </li>
  </div>
</div>

最后在 static/js/public.js 中写个判断

if ($(".list-group-item").length) {
    
    
  let href = location.pathname;
  console.log(href);
  $(`.list-group-item a[href='${
      
      href}']`).parent().addClass("active");
}

开启服务,页面出现以下效果

博客后台 文章列表显示

model/article.js 文件中新建获取指定页文章列表的方法

/**
   * 获取指定页文章列表
   */
  static getPage() {
    
    
    return new Promise((resolve, reject) => {
    
    
      let sql = "SELECT id,title,thumbnail,hot FROM article ORDER BY time DESC";
      this.query(sql)
        .then((results) => {
    
    
          resolve(results);
        })
        .catch((err) => {
    
    
          console.log(`获取指定页文章列表失败:${
      
      err.message}`);
          reject(err);
        });
    });
  }

middleware/article.js 文件中新建获取指定页文章列表的属性

/**
   * 获取指定页的文章列表
   */
  getPage: (req, res, next) => {
    
    
    Article.getPage()
      .then((results) => {
    
    
        req.pageList = results;
        next();
      })
      .catch((err) => {
    
    
        next(err);
      });
  },
};

router/admin/article.js 文件对pageList进行封装处理,为后面的分页操作打基础

/**
 * 后台文章管理
 */
const express = require("express");
const article = require("../../middleware/article");
const articleApp = express();

articleApp.get("/", [article.getPage], (req, res) => {
    
    
  let {
    
     user, pageList } = req;
  let page = {
    
    };
  page.list = pageList;
  res.render("admin/article/index", {
    
     user, page });
});

module.exports = articleApp;

views/admin/article/index.html 中的文件进行修改,用取得的数据库的数据重新渲染页面

<tbody>
    <% page.list.forEach(article => { %>
        <tr>
            <th><%= article.id %></th>
            <td><%= article.title %></td>
            <td>
                <% if (article.thumbnail) { %>
                    <i
                       class="iconfont icon-photo"
                       data-toggle="popover"
                       data-content="<img src='<%= article.thumbnail %>'>"
                       ></i>
                    <% } %>
                        </td>
                    <td>
                        <div class="custom-control custom-switch">
                            <input type="checkbox" class="custom-control-input"
                                   id="hot1" <%= article.hot?'checked':'' %> />
                            <label
                                   class="custom-control-label"
                                   for="hot1"
                                   ></label>
                        </div>
                    </td>
                    <td>
                        <a
                           href="./edit.html"
                           class="text-primary"
                           title="编辑"
                           ><i class="iconfont icon-bianji"></i>编辑</a
                            >
                        <a
                           href="#"
                           onclick="return confirm('确定删除?')"
                           class="text-danger"
                           title="删除"
                           ><i class="iconfont icon-delete"></i>删除</a
                            >
              </td>
          </tr>
     <% }) %>
</tbody>

启动服务,呈现以下效果

博客后台 文章列表分页

router/admin/article.js 文件中对articleCount等的封装

/**
 * 后台文章管理
 */
const express = require("express");
const article = require("../../middleware/article");
const articleApp = express();

articleApp.get("/", [article.getPage, article.getCount], (req, res) => {
    
    
  let {
    
     user, pageList, articleCount } = req;
  let size = 5; // 每页显示5条
  let page = {
    
    };
  page.count = articleCount; // 总页数
  page.total = Math.ceil(page.count / size); // 最大页数
  page.list = pageList;
  page.p = req.query.p ? req.query.p : 1; // 页数
  page.p = page.p > page.total ? page.total : page.p;
  page.p = page.p < 1 ? 1 : page.p;
  res.render("admin/article/index", {
    
     user, page });
});

module.exports = articleApp;

修改 views/admin/index.html 文件

<div class="align-self-center">
  共 <%= page.count %> 条 / 共 <%= page.total %> 页 / 第 <%=page.p %> 页
</div>

开启服务,浏览器访问http://127.0.0.1:3000/admin/article?p=7,页面显示以下效果

model/article.js 文件中新建获取指定页文章列表的方法

/**
   * 获取指定页文章列表
   * @param {integer} start 起始索引
   * @param {integer} size 查询条目数
   */
  static getPage(start, size) {
    
    
    return new Promise((resolve, reject) => {
    
    
      let sql =
        "SELECT id,title,thumbnail,hot FROM article ORDER BY time DESC LIMIT ?,?";
      this.query(sql, [start, size])
        .then((results) => {
    
    
          resolve(results);
        })
        .catch((err) => {
    
    
          console.log(`获取指定页文章列表失败:${
      
      err.message}`);
          reject(err);
        });
    });
  }

middleware/article.js 文件中的getPage属性传上刚加上的参数

/**
   * 获取指定页的文章列表
   */
  getPage: (req, res, next) => {
    
    
    Article.getPage(res.start, res.size)
      .then((results) => {
    
    
        req.pageList = results;
        next();
      })
      .catch((err) => {
    
    
        next(err);
      });
  },

router/admin/article.js 文件进行修改 - 异步操作

/**
 * 后台文章管理
 */
const express = require("express");
const article = require("../../middleware/article");
const articleApp = express();

articleApp.get(
  "/",
  [article.getCount],
  (req, res, next) => {
    
    
    let {
    
     articleCount } = req;
    let size = 5; // 每页显示5条
    req.page = {
    
    };
    req.page.count = articleCount; // 总页数
    req.page.total = Math.ceil(req.page.count / size); // 最大页数
    req.page.p = req.query.p ? req.query.p : 1; // 页数
    req.page.p = req.page.p > req.page.total ? req.page.total : req.page.p;
    req.page.p = req.page.p < 1 ? 1 : req.page.p;

    res.start = (req.page.p - 1) * size;
    res.size = size;

    next();
  },
  [article.getPage],
  (req, res) => {
    
    
    let {
    
     user, pageList, page } = req;
    page.list = pageList;
    res.render("admin/article/index", {
    
     user, page });
  }
);

module.exports = articleApp;

修改 views/admin/article/index.html 文件

<nav
     class="d-flex justify-content-between border-top pt-3 px-3"
     >
    <div class="align-self-center">
        共 <%= page.count %> 条 / 共 <%= page.total %> 页 / 第 <%=
                                                             page.p %> 页
        </div>
        <ul class="pagination mb-0">
            <% if (page.p!=1) { %>
                <li class="page-item">
                    <a
                       class="page-link"
                       href="/admin/article?p=<%= page.p-1 %>"
                       >上一页</a
                        >
                </li>
                <% } %> <% for( let index = 1; index <= page.total;
                    index++ ) { %>
                    <li class="page-item">
                        <a
                           class="page-link"
                           href="/admin/article?p=<%= index %>"
                           ><%= index %></a
                            >
                            </li>
                        <% } %> <% if (page.p!=page.total) { %>
                            <li class="page-item">
                                <a
                                   class="page-link"
                                   href="/admin/article?p=<%= page.p+1 %>"
                                   >下一页</a>
     	</li>
     <% } %>
     </ul>
</nav>

启动服务,如下效果

博客后台 文章列表分页查询

router/admin/article.js 文件中新增类目列表数据

/**
 * 后台文章管理
 */
const express = require("express");
const article = require("../../middleware/article");
const category = require("../../middleware/category");
const articleApp = express();

articleApp.get(
  "/",
  [article.getCount],
  (req, res, next) => {
    
    
    let {
    
     articleCount } = req;
    let size = 5; // 每页显示5条
    req.page = {
    
    };
    req.page.count = articleCount; // 总页数
    req.page.total = Math.ceil(req.page.count / size); // 最大页数
    req.page.p = req.query.p ? req.query.p : 1; // 页数
    req.page.p = req.page.p > req.page.total ? req.page.total : req.page.p;
    req.page.p = req.page.p < 1 ? 1 : req.page.p;

    res.start = (req.page.p - 1) * size;
    res.size = size;

    next();
  },
  [article.getPage, category.getList],
  (req, res) => {
    
    
    let {
    
     user, pageList, page, categories } = req;
    page.list = pageList;
    res.render("admin/article/index", {
    
     user, page, categories });
  }
);

module.exports = articleApp;

修改 views/admin/article/index.html 文件

<form
      action="/admin/article"
      class="form-inline mb-3"
      method="get"
      >
    <input type="hidden" name="p" value="<%= page.p %>" />
    <div class="form-group">
        <label for="category">类目:</label>
        <select
                name="category"
                id="category"
                class="form-control-sm"
                >
            <option value="-1">全部</option>
            <% categories.forEach(category => { %>
                <option value="<%= category.id %>"
                        ><%= category.name %>
                    </option>
                    <% }) %>
                        </select>
                </div>
            <div class="form-group ml-3">
                <label for="hot">热门:</label>
                <select name="hot" id="hot" class="form-control-sm">
                    <option value="-1">全部</option>
                    <option value="1">热门</option>
                    <option value="0">非热门</option>
                </select>
            </div>
            <div class="from-group ml-3">
                <input
                       type="submit"
                       value="筛选"
                       class="btn btn-primary btn-sm"
                       />
            </div>
            <div class="form-group ml-auto">
                <a href="./add.html" class="btn btn-danger btn-sm"
                   >发表博文</a
                    >
            </div>
</form>

修改 model/article.js 文件,附带两个参数category_id, hot

/**
   * 总博文数
   */
  static getCount(category_id, hot) {
    
    
    return new Promise((resolve, reject) => {
    
    
      let sql = "SELECT COUNT(1) AS count FROM article WHERE 1=1";
      sql +=
        category_id != -1 && category_id
          ? ` AND category_id=${
      
      category_id}`
          : "";
      sql += hot != -1 && hot ? ` AND hot=${
      
      hot}` : "";
      this.query(sql)
        .then((results) => {
    
    
          resolve(results[0].count);
        })
        .catch((err) => {
    
    
          console.log(`获取总博文数失败:${
      
      err.message}`);
          reject(err);
        });
    });
  }

  /**
   * 获取指定页文章列表
   * @param {integer} start 起始索引
   * @param {integer} size 查询条目数
   */
  static getPage(start, size, category_id, hot) {
    
    
    return new Promise((resolve, reject) => {
    
    
      let sql = "SELECT id,title,thumbnail,hot FROM article WHERE 1=1";

      sql +=
        category_id != -1 && category_id
          ? ` AND category_id=${
      
      category_id}`
          : "";
      sql += hot != -1 && hot ? ` AND hot=${
      
      hot}` : "";

      sql += " ORDER BY time DESC LIMIT ?,?";
      this.query(sql, [start, size])
        .then((results) => {
    
    
          resolve(results);
        })
        .catch((err) => {
    
    
          console.log(`获取指定页文章列表失败:${
      
      err.message}`);
          reject(err);
        });
    });
  }

再修改 middleware/article.js 文件

/**
   * 获取总博文数
   */
  getCount: (req, res, next) => {
    
    
    Article.getCount(req.query.category_id, req.query.hot)
      .then((results) => {
    
    
        req.articleCount = results;
        next();
      })
      .catch((err) => {
    
    
        next(err);
      });
  },
  /**
   * 获取指定页的文章列表
   */
  getPage: (req, res, next) => {
    
    
    Article.getPage(res.start, res.size, req.query.category_id, req.query.hot)
      .then((results) => {
    
    
        req.pageList = results;
        next();
      })
      .catch((err) => {
    
    
        next(err);
      });
  },

再到路由中进行修改 router/admin/article.js

/**
 * 后台文章管理
 */
const express = require("express");
const article = require("../../middleware/article");
const category = require("../../middleware/category");
const articleApp = express();

articleApp.get(
  "/",
  [article.getCount],
  (req, res, next) => {
    
    
    let {
    
     articleCount } = req;
    let size = 3; // 每页显示5条
    req.page = {
    
    };
    req.page.count = articleCount; // 总页数
    req.page.total = Math.ceil(req.page.count / size); // 最大页数
    req.page.p = req.query.p ? req.query.p : 1; // 页数
    req.page.p = req.page.p > req.page.total ? req.page.total : req.page.p;
    req.page.p = req.page.p < 1 ? 1 : req.page.p;

    res.start = (req.page.p - 1) * size;
    res.size = size;

    next();
  },
  [article.getPage, category.getList],
  (req, res) => {
    
    
    let {
    
     user, pageList, page, categories } = req;
    let {
    
     category_id, hot } = req.query;

    page.list = pageList;
    res.render("admin/article/index", {
    
    
      user,
      page,
      categories,
      category_id,
      hot,
    });
  }
);

module.exports = articleApp;

修改 views/admin/article/index.html 文件

<ul class="pagination mb-0">
                      <% if (page.p!=1) { %>
                      <li class="page-item">
                        <a
                          class="page-link"
                          href="/admin/article?p=<%= page.p-1 %>&category_id=<%= category_id %>&hot=<%= hot %>"
                          >上一页</a
                        >
                      </li>
                      <% } %> <% for( let index = 1; index <= page.total;
                      index++ ) { %>
                      <li class="page-item">
                        <a
                          class="page-link"
                          href="/admin/article?p=<%= index %>&category_id=<%= category_id %>&hot=<%= hot %>"
                          ><%= index %></a
                        >
                      </li>
                      <% } %> <% if (page.p!=page.total) { %>
                      <li class="page-item">
                        <a
                          class="page-link"
                          href="/admin/article?p=<%= page.p+1 %>&category_id=<%= category_id %>&hot=<%= hot %>"
                          >下一页</a
                        >
                      </li>
                      <% } %>
</ul>

博客后台 文章列表-热门推荐

首先在 model/article.js 文件中设置热门方法

/**
   * 设置热门
   * @param {integer} id 文章编号
   * @param {integer} hot 热门状态
   */
  static setHot(id, hot) {
    
    
    return new Promise((resolve, reject) => {
    
    
      let sql = "UPDATE article SET hot = ? WHERE id = ?";
      this.query(sql, [hot, id])
        .then((results) => {
    
    
          resolve(results.affectedRows);
        })
        .catch((err) => {
    
    
          console.log(`设置热门失败:${
      
      err.message}`);
          reject(err);
        });
    });
  }

然后再 middleware/article.js 文件中设置热门推荐属性

/**
   * 设置热门推荐
   */
  setHot: (req, res, next) => {
    
    
    let {
    
     id, hot } = req.query;
    Article.setHot(id, hot)
      .then((results) => {
    
    
        req.affectedRows = results;
        next();
      })
      .catch((err) => {
    
    
        next(err);
      });
  },

router/admin/article.js 文件中设置热门推荐路由

articleApp.get("/sethot", article.setHot, (req, res) => {
    
    
  // 如果受影响的行数大于0,说明数据库更新成功,否则失败
  if (req.affectedRows > 0) {
    
    
    res.json({
    
     code: 1, msg: "设置成功" });
  } else {
    
    
    res.json({
    
     code: 0, msg: "设置失败" });
  }
});

views/admin/article/index.html 文件中进行修改

首先是页面部分

<div class="custom-control custom-switch">
 <input type="checkbox" class="custom-control-input" value="<%= article.id %>"
 id="hot<%= article.id %>" <%= article.hot?'checked':'' %> οnchange="sethot(this.value,this.checked)" />
                            
 <label
   class="custom-control-label"
   for="hot<%= article.id %>"
   ></label>
</div>

其次是JavaScript脚本部分

function sethot(id,hot){
    
    
    $.get("/admin/article/sethot",{
    
    id,hot:hot?1:0},function(res){
    
    
        if(res.code==1){
    
    
            showToasts("成功","设置热门成功")
        }else{
    
    
            showToasts("失败","设置热门失败")
        }
    })
}

启动服务,即可完成页面点击热门推荐按钮对数据库的hot进行修改

博客后台 添加博文-页面显示

router/admin/article.js 文件中新增显示添加博文的方法

// 显示添加博文页
articleApp.get("/add", [category.getList], (req, res) => {
    
    
  let {
    
     user, categories } = req;
  res.render("admin/article/add", {
    
     user, categories });
});

博客后台 图片上传

安装multer

npm i multer

myblog/index.js 文件中引入multer

const multer = require("multer");

上传配置

// 上传配置
const upload = multer({
    
    
  dest: "./static/upload", // 上传文件的存储目录
  limits: {
    
    
    fileSize: 1024 * 1024 * 2, // 单个文件大小限制在2M
  },
});

上传操作

const fs = require("fs");
const path = require("path");

// 上传操作
app.post("/admin/*", upload.single("upload"), (req, res, next) => {
    
    
  // 上传成功后的文件对象
  let {
    
     file } = req;
  if (file) {
    
    
    // file.originalname ==> 文件的原名称
    let extname = path.extname(file.originalname);
    // file.path ==> 上传后的文件路径
    fs.renameSync(file.path, file.path + extname);
    // file.filename ==> 上传后的文件名
    req.uploadUrl = "/upload/" + file.filename + extname;
  }
  next();
});

router/admin/article.js 文件中新增上传图片的路由

// ckeditor 上传
articleApp.post("/ckeditor", (req, res) => {
    
    
  if (req.uploadUrl) {
    
    
    res.json({
    
    
      uploaded: true,
      url: req.uploadUrl,
    });
  } else {
    
    
    res.json({
    
    
      uploaded: false,
      err: {
    
     message: "上传失败" },
    });
  }
});

完成如下效果

博客后台添加博文-数据库写入

model.article.js 中新增添加文章方法

/**
   * 添加文章
   * @param {Object} article 文章对象
   */
  static add(article) {
    
    
    return new Promise((resolve, reject) => {
    
    
      let sql = "INSERT INTO article SET ?";
      this.query(sql, article)
        .then((results) => {
    
    
          resolve(results.insertId);
        })
        .catch((err) => {
    
    
          console.log(`添加文章失败:${
      
      err.message}`);
          reject(err);
        });
    });
  }

middleware/article.js 文件中新增添加文章的属性

/**
   * 添加文章
   */
  add: (req, res, next) => {
    
    
    let {
    
     title, content, hot, category_id } = req.body;
    let article = {
    
    
      title,
      content,
      hot: hot ? 1 : 0,
      category_id: category_id,
      thumbnail: req.uploadUrl ? req.uploadUrl : null,
    };
    Article.add(article)
      .then((results) => {
    
    
        req.insertId = results;
        next();
      })
      .catch((err) => {
    
    
        next(err);
      });
  },

router/admin/article.js 文件中新增添加文章的路由

// 添加文章
articleApp.post("/add", [article.add, category.getList], (req, res) => {
    
    
  let {
    
     user, categories } = req;
  if (req.insertId) {
    
    
    res.render("admin/article/add", {
    
     user, categories, code: 1 });
  } else {
    
    
    res.render("admin/article/add", {
    
     user, categories, code: 2 });
  }
});

启动服务,上传文章到数据库

博客后台 删除博文

model/article.js 文件中新增删除文章的方法

  /**
   * 删除文章
   * @param {integer} id 文章编号
   */
  static del(id) {
    
    
    return new Promise((resolve, reject) => {
    
    
      let sql = "DELETE FROM article WHERE id = ?";
      this.query(sql, id)
        .then((results) => {
    
    
          resolve(results.affectedRows);
        })
        .catch((err) => {
    
    
          console.log(`删除文章失败:${
      
      err.message}`);
          reject(err);
        });
    });
  }

middleware/article.js 文件中新增删除文章的属性

  /**
   * 删除文章
   */
  del: (req, res, next) => {
    
    
    let {
    
     id } = req.query;
    Article.del(id)
      .then((results) => {
    
    
        req.affectedRows = results;
        next();
      })
      .catch((err) => {
    
    
        next(err);
      });
  },

router/admin/article.js 文件中新增删除文章的路由

// 删除文章
articleApp.get("/del", article.del, (req, res) => {
    
    
  if (req.affectedRows > 0) {
    
    
    res.json({
    
     code: 1, msg: "删除成功" });
  } else {
    
    
    res.json({
    
     code: 2, msg: "删除失败" });
  }
});

修改 views/admin/article/index.html 文件

<button
        onclick="del(<%=article.id%>)"
        class="btn btn-link text-danger"
        title="删除"
        ><i class="iconfont icon-delete"></i>删除</button
    >
function del(id){
    
    
    $.getJSON("/admin/article/del",{
    
    id},function(res){
    
    
        if(res.code==1){
    
    
            showToasts("成功","删除成功")
            // 重新加载页面
            setTimeout(function(){
    
    
                location.reload()
            },2000)
        }else{
    
    
            showToasts("失败","删除失败")
        }
    })
}

启动服务,删除文章删除成功

博客后台 编辑博文-显示数据

model/article.js 文件中修改获取指定文章详情的方法

  /**
   * 获取指定文章的详情
   * @param {integer} id 当前文章编号
   */
  static getArticleById(id) {
    
    
    return new Promise((resolve, reject) => {
    
    
      let sql =
        "SELECT a.id,a.title,a.content,a.`time`,a.hits,a.category_id,c.`name`,a.`thumbnail`,a.`hot` FROM article a,category c WHERE a.id = ? AND a.category_id = c.id";
      this.query(sql, id)
        .then((results) => {
    
    
          resolve(results[0]);
        })
        .catch((err) => {
    
    
          console.log(`获取指定文章的详情失败:${
      
      err.message}`);
          reject(err);
        });
    });
  }

router/admin/article.js 文件中新增路由

// 文章编辑
articleApp.get(
  "/edit/:id",
  [category.getList, article.getArticleById],
  (req, res) => {
    
    
    let {
    
     user, categories, article } = req;
    res.render("admin/article/edit", {
    
     user, categories, article });
  }
);

修改 views/admin/article/edit.html 文件

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, shrink-to-fit=no"
    />
    <title>首页</title>
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
    />
    <link
      rel="stylesheet"
      href="//at.alicdn.com/t/font_1658828_vud4w73neg.css"
    />
    <link rel="stylesheet" href="/admin/css/style.css" />
  </head>

  <body class="bg-light">
    <%- include("../header.html") -%>
    <div class="container-fluid vh-100">
      <div class="row h-100">
        <%- include("../navs.html") -%>
        <div class="col-10">
          <div class="p-3 border mb-3 bg-white">
            <h4 class="mb-0">文章管理</h4>
          </div>
          <div class="row mt-3">
            <div class="col">
              <div class="card">
                <div
                  class="card-header bg-primary text-white d-flex justify-content-between"
                >
                  <h6 class="mb-0 align-self-center">编辑文章</h6>
                </div>
                <div class="card-body">
                  <form
                    action="/admin/article/edit"
                    method="POST"
                    enctype="multipart/form-data"
                  >
                    <input type="hidden" name="id" value="<%= article.id %>" />
                    <div class="form-row">
                      <label
                        for="title"
                        class="col-2 text-center col-form-label"
                        >标题:</label
                      >
                      <input
                        type="text"
                        class="col-8 form-control"
                        name="title"
                        id="title"
                        value="<%= article.title %> "
                        required
                      />
                      <span class="col-2 col-form-label text-danger"
                        >* 必填项</span
                      >
                    </div>
                    <div class="form-row mt-3">
                      <label
                        for="category_id"
                        class="col-2 text-center col-form-label"
                        >类目:</label
                      >
                      <select
                        name="category_id"
                        id="category_id"
                        class="col-8 form-control"
                      >
                        <option value="0">请选择</option>
                        <% categories.forEach(category => { %>
                        <option value="<%= category.id %>" <%= category.id == article.category_id ? "selected" : "" %> 
                          ><%= category.name %>
                        </option>
                        <% }) %>
                      </select>
                      <span class="col-2 col-form-label text-danger"
                        >* 必填项</span
                      >
                    </div>
                    <div class="form-row mt-3">
                      <label
                        for="ue_content"
                        class="col-2 text-center col-form-label"
                        >内容:</label
                      >
                      <div class="col-8 p-0">
                        <textarea id="editor" name="content"><%= article.content %> </textarea>
                      </div>
                      <span class="col-2 col-form-label text-danger"
                        >* 必填项</span
                      >
                    </div>
                    <div class="form-row mt-3">
                      <label
                        for="content"
                        class="col-2 text-center col-form-label"
                        >热门:</label
                      >
                      <div class="col-8 col-form-label">
                        <div class="custom-control custom-switch">
                          <input
                            type="checkbox"
                            class="custom-control-input"
                            id="hot"
                            name="hot"
                            value="1"
                            <%= article.hot == 1 ? "checked" : "" %>
                          />
                          <label class="custom-control-label" for="hot"></label>
                        </div>
                      </div>
                    </div>
                    <div class="form-row mt-3">
                      <label
                        for="content"
                        class="col-2 text-center col-form-label"
                        >缩略图:</label
                      >
                      <div class="col-8">
                        <input
                          type="file"
                          class="d-none"
                          id="customFile"
                          name="upload"
                          accept="image/*"
                        />
                        <label for="customFile" class="position-relative">
                          <img
                            src="<% article.thumbnail %>"
                            data-src="holder.js/200x200?text=点击上传"
                            class="img-thumbnail"
                            width="200"
                            height="200"
                          />
                          <button
                            type="button"
                            class="delimg btn btn-danger btn-sm position-absolute"
                            title="删除"
                            onclick="delThumbnali()"
                          >
                            &times;
                          </button>
                        </label>
                        <input type="hidden" name="thumbnail" value="<%= article.thumbnail %>">
                      </div>
                    </div>
                    <div class="form-row mt-3">
                      <div class="offset-2">
                        <input
                          type="submit"
                          value="提交"
                          class="btn btn-success"
                        />
                        <a href="/admin/article/" class="btn btn-danger ml-3"
                          >取消</a
                        >
                      </div>
                    </div>
                  </form>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/popper.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/holder/2.9.6/holder.min.js"></script>
    <script src="https://gw.alipayobjects.com/os/lib/antv/g2/3.5.12/dist/g2.min.js"></script>
    <script src="https://gw.alipayobjects.com/os/antv/pkg/_antv.data-set-0.10.2/dist/data-set.min.js"></script>
    <script src="https://cdn.ckeditor.com/ckeditor5/17.0.0/classic/ckeditor.js"></script>
    <script src="/admin/js/public.js"></script>
    <script>
      function delThumbnali(){
     
     
        $("[name='thumbnail']").val('')
      }
    </script>
  </body>
</html>

启动服务,实现对应文章页面显示

博客后台 编辑博文-数据库修改

model/article.js 文件中新增编辑文章的方法

  /**
   * 编辑文章
   * @param {Object} article 文章对象
   */
  static edit(article) {
    
    
    return new Promise((resolve, reject) => {
    
    
      let sql =
        "UPDATE article SET title = ?, content = ?, hot = ?, category_id = ?, thumbnail = ? WHERE id = ?";
      this.query(sql, [
        article.title,
        article.content,
        article.hot,
        article.category_id,
        article.thumbnail,
        article.id,
      ])
        .then((results) => {
    
    
          resolve(results.affectedRows);
        })
        .catch((err) => {
    
    
          console.log(`编辑文章失败:${
      
      err.message}`);
          reject(err);
        });
    });
  }

middleware/article.js 文件中新增编辑文章的属性

  /**
   * 编辑文章
   */
  edit: (req, res, next) => {
    
    
    let {
    
     title, content, hot, category_id, thumbnail, id } = req.body;
    let article = {
    
    
      title,
      content,
      hot: hot ? 1 : 0,
      category_id,
      thumbnail: req.uploadUrl ? req.uploadUrl : thumbnail,
      id,
    };
    Article.edit(article)
      .then((results) => {
    
    
        req.affectedRows = results;
        next();
      })
      .catch((err) => {
    
    
        next(err);
      });
  },

router/admin/article.js 文件中新增路由

articleApp.post("/edit", [article.edit], (req, res) => {
    
    
  if (req.affectedRows > 0) {
    
    
    res.render("admin/alert", {
    
    
      code: true,
      title: "成功提示",
      message: "文章编辑成功",
      url: "/admin/article/",
    });
  } else {
    
    
    res.render("admin/alert", {
    
    
      code: false,
      title: "失败提示",
      message: "文章编辑失败",
      url: "/admin/article/" + req.body.id,
    });
  }

博客后台 前台文章列表与文章内容调整

model/article.js 文件中修改两个方法,数据库查询的时候都加上thumbnail字段

  /**
   * 获取热门推荐
   * @param {integer} num 条目数
   */
  static getHot(num) {
    
    
    return new Promise((resolve, reject) => {
    
    
      let sql =
        "SELECT id,title,content,`time`,thumbnail FROM article WHERE hot = 1 LIMIT ?";
      this.query(sql, num)
        .then((results) => {
    
    
          resolve(results);
        })
        .catch((err) => {
    
    
          console.log(`获取热门文章失败:${
      
      err.message}`);
          reject(err);
        });
    });
  }

  /**
   * 获取文章列表
   */
  static getList() {
    
    
    return new Promise((resolve, reject) => {
    
    
      let sql =
        "SELECT id,title,content,`time`,thumbnail FROM article ORDER BY time DESC";
      this.query(sql)
        .then((results) => {
    
    
          resolve(results);
        })
        .catch((err) => {
    
    
          console.log(`获取文章列表失败:${
      
      err.message}`);
          reject(err);
        });
    });
  }

修改 views/admin/index.html 文件

<img
     src="<%= article.thumbnail %> "
     data-src="holder.js/100px150"
     class="card-img-top"
     alt="..."
     />

使前台页面效果如下

博客管理 类目管理-显示与添加

model/category.js 文件中添加新增类目方法

  /**
   * 新增类目
   */
  static add(name, index) {
    
    
    return new Promise((resolve, reject) => {
    
    
      let sql = "INSERT INTO category (`name`,`index`) VALUES (?,?)";
      this.query(sql, [name, index])
        .then((results) => {
    
    
          resolve(results.insertId);
        })
        .catch((err) => {
    
    
          console.log(`新增类目失败:${
      
      err.message}`);
          reject(err);
        });
    });
  }

middleware/category.js 文件中添加新增类目属性

  /**
   * 添加类目
   */
  add: (req, res, next) => {
    
    
    let {
    
     name, index } = req.body;
    Category.add(name, index)
      .then((results) => {
    
    
        req.insertId = results;
        next();
      })
      .catch((err) => {
    
    
        next(err);
      });
  },

router/admin/category.js 文件中新增路由

categoryApp.post("/add", [category.add], (req, res) => {
    
    
  if (req.insertId) {
    
    
    res.json({
    
     code: 1, msg: "添加成功" });
  } else {
    
    
    res.json({
    
     code: 1, msg: "添加失败" });
  }
});

views/admin/category/index.html 文件中编写JS脚本

function save(t) {
    
    
    let name = $(t).parents("tr").find("[name='name']").val();
    let index = $(t).parents("tr").find("[name='index']").val();

    $.post("/admin/category/add", {
    
     name, index }, function (res) {
    
    
        if (res.code == 1) {
    
    
            showToasts("成功", "添加类目成功");
            setTimeout(function () {
    
    
                location.reload();
            }, 2000);
        } else {
    
    
            showToasts("失败", "添加类目失败");
        }
    });
}

效果如下

博客后台 类目管理-删除与修改

model/category.js 文件中新增删除类目的方法

  /**
   * 删除类目
   * @param {integer} id 类目编号
   */
  static del(id) {
    
    
    return new Promise((resolve, reject) => {
    
    
      let sql = "DELETE FROM category WHERE id = ?";
      this.query(sql, id)
        .then((results) => {
    
    
          resolve(results.affectedRows);
        })
        .catch((err) => {
    
    
          console.log(`删除类目失败:${
      
      err.message}`);
          reject(err);
        });
    });
  }

middleware/category.js 文件中新增删除类目的属性

  /**
   * 删除类目
   */
  del: (req, res, next) => {
    
    
    let {
    
     id } = req.query;
    Category.del(id)
      .then((results) => {
    
    
        req.affectedRows = results;
        next();
      })
      .catch((err) => {
    
    
        next(err);
      });
  },

router/admin/category.js 文件中新增删除类目的路由

categoryApp.get("/del", category.del, (req, res) => {
    
    
  if (req.affectedRows > 0) {
    
    
    res.json({
    
     code: 1, msg: "删除成功" });
  } else {
    
    
    res.json({
    
     code: 1, msg: "删除失败" });
  }
});

views/admin/category/index.html 文件中编写JS脚本

function del(id) {
    
    
    if (confirm("确认删除?")) {
    
    
        $.getJSON("/admin/category/del", {
    
     id }, function (res) {
    
    
            if (res.code == 1) {
    
    
                showToasts("成功", "删除类目成功");
                setTimeout(function () {
    
    
                    location.reload();
                }, 2000);
            } else {
    
    
                showToasts("失败", "删除类目失败");
            }
        });
    }
}

开启服务,实现如下效果

博客后台 日志列表显示

model 目录下新建 log.js 文件

/**
 * 日志数据模型
 */
module.exports = class Tab extends require("./model") {
    
    
  /**
   * 获取日志列表
   */
  static getPage(start, size) {
    
    
    return new Promise((resolve, reject) => {
    
    
      let sql =
        "SELECT handle,`time`,ip FROM `log` ORDER BY `time` DESC LIMIT ?,?";
      this.query(sql, [start, size])
        .then((results) => {
    
    
          resolve(results);
        })
        .catch((err) => {
    
    
          console.log(`获取日志列表失败:${
      
      err.message}`);
          reject(err);
        });
    });
  }

  /**
   * 获取日志总条目数
   */
  static getCount() {
    
    
    return new Promise((resolve, reject) => {
    
    
      let sql = "SELECT COUNT(1) as count FROM `log`";
      this.query(sql)
        .then((results) => {
    
    
          resolve(results[0].count);
        })
        .catch((err) => {
    
    
          console.log(`获取日志总条目数失败:${
      
      err.message}`);
          reject(err);
        });
    });
  }
};

middleware 目录下新建 log.js 文件

/**
 * 访问量中间件
 */
const Log = require("../model/log");

module.exports = {
    
    
  /**
   * 获取日志列表
   */
  getPage: (req, res, next) => {
    
    
    let {
    
     p, size } = req.page;
    Log.getPage((p - 1) * size, size)
      .then((results) => {
    
    
        req.page.list = results;
        next();
      })
      .catch((err) => {
    
    
        next(err);
      });
  },
  /**
   * 获取总条目数
   */
  getCount: (req, res, next) => {
    
    
    Log.getCount()
      .then((results) => {
    
    
        req.count = results;
        next();
      })
      .catch((err) => {
    
    
        next(err);
      });
  },
};

router/admin 目录下新建 log.js 文件

/**
 * 后台日志管理
 */
const express = require("express");
const log = require("../../middleware/log");

const logApp = express();

logApp.get(
  "/",
  log.getCount,
  (req, res, next) => {
    
    
    let page = {
    
    
      p: req.query.p ? req.query.p : 1,
      count: req.count,
      size: 3,
    };
    page.total = Math.ceil(page.count / page.size);
    page.p = page.p > page.total ? page.total : page.p;
    page.p = page.p < 1 ? 1 : page.p;

    req.page = page;
    next();
  },
  log.getPage,
  (req, res) => {
    
    
    let {
    
     user, page } = req;
    res.render("admin/log/index", {
    
     user, page });
  }
);

module.exports = logApp;

修改 views/admin/log/index.html 文件

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, shrink-to-fit=no"
    />
    <title>首页</title>
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
    />
    <link
      rel="stylesheet"
      href="//at.alicdn.com/t/font_1658828_vud4w73neg.css"
    />
    <link rel="stylesheet" href="/admin/css/style.css" />
  </head>

  <body class="bg-light">
    <%- include("../header.html") -%>

    <div class="container-fluid vh-100">
      <div class="row h-100">
        <%- include("../navs.html") -%>
        <div class="col-10">
          <div class="p-3 border mb-3 bg-white">
            <h4 class="mb-0">查看日志</h4>
          </div>
          <div class="row mt-3">
            <div class="col">
              <div class="card">
                <div
                  class="card-header bg-primary text-white d-flex justify-content-between"
                >
                  <h6 class="mb-0 align-self-center">日志列表</h6>
                </div>
                <div class="card-body">
                  <table class="table text-center">
                    <thead>
                      <tr>
                        <th>时间</th>
                        <th>操作</th>
                        <th>IP</th>
                      </tr>
                    </thead>
                    <tbody>
                      <% page.list.forEach(log => { %>
                      <tr>
                        <td><%= log.time.toLocaleString() %></td>
                        <td><%= log.handle %></td>
                        <td><%= log.ip %></td>
                      </tr>
                      <% }) %>
                    </tbody>
                  </table>

                  <nav
                    class="d-flex justify-content-between border-top pt-3 px-3"
                  >
                    <div class="align-self-center">
                      共 <%= page.count %> 条 / 共 <%= page.total %> 页 / 第 <%=
                      page.p %> 页
                    </div>
                    <ul class="pagination mb-0">
                      <% if (page.p > 1) { %>
                      <li class="page-item">
                        <a class="page-link" href="/admin/log?p=<%= parseInt(page.p)-1 %>">上一页</a>
                      </li>
                      <% } %>
                      <% for( let index = 1; index <= page.total; index++ ) { %>
                      <li class="page-item">
                        <a class="page-link" href="/admin/log?p=<%= index %>"><%= index %></a>
                      </li>
                      <% } %>
                      
                      
                      </li>
                      <% if (page.p < page.total) { %>
                      <li class="page-item">
                        <a class="page-link" href="/admin/log?p=<%= parseInt(page.p)+1 %>">下一页</a>
                      </li>
                      <% } %>
                    </ul>
                  </nav>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/popper.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/holder/2.9.6/holder.min.js"></script>
    <script src="https://gw.alipayobjects.com/os/lib/antv/g2/3.5.12/dist/g2.min.js"></script>
    <script src="https://gw.alipayobjects.com/os/antv/pkg/_antv.data-set-0.10.2/dist/data-set.min.js"></script>
    <script src="/admin/js/public.js"></script>
  </body>
</html>

启动服务,实现以下效果

博客后台 日志列表显示

示例:做一个登录日志

router/admin/login.js 中修改实现登录操作的方法

const log = require("../middleware/log");

// 实现登录操作
loginApp.post("/", (req, res, next) => {
    
    
  let {
    
     username, password } = req.body;
  User.login(username, password)
    .then((results) => {
    
    
      if (results) {
    
    
        req.log = {
    
    
          time: new Date(),
          handle: "登录",
          ip: req.ip.split(":")[3],
        };
        log.add(req, res, next);
        // session存储(key=value)
        req.session.user = results;
        res.redirect("/");
      } else {
    
    
        res.render("login", {
    
     msg: "登录失败!用户名或密码错误" });
      }
    })
    .catch((err) => {
    
    
      next(err);
    });
});

主要代码:

req.log = {
    
    
    time: new Date(),
    handle: "登录",
    ip: req.ip.split(":")[3],
};
log.add(req, res, next);

自由发挥即可

博客后台 会话延期

cookie-session会话延期实现

myblog/index.js 文件中新建SESSION延期

// SESSION延期
app.use((req, res, next) => {
    
    
  req.session.nowInMinutes = Math.floor(Date.now() / 60e3);
});

猜你喜欢

转载自blog.csdn.net/Cool_breeze_/article/details/107954506
今日推荐