明树Git Lab

Commit 6aa0ed17 authored by yangyajing's avatar yangyajing

菜单管理

parent b6be8853
Pipeline #104447 passed with stage
in 13 seconds
......@@ -13,6 +13,7 @@
"@element-plus/icons-vue": "^2.3.2",
"@kjgl77/datav-vue3": "^1.7.4",
"axios": "^1.13.2",
"crypto-js": "^4.2.0",
"decimal.js": "^10.6.0",
"echarts": "^6.0.0",
"echarts-map": "^3.0.1",
......@@ -531,6 +532,12 @@
"node": ">=0.8"
}
},
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmmirror.com/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"license": "MIT"
},
"node_modules/csstype": {
"version": "3.2.3",
"license": "MIT"
......
......@@ -392,6 +392,7 @@
</el-form-item>
</el-col>
<!-- 表单操作按钮 -->
<el-col :span="24">
<el-form-item v-if="config.showButtons !== false" class="form-buttons">
<el-button
v-if="config.showSubmit !== false"
......@@ -411,6 +412,7 @@
{{ btn.text }}
</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
......
......@@ -2,6 +2,7 @@ import axios from "axios";
import { ElMessage } from "element-plus";
import windowConfig from "@/window";
import { useUserStore } from "@/stores/user.js";
import { useRouter } from "vue-router";
axios.interceptors.request.use(function (config) {
const userStore = useUserStore();
let token = userStore.authToken || sessionStorage.getItem("authToken") || "";
......@@ -27,7 +28,7 @@ axios.interceptors.response.use(response => {
return Promise.reject(err);
});
export function $get ({ url, params = {}, callback }) {
export function $get ({ url, params = {}, callback, error }) {
return axios({
method: "get",
url: windowConfig.baseUrl + url,
......@@ -36,11 +37,19 @@ export function $get ({ url, params = {}, callback }) {
callback && callback(response.data);
return response;
}).catch((err) => {
ElMessage.error(err);
console.log(err);
error && error(err);
ElMessage.error(err.msg || "操作失败");
if (err && err.response && err.response.status === 401) {
userStore.clearUserInfo();
router.replace("/login");
}
});
};
export function $post ({ url, data = {}, callback }) {
export function $post ({ url, data = {}, callback, error }) {
const router = useRouter();
const userStore = useUserStore();
return axios.post(windowConfig.baseUrl + url, data).then((response) => {
if (response.code === 0) {
callback && callback(response.data);
......@@ -50,8 +59,10 @@ export function $post ({ url, data = {}, callback }) {
return response;
}).catch((err) => {
console.log(err);
error && error(err);
ElMessage.error(err.msg || "操作失败");
if (err && err.response && err.response.status === 401) {
userStore.clearUserInfo();
router.replace("/login");
}
});
......
......@@ -25,43 +25,7 @@
<el-container>
<!-- 侧边Aside -->
<el-aside width="220px" class="city-aside">
<el-menu
:default-active="$route.path"
active-text-color="#409EFF"
background-color="#1f313b"
text-color="#fff"
router
>
<template v-for="route in menuRoutes" :key="route.path">
<!-- 无子菜单的项目 -->
<el-menu-item
v-if="!route.children || route.children.length === 0"
:index="route.path"
>
<el-icon><component :is="route.meta?.icon || 'menu'" /></el-icon>
<span>{{ route.meta?.menuName || route.name }}</span>
</el-menu-item>
<!-- 有子菜单的项目 -->
<el-sub-menu v-else :index="route.path">
<template #title>
<el-icon
><component :is="route.meta?.icon || 'menu'"
/></el-icon>
<span>{{ route.meta?.menuName || route.name }}</span>
</template>
<template v-for="child in route.children" :key="child.path">
<el-menu-item v-if="child.meta?.showInMenu !== false"
:index="child.path"
style="padding-left: 20px !important"
>
<el-icon><component v-if="child.meta?.icon" :is="child.meta?.icon" /></el-icon>
<span>{{ child.meta?.menuName || child.name }}</span>
</el-menu-item>
</template>
</el-sub-menu>
</template>
</el-menu>
<left-menu></left-menu>
</el-aside>
<!-- 主内容Main -->
......@@ -76,6 +40,7 @@
import { computed, onMounted, getCurrentInstance } from "vue";
import { useRouter, useRoute } from "vue-router";
import { useUserStore } from "@/stores/user.js";
import LeftMenu from "./leftMenu.vue";
const userStore = useUserStore();
const { proxy } = getCurrentInstance();
......@@ -110,7 +75,7 @@ const handleLogout = () => {
url: "/api/user/logout",
data: {},
callback: (data) => {
userStore.clearUseInfo();
userStore.clearUserInfo();
router.replace("/login");
}
})
......
<template>
<el-menu
:default-active="$route.path"
router
>
<template v-for="item in menuList">
<template v-if="item.children && item.children.length">
<left-menu-item :key="item.id" :menuItem="item"></left-menu-item>
</template>
<template v-else>
<el-menu-item :key="item.id" :index="item.url">
<span>{{ item.name }}</span>
</el-menu-item>
</template>
</template>
</el-menu>
</template>
<script setup>
import LeftMenuItem from "./leftMenuItem.vue";
const menuList = sessionStorage.getItem("userInfo") ? JSON.parse(sessionStorage.getItem("userInfo")).menus : [];
</script>
<style lang="less"></style>
\ No newline at end of file
<template>
<el-sub-menu :index="menuItem.id">
<template #title>
<span>{{ menuItem.name }}</span>
</template>
<template v-for="(child, index) in menuItem.children" :key="index">
<left-menu-item v-if="child.children && child.children.length"></left-menu-item>
<el-menu-item v-else
:index="child.url"
>
<span>{{ child.name }}</span>
</el-menu-item>
</template>
</el-sub-menu>
</template>
<script setup>
import { defineProps } from 'vue';
const props = defineProps({
menuItem: {
type: Object,
default: {}
}
})
</script>
<style lang="less" scoped>
</style>
\ No newline at end of file
......@@ -9,11 +9,13 @@ export const useUserStore = defineStore('user', {
this.userInfo = data;
this.authToken = data.token;
sessionStorage.setItem("authToken", data.token);
sessionStorage.setItem("userInfo", JSON.stringify(data));
},
clearUserInfo () {
this.userInfo = null;
this.authToken = "";
sessionStorage.removeItem("authToken");
sessionStorage.removeItem("userInfo");
}
}
})
\ No newline at end of file
......@@ -49,6 +49,7 @@ import { ref, getCurrentInstance } from "vue";
import { useRouter } from "vue-router";
import { User, Lock } from "@element-plus/icons-vue";
import { useUserStore } from "@/stores/user.js";
import CryptoJS from "crypto-js";
const userStore = useUserStore();
const { proxy } = getCurrentInstance();
......@@ -78,7 +79,9 @@ const handleLogin = async () => {
if (valid) {
proxy.$post({
url: "/api/user/login",
data: loginForm.value,
data: {
encryptLogStr: CryptoJS.AES.encrypt(`${loginForm.value.mobile},${loginForm.value.password}`, "GFG5w5AP0Ja2rNaa").toString()
},
callback: (data) => {
userStore.setUseInfo(data);
router.replace("/");
......
<template>
<div class="user-manage" v-loading="loading">
<div class="search-form">
<commonForm
v-model="searchForm"
:config="searchConfig"
:items="searchItems"
@submit="handleSearch"
@reset="handleReset"
/>
</div>
<div class="table-container">
<common-table
:maxRows="10"
:rowHeight="40"
:data="tableData"
:columns="tableColumns"
:total="total"
:current-page="currentPage"
:page-size="pageSize"
:indent="10"
title="菜单管理"
:pagination="false"
:indent="30"
:border="true"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
row-key="id"
default-expand-all
@size-change="handleSizeChange"
@current-page-change="handleCurrentPageChange"
>
<template #header-actions>
<el-button type="primary" @click="handleAdd">新增 </el-button>
</template>
<template #operations="{ row, index }">
<el-button type="text" size="small"> 新增 </el-button>
<el-button type="text" size="small" @click="handleEdit(row, index)">
<el-button v-if="!row.url" link type="primary" size="small" @click="addChildMenu(row, index)"> 新增子菜单 </el-button>
<el-button link type="primary" size="small" @click="handleEdit(row, index)">
编辑
</el-button>
<el-button type="text" size="small" @click="handleDelete(row, index)">
<el-button link type="primary" size="small" @click="handleDelete(row, index)">
删除
</el-button>
</template>
......@@ -46,17 +30,28 @@
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="800px"
@close="handleDialogClose"
width="500px"
@close="cancelMenuForm"
>
<commonForm
v-model="userForm"
:config="formConfig"
:items="formItems"
:rules="formRules"
@submit="handleFormSubmit"
@reset="handleFormReset"
<el-form :model="menuFormData" ref="menuForm" :rules="menuRules" label-width="100">
<el-form-item label="项目名称" prop="name">
<el-input v-model="menuFormData.name" />
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="menuFormData.order" :min="0" :precision="0" :max="99999999999"
controls-position="right"
/>
</el-form-item>
<el-form-item label="路由地址">
<el-input v-model="menuFormData.url" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancelMenuForm">取消</el-button>
<el-button type="primary" @click="saveMenuForm">保存</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
......@@ -69,502 +64,122 @@ import commonForm from "@/components/common/commonForm.vue";
import CommonTable from "@/components/common/commonTable.vue";
import { da } from "element-plus/es/locales.mjs";
// 模拟树形菜单数据
const mockMenuData = [
{
id: 1,
name: "系统管理",
code: "system",
app: "管理平台",
type: "目录",
icon: "Setting",
order: 1,
createdAt: "2024-01-01",
children: [
{
id: 11,
name: "用户管理",
code: "user",
app: "管理平台",
type: "菜单",
icon: "User",
order: 1,
createdAt: "2024-01-01",
children: [
{
id: 111,
name: "用户列表",
code: "user-list",
app: "管理平台",
type: "菜单",
icon: "List",
order: 1,
createdAt: "2024-01-01",
},
{
id: 112,
name: "用户详情",
code: "user-detail",
app: "管理平台",
type: "菜单",
icon: "Document",
order: 2,
createdAt: "2024-01-01",
},
],
},
{
id: 12,
name: "角色管理",
code: "role",
app: "管理平台",
type: "菜单",
icon: "UserFilled",
order: 2,
createdAt: "2024-01-01",
},
{
id: 13,
name: "菜单管理",
code: "menu",
app: "管理平台",
type: "菜单",
icon: "Menu",
order: 3,
createdAt: "2024-01-01",
},
],
},
{
id: 2,
name: "业务管理",
code: "business",
app: "业务平台",
type: "目录",
icon: "Briefcase",
order: 2,
createdAt: "2024-01-01",
children: [
{
id: 21,
name: "项目管理",
code: "project",
app: "业务平台",
type: "菜单",
icon: "FolderOpened",
order: 1,
createdAt: "2024-01-01",
},
{
id: 22,
name: "审批管理",
code: "approval",
app: "业务平台",
type: "菜单",
icon: "Select",
order: 2,
createdAt: "2024-01-01",
},
],
},
{
id: 3,
name: "数据统计",
code: "statistics",
app: "分析平台",
type: "目录",
icon: "DataAnalysis",
order: 3,
createdAt: "2024-01-01",
children: [
{
id: 31,
name: "报表中心",
code: "report",
app: "分析平台",
type: "菜单",
icon: "PieChart",
order: 1,
createdAt: "2024-01-01",
},
],
},
];
const { proxy } = getCurrentInstance();
const loading = ref(false);
// 数据转换函数
const convertToTreeData = (apiData) => {
return apiData.map((item) => ({
value: item.id.toString(),
label: item.name,
children: item.children ? convertToTreeData(item.children) : [],
}));
};
// 查询表单数据
const searchForm = ref({
name: "",
mobile: "",
const menuForm = ref();
const menuRules = ref({
name: [
{ required: true, message: "请输入菜单名称", trigger: "blur" }
]
});
// 查询表单配置
const searchConfig = {
inline: true,
labelWidth: "80px",
showButtons: true,
submitText: "查询",
resetText: "重置",
};
// 查询表单项配置
const searchItems = [
{
type: "input",
prop: "name",
label: "菜单名称:",
placeholder: "请输入菜单名称",
clearable: true,
span: 8,
},
{
type: "input",
prop: "mobile",
label: "菜单编码:",
placeholder: "请输入菜单编码",
clearable: true,
span: 8,
},
];
// 表格数据
const tableData = ref([]);
const total = ref(0);
const currentPage = ref(1);
const pageSize = ref(10);
// 表格列配置
const tableColumns = [
{
prop: "name",
label: "菜单名称",
width: 180,
tree: true,
},
{
prop: "code",
label: "菜单编号",
minWidth: 120,
},
{
prop: "type",
label: "菜单类型",
minWidth: 100,
},
{
prop: "icon",
label: "菜单图标",
minWidth: 100,
},
{
prop: "order",
label: "排序",
minWidth: 80,
align: "center",
},
{
prop: "createdAt",
label: "创建时间",
width: 250,
align: "center",
},
// {
// prop: "order",
// label: "排序",
// align: "center",
// },
{
prop: "operations",
label: "操作",
width: 200,
width: 180,
slot: "operations",
fixed: "right",
align: "center",
align: "right"
},
];
// 对话框相关
const dialogVisible = ref(false);
const dialogTitle = ref("新增菜单");
const isEdit = ref(false);
const editIndex = ref(-1);
// 用户表单数据
const userForm = ref({
name: "",
departs: [],
positions: [],
roles: [],
enable: "0",
});
// 用户表单配置
const formConfig = {
labelWidth: "100px",
showButtons: true,
submitText: "保存",
resetText: "取消",
};
// 用户表单项配置
const formItems = computed(() => [
{
type: "select",
prop: "name",
label: "父级菜单:",
placeholder: "请选择顶级菜单",
span: 12,
required: true,
rules: [{ required: true, message: "请选择顶级菜单", trigger: "blur" }],
},
{
type: "radio",
prop: "",
label: "菜单类型:",
span: 12,
options: [
{
label: "目录",
value: "1",
},
{
label: "菜单",
value: "0",
},
],
},
{
type: "input",
prop: "mobile",
label: "菜单名称:",
placeholder: "请输入菜单名称",
span: 12,
required: true,
rules: [{ required: true, message: "请输入菜单名称", trigger: "blur" }],
},
{
type: "input",
prop: "",
label: "菜单编号:",
placeholder: "请输入菜单编号",
span: 12,
required: true,
rules: [{ required: true, message: "请输入菜单编号", trigger: "blur" }],
},
{
type: "input",
prop: "",
label: "Vue组件:",
placeholder: "请输入Vue组件",
span: 12,
},
{
type: "input",
prop: "",
label: "路由名称:",
placeholder: "请输入路由名称",
span: 12,
},
{
type: "input",
prop: "",
label: "路由地址:",
placeholder: "请输入路由地址",
span: 12,
},
{
type: "input",
prop: "",
label: "排序:",
placeholder: "请输入排序",
span: 12,
},
{
type: "radio",
prop: "",
label: "显示状态:",
span: 12,
options: [
{ label: "显示", value: "0" },
{ label: "隐藏", value: "1" },
],
},
{
type: "radio",
prop: "",
label: "嵌套模式:",
span: 12,
options: [
{ label: "是", value: "0" },
{ label: "否", value: "1" },
],
// 表格数据
const loadTableData = () => {
loading.value = true;
proxy.$post({
url: "/api/user/menu/treeMenu",
data: {},
callback: (data) => {
tableData.value = data;
loading.value = false;
},
{
type: "upload",
prop: "",
label: "菜单图标:",
span: 12,
error: (err) => {
loading.value = false;
ElMessage.error("加载数据失败");
},
]);
// 表单验证规则
const formRules = {};
// 事件处理函数
const handleSearch = (formData) => {
currentPage.value = 1;
loadTableData();
};
const handleReset = () => {
searchForm.value = {
name: "",
mobile: "",
};
currentPage.value = 1;
loadTableData();
};
const handleSizeChange = (size) => {
pageSize.value = size;
currentPage.value = 1;
loadTableData();
});
};
const handleCurrentPageChange = (page) => {
currentPage.value = page;
loadTableData();
};
// 对话框相关
const dialogVisible = ref(false);
const dialogTitle = ref("");
// 新增用户
// 用户表单数据
const menuFormData = ref({});
// 新增菜单
const handleAdd = () => {
isEdit.value = false;
dialogTitle.value = "新增菜单";
userForm.value = {
name: "",
departs: [],
positions: [],
roles: [],
enable: "0",
};
dialogVisible.value = true;
};
let currentID = ref();
const addChildMenu = (row, index) => {
dialogTitle.value = "新增菜单";
menuFormData.value.parentId = row.id;
dialogVisible.value = true;
}
// 编辑
const handleEdit = (row, index) => {
isEdit.value = true;
dialogTitle.value = "编辑用户";
editIndex.value = index;
// proxy.$post({
// url: "/api/user/manage/getUserInfo",
// data: { id: row.id },
// callback: (data) => {
// userForm.value = { ...data };
// currentID.value = data.id;
// },
// error: (err) => {
// ElMessage.error("编辑失败:", err);
// },
// });
menuFormData.value = { ...row };
dialogTitle.value = "编辑菜单";
dialogVisible.value = true;
};
// 删除
const handleDelete = async (row, index) => {
try {
// await ElMessageBox.confirm(`确定要删除用户"${row.name}"吗?`, "提示", {
// confirmButtonText: "确定",
// cancelButtonText: "取消",
// type: "warning",
// });
// proxy.$post({
// url: "/api/user/manage/deleteUser",
// data: { id: row.id },
// callback: (data) => {
// dialogVisible.value = false;
// loadTableData();
// ElMessage.success("删除成功");
// },
// error: (err) => {
// ElMessage.error("删除失败:", err);
// },
// });
// loadTableData();
} catch {}
};
const handleFormSubmit = (formData) => {
if (isEdit.value) {
// 编辑用户
const updateUser = {
...formData,
};
// proxy.$post({
// url: "/api/user/manage/updateUser",
// data: updateUser,
// callback: (data) => {
// dialogVisible.value = false;
// loadTableData();
// ElMessage.success("用户信息更新成功");
// },
// error: (err) => {
// ElMessage.error("用户信息更新失败:", err);
// },
// });
} else {
// 新增用户
const newUser = {
...formData,
};
// proxy.$post({
// url: "/api/user/manage/createUser",
// data: newUser,
// callback: (data) => {
// dialogVisible.value = false;
// loadTableData();
// ElMessage.success("用户添加成功");
// },
// error: (err) => {
// ElMessage.error("用户添加失败:", err);
// },
// });
}
};
const handleFormReset = () => {
dialogVisible.value = false;
};
const handleDialogClose = () => {
dialogVisible.value = false;
};
// 表格数据
const loadTableData = () => {
loading.value = true;
ElMessageBox.confirm(`确定删除${row.name}?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(() => {
proxy.$post({
url: "/api/user/menu/treeMenu",
data: {
...searchForm.value,
page: currentPage.value,
pageSize: pageSize.value,
},
url: "/api/user/menu/deleteMenu",
data: { id: row.id },
callback: (data) => {
tableData.value = data;
total.value = data.count;
loading.value = false;
ElMessage.success("删除成功");
loadTableData();
},
error: (err) => {
loading.value = false;
ElMessage.error("加载数据失败");
ElMessage.error("删除失败:", err);
},
});
});
};
const saveMenuForm = () => {
menuForm.value.validate(valid => {
if (valid) {
let url = menuFormData.value.id ? "updateMenu" : "createMenu"
proxy.$post({
url: "/api/user/menu/" + url,
data: menuFormData.value,
callback: (data) => {
ElMessage.success(dialogTitle.value + "成功");
loadTableData();
cancelMenuForm();
}
})
}
});
};
const cancelMenuForm = () => {
menuForm.value.resetFields();
menuFormData.value = {};
dialogVisible.value = false;
};
onMounted(() => {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment