# 一、搭建界面引入模版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.js

module.exports = app => {
    const { router, controller } = app;
    router.get('/admin/manager/create', controller.admin.manager.create);
}

app/router.js

module.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/

  1. 修改网页标题: <title>后台管理-创建管理员</title>
  2. 修改栏目路径名称:创建管理员

# ⑤ 分离模版

大家发现后台页面的顶部,左边菜单栏每个页面都是一样的,因此我们可以抽离出来管理,让整个后台页面更加简洁。
另外,希望通过这个案例,同学们可以自己尝试抽离一下我们开发的企业网站。

那么如何分离呢,此时就可以使用egg.js项目的模版引擎中的模版继承功能了。

# 1. 创建模版文件夹

新建文件夹: app/view/admin/layout 一般取名layout
新建文件: app/view/admin/layout/main_app.html 把所有页面的相同部分抽离放到这个网页里面来

main_app.html实例代码(一):初步抽离公共部分

# 2.使用模版引擎语法

如果语法提示不好,可以安装一下另外一个扩展:Better Nunjucks

  1. app/view/admin/layout/main_app.html非公共部分代码如下:
{% block main %}{% endblock %}
  1. 此时,在 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进行细化分离头部、左侧菜单栏

  1. 分离头部 app/view/admin/layout/_header.html 放入相应的头部代码
  2. 分离左侧菜单栏 app/view/admin/layout/_slider.html 放入相应的左侧菜单栏代码
  3. 处理公共模版 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 %}

# 二、创建管理员(页面、创建数据库表及提交数据)

# ① 创建数据库管理员表

  1. 分析创建管理员页面: 管理员最起码的字段:管理员账号 + 管理员密码 + 头像(可选)+ 其他字段(如果后期后台很复杂,还可以给管理员分配权限)等等字段后期再扩展
  2. 设计管理员数据库表管理员数据库表设计

涉及的知识点:

  1. 章节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

说两点:

  1. 关于界面:页面上的组件都是bootstrap的组件,如:输入框组
  2. 关于提交数据:我们在第二学期学习过ajax提交数据,另外,我们在讲表单的时候,form表单默认就有提交功能,这里我们采用form表单默认提交功能简单一些
  3. 提交数据需要有一个api接口处理
  1. 提交数据方法 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;

  1. 提交数据路由定义 app/router/admin.js
module.exports = app => {
    const { router, controller } = app;
    // 创建管理员界面
    router.get('/admin/manager/create', controller.admin.manager.create);
    //创建管理员提交数据
    router.post('/admin/manager/save', controller.admin.manager.save);
}
  1. 创建管理员页面 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.js

async 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;
        }
    }
}

# ⑤ 修改器对管理员密码进行加密

知识回顾:

  1. 文档搜索 修改器 回顾,涉及到:章节2:Egg.js基础--六、egg.js项目中sequelize模型新增数据到数据库--5. 修改器set()方法:数据插入到数据库前可自动修改成指定要求的数据
  2. 文档搜索 加密,涉及到:第二学期-第二季-章节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'
};

# 三、管理员列表

注意开发之前,前面讲过的关于模版引擎的语法问题:

  1. 模版引擎语法扩展:Better Nunjucks
  2. 模版引擎语法: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.js

egg.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.js

egg.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.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,
         });

         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参数

知识点:

  1. 判断对象是否存在某个属性 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、利用中间件动态处理后台左侧菜单

  1. 新建中间件 app/middleware/admin_menu.js
module.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();
    }
}
  1. 配置中间件 config/config.default.js
... 
config.middleware = ['errorHandler','adminAuth','adminMenu'];
  // 对中间件adminMenu进一步配置
  config.adminMenu = {
    ignore:[
      "/api",
      "/admin/login",
      "/admin/loginevent",
      "/public",
    ],
  };
... 
  1. 对左侧菜单栏模版进行设置 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
              });
          }
      });
  }
}
...
更新时间: 2024年3月14日星期四上午11点02分