Nuxt3 全栈作品【通用信息管理系统】菜单管理
最终效果
前端代码
pages/system/menu.vue
<script lang="ts" setup>
import type Node from "element-plus/es/components/tree/src/model/node";
import { initMenu } from "@/data/menu";
import { SForm } from "#components";// 使用索引签名定义对象类型
type GenericObject = {[key: string]: any;
};
interface Tree {label: string;path: string;icon?: string;noChild: boolean;children?: Tree[];
}const editMenuFormRef = ref<InstanceType<typeof SForm> | null>(null);
const callbackMessage = useCallbackMessage();
const callbackMessage_dialog = useCallbackMessage();
let old_unique = "";
const PageConfig = {//被操作实体的名称entity: "system",api: {//获取列表数据list: "/list",},
};
const action = ref("list");
const id = ref("");
const addType = ref("");
const Model: {[key: string]: any;
} = {label: {label: "菜单名称",require: true,},path: {label: "菜单路径",require: true,},icon: {label: "菜单图标",},noChild: {label: "页面菜单",type: "switch",},
};
const dataSource = ref<GenericObject[]>([]);
let handle_data: GenericObject = {};
const dialogVisible = ref(false);onMounted(() => {getList();
});const getList = async () => {action.value = "list";const res: {[key: string]: any;}[] = await $fetch(`/api/${PageConfig.entity}${PageConfig.api.list}`, {query: {name: "menu",},});if (Array.isArray(res) && res.length > 0) {dataSource.value = JSON.parse(res[0].content);id.value = res[0]._id;} else {try {await $fetch(`/api/system/add`, {body: {name: "menu",content: JSON.stringify(initMenu),},method: "POST",});saveOK();} catch (e: any) {callbackMessage.value = {show: true,valid: false,content: "菜单初始化失败",};}}
};// 按钮--添加(子菜单)
const handle_add = async (data: Tree, node: Tree) => {action.value = "add";addType.value = "addChild";dialogVisible.value = true;old_unique = "";handle_data = data;await nextTick();if (editMenuFormRef.value) {editMenuFormRef.value.localFormData = {};}
};// 按钮-编辑
const handle_edit = async (data: Tree) => {action.value = "edit";addType.value = "";dialogVisible.value = true;old_unique = data.path;handle_data = data;await nextTick();if (editMenuFormRef.value) {editMenuFormRef.value.localFormData = JSON.parse(JSON.stringify(data));}
};// 按钮--删除
const handle_del = (node: Node, data: Tree) => {const parent = node.parent;const children: Tree[] = parent.data.children || parent.data;const index = children.findIndex((d) => d.path === data.path);children.splice(index, 1);dataSource.value = [...dataSource.value];saveToDB();
};// 按钮--添加顶级菜单
const addTop = () => {action.value = "add";addType.value = "addTop";if (editMenuFormRef.value) {editMenuFormRef.value!.localFormData = {};}dialogVisible.value = true;
};// 弹窗按钮--保存
const save = async () => {if (editMenuFormRef.value) {let new_unique = editMenuFormRef.value.localFormData.path;if (old_unique !== new_unique &&hasTargetValueInTree(dataSource.value, "path", new_unique)) {callbackMessage_dialog.value = {show: true,valid: false,content: "菜单路径已存在,请修改后重试",};return;}editMenuFormRef.value.submitForm(editMenuFormRef.value.formRef);}
};// 保存到数据库
const saveToDB = async (formData?: GenericObject) => {let old_dataSource = JSON.parse(JSON.stringify(dataSource.value));if (formData) {if (addType.value === "addTop") {dataSource.value.unshift(formData);}if (addType.value === "addChild") {const newChild = formData;if (!handle_data.children) {handle_data.children = [];}handle_data.children.push(newChild);}if (action.value === "edit") {handle_data.label = formData.label;handle_data.path = formData.path;handle_data.icon = formData.icon;handle_data.noChild = formData.noChild;}}try {await $fetch(`/api/system/edit`, {body: {_id: id.value,name: "menu",content: JSON.stringify(dataSource.value),},method: "POST",});callbackMessage.value = {show: true,valid: true,content: "操作成功",};saveOK();} catch (e: any) {callbackMessage.value = {show: true,valid: false,content: e.data.message,};dataSource.value = JSON.parse(JSON.stringify(old_dataSource));}
};// 保存成功后的回调 -- 刷新菜单
const saveOK = () => {addType.value === "";getList();setTimeout(() => {dialogVisible.value = false;}, 500);
};// 拖拽菜单后,将新菜单保存到数据库
const handle_drop = async () => {saveToDB();
};
</script>
<template><div class="flex flex-col items-center fullParent overflow-auto pt-10"><el-dialog v-model="dialogVisible" width="600" title="编辑菜单"><S-formref="editMenuFormRef"hideHandle:Model="Model":colNum="1":local_save="saveToDB"/><template #footer><div class="dialog-footer"><el-button @click="dialogVisible = false">取消</el-button><el-button type="primary" @click="save"> 保存 </el-button></div></template><S-msgWin top="20%" :msg="callbackMessage_dialog" /></el-dialog><el-button type="primary" @click="addTop"> 添加顶级菜单 </el-button><el-treeclass="mt4"style="min-width: 600px":data="dataSource"node-key="path"accordiondraggabledefault-expand-all:expand-on-click-node="false":indent="40"@node-drop="handle_drop"><template #default="{ node, data }"><div class="custom-tree-node"><divclass="flex items-center gap-2":class="{'font-bold': node.level === 1,'text-4': node.level === 1,}"><S-icon v-if="data.icon" :icon="data.icon" /><div>{{ node.label }}</div></div><div><el-buttontype="primary"link@click="handle_add(data, node)":disabled="data.noChild"><S-icon icon="material-symbols-light:add-rounded" /><span class="ml2">添加</span></el-button><el-buttonclass="ml4"type="primary"link@click="handle_edit(data)"><S-icon icon="line-md:edit-twotone" /><span class="ml2">修改</span></el-button><el-popconfirmtitle="确定删除吗?"@confirm="handle_del(node, data)"><template #reference><el-button class="ml4" type="danger" link><S-icon icon="wpf:delete" /><span class="ml2">删除</span></el-button></template></el-popconfirm></div></div></template></el-tree><S-msgWin top="20%" :msg="callbackMessage" /></div>
</template>
<style scoped>
.custom-tree-node {flex: 1;display: flex;align-items: center;justify-content: space-between;font-size: 14px;padding-right: 8px;
}
.fullParent {position: absolute;top: 0;right: 0;bottom: 0;left: 0;
}
:deep(.el-tree-node) {margin-top: 12px;
}
</style>
初始菜单
data/menu.ts
// 无菜单数据时,初始化菜单
export const initMenu = [{id: "1",label: "用户管理",path: "/user/list",icon: "mdi:user",},{id: "2",label: "个人中心",path: "/me",icon: "tdesign:personal-information-filled",children: [{id: "2-1",label: "修改资料",path: "/me/editInfo",},],},{id: "3",label: "系统管理",path: "/system",icon: "mdi:settings",children: [{id: "3-1",label: "菜单管理",path: "/system/menu",},{id: "3-5",label: "角色管理",path: "/system/role/list",},{id: "3-4",label: "权限管理",path: "/system/permission/list",},{id: "3-2",label: "字典管理",path: "/system/dic/list",},{id: "3-6",label: "部门管理",path: "/system/department",},{id: "3-7",label: "岗位管理",path: "/system/position/list",},{id: "3-3",label: "操作日志",path: "/system/log/list",},],},
];
依赖组件 S-form
https://blog.csdn.net/weixin_41192489/article/details/149717692
依赖组件 S-msgWin
https://blog.csdn.net/weixin_41192489/article/details/149717948
注意事项
- 删除并没有专门的接口,直接更新菜单的json数据。
接口开发
查询 /api/system/list
见 https://blog.csdn.net/weixin_41192489/article/details/149712067
新增 /api/system/add
server/api/system/add.post.ts
import { systemCreateSchema } from "~/validators/system";
export default defineEventHandler(async (event) => {const result = await runValidate(systemCreateSchema, event);const { name } = result.data;const system = await SystemSchema.findOne({ name: name }).lean();if (system) {throw createError({statusCode: 409,statusMessage: "系统配置已存在,请修改后重试",});}const newData = await SystemSchema.create(result.data);return newData;
});
修改 /api/system/edit
server/api/system/edit.post.ts
import { systemUpdateSchema } from "~/validators/system";
export default defineEventHandler(async (event) => {const result = await runValidate(systemUpdateSchema, event);const newData = SystemSchema.findByIdAndUpdate(result.data._id, result.data, {new: true,});return newData;
});