个人博客系统
欢迎访问我的博客~
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 i express
npm i ejs
npm i mysql
会生成一个 package-lock.json
文件
/**
* 入口函数
*/
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"
},
静态资源目录
里面存放 css js img 等等的静态资源
模板目录
里面存放模板文件
子应用目录
将我们的程序进行模块化管理,每一个模块都将视为一个子应用
中间件目录
与路由器配套的一些操作方法,在路由加载页面之前获取所需要的一些数据
数据模型目录
操作数据库的一些文件
从数据库中进行增删改查,把得到的一些数据返回给中间件,由中间件返回给路由,由路由进行页面的渲染和加载,最后返回给客户端
在views目录和static目录下导入文件
导入完成之后可以把重复的代码提取出来,进行简化处理
<%- include('header.html') -%>
html代码不重复的部分
<%- include('footer.html') -%>
/**
* 首页子应用(首页路由)
*/
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 出现以下页面说明搭建成功
在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;
启动服务显示以下页面
发现这个时间的格式有点别扭,修改为本地时间
<%= hot.time.toLocaleString() %>
发现这个内容有点别扭
<%= hot.content.replace(/<[^>]+>/g,"").substring(0,100) %>
修改后如下图:
数据库查询
在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 });
});
开启服务,出现以下页面
数据库查询
在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>
<% }) %>
启动服务。显示页面如下
在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 ,出现以下页面
虽然现在输入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>
启动服务,出现以下页面
在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表单
启动服务,搜索css出现以下页面
在router目录下新建 article.js
文件
// 文章详情页
articleApp.get("/:id", (req, res) => {
let {
categories } = req;
res.render("article", {
categories });
});
启动服务,在浏览器输入url网址 http://127.0.0.1:3000/article/1 会出现以下页面
在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>
<div class="card-body"><%- article.content %>div>
<div class="card-footer bg-white border-0">
<span class="badge badge-pill">标签1span>
<span class="badge badge-pill">标签2span>
<span class="badge badge-pill">标签3span>
<span class="badge badge-pill">标签4span>
<span class="badge badge-pill">标签5span>
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>
开启服务,出现以下页面
在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>
开启服务,页面完成以下效果
首先在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 }));
开启服务,出现以下效果
安装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.js
、 index.js
、 search.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>
启动服务,用户未登录显示以下效果
用户已登录显示以下效果
在myblog目录下的 index.js
文件中实现退出登录
// 退出登录
app.get("/user/logout", (req, res) => {
req.session.user = null;
res.render("login", {
msg: "退出成功" });
});
启动服务,实现以下效果
在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="'<%= article.thumbnail %>'>"
>i>
<% } %>
td>
<td>
<div class="custom-control custom-switch">
/>
<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">
ο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
文件
<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 => { %>
启动服务,实现对应文章页面显示
在 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
文件
<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>IPth>
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);
});