Parcourir la source

feat(web): 菜单编辑改用树形选择器作为上级菜单

- 替换 flat el-select 为 el-tree 扁平树(含层级缩进+图标)
- 新增 treeData computed:递归展平菜单树,带 padding 缩进
- 新增 currentLabel computed:选中后显示面包屑路径(如 系统配置 > 基础管理 > 用户管理)
- 新增 excludedIds computed:编辑模式下自动排除自身及所有子孙节点,防循环引用
- 新增 selectParentNode 方法:点击树节点设置 parentId
- 添加 parent-hint 提示条:显示当前选中上级路径或'顶级菜单'
Sisyphus il y a 2 mois
Parent
commit
5f7889940d
1 fichiers modifiés avec 106 ajouts et 6 suppressions
  1. 106 6
      cfc-web/src/views/admin/MenuManage.vue

+ 106 - 6
cfc-web/src/views/admin/MenuManage.vue

@@ -71,11 +71,29 @@
     <!-- 创建/编辑弹窗 -->
     <el-dialog :title="isEdit ? '编辑菜单' : '新增菜单'" :visible.sync="menuDialogVisible" width="550px">
       <el-form :model="menuForm" :rules="menuRules" ref="menuForm" label-width="100px">
-        <el-form-item label="上级菜单">
-          <el-select v-model="menuForm.parentId" placeholder="请选择上级菜单" style="width:100%">
-            <el-option :value="0" label="顶级菜单">顶级菜单</el-option>
-            <el-option v-for="item in flatMenuOptions" :key="item.id" :label="item.displayName" :value="item.id"></el-option>
-          </el-select>
+        <el-form-item label="上级菜单" prop="parentId">
+          <el-tree
+            ref="parentTree"
+            :data="treeData"
+            :props="{ label: 'label', disabled: 'disabled' }"
+            node-key="id"
+            :default-expanded-keys="[0]"
+            :highlight-current="true"
+            :expand-on-click-node="false"
+            @node-click="selectParentNode"
+            accordion
+          >
+            <span class="custom-tree-node" slot-scope="{ node, data }">
+              <i :class="data.icon" style="margin-right:4px;font-size:13px"></i>
+              <span>{{ node.label }}</span>
+            </span>
+          </el-tree>
+          <div v-if="currentLabel" class="parent-hint">
+            <i class="el-icon-location-outline"></i> 当前上级:{{ currentLabel }}
+          </div>
+          <div v-else class="parent-hint parent-hint-top">
+            <i class="el-icon-arrow-up"></i> 顶级菜单(无上级)
+          </div>
         </el-form-item>
         <el-form-item label="目录名称">
           <el-input v-model="menuForm.title" placeholder="有子菜单时填写"></el-input>
@@ -254,6 +272,7 @@ export default {
       const kw = this.searchKeyword.toLowerCase()
       return this.filterTree(this.menuTree, kw)
     },
+<<<<<<< HEAD
     flatMenuOptions() {
       const options = []
       const flatten = (nodes) => {
@@ -276,6 +295,61 @@ export default {
       }
       const kw = this.menuSearchKeyword.toLowerCase()
       return this.filterTree(this.menuTree, kw)
+    },
+    // 扁平树数据(用于上级菜单选择器)
+    treeData() {
+      const result = [{ id: 0, label: '顶级菜单', icon: 'el-icon-arrow-up', disabled: false }]
+      const flatten = (nodes, depth) => {
+        nodes.forEach(node => {
+          const padding = 'padding-left:' + (depth * 16) + 'px'
+          const disabled = this.isEdit && (node.id === this.menuForm.id || this.excludedIds.has(node.id))
+          result.push({ id: node.id, label: node.title || node.label, icon: node.icon || '', disabled, style: padding })
+          if (node.children) flatten(node.children, depth + 1)
+        })
+      }
+      flatten(this.menuTree, 0)
+      return result
+    },
+    // 当前选中上级的面包屑路径
+    currentLabel() {
+      if (!this.menuForm.parentId) return ''
+      const findPath = (nodes, targetId, path) => {
+        for (const n of nodes) {
+          const cur = (path ? path + ' / ' : '') + (n.title || n.label)
+          if (n.id === targetId) return cur
+          if (n.children) {
+            const r = findPath(n.children, targetId, cur)
+            if (r) return r
+          }
+        }
+        return null
+      }
+      return findPath(this.menuTree, this.menuForm.parentId, '')
+    },
+    // 被编辑项的子孙ID集合(防止循环引用)
+    excludedIds() {
+      if (!this.menuForm.id) return new Set()
+      const ids = new Set([this.menuForm.id])
+      const collect = (nodes) => {
+        nodes.forEach(n => {
+          if (n.children) {
+            n.children.forEach(c => { ids.add(c.id); collect([c]) })
+          }
+        })
+      }
+      const findNode = (nodes, targetId) => {
+        for (const n of nodes) {
+          if (n.id === targetId) return n
+          if (n.children) {
+            const f = findNode(n.children, targetId)
+            if (f) return f
+          }
+        }
+        return null
+      }
+      const node = findNode(this.menuTree, this.menuForm.id)
+      if (node && node.children) collect(node.children)
+      return ids
     }
   },
   created() {
@@ -294,6 +368,7 @@ export default {
         this.$message.error('加载菜单树失败')
       }
     },
+<<<<<<< HEAD
     // 递归添加 displayName
     addDisplayName(nodes) {
       return (nodes || []).map(n => {
@@ -322,6 +397,9 @@ export default {
       return result
     },
     // === 菜单 CRUD ===
+    selectParentNode(data) {
+      this.menuForm.parentId = data.id
+    },
     handleCreate(parentId) {
       this.isEdit = false
       this.menuForm = { ...this.getEmptyForm(), parentId }
@@ -653,4 +731,26 @@ export default {
 .flex { display: flex; }
 .items-center { align-items: center; }
 .gap-sm { gap: 8px; }
-</style>
+
+/* 上级菜单树提示 */
+.parent-hint {
+  margin-top: 8px;
+  font-size: 12px;
+  color: #909399;
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+.parent-hint-top {
+  color: #67c23a;
+}
+.custom-tree-node {
+  font-size: 13px;
+  display: flex;
+  align-items: center;
+  width: 100%;
+}
+.custom-tree-node .el-tree-node__content {
+  height: 32px;
+}
+</style>