<template>
|
<div class="page">
|
<div class="page-title">
|
<div><h2>菜单管理</h2><p>支持目录、菜单、按钮三种类型;按钮只能挂在菜单下级,用于配置页面操作权限</p></div>
|
<el-button type="primary" icon="el-icon-plus" v-permission="'system:menus:add'" @click="openCreate(null)">新增一级菜单</el-button>
|
</div>
|
<div class="table-panel">
|
<el-table :data="tree" row-key="id" border default-expand-all :tree-props="{ children: 'children' }">
|
<el-table-column prop="name" label="菜单名称" min-width="220"></el-table-column>
|
<el-table-column prop="icon" label="图标" width="180">
|
<template slot-scope="{ row }">
|
<span v-if="row.menuType !== 2" class="icon-cell"><i :class="row.icon"></i><span>{{ row.icon }}</span></span>
|
<span v-else class="muted">-</span>
|
</template>
|
</el-table-column>
|
<el-table-column prop="url" label="路由地址" min-width="220">
|
<template slot-scope="{ row }">
|
<span v-if="row.menuType !== 2">{{ row.url }}</span>
|
<span v-else class="muted">-</span>
|
</template>
|
</el-table-column>
|
<el-table-column prop="permissions" label="权限标识" min-width="200"></el-table-column>
|
<el-table-column label="类型" width="90">
|
<template slot-scope="{ row }">
|
<el-tag size="mini" :type="typeTag(row.menuType)">{{ typeName(row.menuType) }}</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column prop="sortNo" label="排序" width="80" align="center"></el-table-column>
|
<el-table-column label="显示" width="80" align="center">
|
<template slot-scope="{ row }">
|
<el-tag size="mini" :type="row.visible === 0 ? 'info' : 'success'">{{ row.visible === 0 ? '隐藏' : '显示' }}</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column label="操作" width="270" fixed="right">
|
<template slot-scope="{ row }">
|
<el-button v-if="row.menuType !== 2" type="text" v-permission="'system:menus:add'" @click="openCreate(row)">新增下级</el-button>
|
<el-button v-if="row.menuType === 1" type="text" v-permission="'system:menus:buttons'" @click="openButtons(row)">页面按钮</el-button>
|
<el-button type="text" v-permission="'system:menus:edit'" @click="openEdit(row)">编辑</el-button>
|
<el-button type="text" class="danger-link" v-permission="'system:menus:delete'" @click="remove(row)">删除</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
</div>
|
|
<el-dialog :title="form.id ? '编辑菜单' : '新增菜单'" :visible.sync="dialogVisible" width="640px" append-to-body>
|
<el-form ref="form" :model="form" :rules="rules" label-width="105px">
|
<el-form-item label="上级菜单">
|
<el-cascader v-model="form.pid" :options="parentOptions" :props="parentProps" :show-all-levels="false" filterable clearable placeholder="请选择上级菜单" style="width:100%"></el-cascader>
|
</el-form-item>
|
<el-form-item label="菜单类型">
|
<el-radio-group v-model="form.menuType">
|
<el-radio-button v-for="item in typeOptions" :key="item.value" :label="item.value">{{ item.label }}</el-radio-button>
|
</el-radio-group>
|
</el-form-item>
|
<el-form-item label="菜单名称" prop="name"><el-input v-model="form.name"></el-input></el-form-item>
|
<el-form-item v-if="form.menuType !== 2" label="路由地址"><el-input v-model="form.url" placeholder="/fault/report"></el-input></el-form-item>
|
<el-form-item v-if="form.menuType !== 2" label="图标"><el-input v-model="form.icon" placeholder="el-icon-tickets"></el-input></el-form-item>
|
<el-form-item label="权限标识" :prop="form.menuType === 2 ? 'permissions' : ''">
|
<el-input v-model="form.permissions" :placeholder="form.menuType === 2 ? '例如 fault:report:add' : '可选,例如 fault-report'"></el-input>
|
</el-form-item>
|
<el-form-item label="排序"><el-input-number v-model="form.sortNo" :min="1" style="width:160px"></el-input-number></el-form-item>
|
<el-form-item label="是否显示"><el-switch v-model="form.visible" :active-value="1" :inactive-value="0"></el-switch></el-form-item>
|
</el-form>
|
<template slot="footer"><el-button @click="dialogVisible = false">取消</el-button><el-button type="primary" :loading="saving" @click="save">保存</el-button></template>
|
</el-dialog>
|
|
<el-dialog :title="'页面按钮配置 - ' + (buttonMenu.name || '')" :visible.sync="buttonDialogVisible" width="620px" append-to-body>
|
<p class="dialog-tip">下列按钮来自后端按钮目录,勾选后自动生成按钮类型菜单,可在角色管理的权限设置中授权给角色。</p>
|
<el-checkbox-group v-model="buttonSelected" class="button-list">
|
<el-checkbox v-for="item in buttonAvailable" :key="item.code" :label="item.code" class="button-item">
|
<span class="button-name">{{ item.name }}</span>
|
<span class="button-code">{{ item.code }}</span>
|
</el-checkbox>
|
</el-checkbox-group>
|
<div v-if="!buttonAvailable.length" class="empty-tip">该页面暂未登记可用按钮</div>
|
<template slot="footer">
|
<el-button @click="buttonDialogVisible = false">取消</el-button>
|
<el-button type="primary" :loading="buttonSaving" @click="saveButtons">保存按钮</el-button>
|
</template>
|
</el-dialog>
|
</div>
|
</template>
|
<script>
|
import api from '@/api'
|
export default {
|
name: 'MenuManage',
|
data() {
|
return {
|
tree: [],
|
flat: [],
|
dialogVisible: false,
|
saving: false,
|
form: {},
|
rules: { name: [{ required: true, message: '请输入菜单名称', trigger: 'blur' }] },
|
buttonDialogVisible: false,
|
buttonSaving: false,
|
buttonMenu: {},
|
buttonAvailable: [],
|
buttonSelected: []
|
}
|
},
|
computed: {
|
parentProps() {
|
return { value: 'id', label: 'name', children: 'children', checkStrictly: true, emitPath: false, expandTrigger: 'hover' }
|
},
|
selectedParent() {
|
const pid = String(this.form.pid || '0')
|
if (pid === '0') return null
|
return this.flat.find(item => String(item.id) === pid) || null
|
},
|
parentOptions() {
|
const excluded = this.excludedMenuIds()
|
const allowedType = this.form.menuType === 2 ? 1 : 0
|
const nodes = this.buildParentOptions(this.tree, allowedType, excluded)
|
return this.form.menuType === 2 ? nodes : [{ id: '0', name: '无(一级菜单)', children: nodes }]
|
},
|
typeOptions() {
|
if (this.selectedParent && this.selectedParent.menuType === 1) {
|
return [{ label: '按钮', value: 2 }]
|
}
|
if (this.selectedParent && this.selectedParent.menuType === 0) {
|
return [{ label: '目录', value: 0 }, { label: '菜单', value: 1 }]
|
}
|
if (this.form.id && this.form.menuType === 2) {
|
return [{ label: '按钮', value: 2 }]
|
}
|
return [{ label: '目录', value: 0 }, { label: '菜单', value: 1 }]
|
}
|
},
|
created() { this.load() },
|
methods: {
|
load() {
|
Promise.all([api.get('/system/menu/tree'), api.get('/system/menu/list')]).then(([tree, list]) => {
|
this.tree = tree.data || []
|
this.flat = list.data || []
|
})
|
},
|
typeName(type) {
|
return type === 2 ? '按钮' : type === 0 ? '目录' : '菜单'
|
},
|
typeTag(type) {
|
return type === 2 ? 'warning' : type === 0 ? 'info' : 'primary'
|
},
|
openCreate(parent) {
|
const menuType = parent && parent.menuType === 1 ? 2 : parent ? 1 : 0
|
this.form = {
|
pid: parent ? String(parent.id) : '0',
|
name: '',
|
url: '',
|
icon: menuType === 2 ? '' : 'el-icon-tickets',
|
menuType,
|
permissions: '',
|
sortNo: 10,
|
visible: 1
|
}
|
this.dialogVisible = true
|
},
|
openEdit(row) {
|
this.form = { ...row, pid: row.pid == null || row.pid === 0 ? '0' : String(row.pid) }
|
this.dialogVisible = true
|
},
|
save() {
|
this.$refs.form.validate(valid => {
|
if (!valid) return
|
if (this.form.menuType === 2 && !this.form.permissions) {
|
this.$message.error('按钮权限标识不能为空')
|
return
|
}
|
const payload = { ...this.form }
|
if (payload.pid === null || payload.pid === undefined || payload.pid === '') payload.pid = '0'
|
if (payload.menuType === 2) {
|
payload.url = null
|
payload.icon = null
|
}
|
delete payload.children
|
this.saving = true
|
const request = payload.id ? api.put('/system/menu/' + payload.id, payload) : api.post('/system/menu', payload)
|
request.then(() => {
|
this.$message.success('保存成功')
|
this.dialogVisible = false
|
this.load()
|
}).finally(() => { this.saving = false })
|
})
|
},
|
excludedMenuIds() {
|
const excluded = new Set()
|
const walk = (nodes, underSelf) => {
|
;(nodes || []).forEach(node => {
|
const next = underSelf || String(node.id) === String(this.form.id)
|
if (next) excluded.add(String(node.id))
|
walk(node.children, next)
|
})
|
}
|
walk(this.tree, false)
|
return excluded
|
},
|
buildParentOptions(nodes, allowedType, excluded) {
|
return (nodes || [])
|
.filter(node => node.menuType === allowedType && !excluded.has(String(node.id)))
|
.map(node => ({
|
id: String(node.id),
|
name: node.name,
|
children: this.buildParentOptions(node.children, allowedType, excluded)
|
}))
|
},
|
remove(row) {
|
this.$confirm(`确定删除“${row.name}”吗?`, '删除确认', { type: 'warning' })
|
.then(() => api.delete('/system/menu/' + row.id))
|
.then(() => { this.$message.success('删除成功'); this.load() })
|
.catch(() => {})
|
},
|
openButtons(row) {
|
this.buttonMenu = row
|
api.get('/system/menu/buttons', { params: { menuId: row.id } }).then(res => {
|
const data = res.data || {}
|
this.buttonAvailable = data.available || []
|
this.buttonSelected = data.selected || []
|
this.buttonDialogVisible = true
|
})
|
},
|
saveButtons() {
|
this.buttonSaving = true
|
api.post('/system/menu/buttons', { menuId: this.buttonMenu.id, codes: this.buttonSelected }).then(() => {
|
this.$message.success('页面按钮已同步')
|
this.buttonDialogVisible = false
|
this.load()
|
}).finally(() => { this.buttonSaving = false })
|
}
|
}
|
}
|
</script>
|
<style scoped>
|
.danger-link { color: #d9534f; }
|
.icon-cell { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
|
.muted { color: #a3afbc; }
|
.dialog-tip { margin: 0 0 12px; color: #738296; font-size: 13px; }
|
.button-list { display: flex; flex-direction: column; gap: 2px; }
|
.button-item { margin-right: 0; }
|
.button-name { color: #31465d; }
|
.button-code { margin-left: 8px; color: #93a2b1; font-size: 12px; }
|
.empty-tip { padding: 30px 0; color: #93a2b1; text-align: center; }
|
</style>
|