# 一、企业网站后台栏目管理(网站导航栏,栏目分类)表 category
# 1、网站栏目分析
网站首页
关于我们
公司介绍
公司简介
企业资质
企业文化
发展历程
公司动态
公司团建
...
产品中心
产品分类
产品分类1
产品分类1-1
产品分类1-2
产品分类2
产品分类2-1
产品分类2-2
产品分类3
...
上市产品
研发产品
...
新闻中心
公司新闻
行业新闻
...
人才招聘
招聘职位
招聘要求
...
联系我们
联系方式
在线留言
...
# 2、分析怎样的数据结构可以方便展示上面的数据?
上面的结构只是做了4层分类,如果业务需求更大,还需要继续进行分类。很显然,通过外键关联查询无法办到,这个时候我们需要对数据进行处理了。
[
{
id:1,
name:"关于我们",
children:[
{
id:2,
name:"公司介绍",
children:[
{
id:5,
name:"公司简介",
children:[]
},
{
id:6,
name:"企业资质",
children:[]
},
{
id:7,
name:"企业文化",
children:[]
},
{
id:8,
name:"发展历程",
children:[]
},
]
},
{
id:3,
name:"公司动态",
children:[]
},
{
id:4,
name:"公司团建",
children:[]
},
]
},
...
{
id:40,
name:"联系我们",
children:[
{
id:41,
name:"联系方式",
children:[]
},
{
id:42,
name:"在线留言",
children:[]
}
]
}
]
//那么这样的结果,可以通过什么数据处理呢?
[
{ id:1 , name:"关于我们" , pid:0 },
{ id:2 , name:"公司介绍" , pid:1 },
{ id:3 , name:"公司动态" , pid:1 },
{ id:4 , name:"公司团建" , pid:1 },
{ id:5 , name:"公司简介" , pid:2 },
{ id:6 , name:"企业资质" , pid:2 },
{ id:7 , name:"企业文化" , pid:2 },
{ id:8 , name:"发展历程" , pid:2 },
...
]
# 3、如何将以上结构转成树形结构来方便程序处理和展示?
# 入门级方法
function transformToTree(data) {
// 创建一个空对象用于存储键值对形式的节点映射
const nodeMap = {};
// 遍历原始数据,构建节点映射表并初始化节点的 children 属性
data.forEach(item => {
const node = { id: item.id, name: item.name, children: [] };
nodeMap[item.id] = node;
if (item.pid !== 0) {
// 如果当前节点不是根节点,则将其添加到其父节点的 children 数组中
if (nodeMap[item.pid]) {
nodeMap[item.pid].children.push(node);
}
}
});
// 从映射表中提取所有 pid 为 0 的根节点
return Object.values(nodeMap).filter(node => node.pid === 0);
}
// 使用给定的数据进行转换
const menuData = [
{ id:1 , name:"关于我们" , pid:0 },
{ id:2 , name:"公司介绍" , pid:1 },
{ id:3 , name:"公司动态" , pid:1 },
{ id:4 , name:"公司团建" , pid:1 },
{ id:5 , name:"公司简介" , pid:2 },
{ id:6 , name:"企业资质" , pid:2 },
{ id:7 , name:"企业文化" , pid:2 },
{ id:8 , name:"发展历程" , pid:2 },
];
const treeData = transformToTree(menuData);
console.log(treeData);
# 树形结构方法演变
由于树形结构数据在很多场景会使用到,如:网站导航栏、产品分类、权限选择、无限极评论及回复等等常景都会用到,所以这里把整个方法的演变过程,单独给大家写了一下,感兴趣的同学可以查阅:
树形结构方法演变
# 演变最终方法:兼容性和扩展性更好的方法
function transformToTree(data, rootPid = 0, level = 0, defaultName = "未命名") {
const nodeMap = {};
data.forEach(({ id, pid, enname, ...otherProps }) => {
let name = enname || defaultName;
const node = { id, name, pid, level, children: [], ...otherProps };
nodeMap[id] = node;
if (pid !== rootPid && nodeMap[pid]) {
nodeMap[pid].children.push(node);
node.level = nodeMap[pid].level + 1;
} else if (pid === rootPid) {
node.level = level;
}
});
return Object.values(nodeMap).filter(node => node.pid === rootPid);
}
// 示例数据(这种数据更具代表性,也兼容我们上面的数据格式)
const menuData = [
{ id:1 , pid:0 },
{ id:2 , pid:1, enname:"about us" },
{ id:3 , pid:1 },
{ id:4 , pid:1 },
{ id:5 , pid:2 },
{ id:6 , pid:2 },
{ id:7 , pid:2 },
{ id:8 , pid:2 },
];
// 默认情况,rootPid 为 0,层级为 0,默认名称为 "未命名"
const treeData = transformToTree(menuData);
console.log(treeData);
# 测试方法
方法封装到 'app/extend/application.js' 扩展里面
...
//5、将普通带有层级关系数组转成树形结构数组[基本结构须有id,pid字段]
transformToTree(data, rootPid = 0, level = 0, defaultName = "未命名") {
const nodeMap = {};
data.forEach(({ id, pid, enname, ...otherProps }) => {
let name = enname || defaultName;
const node = { id, name, pid, level, children: [], ...otherProps };
nodeMap[id] = node;
if (pid !== rootPid && nodeMap[pid]) {
nodeMap[pid].children.push(node);
node.level = nodeMap[pid].level + 1;
} else if (pid === rootPid) {
node.level = level;
}
});
return Object.values(nodeMap).filter(node => node.pid === rootPid);
}
app/controller/admin/live.js 写在index方法里面感受一下
//创建直播功能中的直播间列表页面
async index() {
...
//测试树形结构
const menuData = [
{ id: 1, name: "关于我们", pid: 0 },
{ id: 2, name: "公司介绍", pid: 1 },
{ id: 3, name: "公司动态", pid: 1 },
{ id: 4, name: "公司团建", pid: 1 },
{ id: 5, name: "公司简介", pid: 2 },
{ id: 6, name: "企业资质", pid: 2 },
{ id: 7, name: "企业文化", pid: 2 },
{ id: 8, name: "发展历程", pid: 2 },
];
// 默认情况,rootPid 为 0,层级为 0,默认名称为 "未命名"
const treeData = app.transformToTree(menuData);
console.log('测试树形结构', JSON.stringify(treeData));
}
结果可用 在线json 比对一下:https://www.bejson.com/
# 4、最终表设计
企业网站后台栏目管理(网站导航栏,栏目分类)表 category 字段设计(更多字段根据业务需求来扩展)
表名:category
字段名 数据类型 描述 空 默认值 字段含义
id int(20) 主键、自增长、UNSIGNED无符号 否 无 pid int(20) 否 0 上一级(父级)id name varchar(100) 否 分类名称 enname varchar(100) 是 分类英文名称 status int(1) 否 1 分类状态 0不可用 1可用 等等状态 description varchar(255) 是 分类描述,分类简单介绍 order int(11) 是 50 分类排序,默认50 create_time datetime 否 CURRENT_TIMESTAMP 数据创建时间 update_time datetime 否 CURRENT_TIMESTAMP 数据更新时间 额外说明:
mysql每行最大只能存65535个字节。假设是utf-8编码,每个字符占3个字节。varchar存储最大字符数为(65535-2-1)/3=21844字符长度
# 5、创建迁移文件、执行迁移命令创建数据表 category
在数据库创建数据表的方式很多,在上面定义了表字段之后,可以使用
phpmyAdmin、数据库插件执行sql语句创建等等方式,但是建议大家通过:创建迁移文件、执行迁移命令创建数据表。涉及的知识点:
- 章节2:egg.js基础-五、eggjs项目中sequelize模型创建mysql数据库
创建迁移文件 命令:
npx sequelize migration:generate --name=init-category创建迁移文件:
'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('category', { id: { type: INTEGER(20).UNSIGNED, primaryKey: true, autoIncrement: true, comment: '分类主键id' }, pid: { type: INTEGER(20).UNSIGNED, allowNull: false, defaultValue: 0, comment: '上一级(父级)id', }, name: { type: STRING(100), allowNull: false, defaultValue: '', comment: '分类名称' }, enname: { type: STRING(100), allowNull: true, defaultValue: '', comment: '分类英文名称' }, status: { type: INTEGER(1), allowNull: false, defaultValue: 1, comment: '分类状态 0不可用 1可用 等等状态' }, 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('category') } };执行迁移文件命令生成数据库表:
// 升级数据库-创建数据表 npx sequelize db:migrate // 如果有问题需要回滚,可以通过 `db:migrate:undo` 回退一个变更 npx sequelize db:migrate:undo // 可以通过 `db:migrate:undo:all` 回退到初始状态 npx sequelize db:migrate:undo:all
# 6、创建 category 的模型
模型文件主要是用于处理数据库表的增删改查等操作
app/model/category.js
'use strict';
module.exports = app => {
const { INTEGER, STRING, DATE, ENUM, TEXT, BIGINT } = app.Sequelize;
const Category = app.model.define('category', {
id: {
type: INTEGER(20).UNSIGNED,
primaryKey: true,
autoIncrement: true,
comment: '分类主键id'
},
pid: {
type: INTEGER(20).UNSIGNED,
allowNull: false,
defaultValue: 0,
comment: '上一级(父级)id',
},
name: {
type: STRING(100),
allowNull: false,
defaultValue: '',
comment: '分类名称'
},
enname: {
type: STRING(100),
allowNull: true,
defaultValue: '',
comment: '分类英文名称'
},
status: {
type: INTEGER(1),
allowNull: false,
defaultValue: 1,
comment: '分类状态 0不可用 1可用 等等状态'
},
// sex: { type: ENUM, values: ['男','女','保密'], allowNull: true, defaultValue: '保密', comment: '留言用户性别'},
create_time: {
type: DATE,
allowNull: false,
defaultValue: app.Sequelize.fn('NOW'),
get() {
return app.formatTime(this.getDataValue('create_time'));
}
},
update_time: { type: DATE, allowNull: false, defaultValue: app.Sequelize.fn('NOW') }
});
return Category;
}
# 7、创建表 category 的控制器、完成后台分类列表功能
创建控制器
app/controller/admin/category.js完成功能
'use strict';
const Controller = require('egg').Controller;
class CategoryController extends Controller {
//创建分类表表单
async create() {
const { ctx } = this;
//渲染公共模版
await ctx.renderTemplate({
title: '创建分类表表单',//现在网页title,面包屑导航title,页面标题
tempType: 'form', //模板类型:table表格模板 ,form表单模板
form: {
//提交地址
action: "/admin/category/save",
// 字段
fields: [
{
label: '上一级(父级)id',
type: 'number',
name: 'pid',
default: '0',
placeholder: '上一级(父级)id',
},
{
label: '分类名称',
type: 'text',
name: 'name',
placeholder: '请输入分类名称',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '分类英文名称',
type: 'text',
name: 'enname',
placeholder: '请输入分类英文名称',
},
{
label: '分类状态',
type: 'number',
name: 'status',
default: '1',
placeholder: '分类状态 0不可用 1可用 等等状态',
}
],
},
//新增成功之后跳转到哪个页面
successUrl: '/admin/category/index',
});
}
//创建分类表表单提交数据
async save() {
//一般处理流程
//1.参数验证
this.ctx.validate({
name: {
type: 'string', //参数类型
required: true, //是否必须
// defValue: '',
desc: '分类名称' //字段含义
},
enname: {
type: 'string',
required: false,
defValue: '',
desc: '礼物图标'
},
status: {
type: 'int',
required: true,
defValue: 1,
desc: '分类状态 0不可用 1可用 等等状态'
},
pid: {
type: 'int',
required: true,
defValue: 0,
desc: '上一级(父级)id'
}
});
//先判断一下直播功能中的礼物账号是否存在,不存在在写入数据库
//2.写入数据库
//3.成功之后给页面反馈
let { name, enname, status, pid} = this.ctx.request.body;
if (await this.app.model.Category.findOne({ where: { name } })) {
return this.ctx.apiFail('分类名称已存在');
}
//否则不存在则写入数据库
const res = await this.app.model.Category.create({
name,
enname,
status,
pid
});
this.ctx.apiSuccess('创建分类成功');
}
//创建分类列表页面
async index() {
const { ctx, app } = this;
//分页:可以提炼成一个公共方法page(模型名称,where条件,其他参数options)
let data = await ctx.page('Category');
//渲染公共模版
await ctx.renderTemplate({
title: '分类列表',//现在网页title,面包屑导航title,页面标题
data,
tempType: 'table', //模板类型:table表格模板 ,form表单模板
table: {
//表格上方按钮,没有不要填buttons
buttons: [
{
url: '/admin/category/create',//新增路径
desc: '新增分类',//新增 //按钮名称
// icon: 'fa fa-plus fa-lg',//按钮图标
}
],
//表头
columns: [
{
title: '分类名称',
key: 'name',
class: 'text-center',//可选
},
{
title: '分类英文名称',
key: 'enname',
class: 'text-center',//可选
},
{
title: '创建时间',
key: 'create_time',
width: 200,//可选
class: 'text-center',//可选
},
{
title: '操作',
class: 'text-right',//可选
action: {
//修改
edit: function (id) {
return `/admin/category/edit/${id}`;
},
//删除
delete: function (id) {
return `/admin/category/delete/${id}`;
}
}
},
],
},
});
}
}
module.exports = CategoryController;
# 8、定义路由
// 创建分类界面
router.get('/admin/category/create', controller.admin.category.create);
//创建分类提交数据
router.post('/admin/category/save', controller.admin.category.save);
//创建分类列表页面
router.get('/admin/category/index', controller.admin.category.index);
# 二、后台栏目管理功能升级
# 1、根据下拉框选择新增分类到哪个栏目下
修改控制器
app/controller/admin/category.js
//创建分类表单
async create() {
const { ctx,app } = this;
// 渲染模版前先拿到所有分类
let data = await ctx.app.model.Category.findAll({
where: {
// status: 1
},
attributes: ['id', 'name','pid','status'],
});
data = JSON.parse(JSON.stringify(data));
data = data.map(item => {
return {
...item,
value: item.id,
}
});
console.log('所有分类',data);
data = app.transformToTree(data);
data.unshift({id:0,name:'一级分类',pid:0,value:0});
console.log('处理之后的分类', JSON.stringify(data));
//渲染公共模版
await ctx.renderTemplate({
title: '创建分类表单',//现在网页title,面包屑导航title,页面标题
tempType: 'form', //模板类型:table表格模板 ,form表单模板
form: {
//提交地址
action: "/admin/category/save",
// 字段
fields: [
{
label: '放在哪个分类里面',
type: 'dropdown', //下拉框
name: 'pid',
default: JSON.stringify(data), //下拉框的数据
placeholder: '',
},
{
label: '分类名称',
type: 'text',
name: 'name',
placeholder: '请输入分类名称',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '分类英文名称',
type: 'text',
name: 'enname',
placeholder: '请输入分类英文名称,选填',
},
{
label: '可用状态',
type: 'btncheck', //按钮组选择
name: 'status',
//按钮组选择提供的选择值
default: JSON.stringify([
{ value: 1, name: '可用', checked: true },
{ value: 0, name: '不可用' },
]),
placeholder: '可用状态 0不可用 1可用 等等状态',
},
],
},
//新增成功之后跳转到哪个页面
successUrl: '/admin/category/index',
});
}
调整表单模版
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}}')">{{item.name}}</span>
{# 检查是否存在子菜单项 #}
{% if item.children and item.children.length %}
<div class="dropdown-submenu">
{% call renderMenu(item.children,keyname) %}
{# 这里通过调用macro自身实现了递归 #}
{% endcall %}
</div>
{% endif %}
{% endfor %}
{% endmacro %}
<div class="card">
<div class="card-body">
{% if form %}
<form action="{{form.action}}" method="post">
{% 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' %}
<input class="form-control" type="file" 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" >
{# 如果是下拉框类型 #}
{% elif item.type == 'dropdown' and item.default %}
<div class="dropdown">
<button type="button" class="btn dropdown-toggle" data-toggle="dropdown" id="dropdownData">
请选择
</button>
<div class="dropdown-menu">
<h5 class="dropdown-header">请选择要放在哪个分类下</h5>
<div class="dropdown-divider"></div>
{# <a class="dropdown-item" href="#">关于我们</a>
<a class="dropdown-item" href="#">产品分类</a>
<a class="dropdown-item" href="#">新闻中心</a> #}
<!-- 在上面的代码块之前或之外的合适位置,提前解析item.default为JSON对象 -->
{% set itemDefaultParsed = item.default | safe | fromJson %}
{# 使用定义好的子模板递归渲染菜单 #}
{% call renderMenu(itemDefaultParsed,item.name) %}
{% endcall %}
</div>
</div>
{# 如果是按钮选择 #}
{% elif item.type == 'btncheck' %}
<div class="btn-group btn-group-{{item.name}}">
{# <button type="button" class="btn btn-success">可用</button>
<button type="button" class="btn btn-light">不可用</button> #}
{% set itemDefaultParsed = item.default | safe | fromJson %}
{% for btn in itemDefaultParsed %}
<button type="button" class="btn {{'btn-success' if btn.checked else 'btn-light'}}"
value="{{btn.value}}" @click="changeBtnStatus('{{item.name}}','btn-group-{{item.name}}','{{btn.value}}','{{ loop.index }}')">{{btn.name}}</button>
{% endfor %}
</div>
{% else %}
<input type="{{item.type}}" class="form-control" name="{{item.name}}"
placeholder="{{item.placeholder}}..."
v-model="form.{{item.name}}">
{% endif %}
</div>
</div>
{% endfor %}
...
</form>
{% endif %}
</div>
</div>
<script>
Vueapp = new Vue({
el:'#vueapp',
data(){
return {
...
}
},
methods:{
submit(){
console.log('提交成功',this.form);
console.log('下拉框默认第一项',JSON.parse(this.form.pid.replaceAll('"','"'))[0]);
console.log('按钮组默认第一项',JSON.parse(this.form.status.replaceAll('"','"'))[0]);
for(const key in this.form){
// const value = this.form[key];
// console.log(value);
if(this.form[key].indexOf('"')>-1){
this.form[key] = JSON.parse(this.form[key].replaceAll('"','"'))[0].value;
}
}
console.log('提交数据',this.form);
// return;
...
},
...
//下拉菜单选中项
dropdownItemClick(keyname,name,id){
console.log('keyname', keyname);
console.log('name', name);
console.log('id', id);
$('#dropdownData').text(name);
this.form[keyname] = id;
console.log('下拉菜单选中项此时的form', JSON.stringify(this.form));
},
//按钮组选择
changeBtnStatus(name,classname,btnValue,index){
console.log(name,classname,btnValue,index);
this.form[name] = btnValue;
console.log('按钮组选择此时的form', JSON.stringify(this.form));
$('.' + classname).find('button').eq(index-1).addClass('btn-success')
.siblings().removeClass('btn-success').addClass('btn-light');
}
}
});
</script>
新增过滤器
app/extend/filter.js
module.exports = {
fromJson(str) {
try {
return JSON.parse(str);
} catch (err) {
console.error('Failed to parse JSON:', str, err);
return null; // 或者抛出错误,取决于你的应用逻辑
}
},
};
# 2、分类列表树形结构展示
控制器
app/controller/admin/category.js
'use strict';
const Controller = require('egg').Controller;
class CategoryController extends Controller {
//分类列表页面
async index() {
const { ctx, app } = this;
//分页:可以提炼成一个公共方法page(模型名称,where条件,其他参数options)
// let data = await ctx.page('Category');
let data = await ctx.service.category.datalist({limit:1000});
console.log('分类列表数据', data);
data = data.rules;
// return;
//渲染公共模版
await ctx.renderTemplate({
...
table: {
...
//表头
columns: [
{
title: '分类名称',
key: 'name',
class: 'text-left',//可选
treeData(item){//树形结构
console.log('每个item',item);
if(item.level){
let w = item.level * 40;
return `<span style="display: inline-block;width:${w}px;"></span>`;
}
}
},
...
],
},
});
}
}
module.exports = CategoryController;
将数据写入服务service中,新建
app/service/category.js
'use strict';
const Service = require('egg').Service;
class CategoryService extends Service {
//分类数据
async datalist(options) {
const { ctx, app } = this;
let data = await ctx.page('Category', {}, {
order: [
// ['order', 'desc'],
['id', 'desc']
],
attributes: {
exclude: ['create_time', 'update_time'],//除了这些字段,其他字段都显示
},
limit: options && options.limit ? options.limit : 100,
});
// 转一下data处理
let list = JSON.parse(JSON.stringify(data));
//console.log('list', list);
let rules = JSON.parse(JSON.stringify(data));
//console.log('rules', rules);
// 数据集组合分类树(一维数组) 带level
let $rule = [];
function list_to_tree($array, $field = 'pid', $pid = 0, $level = 0) {
$array.forEach(($value, $index) => {
// console.log($value);
if ($value[$field] == $pid) {
$value['level'] = $level;
$rule.push($value);
// unset($array[$key]);
// console.log('看一下rule',$rule);
// $array.splice($index, 1);
list_to_tree($array, $field, $value['id'], $level + 1);
}
});
return $rule;
}
//数据集组合分类树(多维数组)
function list_to_tree2($cate, $field = 'pid', $child = 'child', $pid = 0, $callback = false) {
// if(!($cate instanceof Array)) return [];
if (typeof $cate != 'object' || typeof $cate.length != 'number') return [];
let $arr = [];
$cate.forEach(($v, $index) => {
let $extra = true;
if (typeof $callback == 'function') {
$extra = $callback($v);
}
if ($v[$field] == $pid && $extra) {
$v[$child] = list_to_tree2($cate, $field, $child, $v['id'], $callback);
$arr.push($v);
}
})
return $arr;
}
return {
totalCount: data.length,
rules: list_to_tree(rules),
// list: list_to_tree2(list),
list: list_to_tree2(list, 'pid', 'children',0),
}
}
}
module.exports = CategoryService;
模版调整
app/view/admin/layout/_table.html
...
{% if item2.key %}
{# 如果存在key,说明是渲染数据 #}
<td class="{{item2.class}}" width="{{item2.width}}">
{% if item2.treeData %}
{{item2.treeData(item) | safe }}
{% endif %}
<span>{{ item[item2.key] }}</span>
</td>
...
# 3、分类列表中分类状态显示及修改
控制器
app/controller/admin/category.js
//分类列表页面
async index() {
...
//渲染公共模版
await ctx.renderTemplate({
...
table: {
...
//表头
columns: [
{
title: '分类名称',
key: 'name',
class: 'text-left',//可选
render(item){ //树形数据
// console.log('每个item',item);
if(item.level){
let w = item.level * 40;
return `<span style="display:inline-block;width:${w}px"></span>`;
}
}
},
...
{
title: '可用状态',
key: 'status',
width: 200,//可选
class: 'text-center',//可选
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" value="${arr[i].value}" data="${item.status}"
@click="changeBtnStatus('status','btn-group-${item.id}',${arr[i].value},${i},${item.id})">${arr[i].name}</button>`;
}
str += `</div>`;
return str;
},
hidekeyData:true,//是否隐藏key对应的数据
},
...
],
},
});
}
...
//根据id修改分类状态
async updateStatus(){
const {id,status} = this.ctx.request.body;
//1.参数验证
this.ctx.validate({
status: {
type: 'int',
required: true,
defValue: 1,
desc: '分类状态 0不可用 1可用 等等状态'
},
id: {
type: 'int',
required: true,
// defValue: 0,
desc: '分类id'
}
});
let data = await this.app.model.Category.findByPk(id);
if (!data) {
return this.ctx.apiFail('分类数据存在');
}
data.status = status;
await data.save();
this.ctx.apiSuccess('修改分类状态成功');
}
路由
app/router/admin/admin.js
//根据id修改分类状态
router.post('/admin/category/updateStatus', controller.admin.category.updateStatus);
模版调整
app/view/admin/layout/_table.html
需要在vue.js钩子函数里面使用jquery,记得在主模版app/view/admin/layout/main_app.html将jquery.js的引入提前到vue.js引入之前
...
{% if item2.key %}
{# 如果存在key,说明是渲染数据 #}
<td class="{{item2.class}}" width="{{item2.width}}">
{% if item2.render %}
{{ item2.render(item) | safe }}
{% endif %}
{% if not item2.hidekeyData %}
<span>{{ item[item2.key] }}</span>
{% endif %}
</td>
...
<script>
Vueapp = new Vue({
el:'#vueapp',
methods:{
...
//按钮组选中项
changeBtnStatus(keyname,classname,btnValue,index,id){
console.log(keyname,classname,btnValue,index,id);
$.ajax({
type: 'POST',
url: "/admin/category/updateStatus?_csrf={{ctx.csrf|safe}}",
contentType:'application/json;charset=UTF-8;',
data:JSON.stringify({
id,
status:btnValue
}),
success: function (response, stutas, xhr) {
console.log(response)
Vueapp.$refs.toast.show({
msg:"修改成功",
type:'success',
delay:1000,
success:function(){
// 跳转到某个页面
window.location.href = "/admin/category/index";
}
});
},
error:function(e){
console.log(e)
Vueapp.$refs.toast.show({
msg:e.responseJSON.data,
type:'danger',
delay:3000
});
}
});
},
},
mounted(){
$('.btn-group').each(function(index,ele){
$(ele).find('button').each(function(index,element){
// console.log('每一个button的data', $(element).attr('data'));
// console.log('每一个button的value', $(element).attr('value'));
if($(element).attr('data') == $(element).attr('value')){
$(element).removeClass('btn-light').addClass('btn-success');
}
})
});
}
});
</script>
# 4、对分类表补充两个字段:排序字段order、分类描述字段description
先对迁移文件、模型文件、数据库表针对分类表新增一个排序字段:order,并对新增分类增加排序字段
数据库表手动增加排序字段order、分类描述字段description
迁移文件:database/migrations/...category.js模型文件:app/model/category.js
...
description: {
type: STRING, //不限定长度.默认varchar(255)
allowNull: true,
defaultValue: '',
comment: '分类描述'
},
order: {
type: INTEGER, //不限定长度.默认int(11)
allowNull: true,
defaultValue: 50,
comment: '分类排序,默认50'
},
...
新增分类添加排序和描述,控制器
app/controller/admin/category.js
//创建分类表单
async create() {
const { ctx, app } = this;
// 渲染模版前先拿到所有分类
let data = await ctx.app.model.Category.findAll({
where: {
// status: 1
},
attributes: ['id', 'name', 'pid', 'status'],
});
data = JSON.parse(JSON.stringify(data));
data.unshift({ id: 0, name: '一级分类', pid: 0, status: 1, value: 0 });
data = data.map(item => {
return {
...item,
value: item.id
}
});
console.log(data);
data = app.transformToTree(data);
console.log('处理之后的分类', JSON.stringify(data));
//渲染公共模版
await ctx.renderTemplate({
title: '创建分类表单',//现在网页title,面包屑导航title,页面标题
tempType: 'form', //模板类型:table表格模板 ,form表单模板
form: {
//提交地址
action: "/admin/category/save",
// 字段
fields: [
{
label: '放在哪个分类里面',
type: 'dropdown', //下拉框
name: 'pid',
default: JSON.stringify(data),
placeholder: '',
},
{
label: '分类名称',
type: 'text',
name: 'name',
placeholder: '请输入分类名称',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '分类英文名称',
type: 'text',
name: 'enname',
placeholder: '请输入分类英文名称',
},
{
label: '分类描述',
type: 'text',
name: 'description',
placeholder: '请输入分类描述,选填',
},
{
label: '分类排序',
type: 'number',
name: 'order',
placeholder: '分类排序,默认50',
default:50
},
{
label: '可用状态',
type: 'btncheck', //按钮组选择
name: 'status',
default: JSON.stringify([
{ value: 1, name: '可用', checked: true },
{ value: 0, name: '不可用' },
]),
placeholder: '分类状态 0不可用 1可用 等等状态',
},
],
},
//新增成功之后跳转到哪个页面
successUrl: '/admin/category/index',
});
}
//分类表单提交数据
async save() {
//一般处理流程
//1.参数验证
this.ctx.validate({
name: {
type: 'string', //参数类型
required: true, //是否必须
// defValue: '',
desc: '分类名称' //字段含义
},
enname: {
type: 'string',
required: false,
defValue: '',
desc: '分类英文名称'
},
status: {
type: 'int',
required: true,
defValue: 1,
desc: '分类状态 0不可用 1可用 等等状态'
},
pid: {
type: 'int',
required: true,
defValue: 0,
desc: '上一级(父级)id'
},
description: {
type: 'string',
required: false,
defValue: '',
desc: '分类描述,分类简单介绍'
},
order: {
type: 'int',
required: false,
defValue: 50,
desc: '分类排序,默认50'
}
});
//先判断一下直播功能中的礼物账号是否存在,不存在在写入数据库
//2.写入数据库
//3.成功之后给页面反馈
let { name, enname, status, pid,description, order} = this.ctx.request.body;
if (await this.app.model.Category.findOne({ where: { name } })) {
return this.ctx.apiFail('分类名称已存在');
}
//否则不存在则写入数据库
const res = await this.app.model.Category.create({
name,
enname,
status,
pid,
description, order
});
this.ctx.apiSuccess('创建分类成功');
}
# 5、修改分类
控制器
app/controller/admin/category.js
//修改分类界面
async edit() {
const { ctx, app } = this;
const id = ctx.params.id;
let currentdata = await app.model.Category.findOne({
where: {
id
}
});
if (!currentdata) {
return ctx.apiFail('该分类不存在');
}
currentdata = JSON.parse(JSON.stringify(currentdata));
console.log('当前分类数据', currentdata);
// return;
//下拉框获取所有分类
let data = await ctx.service.category.dropdown_Categorylist();
console.log('下拉框显示的所有分类', JSON.stringify(data));
//return;
//渲染公共模版
await ctx.renderTemplate({
id,
title: '修改分类:' + currentdata.name ,//现在网页title,面包屑导航title,页面标题
tempType: 'form', //模板类型:table表格模板 ,form表单模板
form: {
//修改直播功能中的礼物提交地址
action: '/admin/category/update/' + id,
// 字段
fields: [
{
label: '放在哪个分类里面',
type: 'dropdown', //下拉框
name: 'pid',
default: JSON.stringify(data),
placeholder: '',
id:id,//有id说明操作是修改不是新增
},
{
label: '分类名称',
type: 'text',
name: 'name',
placeholder: '请输入分类名称',
// default:'默认值测试', //新增时候默认值,可选
},
{
label: '分类英文名称',
type: 'text',
name: 'enname',
placeholder: '请输入分类英文名称',
},
{
label: '分类描述',
type: 'text',
name: 'description',
placeholder: '请输入分类描述,选填',
},
{
label: '分类排序',
type: 'number',
name: 'order',
placeholder: '请输入分类排序',
default:50,
},
{
label: '可用状态',
type: 'btncheck', //按钮组选择
name: 'status',
default: JSON.stringify([
{ value: 1, name: '可用', checked: true },
{ value: 0, name: '不可用' },
]),
placeholder: '分类状态 0不可用 1可用 等等状态',
},
],
//修改内容默认值
data:currentdata,
},
//修改成功之后跳转到哪个页面
successUrl: '/admin/category/index',
});
}
//修改分类数据
async update() {
const { ctx, app } = this;
//1.参数验证
this.ctx.validate({
id: {
type: 'int',
required: true,
desc: '分类id'
},
name: {
type: 'string', //参数类型
required: true, //是否必须
// defValue: '',
desc: '分类名称' //字段含义
},
enname: {
type: 'string',
required: false,
defValue: '',
desc: '分类英文名称'
},
status: {
type: 'int',
required: true,
defValue: 1,
desc: '分类状态 0不可用 1可用 等等状态'
},
pid: {
type: 'int',
required: true,
defValue: 0,
desc: '上一级(父级)id'
},
description: {
type: 'string',
required: false,
defValue: '',
desc: '分类描述,分类简单介绍'
},
order: {
type: 'int',
required: false,
defValue: 50,
desc: '分类排序'
}
});
// 参数
const id = ctx.params.id;
const { name, enname, status, pid,description,order } = ctx.request.body;
// 先看一下是否存在
const category = await app.model.Category.findOne({ where: { id } });
if (!category) {
return ctx.apiFail('该分类记录不存在');
}
//存在,分类名称可以一样,只要保证它在不同的分类下面
const Op = this.app.Sequelize.Op;//拿Op,固定写法
//先查一下修改的分类名称,是否已经存在,如果存在,但是只要不放在同一分类下还是可以的
const hasdata = await app.model.Category.findOne({
where: {
name,
id: {
[Op.ne]: id
}
}
});
if (hasdata && hasdata.pid == pid) {
return ctx.apiFail('同一个分类下不能有相同的分类名称');
}
// 修改数据
category.name = name;
category.enname = enname;
category.status = status;
category.pid = pid;
category.description = description;
category.order = order;
await category.save();
// 给一个反馈
ctx.apiSuccess('修改分类成功');
}
服务
app/service/category.js
//下拉框获取所有分类
async dropdown_Categorylist() {
const { ctx, app } = this;
// 渲染模版前先拿到所有分类
let data = await ctx.app.model.Category.findAll({
where: {
// status: 1
},
attributes: ['id', 'name', 'pid', 'status'],
});
data = JSON.parse(JSON.stringify(data));
data.unshift({ id: 0, name: '一级分类', pid: 0, status: 1, value: 0 });
data = data.map(item => {
return {
...item,
value: item.id
}
});
// console.log(data);
data = app.transformToTree(data);
// console.log('处理之后的分类', JSON.stringify(data));
return data;
}
路由
app/router/admin/admin.js
//修改分类界面
router.get('/admin/category/edit/:id', controller.admin.category.edit);
//修改分类数据功能
router.post('/admin/category/update/:id', controller.admin.category.update);
模版
app/view/admin/layout/_form.html
{# 如果是下拉框类型 #}
<div class="dropdown">
<button type="button" class="btn dropdown-toggle dropdown-{{item.name}}" data-toggle="dropdown" id="dropdownData">
{{'不调整(如需调整请选择)' if item.id else '一级分类'}}
</button>
...
# 6、删除分类
控制器
app/controller/admin/category.js
//删除分类
async delete() {
const { ctx, app } = this;
const id = ctx.params.id;
let data = await ctx.service.category.categorylist();
// console.log(app._collectNodeIds(data, Number(id)));
let ids = app._collectNodeIds(data, Number(id));
await app.model.Category.destroy({
where: {
id:{
[this.app.Sequelize.Op.in] : ids
}
}
});
//提示
ctx.toast('分类删除成功', 'success');
//跳转
ctx.redirect('/admin/category/index');
}
服务
app/service/category.js
//下拉框获取所有分类
async dropdown_Categorylist() {
const { ctx, app } = this;
let data = await this.categorylist();
data = app.transformToTree(data);
// console.log('处理之后的分类', JSON.stringify(data));
return data;
}
//取所有分类
async categorylist() {
const { ctx, app } = this;
// 渲染模版前先拿到所有分类
let data = await ctx.app.model.Category.findAll({
where: {
// status: 1
},
attributes: ['id', 'name', 'pid', 'status'],
});
data = JSON.parse(JSON.stringify(data));
data.unshift({ id: 0, name: '一级分类', pid: 0, status: 1, value: 0 });
data = data.map(item => {
return {
...item,
value: item.id
}
});
console.log('取所有分类',data);
return data;
}
扩展方法
app/extend/application.js
//6、根据id和父级pid的关系,获取所有子节点id
collectNodeIds(data, nodeId) { //数据量不大使用
const result = [];
const visited = new Set();
function recursiveCollect(node) {
if (!visited.has(node.id)) {
result.push(node.id);
visited.add(node.id);
const children = data.filter(child => child.pid === node.id);
children.forEach(recursiveCollect);
}
}
const targetNode = data.find(node => node.id === nodeId);
if (targetNode !== undefined) {
recursiveCollect(targetNode);
}
return result;
},
//7、对6的升级,(构建节点索引)会更高效,特别是在处理大量数据时
buildNodeIndex(data) {
const nodeIndex = new Map();
for (const node of data) {
if (!nodeIndex.has(node.pid)) {
nodeIndex.set(node.pid, []);
}
nodeIndex.get(node.pid).push(node);
}
return nodeIndex;
},
_collectNodeIds(data, nodeId, nodeIndex = this.buildNodeIndex(data)) {
// console.log('nodeIndex',nodeIndex);
const result = [];
const visited = new Set();
function recursiveCollect(node) {
if (!visited.has(node.id)) {
result.push(node.id);
visited.add(node.id);
const children = nodeIndex.get(node.id) || [];
children.forEach(recursiveCollect);
}
}
const targetNode = data.find(node => node.id === nodeId);
if (targetNode !== undefined) {
recursiveCollect(targetNode);
}
return result;
},
路由
app/router/admin/admin.js
//删除分类功能
router.get('/admin/category/delete/:id', controller.admin.category.delete);