zzw
3 天以前 757dcb3fcad665eaef93074a4981c4134f7a027e
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
<template>
  <div class="page">
    <div class="page-title">
      <div><h2>菜单权限管理</h2><p>同一菜单必须同时获得租户授权和角色授权,用户才能访问;第一版 admin 直接显示全部菜单</p></div>
    </div>
    <div class="toolbar">
      <span>租户</span>
      <el-select v-model="tenantId" style="width:220px" @change="loadPermissions"><el-option v-for="item in tenants" :key="item.id" :label="item.name" :value="item.id"></el-option></el-select>
      <span>角色</span>
      <el-select v-model="roleId" style="width:220px" @change="loadPermissions"><el-option v-for="item in roles" :key="item.id" :label="item.name" :value="item.id"></el-option></el-select>
      <div class="spacer"></div>
      <el-tag type="success">最终可用菜单:{{ effectiveCount }} 个</el-tag>
      <el-button type="primary" :loading="saving" @click="save">保存双授权</el-button>
    </div>
    <div class="permission-grid">
      <section class="permission-panel">
        <div class="panel-head"><h3>租户菜单授权</h3><el-checkbox v-model="tenantCheckAll" @change="checkAll('tenant')">全选</el-checkbox></div>
        <el-tree ref="tenantTree" :data="menus" node-key="id" show-checkbox default-expand-all :render-content="renderContent" :props="{ label: 'name', children: 'children' }" @check="syncCheckAllState('tenant')"></el-tree>
      </section>
      <section class="permission-panel">
        <div class="panel-head"><h3>角色菜单授权</h3><el-checkbox v-model="roleCheckAll" @change="checkAll('role')">全选</el-checkbox></div>
        <el-tree ref="roleTree" :data="menus" node-key="id" show-checkbox default-expand-all :render-content="renderContent" :props="{ label: 'name', children: 'children' }" @check="syncCheckAllState('role')"></el-tree>
      </section>
      <section class="permission-panel intersection-panel">
        <div class="panel-head"><h3>交集预览</h3></div>
        <div class="intersection-list"><el-tag v-for="item in effectiveMenus" :key="item.id" size="small">{{ item.name }}</el-tag></div>
      </section>
    </div>
  </div>
</template>
<script>
import api from '@/api'
export default {
  name: 'MenuPermission',
  data() { return { menus: [], flatMenus: [], roles: [], tenants: [], tenantId: 1, roleId: null, saving: false, tenantCheckAll: false, roleCheckAll: false, selectedTenantIds: [], selectedRoleIds: [] } },
  computed: {
    allMenuIds() { return this.flatMenus.map(item => String(item.id)) },
    parentMenuIds() { const ids = new Set(); this.flatMenus.forEach(item => ids.add(String(item.pid))); return ids },
    leafMenuIds() { return this.flatMenus.filter(item => !this.parentMenuIds.has(String(item.id))).map(item => String(item.id)) },
    effectiveCount() { return this.effectiveMenus.length },
    effectiveMenus() { return this.flatMenus.filter(item => this.selectedTenantIds.map(String).includes(String(item.id)) && this.selectedRoleIds.map(String).includes(String(item.id))) }
  },
  created() { this.loadBase() },
  methods: {
    loadBase() {
      Promise.all([api.get('/system/menu/tree'), api.get('/system/menu/list'), api.get('/system/roles'), api.get('/system/tenants')]).then(([tree, flat, roles, tenants]) => {
        this.menus = tree.data || []; this.flatMenus = flat.data || []; this.roles = roles.data || []; this.tenants = tenants.data || []
        if (!this.roleId && this.roles.length) this.roleId = this.roles[0].id
        this.$nextTick(this.loadPermissions)
      })
    },
    loadPermissions() {
      if (!this.roleId || !this.tenantId) return
      api.get('/system/menu-permissions', { params: { roleId: this.roleId, tenantId: this.tenantId } }).then(res => {
        const data = res.data || {}
        this.selectedTenantIds = data.tenantMenuIds || []
        this.selectedRoleIds = data.roleMenuIds || []
        this.$nextTick(() => {
          this.$refs.tenantTree.setCheckedKeys(this.leafKeys(this.selectedTenantIds))
          this.$refs.roleTree.setCheckedKeys(this.leafKeys(this.selectedRoleIds))
          this.syncCheckAllState('tenant'); this.syncCheckAllState('role')
        })
      })
    },
    renderContent(h, { data }) {
      const label = data.menuType === 2 ? data.name + ' [按钮 ' + (data.permissions || '') + ']' : data.name
      return h('span', { class: { 'perm-button-node': data.menuType === 2 } }, label)
    },
    leafKeys(ids) { const leafIds = new Set(this.leafMenuIds); return (ids || []).map(String).filter(id => leafIds.has(id)) },
    checkedMenuIds(ref) { return [...new Set([...(ref.getCheckedKeys(false) || []), ...(ref.getHalfCheckedKeys() || [])].map(String))] },
    syncCheckAllState(target) { const ref = target === 'tenant' ? this.$refs.tenantTree : this.$refs.roleTree; if (!ref) return; const checked = this.checkedMenuIds(ref).length === this.allMenuIds.length; if (target === 'tenant') this.tenantCheckAll = checked; if (target === 'role') this.roleCheckAll = checked },
    checkAll(target) {
      const checked = target === 'tenant' ? this.tenantCheckAll : this.roleCheckAll
      const ref = target === 'tenant' ? this.$refs.tenantTree : this.$refs.roleTree
      ref.setCheckedKeys(checked ? this.allMenuIds : [])
      this.$nextTick(() => this.syncCheckAllState(target))
    },
    save() {
      const tenantMenuIds = this.checkedMenuIds(this.$refs.tenantTree)
      const roleMenuIds = this.checkedMenuIds(this.$refs.roleTree)
      this.saving = true
      api.post('/system/menu-permissions', { tenantId: this.tenantId, roleId: this.roleId, tenantMenuIds, roleMenuIds }).then(() => {
        this.selectedTenantIds = tenantMenuIds; this.selectedRoleIds = roleMenuIds; this.$message.success('菜单权限已保存')
      }).finally(() => { this.saving = false })
    }
  }
}
</script>
<style scoped>
::v-deep .perm-button-node { color: #b8791f; }
 
.permission-grid { display: grid; grid-template-columns: 1fr 1fr 320px; gap: 12px; }
.permission-panel { min-height: calc(100vh - 220px); padding: 14px; border: 1px solid #dce4ec; border-radius: 4px; background: #fff; overflow: auto; }
.panel-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.panel-head h3 { margin: 0; color: #31465d; font-size: 15px; }
.intersection-list { display: flex; flex-wrap: wrap; gap: 6px; }
@media (max-width: 1100px) { .permission-grid { grid-template-columns: 1fr; } }
</style>