
本章节是在于如何实现 vue + eggjs 实现一个菜单树的添加
章节介绍
框架
vue3 + elementPlus + axios
大致要求
现在开始 code
数据库结构

后端代码 code
新建
service -> menu.js
"use strict";
const Service = require("egg").Service;
class MenuService extends Service {
// 查询
async query(params) {
const { app } = this;
const { whereObj, limit, offset } = params;
try {
const list = await app.mysql.select("menu", {
where: whereObj,
limit,
offset,
});
const total = await app.mysql.count("menu", whereObj);
return { list, total };
} catch (error) {
return null;
}
}
// 添加菜单
async addMenu(params) {
const { app } = this;
const { pid, icon, url, status, title , menuType, buttonRole,orders } = params;
try {
const result = await app.mysql.insert("menu", {
pid,
icon,
url,
status,
title,
menuType,
buttonRole,
orders
});
return result;
} catch (error) {
console.log(error);
return null;
}
}
// 编辑
async editMenu(params) {
const { app } = this;
const { pid, icon, url, status, title , menuType, buttonRole,orders } = params;
try {
const result = await app.mysql.update(
"menu",
{
pid,
icon,
url,
status,
title,
menuType,
buttonRole,
orders
},
{
where: {
id: params.id,
},
}
);
return result;
} catch (error) {
console.log(error);
return null;
}
}
// 删除
async delete(id) {
const { app } = this;
try {
const result = await app.mysql.update(
"menu",
{ deleteFlag: 0 },
{
where: {
id:id
},
}
);
return result;
} catch (error) {
console.log(error);
return null;
}
}
}
module.exports = MenuService;
新建
controller -> menu.js
"use strict";
const Controller = require("egg").Controller;
const moment = require("moment");
const { responseFormat, responseHandleFormat } = require("../utils/utils");
class MenuController extends Controller {
async query() {
const { ctx } = this;
const { page = 1, ...filter } = ctx.query;
const limit = parseInt(ctx.query.limit) || 10;
// 假删除
const whereObj = { deleteFlag: 1 };
for (const x in filter) {
switch (x) {
case "id":
whereObj.id = filter[x];
break;
default:
break;
}
}
const { total, list } = await ctx.service.menu.query({
whereObj,
limit: limit ? limit : null,
offset: page ? (page - 1) * limit : null,
});
ctx.body = responseFormat(true, {
limit,
page,
total,
pages: Math.ceil(total / limit),
list,
});
}
async saveMenu() {
const { ctx } = this;
const { id, pid = 0, title, icon, url, status , menuType , buttonRole,orders } = ctx.request.body;
let result = null;
if (id) {
result = await ctx.service.menu.editMenu({
id,
pid,
title,
icon,
url,
status,
menuType,
buttonRole,
orders
});
} else {
result = await ctx.service.menu.addMenu({
pid,
title,
icon,
url,
status,
menuType,
buttonRole,
orders,
createtime: moment(new Date()).format("YYYY/MM/DD"),
});
}
if (result) {
ctx.body = responseHandleFormat(true);
} else {
ctx.body = responseHandleFormat(false);
}
}
async deleteMenu() {
const { ctx } = this;
const { id } = ctx.request.body;
const result = await ctx.service.menu.delete(id);
if (result) {
ctx.body = responseFormat(true, {
result,
});
} else {
ctx.body = responseHandleFormat(false);
}
}
}
module.exports = MenuController;
添加路由
router.get('/api/menus/role/query',_jwt, controller.menu.query);
router.post('/api/menus/role/save', _jwt,controller.menu.saveMenu);
router.post('/api/menus/role/delete',_jwt,controller.menu.deleteMenu);
前端代码 code
utils.js
主要 添加两个方法 一个格式化菜单 ,一个 排序
/**
* 排序
* @param {String} 需要排序的字段
* @return {Function} 返回排序的方法
*/
export const compare = (property) => {
return function (a, b) {
var value1 = a[property];
var value2 = b[property];
return value1 - value2;
};
};
/**
* 菜单排序
* @param {json} json数据
* @param {String} id的字符串
* @param {String} 父id的字符串
* @param {String} children的字符串
* @return {Array} 数组
*/
export const transData = (a, idStr, pidStr, childrenStr) => {
var r = [],
hash = {},
id = idStr,
pid = pidStr,
children = childrenStr,
i = 0,
j = 0,
len = a.length;
for (; i < len; i++) {
hash[a[i][id]] = a[i];
}
for (; j < len; j++) {
var aVal = a[j],
hashVP = hash[aVal[pid]];
if (hashVP ) {
!hashVP[children] && (hashVP[children] = []);
aVal.parentsNode = hashVP;
// 如果菜单是按钮,则不添加他的子菜单
if( hashVP.menuType != '1'){
continue
}
hashVP[children].push(aVal);
hashVP[children] = hashVP[children].sort(compare("order"));
} else {
r.push(aVal);
r = r.sort(compare("order"));
}
}
return r;
};
页面代码
<template>
<div>
<div class="crumbs">
<el-breadcrumb separator="/">
<el-breadcrumb-item>
<i class="el-icon-lx-cascades"></i> 菜单管理
</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="menu-container">
<el-row>
<el-col :span="5" class="basic-menu-container">
<el-input type='mini' v-model="filterText" placeholder="查找节点" />
<div class="custom-tree-container" v-loading="loading">
<el-tree
:data="dataSource"
node-key="id"
ref="treedom"
:filter-node-method="filterNode"
default-expand-all
:expand-on-click-node="false"
:highlight-current="true"
>
<template #default="{ node, data }" >
<span
class="custom-tree-node"
@mouseenter="enters(data.id)"
@click="chooseData(data)"
>
<div style="display: flex; align-items: center">
<div style="width: 16px; height: 16px; margin-right: 10px">
<Menu v-if="data.menuType === '1'" />
<Link v-else />
</div>
<span class="title" :title="data.title">{{ data.title }}</span>
<span
class="icons"
:style="{
display: data.id === hoverid ? 'block' : 'none',
}"
>
<Plus @click="(e) => addNode(e, node, data)" v-if="data.menuType === '1'"/>
<Minus @click="(e) => deleteNode(e, node, data)" />
</span>
</div>
</span>
</template>
</el-tree>
</div>
</el-col>
<el-col :span="18" class="basic-menu-container" :offset="1">
<el-form
:model="form"
label-width="120px"
ref="ruleFormRef"
:rules="rules"
>
<el-form-item label="父亲节点">
<el-input v-model="form.parentTitle" readonly />
</el-form-item>
<el-form-item label="节点名称" prop="title">
<el-input v-model="form.title" />
</el-form-item>
<el-form-item label="路径">
<el-input v-model="form.url" />
</el-form-item>
<el-form-item label="图标">
<el-input v-model="form.icon" />
</el-form-item>
<el-form-item label="排序" prop="orders">
<el-input v-model="form.orders" type="number" min="0" step="1" />
</el-form-item>
<el-form-item label="菜单类型" prop="menuType">
<el-select v-model="form.menuType">
<el-option
v-for="item in [
{ label: '菜单', value: '1' },
{ label: '按钮', value: '2' },
]"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="按钮权限" prop="buttonRole">
<el-input v-model="form.buttonRole" />
</el-form-item>
<el-form-item label="是否启用">
<el-switch
v-model="form.status"
:active-value="1"
:inactive-value="0"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">提交</el-button>
<el-button @click="onClear">清空</el-button>
</el-form-item>
</el-form>
</el-col>
</el-row>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted , watch } from "vue";
import { getMenu, saveMenu, deleteMenu } from "@/api/menu";
import { responseNotify, transData } from "@/utils/utils";
import { ElMessage, ElMessageBox } from "element-plus";
import { Menu } from "@element-plus/icons-vue";
import { validateWholeNumber } from "@/utils/pattern";
const dataSource = ref();
const hoverid = ref(0);
const treedom = ref(null);
const filterText = ref('')
const form = ref({
title: "",
status: 1,
});
const loading = ref(false);
const ruleFormRef = ref();
let menuData = [];
// 获取数据
onMounted(() => {
getMenuData();
});
// 过滤
watch(filterText, (val) => {
treedom.value = treedom.value.filter(val)
})
const filterNode = (value, data) => {
if (!value) return true
return data.title.includes(value)
}
function getMenuData() {
loading.value = true;
getMenu({limit:9999}).then(({ data }) => {
loading.value = false;
dataSource.value = transData(data.list, "id", "pid", "children");
});
}
const rules = reactive({
title: [{ required: true, message: "请填写节点名称", trigger: "blur" }],
menuType: [{ required: true, message: "请选择菜单类型", trigger: "blur" }],
orders: [
{
validator: validateWholeNumber,
trigger: "blur",
},
],
});
function enters(index) {
hoverid.value = index;
}
function addNode(e, node, data) {
e.stopPropagation();
treedom.value.setCurrentKey(data.id,false)
const newMenu = {
title: "新建菜单",
pid: data.id,
parentTitle: data.title,
id: new Date().getTime(),
handletype: "add",
status: 1,
};
form.value = newMenu;
// treedom.value.append(newMenu,node);
}
function chooseData(data) {
const newMenu = {
title: data.title,
pid: data.parentsNode ? data.parentsNode.id : null,
parentTitle: data.parentsNode ? data.parentsNode.title : null,
id: data.id,
handletype: "edit",
icon: data.icon,
url: data.url,
status: data.status,
buttonRole: data.buttonRole,
orders: data.orders,
menuType: data.menuType,
};
form.value = newMenu;
}
function onSubmit() {
ruleFormRef.value.validate((valid, fields) => {
if (valid) {
let message = form.value.handletype === "add" ? "新增" : "修改";
if (form.value.handletype === "add" || !form.value.id) {
delete form.value.id;
}
saveMenu(form.value).then((res) => {
if (responseNotify(res)) {
ElMessage.success(message + "成功");
getMenuData();
onClear();
}
});
}
});
}
// 获取该节点下所有子节点 id
function getAllDeleteNodeId(arr, data) {
arr.push(data.id);
if (data.children) {
for (let i of data.children) {
getAllDeleteNodeId(arr, i);
}
}
return arr;
}
function deleteNode(e, node, data) {
e.stopPropagation();
treedom.value.setCurrentKey(data.id,false)
ElMessageBox.confirm("确定要该节点吗,其下面的子节点也会随之删除?", "提示", {
type: "warning",
})
.then(() => {
let arr = getAllDeleteNodeId([], data);
deleteMenu({ id: arr }).then((res) => {
if (res.success) {
ElMessage.success("删除成功");
getMenuData();
onClear();
}
});
})
.catch(() => {});
}
function onClear() {
form.value.title = "";
form.value.parentTitle = "";
form.value.pid = 0;
form.value.status = 1;
form.value.url = "";
form.value.icon = "";
form.value.handletype = "add";
form.value.orders = "";
form.value.menuType = "";
form.value.buttonRole = "";
}
</script>
<style scoped>
.handle-box {
margin-bottom: 20px;
}
.handle-select {
width: 120px;
}
.handle-input {
width: 300px;
display: inline-block;
}
.table {
width: 100%;
font-size: 14px;
}
.red {
color: #ff0000;
}
.mr10 {
margin-right: 10px;
}
.table-td-thumb {
display: block;
margin: auto;
width: 40px;
height: 40px;
}
.menu-container {
width: 100%;
}
.basic-menu-container {
padding: 30px 16px;
background: #fff;
border: 1px solid #ddd;
border-radius: 5px;
height: calc(100vh - 200px);
}
.basic-menu-container .icons {
align-items: center;
margin-left: 8px;
height: 16px;
width: 16px;
display: none;
}
.basic-menu-container .title {
overflow: hidden;
display: block;
max-width: 160px;
text-overflow: ellipsis;
}
.custom-tree-container{
height: 70vh;
margin-top:10px;
overflow-y:auto
}
</style>
效果展示

出现的问题
菜单 的 树转化是在 前端 完成的,所以这里 排序 也是在前端完成的,而且在 删除 会统一收集菜单下的 子菜单 然后逐一删除,本来想在后端处理,但是觉得这样子后端代码会特别混乱菜单类型 可以 从 菜单 改到 按钮,会导致和 (1)冲突,这里还没改,暂时没想到好的方法,有建议可以直接评论git
前端: https://github.com/zhengyongkai/vue3-backend
后端:https://github.com/zhengyongkai/backend-eggjs
两个项目都是没事写写的项目,代码可能会不规范,但是欢迎大家 评论 和 fork
更改记录
暂无