|
|
@@ -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>
|