zzw
1 天以前 943b68c17b27f3b0f9973533f832c48dd6afbb6f
emergency-repair-web/src/views/system/MenuManage.vue
@@ -1,33 +1,80 @@
<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>
@@ -35,19 +82,130 @@
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>