# 一、搭建界面引入模版html文件
# ① 控制器新建文件夹处理后台界面和路由
新建: app/controller/admin/manager.js
'use strict';
const Controller = require('egg').Controller;
class ManagerController extends Controller {
async create() {
const {ctx} = this;
await ctx.render('admin/manager/create.html');
}
}
module.exports = ManagerController;
# ② 新建后台模板文件夹
新建文件夹: app/view/admin/manager
新建文件: app/view/admin/manager/create.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>create页面</title>
</head>
<body>
create
</body>
</html>
# ③ 路由配置
新建:
app/router/admin.jsmodule.exports = app => { const { router, controller } = app; router.get('/admin/manager/create', controller.admin.manager.create); }
app/router.jsmodule.exports = app => { const { router, controller } = app; //路由分组 //... require('./router/admin')(app); };
访问:http://127.0.0.1:7001/admin/manager/create (opens new window) 看效果
# ④ 模版代码替换create.html页面代码,并引入静态资源
发现页面排版有问题,是由于css等静态资源路径不对
将静态资源
assets文件夹放进app/public文件夹下,并替换页面的引入路径即可,路径替换方式:
"assets/换成"/public/assets/
- 修改网页标题:
<title>后台管理-创建管理员</title>- 修改栏目路径名称:
创建管理员
# ⑤ 分离模版
大家发现后台页面的顶部,左边菜单栏每个页面都是一样的,因此我们可以抽离出来管理,让整个后台页面更加简洁。
另外,希望通过这个案例,同学们可以自己尝试抽离一下我们开发的企业网站。
那么如何分离呢,此时就可以使用egg.js项目的模版引擎中的模版继承功能了。
# 1. 创建模版文件夹
新建文件夹:
app/view/admin/layout一般取名layout
新建文件:app/view/admin/layout/main_app.html把所有页面的相同部分抽离放到这个网页里面来
# 2.使用模版引擎语法
如果语法提示不好,可以安装一下另外一个扩展:
Better Nunjucks
app/view/admin/layout/main_app.html非公共部分代码如下:{% block main %}{% endblock %}
- 此时,在
app/view/admin/manager/create.html文件,就可以继承main_app.html公共模版的内容:{# 申明继承一下主布局 #} {% extends "admin/layout/main_app.html" %} {# 对某个区块进行重新编写代码 #} {% block main %} 123456 {% endblock %}
模版引擎语法:模版引擎语法:https://nunjucks.bootcss.com/templating.html
# 3. 对公共模版main_app.html进行细化分离头部、左侧菜单栏
- 分离头部
app/view/admin/layout/_header.html放入相应的头部代码- 分离左侧菜单栏
app/view/admin/layout/_slider.html放入相应的左侧菜单栏代码- 处理公共模版
app/view/admin/layout/main_app.html文件,把头部和左侧菜单栏引入进来通过模版引擎语法可知道:
include 可引入其他的模板,可以在多模板之间共享一些小模板,如果某个模板已使用了继承那么 include 将会非常有用。<!-- 头部 --> {% include "admin/layout/_header.html" %} <!-- /头部 --> <!-- 左边菜单栏 --> {% include "admin/layout/_slider.html" %} <!-- /左边菜单栏 -->
# 4. 对公共模版main_app.html网页title进行分离标注
app/view/admin/layout/main_app.html<title>网站后台- {% block title %}{% endblock %}</title>
app/view/admin/manager/create.html{% block title %} 创建管理员 {% endblock %}
# 二、创建管理员(页面、创建数据库表及提交数据)
# ① 创建数据库管理员表
- 分析
创建管理员页面: 管理员最起码的字段:管理员账号 + 管理员密码 + 头像(可选)+ 其他字段(如果后期后台很复杂,还可以给管理员分配权限)等等字段后期再扩展设计管理员数据库表:管理员数据库表设计涉及的知识点:
- 章节2:egg.js基础-五、eggjs项目中sequelize模型创建mysql数据库
创建迁移文件 命令:
npx sequelize migration:generate --name=init-manager创建迁移文件:
'use strict'; /** @type {import('sequelize-cli').Migration} */ module.exports = { async up (queryInterface, Sequelize) { const { INTEGER, STRING, DATE, ENUM, TEXT, BIGINT} = Sequelize; // 创建表 --- 类似我们sql语句定义表结构 await queryInterface.createTable('manager', { id: { type: INTEGER(20).UNSIGNED, primaryKey: true, autoIncrement: true, comment: '主键id' }, username: { type: STRING(30), allowNull: false, defaultValue: '', comment: '管理员账号', }, password: { type: STRING(255), allowNull: false, defaultValue: '' , comment: '管理员密码' }, avatar : { type: STRING(1000), allowNull: true, defaultValue: '/public/assets/img/profiles/avatar-01.jpg', comment: '管理员头像' }, // sex: { type: ENUM, values: ['男','女','保密'], allowNull: true, defaultValue: '保密', comment: '留言用户性别'}, create_time: {type: DATE, allowNull: false, defaultValue:Sequelize.fn('NOW')}, update_time: {type: DATE, allowNull: false, defaultValue:Sequelize.fn('NOW')} }); }, async down (queryInterface, Sequelize) { await queryInterface.dropTable('manager') } };执行迁移文件命令生成数据库表:
// 升级数据库 npx sequelize db:migrate
# ② 创建管理员模型并测试提交数据写入数据库
# 1. 创建 app/model/manager.js模型文件
'use strict';
module.exports = app => {
const { INTEGER, STRING, DATE, ENUM, TEXT, BIGINT } = app.Sequelize;
const Manager = app.model.define('manager', {
id: {
type: INTEGER(20).UNSIGNED,
primaryKey: true,
autoIncrement: true,
comment: '管理员表主键id'
},
username: {
type: STRING(30),
allowNull: false,
defaultValue: '',
comment: '管理员账号'
},
password: {
type: STRING(255),
allowNull: false,
defaultValue: '',
comment: '管理员密码'
},
avatar: {
type: STRING(1000),
allowNull: true,
defaultValue: '/public/assets/img/profiles/avatar-01.jpg',
comment: '管理员头像(本地、网络图片地址)'
},
// sex: { type: ENUM, values: ['男','女','保密'], allowNull: true, defaultValue: '保密', comment: '留言用户性别'},
create_time: { type: DATE, allowNull: false, defaultValue: app.Sequelize.fn('NOW') },
update_time: { type: DATE, allowNull: false, defaultValue: app.Sequelize.fn('NOW') }
});
return Manager;
}
# 2. 创建管理员页面调整 app/view/admin/manager/create.html
说两点:
关于界面:页面上的组件都是bootstrap的组件,如:输入框组关于提交数据:我们在第二学期学习过ajax提交数据,另外,我们在讲表单的时候,form表单默认就有提交功能,这里我们采用form表单默认提交功能简单一些提交数据需要有一个api接口处理:
- 提交数据方法
app/controller/admin/manager.js'use strict'; const Controller = require('egg').Controller; class ManagerController extends Controller { //创建管理员---创建页面表单 async create() { const { ctx } = this; await ctx.render('admin/manager/create.html'); } //创建管理员---提交数据 async save() { //一般处理流程 // let params = this.ctx.request.body; let { username, password, avatar } = this.ctx.request.body; //1.参数验证 //先判断一下管理员账号是否存在,不存在在写入数据库 //2.写入数据库 //3.成功之后给页面反馈 /* let manager = await this.app.model.Manager.findOne({ where:{ username: username } }); */ if (await this.app.model.Manager.findOne({ where: { username: username } })) { this.ctx.status = 400; return this.ctx.body = { msg: 'fail', data: '该管理员账号已存在' } } //否则不存在账号写入数据库 let res = await this.app.model.Manager.create({ username, password, // avatar }); this.ctx.status = 200; this.ctx.body = { msg: 'ok', data: res } } } module.exports = ManagerController;
- 提交数据路由定义
app/router/admin.jsmodule.exports = app => { const { router, controller } = app; // 创建管理员界面 router.get('/admin/manager/create', controller.admin.manager.create); //创建管理员提交数据 router.post('/admin/manager/save', controller.admin.manager.save); }
- 创建管理员页面
app/view/admin/manager/create.html
{# 申明继承一下主布局 #} {% extends "admin/layout/main_app.html" %} {% block title %} 创建管理员 {% endblock %} {# 对某个区块进行重新编写代码 #} {% block main %} <div class="card"> <div class="card-body"> <form action="/admin/manager/save" method="post"> <div class="form-group row"> <label class="col-form-label col-md-2">用户名</label> <div class="col-md-10"> <input type="text" class="form-control" name="username" placeholder="用户名..."> </div> </div> <div class="form-group row"> <label class="col-form-label col-md-2">密码</label> <div class="col-md-10"> <input type="password" class="form-control" name="password" placeholder="密码..."> </div> </div> <div class="form-group row"> <label class="col-form-label col-md-2">头像</label> <div class="col-md-10"> <input class="form-control" type="file" name="avatar"> </div> </div> <div class="text-right mt-3"> <button type="submit" class="btn btn-primary">提 交</button> </div> </form> </div> </div> {% endblock %}
# ③ 参数验证
验证表单参数的合法性:如是否必填、格式是否正确、长度是否正确等
app/controller/admin/manager.jsasync save() { //一般处理流程 //1.参数验证 this.ctx.validate({ username: { type: 'string', //参数类型 required: true, //是否必须 // defValue: '', desc: '管理员账号' //字段含义 }, password: { type: 'string', required: true, // defValue: '', desc: '管理员密码' }, }); //后面的代码省略... }
# ④ 中间件 app/middleware/error_handler.js参数错误提示
module.exports = (option, app) => {
// 注意函数名errorHandler要和我们的文件名保持一致,
//如果文件名error_handler.js有下划线,则写成驼峰式 errorHandler
return async function errorHandler(ctx, next) {
// console.log('我是errorHandler');//测试的话,需要到config/config.default.js中设置配置一下
// return next();//程序继续往下走
//由此,我们可以对错误或异常做一个拦截
try {
await next();
// 404 处理
if (ctx.status === 404 && !ctx.body) {
ctx.body = {
msg: "fail",
data: "404 错误",
};
}
} catch (err) {
// console.log('catch中的err',err);
// 所有的异常都在 app 上触发一个 error 事件,框架会记录一条错误日志
ctx.app.emit('error', err, ctx); //日志在 logs/myegg01/common-error.log查看
let status = err.status || 500;
// 生产环境时 500 错误的详细错误内容不返回给客户端,因为可能包含敏感信息
let error = status === 500 && app.config.env === "prod"
? "Internal Server Error"
: err.message;
// 从 error 对象上读出各个属性,设置到响应中
ctx.body = {
msg: "fail",
data: error,
};
// 参数验证异常
if (status === 422 && err.message === "Validation Failed") {
if (err.errors && Array.isArray(err.errors)) {
error = err.errors[0].err[0]
? err.errors[0].err[0]
: err.errors[0].err[1];
}
ctx.body = {
msg: "fail",
data: error,
};
}
ctx.status = status;
}
}
}
# ⑤ 修改器对管理员密码进行加密
知识回顾:
- 文档搜索
修改器回顾,涉及到:章节2:Egg.js基础--六、egg.js项目中sequelize模型新增数据到数据库--5. 修改器set()方法:数据插入到数据库前可自动修改成指定要求的数据- 文档搜索
加密,涉及到:第二学期-第二季-章节7:Node.js基础--十、系统模块:crypto模块详解-加密-③哈希函数加密
app/model/manager.js模型文件password: { type: STRING(255), allowNull: false, defaultValue: '', comment: '管理员密码', set(val) { //'sha256'加密 let hash = crypto.createHash('sha256',app.config.crypto.secret); //或md5 hash.update(val); let result = hash.digest('hex'); // console.log(result); this.setDataValue('password', result); //将加密后的密码写入数据库 } },设置密钥
config/config.default.js//加密处理秘钥---随便定义:字母符号下划线数字等任意组合 config.crypto = { secret: 'qhdgw@45ncashdaksh2!#@3nxjdas*_672' };
# 三、管理员列表
注意开发之前,前面讲过的关于模版引擎的语法问题:
- 模版引擎语法扩展:
Better Nunjucks- 模版引擎语法:https://nunjucks.bootcss.com/templating.html
# 1 创建管理员列表页面、定义路由
# 1.1 创建管理员列表页面,写一个页面方法
app/controller/admin/manager.js//管理员列表 async index() { const { ctx,app } = this; let data = await app.model.Manager.findAll(); console.log(data); await ctx.render('admin/manager/index.html',{ // data: JSON.stringify(data) data }); }# 1.2 定义路由
app/router/admin.js//管理员列表 router.get('/admin/manager/index', controller.admin.manager.index);# 1.3 管理员列表页面
app/view/admin/manager/index.html{# 申明继承一下主布局 #} {% extends "admin/layout/main_app.html" %} {% block title %} 管理员列表 {% endblock %} {# 对某个区块进行重新编写代码 #} {% block main %} {# {{data}} {% for item in data %} {{item.username}} {% endfor %} #} <div class="card card-table"> <div class="card-header"> <button type="button" class="btn btn-outline-primary">管理员列表</button> </div> <div class="card-body"> <div class="table-responsive"> <table class="table table-hover table-center mb-0"> <thead> <tr> <th>用户</th> <th width="120">创建时间</th> <th class="text-center" width="100">操作</th> </tr> </thead> <tbody> {% for item in data %} <tr> <td> <h2 class="table-avatar"> <a href="#" class="avatar avatar-sm mr-2"> <img class="avatar-img rounded-circle" src="{{item.avatar}}" alt="User Image"></a> <a href="#">{{item.username}} <span>管理员</span></a> </h2> </td> <td>{{item.create_time}}</td> <td class="text-right"> <div class="actions"> <a href="#" class="btn btn-sm bg-success-light mr-2"> <i class="fe fe-pencil"></i> 修改 </a> <a href="#" class="btn btn-sm bg-danger-light"> <i class="fe fe-trash"></i> 删除 </a> </div> </td> </tr> {% endfor %} </tbody> </table> </div> </div> <div class="card-footer d-flex justify-content-center"> <ul class="pagination"> <li class="page-item"> <a class="page-link" href="#" aria-label="Previous"> <span aria-hidden="true">«</span> <span class="sr-only">Previous</span> </a> </li> <li class="page-item"><a class="page-link" href="#">1</a></li> <li class="page-item"><a class="page-link" href="#">2</a></li> <li class="page-item"><a class="page-link" href="#">3</a></li> <li class="page-item"> <a class="page-link" href="#" aria-label="Next"> <span aria-hidden="true">»</span> <span class="sr-only">Next</span> </a> </li> </ul> </div> </div> {% endblock %}
# 2 获取器对管理员列表中的时间进行处理
# 2.1 创建
app/extend/application.jsegg.js项目扩展文件夹
extend, 里面js文件写的方法可以进行访问使用module.exports = { // 时间转换 formatTime(time){ let d = new Date(time); const Month = (d.getMonth() + 1) >= 10 ? (d.getMonth() + 1) : '0'+(d.getMonth() + 1) const Day = d.getDate() >= 10 ? d.getDate() : '0' + d.getDate() const h = d.getHours() >= 10 ? d.getHours() : '0' + d.getHours() const m = d.getMinutes() >= 10 ? d.getMinutes() : '0' + d.getMinutes() const s = d.getSeconds() >= 10 ? d.getSeconds() : '0' + d.getSeconds() return d.getFullYear() + '-' + Month + '-' + Day + ' ' + h + ':' + m + ':' + s; }, //1、当前时间换时间戳 currentStamp(){ return parseInt(new Date().getTime()/1000); // 当前时间戳 }, //2、当前时间换日期字符串 parse_yyyy_mm_dd(){ var now = new Date(); var yy = now.getFullYear(); //年 var mm = now.getMonth() + 1; //月 var dd = now.getDate(); //日 var hh = now.getHours(); //时 var ii = now.getMinutes(); //分 var ss = now.getSeconds(); //秒 var clock = yy + "-"; if(mm < 10) clock += "0"; clock += mm + "-"; if(dd < 10) clock += "0"; clock += dd + " "; if(hh < 10) clock += "0"; clock += hh + ":"; if (ii < 10) clock += '0'; clock += ii + ":"; if (ss < 10) clock += '0'; clock += ss; return clock; //获取当前日期 }, //3、日期字符串转时间戳 riqi_to_stamp(date){ // var date = '2015-03-05 17:59:00.0'; date = date.substring(0,19); date = date.replace(/-/g,'/'); //必须把日期'-'转为'/' var timestamp = new Date(date).getTime(); return timestamp; }, //4、时间戳转日期字符串 stamp_to_riqi(timestamp){ // var timestamp = '1425553097'; var d = new Date(timestamp * 1000); //根据时间戳生成的时间对象 var date = (d.getFullYear()) + "-" + (d.getMonth() + 1) + "-" + (d.getDate()) + " " + (d.getHours()) + ":" + (d.getMinutes()) + ":" + (d.getSeconds()); return date; } }# 2.2 获取器对管理员列表中的时间进行处理展示
app/model/manager.js模型文件create_time: { type: DATE, allowNull: false, defaultValue: app.Sequelize.fn('NOW'), get(){ return app.formatTime(this.getDataValue('create_time')); } },
# 四、管理员列表分页功能
# 1、 首次简单分页功能实现
# ①
/app/controller/admin/manager.js控制器//创建管理员列表页面 async index(){ const { ctx,app } = this; /* let data = await app.model.Manager.findAll(); // console.log(data); await ctx.render('admin/manager/index.html',{ // data: JSON.stringify(data) data }); */ /* //分页:http://127.0.0.1:7001/admin/manager/index?page=1&limit=10 let page = ctx.query.page ? parseInt(ctx.query.page) : 1; let limit = ctx.query.limit ? parseInt(ctx.query.limit) : 10; let offset = (page - 1) * limit; let data = await app.model.Manager.findAndCountAll({ offset, limit }); console.log(JSON.parse(JSON.stringify(data))); */ //分页:可以提炼成一个公共方法page(模型名称,where条件,其他参数options) let data = await ctx.page('Manager'); await ctx.render('admin/manager/index.html',{ data }); }# ② 新建扩展方法:
app/extend/context.jsegg.js项目的框架扩展:https://www.eggjs.org/zh-CN/basics/extend#context
module.exports = { //分页 async page(modelName,where={},options={}){ let page = this.query.page ? parseInt(this.query.page) : 1; let limit = this.query.limit ? parseInt(this.query.limit) : 10; let offset = (page - 1) * limit; let res = await this.app.model[modelName].findAndCountAll({ offset, limit }); return res.rows; } }
# 2、 扩展分页功能:增加where、排序等条件
# ①
app/extend/context.jsmodule.exports = { // 分页 async page(modelName,where={},options={}){ let page = this.query.page ? parseInt(this.query.page) : 1; let limit = this.query.limit ? parseInt(this.query.limit) : 10; let offset = (page - 1) * limit; //如果没有传排序规则,则默认给它id降序 if(!options.order){ options.order = [ ['id','DESC'] ] } let data = await this.app.model[modelName].findAndCountAll({ limit: limit, offset: offset, where: where, ...options, }); return data.rows } }# ②
/app/controller/admin/manager.js//创建管理员列表页面 async index(){ const { ctx,app } = this; //分页:可以提炼成一个公共方法page(模型名称,where条件,其他参数options) let data = await ctx.page('Manager', { // username:'admin4' }, { // order:[ // ['id','desc'] // ], }); await ctx.render('admin/manager/index.html',{ data }); }
# 3、 页面分页按钮实现
bootstrap分页组件:https://www.runoob.com/bootstrap4/bootstrap4-pagination.html
<!-- 分页整合 --> <div class="card-footer d-flex justify-content-center"> {# 安全起见,egg.js官方默认将代码过滤成字符串,这里我们采用过滤器进行解码 #} {{ ctx.locals.pageHtml | safe }} </div># 如何将context.js定义的变量放在模版里面去
如何将我们定义的这个pageHtml变量放在模版里面去?
egg.js框架给我们在context扩展里面提供了locals对象,可将变量挂载到这个对象里面,然后可在模版里面使用
app/extend/context.js代码module.exports = { // 分页 async page(modelName, where = {}, options = {}) { let page = this.query.page ? parseInt(this.query.page) : 1; let limit = this.query.limit ? parseInt(this.query.limit) : 10; let offset = (page - 1) * limit; //如果没有传排序规则,则默认给它id降序 if(!options.order){ options.order = [ ['id','desc'] ] } let data = await this.app.model[modelName].findAndCountAll({ limit: limit, offset: offset, where: where, ...options }); //求得总分页数 let totalPage = Math.ceil(data.count / limit); //分页模版代码 let pageEl = ''; for(let i = 1;i <= totalPage;i++){ //判断当前页是否是active let active = ''; if(page == i){ active = 'active'; } pageEl += ` <li class="page-item ${active}"><a class="page-link" href="?page=${i}&limit=${limit}">${i}</a></li> `; } //如果当前就是第一页,就不存在上一页,禁用上一页按钮,下一页同理 let prevDisabled = page <= 1 ? 'disabled' : ''; let nextDisabled = page >= totalPage ? 'disabled' : ''; //最后将所有模版代码组装一起 let pageHtml = ` <ul class="pagination"> <li class="page-item ${prevDisabled}"> <a class="page-link" href="?page=${page-1}&limit=${limit}" aria-label="Previous"> <span aria-hidden="true">«</span> <span class="sr-only">Previous</span> </a> </li> ${pageEl} <li class="page-item ${nextDisabled}"> <a class="page-link" href="?page=${page+1}&limit=${limit}" aria-label="Next"> <span aria-hidden="true">»</span> <span class="sr-only">Next</span> </a> </li> </ul> `; //如何将我们定义的这个pageHtml变量放在模版里面去? //egg.js框架给我们在context扩展里面提供了locals对象,可将变量挂载到这个对象里面,然后可在模版里面使用 this.locals.pageHtml = pageHtml; return data.rows } }
# 4、 处理页面分页按钮网址上有其他参数的情况
http://127.0.0.1:7001/admin/manager/index?page=1&limit=3&keyword=哈哈&cid=100点击分页按钮,新页面没有keyword参数
知识点:
- 判断对象是否存在某个属性
hasOwnProperty,存在则删除掉delete# 对象转&拼接字符串方法
app/extend/context.js代码
module.exports = {
// 分页
async page(modelName, where = {}, options = {}) {
let page = this.query.page ? parseInt(this.query.page) : 1;
let limit = this.query.limit ? parseInt(this.query.limit) : 10;
let offset = (page - 1) * limit;
//如果没有传排序规则,则默认给它id降序
if (!options.order) {
options.order = [
['id', 'desc']
]
}
let data = await this.app.model[modelName].findAndCountAll({
limit: limit,
offset: offset,
where: where,
...options
});
//求得总分页数
let totalPage = Math.ceil(data.count / limit);
// -----------------------处理页面分页按钮网址上有其他参数的情况-----------------------------------
//网址有其他参数,如:http://127.0.0.1:7001/admin/manager/index?page=1&limit=3&keyword=哈哈&cid=100
let query = { ...this.query };
// console.log(query);//{page: 1, limit: 3, keyword: "哈哈", cid: '100'}
//为了保证在切换页码之后,其他参数还在,应该先去掉page,limit得到其他参数,最后把page,limit在拼接过来
if (query.hasOwnProperty('page')) {
delete query.page;
}
if (query.hasOwnProperty('limit')) {
delete query.limit;
}
// console.log(query);//{ keyword: '哈哈', cid: '100' }
// 对象转&拼接字符串
//{ keyword: '哈哈', cid: '100'} 转成 &keyword=哈哈&cid=100
const urlEncode = (param, key, encode) => {
if (param == null) return '';
var paramStr = '';
var t = typeof (param);
if (t == 'string' || t == 'number' || t == 'boolean') {
paramStr += '&' + key + '=' + ((encode == null || encode) ? encodeURIComponent(param) : param);
} else {
for (var i in param) {
var k = key == null ? i : key + (param instanceof Array ? '[' + i + ']' : '.' + i)
paramStr += urlEncode(param[i], k, encode)
}
}
return paramStr;
}
//调用urlEncode方法
query = urlEncode(query);
console.log(query);//结果:&keyword=哈哈&cid=100 (转码后:&keyword=%E5%93%88%E5%93%88&cid=100)
// -----------------------处理页面分页按钮网址上有其他参数的情况-----------------------------------------
// 下面分页链接上拼上query
//分页模版代码
let pageEl = '';
for (let i = 1; i <= totalPage; i++) {
//判断当前页是否是active
let active = '';
if (i == page) {
active = 'active';
}
pageEl += `<li class="page-item ${active}"><a class="page-link" href="?page=${i}&limit=${limit}${query}">${i}</a></li>`;
}
//如果当前就是第一页,就不存在上一页,禁用上一页按钮,下一页同理
let prevDisabled = page <= 1 ? 'disabled' : '';
let nextDisabled = page >= totalPage ? 'disabled' : '';
//最后将所有模版代码组装一起
let pageHtml = `
<ul class="pagination">
<li class="page-item ${prevDisabled}">
<a class="page-link" href="?page=${page - 1}&limit=${limit}${query}" aria-label="Previous">
<span aria-hidden="true">«</span>
<span class="sr-only">Previous</span>
</a>
</li>
${pageEl}
<li class="page-item ${nextDisabled}">
<a class="page-link" href="?page=${page + 1}&limit=${limit}${query}" aria-label="Next">
<span aria-hidden="true">»</span>
<span class="sr-only">Next</span>
</a>
</li>
</ul>
`;
//如何将我们定义的这个pageHtml变量放在模版里面去?
//egg.js框架给我们在context扩展里面提供了locals对象,可将变量挂载到这个对象里面,然后可在模版里面使用
this.locals.pageHtml = pageHtml;
return data.rows
}
}
# 五、公共模板开发
大家想一想,关于列表页,我们除了管理员,还有用户,留言板等等很多模块,那是不是每个模块都创建一个模版文件index.html呢?很显然是不需要的,接下来我们来创建公共模版。
# ①、初步写一个渲染公共模版的方法
具体查看 :初步写一个渲染公共模版的方法
# ②、 新建公共模版
# 1.自定义表格上面的按钮和表格头部
具体查看 :自定义表格上面的按钮和表格头部
# 2. 自定义表格头部的操作【修改、删除】部分
具体查看 :自定义表格头部的操作【修改、删除】部分
# 3. 处理表格模版、表单模版(创建管理员表单公共模版为例)
具体查看 :处理表格模版、表单模版(创建管理员表单公共模版为例)
# ③ 扩展公共模版功能,如公共模版表格中的删除管理员功能,并封装一个提示消息组件
具体查看 :扩展公共模版功能,如公共模版表格中的删除管理员功能,并封装一个提示消息组件
# ④ 扩展公共模版功能,如公共模版表格中的删除管理员功能,在删除的时候,提示确认删除弹出框组件
具体查看 :扩展公共模版功能,如公共模版表格中的删除管理员功能,在删除的时候,提示确认删除弹出框组件
# ⑤ 扩展公共模版功能,如修改管理员
具体查看 :扩展公共模版功能,如修改管理员
# ⑥、优化公共表单模版,让表单提交显示相应的提示消息
具体查看 :优化公共表单模版
# 六、后台管理员登录
具体查看 :响应式后台管理员登录
# 七、后台用户留言板管理板块
台上一分钟,台下十年功,我们前面花了那么大的力气创建后台公共模版,就是为了后台在开发其他栏目板块能够快速利用我们的公共模版,我们本节课再讲一个用户留言板管理板块,来再次带领大家回忆一下后台管理板块的整个流程,之后,大家开发其他板块就按照这个流程就可以了。
具体查看 :响应式后台用户留言板管理
# 八、优化公共模版表格能够显示头像
以管理员列表为例
app/controller/admin/manager.js//创建管理员列表页面 async index() { ... //表头 columns: [ { title: '管理员账号', // key: 'username', render(item){ return ` <h2 class="table-avatar"> <a href="#" class="avatar avatar-sm mr-2"> <img class="avatar-img rounded-circle" src="${item.avatar}" alt="User Image"></a> <a href="#"> ${item.username} <span>管理员</span></a> </h2> `; } }, ... }表格模版
app/view/admin/layout/_table.html... {% if item2.key %} {# 如果存在key,说明是渲染数据 #} <td class="{{item2.class}}" width="{{item2.width}}">{{ item[item2.key] }}</td> {# 如果存在render,说明是执行函数 #} {% elif item2.render %} <td class="{{item2.class}}" width="{{item2.width}}">{{ item2.render(item) | safe }}</td> {% elif item2.action %} {# 如果存在action 说明是操作 #} ...
# 九、后台左侧菜单栏
# 1、后台首页
app/controller/admin/home.js控制器//后台首页 async index() { let { ctx } = this; await ctx.render('admin/home/index.html'); }路由
app/router/admin.js放在最底部//后台首页 router.get('/admin', controller.admin.home.index);模版
app/view/admin/home/index.html{# 申明继承一下主布局 #} {% extends "admin/layout/main_app.html" %} {% block title %}后台首页{% endblock %} {# 对某个区块进行重新编写代码 #} {% block main %} 管理员,欢迎您! {% endblock %}主模版
app/view/admin/layout/main_app.html{% if title %} <div class="page-header"> <div class="row align-items-center"> <div class="col"> <h3 class="page-title">{{title}}</h3> <ul class="breadcrumb"> <li class="breadcrumb-item"><a href="/admin">后台首页</a></li> <li class="breadcrumb-item active">{{title}}</li> </ul> </div> </div> </div> {% endif %}
# 2、利用中间件动态处理后台左侧菜单
- 新建中间件
app/middleware/admin_menu.jsmodule.exports = (option, app) => { return async function adminMenu(ctx, next) { let menus = [ { name: '主面板', icon: 'fe fe-home', url: '/admin' }, { name: '管理员', icon: 'fe fe-user-plus', url: '/admin/manager/index' }, { name: '留言板', icon: 'fe fe-document', url: '/admin/message/index' }, ]; // 和我们分页模版类似,通过ctx.locals对象挂载代码 ctx.locals.menus = menus.map(item => { console.log('当前请求地址', ctx.request.url) // console.log('遍历项地址', item.url) let baseURL = item.url.replace('/index',''); console.log('处理后遍历项地址', item.url) // if(ctx.request.url == item.url){ // item.active = 'active'; // } if((ctx.request.url == '/admin' && item.url == '/admin') || (ctx.request.url != '/admin' && item.url != '/admin' && ctx.request.url.startsWith(baseURL))){ item.active = 'active'; } return item; }); console.log(ctx.locals.menus); await next(); } }
- 配置中间件
config/config.default.js... config.middleware = ['errorHandler','adminAuth','adminMenu']; // 对中间件adminMenu进一步配置 config.adminMenu = { ignore:[ "/api", "/admin/login", "/admin/loginevent", "/public", ], }; ...
- 对左侧菜单栏模版进行设置
app/view/admin/layout/_slider.html<div class="sidebar" id="sidebar"> <div class="sidebar-inner slimscroll"> <div id="sidebar-menu" class="sidebar-menu"> <ul> {% for item in ctx.locals.menus %} <li class="{{item.active}}"> <a href="{{item.url}}" ><i class="{{item.icon}}"></i> <span>{{item.name}}</span></a> </li> {% endfor %} </ul> </div> </div> </div>
# 十、上传文件
具体查看 :上传文件功能
# 十一、上传或修改管理员头像
app/view/admin/layout/_form.html调整... {% for item in form.fields %} <div class="form-group row"> <label class="col-form-label col-md-2">{{item.label}}</label> <div class="col-md-10"> {% if item.type == 'file' %} {# 如果type是文件类型 #} <input type="file" class="form-control" name="{{item.name}}" @change="uploadFile($event,'{{item.name}}')"> <img :src="form.{{item.name}}" v-if="form.{{item.name}}" class="mt-2 p-1 rounded border avatar-lg"> {% else %} <input type="{{item.type}}" class="form-control" name="{{item.name}}" placeholder="{{item.placeholder}}..." v-model="form.{{item.name}}"> {% endif %} </div> </div> {% endfor %} ... methods:{ uploadFile(e,name){ //console.log('e',e); //console.log('name',name); let file = e.target.files[0]; //console.log(file); let formData = new FormData(); formData.append('file',file); $.ajax({ type: 'POST', url: "/uploadStreamSingleToServerDiy/adminImg?_csrf={{ctx.csrf|safe}}", processData: false, // 告诉jQuery不要去处理发送的数据 data: formData, contentType: false, // 告诉jQuery不要去设置Content-Type请求头 success: (response, stutas, xhr)=> { console.log(response) this.form[name] = response.data.url; Vueapp.$refs.toast.show({ msg:"图片上传成功", type:'success', delay:1000, success:()=>{} }); }, error:(e)=>{ console.log(e) Vueapp.$refs.toast.show({ msg:e.responseJSON.data, type:'danger', delay:3000 }); } }); } } ...