| | |
| | | <template> |
| | | <div class="page"> |
| | | <div class="page-title"> |
| | | <div><h2>菜单管理</h2><p>菜单为动态配置,左侧导航直接读取这里的数据</p></div> |
| | | <el-button type="primary" icon="el-icon-plus" @click="openCreate(null)">新增一级菜单</el-button> |
| | | <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="200"><template slot-scope="{ row }"><span class="icon-cell"><i :class="row.icon"></i><span>{{ row.icon }}</span></span></template></el-table-column> |
| | | <el-table-column prop="url" label="路由地址" min-width="240"></el-table-column> |
| | | <el-table-column prop="permissions" label="权限标识" min-width="170"></el-table-column> |
| | | <el-table-column prop="menuType" label="类型" width="90"><template slot-scope="{ row }"><el-tag size="mini" :type="row.menuType === 0 ? 'info' : 'primary'">{{ row.menuType === 0 ? '目录' : '菜单' }}</el-tag></template></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 prop="visible" 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="200" fixed="right"><template slot-scope="{ row }"><el-button type="text" @click="openCreate(row)">新增下级</el-button><el-button type="text" @click="openEdit(row)">编辑</el-button><el-button type="text" class="danger-link" @click="remove(row)">删除</el-button></template></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-input :value="parentName" readonly></el-input></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 label="菜单类型"><el-radio-group v-model="form.menuType"><el-radio-button :label="0">目录</el-radio-button><el-radio-button :label="1">菜单</el-radio-button></el-radio-group></el-form-item> |
| | | <el-form-item label="路由地址"><el-input v-model="form.url" placeholder="/fault/report"></el-input></el-form-item> |
| | | <el-form-item label="图标"><el-input v-model="form.icon" placeholder="el-icon-tickets"></el-input></el-form-item> |
| | | <el-form-item label="权限标识"><el-input v-model="form.permissions"></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> |
| | |
| | | import api from '@/api' |
| | | export default { |
| | | name: 'MenuManage', |
| | | data() { return { tree: [], flat: [], dialogVisible: false, saving: false, form: {}, rules: { name: [{ required: true, message: '请输入菜单名称', trigger: 'blur' }] } } }, |
| | | computed: { parentName() { const parent = this.flat.find(item => item.id === this.form.pid); return parent ? parent.name : '无(一级菜单)' } }, |
| | | data() { |
| | | return { |
| | | tree: [], |
| | | flat: [], |
| | | dialogVisible: false, |
| | | saving: false, |
| | | form: {}, |
| | | parent: null, |
| | | rules: { name: [{ required: true, message: '请输入菜单名称', trigger: 'blur' }] }, |
| | | buttonDialogVisible: false, |
| | | buttonSaving: false, |
| | | buttonMenu: {}, |
| | | buttonAvailable: [], |
| | | buttonSelected: [] |
| | | } |
| | | }, |
| | | computed: { |
| | | parentName() { |
| | | if (!this.parent) return this.form.pid && this.form.pid !== 0 ? '上级菜单' : '无(一级菜单)' |
| | | return this.parent.name |
| | | }, |
| | | typeOptions() { |
| | | if (this.parent && this.parent.menuType === 1) { |
| | | return [{ label: '按钮', value: 2 }] |
| | | } |
| | | if (this.parent && this.parent.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/nav'), api.get('/system/menu/list')]).then(([tree, list]) => { this.tree = tree.data || []; this.flat = list.data || [] }) }, |
| | | openCreate(parent) { this.form = { pid: parent ? parent.id : 0, name: '', url: '', icon: 'el-icon-tickets', menuType: parent ? 1 : 0, permissions: '', sortNo: 10, visible: 1 }; this.dialogVisible = true }, |
| | | openEdit(row) { this.form = { ...row }; this.dialogVisible = true }, |
| | | save() { this.$refs.form.validate(valid => { if (!valid) return; this.saving = true; const request = this.form.id ? api.put('/system/menu/' + this.form.id, this.form) : api.post('/system/menu', this.form); request.then(() => { this.$message.success('保存成功'); this.dialogVisible = false; this.load() }).finally(() => { this.saving = false }) }) }, |
| | | remove(row) { this.$confirm(`确定删除菜单“${row.name}”吗?`, '删除确认', { type: 'warning' }).then(() => api.delete('/system/menu/' + row.id)).then(() => { this.$message.success('删除成功'); this.load() }).catch(() => {}) } |
| | | 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) { |
| | | this.parent = parent || null |
| | | const menuType = parent && parent.menuType === 1 ? 2 : parent ? 1 : 0 |
| | | this.form = { |
| | | pid: parent ? parent.id : 0, |
| | | name: '', |
| | | url: '', |
| | | icon: menuType === 2 ? '' : 'el-icon-tickets', |
| | | menuType, |
| | | permissions: '', |
| | | sortNo: 10, |
| | | visible: 1 |
| | | } |
| | | this.dialogVisible = true |
| | | }, |
| | | openEdit(row) { |
| | | this.parent = this.flat.find(item => item.id === row.pid) || null |
| | | this.form = { ...row } |
| | | 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.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 }) |
| | | }) |
| | | }, |
| | | 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> |