el-table的树形表格全选 半选 取消全选问题解决
页面加载时序created → getList → 请求接口 → 拷贝树 初始化数据 → 构建父子映射 → nextTick 回显 echoCheckedState自底向上手动点单行时序selectFun → setRowIsSelect → 同步所有子节点 → 逐层向上更新所有父节点半选状态 → 更新行 / 表头半选样式表头全选时序selectAllFun → 判断是否全选 → 遍历所有顶层节点 → 批量触发单行联动同步整树保存时序收集所有选中 / 半选 ID → 弹窗确认 → 提交后端上代码 template div classrolemeu-container p-5 div classrolemenu-box !-- 权限表格 -- el-table v-loadingloading refproduct :datarenderDynamic border row-keyid heightcalc(100vh - 205px) sizemini max-height500px stylewidth: 100% :row-class-namerowClassNameFun :header-row-class-nameheaderRowClassName :header-cell-style{ background: #fafafa } selectselectFun select-allselectAllFun !-- 多选框列 -- el-table-column typeselection width55/el-table-column !-- 序号 -- el-table-column typeindex label序号 width50 aligncenter / el-table-column propname label菜单名称 aligncenter / el-table-column proptype label角色类型 aligncenter width200 / el-table-column proppermission label权限类型 min-width100 aligncenter / /el-table div stylemargin: 10px 0 el-button typeprimary clicksaveCheckedIds保存权限/el-button /div /div /div /template script export default { data() { return { loading: false, // 表格加载loading menuList: [], // 后端返回完整权限树形原始数据 roleId: this.$route.query.roleId || , // 当前编辑角色ID checkedIds: [], // 角色已有权限ID集合回显用 renderDynamic: [], // 页面渲染用树形副本避免污染原始数据 parentMap: {}, // 节点ID - 父节点 对象映射快速向上递归父级 oneProductIsSelect: [], // 存储顶级节点选中状态用于判断表头全选框 isInitializingSelection: false, // 初始化回显开关初始化时禁止触发联动逻辑 }; }, methods: { /** * 页面初始化获取权限树 当前角色已分配权限 */ getList() { if (!this.roleId) { this.$message.warning(未获取到角色信息); return; } this.loading true; // 并行请求全量权限树、当前角色已勾选权限 Promise.all([ premissionApi.getPermissionTreeList(), premissionApi.getRoleMenuList(this.roleId), ]) .then(([menuRes, roleRes]) { this.menuList Array.isArray(menuRes.data) ? menuRes.data : []; // 提取当前角色所有权限ID用于回显勾选 this.checkedIds this.getAllIds(roleRes.data || []); // 深拷贝树形数据操作副本不污染原始树 this.renderDynamic this.cloneTree(this.menuList); // 给每个节点初始化isSelect选中标记 this.initData(this.renderDynamic); this.parentMap {}; // 递归构建父子节点映射表方便向上查找父级 this.registerParentMap(this.renderDynamic); this.$nextTick(() { // 开启初始化标识阻止回显时触发勾选联动逻辑 this.isInitializingSelection true; // 根据checkedIds回显所有节点勾选状态自动计算半选父节点 this.echoCheckedState(this.renderDynamic); // 关闭初始化标识用户手动勾选才会走联动 this.isInitializingSelection false; }); }) .catch(() { this.$message.error(权限数据加载失败); }) .finally(() { this.loading false; }); }, /** * 深拷贝树形结构防止修改页面渲染数据污染原始接口数据 * param {Array} list 树形数组 * returns {Array} 全新拷贝树 */ cloneTree(list) { return (Array.isArray(list) ? list : []).map((item) ({ ...item, children: this.cloneTree(item.children || []), })); }, /** * 递归构建父子映射 parentMap * key: 节点id value: 父节点完整对象 * param {Array} list 当前层级节点数组 * param {Object} parent 当前层级的父节点 */ registerParentMap(list, parent) { (Array.isArray(list) ? list : []).forEach((item) { this.parentMap[item.id] parent || null; if (item.children item.children.length) { this.registerParentMap(item.children, item); } }); }, /** * 根据节点ID获取父节点对象 * param {Object} row 当前行节点 * returns {Object|null} 父节点 */ getParentNode(row) { if (!row || row.id undefined || row.id null) { return null; } return this.parentMap[row.id] || null; }, /** * 递归提取一棵树所有节点ID回显角色权限用 * param {Array} tree 角色已有权限树 * returns {Array} 所有id集合 */ getAllIds(tree) { const ids []; const loop (nodes) { if (!Array.isArray(nodes)) return; nodes.forEach((item) { ids.push(item.id); loop(item.children); }); }; loop(tree); return ids; }, /** * 初始化树形所有节点选中状态默认全部未选中 false * param {Array} data 树形数组 */ initData(data) { data.forEach((item) { item.isSelect false; if (item.children item.children.length) { this.initData(item.children); } }); }, /** * 权限回显核心根据checkedIds设置节点勾选自动计算父级半选/全选 * 递归顺序先子后父自底向上计算父节点状态 * param {Array} data 树形数组 */ echoCheckedState(data) { data.forEach((item) { const children item.children || []; // 1. 先递归处理所有子节点 if (children.length) { this.echoCheckedState(children); // 判断子节点状态 const allChildrenChecked children.every((child) child.isSelect true); // 全部子节点选中 const hasCheckedChild children.some((child) child.isSelect true || child.isSelect ); // 存在任意选中/半选子节点 // 父节点状态判定 if (allChildrenChecked) { item.isSelect true; // 子全选 → 父全选 } else if (hasCheckedChild) { item.isSelect ; // 部分子选中 → 父半选 } else { item.isSelect this.checkedIds.includes(item.id); // 子全不选看自身是否被角色勾选 } } else { // 叶子节点直接根据角色权限ID判断是否选中 item.isSelect this.checkedIds.includes(item.id); } // 同步table多选框UI状态 this.syncRowSelection(item); }); }, /** * 同步el-table多选框勾选UI * param {Object} row 节点行数据 */ syncRowSelection(row) { if (!this.$refs.product) return; // 只有isSelecttrue才显示勾选半选不打勾靠css样式区分 this.$refs.product.toggleRowSelection(row, row.isSelect true); }, /** * 单行勾选/取消触发事件 select * param {Array} selection 当前已勾选行集合 * param {Object} row 当前操作行 */ selectFun(selection, row) { // 初始化回显阶段不执行联动逻辑防止递归死循环 if (this.isInitializingSelection) return; // 执行勾选联动子全选、父级向上更新半选状态 this.setRowIsSelect(row); }, /** * 核心联动逻辑勾选单行后同步所有子节点 向上更新所有父节点状态 * param {Object} row 当前操作行 */ setRowIsSelect(row) { // 如果当前是半选状态点击直接转为选中 if (row.isSelect ) { row.isSelect false; this.$refs.product.toggleRowSelection(row, true); } // 取反当前节点选中状态 true ↔ false row.isSelect !row.isSelect; const that this; /** * 递归全选/取消当前节点所有后代子节点 * param {Array} data 子节点数组 */ function selectAllChildrens(data) { data.forEach((item) { item.isSelect row.isSelect; that.$refs.product.toggleRowSelection(item, row.isSelect); if (item.children item.children.length) { selectAllChildrens(item.children); } }); } /** * 递归收集当前节点下所有后代叶子选中状态 * param {Array} selectStatusArr 存储状态数组 * param {Array} data 子节点数组 * returns {Array} 所有后代isSelect状态集合 */ function getSelectStatus(selectStatusArr, data) { data.forEach((childrenItem) { selectStatusArr.push(childrenItem.isSelect); if (childrenItem.children childrenItem.children.length) { getSelectStatus(selectStatusArr, childrenItem.children); } }); return selectStatusArr; } /** * 判断当前节点层级类型返回层级标识 * 1顶级父节点有子节点 * 2中间父节点有子节点 * 3叶子节点无子节点 * 4无父级且无子节点孤立节点 * param {Object} currentRow 当前操作行 * returns {Number} 层级标记 */ function getLevelStatus(currentRow) { const parentNode that.getParentNode(currentRow); if (!parentNode) { if (currentRow.children currentRow.children.length) return 1; return 4; } if (!currentRow.children || !currentRow.children.length) return 3; return 2; } /** * 向上递归更新所有父节点选中状态半选/全选/未选 * 操作完子节点后逐层刷新父级isSelect * param {Object} currentRow 当前操作节点 */ function operateLastLeve(currentRow) { let selectStatusArr []; const item that.getParentNode(currentRow); if (!item || !item.children) return; // 获取当前父节点所有子节点的选中状态 selectStatusArr getSelectStatus(selectStatusArr, item.children); // 1. 所有子全选 → 父全选 if (selectStatusArr.length selectStatusArr.every((selectItem) selectItem true)) { item.isSelect true; that.$refs.product.toggleRowSelection(item, true); } // 2. 所有子全不选 → 父取消 else if (selectStatusArr.length selectStatusArr.every((selectItem) selectItem false)) { item.isSelect false; that.$refs.product.toggleRowSelection(item, false); } // 3. 部分选中 → 父半选 else { item.isSelect ; that.$refs.product.toggleRowSelection(item, false); } // 存在更上层父节点继续向上递归更新 if (that.getParentNode(item)) { operateLastLeve(item); } } // 获取当前节点层级类型执行对应联动逻辑 const levelStatus getLevelStatus(row); if (levelStatus 1) { // 顶级父节点勾选后全选所有子节点 selectAllChildrens(row.children); } else if (levelStatus 2) { // 中间父节点全选子节点 向上更新所有父级状态 selectAllChildrens(row.children); operateLastLeve(row); } else if (levelStatus 3) { // 叶子节点仅向上更新所有父级半选/选中状态 operateLastLeve(row); } }, /** * 判断所有顶级节点是否全部选中用于表头全选框状态计算 * returns {Boolean} true全部顶级选中 false存在未选/半选 */ checkIsAllSelect() { this.oneProductIsSelect []; this.renderDynamic.forEach((item) { this.oneProductIsSelect.push(item.isSelect); }); return this.oneProductIsSelect.every((selectStatusItem) selectStatusItem true); }, /** * 表头全选/取消全选事件 select-all * param {Array} selection 已勾选行集合 */ selectAllFun(selection) { // 初始化回显阶段不执行 if (this.isInitializingSelection) return; // 判断当前是否已经全部选中 const isAllSelect this.checkIsAllSelect(); // 遍历所有顶级节点取反全选状态 this.renderDynamic.forEach((item) { item.isSelect isAllSelect; this.$refs.product.toggleRowSelection(item, !isAllSelect); // 递归触发子节点联动 this.selectFun(selection, item); }); }, /** * 行class半选行添加indeterminate样式渲染中间横线 * param {Object} {row} 行对象 * returns {String} class名 */ rowClassNameFun({ row }) { if (row.isSelect ) return indeterminate; return ; }, /** * 表头多选框class存在任意半选节点表头变为半选样式 * returns {String} class名 */ headerRowClassName() { const topLevelSelectStatus []; this.renderDynamic.forEach((item) { topLevelSelectStatus.push(item.isSelect); }); // 顶级节点包含半选表头显示半选样式 if (topLevelSelectStatus.includes()) return indeterminate; return ; }, /** * 递归收集所有有效权限ID选中true 半选都要提交 * returns {Array} 待提交权限ID数组 */ getCheckedIds() { const ids []; const loop (list) { list.forEach((item) { // 半选父节点也要提交ID后端需要完整权限树 if (item.isSelect true || item.isSelect ) { ids.push(item.id); } if (item.children item.children.length) { loop(item.children); } }); }; loop(this.renderDynamic); return ids; }, /** * 保存权限按钮点击事件 */ saveCheckedIds() { const ids this.getCheckedIds(); if (!ids.length) { this.$message.warning(请至少勾选一个权限); return; } this.$confirm(确认保存当前权限配置吗, 提示, { confirmButtonText: 确定, cancelButtonText: 取消, type: warning, lockScroll: false, }) .then(() { // 提交角色ID 所有选中/半选权限ID return premissionApi.grantRoleMenu({ roleId: this.roleId, permissionIds: ids, }); }) .then(() { // 更新本地缓存回显ID this.checkedIds ids; this.$message.success(保存成功); }) .catch((error) { if (error cancel || error close) { this.$message.info(已取消保存); return; } this.$message.error(保存失败); }); }, }, created() { // 页面创建加载权限数据 this.getList(); }, } /script style langscss scoped .rolemenu-box { padding: 10px; background: #fff; border-radius: 4px; height: calc(100vh - 110px); /* 半选样式核心给indeterminate类的复选框绘制中间横线 */ ::v-deep .indeterminate .el-checkbox__input .el-checkbox__inner { background-color: #409eff !important; border-color: #409eff !important; color: #fff !important; } ::v-deep .indeterminate .el-checkbox__input.is-checked .el-checkbox__inner::after { transform: scale(0.5); } ::v-deep .indeterminate .el-checkbox__input .el-checkbox__inner { background-color: #f2f6fc; border-color: #dcdfe6; } ::v-deep .indeterminate .el-checkbox__input .el-checkbox__inner::after { border-color: #c0c4cc !important; background-color: #c0c4cc; } /* 半选复选框中间横线 */ ::v-deep .indeterminate .el-checkbox__input .el-checkbox__inner::after { content: ; position: absolute; display: block; background-color: #fff; height: 2px; transform: scale(0.5); left: 0; right: 0; top: 5px; width: auto !important; } ::v-deep .product-show th .el-checkbox__inner { display: none !important; } ::v-deep .product-show .el-checkbox__inner { display: block !important; } ::v-deep .product-show .el-checkbox { display: block !important; } } /style