# 一、分析权限分配界面并初步实现界面

控制器 app/controller/admin/manager.js

//创建管理员---创建页面表单
  async create() {
    ...

    let data = [
      { id: 1, pid: 0, name: '网站', icon: 'fa fa-chrome', url: '' },
      { id: 2, pid: 1, name: '主面板', icon: 'fa fa-pie-chart', url: '/admin' },
      { id: 3, pid: 1, name: '管理员', icon: 'fe fe-user-plus', url: '/admin/manager/index' },
      { id: 4, pid: 1, name: '留言板', icon: 'fe fe-document', url: '/admin/message/index' },
      { id: 5, pid: 1, name: '分类管理', icon: 'fa fa-list', url: '/admin/category/index' },
      { id: 6, pid: 1, name: '新闻内容管理', icon: 'fa fa-file-text-o', url: '/admin/news/index' },
      { id: 7, pid: 1, name: '配置管理', icon: 'fa fa-wrench', url: '/admin/config/index' },
      { id: 8, pid: 0, name: '直播', icon: 'fa fa-video-camera', url: '' },
      { id: 9, pid: 8, name: '直播用户', icon: 'fa fa-venus-mars', url: '/admin/liveuser/index' },
      { id: 10, pid: 8, name: '直播礼物管理', icon: 'fa fa-gift', url: '/admin/livegift/index' },
      { id: 11, pid: 8, name: '直播订单管理', icon: 'fa fa-credit-card', url: '/admin/liveorder/index' },
      { id: 12, pid: 8, name: '直播间管理', icon: 'fa fa-tv', url: '/admin/live-/index' },
      
    ];

    data = this.app.transformToTree(data);
    // console.log('处理之后的data', JSON.stringify(data));
    // return;

    //渲染公共模版
    await ctx.renderTemplate({
      ...
      form: {
        ...
        //  字段
        fields: [
          ...
          {
            label: '权限分配',
            type: 'treeDataSelect', //树形结构数据选择
            name: 'auth',
            default: JSON.stringify(data),
          },
        ],
      },
      ...
    });
  }

模版 app/view/admin/layout/_form.html

{# 定义一个可递归调用的子模板 #}
{% macro renderMenu(items,keyname) %}
{% for item in items %}
<span class="dropdown-item" style="margin-left: {{item.level * 20}}px;
    cursor: pointer;background:none;"
    onmouseover="this.style.color = '#000000';this.style.fontWeight = 800;"
    onmouseout="this.style.color = '#002222';this.style.fontWeight = 'normal';"
    @click="dropdownItemClick('{{keyname}}','{{item.name}}','{{item.id}}')">
    <input type="checkbox" name="{{keyname}}" style="margin-right: 10px;">{{item.name}}</span>
...
{# 如果是树形结构数据选择 #}
{% elif item.type == 'treeDataSelect' %}
  {# 使用定义好的子模板递归渲染菜单 #}
  <!-- 在上面的代码块之前或之外的合适位置,提前解析item.default为JSON对象 -->
  {% set itemDefaultParsed  = item.default | safe | fromJson %}
  {% call renderMenu(itemDefaultParsed,item.name) %}
  {% endcall %}

# 二、权限id集合写进数据库

# 1、迁移文件、模型文件、数据库管理员表新增 权限字段 auth

auth:{
  type: STRING(255),
  allowNull: true, 
  defaultValue:'',
  comment: '权限id'
},

# 2、控制器 app/controller/admin/manager.js

//创建管理员提交数据
  async save() {
    ...
    //1.参数验证
    this.ctx.validate({
      ...
      auth: {
        type: 'string',
        required: false,
        defValue: '', 
        desc: '权限id集合'
      },
    });
    ...
  }

# 3、 模版 app/view/admin/layout/_form.html

{# 定义一个可递归调用的子模板 #}
{% macro renderMenu(items,keyname,type) %}
{% for item in items %}
<span class="dropdown-item" style="margin-left: {{item.level * 20}}px;
cursor: pointer;background:none;"
onmouseover="this.style.color = '#000000';this.style.fontWeight = 800;"
onmouseout="this.style.color = '#002222';this.style.fontWeight = 'normal';"
@click="dropdownItemClick('{{keyname}}','{{item.name}}','{{item.id}}')"> 
   {% if type == 'treeDataSelect' %}
   <input type="checkbox" name="{{keyname}}" style="margin-right: 10px;" class="checkbox-{{keyname}}"
   @click.stop="treeDataSelectClick('{{keyname}}','{{item.name}}','{{item.id}}','checkbox-{{keyname}}',$event)"
   data="{{item.id}}">
   {% endif %}
   {{item.name}}
</span>
{# 检查是否存在子菜单项 #}
{% if item.children and item.children.length %}
    <div class="dropdown-submenu">
        {% call renderMenu(item.children,keyname,type) %}
        {# 这里通过调用macro自身实现了递归 #}
        {% endcall %}
    </div>
{% endif %}
{% endfor %}
{% endmacro %}
...
{# 如果是下拉框类型 #}
<div class="dropdown">
    <button type="button" class="btn dropdown-toggle dropdown-{{item.name}}" data-toggle="dropdown" id="dropdownData">
        {{item.placeholder  if item.placeholder  else '一级分类'}}
    </button>
    <div class="dropdown-menu">
      <h5 class="dropdown-header">请选择要放在哪个分类下</h5>
      <div class="dropdown-divider"></div>
      {# 使用定义好的子模板递归渲染菜单 #}
      <!-- 在上面的代码块之前或之外的合适位置,提前解析item.default为JSON对象 -->
      {% set itemDefaultParsed  = item.default | safe | fromJson %}
      {% call renderMenu(itemDefaultParsed,item.name,item.type) %}
      {% endcall %}
    </div>
</div>
...
{# 如果是树形结构数据选择 #}
{% elif item.type == 'treeDataSelect' %}
      {# 使用定义好的子模板递归渲染菜单 #}
      <!-- 在上面的代码块之前或之外的合适位置,提前解析item.default为JSON对象 -->
      {% set itemDefaultParsed  = item.default | safe | fromJson %}
      {% call renderMenu(itemDefaultParsed,item.name,item.type) %}
      {% endcall %}
{% else %}
...

<script>
Vueapp = new Vue({
  ...
  //树形结构数据点击多选框
  treeDataSelectClick(keyname,name,id,classname,e){
    // console.log('keyname', keyname);
    // console.log('name', name);
    // console.log('id', id);
    // console.log('classname', classname);
    // console.log('e事件对象', e);

    // 选中上一级下一级所有都选中
    // console.log($(e.target).parent().parent().attr('class'));
    if($(e.target).parent().parent().attr('class') != 'dropdown-submenu'){
        let checkbox = $(e.target).parent().next().find('input[type="checkbox"]');
        if($(e.target).is(':checked')){
            checkbox.prop('checked',true);
        }else{
            checkbox.prop('checked',false);
        }
    }else{
        //下一级全部选中时候,上一级选中
        let all = $(e.target).parent().parent().find('input[type="checkbox"]').length;
        let len = $(e.target).parent().parent().find('input[type="checkbox"]:checked').length;
        console.log('多少input',all);
        console.log('选中多少input',len);
        if(all == len){
          $(e.target).parent().parent().prev().find('input[type="checkbox"]').prop('checked',true);
        }else{
          $(e.target).parent().parent().prev().find('input[type="checkbox"]').prop('checked',false);
        }
    }

    //读取选中的值
    // console.log($(`input[name=${keyname}]:checked`).length);
    let arr = [];
    $(`input[name=${keyname}]:checked`).each(function(index,ele){
        // console.log($(ele).attr('data'));
        arr.push(parseInt($(ele).attr('data')));
    });
    console.log('获取到的id集合',arr);
    this.form[keyname] = arr.join(',');

  }
});
</script>

# 三、后台根据权限id显示相应管理栏目

# 1. 创建json文件(推荐),也可以写入数据库,展示后台管理栏目

data/root.json

[
    {"id":1,"pid":0, "name": "网站", "icon": "fa fa-chrome", "url": ""},
    {"id":2,"pid":1, "name": "主面板", "icon": "fa fa-pie-chart", "url": "/admin" },
    {"id":3,"pid":1,  "name": "管理员", "icon": "fe fe-user-plus", "url": "/admin/manager/index" },
    {"id":4,"pid":1,  "name": "留言板", "icon": "fe fe-document", "url": "/admin/message/index" },
    {"id":5,"pid":1, "name": "分类管理", "icon": "fa fa-list", "url": "/admin/category/index" },
    {"id":6,"pid":1, "name": "新闻内容管理", "icon": "fa fa-file-text-o", "url": "/admin/news/index" },
    {"id":7,"pid":1, "name": "配置管理", "icon": "fa fa-wrench", "url": "/admin/config/index" },
    {"id":8,"pid":0, "name": "直播","icon": "fa fa-video-camera", "url": ""},
    {"id":9,"pid":8, "name": "直播用户", "icon": "fa fa-venus-mars", "url": "/admin/liveuser/index" },
    {"id":10,"pid":8, "name": "直播礼物管理", "icon": "fa fa-gift", "url": "/admin/livegift/index" },
    {"id":11,"pid":8, "name": "直播订单管理", "icon": "fa fa-credit-card", "url": "/admin/liveorder/index" },
    {"id":12,"pid":8, "name": "直播间管理", "icon": "fa fa-tv", "url": "/admin/live-/index" },
    {"id":13,"pid":1, "name": "网站公告管理", "icon": "fa fa-tv", "url": "xxxx" }
]

# 2.读取json文件栏目菜单

控制器 app/controller/admin/manager.js

const fs = require('node:fs');

//创建管理员---创建页面表单
async create() {
  const { ctx } = this;
  // await ctx.render('admin/manager/create.html');

  // let data = [
  //   { id: 1, pid: 0, name: '网站', icon: 'fa fa-chrome', url: '' },
  //   { id: 2, pid: 1, name: '主面板', icon: 'fa fa-pie-chart', url: '/admin' },
  //   { id: 3, pid: 1, name: '管理员', icon: 'fe fe-user-plus', url: '/admin/manager/index' },
  //   { id: 4, pid: 1, name: '留言板', icon: 'fe fe-document', url: '/admin/message/index' },
  //   { id: 5, pid: 1, name: '分类管理', icon: 'fa fa-list', url: '/admin/category/index' },
  //   { id: 6, pid: 1, name: '新闻内容管理', icon: 'fa fa-file-text-o', url: '/admin/news/index' },
  //   { id: 7, pid: 1, name: '配置管理', icon: 'fa fa-wrench', url: '/admin/config/index' },
  //   { id: 8, pid: 0, name: '直播', icon: 'fa fa-video-camera', url: '' },
  //   { id: 9, pid: 8, name: '直播用户', icon: 'fa fa-venus-mars', url: '/admin/liveuser/index' },
  //   { id: 10, pid: 8, name: '直播礼物管理', icon: 'fa fa-gift', url: '/admin/livegift/index' },
  //   { id: 11, pid: 8, name: '直播订单管理', icon: 'fa fa-credit-card', url: '/admin/liveorder/index' },
  //   { id: 12, pid: 8, name: '直播间管理', icon: 'fa fa-tv', url: '/admin/live-/index' },
  // ];

  let data = fs.readFileSync('./data/root.json', {
      encoding: 'utf-8'
  });
  data = this.app.transformToTree(JSON.parse(data));
  // console.log('处理之后的data', JSON.stringify(data));
  // return;
  ...
}

菜单中间件 app/middleware/admin_menu.js

module.exports = (option, app) => {
    return async function adminMenu(ctx, next) {
      // let menus = [
      //     {id:1,pid:0,  name: '网站', icon: 'fa fa-chrome', url: '' },
      //     {id:2,pid:1, name: '主面板', icon: 'fa fa-pie-chart', url: '/admin' },
      //     {id:3,pid:1, name: '管理员', icon: 'fe fe-user-plus', url: '/admin/manager/index' },
      //     {id:4,pid:1, name: '留言板', icon: 'fe fe-document', url: '/admin/message/index' },
      //     {id:5,pid:1, name: '分类管理', icon: 'fa fa-list', url: '/admin/category/index' },
      //     {id:6,pid:1, name: '新闻内容管理', icon: 'fa fa-file-text-o', url: '/admin/news/index' },
      //     {id:7,pid:1, name: '配置管理', icon: 'fa fa-wrench', url: '/admin/config/index' },
      //     {id:8,pid:0, name: '直播', icon: 'fa fa-video-camera', url: '' },
      //     {id:9,pid:8, name: '直播用户', icon: 'fa fa-venus-mars', url: '/admin/liveuser/index' },
      //     {id:10,pid:8, name: '直播礼物管理', icon: 'fa fa-gift', url: '/admin/livegift/index' },
      //     {id:11,pid:8, name: '直播订单管理', icon: 'fa fa-credit-card', url: '/admin/liveorder/index' },
      //     {id:12,pid:8, name: '直播间管理', icon: 'fa fa-tv', url: '/admin/live-/index' },
      // ];
      let menus = fs.readFileSync('./data/root.json', {
          encoding: 'utf-8'
      });
      menus = JSON.parse(menus);

      //根据权限显示菜单:普通管理员的处理
      console.log('管理员信息', ctx.session.auth);
      if (ctx.session.auth.super != 1) {
        if (!ctx.session.auth.auth) {
            menus = [{ id: 2, pid: 1, name: '主面板', icon: 'fa fa-pie-chart', url: '/admin' }];
        } else {
            // 根据权限分配管理栏目
            const ids = ctx.session.auth.auth;
            const selectedIds = ids.split(',').map(id => parseInt(id));
            let result = [];
            for(let i=0;i<selectedIds.length;i++){
                let id = selectedIds[i];
                for(let j=0;j<menus.length;j++){
                  if(menus[j].id == id){
                    result.push(menus[j]);
                  }
                }
            }
            console.log('最终结果', result);
            menus = result;
        }
      }

      // 和我们分页模版类似,通过ctx.locals对象挂载代码
      ...
    }
}

模版 app/view/admin/layout/_form.html方法进一步调整

treeDataSelectClick(keyname,name,id,classname,e){
    // console.log('keyname', keyname);
    // console.log('name', name);
    // console.log('id', id);
    // console.log('classname', classname);
    // console.log('事件对象', e);

    // 选中上一级下一级所有都选中
    // console.log('父级class', $(e.target).parent().parent().attr('class'));
    if($(e.target).parent().parent().attr('class') != 'dropdown-submenu'){
        let checkbox = $(e.target).parent().next().find('input[type="checkbox"]'); 
        if($(e.target).is(':checked')){
            checkbox.prop('checked',true).attr('select',1);
        }else{
            checkbox.prop('checked',false).attr('select',0);
        }
        // 设置自己的select
        if($(e.target).is(':checked')){
            $(e.target).attr('select',1);
        }else{
            $(e.target).attr('select',0);
        }
    }else{
        //每个级别下的所有多选框数量
        let all = $(e.target).parent().parent().find('input[type="checkbox"]').length;
        //当前这个级别下面选了多少个框了
        let len = $(e.target).parent().parent().find('input[type="checkbox"]:checked').length;
        // console.log('当前级别总共多少框',all);
        // console.log('当前级别选了多少框',len);
        if(all == len){
            $(e.target).parent().parent().prev().find('input[type="checkbox"]')
            .prop('checked',true).attr('select',1);
        }else{
            $(e.target).parent().parent().prev().find('input[type="checkbox"]')
            .prop('checked',false).attr('select',1);
        }
        // 设置自己的select
        if($(e.target).is(':checked')){
            $(e.target).attr('select',1);
        }else{
            $(e.target).attr('select',0);
        }
        //如果一个没选
        if(len == 0){
            $(e.target).parent().parent().prev().find('input[type="checkbox"]')
            .prop('checked',false).attr('select',0);
        }

    }

    //选取选中的值
    // console.log('选取框的长度', $(`input[name=${keyname}]:checked`).length);
    let arr = [];
    // $(`input[name=${keyname}]:checked`).each(function(index,element){
    //      console.log($(element).attr('data'));
    //      arr.push(parseInt($(element).attr('data')));
    // });
    $(`input[name=${keyname}]`).each(function(index,element){
        console.log('select的情况', $(element).attr('select'));
        if($(element).attr('select') == 1){
            arr.push(parseInt($(element).attr('data')));
        } 
    });

    //  console.log('获取权限的id值集合数据',arr);
    console.log('写进数据库数据',arr.join(','));
    this.form[keyname] = arr.join(',');

}

# 四、管理员根据所分配的权限访问对应栏目,否则无权访问

菜单中间件 app/middleware/admin_menu.js

const fs = require('node:fs');

module.exports = (option, app) => {
  return async function adminMenu(ctx, next) {
    console.log('菜单中间件');
    // let menus = [
    //     {id:1,pid:0,  name: '网站', icon: 'fa fa-chrome', url: '' },
    //     {id:2,pid:1, name: '主面板', icon: 'fa fa-pie-chart', url: '/admin' },
    //     {id:3,pid:1, name: '管理员', icon: 'fe fe-user-plus', url: '/admin/manager/index' },
    //     {id:4,pid:1, name: '留言板', icon: 'fe fe-document', url: '/admin/message/index' },
    //     {id:5,pid:1, name: '分类管理', icon: 'fa fa-list', url: '/admin/category/index' },
    //     {id:6,pid:1, name: '新闻内容管理', icon: 'fa fa-file-text-o', url: '/admin/news/index' },
    //     {id:7,pid:1, name: '配置管理', icon: 'fa fa-wrench', url: '/admin/config/index' },
    //     {id:8,pid:0, name: '直播', icon: 'fa fa-video-camera', url: '' },
    //     {id:9,pid:8, name: '直播用户', icon: 'fa fa-venus-mars', url: '/admin/liveuser/index' },
    //     {id:10,pid:8, name: '直播礼物管理', icon: 'fa fa-gift', url: '/admin/livegift/index' },
    //     {id:11,pid:8, name: '直播订单管理', icon: 'fa fa-credit-card', url: '/admin/liveorder/index' },
    //     {id:12,pid:8, name: '直播间管理', icon: 'fa fa-tv', url: '/admin/live-/index' },
    // ];

    let menus = fs.readFileSync('./data/root.json', {
      encoding: 'utf-8'
    });
    menus = JSON.parse(menus);

    //根据权限显示菜单:普通管理员的处理
    //  console.log('管理员信息', ctx.session.auth);
    if (ctx.session.auth.super != 1) {
      if (!ctx.session.auth.auth) {
        // menus = [{id:2,pid:1, name: '主面板', icon: 'fa fa-pie-chart', url: '/admin' }];
        // return ctx.pageFail('没有权限访问后台管理',500);
        return ctx.apiFail('没有权限访问后台管理', 500);
      } else {
        const ids = ctx.session.auth.auth;
        const selectedIds = ids.split(',').map(id => parseInt(id));
        // console.log('selectedIds', selectedIds);
        let result = [];
        for (let i = 0; i < selectedIds.length; i++) {
          let id = selectedIds[i];
          for (let j = 0; j < menus.length; j++) {
            if (menus[j].id == id) {
              result.push(menus[j]);
            }
          }
        }
        // console.log('最终结果',result);
        menus = result;

        // 权限判断
        let arr = result.map(item=>item.url && item.url.replace('/index', ''));
        arr = arr.filter(item=>item.length);
        arr.push('/admin/logout');//任何管理员都可以退出
        console.log('arr',arr);
        if(ctx.request.url == '/admin'){
          if(!arr.includes('/admin')){
            return ctx.apiFail('没有权限访问该内容', 500);
          }
        }else{
          arr = arr.filter(item=>item != '/admin');
          console.log('新的arr',arr);
          let flag = false;
          for (let i = 0; i < arr.length; i++) {
             if(ctx.request.url.startsWith(arr[i])){
               flag = true;
               break;
             }
          }
          if(!flag){
            return ctx.apiFail('没有权限访问该内容', 500);
          }
        }
      }
    }

    // 和我们分页模版类似,通过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('处理之后遍历项地址', baseURL);
      //   console.log('判断',ctx.request.url.startsWith(baseURL));
      if ((ctx.request.url == '/admin' && item.url == '/admin') ||
        (ctx.request.url != '/admin' && item.url != '/admin' && ctx.request.url.startsWith(baseURL) && item.url)
      ) {
        item.active = 'active';
      }

      if (item.url) {
        item.style = "text-indent:2em";
      } else {
        item.style = "cursor:default";
      }

      return item;
    });

    console.log(ctx.locals.menus);
    //转成树形结构数据
    ctx.locals.menus = app.transformToTree(ctx.locals.menus);
    console.log(ctx.locals.menus);

    await next();
  }
}

菜单模版 app/view/admin/layout/_slider.html

{# 定义一个可递归调用的子模板 #}
{% macro renderMenu(items) %}
{% for item in items %}
<li class="{{item.active}}" >
    <a href="{{item.url}}" style="{{item.style}}"><i class="{{item.icon}}" style="font-size: 18px;"></i> <span>{{item.name}}</span></a>
</li>
{# 检查是否存在子菜单项 #}
{% if item.children and item.children.length %}
{% call renderMenu(item.children) %}
{# 这里通过调用macro自身实现了递归 #}
{% endcall %}
{% endif %}
{% endfor %}
{% endmacro %}

<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}}" style="{{item.style}}"><i class="{{item.icon}}" style="font-size: 18px;"></i> <span>{{item.name}}</span></a>
                </li>
                {% endfor %} #}

                {# 使用定义好的子模板递归渲染菜单 #}
                <!-- 在上面的代码块之前或之外的合适位置,提前解析item.default为JSON对象 -->
                {% call renderMenu(ctx.locals.menus) %}
                {% endcall %}


            </ul>
        </div>
    </div>
</div>

# 五、超级管理员修改普通管理员权限及,超级管理员可在管理员列表查看各个管理员权限

控制器 app/controller/admin/manager.js

//修改管理员界面
  async edit() {

    const { ctx, app } = this;
    const id = ctx.params.id;
    let data = await app.model.Manager.findOne({
      where: {
        id
      }
    });
    if (!data) {
      return ctx.pageFail('该管理员不存在', 404);
    }
    data = JSON.parse(JSON.stringify(data));
    delete data.password;
    console.log(data);

    //读取所有权限栏目
    let fields = [];
    if(this.ctx.session.auth.super == 1){
      let roots =  fs.readFileSync('./data/root.json', {
        encoding: 'utf-8'
      });
      roots = this.app.transformToTree(JSON.parse(roots));
      //赋值给表单字段
      fields = [
        {
          label: '权限分配',
          type: 'treeDataSelect',//树形结构数据选择
          name: 'auth',
          default: JSON.stringify(roots),
        }
      ];
    }
    




    //渲染公共模版
    await ctx.renderTemplate({
      id,
      title: '修改管理员',//现在网页title,面包屑导航title,页面标题
      tempType: 'form', //模板类型:table表格模板 ,form表单模板
      form: {
        //修改管理员提交地址
        action: '/admin/manager/update/' + id,
        //  字段
        fields: [
          {
            label: '管理员账号',
            type: 'text',
            name: 'username',
            placeholder: '请输入管理员账号',
          },
          {
            label: '管理员密码',
            type: 'password',
            name: 'password',
            placeholder: '请输入管理员密码',
          },
          {
            label: '管理员头像',
            type: 'file',
            name: 'avatar',
          },
          ...fields,
        ],
        //修改内容默认值
        data,
      },
      //修改成功之后跳转到哪个页面
      successUrl: '/admin/manager/index',
    });

  }
  //修改管理员数据功能
  async update() {

    if (this.ctx.session.auth.super == 0 && this.ctx.params.id != this.ctx.session.auth.id) {
      return this.ctx.apiFail('您无权操作此项功能');
    }

    const { ctx, app } = this;
    //1.参数验证
    this.ctx.validate({
      id: {
        type: 'int',
        required: true,
        desc: '管理员id'
      },
      username: {
        type: 'string',  //参数类型
        required: true, //是否必须
        // defValue: '', 
        desc: '管理员账号' //字段含义
      },
      password: {
        type: 'string',
        // required: true,
        // defValue: '', 
        desc: '管理员密码'
      },
      avatar: {
        type: 'string',
        required: false,
        // defValue: '', 
        desc: '管理员头像'
      },
      auth: {
        type: 'string',
        required: false,
        // defValue: '', 
        desc: '权限id'
      },
    });

    // 参数
    const id = ctx.params.id;
    const { username, password, avatar,auth } = ctx.request.body;
    // 先看一下管理员是否存在
    const manager = await app.model.Manager.findOne({ where: { id } });
    if (!manager) {
      return ctx.pageFail('该管理员记录不存在');
    }
    //存在,由于管理员的账号具有唯一性,你不能修改账号的时候,修改成存在的账号
    const Op = this.app.Sequelize.Op;//拿Op,固定写法
    if (await app.model.Manager.findOne({
      where: {
        username,
        id: {
          [Op.ne]: id
        }
      }
    })) {
      // return ctx.pageFail('该管理员账号已经存在,不能修改成该管理员账号', 404);
      return ctx.apiFail('该管理员账号已经存在,不能修改成该管理员账号');
    }
    // 修改数据
    manager.username = username;
    if (password) {
      manager.password = password;
    }
    if (avatar) {
      manager.avatar = avatar;
    }

    if(auth){
      manager.auth = auth;
    }

    await manager.save();
    // 给一个反馈
    ctx.apiSuccess('修改成功');
  }

  //创建管理员列表页面
  async index() {
    const { ctx, app } = this;
    //分页:可以提炼成一个公共方法page(模型名称,where条件,其他参数options)
    let data = await ctx.page('Manager');
    // await ctx.render('admin/manager/index.html',{
    //   title: '管理员列表',
    //   data
    // });
    //超级管理员的特殊权限
    let menus = fs.readFileSync('./data/root.json', {
      encoding: 'utf-8'
    });
    menus = JSON.parse(menus);
    console.log(ctx.session.auth);
    let btns = null;
    let cols = [];
    if (ctx.session.auth.super == 1) {
      btns = {
        buttons: [
          {
            url: '/admin/manager/create',//新增路径
            desc: '新增管理员',//新增 //按钮名称
            // icon: 'fa fa-plus fa-lg',//按钮图标
          }
        ],
      };
      cols = [
        {
          title: '可用状态',
          key: 'status',
          width: 200,//可选
          class: 'text-center',//可选
          hidekeyData: true,//是否隐藏key对应的数据
          render(item) {
            console.log('可用状态里面每个item', item);
            let arr = [
              { value: 1, name: '启用' },
              { value: 0, name: '禁用' },
            ];
            let str = `<div class="btn-group btn-group-${item.id}">`;
            for (let i = 0; i < arr.length; i++) {
              str += `<button type="button" class="btn btn-light" data="${item.status}"
                    value="${arr[i].value}"
                    @click="changeBtnStatus('status','btn-group-${item.id}',${arr[i].value},${i},${item.id},'manager','Manager')">${arr[i].name}</button>`;
            }
            str += `</div>`;
            return str;
          }
        },
        {
          title: '权限',
          // key: 'auth',
          // width: 200,//可选
          class: 'text-left',//可选
          render(item) {
            console.log('每个item',item.auth);
            const ids = item.auth;
            const selectedIds = ids.split(',').map(id=>parseInt(id));
            let result = [];
            for(let i=0; i < selectedIds.length; i++){
                let id = selectedIds[i];
                for(let j=0;j<menus.length;j++){
                  if(menus[j].id == id ){
                    result.push(menus[j]);
                  }
                }
            }
            console.log('最终结果',result);
            result = result.map(item=>item.url && item.name);
            result = result.filter(item => item.length);
            let strhtml = '';
            for(let i=0;i<result.length;i++){
              strhtml += `<span style="background-color:#eeeeee;padding:5px 10px;margin-right:10px;font-size:12px;margin-top:10px;">${result[i]}</span>`;
              if(i>0 && i % 4 == 0){
                strhtml += `<div style="height:10px;"></div>`;
              }
            }
            if(item.super == 1){
              strhtml = `拥有所有的权限`;
            }
            return strhtml;
          }
        },
      ];
    } else {
      data = await ctx.page('Manager', {
        id: ctx.session.auth.id
      });
    }

    //渲染公共模版
    await ctx.renderTemplate({
      title: '管理员列表',//现在网页title,面包屑导航title,页面标题
      data,
      tempType: 'table', //模板类型:table表格模板 ,form表单模板
      table: {
        //表格上方按钮,没有不要填buttons
        // buttons: [
        //   {
        //     url: '/admin/manager/create',//新增路径
        //     desc: '新增管理员',//新增 //按钮名称
        //     // icon: 'fa fa-plus fa-lg',//按钮图标
        //   }
        // ],
        ...btns,
        //表头
        columns: [
          {
            title: '管理员账号',
            // key: 'username',
            render(item) {
              const type = item.super == 1 ? '超级管理员' : '普通管理员';
              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>${type}</span></a>
                </h2>
               `;
            },
          },
          ...cols,
          // {
          //   title: '创建时间',
          //   key: 'create_time',
          //   width: 200,//可选
          //   class: 'text-center',//可选
          // },
          {
            title: '操作',
            class: 'text-right',//可选
            action: {
              //修改
              edit: function (id) {
                return `/admin/manager/edit/${id}`;
              },
              //删除
              delete: function (id) {
                return `/admin/manager/delete/${id}`;
              }
            }
          },
        ],
      },
    });
  }

微调一下后台管理员头像和手机模式显示后台标题
app/view/admin/layout/_header.html

...
<a href="/admin" class="logo logo-small">
    {# <img src="/public/assets/img/logo-small.png" alt="Logo" width="30" height="30"> #}
    <span class="text-white" 
    style="font-size: 22px;">
        后台管理中心
    </span>
</a>
...
<span class="user-img"><img class="rounded-circle" src="{{ctx.session.auth.avatar}}" width="31" height="31" alt="Ryan Taylor"></span>
...
更新时间: 2024年5月16日星期四中午12点14分