Ver Fonte

修复菜单赋权

ith5 há 3 meses atrás
pai
commit
33662ec578

+ 51 - 40
src/components/sa-treeSlider/index.vue

@@ -1,15 +1,23 @@
 <template>
-  <div class="flex flex-col w-full" :class="props.border ? 'slider-border p-2' : ''">
+  <div
+    class="flex flex-col w-full"
+    :class="props.border ? 'slider-border p-2' : ''"
+  >
     <a-input-group class="mb-2 w-full" size="mini">
-      <a-input :placeholder="props?.searchPlaceholder" allow-clear @input="changeKeyword" @clear="resetData" />
+      <a-input
+        :placeholder="props?.searchPlaceholder"
+        allow-clear
+        @input="changeKeyword"
+        @clear="resetData"
+      />
       <a-button
         @click="
           () => {
-            isExpand ? saTree.expandAll(false) : saTree.expandAll(true)
-            isExpand = !isExpand
+            isExpand ? saTree.expandAll(false) : saTree.expandAll(true);
+            isExpand = !isExpand;
           }
         "
-        >{{ isExpand ? '折叠' : '展开' }}</a-button
+        >{{ isExpand ? "折叠" : "展开" }}</a-button
       >
       <slot name="treeAfterButtons"></slot>
     </a-input-group>
@@ -21,8 +29,11 @@
       @select="handlerSelect"
       :field-names="props.fieldNames"
       v-model:selected-keys="modelValue"
-      v-bind="$attrs">
-      <template #icon v-if="props.icon"><component :is="props.icon" /></template>
+      v-bind="$attrs"
+    >
+      <template #icon v-if="props.icon"
+        ><component :is="props.icon"
+      /></template>
       <template v-for="(_, name) in $slots" v-slot:[name]="data">
         <slot :name="name" v-bind="data"></slot>
       </template>
@@ -31,13 +42,13 @@
 </template>
 
 <script setup>
-import { ref, watch, computed, onMounted } from 'vue'
+import { ref, watch, computed, onMounted } from "vue";
 
-const treeData = ref([])
-const saTree = ref()
-const isExpand = ref(false)
+const treeData = ref([]);
+const saTree = ref();
+const isExpand = ref(false);
 
-const emit = defineEmits(['update:modelValue', 'click'])
+const emit = defineEmits(["update:modelValue", "click"]);
 
 const props = defineProps({
   modelValue: { type: Array },
@@ -47,63 +58,63 @@ const props = defineProps({
   fieldNames: {
     type: Object,
     default: () => {
-      return { title: 'label', key: 'value' }
+      return { title: "label", key: "value" };
     },
   },
   icon: { type: String, default: undefined },
-})
+});
 
 const modelValue = computed({
   get() {
-    return props.modelValue
+    return props.modelValue;
   },
   set(newVal) {
-    emit('update:modelValue', newVal)
+    emit("update:modelValue", newVal);
   },
-})
+});
 
 watch(
   () => props.data,
   (val) => {
-    treeData.value = val
+    treeData.value = val;
   },
   { immediate: true, deep: true }
-)
+);
 
 const handlerSelect = (item, data) => {
-  modelValue.value = [item]
-  emit('click', ...[item, data])
-}
+  modelValue.value = [item];
+  emit("click", ...[item, data]);
+};
 
-const resetData = () => (treeData.value = props.data)
+const resetData = () => (treeData.value = props.data);
 
 const changeKeyword = (keyword) => {
-  if (!keyword || keyword === '') {
-    treeData.value = Object.assign(props.data, [])
-    return false
+  if (!keyword || keyword === "") {
+    treeData.value = Object.assign(props.data, []);
+    return false;
   }
-  treeData.value = searchNode(keyword)
-}
+  treeData.value = searchNode(keyword);
+};
 
 const searchNode = (keyword) => {
   const loop = (data) => {
-    let tree = []
+    let tree = [];
     data.map((item) => {
-      if (item[props.fieldNames['title']].indexOf(keyword) !== -1) {
-        tree.push(item)
+      if (item[props.fieldNames["title"]].indexOf(keyword) !== -1) {
+        tree.push(item);
       } else if (item.children && item.children.length > 0) {
-        const temp = loop(item.children)
-        tree.push(...temp)
+        const temp = loop(item.children);
+        tree.push(...temp);
       }
-      return tree
-    })
+      return tree;
+    });
 
-    return tree
-  }
-  return loop(treeData.value)
-}
+    return tree;
+  };
+  return loop(treeData.value);
+};
 
-defineExpose({ saTree })
+defineExpose({ saTree });
 </script>
 
 <style scoped lang="less">

+ 78 - 2
src/views/system/role/components/menuPermission.vue

@@ -51,6 +51,71 @@ const form = ref({ name: undefined, code: undefined });
 
 const emit = defineEmits(["success"]);
 
+/**
+ * 过滤掉自动勾选的父节点,只保留用户真正勾选的节点
+ * @param menus 后端返回的菜单 ID 列表
+ * @param treeData 树形菜单数据
+ * @returns 过滤后的菜单 ID 列表
+ */
+const filterAutoCheckedParents = (menus, treeData) => {
+  const menusSet = new Set(menus);
+  const result = new Set();
+
+  const traverse = (node) => {
+    if (!node.children || node.children.length === 0) {
+      // 是叶子节点,检查是否勾选
+      if (menusSet.has(node.id)) result.add(node.id);
+      return menusSet.has(node.id);
+    }
+
+    let childSelectedCount = 0;
+    node.children.forEach((child) => {
+      if (traverse(child)) childSelectedCount++;
+    });
+
+    // 只有在所有子节点都选中时,才加上父节点
+    const allChildrenSelected = childSelectedCount === node.children.length;
+    if (allChildrenSelected && menusSet.has(node.id)) {
+      result.add(node.id);
+    }
+
+    return allChildrenSelected && menusSet.has(node.id);
+  };
+
+  treeData.forEach(traverse);
+  return Array.from(result);
+};
+
+/**
+ * 自动补全父节点,用于提交后端
+ * @param treeData 树形菜单数据
+ * @param selectedKeys 用户选中的菜单 ID 列表
+ * @returns 包含所有父节点的菜单 ID 列表
+ */
+const getAllParentKeys = (treeData, selectedKeys) => {
+  const parentMap = new Map();
+
+  const buildMap = (nodes, parent = null) => {
+    nodes.forEach((node) => {
+      parentMap.set(node.id, parent);
+      if (node.children) buildMap(node.children, node.id);
+    });
+  };
+
+  buildMap(treeData);
+
+  const result = new Set(selectedKeys);
+  selectedKeys.forEach((key) => {
+    let parent = parentMap.get(key);
+    while (parent) {
+      result.add(parent);
+      parent = parentMap.get(parent);
+    }
+  });
+
+  return Array.from(result);
+};
+
 // 打开弹窗
 const open = async (row) => {
   visible.value = true;
@@ -89,7 +154,14 @@ const setData = async (roleId) => {
   const menuResponse = await menu.accessMenu({ tree: true });
   menuList.value = menuResponse.data;
   const roleResponse = await role.getMenuByRole(roleId);
-  selectKeys.value = roleResponse.data.menus.map((item) => item.id);
+
+  // 获取后端返回的菜单 IDs
+  const menuIds = roleResponse.data.menus.map((item) => item.id);
+
+  // 过滤掉自动勾选的父节点,只保留用户真正勾选的节点
+  const filteredKeys = filterAutoCheckedParents(menuIds, menuList.value);
+  selectKeys.value = filteredKeys;
+
   selectKeys.value.length > 0 && handlerLinkage(true);
   loading.value = false;
 };
@@ -98,8 +170,12 @@ const setData = async (roleId) => {
 const submit = async (done) => {
   const nodes = tree.value.saTree.getCheckedNodes();
   const ids = nodes.map((item) => item.id);
+
+  // 自动补全父节点,确保后端接收完整的菜单权限数据
+  const parentKeys = getAllParentKeys(menuList.value, ids);
+
   const response = await role.updateMenuPermission(form.value.id, {
-    menu_ids: ids,
+    menu_ids: parentKeys,
   });
   response.code === 200 && Message.success(response.message);
   emit("success");