Browse Source

权限菜单调整

zhangchong 2 years ago
parent
commit
eaf541ab22

+ 1 - 1
package.json

@@ -1,7 +1,7 @@
 {
 	"name": "hoteline",
 	"version": "1.0.0",
-	"description": "12345便民服务热线系统",
+	"description": "12345政务服务便民系统",
 	"author": "zc",
 	"license": "MIT",
 	"scripts": {

+ 12 - 10
src/api/login/user.ts

@@ -7,6 +7,18 @@
  * @LastEditTime: 2022-11-16 14:49:01
  */
 import request from '/@/utils/request';
+/**
+ * @description: 获取当前用户下的菜单
+ * @param {object} params
+ * @return {*}
+ */
+export const getMenu = (params?: object) => {
+	return request({
+		url: '/api/v1/Home/get-my-auth-menu',
+		method: 'get',
+		params,
+	});
+};
 /**
  * 修改密码
  * @param data
@@ -39,16 +51,6 @@ export function getUserInfo() {
         method: 'get'
     });
 }
-/**
- * 获取当前用户所有按钮
- * @param 
- */
-export function getBtns() {
-    return request({
-        url: `/api/v1/Home/get-my-auth-button`,
-        method: 'get'
-    });
-}
 /**
  * @description: 根据姓名模糊查询用户
  * @param {object} params

+ 0 - 17
src/api/system/menu.ts

@@ -7,23 +7,6 @@
  * @LastEditTime: 2022-11-09 17:03:01
  */
 import request from '/@/utils/request';
-/**
- * 后端控制菜单模拟json,路径在 https://gitee.com/lyt-top/vue-next-admin-images/tree/master/menu
- * 后端控制路由,isRequestRoutes 为 true,则开启后端控制路由
- * @method getMenu 获取后端动态路由菜单(admin)
- */
-/**
- * @description: 获取当前用户下的菜单
- * @param {object} params
- * @return {*}
- */
-export const getMenu = (params?: object) => {
-	return request({
-		url: '/api/v1/Home/get-my-auth-menu',
-		method: 'get',
-		params,
-	});
-};
 /**
  * @description: 获取所有菜单列表
  * @param {object} params

+ 1 - 6
src/layout/logo/index.vue

@@ -69,17 +69,12 @@ const onThemeConfigChange = () => {
 	height: 90px;
 	display: flex;
 	cursor: pointer;
-	animation: logoAnimation 0.3s ease-in-out;
 
 	&-img {
 		width: 50px;
 		margin: auto;
 	}
 
-	&:hover {
-		img {
-			animation: logoAnimation 0.3s ease-in-out;
-		}
-	}
+
 }
 </style>

+ 15 - 7
src/router/backEnd.ts

@@ -10,7 +10,7 @@ import { dynamicRoutes, notFoundAndNoPower } from '/@/router/route';
 import { formatFlatteningRoutes, formatTwoStageRoutes, router } from '/@/router/index';
 import { useRoutesList } from '/@/stores/routesList';
 import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
-import { getMenu } from '/@/api/system/menu';
+import { getMenu } from '/@/api/login/user';
 import { pwdCheck, getAppConfig } from '/@/api/login';
 const layouModules: any = import.meta.glob('../layout/routerView/*.{vue,tsx}');
 const viewsModules: any = import.meta.glob('../views/**/*.{vue,tsx}');
@@ -26,6 +26,7 @@ const dynamicViewsModules: Record<string, Function> = Object.assign({}, { ...lay
 // 菜单格式化
 function formatRouter(arr: any) {
 	if (!arr) return [];
+	const newArr: any = [];
 	arr.forEach((v: any) => {
 		v.name = v.ruleName;
 		v.meta = {};
@@ -41,11 +42,12 @@ function formatRouter(arr: any) {
 			v.meta['isDynamic'] = true;
 			v.meta['isDynamicPath'] = v.path;
 		}
+		newArr.push({ ...v });
 		if (v.children?.length) {
 			formatRouter(v.children)
 		}
 	});
-	return arr;
+	return newArr;
 }
 // 获取系统配置
 const getAppConfigFn = async () => {
@@ -79,29 +81,35 @@ export async function initBackEndControlRoutes() {
 	let resRouter = null;
 	if (Local.get('requestOldRoutes')) { //获取到缓存
 		resRouter = Local.get('requestOldRoutes');
+		// 触发初始化用户信息 pinia
+		const authBtnList = Local.get('authBtns') ?? [];
+		await useUserInfo().setUserInfos(authBtnList);
 		// 无登录权限时,添加判断
 		// https://gitee.com/lyt-top/vue-next-admin/issues/I64HVO
 		if (resRouter?.length <= 0) return Promise.resolve(true);
 	} else {
 		// 获取路由菜单数据
-		let res: any = await getBackEndControlRoutes();
+		const res: any = await getBackEndControlRoutes();
+		const buttons = res.result?.buttons ?? [] // 权限按钮列表
 		// 无登录权限时,添加判断
 		// https://gitee.com/lyt-top/vue-next-admin/issues/I64HVO
-		let routerList = res?.result ?? [];
+		let routerList = res.result?.menus ?? [];
 		if (routerList <= 0) return Promise.resolve(true);
 		// 路由内容格式化
-		resRouter = formatRouter(res.result);
+		resRouter = formatRouter(routerList);
 		// 存储接口原始路由(未处理component),根据需求选择使用
 		useRequestOldRoutes().setRequestOldRoutes(JSON.parse(JSON.stringify(resRouter)));
 		// 存入缓存
 		Local.set('requestOldRoutes', resRouter);
 		// 获取系统配置
 		getAppConfigFn();
+		// 触发初始化用户信息 pinia
+		await useUserInfo().setUserInfos(buttons);
+		// 存入缓存
+		Local.set('authBtns', buttons);
 	}
 	// 检查是否修改过密码
 	checkPwd();
-	// 触发初始化用户信息 pinia
-	await useUserInfo().setUserInfos();
 	// 处理路由(component),替换 dynamicRoutes(/@/router/route)第一个顶级 children 的路由
 	dynamicRoutes[0].children = await backEndComponent(resRouter);
 	// 添加动态路由

+ 8 - 8
src/stores/userInfo.ts

@@ -2,7 +2,7 @@ import { defineStore } from 'pinia';
 import { UserInfosStates } from './interface';
 import { Session } from '/@/utils/storage';
 import { seatLayoutCode } from '/@/utils/appConfig'
-import { getBtns, getUserInfo } from "/@/api/login/user"
+import { getUserInfo } from "/@/api/login/user"
 
 /**
  * 用户信息
@@ -23,19 +23,15 @@ export const useUserInfo = defineStore('userInfo', {
 		},
 	}),
 	actions: {
-		async setUserInfos() {
+		async setUserInfos(buttons: string[]) {
 			if (Session.get('userInfo')) { //有缓存 取缓存
 				this.userInfos = Session.get('userInfo');
 			} else {
-				this.userInfos = this.getApiUserInfo() as any;
+				this.userInfos = this.getApiUserInfo(buttons) as any;
 			}
 		},
-		async getApiUserInfo() {
+		async getApiUserInfo(buttons: string[]) {
 			try {
-				//授权按钮
-				let btnInfo: any = await getBtns();
-				this.userInfos.showTelControl = btnInfo.result.includes(seatLayoutCode); // 查询是否有展示面板权限
-				this.userInfos.authBtnList = btnInfo.result;
 				// 个人信息
 				let userInfo: any = await getUserInfo();
 				this.userInfos.name = userInfo.result.name;
@@ -45,7 +41,11 @@ export const useUserInfo = defineStore('userInfo', {
 				this.userInfos.id = userInfo.result.id;
 				this.userInfos.token = Session.get('token');
 				this.userInfos.photo = "";
+				//授权按钮
+				this.userInfos.showTelControl = buttons.includes(seatLayoutCode); // 查询是否有展示面板权限
+				this.userInfos.authBtnList = buttons;
 				Session.set('userInfo', this.userInfos);
+
 				return this.userInfos;
 			} catch (error) {
 				this.userInfos = {

+ 55 - 13
src/utils/tools.ts

@@ -142,22 +142,64 @@ export const commonEeum = {
     Rest: 'Rest', // 小休常用意见
 };
 /**
- * 数字转换
- * @param  {number} value  数字
- * @returns 转换为大写 一二三四
+ * //阿拉伯数字转大写,整数转大写
+ * @param  {number | string} value  数字
+ * @returns
  */
-export function upNumber(value: number) {
-    if (!/(^[1-9]\d*$)/) {
-        return '非法数字';
+export function upNumber(num: string | number, type = '') {
+    if (!num) return 0
+    const strNum = Number((num + '').replace(/[,,]*/g, '')) + '' // 记录字符
+    num = parseInt(strNum) // 转为整数,
+    let capitalAr = '零一二三四五六七八九十'
+    let unitAr = ['十', '百', '千', '万', '十', '百', '千', '亿', '十', '百', '千']
+    if (type) {
+        capitalAr = '零壹贰叁肆伍陆柒捌玖拾'
+        unitAr = ['拾', '佰', '仟', '万', '拾', '佰', '仟', '亿', '拾', '佰', '仟'] // 单位
     }
-    let N = ["零", "一", "二", "三", "四", "五", "六", "七", "八", "九"];
-    let str = value.toString();
-    let len = value.toString().length;
-    let C_Num = [];
-    for (let i = 0; i < len; i++) {
-        C_Num.push(N[str.charAt(i)]);
+    const resultAr = <any>[] // 记录结果,后边json.in就可
+    let index = strNum.length - 1 //记录位数
+    let idx = 0 // 记录单位
+    let percent = 10
+    const turnNum = (num: number, percent: number, index: number) => {
+        const unit = num / percent
+        const capital = capitalAr[Number(strNum[index])]
+        if (unit < 1) {
+            resultAr.push(capital)
+            // 出现11【一十一】这种情况
+            if (Number(strNum[index]) === 1 && (strNum.length === 2 || strNum.length === 6 || strNum.length === 10)) {
+                resultAr.pop()
+            }
+            return false //结束递归
+        } else {
+            if (capital === '零') {
+                // 万和亿单位不删除
+                if (!['万', '亿'].includes(resultAr[resultAr.length - 1])) {
+                    resultAr.pop()
+                }
+                // 前面有零在删掉一个零
+                if (resultAr[resultAr.length - 1] === '零') {
+                    resultAr.pop()
+                }
+            }
+            resultAr.push(capital)
+            // 过滤存在【零万】【零亿】这种情况
+            if (['万', '亿'].includes(resultAr[resultAr.length - 2]) && capital === '零') {
+                resultAr.pop()
+            }
+            // 过滤【1亿万】这种情况
+            if (resultAr[0] === '万' && resultAr[1] === '亿') {
+                resultAr.shift()
+            }
+            // 末尾【零】删掉
+            if (resultAr[0] === '零') {
+                resultAr.pop()
+            }
+            resultAr.push(unitAr[idx++])
+            turnNum(num, percent * 10, --index)
+        }
     }
-    return C_Num.join("");
+    turnNum(num, percent, index)
+    return resultAr.reverse().join('')
 }
 function S4(): string {
     return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);

+ 4 - 4
src/views/business/order/accept/repeatOrderDetail.vue

@@ -137,7 +137,7 @@
 										{{ state.ruleForm.content }}
 									</el-form-item>
 								</el-col>
-								<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+								<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" v-if="state.ruleForm.additions && state.ruleForm.additions.length">
 									<el-form-item label="附件"> </el-form-item>
 								</el-col>
 							</el-row>
@@ -151,7 +151,7 @@
 							<b class="font14">补充信息</b>
 						</p>
 					</template>
-					<div class="collapse-container">
+					<div class="collapse-container pb10">
 						<div v-for="(i, index) in state.supplements" :key="i" class="plug-container">
 							<p class="plug-container-title">第{{ upNumber(index + 1) }}次</p>
 							<el-form label-width="100px" ref="ruleFormRef" class="pt10 pb10">
@@ -167,7 +167,7 @@
 											{{ i.opinion }}
 										</el-form-item>
 									</el-col>
-									<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+									<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" v-if="i.additions && i.additions.length">
 										<el-form-item label="附件"> </el-form-item>
 									</el-col>
 								</el-row>
@@ -367,7 +367,7 @@ onMounted(() => {
 		border-radius: var(--el-border-radius-base);
 	}
 	:deep(.el-collapse-item__content) {
-		padding-bottom: 0 !important;
+		padding-bottom: 10px !important;
 		.el-form-item {
 			margin-bottom: 5px;
 			.el-form-item__content {

+ 12 - 10
src/views/business/order/components/orderDetail.vue

@@ -137,7 +137,7 @@
 										{{ state.ruleForm.content }}
 									</el-form-item>
 								</el-col>
-								<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+								<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" v-if="state.ruleForm.additions && state.ruleForm.additions.length">
 									<el-form-item label="附件"> </el-form-item>
 								</el-col>
 							</el-row>
@@ -157,17 +157,19 @@
 							<el-form label-width="100px" ref="ruleFormRef" class="pt10 pb10">
 								<el-row :gutter="35">
 									<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
-										<el-form-item label="补充人"> {{ i.creator.name }}[{{ i.creator.staffNo }} ] </el-form-item>
+										<el-form-item label="补充人">
+											{{ i.creator.name }} <span v-if="i.creator.staffNo">[{{ i.creator.staffNo }} ]</span>
+										</el-form-item>
 									</el-col>
 									<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
-										<el-form-item label="补充时间"> {{ formatDate(i.creator.creationTime, 'YYYY-mm-dd HH:MM:SS') }} </el-form-item>
+										<el-form-item label="补充时间"> {{ formatDate(i.creationTime, 'YYYY-mm-dd HH:MM:SS') }} </el-form-item>
 									</el-col>
 									<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
 										<el-form-item label="补充详情">
 											{{ i.opinion }}
 										</el-form-item>
 									</el-col>
-									<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+									<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" v-if="i.additions && i.additions.length">
 										<el-form-item label="附件"> </el-form-item>
 									</el-col>
 								</el-row>
@@ -267,13 +269,13 @@
 						v-if="state.ruleForm.status != 60 && state.workflow.canHandle"
 						>办 理</el-button
 					>
-					<!-- <el-button type="primary" @click="onSubmit('延期')" :loading="state.loading">延 期</el-button> -->
+					<!-- <el-button type="primary" @click="onSubmit('延期')" v-if="state.ruleForm.status != 60 && state.workflow.canHandle" :loading="state.loading">延 期</el-button> -->
 					<!-- 流程结束之后不展示补充按钮 -->
-					<el-button type="primary" @click="onSupply" :loading="state.loading" v-if="state.workflow.status != 2">补 充</el-button>
-					<!-- <el-button type="primary" @click="onSubmit('督办')" :loading="state.loading">督 办</el-button> -->
-					<!-- <el-button type="primary" @click="onSubmit('撤销')" :loading="state.loading">撤 销</el-button> -->
+					<el-button type="primary" @click="onSupply" :loading="state.loading" v-if="state.workflow.status == 0">补 充</el-button>
+					<!-- <el-button type="primary" @click="onSubmit('督办')" :loading="state.loading" v-if="state.ruleForm.status == 0">督 办</el-button> -->
+					<!-- <el-button type="primary" @click="onSubmit('撤销')" :loading="state.loading" v-if="state.ruleForm.status == 0">撤 销</el-button> -->
 					<!-- 工单未归档都可以撤回 -->
-					<el-button type="primary" @click="onSubmit('撤回', 'recall')" :loading="state.loading" v-if="state.ruleForm.status != 60">撤 回</el-button>
+					<el-button type="primary" @click="onSubmit('撤回', 'recall')" :loading="state.loading" v-if="state.ruleForm.status == 0">撤 回</el-button>
 					<!-- 工单未归档和可以办理展示退回按钮 -->
 					<el-button
 						type="primary"
@@ -449,7 +451,7 @@ defineExpose({
 		border-radius: var(--el-border-radius-base);
 	}
 	:deep(.el-collapse-item__content) {
-		padding-bottom: 0 !important;
+		padding-bottom: 10px !important;
 		.el-form-item {
 			margin-bottom: 5px;
 			.el-form-item__content {

+ 0 - 1
src/views/business/order/index.vue

@@ -571,7 +571,6 @@ onMounted(async () => {
 .business-oreder-container {
 	.arrow {
 		transition: transform var(--el-transition-duration);
-		transform: rotateZ(0);
 		cursor: pointer;
 	}
 	.arrow.is-reverse {

+ 19 - 16
src/views/system/config/dict/index.vue

@@ -31,10 +31,10 @@
 						<div class="flex-column">
 							<div class="flex-between mb10">
 								<el-form :model="state.queryParams" ref="ruleFormRef" :inline="true" @submit.native.prevent>
-									<el-form-item label="关键字" prop="keyword"  class="mb0">
+									<el-form-item label="关键字" prop="keyword" class="mb0">
 										<el-input v-model="state.queryParams.keyword" placeholder="字典值名称/字典值" clearable @keyup.enter="handleQuery" />
 									</el-form-item>
-									<el-form-item  class="mb0">
+									<el-form-item class="mb0">
 										<el-button type="primary" @click="handleQuery" :loading="state.tableLoading" v-waves>
 											<SvgIcon name="ele-Search" class="mr5" />查询
 										</el-button>
@@ -112,7 +112,7 @@ const state = reactive<any>({
 		loading: false,
 	},
 	tableData: [],
-	staticArr:[],
+	staticArr: [],
 	total: 0,
 	loading: false,
 	tableLoading: false,
@@ -208,21 +208,24 @@ const getAllIds = (arr: any) => {
 };
 // 获取字典类型列表
 const getDictypeList = () => {
-	state.loading = true;
-	dictypeList()
-		.then((res: any) => {
-			state.dictypeList = res?.result ?? [];
-			state.queryParams.typeid = res?.result[0].id ?? '';
-			getList();
-			state.loading = false;
-		})
-		.catch(() => {
-			state.loading = false;
-		});
+	if (!auth('100601')) ElMessage.error('抱歉,您没有字典类型列表权限!');
+	else {
+		state.loading = true;
+		dictypeList()
+			.then((res: any) => {
+				state.dictypeList = res?.result ?? [];
+				state.queryParams.typeid = res?.result[0].id ?? '';
+				getList();
+				state.loading = false;
+			})
+			.catch(() => {
+				state.loading = false;
+			});
+	}
 };
 /* 获取字典列表 */
 const getList = () => {
-	if (!auth('100601')) ElMessage.error('抱歉,您没有字典列表权限!');
+	if (!auth('100602')) ElMessage.error('抱歉,您没有字典列表权限!');
 	else {
 		state.tableLoading = true;
 		getDataByTypeid(state.queryParams)
@@ -288,7 +291,7 @@ const resetQuery = throttle((formEl: FormInstance | undefined) => {
 	getList();
 	state.expandedRowKeys = [];
 	emptyArr = [];
-},500)
+}, 500);
 
 // 删除字典
 // const onRowDel = (row: any) => {

+ 11 - 18
src/views/system/menu/component/addMenu.vue

@@ -24,8 +24,9 @@
 					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
 						<el-form-item label="菜单类型">
 							<el-radio-group v-model="state.ruleForm.menuType">
-								<el-radio label="menu">菜单</el-radio>
-								<el-radio label="btn">按钮</el-radio>
+								<el-radio :label="1">目录</el-radio>
+								<el-radio :label="2">菜单</el-radio>
+								<el-radio :label="3">按钮</el-radio>
 							</el-radio-group>
 						</el-form-item>
 					</el-col>
@@ -34,7 +35,7 @@
 							<el-input v-model="state.ruleForm.pageName" placeholder="请输入菜单名称" clearable></el-input>
 						</el-form-item>
 					</el-col>
-					<template v-if="state.ruleForm.menuType === 'menu'">
+					<template v-if="[1, 2].includes(state.ruleForm.menuType)">
 						<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
 							<el-form-item
 								label="路由名称"
@@ -79,18 +80,18 @@
 								</el-input>
 							</el-form-item>
 						</el-col>
-						<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
-							<el-form-item label="权限编码" prop="permissionCode" :rules="[{ required: true, message: '请输入权限编码', trigger: 'blur' }]">
-								<el-input v-model="state.ruleForm.permissionCode" placeholder="权限编码" clearable> </el-input>
-							</el-form-item>
-						</el-col>
 					</template>
+					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
+						<el-form-item label="权限编码" prop="permissionCode" :rules="[{ required: true, message: '请输入权限编码', trigger: 'blur' }]">
+							<el-input v-model="state.ruleForm.permissionCode" placeholder="权限编码" clearable> </el-input>
+						</el-form-item>
+					</el-col>
 					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
 						<el-form-item label="菜单排序">
 							<el-input-number v-model="state.ruleForm.displayOrder" controls-position="right" placeholder="请输入排序" class="w100" />
 						</el-form-item>
 					</el-col>
-					<template v-if="state.ruleForm.menuType === 'menu'">
+					<template v-if="[1, 2].includes(state.ruleForm.menuType)">
 						<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
 							<el-form-item label="快捷入口">
 								<el-switch v-model="state.ruleForm.isFast" inline-prompt active-text="是" inactive-text="否" />
@@ -148,13 +149,6 @@
 							</el-form-item>
 						</el-col>
 					</template>
-					<template v-if="state.ruleForm.menuType === 'btn'">
-						<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
-							<el-form-item label="权限标识">
-								<el-input v-model="state.ruleForm.btnPower" placeholder="请输入权限标识" clearable></el-input>
-							</el-form-item>
-						</el-col>
-					</template>
 				</el-row>
 			</el-form>
 			<template #footer>
@@ -171,7 +165,6 @@
 import { defineAsyncComponent, reactive, ref } from 'vue';
 import { ElMessage } from 'element-plus';
 import { getImageUrl } from '/@/utils/tools';
-import { setBackEndControlRefreshRoutes } from '/@/router/backEnd';
 import { FormInstance } from 'element-plus';
 import { addMenu, getMenuList } from '/@/api/system/menu';
 
@@ -204,6 +197,7 @@ const state = reactive<any>({
 		isFast: false, //是否是快捷入口
 		fastIcon: '', //入口图片
 		redirect: '', // 重定向地址
+		menuType: 1, //菜单类型 1:目录 2:菜单 3:按钮
 	},
 	menuData: [], // 上级菜单数据
 	loading: false,
@@ -270,7 +264,6 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
 				emit('updateList');
 				closeDialog(); // 关闭弹窗
 				ElMessage.success('新增成功');
-				setBackEndControlRefreshRoutes();
 			});
 		} else {
 			console.log('error submit!', fields);

+ 114 - 97
src/views/system/menu/component/editMenu.vue

@@ -21,50 +21,66 @@
 							</el-cascader>
 						</el-form-item>
 					</el-col>
-					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
-						<el-form-item label="菜单名称" prop="pageName" :rules="[{ required: true, message: '请输入菜单名称', trigger: 'blur' }]">
-							<el-input v-model="state.ruleForm.pageName" placeholder="请输入菜单名称" clearable></el-input>
-						</el-form-item>
-					</el-col>
-					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
-						<el-form-item
-							label="路由名称"
-							prop="ruleName"
-							:rules="[{ required: state.ruleForm.isKeepAlive, message: '请输入路由名称', trigger: 'blur' }]"
-						>
-							<el-input v-model="state.ruleForm.ruleName" placeholder="路由中的 name 值" clearable></el-input>
-						</el-form-item>
-					</el-col>
-					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
-						<el-form-item label="路由路径" prop="path" :rules="[{ required: true, message: '请输入路由路径', trigger: 'blur' }]">
-							<el-input v-model="state.ruleForm.path" placeholder="路由中的 path 值" clearable></el-input>
-						</el-form-item>
-					</el-col>
-					<el-col :xs="12" :sm="12" :md="12" :lg="12" :xl="12">
-						<el-form-item label="重定向" prop="redirect" :rules="[{ required: false, message: '请输入重定向', trigger: 'blur' }]">
-							<el-input v-model="state.ruleForm.redirect" placeholder="重定向" clearable></el-input>
-						</el-form-item>
-					</el-col>
 					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
-						<el-form-item label="组件路径" prop="component" :rules="[{ required: false, message: '请输入组件路径', trigger: 'blur' }]">
-							<el-input v-model="state.ruleForm.component" placeholder="组件路径" clearable></el-input>
-						</el-form-item>
-					</el-col>
-					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
-						<el-form-item label="菜单图标" prop="icon" :rules="[{ required: false, message: '请输入菜单名称', trigger: 'blur' }]">
-							<IconSelector placeholder="请输入菜单图标" v-model="state.ruleForm.icon" type="all" />
+						<el-form-item label="菜单类型">
+							<el-radio-group v-model="state.ruleForm.menuType">
+								<el-radio :label="1">目录</el-radio>
+								<el-radio :label="2">菜单</el-radio>
+								<el-radio :label="3">按钮</el-radio>
+							</el-radio-group>
 						</el-form-item>
 					</el-col>
 					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
-						<el-form-item
-							label="链接地址"
-							prop="link"
-							:rules="[{ required: state.ruleForm.isLink, message: '请输入外链/内嵌时链接地址', trigger: 'blur' }]"
-						>
-							<el-input v-model="state.ruleForm.link" placeholder="外链/内嵌时链接地址(http:xxx.com)" clearable :disabled="!state.ruleForm.isLink">
-							</el-input>
+						<el-form-item label="菜单名称" prop="pageName" :rules="[{ required: true, message: '请输入菜单名称', trigger: 'blur' }]">
+							<el-input v-model="state.ruleForm.pageName" placeholder="请输入菜单名称" clearable></el-input>
 						</el-form-item>
 					</el-col>
+					<template v-if="[1, 2].includes(state.ruleForm.menuType)">
+						<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
+							<el-form-item
+								label="路由名称"
+								prop="ruleName"
+								:rules="[{ required: state.ruleForm.isKeepAlive, message: '请输入路由名称', trigger: 'blur' }]"
+							>
+								<el-input v-model="state.ruleForm.ruleName" placeholder="路由中的 name 值" clearable></el-input>
+							</el-form-item>
+						</el-col>
+						<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
+							<el-form-item label="路由路径" prop="path" :rules="[{ required: true, message: '请输入路由路径', trigger: 'blur' }]">
+								<el-input v-model="state.ruleForm.path" placeholder="路由中的 path 值" clearable></el-input>
+							</el-form-item>
+						</el-col>
+						<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
+							<el-form-item label="重定向" prop="redirect" :rules="[{ required: false, message: '请输入重定向', trigger: 'blur' }]">
+								<el-input v-model="state.ruleForm.redirect" placeholder="重定向" clearable></el-input>
+							</el-form-item>
+						</el-col>
+						<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+							<el-form-item label="组件路径" prop="component" :rules="[{ required: false, message: '请输入组件路径', trigger: 'blur' }]">
+								<el-input v-model="state.ruleForm.component" placeholder="组件路径" clearable></el-input>
+							</el-form-item>
+						</el-col>
+						<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
+							<el-form-item label="菜单图标" prop="icon" :rules="[{ required: false, message: '请输入菜单名称', trigger: 'blur' }]">
+								<IconSelector placeholder="请输入菜单图标" v-model="state.ruleForm.icon" type="all" />
+							</el-form-item>
+						</el-col>
+						<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
+							<el-form-item
+								label="链接地址"
+								prop="link"
+								:rules="[{ required: state.ruleForm.isLink, message: '请输入外链/内嵌时链接地址', trigger: 'blur' }]"
+							>
+								<el-input
+									v-model="state.ruleForm.link"
+									placeholder="外链/内嵌时链接地址(http:xxx.com)"
+									clearable
+									:disabled="!state.ruleForm.isLink"
+								>
+								</el-input>
+							</el-form-item>
+						</el-col>
+					</template>
 					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
 						<el-form-item label="权限编码" prop="permissionCode" :rules="[{ required: true, message: '请输入权限编码', trigger: 'blur' }]">
 							<el-input v-model="state.ruleForm.permissionCode" placeholder="权限编码" clearable> </el-input>
@@ -75,62 +91,64 @@
 							<el-input-number v-model="state.ruleForm.displayOrder" controls-position="right" placeholder="请输入排序" class="w100" />
 						</el-form-item>
 					</el-col>
-					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
-						<el-form-item label="快捷入口">
-							<el-switch v-model="state.ruleForm.isFast" inline-prompt active-text="是" inactive-text="否" />
-						</el-form-item>
-					</el-col>
-					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="state.ruleForm.isFast">
-						<el-form-item label="入口图片" prop="title" :rules="[{ required: false, message: '请选择入口图片', trigger: 'blur' }]">
-							<template #label>
-								<p class="flex-center-align">
-									入口图片
-									<el-tooltip content="首页快捷入口图标" placement="top-start" trigger="click">
-										<SvgIcon name="ele-QuestionFilled" class="cursor-help"></SvgIcon>
-									</el-tooltip>
-								</p>
-							</template>
-							<el-select v-model="state.ruleForm.fastIcon" placeholder="请选择入口图片" class="w100" clearable>
-								<el-option v-for="item in state.imgList" :key="item.value" :label="item.label" :value="item.value">
-									<div class="item-Img">
-										<span>{{ item.label }}</span>
-										<img v-lazy="getImageUrl(item.label)" alt="" />
-									</div>
-								</el-option>
-							</el-select>
-						</el-form-item>
-					</el-col>
-					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
-						<el-form-item label="是否隐藏">
-							<el-switch v-model="state.ruleForm.isHide" inline-prompt active-text="是" inactive-text="否" />
-						</el-form-item>
-					</el-col>
-					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
-						<el-form-item label="是否缓存">
-							<el-switch v-model="state.ruleForm.isKeepAlive" inline-prompt active-text="是" inactive-text="否" />
-						</el-form-item>
-					</el-col>
-					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
-						<el-form-item label="是否固定">
-							<el-switch v-model="state.ruleForm.isAffix" inline-prompt active-text="是" inactive-text="否" />
-						</el-form-item>
-					</el-col>
-					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
-						<el-form-item label="是否外链">
-							<el-switch
-								v-model="state.ruleForm.isLink"
-								:disabled="state.ruleForm.isLink && state.ruleForm.isIframe"
-								inline-prompt
-								active-text="是"
-								inactive-text="否"
-							/>
-						</el-form-item>
-					</el-col>
-					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
-						<el-form-item label="是否内嵌">
-							<el-switch v-model="state.ruleForm.isIframe" @change="onSelectIframeChange" inline-prompt active-text="是" inactive-text="否" />
-						</el-form-item>
-					</el-col>
+					<template v-if="[1, 2].includes(state.ruleForm.menuType)">
+						<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
+							<el-form-item label="快捷入口">
+								<el-switch v-model="state.ruleForm.isFast" inline-prompt active-text="是" inactive-text="否" />
+							</el-form-item>
+						</el-col>
+						<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" v-if="state.ruleForm.isFast">
+							<el-form-item label="入口图片" prop="title" :rules="[{ required: false, message: '请选择入口图片', trigger: 'blur' }]">
+								<template #label>
+									<p class="flex-center-align">
+										入口图片
+										<el-tooltip content="首页快捷入口图标" placement="top-start" trigger="click">
+											<SvgIcon name="ele-QuestionFilled" class="cursor-help"></SvgIcon>
+										</el-tooltip>
+									</p>
+								</template>
+								<el-select v-model="state.ruleForm.fastIcon" placeholder="请选择入口图片" class="w100" clearable>
+									<el-option v-for="item in state.imgList" :key="item.value" :label="item.label" :value="item.value">
+										<div class="item-Img">
+											<span>{{ item.label }}</span>
+											<img v-lazy="getImageUrl(item.label)" alt="" />
+										</div>
+									</el-option>
+								</el-select>
+							</el-form-item>
+						</el-col>
+						<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
+							<el-form-item label="是否隐藏">
+								<el-switch v-model="state.ruleForm.isHide" inline-prompt active-text="是" inactive-text="否" />
+							</el-form-item>
+						</el-col>
+						<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
+							<el-form-item label="是否缓存">
+								<el-switch v-model="state.ruleForm.isKeepAlive" inline-prompt active-text="是" inactive-text="否" />
+							</el-form-item>
+						</el-col>
+						<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
+							<el-form-item label="是否固定">
+								<el-switch v-model="state.ruleForm.isAffix" inline-prompt active-text="是" inactive-text="否" />
+							</el-form-item>
+						</el-col>
+						<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
+							<el-form-item label="是否外链">
+								<el-switch
+									v-model="state.ruleForm.isLink"
+									:disabled="state.ruleForm.isLink && state.ruleForm.isIframe"
+									inline-prompt
+									active-text="是"
+									inactive-text="否"
+								/>
+							</el-form-item>
+						</el-col>
+						<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12">
+							<el-form-item label="是否内嵌">
+								<el-switch v-model="state.ruleForm.isIframe" @change="onSelectIframeChange" inline-prompt active-text="是" inactive-text="否" />
+							</el-form-item>
+						</el-col>
+					</template>
 				</el-row>
 			</el-form>
 			<template #footer>
@@ -148,7 +166,6 @@ import { defineAsyncComponent, reactive, ref } from 'vue';
 import { ElMessage, FormInstance } from 'element-plus';
 import { getImageUrl, excludeSelfById } from '/@/utils/tools';
 import { auth } from '/@/utils/authFunction';
-import { setBackEndControlRefreshRoutes } from '/@/router/backEnd';
 import { updateMenu, getMenuList, getMenuById } from '/@/api/system/menu';
 
 // 引入组件
@@ -159,7 +176,7 @@ const emit = defineEmits(['updateList']);
 
 // 定义变量内容
 const ruleFormRef = ref<FormInstance>();
-const state = reactive({
+const state = reactive<any>({
 	isShowDialog: false,
 	// 参数请参考 `/src/router/route.ts` 中的 `dynamicRoutes` 路由菜单格式
 	ruleForm: {
@@ -180,6 +197,7 @@ const state = reactive({
 		isFast: false, //是否是快捷入口
 		fastIcon: '', //入口图片
 		redirect: '', // 重定向地址
+		menuType: 1, //菜单类型 1:目录 2:菜单 3:按钮
 	},
 	menuData: <any>[], // 上级菜单数据
 	loading: false,
@@ -253,7 +271,6 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
 				emit('updateList');
 				closeDialog(); // 关闭弹窗
 				ElMessage.success('修改成功');
-				setBackEndControlRefreshRoutes();
 			});
 		} else {
 			console.log('error submit!', fields);

+ 219 - 64
src/views/system/menu/index.vue

@@ -26,55 +26,23 @@
 				</div>
 			</div>
 			<!-- 表格 -->
-			<el-table :data="state.menuTableData" row-key="id" class="table" v-loading="state.loading" ref="dataTreeList">
-				<el-table-column prop="date" label="菜单名称" width="400">
-					<template #default="{ row }"> <SvgIcon :name="row.icon" class="mr5" />{{ row.pageName }}</template>
-				</el-table-column>
-				<el-table-column prop="path" label="路由路径" width="400" />
-				<el-table-column prop="component" label="组件路径" width="300" />
-				<el-table-column prop="displayOrder" label="排序" />
-				<el-table-column prop="isFast" label="快捷入口">
-					<template #default="{ row }">
-						<el-tag :type="row.isFast ? 'success' : 'info'">{{ row.isFast ? '是' : '否' }} </el-tag>
-					</template>
-				</el-table-column>
-				<el-table-column prop="isFast" label="快捷入口">
-					<template #default="{ row }">
-						<el-tag :type="row.isFast ? 'success' : 'info'">{{ row.isFast ? '是' : '否' }} </el-tag>
-					</template>
-				</el-table-column>
-				<el-table-column prop="isHide" label="是否隐藏">
-					<template #default="{ row }">
-						<el-tag :type="row.isHide ? 'success' : 'info'">{{ row.isHide ? '是' : '否' }} </el-tag>
-					</template>
-				</el-table-column>
-				<el-table-column prop="isKeepAlive" label="是否隐藏">
-					<template #default="{ row }">
-						<el-tag :type="row.isKeepAlive ? 'success' : 'info'">{{ row.isKeepAlive ? '是' : '否' }} </el-tag>
-					</template>
-				</el-table-column>
-				<el-table-column prop="isAffix" label="是否固定">
-					<template #default="{ row }">
-						<el-tag :type="row.isAffix ? 'success' : 'info'">{{ row.isAffix ? '是' : '否' }} </el-tag>
-					</template>
-				</el-table-column>
-				<el-table-column prop="isLink" label="是否外链">
-					<template #default="{ row }">
-						<el-tag :type="row.isLink ? 'success' : 'info'">{{ row.isLink ? '是' : '否' }} </el-tag>
-					</template>
-				</el-table-column>
-				<el-table-column prop="isLink" label="菜单类型">
-					<template #default="{ row }">
-						<el-tag :type="row.isLink ? 'success' : 'info'">{{ row.isLink ? '菜单' : '按钮' }} </el-tag>
-					</template>
-				</el-table-column>
-				<el-table-column label="操作" fixed="right" align="center" width="110">
-					<template #default="{ row }">
-						<el-button link type="primary" @click="onOpenEditMenu(row)" title="修改菜单" v-auth="'100402'">修改</el-button>
-						<el-button link type="danger" @click="onTabelRowDel(row)" title="删除菜单" v-auth="'100403'">删除</el-button>
-					</template>
-				</el-table-column>
-			</el-table>
+			<el-auto-resizer class="table" v-loading="state.loading">
+				<template #default="{ height, width }">
+					<el-table-v2
+						v-model:expanded-row-keys="state.expandedRowKeys"
+						:columns="state.columns"
+						:data="state.menuTableData"
+						expand-column-key="pageName"
+						fixed
+						:width="width"
+						:height="height"
+					>
+						<template #empty>
+							<Empty />
+						</template>
+					</el-table-v2>
+				</template>
+			</el-auto-resizer>
 		</div>
 		<AddMenu ref="addMenuRef" @updateList="getMenuListApi" />
 		<EditMenu ref="editMenuRef" @updateList="getMenuListApi" />
@@ -82,11 +50,12 @@
 </template>
 
 <script lang="ts" setup name="systemmenu">
-import { defineAsyncComponent, ref, reactive, onMounted } from 'vue';
+import { defineAsyncComponent, ref, h, reactive, onMounted, watch } from 'vue';
 import { RouteRecordRaw } from 'vue-router';
-import { ElMessageBox, ElMessage } from 'element-plus';
+import { ElMessageBox, ElMessage, ElButton, ElTag } from 'element-plus';
 import type { FormInstance } from 'element-plus';
 import { getMenuList, removeMenu } from '/@/api/system/menu';
+import { auth, authAll } from '/@/utils/authFunction';
 import SvgIcon from '/@/views/system/menu/component/SvgIcon.vue';
 import { throttle } from '/@/utils/tools';
 // 引入组件
@@ -104,15 +73,180 @@ const state = reactive({
 	queryParams: {
 		keyword: '',
 	},
+	expandedRowKeys: <any>[],
+	columns: [
+		{
+			key: 'pageName',
+			dataKey: 'pageName',
+			title: '菜单名称',
+			width: 300,
+			cellRenderer: (data: any) => {
+				return h('p', { style: { justifyContent: 'flex-end' } }, [
+					h(SvgIcon, { name: data.rowData.icon }, ''),
+					h('span', { class: 'pl5' }, { default: () => data.rowData.pageName }),
+				]);
+			},
+		},
+		{
+			key: 'menuType',
+			dataKey: 'menuType',
+			title: '菜单类型',
+			width: 100,
+			cellRenderer: ({ rowData }: any) => {
+				switch (rowData.menuType) {
+					case 1:
+						return h(ElTag, { type: 'success' }, { default: () => '目录' });
+						break;
+					case 2:
+						return h(ElTag, { type: 'success' }, { default: () => '菜单' });
+						break;
+					case 3:
+						return h(ElTag, { type: 'info' }, { default: () => '按钮' });
+						break;
+					default:
+						return h(ElTag, { type: 'success' }, { default: () => '按钮' });
+						break;
+				}
+			},
+		},
+		{
+			key: 'permissionCode',
+			dataKey: 'permissionCode',
+			title: '权限编码',
+			width: 100,
+		},
+		{
+			key: 'displayOrder',
+			dataKey: 'displayOrder',
+			title: '排序',
+			width: 80,
+		},
+		{
+			key: 'component',
+			dataKey: 'component',
+			title: '组件路径',
+			width: 400,
+		},
+		{
+			key: 'isFast',
+			dataKey: 'isFast',
+			title: '快捷入口',
+			width: 100,
+			cellRenderer: ({ rowData }) => {
+				if ([1, 2].includes(rowData.menuType)) {
+					return h(ElTag, { type: rowData.isFast ? 'success' : 'info' }, { default: () => (rowData.isFast ? '是' : '否') });
+				}
+			},
+		},
+		{
+			key: 'isHide',
+			dataKey: 'isHide',
+			title: '是否隐藏',
+			width: 100,
+			cellRenderer: ({ rowData }) => {
+				if ([1, 2].includes(rowData.menuType)) {
+					return h(ElTag, { type: rowData.isHide ? 'success' : 'info' }, { default: () => (rowData.isFast ? '是' : '否') });
+				}
+			},
+		},
+		{
+			key: 'isKeepAlive',
+			dataKey: 'isKeepAlive',
+			title: '是否缓存',
+			width: 100,
+			cellRenderer: ({ rowData }) => {
+				if ([1, 2].includes(rowData.menuType)) {
+					return h(ElTag, { type: rowData.isKeepAlive ? 'success' : 'info' }, { default: () => (rowData.isFast ? '是' : '否') });
+				}
+			},
+		},
+		{
+			key: 'isAffix',
+			dataKey: 'isAffix',
+			title: '是否固定',
+			width: 100,
+			cellRenderer: ({ rowData }) => {
+				if ([1, 2].includes(rowData.menuType)) {
+					return h(ElTag, { type: rowData.isAffix ? 'success' : 'info' }, { default: () => (rowData.isFast ? '是' : '否') });
+				}
+			},
+		},
+		{
+			key: 'isLink',
+			dataKey: 'isLink',
+			title: '是否外链',
+			width: 100,
+			cellRenderer: ({ rowData }) => {
+				if ([1, 2].includes(rowData.menuType)) {
+					return h(ElTag, { type: rowData.isLink ? 'success' : 'info' }, { default: () => (rowData.isFast ? '是' : '否') });
+				}
+			},
+		},
+		{
+			key: 'handle',
+			title: '操作',
+			width: 100,
+			fixed: 'right',
+			align: 'center',
+			cellRenderer: (data: any) => {
+				if (authAll(['100402', '100403'])) {
+					// 权限判断
+					return h('span', { class: 'flex' }, [
+						h(
+							ElButton,
+							{
+								onClick: () => onOpenEditMenu(data.rowData),
+								type: 'primary',
+								link: true,
+								title: '修改菜单',
+							},
+							{ default: () => '修改' }
+						),
+						h(
+							ElButton,
+							{
+								onClick: () => onTabelRowDel(data.rowData),
+								type: 'danger',
+								link: true,
+								title: '删除菜单',
+							},
+							{ default: () => '删除' }
+						),
+					]);
+				} else if (auth('100402')) {
+					return h(
+						ElButton,
+						{
+							onClick: () => onOpenEditMenu(data.rowData),
+							type: 'primary',
+							link: true,
+							title: '修改菜单',
+						},
+						{ default: () => '修改' }
+					);
+				} else if (auth('100403')) {
+					return h(
+						ElButton,
+						{
+							onClick: () => onTabelRowDel(data.rowData),
+							type: 'danger',
+							link: true,
+							title: '删除菜单',
+						},
+						{ default: () => '删除' }
+					);
+				}
+			},
+		},
+	],
 	isExpand: false,
 });
-const dataTreeList = ref();
 const formatTable = (list: any[], keyword: string) => {
 	if (!list.length || !Array.isArray(list)) return [];
 	let emptyArr: any[] = [];
 	list.map((item) => {
 		if (item.pageName.includes(keyword)) {
-			if (item.children && Array.isArray(item.children) && item.children.length > 0) { 
+			if (item.children && Array.isArray(item.children) && item.children.length > 0) {
 				item.children = formatTable(item.children, keyword);
 			}
 			emptyArr.push(item);
@@ -125,24 +259,38 @@ const formatTable = (list: any[], keyword: string) => {
 	});
 	return emptyArr;
 };
+let emptyArr: any[] = [];
+const getExpand = (list: any[]) => {
+	if (!list.length || !Array.isArray(list)) return [];
+	list.map((item) => {
+		if (item.children && Array.isArray(item.children) && item.children.length > 0) {
+			getExpand(item.children);
+		}
+		emptyArr.push(item.id);
+	});
+	return emptyArr;
+};
 /** 搜索按钮操作 节流操作 */
 const handleQuery = throttle(() => {
 	if (state.queryParams.keyword) {
 		state.loading = true;
+		state.expandedRowKeys = [];
+		emptyArr = [];
 		state.menuTableData = formatTable(JSON.parse(JSON.stringify(state.staticArr)), state.queryParams.keyword);
-		state.isExpand = true;
-		toggleRowExpansionAll(state.menuTableData, state.isExpand);
+		state.expandedRowKeys = getExpand(state.menuTableData);
 		state.loading = false;
 	} else {
 		getMenuListApi();
 	}
-}, 300);
+}, 1000);
 /** 重置按钮操作 */
-const resetQuery = throttle((formEl: FormInstance | undefined) => {
+const resetQuery = (formEl: FormInstance | undefined) => {
 	if (!formEl) return;
 	formEl.resetFields();
 	getMenuListApi();
-}, 300);
+	state.expandedRowKeys = [];
+	emptyArr = [];
+};
 // 打开新增菜单弹窗
 const onOpenAddMenu = () => {
 	addMenuRef.value.openDialog();
@@ -153,13 +301,20 @@ const onOpenEditMenu = (row: RouteRecordRaw) => {
 };
 const expand = () => {
 	state.isExpand = !state.isExpand;
-	toggleRowExpansionAll(state.menuTableData, state.isExpand);
 };
-const toggleRowExpansionAll = (data: any, isExpand: boolean) => {
-	data.forEach((item: any) => {
-		dataTreeList.value.toggleRowExpansion(item, isExpand);
-		if (item.children !== undefined && item.children !== null) {
-			toggleRowExpansionAll(item.children, isExpand);
+watch(
+	() => state.isExpand,
+	(old: Boolean) => {
+		if (old) getAllIds(state.menuTableData);
+		else state.expandedRowKeys = [];
+	}
+);
+const getAllIds = (arr: any) => {
+	if (!arr) return [];
+	arr.forEach((v: any) => {
+		if (v.children?.length) {
+			getAllIds(v.children);
+			state.expandedRowKeys.push(v.id);
 		}
 	});
 };

+ 114 - 230
src/views/system/roles/component/permission.vue

@@ -1,35 +1,48 @@
 <template>
 	<div class="system-edit-role-container">
-		<el-dialog title="权限配置" v-model="state.isShowDialog" draggable @opened="opened">
-			<div class="custom-tree-node-container">
-				<el-table :data="state.menuTableData" stripe row-key="permissionCode" border :expand-row-keys="state.expandRowKeys">
-					<el-table-column width="180" label="菜单">
-						<template #default="scope">
-							<el-checkbox
-								v-if="scope.row.children"
-								:indeterminate="scope.row.indeterminate"
-								v-model="scope.row.checked"
-								@change="changeRowSelect(scope.row)"
-								>{{ scope.row.pageName }}</el-checkbox
-							>
-							<el-checkbox v-else v-model="scope.row.checked" @change="changeRowSelect(scope.row)">{{ scope.row.pageName }}</el-checkbox>
-						</template>
-					</el-table-column>
-					<el-table-column prop="children" label="按钮">
-						<template #default="scope">
-							<el-checkbox-group v-model="state.systemButtonArr" @change="handleCheckedBtnChange">
-								<el-checkbox v-for="item in scope.row.buttonArr" :key="item.id" :label="item.permissionCode"
-									><span v-if="showSetting">{{ item.permissionCode }}-{{ item.btnName }}</span>
-									<span v-else>{{ item.btnName }}</span>
-								</el-checkbox>
-							</el-checkbox-group>
-						</template>
-					</el-table-column>
-				</el-table>
+		<el-dialog :title="`【${state.rowName}】权限配置`" v-model="state.isShowDialog" draggable @opened="opened">
+			<div class="custom-tree-node-container" v-loading="state.loading">
+				<el-row :gutter="10">
+					<el-col :xs="24" :sm="8" :md="8" :lg="8" :xl="8">
+						展开/折叠:<el-switch
+							v-model="state.menuExpand"
+							@change="handleCheckedTreeExpand"
+							inline-prompt
+							active-text="折叠"
+							inactive-text="展开"
+						></el-switch>
+					</el-col>
+					<el-col :xs="24" :sm="8" :md="8" :lg="8" :xl="8">
+						全选/全不选:<el-switch
+							v-model="state.menuNodeAll"
+							@change="handleCheckedTreeNodeAll"
+							inline-prompt
+							active-text="全选"
+							inactive-text="全不选"
+						></el-switch>
+					</el-col>
+					<el-col :xs="24" :sm="8" :md="8" :lg="8" :xl="8">
+						父子联动:<el-switch
+							v-model="state.menuCheckStrictly"
+							@change="handleCheckedTreeConnect"
+							inline-prompt
+							active-text="联动"
+							inactive-text="不联动"
+						></el-switch>
+					</el-col>
+				</el-row>
+				<el-tree
+					class="tree-border mt20"
+					:data="state.menuTableData"
+					show-checkbox
+					ref="menuRef"
+					node-key="permissionCode"
+					:check-strictly="!state.menuCheckStrictly"
+					:props="{ label: 'pageName', children: 'children', class: customNodeClass }"
+				></el-tree>
 			</div>
 			<template #footer>
 				<span class="dialog-footer">
-					<el-button class="default-button"><el-checkbox v-model="state.checkedAll" @change="changeAllSelect">全选</el-checkbox></el-button>
 					<el-button @click="onCancel" class="default-button">取 消</el-button>
 					<el-button type="primary" @click="onSubmit">确 定</el-button>
 				</span>
@@ -39,7 +52,7 @@
 </template>
 
 <script setup lang="ts" name="systemEditRole">
-import { reactive, onMounted, computed } from 'vue';
+import { reactive, onMounted, ref } from 'vue';
 import { ElMessage } from 'element-plus';
 import { auth } from '/@/utils/authFunction';
 import { getMenuList } from '/@/api/system/menu';
@@ -47,66 +60,70 @@ import { getRolePower, setRolePower } from '/@/api/system/roles';
 
 // 定义子组件向父组件传值/事件
 const emit = defineEmits(['updateList']);
+interface Tree {
+	id: number;
+	label: string;
+	isPenultimate?: boolean;
+	menuType?: number;
+	children?: Tree[];
+}
 
 // 定义变量内容
 const state = reactive<any>({
 	isShowDialog: false,
-	menuTableData: <any>[],
-	systemMenuArr: <any>[],
-	systemButtonArr: <any>[],
+	menuTableData: <any>[], //所有菜单按钮
+	systemMenuArr: <any>[], // 选中的菜单
 	currentRow: {},
-	checkedAll: false,
 	expandRowKeys: [],
+	rowName: '',
+	menuExpand: false,
+	menuNodeAll: false,
+	menuCheckStrictly: true,
+	loading: false,
 });
+const menuRef = ref();
+const customNodeClass = (data: Tree) => {
+	if (data.menuType === 2) {
+		return 'is-penultimate';
+	}
+	return null;
+};
 // 打开弹窗
 const openDialog = (row: any) => {
 	state.currentRow = row;
 	state.systemMenuArr = <any>[];
-	state.systemButtonArr = <any>[];
-	const loop = (data: any) => {
-		data.forEach((item: any) => {
-			item.checked = false;
-			if (item.children && item.children.length) {
-				loop(item.children);
-			}
-		});
-	};
-	loop(state.menuTableData);
+	state.menuExpand = false;
+	state.menuNodeAll = false;
+	state.rowName = row.displayName;
+	state.loading = true;
+	state.isShowDialog = true;
+};
+// 树形扁平化
+const treeFlat = (source: any[]) => {
+	let res: any = [];
+	source.forEach((el) => {
+		res.push(el);
+		el.children && res.push(...treeFlat(el.children));
+	});
+	return res;
+};
+
+const opened = () => {
 	if (!auth('100205')) ElMessage.error('抱歉,您没有获取角色权限的权限!');
 	else {
-		getRolePower({ roleid: row.id }).then((res: any) => {
-			state.systemMenuArr = res.result?.systemMenuArr ?? [];
-			state.systemButtonArr = res.result?.systemButtonArr ?? [];
-			const loop = (data: any) => {
-				data.forEach((item: any) => {
-					for (let i of state.systemMenuArr) {
-						if (item.permissionCode === i) {
-							item.checked = true;
-							if (item.children && item.children.length) {
-								loop(item.children);
-							}
-						}
-					}
-				});
-			};
-			loop(state.menuTableData);
-			state.expandRowKeys = state.systemMenuArr;
-			state.isShowDialog = true;
-		});
+		getRolePower({ roleid: state.currentRow.id })
+			.then((res: any) => {
+				const arr: any[] = res.result?.systemMenuArr ?? [];
+				menuRef.value.setCheckedKeys(arr);
+				const arr1 = treeFlat(state.menuTableData);
+				if (arr.length === arr1.length) state.menuNodeAll = true;
+				state.loading = false;
+			})
+			.catch(() => {
+				state.loading = false;
+			});
 	}
 };
-// 检查是否全选
-const opened = () => {
-	const loopMenu = (data: any) => {
-		return data.every((item: any) => {
-			if (item.children && item.children.length) {
-				loopMenu(item.children);
-			}
-			return item.checked;
-		});
-	};
-	state.checkedAll = loopMenu(state.menuTableData);
-};
 // 关闭弹窗
 const closeDialog = () => {
 	state.isShowDialog = false;
@@ -121,172 +138,26 @@ const getMenuListApi = () => {
 		state.menuTableData = res?.result ?? [];
 	});
 };
-// 开发环境不显示设置
-const showSetting = computed(() => {
-	return import.meta.env.VITE_MODE_NAME === 'development';
-});
-
-// 选择表格(全选)
-const changeAllSelect = (val: any) => {
-	const loop = (data: any) => {
-		data.forEach((item: any) => {
-			item.checked = val;
-			if ('indeterminate' in item) {
-				item.indeterminate = false;
-			}
-			if (item.buttonArr && item.buttonArr.length) {
-				item.buttonArr.forEach((item: any) => {
-					if (val) {
-						state.systemButtonArr.push(item.permissionCode);
-					} else {
-						for (let i in state.systemButtonArr) {
-							if (state.systemButtonArr[i] === item.permissionCode) {
-								state.systemButtonArr.splice(i, 1);
-							}
-						}
-					}
-				});
-			}
-			state.systemButtonArr = Array.from(new Set(state.systemButtonArr));
-			if (item.children && item.children.length) {
-				loop(item.children);
-			}
-		});
-	};
-	loop(state.menuTableData);
-};
-const find = (v: any, list: any[]) => {
-	let data;
-	(list || []).map((i) => {
-		if (i.id === v) {
-			data = i;
-		} else {
-			const children = find(v, i.children);
-			if (children) {
-				data = children;
-			}
-		}
-	});
-	return data;
-};
-// 选择表格(表格行选择)
-const changeRowSelect = (val: any) => {
-	const loopBtn = (data: any, checked = val.checked) => {
-		data.forEach((item: any) => {
-			if (checked) {
-				state.systemButtonArr.push(item.permissionCode);
-			} else {
-				for (let i in state.systemButtonArr) {
-					if (state.systemButtonArr[i] === item.permissionCode) {
-						state.systemButtonArr.splice(i, 1);
-					}
-				}
-			}
-			if (item.buttonArr && item.buttonArr.length) {
-				loopBtn(item.buttonArr);
-			}
-		});
-	};
-	const loop = (data: any) => {
-		data.forEach((item: any) => {
-			item.checked = val.checked;
-			if (item.children && item.children.length) {
-				loop(item.children);
-			}
-			if (item.buttonArr && item.buttonArr.length) {
-				loopBtn(item.buttonArr);
-			}
-		});
-	};
-	state.systemButtonArr = Array.from(new Set(state.systemButtonArr));
-	if (val.buttonArr && val.buttonArr.length) {
-		loopBtn(val.buttonArr);
-	}
-	if (val.children && val.children.length) {
-		loop(val.children);
-		if ('indeterminate' in val) {
-			val.indeterminate = false;
-		}
-	} else {
-		let checkedLeg = 0;
-		const loop = (data: any) => {
-			data.forEach((item: any) => {
-				if (item.id === val.parentId) {
-					// 获取当前父级下子级选中条数
-					const leg = item.children.length;
-					checkedLeg = item.children.filter((ss: any) => ss.checked).length;
-					// 根据条数改变父级的indeterminate和checked
-					if (checkedLeg === 0) {
-						item.indeterminate = false;
-						item.checked = false;
-					} else if (checkedLeg < leg) {
-						item.indeterminate = true;
-						item.checked = false;
-					} else if (checkedLeg === leg) {
-						item.indeterminate = false;
-						item.checked = true;
-					}
-
-					const currentI: any = find(item.parentId, state.menuTableData);
-					if (currentI) {
-						let checkedLeg1 = 0;
-						const leg1 = currentI.children.length;
-						checkedLeg1 = item.children.filter((ss: any) => ss.checked).length;
-						// 根据条数改变父级的indeterminate和checked
-						if (checkedLeg1 === 0) {
-							currentI.indeterminate = false;
-						} else if (checkedLeg < leg1) {
-							currentI.indeterminate = true;
-						} else if (checkedLeg === leg1) {
-							currentI.indeterminate = false;
-						}
-					}
-					return;
-				}
-				if (item.children && item.children.length) {
-					loop(item.children);
-				}
-			});
-		};
-		loop(state.menuTableData);
+/** 树权限(展开/折叠)*/
+const handleCheckedTreeExpand = (value: boolean) => {
+	for (let i = 0; i < state.menuTableData.length; i++) {
+		menuRef.value.store.nodesMap[state.menuTableData[i].permissionCode].expanded = value;
 	}
-	// 判断是否全部选择了,改变全选框的样式
-	let flag = true;
-	state.menuTableData.some((item: any) => {
-		if (!item.checked) {
-			flag = false;
-			return;
-		}
-	});
-	state.checkedAll = flag;
 };
-// 选择按钮
-const handleCheckedBtnChange = (value: string[]) => {
-	state.systemButtonArr = value;
+/** 树权限(全选/全不选) */
+const handleCheckedTreeNodeAll = (value: boolean) => {
+	menuRef.value.setCheckedNodes(value ? state.menuTableData : []);
 };
-// 点击提交选中的表格
-const handleSelectTable = () => {
-	let selectedCodes = <any>[];
-	const loop = (data: any) => {
-		data.forEach((item: any) => {
-			if (item.checked || item.indeterminate) {
-				selectedCodes.push(item.permissionCode);
-				if (item.children) {
-					loop(item.children);
-				}
-			}
-		});
-	};
-	loop(state.menuTableData);
-	return selectedCodes;
+/** 树权限(父子联动) */
+const handleCheckedTreeConnect = (value: boolean) => {
+	state.menuCheckStrictly = value ? true : false;
 };
 // 保存
 const onSubmit = () => {
 	let req = {
 		roleId: state.currentRow.id,
 		roleCode: state.currentRow.name,
-		systemMenuArr: handleSelectTable(),
-		systemButtonArr: state.systemButtonArr,
+		systemMenuArr: menuRef.value.getCheckedKeys(),
 	};
 	setRolePower(req)
 		.then(() => {
@@ -318,4 +189,17 @@ defineExpose({
 	box-sizing: border-box;
 	align-items: center;
 }
+.tree-border {
+	border: var(--el-border);
+	border-radius: var(--el-border-radius-base);
+	padding: 15px;
+}
+:deep(.el-tree-node.is-expanded.is-penultimate > .el-tree-node__children) {
+	display: flex;
+	flex-direction: row;
+	flex-wrap: wrap;
+}
+:deep(.is-penultimate > .el-tree-node__children > div) {
+	width: 25%;
+}
 </style>

+ 2 - 2
src/views/system/roles/index.vue

@@ -44,10 +44,10 @@
 				</el-table-column>
 				<el-table-column label="操作" width="260" fixed="right" align="center">
 					<template #default="{ row }">
-						<el-button link type="primary" @click="onOpenEditRole(row)" v-auth="'100203'" title="修改角色信息" v-if="!row.isDeleted">
+						<el-button link type="primary" @click="onOpenEditRole(row)"  title="修改角色信息" v-if="!row.isDeleted">
 							修改
 						</el-button>
-						<el-button link type="success" @click="onPermissions(row)" v-auth="'100204'" title="配置角色权限" v-if="!row.isDeleted">
+						<el-button link type="success" @click="onPermissions(row)" title="配置角色权限" v-if="!row.isDeleted">
 							配置权限
 						</el-button>
 						<el-button link type="info" @click="onDataAuth(row)" v-auth="'100204'" title="配置角色数据权限" v-if="!row.isDeleted">