feat(xumu): 新增应用角色权限管理功能

- 添加 AppRoleRightsManager 组件,实现角色管理、权限分配等功能
- 新增 rightsAdd 组件,用于添加和编辑应用角色
- 新增 rightsGraph 组件,用于展示权限关系图
- 集成 ECharts 实现复杂的权限关系可视化
This commit is contained in:
aixianling
2024-12-18 10:16:27 +08:00
parent 0de94d76ee
commit e789570a1b
3 changed files with 804 additions and 0 deletions

View File

@@ -0,0 +1,416 @@
<template>
<section class="AppRoleRightsManager">
<ai-list v-if="!showDetail">
<ai-title slot="title" title="角色管理" isShowBottomBorder/>
<template #content>
<ai-search-bar>
<template #left>
<el-button size="small" type="primary" icon="iconfont iconAdd"
@click="$router.push({hash:'#add'})"
v-if="$permissions('admin_sysapprole_add')">
添加
</el-button>
<el-button
size="small"
icon="iconfont iconDelete"
:disabled="!multipleSelection.length"
class="del-btn-list"
@click="deleteApp('all')"
v-if="$permissions('admin_sysapprole_del')"
>删除
</el-button>
</template>
<template #right>
<el-input
size="small"
v-model="search.roleName"
placeholder="角色名称"
clearable
@change="searchList()"
suffix-icon="iconfont iconSearch"/>
</template>
</ai-search-bar>
<ai-table :tableData="adminList" :colConfigs="colConfigs" :total="total" :current.sync="page.pageNum"
:size.sync="page.pageSize"
@getList="getTableData" :col-configs="colConfigs" :dict="dict"
@selection-change="v=>multipleSelection=v">
<el-table-column label="角色用户" slot="users" align="center">
<template slot-scope="scope">
<el-tooltip
effect="light"
placement="top"
:disabled="scope.row.users.length <= 2"
content="更多角色用户请点击详情按钮">
<span v-if="scope.row.users.length">
{{
scope.row.users
.slice(0, 2)
.map((e) => e.name + "(" + e.phone + ")")
.join(";")
}}
<span v-if="scope.row.users.length > 2">...</span>
</span>
<span v-else>-</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column slot="options" label="操作" fixed="right" align="center">
<template slot-scope="{row}">
<el-button type="text" @click="beforeCopy(row)" v-if="$permissions('admin_sysapprole_edit')">复制
</el-button>
<el-button type="text" @click="viewApp(row)" v-if="$permissions('admin_sysapprole_detail')">详情
</el-button>
<el-button type="text" @click="openRightsGraph(row)" v-if="$permissions('admin_sysapprole_detail')">关系图
</el-button>
<el-button type="text" @click="toAddAppRole(row)" v-if="$permissions('admin_sysapprole_edit')">编辑
</el-button>
<el-button type="text" @click="deleteApp(row)" v-if="$permissions('admin_sysapprole_del')">删除</el-button>
</template>
</el-table-column>
</ai-table>
<ai-dialog
title="应用角色详情"
:visible.sync="viewShow"
width="600px"
customFooter>
<ai-card title="基本信息">
<template #content>
<ai-wrapper>
<ai-info-item label="应用角色名称" :value="viewInfo.name" isLine/>
</ai-wrapper>
</template>
</ai-card>
<ai-card title="权限信息">
<template #content>
<div style="margin-bottom: 16px" v-text="roleList.map(e => e.name).join('、')"/>
</template>
</ai-card>
<ai-card title="角色账号">
<template #right>
<span style="text-align: right; color: #999">
共<span style="color: #26f" v-text="userList.length"/>个账号
</span>
</template>
<template #content>
<div class="datail-table-body" v-if="userList.length">
<div class="datail-item" v-for="(item, index) in userList" :key="index">
<span class="item-name">{{ item.name }}</span>
<span style="color: #999">{{ item.phone }}</span>
</div>
</div>
</template>
</ai-card>
<template #footer>
<el-button
type="primary"
@click="toAddAppRole(viewInfo)"
v-if="$permissions('admin_sysapprole_edit')"
>编辑角色
</el-button>
</template>
</ai-dialog>
<ai-dialog title="权限关系图" :visible.sync="rightsGraph" class="rightsGraphDialog" customFooter>
<rights-graph :instance="instance" :dict="dict" :app="selectApp"/>
<el-button slot="footer" @click="rightsGraph=false">关闭</el-button>
</ai-dialog>
<!--复制角色-->
<el-dialog
class="editStyle"
:visible.sync="copyDialog"
width="520px"
@close="dataInit()"
title="复制角色">
<el-form :model="form" label-width="80px">
<el-form-item label="角色名" :rules="[{ required: true, message: '', trigger: 'blur' }]">
<el-input
v-model="editName"
placeholder="请输入..."
size="small"
clearable
/>
</el-form-item>
</el-form>
<div slot="footer" style="text-align: center">
<el-button
style="width: 92px"
size="small"
@click="copyDialog = false"
>取消
</el-button
>
<el-button
style="width: 92px"
size="small"
type="primary"
@click="copyFn()"
:disabled="!editName"
>
确认
</el-button>
</div>
</el-dialog>
</template>
</ai-list>
<rights-add v-else :instance="instance" :dict="dict" :permissions="permissions"/>
</section>
</template>
<script>
import RightsAdd from "./rightsAdd";
import RightsGraph from "./rightsGraph";
export default {
name: "AppRoleRightsManager",
components: {RightsGraph, RightsAdd},
label: "角色管理",
provide() {
return {
top: this
}
},
props: {
instance: Function,
dict: Object,
permissions: Function,
actions: {
default: () => ({
list: '/admin/role/page',
apps: '/admin/role/list-all',
delete: '/admin/role/del',
detail: '/admin/role/queryById-checked',
modify: '/admin/role/modify',
})
}
},
computed: {
colConfigs() {
return [
{type: "selection"},
{label: "角色名", prop: "name", width: '100px'},
{label: "所属端", prop: "type", width: '100px', dict: "roleType"},
{label: "用户数量", prop: "roleCount", align: 'center', width: '80px'},
{slot: "users"},
{slot: "options"}
]
},
showDetail() {
return this.$route.hash == "#add"
}
},
data() {
return {
page: {pageNum: 1, pageSize: 10},
search: {roleName: ''},
adminList: [], //列表数据
total: 0,
multipleSelection: [],
delShow: false,
delParams: "",
delIds: [],
viewShow: false,
viewInfo: {},
roleList: [], //详情权限列表
row: {},
copyDialog: false,
titleDel: "",
form: {},
editName: "",
userList: [],
rightsGraph: false,
selectApp: {}
};
},
created() {
this.getTableData();
this.dict.load('roleType')
},
methods: {
//查询table列表
getTableData() {
this.adminList = [];
this.instance.post(this.actions.list, null, {
params: {...this.page, ...this.search}
}).then(res => {
if (res?.data) {
this.total = res.data.total;
this.adminList = res.data.records;
}
})
},
//查询
searchList() {
this.page.pageNum = 1;
this.getTableData();
},
//添加按钮
toAddAppRole(item) {
this.$router.push({
hash: "#add",
query: {
id: item.id,
name: item.name
},
});
},
//删除
deleteApp(e) {
if (e == "all") {
this.multipleSelection.map((item) => {
this.delIds.push(item.id);
});
this.delParams = `ids=${this.delIds}`;
this.titleDel = "确定要执行删除操作吗";
} else {
this.delParams = `ids=${e.id}`;
this.titleDel = "确定需要删除该角色吗";
}
this.$confirm(this.titleDel, {
type: "error",
}).then(() => {
this.instance.post(`${this.actions.delete}?${this.delParams}`).then(res => {
if (res?.msg == "success") {
this.getTableData();
} else {
this.$message.error(res.msg);
}
});
}).catch(() => 0);
},
//查看信息
viewApp(e) {
this.userList = e.users;
this.viewInfo = e;
this.viewShow = true;
this.getRowInfo(this.viewInfo.appId, this.viewInfo.id);
},
//查询 row 信息
getRowInfo(appId, id) {
this.roleList = [];
this.instance.post(`${this.actions.detail}?id=${appId}&roleId=${id}`)
.then(res => {
if (res?.data) {
this.roleList = res.data.filter(e => e.checked)
}
})
},
//复制
beforeCopy(row) {
this.row = row;
this.copyDialog = true;
this.getRowInfo(this.row.appId, this.row.id);
},
//确认复制
copyFn() {
let crr = [];
let appRoleList = this.roleList;
for (let i = 0; i < appRoleList.length; i++) {
if (appRoleList[i].checked) {
crr.push(appRoleList[i].id);
if (appRoleList[i].list.length) {
for (let j = 0; j < appRoleList[i].list.length; j++) {
if (appRoleList[i].list[j].checked) {
crr.push(appRoleList[i].list[j].id);
}
}
}
}
}
this.instance.post(`${this.actions.modify}?menus=${crr}`, null, {
params: {
roleName: this.editName,
appId: this.row.appId,
},
})
.then((res) => {
if (res.code == 0) {
this.$message({message: "复制成功", type: "success"});
this.copyDialog = false;
this.searchList()
}
});
},
dataInit() {
this.multipleSelection = [];
this.row = {};
},
openRightsGraph(row) {
this.rightsGraph = true
this.selectApp = row
}
},
}
</script>
<style lang="scss" scoped>
.AppRoleRightsManager {
height: 100%;
:deep( .ai-dialog ) {
.ai-card {
box-shadow: none;
border: 1px solid #eee;
.aibar {
height: 40px;
background: #f3f6f9;
}
.ai-card__body {
padding: 0 16px;
}
}
}
:deep( .rightsGraphDialog ) {
.el-dialog__body {
padding: 0;
}
.ai-dialog__content {
padding-bottom: 0;
}
}
:deep( .datail-table-body ) {
width: 100%;
height: auto;
margin-bottom: 16px;
display: flex;
flex-wrap: wrap;
.datail-item {
flex-shrink: 0;
width: 50%;
height: 24px;
line-height: 24px;
span {
display: inline-block;
font-size: 12px;
}
.item-name {
width: 102px;
padding-left: 16px;
color: #333;
}
}
.datail-item:nth-of-type(2n - 1) {
border-right: 1px solid rgba(208, 212, 220, 1);
width: calc(50% - 1px);
}
}
.padd-l0 {
padding-left: 0 !important;
}
.pad-l16 {
padding-left: 16px;
}
}
</style>

View File

@@ -0,0 +1,196 @@
<template>
<ai-detail class="rightsAdd">
<ai-title :title="addTitle" slot="title" isShowBottomBorder isShowBack @onBackClick="back"/>
<template #content>
<el-form size="small" ref="rightsForm" :model="form" label-width="120px" :rules="rules">
<ai-card title="基本信息">
<template #content>
<div class="grid">
<el-form-item label="应用角色名称" prop="roleName">
<el-input v-model="form.roleName" placeholder="请输入应用角色名称" clearable/>
</el-form-item>
<el-form-item label="角色标记" prop="type">
<ai-select v-model="form.type" dict="roleType" placeholder="请选择角色标记" clearable/>
</el-form-item>
</div>
</template>
</ai-card>
<ai-card title="权限信息">
<template #content>
<el-form-item label="权限列表" prop="menus">
<div class="roleList">
<el-input v-model="filterText" placeholder="请输入..." clearable suffix-icon="iconfont iconSearch"
@change="$refs.tree.filter(filterText)" :validate-event="false"/>
<div class="tree_list">
<el-tree class="filter-tree" ref="roleTree"
:data="roleList"
show-checkbox
:props="defaultProps"
default-expand-all
:check-strictly="false"
node-key="id"
:default-checked-keys="form.menus"
:filter-node-method="filterNode"
@check="handleMenusSelect"/>
</div>
</div>
</el-form-item>
</template>
</ai-card>
</el-form>
</template>
<template #footer>
<el-button @click="back()">取消</el-button>
<el-button type="primary" @click="confirm">保存</el-button>
</template>
</ai-detail>
</template>
<script>
export default {
name: "rightsAdd",
inject: ['top'],
props: {
instance: Function,
dict: Object,
permissions: Function
},
data() {
return {
form: {},
roleName: '',
id: '',
appList: [],
roleList: [],
defaultProps: {
children: 'list',
label: 'name'
},
treeList: [],
filterText: '',
msgTitle: '添加'
}
},
created() {
if (this.isEdit) {
let {id, name: roleName} = this.$route.query
this.form = {menus: [], id, roleName}
this.msgTitle = '编辑'
}
this.getPermissions()
},
computed: {
isEdit() {
return this.$route.query.id
},
addTitle() {
return this.isEdit ? '编辑应用角色' : '新增应用角色'
},
rules() {
return {
roleName: {required: true, message: '请输入应用角色名称'},
roleType: {required: true, message: '请选择角色标记'},
menus: {required: true, message: '请选择权限列表内容'},
}
}
},
methods: {
filterNode(value, data) {
if (!value) return true;
return data.name.indexOf(value) !== -1;
},
//应用名称选择 获取权限列表
getId(data) {
if (data.list.length) {
data.list.forEach(item => {
this.getId(item)
})
} else {
if (data.checked) {
this.form.menus?.push(data.id)
}
}
},
getPermissions() {
this.filterText = ''
let {id: roleId} = this.form
this.instance.post(this.top.actions.detail, null, {
params: {roleId}
}).then(res => {
if (res?.data) {
this.roleList = res.data;
if (this.isEdit) {
this.roleList.forEach(e => this.getId(e))
}
this.roleList = this.roleList.filter(item => !(item.component && item.isApp == 0 && item.isMenu == 0))
}
})
},
handleMenusSelect(node, selected) {
this.$set(this.form, 'menus', [...selected?.checkedKeys])
this.$refs.rightsForm.validateField('menus')
},
//保存提交
confirm() {
this.$refs.rightsForm.validate(v => {
if (v) {
let menus = [this.$refs.roleTree?.getHalfCheckedKeys(), this.$refs.roleTree?.getCheckedKeys()]?.flat()?.toString()
this.instance.post(this.top.actions.modify, null, {
params: {...this.form, menus}
}).then(res => {
if (res?.msg == "success") {
this.$message.success(`${this.msgTitle}应用角色成功`)
this.back()
this.top.searchList()
}
})
}
})
},
//取消 返回
back() {
this.$router.push({})
}
},
}
</script>
<style lang="scss" scoped>
.rightsAdd {
width: 100%;
height: 100%;
position: relative;
.el-form-item {
.el-select {
width: 100%;
}
&.is-error {
.roleList {
border-color: #f46;
}
}
}
.roleList {
background-color: #fcfcfc;
border-radius: 2px;
border: solid 1px #d0d4dc;
padding: 8px;
.input {
display: flex;
justify-content: space-between;
padding: 5px;
margin: 0;
}
.tree_list {
padding: 5px;
height: 370px;
overflow: auto;
}
}
}
</style>

View File

@@ -0,0 +1,192 @@
<template>
<section class="rightsGraph">
<div id="RightGraph"/>
</section>
</template>
<script>
;
export default {
name: "rightsGraph",
inject: ['top'],
props: {
instance: Function,
dict: Object,
app: Object
},
computed: {
graphData() {
let data = [...this.users, ...this.nodes].map(e => {
if (e.x) {
return e
} else return {...e, ...this.renderPosition(e)}
})
return [
{
data,
links: this.links,
categories: data.map(e => e.category).flat().map(name => ({name}))
}
]
}
},
data() {
return {
graph: null,
nodes: [],
links: [],
users: []
}
},
watch: {
graphData: {
deep: true, handler() {
this.refreshGraph()
}
}
},
methods: {
initGraph() {
let dom = document.querySelector("#RightGraph")
if (dom) {
this.graph = echarts.init(dom)
this.graph.setOption({
tooltip: {},
series: [
{
type: 'graph',
layout: 'none',
roam: true,
label: {
show: true,
position: 'right',
formatter: '{b}'
},
labelLayout: {
hideOverlap: true,
},
scaleLimit: {
min: 0.4,
max: 4
},
lineStyle: {
color: 'target',
curveness: 0.1
},
emphasis: {
focus: 'adjacency',
lineStyle: {
width: 5
}
}
}
]
})
this.graph.on('click', this.handleNodeClick)
}
},
refreshGraph() {
this.graph?.setOption({
series: this.graphData
})
},
getAppRoles(role) {
if (role) {
this.nodes.push({...role, category: '应用角色', value: "应用角色", symbolSize: 15})
} else this.instance.post(this.top.actions.list, null, {
params: {pageSize: 999}
}).then(res => {
if (res?.data) {
res.data.records.map(e => {
this.getUsers(e.users, e.id)
this.nodes.push({...e, category: '应用角色', value: "应用角色"})
})
}
})
},
getUsers(pending, source) {
pending?.map(e => {
if (!this.users.some(u => u.id == e.phone)) {
this.users.push({id: e.phone, name: e.name, symbolSize: 5, category: '用户', value: "用户"})
}
this.links.push({source, target: e.phone})
})
},
getPermissions({id: roleId, name: category, dataIndex}) {
const addNodes = (list, source, category, pos) => {
list?.map(e => {
let node = {
...e,
symbolSize: 5,
category,
value: e.list?.length > 0 ? "应用" : "权限",
}
node = {...node, ...this.renderPosition(pos || node)}
this.nodes.splice(dataIndex, 0, node)
if (e.checked == 1) {
this.links.push({source, target: e.id})
}
addNodes(e.list, e.id, e.label, node)
})
}
roleId && this.instance.post(this.top.actions.detail, null, {
params: {roleId}
}).then(res => {
if (res?.data) {
addNodes(res.data, roleId, category)
}
})
},
handleNodeClick(v) {
let {data: role, dataIndex} = v
if (!this.nodes.some(e => e.category == role?.name)) {
role && this.getPermissions({...role, dataIndex})
}
},
renderPosition(node) {
node = JSON.parse(JSON.stringify(node))
let pos = {x: 0, y: 0}
if (node?.x) {
pos.x = Math.max(this.graph?.getWidth() / 3 * 2, node.x) + 100 + Math.random() * 50
pos.y = node.y + Math.random() * 100 - 50
} else if (node.value == '应用角色') {
pos.x = this.graph?.getWidth() / 3 - 200 + Math.random() * 200
pos.y = this.graph?.getHeight() / 2 - 100 + Math.random() * 200
} else if (node.value == '应用') {
pos.x = this.graph?.getWidth() / 3 * 2 - 100 + Math.random() * 100
pos.y = Math.random() * this.graph?.getHeight()
} else if (node.value == '用户') {
pos.x = Math.random() * 50
pos.y = Math.random() * this.graph?.getHeight()
} else {
pos.x = this.graph?.getWidth() - 100 + Math.random() * 100
pos.y = Math.random() * this.graph?.getHeight()
}
return pos
}
},
created() {
this.getAppRoles(this.app)
this.getUsers(this.app.users, this.app.id)
this.getPermissions(this.app)
},
mounted() {
this.$nextTick(() => {
this.initGraph()
})
}
}
</script>
<style lang="scss" scoped>
.rightsGraph {
height: 100%;
:deep( #RightGraph ){
width: 100%;
height: 100%;
min-height: 500px;
}
}
</style>