前后端全面升级

This commit is contained in:
pycook
2023-07-10 17:42:15 +08:00
parent f57ff80099
commit 98cc853dbc
641 changed files with 97789 additions and 23995 deletions

View File

@@ -0,0 +1,72 @@
import { axios } from '@/utils/request'
const urlPrefix = '/v1/acl'
export function searchRole(params) {
return axios({
url: urlPrefix + `/roles`,
method: 'GET',
params: params
})
}
export function addRole(params) {
return axios({
url: urlPrefix + '/roles',
method: 'POST',
data: params
})
}
export function updateRoleById(id, params) {
return axios({
url: urlPrefix + `/roles/${id}`,
method: 'PUT',
data: params
})
}
export function deleteRoleById(id) {
return axios({
url: urlPrefix + `/roles/${id}`,
method: 'DELETE'
})
}
export function searchApp(params = {}) {
return axios({
url: urlPrefix + '/apps',
method: 'GET',
params: { ...params, page_size: 9999 }
})
}
export function addApp(data) {
return axios({
url: urlPrefix + '/apps',
method: 'POST',
data: data
})
}
export function updateApp(aid, data) {
return axios({
url: urlPrefix + `/apps/${aid}`,
method: 'PUT',
data: data
})
}
export function getApp(aid) {
return axios({
url: urlPrefix + `/apps/${aid}`,
method: 'GET',
})
}
export function deleteApp(aid) {
return axios({
url: urlPrefix + `/apps/${aid}`,
method: 'DELETE'
})
}

View File

@@ -0,0 +1,35 @@
import { axios } from '@/utils/request'
const urlPrefix = '/v1/acl'
export function searchPermissonHistory(params) {
return axios({
url: urlPrefix + `/audit_log/permission`,
method: 'GET',
params: params
})
}
export function searchRoleHistory(params) {
return axios({
url: urlPrefix + `/audit_log/role`,
method: 'GET',
params: params
})
}
export function searchResourceHistory(params) {
return axios({
url: urlPrefix + `/audit_log/resource`,
method: 'GET',
params: params
})
}
export function searchTriggerHistory(params) {
return axios({
url: urlPrefix + `/audit_log/trigger`,
method: 'GET',
params: params
})
}

View File

@@ -0,0 +1,136 @@
import { axios } from '@/utils/request'
const urlPrefix = '/v1/acl'
export function getResourcePerms(resourceID, params) {
return axios({
url: urlPrefix + `/resources/${resourceID}/permissions`,
method: 'GET',
params
})
}
export function getResourceTypePerms(typeID) {
return axios({
url: urlPrefix + `/resource_types/${typeID}/perms`,
method: 'GET'
})
}
export function getResourceGroupPerms(resourceGroupID) {
return axios({
url: urlPrefix + `/resource_groups/${resourceGroupID}/permissions`,
method: 'GET'
})
}
export function setRoleResourcePerm(rid, resourceID, params) {
return axios({
url: urlPrefix + `/roles/${rid}/resources/${resourceID}/grant2`,
method: 'POST',
data: params
})
}
export function setRoleResourceGroupPerm(rid, resourceGroupID, params) {
return axios({
url: urlPrefix + `/roles/${rid}/resource_groups/${resourceGroupID}/grant`,
method: 'POST',
data: params
})
}
export function deleteRoleResourcePerm(rid, resourceID, params) {
return axios({
url: urlPrefix + `/roles/${rid}/resources/${resourceID}/revoke2`,
method: 'POST',
data: params
})
}
export function deleteRoleResourceGroupPerm(rid, resourceGroupID, params) {
return axios({
url: urlPrefix + `/roles/${rid}/resource_groups/${resourceGroupID}/revoke`,
method: 'POST',
data: params
})
}
// 资源组 清空按钮使用
export function deleteRoleResourceGroupPerm2(rid, resourceGroupID, params) {
return axios({
url: urlPrefix + `/roles/${rid}/resource_groups/${resourceGroupID}/revoke2`,
method: 'POST',
data: params
})
}
export function searchPermResourceByRoleId(rid, params) {
return axios({
url: urlPrefix + `/roles/${rid}/resources`,
method: 'GET',
params: params
})
}
export function roleHasPermissionToGrant(params) {
return axios({
url: urlPrefix + '/roles/has_perm',
method: 'GET',
params: params
})
}
// 资源批量授权
export function setBatchRoleResourcePerm(rid, params) {
return axios({
url: urlPrefix + `/roles/${rid}/resources/batch/grant`,
method: 'POST',
data: params
})
}
// 资源组批量授权
export function setBatchRoleResourceGroupPerm(rid, params) {
return axios({
url: urlPrefix + `/roles/${rid}/resource_groups/batch/grant`,
method: 'POST',
data: params
})
}
// 资源批量权限回收
export function setBatchRoleResourceRevoke(rid, params) {
return axios({
url: urlPrefix + `/roles/${rid}/resources/batch/revoke`,
method: 'POST',
data: params
})
}
// 资源组批量授权
export function setBatchRoleResourceGroupRevoke(rid, params) {
return axios({
url: urlPrefix + `/roles/${rid}/resource_groups/batch/revoke`,
method: 'POST',
data: params
})
}
// 按资源名批量授权
export function setBatchRoleResourceByResourceName(rid, params) {
return axios({
url: urlPrefix + `/roles/${rid}/resources/batch/grant2`,
method: 'POST',
data: params
})
}
// 资源名批量回收
export function setBatchRoleResourceRevokeByResourceName(rid, params) {
return axios({
url: urlPrefix + `/roles/${rid}/resources/batch/revoke2`,
method: 'POST',
data: params
})
}

View File

@@ -0,0 +1,104 @@
import { axios } from '@/utils/request'
const urlPrefix = '/v1/acl'
export function searchResource (params) {
return axios({
url: urlPrefix + `/resources`,
method: 'GET',
params: params
})
}
export function addResource (params) {
return axios({
url: urlPrefix + '/resources',
method: 'POST',
data: params
})
}
export function updateResourceById (id, params) {
return axios({
url: urlPrefix + `/resources/${id}`,
method: 'PUT',
data: params
})
}
export function deleteResourceById (id) {
return axios({
url: urlPrefix + `/resources/${id}`,
method: 'DELETE'
})
}
export function searchResourceType (params) {
return axios({
url: urlPrefix + `/resource_types`,
method: 'GET',
params: params
})
}
export function addResourceType (params) {
return axios({
url: urlPrefix + '/resource_types',
method: 'POST',
data: params
})
}
export function updateResourceTypeById (id, params) {
return axios({
url: urlPrefix + `/resource_types/${id}`,
method: 'PUT',
data: params
})
}
export function deleteResourceTypeById (id) {
return axios({
url: urlPrefix + `/resource_types/${id}`,
method: 'DELETE'
})
}
// add resource group
export function getResourceGroups (params) {
return axios({
url: `${urlPrefix}/resource_groups`,
method: 'GET',
params: params
})
}
export function addResourceGroup (data) {
return axios({
url: `${urlPrefix}/resource_groups`,
method: 'POST',
data: data
})
}
export function updateResourceGroup (_id, data) {
return axios({
url: `${urlPrefix}/resource_groups/${_id}`,
method: 'PUT',
data: data
})
}
export function deleteResourceGroup (_id) {
return axios({
url: `${urlPrefix}/resource_groups/${_id}`,
method: 'DELETE'
})
}
export function getResourceGroupItems (_id) {
return axios({
url: `${urlPrefix}/resource_groups/${_id}/items`,
method: 'GET'
})
}

View File

@@ -0,0 +1,83 @@
import { axios } from '@/utils/request'
const urlPrefix = '/v1/acl'
export function searchRole(params) {
return axios({
url: urlPrefix + `/roles`,
method: 'GET',
params: params
})
}
export function addRole(params) {
return axios({
url: urlPrefix + '/roles',
method: 'POST',
data: params
})
}
export function updateRoleById(id, params) {
return axios({
url: urlPrefix + `/roles/${id}`,
method: 'PUT',
data: params
})
}
export function deleteRoleById(id, data) {
return axios({
url: urlPrefix + `/roles/${id}`,
method: 'DELETE',
data: data
})
}
export function addParentRole(id, otherID, data) {
return axios({
url: urlPrefix + `/roles/${id}/parents`,
method: 'POST',
data: { ...data, parent_id: otherID }
})
}
// export function addChildRole (id, otherID, data) {
// return axios({
// url: urlPrefix + `/roles/${otherID}/parents`,
// method: 'POST',
// data: {...data, parent_id: id }
// })
// }
export function delParentRole(cid, pid, data) {
return axios({
url: urlPrefix + `/roles/${cid}/parents`,
method: 'DELETE',
data: { ...data, parent_id: pid }
})
}
// export function delChildRole (pid, cid, data) {
// return axios({
// url: urlPrefix + `/roles/${cid}/parents`,
// method: 'DELETE',
// data: { data, parent_id: pid }
// })
// }
export function getUsersUnderRole(rid, data) {
return axios({
url: urlPrefix + `/roles/${rid}/users`,
method: 'GET',
params: data
})
}
export function addBatchParentRole(parent_id, data) {
return axios({
url: urlPrefix + `/roles/${parent_id}/children`,
method: 'POST',
data
})
}

View File

@@ -0,0 +1,16 @@
import { axios } from '@/utils/request'
export function getSecret() {
return axios({
url: '/v1/acl/users/secret',
method: 'GET'
})
}
export function updateSecret(data) {
return axios({
url: '/v1/acl/users/reset_key_secret',
method: 'POST',
data,
})
}

View File

@@ -0,0 +1,56 @@
import { axios } from '@/utils/request'
const urlPrefix = '/v1/acl'
export function getTriggers(params) {
return axios({
url: urlPrefix + '/triggers',
method: 'GET',
params: params
})
}
export function addTrigger(data) {
return axios({
url: urlPrefix + '/triggers',
method: 'POST',
data: data
})
}
export function updateTrigger(tid, data) {
return axios({
url: urlPrefix + `/triggers/${tid}`,
method: 'PUT',
data: data
})
}
export function deleteTrigger(tid) {
return axios({
url: urlPrefix + `/triggers/${tid}`,
method: 'DELETE'
})
}
export function applyTrigger(tid) {
return axios({
url: urlPrefix + `/triggers/${tid}/apply`,
method: 'POST'
})
}
export function cancelTrigger(tid) {
return axios({
url: urlPrefix + `/triggers/${tid}/cancel`,
method: 'POST'
})
}
export function patternResults(params) {
return axios({
url: `${urlPrefix}/triggers/resources`,
method: 'POST',
data: params
})
}

View File

@@ -0,0 +1,49 @@
/* eslint-disable */
import { axios } from '@/utils/request'
const urlPrefix = '/v1/acl'
export function currentUser() {
return axios({
url: urlPrefix + `/users/info`,
method: 'GET'
})
}
export function getOnDutyUser() {
return axios({
url: urlPrefix + '/users/employee',
method: 'GET',
})
}
export function searchUser(params) {
return axios({
url: urlPrefix + `/users`,
method: 'GET',
params: params
})
}
export function addUser(params) {
return axios({
url: urlPrefix + '/users',
method: 'POST',
data: params
})
}
export function updateUserById(id, params) {
return axios({
url: urlPrefix + `/users/${id}`,
method: 'PUT',
data: params
})
}
export function deleteUserById(id) {
return axios({
url: urlPrefix + `/users/${id}`,
method: 'DELETE'
})
}

View File

@@ -0,0 +1,9 @@
export const valueTypeMap = {
'0': '整数',
'1': '浮点数',
'2': '文本',
'3': 'datetime',
'4': 'date',
'5': 'time',
'6': 'json'
}

View File

@@ -0,0 +1,6 @@
import genAclRoutes from './router'
export default {
name: 'acl',
route: genAclRoutes
}

View File

@@ -0,0 +1,100 @@
import { BasicLayout, RouteView } from '@/layouts/index'
import { searchApp } from '@/modules/acl/api/app'
const genAppRoute = ({ name }) => {
return {
path: `/acl/${name}`,
component: RouteView,
meta: { title: name, icon: 'solution', permission: [`${name}_admin`, 'acl_admin'] },
children: [
{
path: `/acl/${name}/roles`,
name: `${name}_roles_acl`,
hideChildrenInMenu: true,
component: () => import('../views/roles'),
meta: { title: '角色管理', icon: 'team', keepAlive: true }
},
{
path: `/acl/${name}/resources`,
name: `${name}_resources_acl`,
hideChildrenInMenu: true,
component: () => import('../views/resources'),
meta: { title: '资源管理', icon: 'credit-card', keepAlive: false }
},
{
path: `/acl/${name}/resource_types`,
name: `${name}_resource_types_acl`,
hideChildrenInMenu: true,
component: () => import('../views/resource_types'),
meta: { title: '资源类型', icon: 'file-text', keepAlive: true }
},
{
path: `/acl/${name}/trigger`,
name: `${name}_trigger_acl`,
hideChildrenInMenu: true,
component: () => import('../views/trigger'),
meta: { title: '触发器', icon: 'clock-circle', keepAlive: true }
},
{
path: `/acl/${name}/history`,
name: `${name}_history_acl`,
hideChildrenInMenu: true,
component: () => import('../views/history'),
meta: { title: '操作审计', icon: 'search', keepAlive: false }
}
]
}
}
const genAclRoutes = async () => {
const aclApps = await searchApp()
const routes = {
path: '/acl',
name: 'acl',
component: BasicLayout,
meta: { title: 'ACL', keepAlive: true },
redirect: '/acl/secret_key',
children: [
{
path: `/acl/secret_key`,
name: 'acl_secret_key',
component: () => import('../views/secretKey'),
meta: { title: '用户密钥', icon: 'key' }
},
{
path: `/acl/operate_history`,
name: 'acl_operate_history',
component: () => import('../views/operation_history/index.vue'),
// meta: { title: '操作审计', icon: 'search', permission: ['acl_admin'] },
meta: { title: '操作审计', icon: 'search' }
},
{
path: `/acl/user`,
name: 'acl_user',
component: () => import('../views/users'),
meta: { title: '用户管理', icon: 'user', permission: ['acl_admin'] }
},
{
path: `/acl/roles`,
name: `acl_roles`,
component: () => import('../views/roles'),
meta: { title: '角色管理', icon: 'team', keepAlive: true, permission: ['acl_admin'] }
},
{
path: `/acl/apps`,
name: 'acl_apps',
component: () => import('../views/apps'),
meta: { title: '应用管理', icon: 'appstore', permission: ['acl_admin'] }
}
]
}
aclApps.apps.forEach(app => {
if (app.name !== 'acl') {
routes.children.push(genAppRoute(app))
}
})
return routes
}
export default genAclRoutes

View File

View File

View File

View File

@@ -0,0 +1,118 @@
<template>
<div>
<a-card title="应用管理" class="acl-app-wrapper" :style="{ '--ant-body-height': `${windowHeight - 150}px` }">
<a-row :guttr="24">
<a-col v-for="app in apps" :key="app.id" :span="8">
<div style="margin: 15px">
<a-card class="acl-app-single-card">
<a-card-meta :title="app.name">
<div slot="description" :title="app.description || ''">{{ app.description || '' }}</div>
<a-avatar style="background-color: #5dc2f1" slot="avatar">{{ app.name[0].toUpperCase() }}</a-avatar>
</a-card-meta>
<template class="ant-card-actions" slot="actions">
<a-icon type="edit" @click="handleEditApp(app)" />
<a-icon type="delete" @click="handleDeleteApp(app)" />
</template>
</a-card>
</div>
</a-col>
<a-col :span="8">
<div class="acl-app-add" @click="handleCreateApp">
<span class="acl-app-add-icon" style="">+</span>
</div>
</a-col>
</a-row>
</a-card>
<app-form ref="appForm" @refresh="loadApps"></app-form>
</div>
</template>
<script>
/* eslint-disable */
import { mapState } from 'vuex'
import { searchApp, addApp, updateApp, deleteApp } from '@/modules/acl/api/app'
import AppForm from './module/appForm'
export default {
name: 'AclApps',
data() {
return {
apps: [],
selected: null,
modalVisible: false,
allUsers: [],
permissionRoles: [],
}
},
computed: mapState({
windowHeight: (state) => state.windowHeight,
}),
components: { AppForm },
mounted() {
this.loadApps()
},
methods: {
loadApps() {
searchApp().then((res) => {
this.apps = res.apps
})
},
handleCreateApp() {
this.selected = null
this.$refs.appForm.handleEdit()
},
handleDeleteApp(app) {
const that = this
this.$confirm({
title: '危险操作',
content: '确定要删除该App吗',
onOk() {
deleteApp(app.id).then((res) => {
that.$message.success(`删除成功${app.name}`)
that.loadApps()
})
},
})
},
handleEditApp(app) {
this.$nextTick(() => {
this.$refs.appForm.handleEdit(app)
})
},
},
}
</script>
<style lang="less">
.acl-app-wrapper {
> .ant-card-body {
overflow-y: auto;
height: var(--ant-body-height);
cursor: default;
}
.acl-app-single-card {
.ant-card-meta-description > div {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
&:hover {
box-shadow: 0 2px 8px #0000004d;
}
}
.acl-app-add {
height: 141px;
margin: 15px;
border: 1px #e8e8e8 solid;
transition: all 0.3s;
&:hover {
box-shadow: 0 2px 8px #0000004d;
}
.acl-app-add-icon {
font-size: 70px;
display: block;
text-align: center;
color: #5dc2f1;
cursor: pointer;
}
}
}
</style>

View File

@@ -0,0 +1,237 @@
<template>
<div>
<a-card :bordered="false">
<a-tabs default-active-key="1">
<a-tab-pane key="1" tab="权限变更">
<permisson-history-table
v-if="isloaded"
:allResourceTypes="allResourceTypes"
:allResources="allResources"
:allUsers="allUsers"
:allRoles="allRoles"
:allRolesMap="allRolesMap"
:allUsersMap="allUsersMap"
:allResourceTypesMap="allResourceTypesMap"
@loadMoreResources="loadMoreResources"
@reloadResources="reloadResources"
@fetchResources="fetchResources"
@resourceClear="resourceClear"
></permisson-history-table>
</a-tab-pane>
<a-tab-pane key="2" tab="角色变更">
<role-history-table
v-if="isloaded"
:allUsers="allUsers"
:allRoles="allRoles"
:allRolesMap="allRolesMap"
:allUsersMap="allUsersMap"
></role-history-table>
</a-tab-pane>
<a-tab-pane key="3" tab="资源变更">
<resource-history-table
v-if="isloaded"
:allResources="allResources"
:allUsers="allUsers"
:allRoles="allRoles"
:allRolesMap="allRolesMap"
:allUsersMap="allUsersMap"
:allResourcesMap="allResourcesMap"
@loadMoreResources="loadMoreResources"
@reloadResources="reloadResources"
@fetchResources="fetchResources"
@resourceClear="resourceClear"
></resource-history-table>
</a-tab-pane>
<a-tab-pane key="4" tab="资源类型变更">
<resource-type-history-table
v-if="isloaded"
:allResourceTypes="allResourceTypes"
:allUsers="allUsers"
:allRoles="allRoles"
:allRolesMap="allRolesMap"
:allUsersMap="allUsersMap"
:allResourceTypesMap="allResourceTypesMap"
></resource-type-history-table>
</a-tab-pane>
<a-tab-pane key="5" tab="触发器变更">
<trigger-history-table
v-if="isloaded"
:allTriggers="allTriggers"
:allUsers="allUsers"
:allRoles="allRoles"
:allRolesMap="allRolesMap"
:allUsersMap="allUsersMap"
:allTriggersMap="allTriggersMap"
:allResourceTypesMap="allResourceTypesMap"
></trigger-history-table>
</a-tab-pane>
</a-tabs>
</a-card>
</div>
</template>
<script>
import permissonHistoryTable from './module/permissionHistoryTable.vue'
import roleHistoryTable from './module/roleHistoryTable.vue'
import resourceHistoryTable from './module/resourceHistoryTable.vue'
import triggerHistoryTable from './module/triggerHistoryTable.vue'
import resourceTypeHistoryTable from './module/resourceTypeHistoryTable.vue'
import { getTriggers } from '@/modules/acl/api/trigger'
import { searchRole } from '@/modules/acl/api/role'
import { searchUser } from '@/modules/acl/api/user'
import { searchResource, searchResourceType } from '@/modules/acl/api/resource'
export default {
components: {
permissonHistoryTable,
roleHistoryTable,
resourceHistoryTable,
triggerHistoryTable,
resourceTypeHistoryTable,
},
data() {
return {
app_id: this.$route.name.split('_')[0],
isloaded: false,
resourcesNum: 0,
resourcesPage: 1,
allResourceTypes: [],
allResources: [],
allUsers: [],
allRoles: [],
allTriggers: [],
allRolesMap: new Map(),
allUsersMap: new Map(),
allResourceTypesMap: new Map(),
allResourcesMap: new Map(),
allTriggersMap: new Map(),
}
},
async created() {
await this.initData()
this.isloaded = true
},
watch: {
'$route.name': async function(oldName, newName) {
try {
this.isloaded = false
this.app_id = this.$route.name.split('_')[0]
this.allResources = []
this.resourcesPage = 1
await this.initData()
} finally {
this.isloaded = true
}
},
},
methods: {
async initData() {
await Promise.all([
this.getAllRoles(),
this.getAllUsers(),
this.getAllResourceTypes(),
this.getAllResources(1),
this.getTriggers(),
])
},
async getAllRoles() {
const { roles } = await searchRole({ app_id: this.app_id, page_size: 9999 })
const allRoles = []
const allRolesMap = new Map()
roles.forEach((item) => {
allRoles.push({ [item.name]: item.id })
allRolesMap.set(item.id, item.name)
})
this.allRoles = allRoles
this.allRolesMap = allRolesMap
},
async getAllUsers() {
const { users } = await searchUser({ page_size: 10000 })
const allUsers = []
const allUsersMap = new Map()
users.forEach((item) => {
allUsers.push({ [item.nickname]: item.uid })
allUsersMap.set(item.uid, item.nickname)
})
this.allUsers = allUsers
this.allUsersMap = allUsersMap
},
async getAllResourceTypes() {
const { groups } = await searchResourceType({ app_id: this.app_id, page_size: 9999, page: 1 })
const allResourceTypes = []
const allResourceTypesMap = new Map()
groups.forEach((item) => {
allResourceTypes.push({ [item.name]: item.id })
allResourceTypesMap.set(item.id, item.name)
})
this.allResourceTypes = allResourceTypes
this.allResourceTypesMap = allResourceTypesMap
},
async getAllResources(page = 1, value = undefined) {
const { resources, numfound } = await searchResource({
page: page,
page_size: 50,
app_id: this.app_id,
q: value,
})
this.resourcesNum = numfound
const allResources = this.allResources
const allResourcesMap = this.allResourcesMap
resources.forEach((item) => {
allResources.push({ [item.name]: item.id })
allResourcesMap.set(item.id, item.name)
})
this.allResources = allResources
this.allResourcesMap = allResourcesMap
},
reloadResources() {
this.resourcesPage = 1
this.allResources = []
this.allResourcesMap = new Map()
this.getAllResources()
},
loadMoreResources(value) {
if (this.allResources.length < this.resourcesNum) {
let currentPage = this.resourcesPage
this.getAllResources(++currentPage, value)
this.resourcesPage = currentPage
}
},
resourceClear() {
this.resourcesPage = 1
this.allResources = []
this.getAllResources()
},
async fetchResources(value) {
const allResources = []
const allResourcesMap = new Map()
const { resources, numfound } = await searchResource({
page: 1,
page_size: 50,
app_id: this.app_id,
q: value,
})
this.resourcesNum = numfound
this.resourcesPage = 1
resources.forEach((item) => {
allResources.push({ [item.name]: item.id })
allResourcesMap.set(item.id, item.name)
})
this.allResources = allResources
this.allResourcesMap = allResourcesMap
},
async getTriggers() {
const res = await getTriggers({ app_id: this.app_id })
const allTriggers = []
const allTriggersMap = new Map()
res.forEach((item) => {
allTriggers.push({ [item.name]: item.id })
allTriggersMap.set(item.id, item.name)
})
this.allTriggers = allTriggers
this.allTriggersMap = allTriggersMap
},
},
}
</script>
<style></style>

View File

@@ -0,0 +1,85 @@
<template>
<CustomDrawer @close="handleClose" width="500" :title="title" :visible="visible" :closable="false">
<a-form :form="form" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<a-form-item label="应用名称">
<a-input v-decorator="['name', { rules: [{ required: true, message: '请输入应用名称' }] }]"> </a-input>
</a-form-item>
<a-form-item label="描述">
<a-input v-decorator="['description', { rules: [{ required: true, message: '请输入描述' }] }]"> </a-input>
</a-form-item>
<a-form-item label="AppId">
<a-input v-decorator="['app_id', { rules: [{ required: false }] }]" :disabled="mode === 'update'"> </a-input>
</a-form-item>
<a-form-item label="SecretKey">
<a-input v-decorator="['secret_key', { rules: [{ required: false }] }]" :disabled="mode === 'update'">
</a-input>
</a-form-item>
<a-form-item>
<a-input v-decorator="['id']" type="hidden" />
</a-form-item>
</a-form>
<div class="custom-drawer-bottom-action">
<a-button @click="handleClose">取消</a-button>
<a-button @click="handleSubmit" type="primary">提交</a-button>
</div>
</CustomDrawer>
</template>
<script>
import { addApp, updateApp, getApp } from '@/modules/acl/api/app'
export default {
name: 'AppForm',
data() {
return {
visible: false,
title: '创建应用',
mode: 'create',
}
},
beforeCreate() {
this.form = this.$form.createForm(this)
},
methods: {
handleEdit(ele) {
this.visible = true
if (ele) {
this.title = '修改应用'
this.mode = 'update'
console.log(ele)
const { name, description } = ele
getApp(ele.id).then((res) => {
const { app_id, secret_key } = res
this.$nextTick(() => {
this.form.setFieldsValue({ name, description, app_id, secret_key, id: ele.id })
})
})
} else {
this.mode = 'create'
this.title = '创建应用'
}
},
handleClose() {
this.visible = false
this.form.resetFields()
},
async handleSubmit() {
this.form.validateFields(async (err, values) => {
if (err) {
return
}
if (values.id) {
await updateApp(values.id, values).then((res) => {
this.$message.success('修改成功!')
})
} else {
await addApp(values).then((res) => {
this.$message.success('创建成功!')
})
}
this.handleClose()
this.$emit('refresh')
})
},
},
}
</script>

View File

@@ -0,0 +1,116 @@
<template>
<div>
<a-row class="row" type="flex" justify="end">
<a-col>
<a-space align="end">
<a-button class="left-button" size="small" :disabled="prevIsDisabled" @click="prevPage"><a-icon type="left" /></a-button>
<a-button class="page-button" size="small">{{ currentPage }}</a-button>
<a-button class="right-button" size="small" :disabled="nextIsDisabled" @click="nextPage"><a-icon type="right" /></a-button>
<a-dropdown class="dropdown" size="small" placement="topCenter" :trigger="['click']" :disabled="dropdownIsDisabled">
<a-menu slot="overlay">
<a-menu-item v-for="(size,index) in pageSizes" :key="index" @click="handleItemClick(size)">
{{ size }}/
</a-menu-item>
</a-menu>
<a-button size="small"> {{ pageSize }}/ <a-icon type="down" /> </a-button>
</a-dropdown>
</a-space>
</a-col>
</a-row>
</div>
</template>
<script>
export default {
props: {
currentPage: {
type: Number,
required: true
},
pageSize: {
type: Number,
required: true
},
pageSizes: {
type: Array,
required: true
},
total: {
type: Number,
required: true
},
isLoading: {
type: Boolean,
required: false
}
},
data() {
return {
dropdownIsDisabled: false,
prevIsDisabled: true,
}
},
computed: {
nextIsDisabled() {
return this.isLoading || this.total < this.pageSize
}
},
watch: {
isLoading: {
immediate: true,
handler: function (val) {
if (val === true) {
this.dropdownIsDisabled = true
this.prevIsDisabled = true
} else {
this.dropdownIsDisabled = false
if (this.currentPage === 1) {
this.prevIsDisabled = true
} else {
this.prevIsDisabled = false
}
}
}
},
currentPage: {
immediate: true,
handler: function (val) {
if (val === 1) {
this.prevIsDisabled = true
}
}
}
},
methods: {
handleItemClick(size) {
this.$emit('showSizeChange', size)
},
nextPage() {
const pageNum = this.currentPage + 1
this.$emit('change', pageNum)
},
prevPage() {
const pageNum = this.currentPage - 1
this.$emit('change', pageNum)
}
}
}
</script>
<style lang="less" scoped>
.row{
margin-top: 5px;
.left-button{
padding: 0;
width: 24px;
}
.right-button{
padding: 0;
width: 24px;
}
.page-button{
padding: 0;
width: 24px;
}
}
</style>

View File

@@ -0,0 +1,268 @@
<template>
<CustomDrawer
:title="`权限汇总: ${user.nickname}`"
:visible="visible"
placement="left"
width="100%"
@close="visible = false"
:hasFooter="false"
>
<a-tabs @change="handleSwitchApp">
<a-tab-pane
v-for="app in displayApps"
:key="app.id"
:tab="app.name.slice(0, 1).toUpperCase() + app.name.slice(1)"
>
<a-tabs
v-if="resourceTypes && resourceTypes.length"
:animated="false"
v-model="currentResourceId"
@change="loadResource()"
>
<a-tab-pane v-for="rType in resourceTypes" :key="rType.id" :tab="rType.name">
<a-spin :spinning="spinning">
<vxe-table :max-height="`${windowHeight - 230}px`" :data="resources" ref="rTable">
<vxe-column
field="name"
title="资源名"
width="30%"
:filters="[{ data: '' }]"
:filter-method="filterNameMethod"
:filter-recover-method="filterNameRecoverMethod"
>
<template #filter="{ $panel, column }">
<template v-for="(option, index) in column.filters">
<input
class="my-input"
type="type"
:key="index"
v-model="option.data"
@input="$panel.changeOption($event, !!option.data, option)"
@keyup.enter="$panel.confirmFilter()"
placeholder="按回车确认筛选"
/>
</template>
</template>
</vxe-column>
<vxe-column field="permissions" title="权限列表" width="70%">
<template #default="{row}">
<a-tag color="cyan" v-for="(r, index) in row.permissions" :key="index">{{ r }}</a-tag>
</template>
</vxe-column>
</vxe-table>
<!-- <a-table
:columns="tableColumns"
:dataSource="resources"
:rowKey="record=>record.id"
:pagination="{ showTotal: (total, range) => `${range[0]}-${range[1]} 共 ${total} 条记录` }"
showPagination="auto"
ref="rTable"
size="middle">
<div slot="filterDropdown" slot-scope="{ setSelectedKeys, selectedKeys, confirm, clearFilters, column }" class="custom-filter-dropdown">
<a-input
v-ant-ref="c => searchInput = c"
:placeholder="` ${column.title}`"
:value="selectedKeys[0]"
@change="e => setSelectedKeys(e.target.value ? [e.target.value] : [])"
@pressEnter="() => handleSearch(selectedKeys, confirm, column)"
style="width: 188px; margin-bottom: 8px; display: block;"
/>
<a-button
type="primary"
@click="() => handleSearch(selectedKeys, confirm, column)"
icon="search"
size="small"
style="width: 90px; margin-right: 8px"
>搜索</a-button>
<a-button
@click="() => handleReset(clearFilters, column)"
size="small"
style="width: 90px"
>重置</a-button>
</div>
<a-icon slot="filterIcon" slot-scope="filtered" type="search" :style="{ color: filtered ? '#108ee9' : undefined }" />
<template slot="nameSearchRender" slot-scope="text">
<span v-if="columnSearchText.name">
<template v-for="(fragment, i) in text.toString().split(new RegExp(`(?<=${columnSearchText.name})|(?=${columnSearchText.name})`, 'i'))">
<mark v-if="fragment.toLowerCase() === columnSearchText.name.toLowerCase()" :key="i" class="highlight">{{ fragment }}</mark>
<template v-else>{{ fragment }}</template>
</template>
</span>
<template v-else>{{ text }}</template>
</template>
<template slot="permissions" slot-scope="record">
<a-tag color="cyan" v-for="(r, index) in record" :key="index">{{ r }}</a-tag>
</template>
</a-table> -->
</a-spin>
</a-tab-pane>
</a-tabs>
</a-tab-pane>
</a-tabs>
</CustomDrawer>
</template>
<script>
import { mapState } from 'vuex'
import { searchApp, searchRole } from '@/modules/acl/api/app'
import { searchResourceType } from '@/modules/acl/api/resource'
import { searchPermResourceByRoleId } from '@/modules/acl/api/permission'
export default {
name: 'PermCollectForm',
data() {
return {
spinning: false,
visible: false,
user: {},
roles: [],
apps: [],
currentAppId: 0,
currentResourceId: 0,
resourceTypes: [],
resourceTypePerms: [],
resources: [],
// tableColumns: [
// {
// title: '资源名',
// dataIndex: 'name',
// sorter: false,
// width: 150,
// // scopedSlots: {
// // customRender: 'nameSearchRender',
// // filterDropdown: 'filterDropdown',
// // filterIcon: 'filterIcon'
// // },
// // onFilter: (value, record) => record.name && record.name.toLowerCase().includes(value.toLowerCase()),
// // onFilterDropdownVisibleChange: (visible) => {
// // if (visible) {
// // setTimeout(() => {
// // this.searchInput.focus()
// // }, 0)
// // }
// // }
// },
// {
// title: '权限列表',
// dataIndex: 'permissions',
// width: 300,
// scopedSlots: { customRender: 'permissions' },
// },
// ],
columnSearchText: {
name: '',
},
}
},
computed: {
...mapState({
windowHeight: state => state.windowHeight,
}),
displayApps() {
const roles = this.$store.state.user.roles.permissions
if (roles.includes('acl_admin')) {
return this.apps
}
return this.apps.filter(item => {
if (roles.includes(`${item.name}_admin`)) {
return true
}
return false
})
},
},
async mounted() {
await this.loadApps()
},
methods: {
collect(user) {
this.user = user
this.visible = true
setTimeout(() => {
this.loadResource()
}, 500)
},
handleSearch(selectedKeys, confirm, column) {
confirm()
this.columnSearchText[column.dataIndex] = selectedKeys[0]
},
handleReset(clearFilters, column) {
clearFilters()
this.columnSearchText[column.dataIndex] = ''
},
async loadRoles(_appId) {
const res = await searchRole({ app_id: _appId, page_size: 9999, is_all: true })
this.roles = res.roles.filter(item => item.uid)
},
async handleSwitchApp(appId) {
this.currentAppId = appId
await this.loadResourceTypes(appId)
},
async loadApps() {
const res = await searchApp()
this.apps = res.apps
if (this.displayApps[0]) {
this.currentAppId = this.displayApps[0].id
await this.loadRoles(this.apps[0].id)
await this.loadResourceTypes(this.displayApps[0].id)
} else {
this.$message.info('No apps!')
}
},
async loadResourceTypes(appId) {
const res = await searchResourceType({ app_id: appId, page_size: 9999 })
this.resourceTypes = res['groups']
this.resourceTypePerms = res['id2perms'][`${appId}`]
this.currentResourceId = res['groups'][0] && res['groups'][0]['id']
await this.loadResource()
},
async loadResource() {
this.spinning = true
const fil = this.roles.filter(role => role.uid === this.user.uid)
if (!fil[0]) {
return
}
const res = await searchPermResourceByRoleId(fil[0].id, {
resource_type_id: this.currentResourceId,
app_id: this.currentAppId,
})
this.resources = res['resources']
this.spinning = false
},
filterNameMethod({ option, row }) {
return row.name.toLowerCase().includes(option.data.toLowerCase())
},
filterNameRecoverMethod({ option }) {
option.data = ''
},
},
}
</script>
<style lang="less" scoped>
.search {
margin-bottom: 54px;
}
.fold {
width: calc(100% - 216px);
display: inline-block;
}
.operator {
margin-bottom: 18px;
}
.action-btn {
margin-bottom: 1rem;
}
.custom-filter-dropdown {
padding: 8px;
border-radius: 4px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.highlight {
background-color: rgb(255, 192, 105);
padding: 0;
}
</style>

View File

@@ -0,0 +1,199 @@
<template>
<CustomDrawer
:closable="false"
:title="drawerTitle"
:visible="drawerVisible"
@close="onClose"
placement="right"
width="30%"
>
<a-form :form="form" :layout="formLayout" @submit="handleSubmit">
<a-form-item :label-col="formItemLayout.labelCol" :wrapper-col="formItemLayout.wrapperCol" label="类型名">
<a-input
name="name"
placeholder=""
v-decorator="['name', { rules: [{ required: true, message: '请输入资源名' }] }]"
/>
</a-form-item>
<a-form-item :label-col="formItemLayout.labelCol" :wrapper-col="formItemLayout.wrapperCol" label="描述">
<a-textarea placeholder="请输入描述信息..." name="description" :rows="4" />
</a-form-item>
<a-form-item :label-col="formItemLayout.labelCol" :wrapper-col="formItemLayout.wrapperCol" label="权限">
<div :style="{ borderBottom: '1px solid #E9E9E9' }">
<a-checkbox :indeterminate="indeterminate" @change="onCheckAllChange" :checked="checkAll">
全选
</a-checkbox>
</div>
<br />
<a-checkbox-group :options="plainOptions" v-model="perms" @change="onPermChange" />
</a-form-item>
<a-form-item>
<a-input name="id" type="hidden" v-decorator="['id', { rules: [] }]" />
</a-form-item>
<div class="custom-drawer-bottom-action">
<a-button @click="onClose">取消</a-button>
<a-button @click="handleSubmit" type="primary">确定</a-button>
</div>
</a-form>
</CustomDrawer>
</template>
<script>
import { addResourceType, updateResourceTypeById } from '@/modules/acl/api/resource'
export default {
name: 'ResourceForm',
data() {
return {
drawerTitle: '新增资源类型',
drawerVisible: false,
formLayout: 'vertical',
perms: ['1'],
indeterminate: true,
checkAll: false,
plainOptions: ['1', '2'],
}
},
beforeCreate() {
this.form = this.$form.createForm(this)
},
computed: {
formItemLayout() {
const { formLayout } = this
return formLayout === 'horizontal'
? {
labelCol: { span: 4 },
wrapperCol: { span: 14 },
}
: {}
},
horizontalFormItemLayout() {
return {
labelCol: { span: 5 },
wrapperCol: { span: 12 },
}
},
buttonItemLayout() {
const { formLayout } = this
return formLayout === 'horizontal'
? {
wrapperCol: { span: 14, offset: 4 },
}
: {}
},
},
mounted() {},
methods: {
onPermChange(perms) {
this.indeterminate = !!perms.length && perms.length < this.plainOptions.length
this.checkAll = perms.length === this.plainOptions.length
},
onCheckAllChange(e) {
Object.assign(this, {
perms: e.target.checked ? this.plainOptions : [],
indeterminate: false,
checkAll: e.target.checked,
})
},
handleCreate() {
this.drawerVisible = true
},
onClose() {
this.form.resetFields()
this.drawerVisible = false
},
onChange(e) {
console.log(`checked = ${e}`)
},
handleEdit(record) {
this.drawerVisible = true
console.log(record)
this.$nextTick(() => {
this.form.setFieldsValue({
id: record.id,
name: record.name,
description: record.description,
})
})
},
handleSubmit(e) {
e.preventDefault()
this.form.validateFields((err, values) => {
if (!err) {
console.log('Received values of form: ', values)
values.app_id = this.$route.name.split('_')[0]
values.perms = this.perms
if (values.id) {
this.updateResourceType(values.id, values)
} else {
this.createResourceType(values)
}
}
})
},
updateResourceType(id, data) {
updateResourceTypeById(id, data).then(res => {
this.$message.success(`更新成功`)
this.handleOk()
this.onClose()
})
// .catch(err => this.requestFailed(err))
},
createResourceType(data) {
addResourceType(data).then(res => {
this.$message.success(`添加成功`)
this.handleOk()
this.onClose()
})
// .catch(err => this.requestFailed(err))
},
// requestFailed (err) {
// const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
// this.$message.error(`${msg}`)
// }
},
watch: {},
props: {
handleOk: {
type: Function,
default: null,
},
},
}
</script>
<style lang="less" scoped>
.search {
margin-bottom: 54px;
}
.fold {
width: calc(100% - 216px);
display: inline-block;
}
.operator {
margin-bottom: 18px;
}
.action-btn {
margin-bottom: 1rem;
}
@media screen and (max-width: 900px) {
.fold {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,308 @@
<template>
<div>
<search-form
ref="child"
:attrList="permissionTableAttrList"
@search="handleSearch"
@expandChange="handleExpandChange"
@searchFormReset="searchFormReset"
@loadMoreData="loadMoreResources"
@fetchData="fetchResources"
@resourceClear="resourceClear"
></search-form>
<vxe-table
ref="xTable"
border
resizable
size="small"
:data="tableData"
:loading="loading"
:max-height="`${windowHeight - windowHeightMinus}px`"
:scroll-y="{ enabled: false }"
>
<vxe-column field="created_at" width="144px" title="操作时间">
<template #default="{ row }">
<span>{{ row.deleted_at || row.updated_at || row.created_at }}</span>
</template>
</vxe-column>
<vxe-column field="operate_uid" width="130px" title="操作员"></vxe-column>
<vxe-column field="operate_type" width="80px" title="操作">
<template #default="{ row }">
<a-tag :color="row.operate_type === 'grant' ? 'green' : 'red'">{{
operateTypeMap.get(row.operate_type)
}}</a-tag>
</template>
</vxe-column>
<vxe-column field="rid" title="用户"></vxe-column>
<vxe-column field="resource_type_id" title="资源类型"></vxe-column>
<vxe-column field="resources" title="资源">
<template #default="{ row }">
<template v-if="row.resource_ids.length > 0">
<a-tooltip placement="top">
<template slot="title">
<span>{{ row.resource_ids[0] }}</span>
</template>
<a-tag color="blue" v-for="(resource, index) in row.resource_ids" :key="'resources_' + resource + index">
{{ resource }}
</a-tag>
</a-tooltip>
</template>
<template v-else-if="row.group_ids.length > 0">
<a-tag color="blue" v-for="(group, index) in row.group_ids" :key="'groups_' + group + index">
{{ group }}
</a-tag>
</template>
</template>
</vxe-column>
<vxe-column title="权限">
<template #default="{ row }">
<a-tag v-for="(perm, index) in row.permission_ids" :key="'perms_' + perm + index">
{{ perm }}
</a-tag>
</template>
</vxe-column>
<vxe-column field="source" width="100px" title="来源"></vxe-column>
</vxe-table>
<pager
:current-page.sync="queryParams.page"
:page-size.sync="queryParams.page_size"
:page-sizes="[50, 100, 200]"
:total="tableDataLength"
:isLoading="loading"
@change="onChange"
@showSizeChange="onShowSizeChange"
></pager>
</div>
</template>
<script>
import Pager from './pager.vue'
import SearchForm from './searchForm.vue'
import { searchPermissonHistory } from '@/modules/acl/api/history'
import debounce from 'lodash/debounce'
export default {
components: { SearchForm, Pager },
props: {
allResourceTypes: {
type: Array,
required: true,
},
allResources: {
type: Array,
required: true,
},
allUsers: {
type: Array,
required: true,
},
allRoles: {
type: Array,
required: true,
},
allRolesMap: {
type: Map,
required: true,
},
allUsersMap: {
type: Map,
required: true,
},
allResourceTypesMap: {
type: Map,
required: true,
},
},
data() {
this.fetchResources = debounce(this.fetchResources, 800)
return {
isExpand: false,
loading: true,
app_id: this.$route.name.split('_')[0],
tableData: [],
operateTypeMap: new Map([
['grant', '授权'],
['revoke', '撤销'],
]),
queryParams: {
page: 1,
page_size: 50,
app_id: this.$route.name.split('_')[0],
start: '',
end: '',
},
permissionTableAttrList: [
{
alias: '日期',
is_choice: false,
name: 'datetime',
value_type: '3',
},
{
alias: '操作员',
is_choice: true,
name: 'operate_uid',
value_type: '2',
choice_value: this.allUsers,
},
{
alias: '用户',
is_choice: true,
name: 'rid',
value_type: '2',
choice_value: this.allRoles,
},
{
alias: '资源类型',
is_choice: true,
name: 'resource_type_id',
value_type: '2',
choice_value: this.allResourceTypes,
},
{
alias: '资源',
is_choice: true,
name: 'resource_id',
value_type: '2',
choice_value: this.allResources,
},
{
alias: '操作',
is_choice: true,
name: 'operate_type',
value_type: '2',
choice_value: [{ 授权: 'grant' }, { 撤销: 'revoke' }],
},
],
}
},
async created() {
await this.getTable(this.queryParams)
},
updated() {
this.$refs.xTable.$el.querySelector('.vxe-table--body-wrapper').scrollTop = 0
},
watch: {
'$route.name': async function(oldName, newName) {
this.app_id = this.$route.name.split('_')[0]
await this.getTable(this.queryParams)
},
allResources: {
deep: true,
immediate: true,
handler(val) {
this.permissionTableAttrList[4].choice_value = val
},
},
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
windowHeightMinus() {
return this.isExpand ? 396 : 331
},
tableDataLength() {
return this.tableData.length
},
},
methods: {
// 获取数据
async getTable(queryParams) {
try {
this.loading = true
const { data, id2groups, id2perms, id2resources, id2roles } = await searchPermissonHistory(
this.handleQueryParams(queryParams)
)
data.forEach((item) => {
item.operate_uid = this.allUsersMap.get(item.operate_uid)
item.rid = id2roles[item.rid].name
item.resource_type_id = this.allResourceTypesMap.get(item.resource_type_id)
item.resource_ids.forEach((subItem, index) => {
item.resource_ids[index] = id2resources[subItem].name
})
item.group_ids.forEach((subItem, index) => {
item.group_ids[index] = id2groups[subItem].name
})
item.permission_ids.forEach((subItem, index) => {
item.permission_ids[index] = id2perms[subItem].name
})
})
this.tableData = data
} finally {
this.loading = false
}
},
loadMoreResources(name, value) {
if (name === 'resource_id') {
this.$emit('loadMoreResources', value)
}
},
resourceClear() {
this.$emit('resourceClear')
},
fetchResources(value) {
if (value === '') {
this.$emit('reloadResources')
return
}
this.$emit('fetchResources', value)
},
// 处理查询参数
handleQueryParams(queryParams) {
let q = ''
for (const key in queryParams) {
if (
key !== 'page' &&
key !== 'page_size' &&
key !== 'app_id' &&
key !== 'start' &&
key !== 'end' &&
queryParams[key] !== undefined
) {
if (q) q += `,${key}:${queryParams[key]}`
else q += `${key}:${queryParams[key]}`
delete queryParams[key]
}
}
const newQueryParams = { ...queryParams, q }
return q ? newQueryParams : queryParams
},
// searchForm相关
handleExpandChange(expand) {
this.isExpand = expand
},
searchFormReset() {
this.queryParams = {
page: 1,
page_size: 50,
app_id: this.$route.name.split('_')[0],
}
this.getTable(this.queryParams)
},
handleSearch(queryParams) {
this.queryParams = { ...queryParams, app_id: this.app_id }
this.getTable(this.queryParams)
},
// pager相关
onShowSizeChange(size) {
this.queryParams.page_size = size
this.queryParams.page = 1
this.getTable(this.queryParams)
},
onChange(pageNum) {
this.queryParams.page = pageNum
this.getTable(this.queryParams)
},
},
}
</script>
<style lang="less" scoped>
.ant-tag {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
</style>

View File

@@ -0,0 +1,148 @@
<template>
<CustomDrawer
title="便捷授权"
width="500px"
:maskClosable="false"
:closable="true"
:visible="visible"
@close="handleClose"
>
<a-form :form="form">
<a-form-item>
<div slot="label" style="display: inline-block">
<span>角色列表</span>
<a-divider type="vertical" />
<a-switch
style="display: inline-block"
checked-children="用户"
un-checked-children="虚拟"
@change="handleRoleTypeChange"
v-model="roleType"
/>
</div>
<el-select
:style="{ width: '100%' }"
size="small"
v-decorator="['roleIdList', { rules: [{ required: true, message: '请选择角色名称' }] }]"
multiple
filterable
placeholder="请选择角色名称,可多选!"
>
<el-option v-for="role in allRoles" :key="role.id" :value="role.id" :label="role.name"></el-option>
</el-select>
</a-form-item>
<a-form-item label="权限列表">
<el-select
:style="{ width: '100%' }"
size="small"
name="permName"
v-decorator="['permName', { rules: [{ required: true, message: '请选择权限' }] }]"
multiple
placeholder="请选择权限,可多选!"
>
<el-option v-for="perm in allPerms" :key="perm.name" :value="perm.name" :label="perm.name"></el-option>
</el-select>
</a-form-item>
<a-form-item label="资源名">
<a-textarea
v-decorator="['resource_names', { rules: [{ required: true, message: '请输入资源名,换行分隔' }] }]"
:autoSize="{ minRows: 4 }"
placeholder="请输入资源名,换行分隔"
/>
</a-form-item>
<div class="custom-drawer-bottom-action">
<a-button @click="handleRevoke" type="danger" ghost>权限回收</a-button>
<a-button @click="handleSubmit" type="primary">授权</a-button>
</div>
</a-form>
</CustomDrawer>
</template>
<script>
import { Select, Option } from 'element-ui'
import { searchRole } from '@/modules/acl/api/role'
import {
getResourceTypePerms,
setBatchRoleResourceByResourceName,
setBatchRoleResourceRevokeByResourceName,
} from '@/modules/acl/api/permission'
export default {
name: 'ResourceBatchPerm',
components: { ElSelect: Select, ElOption: Option },
data() {
return {
visible: false,
resource_type_id: null,
roleType: true,
allRoles: [],
allPerms: [],
}
},
beforeCreate() {
this.form = this.$form.createForm(this)
},
methods: {
open(currentTypeId) {
this.visible = true
this.resource_type_id = currentTypeId
this.loadRoles(1)
this.loadPerm(currentTypeId)
},
handleClose() {
this.form.resetFields()
this.roleType = true
this.visible = false
},
handleRoleTypeChange(target) {
this.loadRoles(Number(target))
},
loadRoles(isUserRole) {
searchRole({ page_size: 9999, app_id: this.$route.name.split('_')[0], user_role: isUserRole }).then(res => {
this.allRoles = res.roles
})
},
loadPerm(resourceTypeId) {
getResourceTypePerms(resourceTypeId).then(res => {
this.allPerms = res
})
},
handleSubmit() {
this.form.validateFields((err, values) => {
if (!err) {
console.log(values)
values.roleIdList.forEach(roleId => {
setBatchRoleResourceByResourceName(roleId, {
resource_names: values.resource_names.split('\n'),
perms: values.permName,
resource_type_id: this.resource_type_id,
}).then(res => {
this.$message.success('授权成功')
this.form.resetFields()
})
})
}
})
},
handleRevoke() {
this.form.validateFields((err, values) => {
if (!err) {
console.log(values)
values.roleIdList.forEach(roleId => {
setBatchRoleResourceRevokeByResourceName(roleId, {
resource_names: values.resource_names.split('\n'),
perms: values.permName,
resource_type_id: this.resource_type_id,
}).then(res => {
this.$message.success('权限回收成功')
this.form.resetFields()
})
})
}
})
},
},
}
</script>
<style></style>

View File

@@ -0,0 +1,151 @@
<template>
<CustomDrawer
:closable="false"
:title="drawerTitle"
:visible="drawerVisible"
@close="onClose"
placement="right"
width="30%"
>
<a-form :form="form" @submit="handleSubmit" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<a-form-item label="资源名">
<a-input
name="name"
placeholder=""
v-decorator="['name', { rules: [{ required: true, message: '请输入资源名' }] }]"
/>
</a-form-item>
<a-form-item label="资源类型">
<a-select v-model="selectedTypeId">
<a-select-option v-for="type in allTypes" :key="type.id">{{ type.name }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="是否组">
<a-radio-group v-model="isGroup">
<a-radio :value="true"> </a-radio>
<a-radio :value="false"> </a-radio>
</a-radio-group>
</a-form-item>
<a-form-item>
<a-input name="id" type="hidden" v-decorator="['id', { rules: [] }]" />
</a-form-item>
<div class="custom-drawer-bottom-action">
<a-button @click="onClose">取消</a-button>
<a-button @click="handleSubmit" type="primary">确定</a-button>
</div>
</a-form>
</CustomDrawer>
</template>
<script>
import { addResource, searchResourceType, addResourceGroup } from '@/modules/acl/api/resource'
export default {
name: 'ResourceForm',
components: {},
data() {
return {
drawerTitle: '新增资源',
drawerVisible: false,
allTypes: [],
isGroup: false,
selectedTypeId: 0,
}
},
beforeCreate() {
this.form = this.$form.createForm(this)
},
created() {
this.getAllResourceTypes()
},
computed: {},
mounted() {},
methods: {
getAllResourceTypes() {
searchResourceType({ page_size: 9999, app_id: this.$route.name.split('_')[0] }).then((res) => {
this.allTypes = res.groups
})
},
handleCreate(defaultType) {
this.drawerVisible = true
this.$nextTick(() => {
this.selectedTypeId = defaultType.id
})
},
onClose() {
this.form.resetFields()
this.drawerVisible = false
this.$emit('fresh')
},
handleSubmit(e) {
e.preventDefault()
this.form.validateFields((err, values) => {
if (!err) {
console.log('Received values of form: ', values)
values.type_id = this.selectedTypeId
values.app_id = this.$route.name.split('_')[0]
if (values.id) {
this.$message.error('错误提示')
} else {
this.createResource(values)
}
}
})
},
createResource(data) {
if (!this.isGroup) {
addResource(data).then((res) => {
this.$message.success(`添加成功`)
this.onClose()
})
// .catch(err => this.requestFailed(err))
} else {
addResourceGroup(data).then((res) => {
this.$message.success(`添加成功`)
this.onClose()
})
// .catch(err => this.requestFailed(err))
}
},
// requestFailed(err) {
// const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
// this.$message.error(`${msg}`)
// },
},
watch: {
'$route.name': function (newValue, oldValue) {
this.getAllResourceTypes()
},
},
}
</script>
<style lang="less" scoped>
.search {
margin-bottom: 54px;
}
.fold {
width: calc(100% - 216px);
display: inline-block;
}
.operator {
margin-bottom: 18px;
}
.action-btn {
margin-bottom: 1rem;
}
@media screen and (max-width: 900px) {
.fold {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,35 @@
<template>
<a-modal v-model="visible" :title="`组成员:${editRecord.name}`" :width="800" :footer="null">
<div :style="{ maxHeight: '500px', overflow: 'auto' }">
<a-tag :style="{ marginBottom: '5px' }" v-for="mem in members" :key="mem.name">
{{ mem.name }}
</a-tag>
</div>
</a-modal>
</template>
<script>
import { getResourceGroupItems } from '@/modules/acl/api/resource'
export default {
name: 'ResourceGroupMember',
data() {
return {
visible: false,
editRecord: {},
members: [],
}
},
methods: {
handleEdit(record) {
this.visible = true
this.editRecord = record
this.loadMembers(record.id)
},
loadMembers(_id) {
getResourceGroupItems(_id).then((res) => {
this.members = res
})
// .catch(err => this.$httpError(err))
},
},
}
</script>

View File

@@ -0,0 +1,241 @@
<template>
<a-modal v-model="visible" :title="`成员管理:${editRecord.name}`" @ok="handleSubmit" :width="690">
<!-- <CustomTransfer
ref="customTransfer"
:show-search="true"
:data-source="resources"
:filter-option="filterOption"
:target-keys="targetKeys"
:render="(item) => item.title"
@change="handleChange"
@search="handleSearch"
@selectChange="selectChange"
>
</CustomTransfer> -->
<a-transfer
:dataSource="resources"
:showSearch="true"
:listStyle="{
width: '300px',
height: '450px',
}"
:titles="[]"
:render="(item) => item.title"
:targetKeys="targetKeys"
@change="handleChange"
@selectChange="selectChange"
:selectedKeys="selectedKeys"
>
<span slot="notFoundContent">暂无数据</span>
<template slot="children" slot-scope="{ props: { direction, filteredItems } }">
<div class="ant-transfer-list-content" v-if="direction === 'right'">
<div
@dblclick="(e) => changeSingleItem(e, item)"
v-for="item in filteredItems"
:key="item.key"
:style="{ height: '32px' }"
>
<li
:class="
`ant-transfer-list-content-item ${
selectedKeys.includes(item.key) ? 'ant-transfer-list-content-item-selected' : ''
}`
"
style="padding:0 12px 0 12px;position:relative"
@click="setSelectedKeys(item)"
>
<div :class="`ant-transfer-list-content-item-text`" style="display:inline">
{{ item.title }}
<div class="ant-transfer-list-icon" @click="(e) => changeSingleItem(e, item)">
<a-icon type="left" />
</div>
</div>
</li>
</div>
</div>
<div v-if="direction === 'left'" class="ant-transfer-list-content">
<div
@dblclick="(e) => changeSingleItem(e, item)"
v-for="item in filteredItems"
:key="item.key"
:style="{ height: '32px' }"
>
<li
:class="
`ant-transfer-list-content-item ${
selectedKeys.includes(item.key) ? 'ant-transfer-list-content-item-selected' : ''
}`
"
style="padding:0 12px 0 12px;position:relative"
@click="setSelectedKeys(item)"
>
<div class="ant-transfer-list-content-item-text" style="display:inline">
{{ item.title }}
<div @click="(e) => changeSingleItem(e, item)" class="ant-transfer-list-icon">
<a-icon type="right" />
</div>
</div>
</li>
</div>
</div>
</template>
</a-transfer>
</a-modal>
</template>
<script>
import _ from 'lodash'
import { updateResourceGroup, searchResource, getResourceGroupItems } from '@/modules/acl/api/resource'
export default {
name: 'ResourceGroupModal',
data() {
return {
visible: false,
editRecord: {},
resources: [],
targetKeys: [],
selectedKeys: [],
}
},
methods: {
handleSubmit() {
const items = this.targetKeys.map((key) => {
return this.resources.filter((item) => item.name === key)[0]['id']
})
updateResourceGroup(this.editRecord['id'], { items: items.join(',') }).then(() => {
this.visible = false
this.$message.success('更新成功!')
})
// .catch(err => this.$httpError(err))
},
// filterOption(inputValue, option) {
// return option.description.indexOf(inputValue) > -1
// },
// handleChange(targetKeys, direction, moveKeys) {
// console.log(JSON.parse(JSON.stringify(targetKeys)))
// this.targetKeys = targetKeys
// },
// handleSearch(dir, value) {
// console.log('search:', dir, value)
// },
async handleEdit(record) {
this.editRecord = record
this.visible = true
this.selectedKeys = []
await this.loadChildren(record.id)
await this.loadResource()
},
loadChildren(_id) {
getResourceGroupItems(_id).then((res) => {
this.targetKeys = res.map((item) => item.name)
})
// .catch(err => this.$httpError(err))
},
loadResource() {
const params = {
app_id: this.editRecord['app_id'],
resource_type_id: this.editRecord['resource_type_id'],
page_size: 9999,
}
searchResource(params).then((res) => {
this.resources = res['resources'].map((item) => {
return {
id: item.id,
name: item.name,
key: item.name,
description: item.name,
title: item.name,
}
})
})
// .catch(err => this.$httpError(err))
},
setSelectedKeys(item) {
const idx = this.selectedKeys.findIndex((key) => key === item.key)
if (idx > -1) {
this.selectedKeys.splice(idx, 1)
} else {
this.selectedKeys.push(item.key)
}
},
changeSingleItem(e, item) {
e.stopPropagation()
e.preventDefault()
const idx = this.targetKeys.findIndex((key) => key === item.key)
if (idx > -1) {
this.targetKeys.splice(idx, 1)
} else {
this.targetKeys.push(item.key)
}
},
handleChange(targetKeys, direction, moveKeys) {
const _selectedKeys = _.cloneDeep(this.selectedKeys)
moveKeys.forEach((key) => {
const idx = _selectedKeys.findIndex((selected) => selected === key)
if (idx > -1) {
_selectedKeys.splice(idx, 1)
}
})
this.selectedKeys = _.cloneDeep(_selectedKeys)
this.targetKeys = targetKeys
},
selectChange(sourceSelectedKeys, targetSelectedKeys) {
const _selectedKeys = _.cloneDeep(this.selectedKeys)
const list = [
{ data: sourceSelectedKeys, name: 'source' },
{ data: targetSelectedKeys, name: 'target' },
]
list.forEach((item) => {
if (!item.data.__ob__) {
if (item.data.length) {
item.data.forEach((key) => {
const idx = _selectedKeys.findIndex((selected) => selected === key)
if (idx > -1) {
} else {
_selectedKeys.push(key)
}
})
this.selectedKeys = _.cloneDeep(_selectedKeys)
} else {
let _list = []
if (item.name === 'source') {
_list = _selectedKeys.filter((key) => this.targetKeys.includes(key))
} else {
_list = _selectedKeys.filter((key) => !this.targetKeys.includes(key))
}
this.selectedKeys = _list
}
}
})
},
},
}
</script>
<style lang="less">
.ant-transfer-list-body-customize-wrapper {
padding: 0 !important;
height: 100%;
max-height: calc(100% - 44px);
}
.ant-transfer-list-content-item {
transition: all 0.3s;
.ant-transfer-list-icon {
position: absolute;
top: 4px;
right: 4px;
display: none;
&:hover {
color: #1f78d1;
}
}
&:hover .ant-transfer-list-icon {
display: inline;
background-color: #c0eaff;
border-radius: 4px;
}
}
.ant-transfer-list-content-item-selected {
background-color: #f0faff;
}
</style>

View File

@@ -0,0 +1,326 @@
<template>
<div>
<search-form
ref="child"
:attrList="resourceTableAttrList"
:hasSwitch="true"
switchValue=""
@onSwitchChange="onSwitchChange"
@search="handleSearch"
@searchFormReset="searchFormReset"
@loadMoreData="loadMoreResources"
@fetchData="fetchResources"
@resourceClear="resourceClear"
></search-form>
<vxe-table
ref="xTable"
border
size="small"
:loading="loading"
:data="tableData"
resizable
:max-height="`${windowHeight - 331}px`"
>
<vxe-column field="created_at" width="144px" title="操作时间"></vxe-column>
<vxe-column field="operate_uid" width="130px" title="操作员"></vxe-column>
<vxe-column field="operate_type" width="80px" title="操作">
<template #default="{ row }">
<a-tag :color="handleTagColor(row.operate_type)">
{{ operateTypeMap.get(row.operate_type) }}
</a-tag>
</template>
</vxe-column>
<vxe-column field="link_id" title="资源名">
<template #default="{ row }">
<span>
{{ row.current.name || row.origin.name }}
</span>
</template>
</vxe-column>
<vxe-column title="描述">
<template #default="{ row }">
<p>
{{ row.description }}
</p>
</template>
</vxe-column>
<vxe-column field="source" width="100px" title="来源"></vxe-column>
</vxe-table>
<pager
:current-page.sync="queryParams.page"
:page-size.sync="queryParams.page_size"
:page-sizes="[50, 100, 200]"
:total="tableDataLength"
:isLoading="loading"
@change="onChange"
@showSizeChange="onShowSizeChange"
></pager>
</div>
</template>
<script>
import _ from 'lodash'
import Pager from './pager.vue'
import SearchForm from './searchForm.vue'
import { searchResourceHistory } from '@/modules/acl/api/history'
export default {
components: { SearchForm, Pager },
props: {
allResources: {
type: Array,
required: true,
},
allUsers: {
type: Array,
required: true,
},
allRoles: {
type: Array,
required: true,
},
allRolesMap: {
type: Map,
required: true,
},
allUsersMap: {
type: Map,
required: true,
},
allResourcesMap: {
type: Map,
required: true,
},
},
data() {
return {
loading: true,
checked: false,
tableData: [],
app_id: this.$route.name.split('_')[0],
resourceTableAttrList: [
{
alias: '日期',
is_choice: false,
name: 'datetime',
value_type: '3',
},
{
alias: '操作员',
is_choice: true,
name: 'operate_uid',
value_type: '2',
choice_value: this.allUsers,
},
{
alias: '操作',
is_choice: true,
name: 'operate_type',
value_type: '2',
choice_value: [{ 新建: 'create' }, { 修改: 'update' }, { 删除: 'delete' }],
},
{
alias: '资源名',
is_choice: true,
name: 'link_id',
value_type: '2',
choice_value: this.allResources,
},
],
operateTypeMap: new Map([
['create', '新建'],
['update', '修改'],
['delete', '删除'],
]),
colorMap: new Map([
['create', 'green'],
['update', 'orange'],
['delete', 'red'],
]),
queryParams: {
page: 1,
page_size: 50,
app_id: this.$route.name.split('_')[0],
scope: 'resource',
start: '',
end: '',
},
}
},
async created() {
await this.getTable(this.queryParams)
},
updated() {
this.$refs.xTable.$el.querySelector('.vxe-table--body-wrapper').scrollTop = 0
},
watch: {
'$route.name': async function(oldName, newName) {
this.app_id = this.$route.name.split('_')[0]
await this.getTable(this.queryParams)
},
allResources: {
deep: true,
handler(val) {
this.resourceTableAttrList[3].choice_value = val
},
},
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
tableDataLength() {
return this.tableData.length
},
},
methods: {
async getTable(queryParams) {
try {
this.loading = true
const { data } = await searchResourceHistory(this.handleQueryParams(queryParams))
data.forEach((item) => {
item.originResource_ids = item?.extra?.resource_ids?.origin.map((subItem) =>
this.allResourcesMap.get(Number(subItem))
)
item.currentResource_ids = item?.extra?.resource_ids?.current.map((subItem) =>
this.allResourcesMap.get(Number(subItem))
)
this.handleChangeDescription(item, item.operate_type)
item.operate_uid = this.allUsersMap.get(item.operate_uid)
})
this.tableData = data
} finally {
this.loading = false
}
},
loadMoreResources(name, value) {
if (name === 'link_id') {
this.$emit('loadMoreResources', value)
}
},
resourceClear() {
this.$emit('resourceClear')
},
fetchResources(value) {
if (value === '') {
this.$emit('reloadResources')
return
}
this.$emit('fetchResources', value)
},
// searchForm相关
handleSearch(queryParams) {
this.queryParams = { ...queryParams, app_id: this.app_id, scope: this.checked ? 'resource_group' : 'resource' }
this.getTable(this.queryParams)
},
searchFormReset() {
this.$refs.child.checked = false
this.checked = false
this.queryParams = {
page: 1,
page_size: 50,
app_id: this.$route.name.split('_')[0],
scope: this.checked ? 'resource_group' : 'resource',
}
this.getTable(this.queryParams)
},
onSwitchChange(checked) {
this.checked = checked
this.queryParams.scope = checked ? 'resource_group' : 'resource'
this.queryParams.page = 1
this.getTable(this.queryParams)
},
// pager相关
onShowSizeChange(size) {
this.queryParams.page_size = size
this.queryParams.page = 1
this.getTable(this.queryParams)
},
onChange(pageNum) {
this.queryParams.page = pageNum
this.getTable(this.queryParams)
},
handleQueryParams(queryParams) {
let flag = false
let q = queryParams.q ? queryParams.q : ''
for (const key in queryParams) {
if (
key !== 'page' &&
key !== 'page_size' &&
key !== 'app_id' &&
key !== 'q' &&
key !== 'start' &&
key !== 'end' &&
queryParams[key] !== undefined
) {
flag = true
if (q) q += `,${key}:${queryParams[key]}`
else q += `${key}:${queryParams[key]}`
delete queryParams[key]
}
}
const newQueryParams = { ...queryParams, q }
return flag ? newQueryParams : queryParams
},
handleTagColor(operateType) {
return this.colorMap.get(operateType)
},
handleChangeDescription(item, operate_type) {
switch (operate_type) {
// create
case 'create': {
item.description = `新建资源${item.current.name}`
break
}
case 'update': {
item.description = ''
for (const key in item.origin) {
const newVal = item.current[key]
const oldVal = item.origin[key]
if (!_.isEqual(newVal, oldVal) && key !== 'updated_at' && key !== 'deleted_at' && key !== 'created_at') {
if (oldVal === null) {
item.description += ` ${key} : 改为 ${newVal} `
} else {
item.description += ` ${key} : ${oldVal} 改为 ${newVal} `
}
}
}
const originResource_ids = item.originResource_ids
const currentResource_ids = item.currentResource_ids
if (!_.isEqual(originResource_ids, currentResource_ids)) {
if (originResource_ids.length === 0) {
const str = ` resource_ids : 新增 ${currentResource_ids} `
item.description += str
} else {
const str = ` resource_ids : ${originResource_ids} 改为 ${currentResource_ids} `
item.description += str
}
}
if (!item.description) item.description = '没有修改'
break
}
case 'delete': {
item.description = `删除资源${item.origin.name}`
break
}
}
},
},
}
</script>
<style lang="less" scoped>
.row {
margin-top: 5px;
}
.ant-tag {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
p {
margin-bottom: 0;
}
</style>

View File

@@ -0,0 +1,272 @@
<template>
<CustomDrawer
:title="drawerTitle"
:visible="drawerVisible"
width="80%"
placement="left"
@close="() => (drawerVisible = false)"
:hasFooter="false"
>
<vxe-table :max-height="`${windowHeight - 150}px`" :data="resPerms" ref="rTable">
<vxe-column
field="name"
title="角色名"
width="20%"
:filters="[{ data: '' }]"
:filter-method="filterNameMethod"
:filter-recover-method="filterNameRecoverMethod"
>
<template #filter="{ $panel, column }">
<template v-for="(option, index) in column.filters">
<input
class="my-input"
type="type"
:key="index"
v-model="option.data"
@input="$panel.changeOption($event, !!option.data, option)"
@keyup.enter="$panel.confirmFilter()"
placeholder="按回车确认筛选"
/>
</template>
</template>
</vxe-column>
<vxe-column field="users" title="下属用户" width="35%">
<template #default="{row}">
<a-tag color="green" v-for="user in row.users" :key="user.nickname">
{{ user.nickname }}
</a-tag>
</template>
</vxe-column>
<vxe-column field="perms" title="权限列表" width="35%">
<template #default="{row}">
<a-tag
closable
color="cyan"
v-for="perm in row.perms"
:key="perm.name"
@close="deletePerm(perm.rid, perm.name)"
>
{{ perm.name }}
</a-tag>
</template>
</vxe-column>
<vxe-column field="operate" title="批量操作">
<template #default="{row}">
<a-button size="small" type="danger" @click="handleClearAll(row)">
清空
</a-button>
</template>
</vxe-column>
</vxe-table>
<!-- <a-table
:columns="columns"
:dataSource="resPerms"
:rowKey="record => record.name"
:pagination="{ showTotal: (total, range) => `${range[0]}-${range[1]} 共 ${total} 条记录` }"
showPagination="auto"
ref="rTable"
size="middle"
>
<div slot="perms" slot-scope="text">
<a-tag closable color="cyan" v-for="perm in text" :key="perm.name" @close="deletePerm(perm.rid, perm.name)">
{{ perm.name }}
</a-tag>
</div>
<div slot="users" slot-scope="text">
<a-tag color="green" v-for="user in text" :key="user.nickname">
{{ user.nickname }}
</a-tag>
</div>
<div slot="operate" slot-scope="text">
<a-button size="small" type="danger" @click="handleClearAll(text)">
清空
</a-button>
</div>
</a-table> -->
</CustomDrawer>
</template>
<script>
import { mapState } from 'vuex'
import {
getResourceTypePerms,
getResourcePerms,
deleteRoleResourcePerm,
getResourceGroupPerms,
deleteRoleResourceGroupPerm,
deleteRoleResourceGroupPerm2,
} from '@/modules/acl/api/permission'
export default {
name: 'ResourceForm',
data() {
return {
isGroup: false,
drawerTitle: '权限列表',
drawerVisible: false,
record: null,
allPerms: [],
resPerms: [],
roleID: null,
// columns: [
// {
// title: '角色名',
// dataIndex: 'name',
// sorter: false,
// width: 50,
// },
// {
// title: '下属用户',
// dataIndex: 'users',
// sorter: false,
// width: 150,
// scopedSlots: { customRender: 'users' },
// },
// {
// title: '权限列表',
// dataIndex: 'perms',
// sorter: false,
// width: 150,
// scopedSlots: { customRender: 'perms' },
// },
// {
// title: '批量操作',
// sorter: false,
// width: 50,
// scopedSlots: { customRender: 'operate' },
// },
// ],
childrenDrawer: false,
allRoles: [],
}
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
},
beforeCreate() {
this.form = this.$form.createForm(this)
},
methods: {
handlePerm(record, isGroup) {
this.isGroup = isGroup
this.drawerVisible = true
this.record = record
this.getResPerms(record.id)
this.$nextTick(() => {
this.getAllPerms(record.resource_type_id)
})
},
handleClearAll(text) {
if (this.isGroup) {
deleteRoleResourceGroupPerm2(text.perms[0].rid, this.record.id, {
perms: [],
app_id: this.$route.name.split('_')[0],
}).then((res) => {
this.$message.success('删除成功')
this.$nextTick(() => {
this.getResPerms(this.record.id)
})
})
} else {
deleteRoleResourcePerm(text.perms[0].rid, this.record.id, {
perms: [],
app_id: this.$route.name.split('_')[0],
}).then((res) => {
this.$message.success('删除成功')
// for (let i = 0; i < this.resPerms.length; i++) {
// if (this.resPerms[i].name === text.name) {
// this.resPerms[i].perms = []
// }
// }
this.$nextTick(() => {
this.getResPerms(this.record.id)
})
})
}
},
getResPerms(resId) {
if (!this.isGroup) {
getResourcePerms(resId).then((res) => {
const perms = []
for (const key in res) {
perms.push({ name: key, perms: res[key].perms, users: res[key].users })
}
this.resPerms = perms
})
} else {
getResourceGroupPerms(resId).then((res) => {
const perms = []
for (const key in res) {
perms.push({ name: key, perms: res[key].perms, users: res[key].users })
}
this.resPerms = perms
})
}
},
getAllPerms(resTypeId) {
getResourceTypePerms(resTypeId).then((res) => {
this.allPerms = res
})
},
deletePerm(roleID, permName) {
if (!this.isGroup) {
deleteRoleResourcePerm(roleID, this.record.id, {
perms: [permName],
app_id: this.$route.name.split('_')[0],
}).then((res) => {
this.$message.success(`删除成功`)
})
// .catch(err => this.requestFailed(err))
} else {
deleteRoleResourceGroupPerm(roleID, this.record.id, {
perms: [permName],
app_id: this.$route.name.split('_')[0],
}).then((res) => {
this.$message.success(`删除成功`)
})
// .catch(err => this.requestFailed(err))
}
},
handleCancel(e) {
this.drawerVisible = false
},
// requestFailed(err) {
// const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
// this.$message.error(`${msg}`)
// },
filterNameMethod({ option, row }) {
return row.name.toLowerCase().includes(option.data.toLowerCase())
},
filterNameRecoverMethod({ option }) {
option.data = ''
},
},
watch: {},
}
</script>
<style lang="less" scoped>
.search {
margin-bottom: 54px;
}
.fold {
width: calc(100% - 216px);
display: inline-block;
}
.operator {
margin-bottom: 18px;
}
.action-btn {
margin-bottom: 1rem;
}
@media screen and (max-width: 900px) {
.fold {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,216 @@
<template>
<CustomDrawer :title="title" width="500px" :closable="false" :visible="visible" @close="closeForm">
<a-form :form="form">
<a-form-item>
<div slot="label" style="display: inline-block">
<span>角色列表</span>
<a-divider type="vertical" />
<a-switch
style="display: inline-block"
checked-children="用户"
un-checked-children="虚拟"
@change="handleRoleTypeChange"
/>
</div>
<el-select
:style="{ width: '100%' }"
size="small"
v-decorator="['roleIdList', { rules: [{ required: true, message: '请选择角色名称' }] }]"
multiple
filterable
placeholder="请选择角色名称,可多选!"
>
<el-option v-for="role in allRoles" :key="role.id" :value="role.id" :label="role.name"></el-option>
</el-select>
</a-form-item>
<a-form-item label="权限列表">
<el-select
:style="{ width: '100%' }"
size="small"
name="permName"
v-decorator="['permName', { rules: [{ required: true, message: '请选择权限' }] }]"
multiple
placeholder="请选择权限,可多选!"
>
<el-option v-for="perm in allPerms" :key="perm.name" :value="perm.name" :label="perm.name"></el-option>
</el-select>
</a-form-item>
<div class="custom-drawer-bottom-action">
<a-button @click="closeForm">取消</a-button>
<a-button @click="handleSubmit" type="primary" :loading="loading">确定</a-button>
</div>
</a-form>
</CustomDrawer>
</template>
<script>
import { Select, Option } from 'element-ui'
import { searchRole } from '@/modules/acl/api/role'
import {
getResourceTypePerms,
setRoleResourcePerm,
setRoleResourceGroupPerm,
setBatchRoleResourcePerm,
setBatchRoleResourceGroupPerm,
setBatchRoleResourceRevoke,
setBatchRoleResourceGroupRevoke,
} from '@/modules/acl/api/permission'
export default {
name: 'ResourcePermManageForm',
components: { ElSelect: Select, ElOption: Option },
data() {
return {
isGroup: false,
allRoles: [],
allPerms: [],
visible: false,
instance: {} || [], // 当前对象or批量授权的数组
type: 'grant', // grant or revoke
title: '',
loading: false,
}
},
props: {
groupTypeMessage: {
required: true,
type: Object,
},
},
computed: {},
beforeCreate() {
this.form = this.$form.createForm(this)
},
mounted() {
this.loadRoles(1)
},
methods: {
handleRoleTypeChange(target) {
if (!target) {
this.loadRoles(1)
} else {
this.loadRoles(0)
}
},
loadRoles(isUserRole) {
searchRole({ page_size: 9999, app_id: this.$route.name.split('_')[0], user_role: isUserRole }).then((res) => {
this.allRoles = res.roles
})
// .catch(err => this.requestFailed(err))
},
loadPerm(resourceTypeId) {
getResourceTypePerms(resourceTypeId).then((res) => {
this.allPerms = res
})
// .catch(err => this.requestFailed(err))
},
closeForm() {
this.visible = false
this.form.resetFields()
this.$emit('close')
},
editPerm(record, isGroup, type = 'grant') {
this.isGroup = isGroup
this.visible = true
this.instance = record
this.type = type
if (Array.isArray(record)) {
this.loadPerm(record[0]['resource_type_id'])
this.title = `${type === 'grant' ? '批量授权' : '批量权限回收'}`
} else {
this.title = `添加授权${record.name}`
this.loadPerm(record['resource_type_id'])
}
},
// requestFailed(err) {
// const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
// this.$message.error(`${msg}`)
// },
handleSubmit(e) {
e.preventDefault()
this.form.validateFields((err, values) => {
if (!err) {
values.roleIdList.forEach((roleId) => {
const params = { perms: values.permName, app_id: this.$route.name.split('_')[0] }
this.loading = true
if (!this.isGroup) {
if (Array.isArray(this.instance)) {
// const promises = this.instance.map(item => {
// return setRoleResourcePerm(roleId, item.id, params)
// })
// Promise.all(promises).then(() => {
// this.$message.success('添加授权成功')
// })
if (this.type === 'grant') {
setBatchRoleResourcePerm(roleId, { ...params, resource_ids: this.instance.map((a) => a.id) })
.then((res) => {
this.$message.success('添加授权成功')
})
.finally(() => {
this.loading = false
})
} else {
setBatchRoleResourceRevoke(roleId, { ...params, resource_ids: this.instance.map((a) => a.id) })
.then((res) => {
this.$message.success('批量权限回收成功')
})
.finally(() => {
this.loading = false
})
}
} else {
setRoleResourcePerm(roleId, this.instance.id, params)
.then((res) => {
this.$message.success('添加授权成功')
})
.finally(() => {
this.loading = false
})
}
} else {
if (Array.isArray(this.instance)) {
// const promises = this.instance.map(item => {
// return setRoleResourceGroupPerm(roleId, item.id, params)
// })
// Promise.all(promises).then(() => {
// this.$message.success('添加授权成功')
// })
if (this.type === 'grant') {
setBatchRoleResourceGroupPerm(roleId, { ...params, group_ids: this.instance.map((a) => a.id) })
.then((res) => {
this.$message.success('添加授权成功')
})
.finally(() => {
this.loading = false
})
} else {
setBatchRoleResourceGroupRevoke(roleId, {
...params,
group_ids: this.instance.map((a) => a.id),
})
.then((res) => {
this.$message.success('批量权限回收成功')
})
.finally(() => {
this.loading = false
})
}
} else {
setRoleResourceGroupPerm(roleId, this.instance.id, params)
.then((res) => {
this.$message.success('添加授权成功')
})
.finally(() => {
this.loading = false
})
}
}
})
}
})
},
},
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,160 @@
<template>
<CustomDrawer
:closable="false"
:title="drawerTitle"
:visible="drawerVisible"
@close="onClose"
placement="right"
width="500px"
>
<a-form :form="form" @submit="handleSubmit" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<a-form-item label="类型名">
<a-input
name="name"
placeholder="类型名称"
v-decorator="['name', { rules: [{ required: true, message: '请输入类型名' }] }]"
/>
</a-form-item>
<a-form-item label="描述">
<a-textarea
placeholder="请输入描述信息..."
name="description"
:rows="4"
v-decorator="['description', { rules: [] }]"
/>
</a-form-item>
<a-form-item label="权限">
<a-select mode="tags" v-model="perms" style="width: 100%" placeholder="请输入权限名..."> </a-select>
</a-form-item>
<a-form-item>
<a-input name="id" type="hidden" v-decorator="['id', { rules: [] }]" />
</a-form-item>
<div class="custom-drawer-bottom-action">
<a-button @click="onClose">取消</a-button>
<a-button @click="handleSubmit" type="primary">确定</a-button>
</div>
</a-form>
</CustomDrawer>
</template>
<script>
import { addResourceType, updateResourceTypeById } from '@/modules/acl/api/resource'
export default {
name: 'ResourceForm',
data() {
return {
drawerTitle: '新增资源类型',
drawerVisible: false,
perms: [],
}
},
beforeCreate() {
this.form = this.$form.createForm(this)
},
computed: {},
mounted() {},
methods: {
handleCreate() {
this.drawerVisible = true
},
onClose() {
this.form.resetFields()
this.perms = []
this.drawerVisible = false
},
onChange(e) {
console.log(`checked = ${e}`)
},
handleEdit(record) {
this.drawerVisible = true
console.log(record)
this.perms = record.perms
this.$nextTick(() => {
this.form.setFieldsValue({
id: record.id,
name: record.name,
description: record.description,
})
})
},
handleSubmit(e) {
e.preventDefault()
this.form.validateFields((err, values) => {
if (!err) {
console.log('Received values of form: ', values)
values.app_id = this.$route.name.split('_')[0]
values.perms = this.perms
if (values.id) {
this.updateResourceType(values.id, values)
} else {
this.createResourceType(values)
}
}
})
},
updateResourceType(id, data) {
updateResourceTypeById(id, data).then(res => {
this.$message.success(`更新成功`)
this.handleOk()
this.onClose()
})
// .catch(err => this.requestFailed(err))
},
createResourceType(data) {
addResourceType(data).then(res => {
this.$message.success(`添加成功`)
this.handleOk()
this.onClose()
})
// .catch(err => this.requestFailed(err))
},
// requestFailed (err) {
// const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
// this.$message.error(`${msg}`)
// }
},
watch: {},
props: {
handleOk: {
type: Function,
default: null,
},
},
}
</script>
<style lang="less" scoped>
.search {
margin-bottom: 54px;
}
.fold {
width: calc(100% - 216px);
display: inline-block;
}
.operator {
margin-bottom: 18px;
}
.action-btn {
margin-bottom: 1rem;
}
@media screen and (max-width: 900px) {
.fold {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,277 @@
<template>
<div>
<search-form
ref="child"
:attrList="resourceTableAttrList"
@search="handleSearch"
@searchFormReset="searchFormReset"
></search-form>
<vxe-table
ref="xTable"
border
size="small"
:data="tableData"
resizable
:loading="loading"
:max-height="`${windowHeight - 331}px`"
>
<vxe-column field="created_at" width="144px" title="操作时间"></vxe-column>
<vxe-column field="operate_uid" width="130px" title="操作员"></vxe-column>
<vxe-column field="operate_type" width="80px" title="操作">
<template #default="{ row }">
<a-tag :color="handleTagColor(row.operate_type)">
{{ operateTypeMap.get(row.operate_type) }}
</a-tag>
</template>
</vxe-column>
<vxe-column field="link_id" title="资源类型名">
<template #default="{ row }">
<span>
{{ row.current.name || row.origin.name }}
</span>
</template>
</vxe-column>
<vxe-column title="描述">
<template #default="{ row }">
<p>
{{ row.changeDescription }}
</p>
</template>
</vxe-column>
<vxe-column field="source" width="100px" title="来源"></vxe-column>
</vxe-table>
<pager
:current-page.sync="queryParams.page"
:page-size.sync="queryParams.page_size"
:page-sizes="[50,100,200]"
:total="tableDataLength"
:isLoading="loading"
@change="onChange"
@showSizeChange="onShowSizeChange"
></pager>
</div>
</template>
<script>
import _ from 'lodash'
import Pager from './pager.vue'
import SearchForm from './searchForm.vue'
import { searchResourceHistory } from '@/modules/acl/api/history'
export default {
components: { SearchForm, Pager },
props: {
allResourceTypes: {
type: Array,
required: true
},
allUsers: {
type: Array,
required: true
},
allRoles: {
type: Array,
required: true
},
allRolesMap: {
type: Map,
required: true
},
allUsersMap: {
type: Map,
required: true
},
allResourceTypesMap: {
type: Map,
required: true
},
},
data() {
return {
loading: true,
checked: false,
tableData: [],
app_id: this.$route.name.split('_')[0],
resourceTableAttrList: [
{
alias: '日期',
is_choice: false,
name: 'datetime',
value_type: '3'
},
{
alias: '操作员',
is_choice: true,
name: 'operate_uid',
value_type: '2',
choice_value: this.allUsers
},
{
alias: '操作',
is_choice: true,
name: 'operate_type',
value_type: '2',
choice_value: [
{ '新建': 'create' },
{ '修改': 'update' },
{ '删除': 'delete' },
]
},
{
alias: '资源类型',
is_choice: true,
name: 'link_id',
value_type: '2',
choice_value: this.allResourceTypes
}
],
operateTypeMap: new Map([
['create', '新建'],
['update', '修改'],
['delete', '删除']
]),
colorMap: new Map([
['create', 'green'],
['update', 'orange'],
['delete', 'red']
]),
queryParams: {
page: 1,
page_size: 50,
app_id: this.$route.name.split('_')[0],
scope: 'resource_type',
start: '',
end: ''
},
}
},
async created() {
await this.getTable(this.queryParams)
},
updated() {
this.$refs.xTable.$el.querySelector('.vxe-table--body-wrapper').scrollTop = 0
},
watch: {
'$route.name': async function(oldName, newName) {
this.app_id = this.$route.name.split('_')[0]
await this.getTable(this.queryParams)
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
tableDataLength() {
return this.tableData.length
}
},
methods: {
async getTable(queryParams) {
try {
this.loading = true
const { data } = await searchResourceHistory(this.handleQueryParams(queryParams))
data.forEach(item => {
this.handleChangeDescription(item, item.operate_type)
item.operate_uid = this.allUsersMap.get(item.operate_uid)
})
this.tableData = data
} finally {
this.loading = false
}
},
// searchForm相关
handleSearch(queryParams) {
this.queryParams = { ...queryParams, app_id: this.app_id, scope: 'resource_type' }
this.getTable(this.queryParams)
},
searchFormReset() {
this.$refs.child.checked = false
this.checked = false
this.queryParams = {
page: 1,
page_size: 50,
app_id: this.$route.name.split('_')[0],
scope: 'resource_type',
}
this.getTable(this.queryParams)
},
// pager相关
onShowSizeChange(size) {
this.queryParams.page_size = size
this.queryParams.page = 1
this.getTable(this.queryParams)
},
onChange(pageNum) {
this.queryParams.page = pageNum
this.getTable(this.queryParams)
},
handleQueryParams(queryParams) {
let flag = false
let q = queryParams.q ? queryParams.q : ''
for (const key in queryParams) {
if (key !== 'page' && key !== 'page_size' && key !== 'app_id' && key !== 'q' && key !== 'start' && key !== 'end' && queryParams[key] !== undefined) {
flag = true
if (q) q += `,${key}:${queryParams[key]}`
else q += `${key}:${queryParams[key]}`
delete queryParams[key]
}
}
const newQueryParams = { ...queryParams, q }
return flag ? newQueryParams : queryParams
},
handleTagColor(operateType) {
return this.colorMap.get(operateType)
},
handleChangeDescription(item, operate_type) {
switch (operate_type) {
// create
case 'create': {
item.changeDescription = `新增资源类型${item.current.name}\n描述${item.current.description}\n权限${item.extra.permission_ids.current}`
break
}
case 'update': {
item.changeDescription = ''
for (const key in item.origin) {
const newVal = item.current[key]
const oldVal = item.origin[key]
if (!_.isEqual(newVal, oldVal) && key !== 'updated_at' && key !== 'deleted_at' && key !== 'created_at') {
if (oldVal === null || oldVal === '') {
const str = ` ${key} : 改为 ${newVal} \n`
item.changeDescription += str
} else {
const str = ` ${key} : ${oldVal} 改为 ${newVal} \n`
item.changeDescription += str
}
}
}
const currentPerms = item.extra.permission_ids.current
const originPerms = item.extra.permission_ids.origin
if (!_.isEqual(currentPerms, originPerms)) item.changeDescription += ` permission_ids : ${originPerms} 改为 ${currentPerms} `
if (!item.changeDescription) item.changeDescription = '没有修改'
break
}
case 'delete': {
item.changeDescription = `删除资源类型${item.origin.name}\n描述${item.origin.description}\n权限${item.extra.permission_ids.origin}`
break
}
}
}
}
}
</script>
<style lang="less" scoped>
.row {
margin-top: 5px;
}
.ant-tag {
max-width: 100%;
overflow: hidden;
text-overflow:ellipsis;
}
p {
margin-bottom: 0;
}
</style>

View File

@@ -0,0 +1,292 @@
<template>
<CustomDrawer
width="800px"
placement="left"
title="资源列表"
@close="handleCancel"
:visible="visible"
:hasFooter="false"
>
<a-form-item label="资源类型" :label-col="{ span: 2 }" :wrapper-col="{ span: 14 }">
<a-select v-model="typeSelected" style="width:100%" @change="refresh">
<a-select-option v-for="type in resourceTypes" :value="type.id" :key="type.id">{{ type.name }}</a-select-option>
</a-select>
</a-form-item>
<vxe-table :max-height="`${windowHeight - 180}px`" :data="records" ref="rTable">
<vxe-column
field="name"
title="资源名"
width="30%"
:filters="[{ data: '' }]"
:filter-method="filterNameMethod"
:filter-recover-method="filterNameRecoverMethod"
>
<template #header="{ column }">
<span>{{ column.title }}</span>
<a-tooltip title="复制资源名">
<a-icon @click="copyResourceName" class="resource-user-form-copy" theme="filled" type="copy" />
</a-tooltip>
</template>
<template #filter="{ $panel, column }">
<template v-for="(option, index) in column.filters">
<input
class="my-input"
type="type"
:key="index"
v-model="option.data"
@input="$panel.changeOption($event, !!option.data, option)"
@keyup.enter="$panel.confirmFilter()"
placeholder="按回车确认筛选"
/>
</template>
</template>
</vxe-column>
<vxe-column field="permissions" title="权限列表" width="70%">
<template #default="{row}">
<a-tag color="cyan" v-for="(r, index) in row.permissions" :key="index">{{ r }}</a-tag>
</template>
</vxe-column>
</vxe-table>
<!-- <a-table
:columns="columns"
:dataSource="records"
:rowKey="record => record.id"
:pagination="false"
ref="rTable"
size="middle"
:scroll="{ y: 300 }"
> -->
<!-- <div slot="filterDropdown" slot-scope="{ setSelectedKeys, selectedKeys, confirm, clearFilters, column }" class="custom-filter-dropdown">
<a-input
v-ant-ref="c => searchInput = c"
:placeholder="` ${column.title}`"
:value="selectedKeys[0]"
@change="e => setSelectedKeys(e.target.value ? [e.target.value] : [])"
@pressEnter="() => handleSearch(selectedKeys, confirm, column)"
style="width: 188px; margin-bottom: 8px; display: block;"
/>
<a-button
type="primary"
@click="() => handleSearch(selectedKeys, confirm, column)"
icon="search"
size="small"
style="width: 90px; margin-right: 8px"
>搜索</a-button>
<a-button
@click="() => handleReset(clearFilters, column)"
size="small"
style="width: 90px"
>重置</a-button>
</div>
<a-icon slot="filterIcon" slot-scope="filtered" type="search" :style="{ color: filtered ? '#108ee9' : undefined }" />
<template slot="nameSearchRender" slot-scope="text">
<span v-if="columnSearchText.name">
<template v-for="(fragment, i) in text.toString().split(new RegExp(`(?<=${columnSearchText.name})|(?=${columnSearchText.name})`, 'i'))">
<mark v-if="fragment.toLowerCase() === columnSearchText.name.toLowerCase()" :key="i" class="highlight">{{ fragment }}</mark>
<template v-else>{{ fragment }}</template>
</template>
</span>
<template v-else>{{ text }}</template>
</template> -->
<!-- <template slot="permissions" slot-scope="record">
<a-tag color="cyan" v-for="(r, index) in record" :key="index">{{ r }}</a-tag>
</template>
</a-table> -->
<!-- <div
:style="{
position: 'absolute',
left: 0,
bottom: 0,
width: '100%',
borderTop: '1px solid #e9e9e9',
padding: '10px 16px',
background: '#fff',
textAlign: 'right',
}"
>
<a-button :style="{marginRight: '8px'}" @click="handleCancel">
取消
</a-button>
<a-button @click="handleOk" type="primary">确定</a-button>
</div> -->
</CustomDrawer>
</template>
<script>
import { mapState } from 'vuex'
import { searchPermResourceByRoleId } from '@/modules/acl/api/permission'
import { searchResourceType } from '@/modules/acl/api/resource'
export default {
name: 'ResourceUserForm',
data() {
return {
visible: false,
rid: 0,
records: [],
resourceTypes: [],
typeSelected: null,
columnSearchText: {
name: '',
},
filterName: '',
// columns: [
// {
// title: '资源名',
// field: 'name',
// sorter: false,
// width: '30%',
// // scopedSlots: {
// // customRender: 'nameSearchRender',
// // filterDropdown: 'filterDropdown',
// // filterIcon: 'filterIcon'
// // },
// // onFilter: (value, record) => record.name && record.name.toLowerCase().includes(value.toLowerCase()),
// // onFilterDropdownVisibleChange: (visible) => {
// // if (visible) {
// // setTimeout(() => {
// // this.searchInput.focus()
// // }, 0)
// // }
// // }
// },
// {
// title: '权限列表',
// field: 'permissions',
// width: '70%',
// slots: { default: 'permissions_default' },
// },
// ],
}
},
computed: {
...mapState({
windowHeight: state => state.windowHeight,
}),
},
mounted() {
this.loadResourceTypes()
},
methods: {
loadUserResource(record) {
this.visible = true
this.rid = record.id
this.refresh()
},
// handleSearch(selectedKeys, confirm, column) {
// confirm()
// this.columnSearchText[column.dataIndex] = selectedKeys[0]
// },
// handleReset(clearFilters, column) {
// clearFilters()
// this.columnSearchText[column.dataIndex] = ''
// },
loadResourceTypes() {
this.resourceTypes = []
const appId = this.$route.name.split('_')[0]
searchResourceType({ app_id: appId }).then(res => {
this.resourceTypes = res.groups
if (res.groups && res.groups.length > 0) {
this.typeSelected = res.groups[0].id
console.log(this.typeSelected)
} else {
this.typeSelected = null
}
})
// .catch(err => this.$httpError(err))
},
handleOk() {
this.visible = false
},
refresh() {
if (this.typeSelected) {
searchPermResourceByRoleId(this.rid, {
resource_type_id: this.typeSelected,
app_id: this.$route.name.split('_')[0],
}).then(res => {
this.records = res.resources
})
// .catch(err=>this.$httpError(err))
}
},
handleCancel() {
this.visible = false
},
filterNameMethod({ option, row }) {
this.filterName = option.data
return row.name.toLowerCase().includes(option.data.toLowerCase())
},
filterNameRecoverMethod({ option }) {
this.filterName = ''
option.data = ''
},
copyResourceName() {
const val = this.records
.filter(item => item.name.toLowerCase().includes(this.filterName.toLowerCase()))
.map(item => item.name)
.join('\n')
this.copy(val, () => {
this.$message.success('复制成功')
})
},
copy(value, cb) {
const textarea = document.createElement('textarea')
textarea.readOnly = 'readonly'
textarea.value = value
document.body.appendChild(textarea)
textarea.select()
textarea.setSelectionRange(0, textarea.value.length)
document.execCommand('Copy')
document.body.removeChild(textarea)
if (cb && Object.prototype.toString.call(cb) === '[object Function]') {
cb()
}
},
},
watch: {
'$route.name': function(newName, oldName) {
this.resourceTypes = []
this.loadResourceTypes()
},
},
}
</script>
<style lang="less" scoped>
.search {
margin-bottom: 54px;
}
.fold {
width: calc(100% - 216px);
display: inline-block;
}
.operator {
margin-bottom: 18px;
}
.action-btn {
margin-bottom: 1rem;
}
.custom-filter-dropdown {
padding: 8px;
border-radius: 4px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.highlight {
background-color: rgb(255, 192, 105);
padding: 0;
}
</style>
<style lang="less" scoped>
.resource-user-form-copy {
color: #c0c4cc;
cursor: pointer;
&:hover {
color: #606266;
}
}
</style>

View File

@@ -0,0 +1,225 @@
<template>
<CustomDrawer
:closable="false"
:title="drawerTitle"
:visible="drawerVisible"
@close="onClose"
placement="right"
width="500px"
>
<a-form :form="form" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }" @submit="handleSubmit">
<a-form-item label="角色名">
<a-input
name="name"
placeholder="角色名"
v-decorator="['name', { rules: [{ required: true, message: '请输入角色名' }] }]"
/>
</a-form-item>
<a-form-item v-if="$route.name.split('_')[0] !== 'acl'" label="密码">
<a-input name="password" placeholder="密码" v-decorator="['password', { rules: [{ required: false }] }]" />
</a-form-item>
<!-- <a-form-item label="继承自">
<a-select
showSearch
v-model="selectedParents"
:filterOption="false"
placeholder="可选择继承角色"
mode="multiple"
>
<a-select-option v-for="role in scrollData" :key="role.id">{{ role.name }}</a-select-option>
</a-select>
</a-form-item> -->
<a-form-item label="继承自">
<el-select
:style="{ width: '100%' }"
size="small"
v-model="selectedParents"
multiple
filterable
placeholder="可选择继承角色"
>
<el-option v-for="role in allRoles" :key="role.id" :value="role.id" :label="role.name"></el-option>
</el-select>
</a-form-item>
<a-form-item label="是否应用管理员">
<a-switch
@change="onChange"
name="is_app_admin"
v-decorator="['is_app_admin', { rules: [], valuePropName: 'checked' }]"
/>
</a-form-item>
<a-form-item>
<a-input name="id" type="hidden" v-decorator="['id', { rules: [] }]" />
</a-form-item>
<div class="custom-drawer-bottom-action">
<a-button @click="onClose">取消</a-button>
<a-button @click="handleSubmit" type="primary">确定</a-button>
</div>
</a-form>
</CustomDrawer>
</template>
<script>
import { Select, Option } from 'element-ui'
import { addRole, updateRoleById, delParentRole, addBatchParentRole } from '@/modules/acl/api/role'
export default {
name: 'RoleForm',
components: {
ElSelect: Select,
ElOption: Option,
},
data() {
return {
drawerTitle: '',
current_id: 0,
drawerVisible: false,
selectedParents: [],
oldParents: [],
}
},
beforeCreate() {
this.form = this.$form.createForm(this)
},
computed: {},
mounted() {
console.log(this.$route)
},
methods: {
// filterOption(input, option) {
// return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
// },
handleCreate() {
this.drawerTitle = '新增角色'
this.drawerVisible = true
},
onClose() {
this.form.resetFields()
this.selectedParents = []
this.oldParents = []
this.drawerVisible = false
},
onChange(e) {
console.log(`checked = ${e}`)
},
handleEdit(record) {
this.drawerTitle = `编辑${record.name}`
this.drawerVisible = true
this.current_id = record.id
const _parents = this.id2parents[record.id]
if (_parents) {
_parents.forEach((item) => {
this.selectedParents.push(item.id)
this.oldParents.push(item.id)
})
}
this.$nextTick(() => {
this.form.setFieldsValue({
id: record.id,
name: record.name,
is_app_admin: record.is_app_admin,
})
})
},
handleSubmit(e) {
e.preventDefault()
this.form.validateFields((err, values) => {
if (!err) {
values.app_id = this.$route.name.split('_')[0]
if (values.id) {
this.updateRole(values.id, values)
} else {
this.createRole(values)
}
}
})
},
updateRole(id, data) {
this.updateParents(id)
updateRoleById(id, { ...data, app_id: this.$route.name.split('_')[0] }).then((res) => {
this.$message.success(`更新成功`)
this.handleOk()
this.onClose()
})
// .catch(err => this.requestFailed(err))
},
createRole(data) {
addRole({ ...data, app_id: this.$route.name.split('_')[0] }).then((res) => {
this.$message.success(`添加成功`)
this.updateParents(res.id)
this.handleOk()
this.onClose()
})
// .catch(err => this.requestFailed(err))
},
updateParents(id) {
this.oldParents.forEach((item) => {
if (!this.selectedParents.includes(item)) {
delParentRole(id, item, { app_id: this.$route.name.split('_')[0] })
// .catch(err => this.requestFailed(err))
}
})
this.selectedParents.forEach((item) => {
if (!this.oldParents.includes(item)) {
addBatchParentRole(item, {
child_ids: [id],
app_id: this.$route.name.split('_')[0],
})
// addParentRole(id, item, { app_id: this.$route.name.split('_')[0] })
// .catch(err => this.requestFailed(err))
}
})
},
// requestFailed(err) {
// console.log(err)
// const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
// this.$message.error(`${msg}`)
// },
},
watch: {},
props: {
handleOk: {
type: Function,
default: null,
},
allRoles: {
type: Array,
required: true,
},
id2parents: {
type: Object,
required: true,
},
},
}
</script>
<style lang="less" scoped>
.search {
margin-bottom: 54px;
}
.fold {
width: calc(100% - 216px);
display: inline-block;
}
.operator {
margin-bottom: 18px;
}
.action-btn {
margin-bottom: 1rem;
}
@media screen and (max-width: 900px) {
.fold {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,314 @@
<template>
<div>
<search-form
ref="child"
:attrList="roleTableAttrList"
:hasSwitch="true"
switchValue="角色关系"
@onSwitchChange="onSwitchChange"
@search="handleSearch"
@searchFormReset="searchFormReset"
></search-form>
<vxe-table
ref="xTable"
border
:data="tableData"
size="small"
:loading="loading"
resizable
:max-height="`${windowHeight - 331}px`"
>
<vxe-column field="created_at" width="144px" title="操作时间"></vxe-column>
<vxe-column field="operate_uid" title="操作员" width="130px"></vxe-column>
<vxe-column field="operate_type" title="操作" width="112px">
<template #default="{ row }">
<template>
<a-tag :color="handleTagColor(row.operate_type)">{{ operateTypeMap.get(row.operate_type) }}</a-tag>
</template>
</template>
</vxe-column>
<vxe-column :title="checked ? '角色' : '角色'">
<template #default="{ row }">
<template v-if="!checked">
<a-tag color="blue">{{ row.current.name || row.origin.name }}</a-tag>
</template>
<template v-else>
<a-tag v-for="(id,index) in row.extra.child_ids" :key="'child_ids_' + id + index" color="blue">{{ id }}</a-tag>
</template>
</template>
</vxe-column>
<vxe-column :title="checked ? '继承自' : '管理员'" :width="checked ? '350px' : '80px'">
<template #default="{ row }">
<template v-if="!checked">
<a-icon type="check" v-if="row.current.is_app_admin"/>
</template>
<template v-else>
<a-tag v-for="(id,index) in row.extra.parent_ids" :key="'parent_ids_' + id + index" color="cyan">{{ id }}</a-tag>
</template>
</template>
</vxe-column>
<vxe-column title="描述" v-if="!checked">
<template #default="{ row }">
<p>
{{ row.description }}
</p>
</template>
</vxe-column>
<vxe-column field="source" title="来源" width="100px"></vxe-column>
</vxe-table>
<pager
:current-page.sync="queryParams.page"
:page-size.sync="queryParams.page_size"
:page-sizes="[50,100,200]"
:total="tableDataLength"
:isLoading="loading"
@change="onChange"
@showSizeChange="onShowSizeChange"
></pager>
</div>
</template>
<script>
import _ from 'lodash'
import Pager from './pager.vue'
import SearchForm from './searchForm.vue'
import { searchRoleHistory } from '@/modules/acl/api/history'
export default {
components: { SearchForm, Pager },
props: {
allUsers: {
type: Array,
required: true
},
allRoles: {
type: Array,
required: true
},
allRolesMap: {
type: Map,
required: true
},
allUsersMap: {
type: Map,
required: true
},
},
data() {
return {
loading: true,
checked: false,
tableData: [],
app_id: this.$route.name.split('_')[0],
operateTypeMap: new Map([
['create', '新建'],
['delete', '删除'],
['update', '修改'],
['role_relation_add', '添加角色关系'],
['role_relation_delete', '删除角色关系'],
]),
colorMap: new Map([
['create', 'green'],
['delete', 'red'],
['update', 'orange'],
['role_relation_add', 'green'],
['role_relation_delete', 'red']
]),
queryParams: {
page: 1,
page_size: 50,
app_id: this.$route.name.split('_')[0],
scope: 'role',
start: '',
end: ''
},
roleTableAttrList: [
{
alias: '日期',
is_choice: false,
name: 'datetime',
value_type: '3'
},
{
alias: '操作员',
is_choice: true,
name: 'operate_uid',
value_type: '2',
choice_value: this.allUsers
},
{
alias: '操作',
is_choice: true,
name: 'operate_type',
value_type: '2',
choice_value: [
{ '新建': 'create' },
{ '修改': 'update' },
{ '删除': 'delete' },
{ '添加角色关系': 'role_relation_add' },
{ '删除角色关系': 'role_relation_delete' },
]
}
]
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
tableDataLength() {
return this.tableData.length
},
},
async created() {
await this.getTable(this.queryParams)
},
updated() {
this.$refs.xTable.$el.querySelector('.vxe-table--body-wrapper').scrollTop = 0
},
watch: {
'$route.name': async function(oldName, newName) {
this.app_id = this.$route.name.split('_')[0]
await this.getTable(this.queryParams)
}
},
methods: {
async getTable(queryParams) {
try {
this.loading = true
const { data, id2roles, id2perms, id2resources } = await searchRoleHistory(this.handleQueryParams(queryParams))
data.forEach(item => {
item.operate_uid = this.allUsersMap.get(item.operate_uid)
if (item.operate_type === 'role_relation_add' || item.operate_type === 'role_relation_delete') {
item.extra.child_ids.forEach((subItem, index) => { item.extra.child_ids[index] = id2roles[subItem].name })
item.extra.parent_ids.forEach((subItem, index) => { item.extra.parent_ids[index] = id2roles[subItem].name })
} else {
this.handleChangeDescription(item, item.operate_type, id2roles, id2perms, id2resources)
}
})
this.tableData = data
} finally {
this.loading = false
}
},
// pager相关
onShowSizeChange(size) {
this.queryParams.page_size = size
this.queryParams.page = 1
this.getTable(this.queryParams)
},
onChange(pageNum) {
this.queryParams.page = pageNum
this.getTable(this.queryParams)
},
// searchForm相关
onSwitchChange(checked) {
this.checked = checked
this.queryParams.scope = checked ? 'role_relation' : 'role'
this.queryParams.page = 1
this.getTable(this.queryParams)
},
handleSearch(queryParams) {
this.queryParams = { ...queryParams, app_id: this.app_id, scope: this.checked ? 'role_relation' : 'role' }
this.getTable(this.queryParams)
},
searchFormReset() {
this.$refs.child.checked = false
this.checked = false
this.queryParams = {
page: 1,
page_size: 50,
app_id: this.$route.name.split('_')[0],
scope: this.checked ? 'role_relation' : 'role'
}
this.getTable(this.queryParams)
},
// 处理查询参数
handleQueryParams(queryParams) {
let flag = false
let q = queryParams.q ? queryParams.q : ''
for (const key in queryParams) {
if (key !== 'page' && key !== 'page_size' && key !== 'app_id' && key !== 'q' && key !== 'start' && key !== 'end' && queryParams[key] !== undefined) {
flag = true
if (q) q += `,${key}:${queryParams[key]}`
else q += `${key}:${queryParams[key]}`
delete queryParams[key]
}
}
const newQueryParams = { ...queryParams, q }
return flag ? newQueryParams : queryParams
},
// 处理tag颜色
handleTagColor(operateType) {
return this.colorMap.get(operateType)
},
handleChangeDescription(item, operate_type, id2roles, id2perms, id2resources) {
switch (operate_type) {
// create
case 'create': {
item.description = `新建角色${item.current.name}`
break
}
case 'update': {
item.description = ''
for (const key in item.origin) {
const newVal = item.current[key]
const oldVal = item.origin[key]
if (!_.isEqual(newVal, oldVal) && key !== 'updated_at' && key !== 'deleted_at' && key !== 'created_at') {
if (oldVal === null) {
const str = ` ${key} : 改为 ${newVal} `
item.description += str
} else {
const str = ` ${key} : ${oldVal} 改为 ${newVal} `
item.description += ` ${key} : ${oldVal} 改为 ${newVal} `
}
}
}
if (!item.description) item.description = '没有修改'
break
}
case 'delete': {
const { extra: { child_ids, parent_ids, role_permissions } } = item
child_ids.forEach((subItem, index) => { child_ids[index] = id2roles[subItem].name })
parent_ids.forEach((subItem, index) => { parent_ids[index] = id2roles[subItem].name })
const resourceMap = new Map()
const permsArr = []
role_permissions.forEach(subItem => {
const resource_id = subItem.resource_id
const perm_id = subItem.perm_id
if (resourceMap.has(resource_id)) {
let resource_perms = resourceMap.get(resource_id)
resource_perms += `,${id2perms[perm_id].name}`
resourceMap.set(resource_id, resource_perms)
} else {
resourceMap.set(resource_id, String(id2perms[perm_id].name))
}
})
resourceMap.forEach((value, key) => {
permsArr.push(`${id2resources[key].name}${value}`)
})
item.description = `继承者${child_ids}\n继承自${parent_ids}\n涉及资源及权限\n${permsArr.join(`\n`)}`
break
}
}
},
}
}
</script>
<style lang="less" scoped>
.row {
margin-top: 5px;
}
.ant-tag {
max-width: 100%;
overflow: hidden;
text-overflow:ellipsis;
}
p {
margin-bottom: 0;
}
</style>

View File

@@ -0,0 +1,236 @@
<template>
<div>
<a-form :colon="false">
<a-row :gutter="24">
<a-col
:sm="24"
:md="12"
:lg="12"
:xl="6"
v-for="attr in attrList.slice(0, 4)"
:key="attr.name">
<a-form-item
:label="attr.alias || attr.name"
:labelCol="{ span: 4 }"
:wrapperCol="{ span: 20 }"
labelAlign="right"
>
<a-select
@popupScroll="loadMoreData(attr.name, $event)"
@search="(value) => fetchData(value, attr.name)"
v-model="queryParams[attr.name]"
placeholder="请选择"
v-if="attr.is_choice"
show-search
:filter-option="filterOption"
allowClear
>
<a-select-option
:value="Object.values(choice)[0]"
v-for="(choice, index) in attr.choice_value"
:key="'Search_' + attr.name + index"
>
{{ Object.keys(choice)[0] }}
</a-select-option>
</a-select>
<a-range-picker
v-model="date"
@change="onChange"
:style="{ width: '100%' }"
format="YYYY-MM-DD HH:mm:ss"
:placeholder="['开始时间', '结束时间']"
:show-time="{
hideDisabledOptions: true,
defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')],
}"
v-else-if="valueTypeMap[attr.value_type] == 'date' || valueTypeMap[attr.value_type] == 'datetime'"
/>
<a-input v-model="queryParams[attr.name]" style="width: 100%" allowClear v-else />
</a-form-item>
</a-col>
<template v-if="expand && attrList.length >= 4">
<a-col
:sm="24"
:md="12"
:lg="8"
:xl="6"
:key="'expand_' + item.name"
v-for="item in attrList.slice(4)">
<a-form-item
:label="item.alias || item.name"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
labelAlign="right"
>
<a-select
@popupScroll="loadMoreData(item.name, $event)"
@search="(value) => fetchData(value, item.name)"
v-model="queryParams[item.name]"
placeholder="请选择"
v-if="item.is_choice"
show-search
:filter-option="filterOption"
allowClear
>
<a-select-option
:value="Object.values(choice)[0]"
:key="'Search_' + item.name + index"
v-for="(choice, index) in item.choice_value"
>
{{ Object.keys(choice)[0] }}
</a-select-option>
</a-select>
<a-range-picker
:style="{ width: '100%' }"
@change="onChange"
format="YYYY-MM-DD HH:mm"
:placeholder="['开始时间', '结束时间']"
v-else-if="valueTypeMap[item.value_type] == 'date' || valueTypeMap[item.value_type] == 'datetime'"
:show-time="{
hideDisabledOptions: true,
defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')],
}"
/>
<a-input v-model="queryParams[item.name]" style="width: 100%" allowClear v-else />
</a-form-item>
</a-col>
</template>
</a-row>
<a-row>
<a-col :span="24" :style="{ textAlign: 'right', marginBottom: '10px' }">
<a-switch
ref="switch"
v-if="hasSwitch"
:un-checked-children="switchValue"
@change="onSwitchChange"
v-model="checked"
/>
<a-button :style="{ marginLeft: '8px' }" type="primary" html-type="submit" @click="handleSearch">
查询
</a-button>
<a-button :style="{ marginLeft: '8px' }" @click="handleReset">
重置
</a-button>
<a :style="{ marginLeft: '8px', fontSize: '12px' }" @click="toggle" v-if="attrList.length >= 5">
{{ expand ? '隐藏' : '展开' }} <a-icon :type="expand ? 'up' : 'down'" />
</a>
</a-col>
</a-row>
</a-form>
</div>
</template>
<script>
import moment from 'moment'
import { valueTypeMap } from '../../constants/constants'
export default {
name: 'SearchForm',
props: {
attrList: {
type: Array,
required: true,
},
hasSwitch: {
type: Boolean,
required: false,
},
switchValue: {
default: '',
type: String,
required: false,
},
},
data() {
return {
valueTypeMap,
expand: false,
queryParams: {
page: 1,
page_size: 50,
},
date: undefined,
checked: false,
scrollPage: 1,
searchValue: undefined,
}
},
watch: {
queryParams: {
deep: true,
handler: function(val) {
this.preProcessData()
this.$emit('searchFormChange', val)
},
},
'queryParams.resource_id': {
handler(val) {
if (val === undefined) {
this.$emit('resourceClear')
}
},
},
'queryParams.link_id': {
handler(val) {
if (val === undefined) {
this.$emit('resourceClear')
}
},
},
},
methods: {
moment,
handleSearch() {
this.queryParams.page = 1
this.$emit('search', this.queryParams)
},
handleReset() {
this.queryParams = {
page: 1,
page_size: 50,
}
this.date = undefined
this.$emit('searchFormReset')
},
toggle() {
this.expand = !this.expand
this.$emit('expandChange', this.expand)
},
onChange(date, dateString) {
this.queryParams.start = dateString[0]
this.queryParams.end = dateString[1]
},
onSwitchChange(checked) {
this.$emit('onSwitchChange', checked)
},
filterOption(input, option) {
return option.componentOptions.children[0].text.indexOf(input) >= 0
},
preProcessData() {
Object.keys(this.queryParams).forEach((item) => {
if (this.queryParams[item] === '' || this.queryParams[item] === undefined) {
delete this.queryParams[item]
}
})
return this.queryParams
},
loadMoreData(name, e, value) {
const { target } = e
if (target.scrollTop + target.clientHeight === target.scrollHeight) {
this.$emit('loadMoreData', name, this.searchValue)
}
},
fetchData(value, name) {
this.searchValue = value
if (name === 'link_id' || name === 'resource_id') {
this.$emit('fetchData', value)
}
},
},
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,197 @@
<template>
<CustomDrawer
@close="handleClose"
width="500"
:title="`${triggerId ? '修改' : '新建'}触发器`"
:visible="visible"
:maskClosable="false"
>
<a-form :form="form" :label-col="{ span: 6 }" :wrapper-col="{ span: 15 }">
<a-form-item label="触发器名">
<a-input size="large" v-decorator="['name', { rules: [{ required: true, message: '请输入触发器名' }] }]">
</a-input>
</a-form-item>
<a-form-item label="资源名">
<a-input size="large" v-decorator="['wildcard']" placeholder="优先正则模式(次通配符)"> </a-input>
</a-form-item>
<a-form-item label="创建人">
<el-select :style="{ width: '100%' }" filterable multiple v-decorator="['uid']">
<template v-for="role in roles">
<el-option v-if="role.uid" :key="role.id" :value="role.uid" :label="role.name">{{ role.name }}</el-option>
</template>
</el-select>
</a-form-item>
<a-form-item label="资源类型">
<el-select
:style="{ width: '100%' }"
@change="handleRTChange"
v-decorator="['resource_type_id', { rules: [{ required: true, message: '请选择资源类型' }] }]"
>
<el-option
v-for="resourceType in resourceTypeList"
:key="resourceType.id"
:value="resourceType.id"
:label="resourceType.name"
></el-option>
</el-select>
<a-tooltip title="查看正则匹配结果">
<a class="trigger-form-pattern" @click="handlePattern"><a-icon type="eye"/></a>
</a-tooltip>
</a-form-item>
<a-form-item label="角色">
<el-select
:style="{ width: '100%' }"
filterable
multiple
v-decorator="['roles', { rules: [{ required: true, message: '请选择角色' }] }]"
>
<el-option v-for="role in roles" :key="role.id" :value="role.id" :label="role.name"></el-option>
</el-select>
</a-form-item>
<a-form-item label="权限">
<el-select
:style="{ width: '100%' }"
multiple
v-decorator="['permissions', { rules: [{ required: true, message: '请选择权限' }] }]"
>
<el-option
v-for="perm in selectResourceTypePerms"
:key="perm.id"
:value="perm.name"
:label="perm.name"
></el-option>
</el-select>
</a-form-item>
<a-form-item label="启用/禁用">
<a-switch v-decorator="['enabled', { rules: [], valuePropName: 'checked', initialValue: true }]" />
</a-form-item>
</a-form>
<div class="custom-drawer-bottom-action">
<a-button @click="handleClose">取消</a-button>
<a-button @click="handleSubmit" type="primary">提交</a-button>
</div>
<TriggerPattern ref="triggerPattern" :roles="roles" />
</CustomDrawer>
</template>
<script>
import { Select, Option, Input } from 'element-ui'
import { addTrigger, updateTrigger } from '@/modules/acl/api/trigger'
import TagSelectOption from '@/components/TagSelect/TagSelectOption'
import TriggerPattern from '../module/triggerPattern'
export default {
name: 'TriggerForm',
components: {
TagSelectOption,
TriggerPattern,
ElSelect: Select,
ElOption: Option,
ElInput: Input,
},
data() {
return {
visible: false,
triggerId: null,
selectResourceTypePerms: [],
form: this.$form.createForm(this),
}
},
props: {
roles: {
required: true,
type: Array,
},
resourceTypeList: {
required: true,
type: Array,
},
id2perms: {
required: true,
type: Object,
},
// eslint-disable-next-line vue/prop-name-casing
app_id: {
required: true,
type: String,
},
},
beforeCreate() {},
methods: {
handleEdit(ele) {
this.form.resetFields()
this.visible = true
if (ele) {
this.triggerId = ele.id
this.$nextTick(() => {
this.selectResourceTypePerms = this.id2perms[ele.resource_type_id]
const { name, wildcard, uid, resource_type_id, roles, permissions, enabled } = ele
this.form.setFieldsValue({
name,
wildcard,
uid,
resource_type_id,
permissions,
enabled,
roles: roles.map((x) => Number(x)),
})
})
} else {
this.triggerId = null
}
},
handleClose() {
this.$nextTick(() => {
this.visible = false
})
},
handleRTChange(value) {
this.selectResourceTypePerms = this.id2perms[value]
},
// filterOption(input, option) {
// return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
// },
handleSubmit() {
this.form.validateFields((err, values) => {
if (err) {
return
}
if (this.triggerId) {
updateTrigger(this.triggerId, { ...values, app_id: this.app_id }).then((res) => {
this.visible = false
this.$message.success('修改成功!')
this.$emit('refresh')
})
} else {
addTrigger({ ...values, app_id: this.app_id }).then((res) => {
this.visible = false
this.$message.success('创建成功!')
this.$emit('refresh')
})
}
})
},
handlePattern() {
this.form.validateFields(['wildcard', 'uid', 'resource_type_id'], (err, values) => {
if (!err) {
const { wildcard, uid, resource_type_id } = values
console.log(values)
this.$refs.triggerPattern.open({
resource_type_id,
app_id: this.app_id,
owner: uid,
pattern: wildcard,
})
}
})
},
},
}
</script>
<style lang="less" scoped>
.trigger-form-pattern {
position: absolute;
right: -20px;
}
</style>

View File

@@ -0,0 +1,295 @@
<template>
<div>
<search-form
ref="child"
:attrList="triggerTableAttrList"
@searchFormReset="searchFormReset"
@search="handleSearch"
></search-form>
<vxe-table
ref="xTable"
border
:data="tableData"
:loading="loading"
size="small"
resizable
:max-height="`${windowHeight - 331}px`"
>
<vxe-column field="created_at" width="144px" title="操作时间"></vxe-column>
<vxe-column field="operate_uid" width="130px" title="操作员"></vxe-column>
<vxe-column field="operate_type" width="80px" title="操作">
<template #default="{ row }">
<a-tag :color="handleTagColor(row.operate_type)">
{{ operateTypeMap.get(row.operate_type) }}
</a-tag>
</template>
</vxe-column>
<vxe-column field="trigger_id" width="250px" title="触发器">
<template #default="{ row }">
<span>
{{ row.current.name || row.origin.name }}
</span>
</template>
</vxe-column>
<vxe-column title="描述">
<template #default="{ row }">
<p>
{{ row.changeDescription }}
</p>
</template>
</vxe-column>
</vxe-table>
<pager
:current-page.sync="queryParams.page"
:page-size.sync="queryParams.page_size"
:page-sizes="[50,100,200]"
:total="tableDataLength"
:isLoading="loading"
@change="onChange"
@showSizeChange="onShowSizeChange"
></pager>
</div>
</template>
<script>
import _ from 'lodash'
import Pager from './pager.vue'
import SearchForm from './searchForm.vue'
import { searchTriggerHistory } from '@/modules/acl/api/history'
export default {
components: { SearchForm, Pager },
props: {
allUsers: {
type: Array,
required: true
},
allRoles: {
type: Array,
required: true
},
allTriggers: {
type: Array,
required: true
},
allRolesMap: {
type: Map,
required: true
},
allTriggersMap: {
type: Map,
required: true
},
allUsersMap: {
type: Map,
required: true
},
allResourceTypesMap: {
type: Map,
required: true
},
},
data() {
return {
app_id: this.$route.name.split('_')[0],
loading: true,
tableData: [],
operateTypeMap: new Map([
['create', '新建'],
['update', '修改'],
['delete', '删除'],
['trigger_apply', '应用'],
['trigger_cancel', '取消'],
]),
colorMap: new Map([
['create', 'green'],
['delete', 'red'],
['update', 'orange'],
['trigger_apply', 'green'],
['trigger_cancel', 'red'],
]),
triggerTableAttrList: [
{
alias: '日期',
is_choice: false,
name: 'datetime',
value_type: '3'
},
{
alias: '操作员',
is_choice: true,
name: 'operate_uid',
value_type: '2',
choice_value: this.allUsers
},
{
alias: '操作',
is_choice: true,
name: 'operate_type',
value_type: '2',
choice_value: [
{ '新建': 'create' },
{ '修改': 'update' },
{ '删除': 'delete' },
{ '应用': 'trigger_apply' },
{ '取消': 'trigger_cancel' },
]
},
{
alias: '触发器',
is_choice: true,
name: 'trigger_id',
value_type: '2',
choice_value: this.allTriggers
}
],
queryParams: {
page: 1,
page_size: 50,
app_id: this.$route.name.split('_')[0]
},
}
},
async created() {
await this.getTable(this.queryParams)
},
updated() {
this.$refs.xTable.$el.querySelector('.vxe-table--body-wrapper').scrollTop = 0
},
watch: {
'$route.name': async function(oldName, newName) {
this.app_id = this.$route.name.split('_')[0]
await this.getTable(this.queryParams)
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
tableDataLength() {
return this.tableData.length
}
},
methods: {
async getTable(queryParams) {
try {
this.loading = true
const res = await searchTriggerHistory(this.handleQueryParams(queryParams))
res.data.forEach(item => {
this.handleChangeDescription(item, item.operate_type)
item.trigger_id = this.allTriggersMap.get(item.trigger_id)
item.operate_uid = this.allUsersMap.get(item.operate_uid)
})
this.tableData = res.data
} finally {
this.loading = false
}
},
// pager相关
onShowSizeChange(size) {
this.queryParams.page_size = size
this.queryParams.page = 1
this.getTable(this.queryParams)
},
onChange(pageNum) {
this.queryParams.page = pageNum
this.getTable(this.queryParams)
},
// searchForm相关
handleSearch(queryParams) {
this.queryParams = queryParams
this.queryParams.app_id = this.app_id
this.getTable(this.queryParams)
},
searchFormReset() {
this.queryParams = {
page: 1,
page_size: 50,
app_id: this.$route.name.split('_')[0]
}
this.getTable(this.queryParams)
},
handleChangeDescription(item, operate_type) {
switch (operate_type) {
// create
case 'create': {
const str = item.current.roles
const newArr = str.slice(1, str.length - 1).split(', ')
const newStr = newArr.map(i => this.allRolesMap.get(Number(i))).join('')
item.changeDescription = `新增触发器${item.current.name}\n资源类型${this.allResourceTypesMap.get(item.current.resource_type_id)}资源名${item.current.wildcard}角色[${newStr}]\n权限${item.current.permissions}\n状态${item.current.enabled}`
break
}
case 'update': {
item.changeDescription = ''
for (const key in item.origin) {
const newVal = item.current[key]
const oldVal = item.origin[key]
if (!_.isEqual(newVal, oldVal) && key !== 'updated_at' && key !== 'deleted_at' && key !== 'created_at') {
if (oldVal === null) {
const str = ` ${key} : 改为 ${newVal} `
item.changeDescription += str
} else {
const str = ` ${key} : ${oldVal} 改为 ${newVal} `
item.changeDescription += ` ${key} : ${oldVal} 改为 ${newVal} `
}
}
}
if (!item.changeDescription) item.changeDescription = '没有修改'
break
}
case 'delete': {
const str = item.origin.roles
const newArr = str.slice(1, str.length - 1).split(', ')
const newStr = newArr.map(i => this.allRolesMap.get(Number(i))).join('')
item.changeDescription = `删除触发器${item.origin.name}\n资源类型${this.allResourceTypesMap.get(item.origin.resource_type_id)}资源名${item.origin.wildcard}角色[${newStr}]\n权限${item.origin.permissions}\n状态${item.origin.enabled}`
break
}
case 'trigger_apply': {
const str = item.current.roles
const newArr = str.slice(1, str.length - 1).split(', ')
const newStr = newArr.map(i => this.allRolesMap.get(Number(i))).join('')
item.changeDescription = `应用触发器${item.current.name}\n资源类型${this.allResourceTypesMap.get(item.current.resource_type_id)}资源名${item.current.wildcard}角色[${newStr}]\n权限${item.current.permissions}\n状态${item.current.enabled}`
break
}
case 'trigger_cancel': {
const str = item.current.roles
const newArr = str.slice(1, str.length - 1).split(', ')
const newStr = newArr.map(i => this.allRolesMap.get(Number(i))).join('')
item.changeDescription = `取消触发器${item.current.name}\n资源类型${this.allResourceTypesMap.get(item.current.resource_type_id)}资源名${item.current.wildcard}角色[${newStr}]\n权限${item.current.permissions}\n状态${item.current.enabled}`
break
}
}
},
handleQueryParams(queryParams) {
let q = ''
for (const key in queryParams) {
if (key !== 'page' && key !== 'page_size' && key !== 'app_id' && key !== 'start' && key !== 'end' && queryParams[key] !== undefined) {
if (q) {
q += `,${key}:${queryParams[key]}`
} else {
q += `${key}:${queryParams[key]}`
}
}
}
const newQueryParams = { ...queryParams, q }
return q ? newQueryParams : queryParams
},
handleTagColor(operateType) {
return this.colorMap.get(operateType)
}
}
}
</script>
<style lang="less" scoped>
p {
margin-bottom: 0;
}
.ant-tag {
max-width: 100%;
overflow: hidden;
text-overflow:ellipsis;
}
</style>

View File

@@ -0,0 +1,68 @@
<template>
<CustomDrawer
:hasFooter="false"
title="正则匹配结果"
:visible="patternVisible"
width="500"
@close="
() => {
patternVisible = false
}
"
>
<vxe-table :data="tableData" :max-height="`${windowHeight - 110}px`">
<vxe-table-column field="name" title="资源名"></vxe-table-column>
<vxe-table-column field="uid" title="创建人">
<template #default="{row}">
{{ getRoleName(row.uid) }}
</template>
</vxe-table-column>
<vxe-table-column field="created_at" title="创建时间"></vxe-table-column>
</vxe-table>
</CustomDrawer>
</template>
<script>
import { mapState } from 'vuex'
import { patternResults } from '@/modules/acl/api/trigger'
export default {
name: 'TriggerPattern',
props: {
roles: {
type: Array,
default: () => [],
},
},
data() {
return {
patternVisible: false,
tableData: [],
}
},
computed: {
...mapState({
windowHeight: state => state.windowHeight,
}),
},
methods: {
open(params) {
patternResults(params).then(res => {
this.patternVisible = true
this.tableData = res
})
},
getRoleName(uid) {
if (uid) {
const _find = this.roles.find(item => item.uid === uid)
if (_find) {
return _find.name
}
return ''
}
return ''
},
},
}
</script>
<style></style>

View File

@@ -0,0 +1,198 @@
<template>
<CustomDrawer
:closable="false"
:title="drawerTitle"
:visible="drawerVisible"
@close="onClose"
placement="right"
width="500px"
>
<a-form :form="form" @submit="handleSubmit" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<a-form-item label="用户名(英文)">
<a-input
name="username"
placeholder="英文名"
v-decorator="['username', { rules: [{ required: true, message: '请输入用户名' }] }]"
/>
</a-form-item>
<a-form-item label="中文名">
<a-input name="nickname" v-decorator="['nickname', { rules: [] }]" />
</a-form-item>
<a-form-item label="密码">
<a-input
type="password"
name="password"
v-decorator="['password', { rules: [{ required: true, message: '请输入密码' }] }]"
/>
</a-form-item>
<a-form-item label="部门">
<a-input name="department" v-decorator="['department', { rules: [] }]" />
</a-form-item>
<a-form-item label="小组">
<a-input name="catalog" v-decorator="['catalog', { rules: [] }]" />
</a-form-item>
<a-form-item label="邮箱">
<a-input
name="email"
v-decorator="[
'email',
{
rules: [
{
type: 'email',
message: '请输入正确的邮箱!',
},
{
required: true,
message: '请输入邮箱',
},
],
},
]"
/>
</a-form-item>
<a-form-item label="手机号码">
<a-input
name="mobile"
v-decorator="['mobile', { rules: [{ message: '请输入正确的手机号码', pattern: /^1\d{10}$/ }] }]"
/>
</a-form-item>
<a-form-item label="是否锁定">
<a-switch @change="onChange" name="block" v-decorator="['block', { rules: [], valuePropName: 'checked' }]" />
</a-form-item>
<a-form-item>
<a-input name="id" type="hidden" v-decorator="['id', { rules: [] }]" />
</a-form-item>
<div class="custom-drawer-bottom-action">
<a-button @click="onClose">取消</a-button>
<a-button @click="handleSubmit" type="primary">确定</a-button>
</div>
</a-form>
</CustomDrawer>
</template>
<script>
import { addUser, updateUserById } from '@/modules/acl/api/user'
export default {
name: 'AttributeForm',
data() {
return {
drawerTitle: '新增用户',
drawerVisible: false,
}
},
beforeCreate() {
this.form = this.$form.createForm(this)
},
computed: {},
mounted() {},
methods: {
handleCreate() {
this.drawerVisible = true
},
onClose() {
this.form.resetFields()
this.drawerVisible = false
},
onChange(e) {
console.log(`checked = ${e}`)
},
handleEdit(record) {
this.drawerVisible = true
console.log(record)
this.$nextTick(() => {
this.form.setFieldsValue({
id: record.uid,
username: record.username,
nickname: record.nickname,
password: record.password,
department: record.department,
catalog: record.catalog,
email: record.email,
mobile: record.mobile,
block: record.block,
})
})
},
handleSubmit(e) {
e.preventDefault()
this.form.validateFields((err, values) => {
if (!err) {
console.log('Received values of form: ', values)
if (values.id) {
this.updateUser(values.id, values)
} else {
this.createUser(values)
}
}
})
},
updateUser(attrId, data) {
updateUserById(attrId, data).then((res) => {
this.$message.success(`更新成功`)
this.handleOk()
this.onClose()
})
// .catch(err => this.requestFailed(err))
},
createUser(data) {
addUser(data).then((res) => {
this.$message.success(`添加成功`)
this.handleOk()
this.onClose()
})
// .catch(err => this.requestFailed(err))
},
// requestFailed (err) {
// const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
// this.$message.error(`${msg}`)
// }
},
watch: {},
props: {
handleOk: {
type: Function,
default: null,
},
},
}
</script>
<style lang="less" scoped>
.search {
margin-bottom: 54px;
}
.fold {
width: calc(100% - 216px);
display: inline-block;
}
.operator {
margin-bottom: 18px;
}
.action-btn {
margin-bottom: 1rem;
}
@media screen and (max-width: 900px) {
.fold {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,102 @@
<template>
<CustomDrawer :closable="true" :visible="visible" width="500px" @close="handleClose" title="组用户">
<a-form-item label="添加用户" :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
<a-row>
<a-col span="15">
<el-select v-model="selectedChildrenRole" multiple collapse-tags size="small" filterable>
<el-option
class="drop-down-render"
v-for="role in allRoles"
:key="role.id"
:value="role.id"
:label="role.name"
></el-option>
</el-select>
</a-col>
<a-col span="5" offset="1">
<a-button style="display: inline-block" @click="handleAddRole">确定</a-button>
</a-col>
</a-row>
</a-form-item>
<a-card>
<a-row :gutter="24" v-for="(record, index) in records" :key="record.id" :style="{ marginBottom: '5px' }">
<a-col :span="20">{{ index + 1 }}{{ record.nickname }}</a-col>
<a-col :span="4"><a-button type="danger" size="small" @click="handleRevokeUser(record)">移除</a-button></a-col>
</a-row>
</a-card>
</CustomDrawer>
</template>
<script>
import { getUsersUnderRole, delParentRole, addBatchParentRole } from '@/modules/acl/api/role'
import { Select, Option } from 'element-ui'
export default {
name: 'UsersUnderRoleForm',
components: { ElSelect: Select, ElOption: Option },
props: {
allRoles: {
type: Array,
default: () => [],
},
},
data() {
return {
visible: false,
records: [],
roleId: 0,
selectedChildrenRole: [],
}
},
mounted() {},
methods: {
handleClose() {
this.visible = false
},
loadRecords(roleId) {
getUsersUnderRole(roleId, { app_id: this.$route.name.split('_')[0] }).then((res) => {
this.records = res['users']
})
// .catch(err=> this.$httpError(err))
},
handleProcessRole(roleId) {
this.roleId = roleId
this.visible = true
this.loadRecords(roleId)
},
// filterOption(input, option) {
// return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
// },
async handleAddRole() {
// await this.selectedChildrenRole.forEach(item => {
// addParentRole(item, this.roleId, { app_id: this.$route.name.split('_')[0] }).then(res => {
// this.$message.success('添加成功')
// })
// // .catch(err=>{
// // this.$httpError(err)
// // })
// })
await addBatchParentRole(this.roleId, {
child_ids: this.selectedChildrenRole,
app_id: this.$route.name.split('_')[0],
})
this.loadRecords(this.roleId)
this.$message.success('添加完成')
this.selectedChildrenRole = []
},
handleRevokeUser(record) {
const that = this
this.$confirm({
content: '是否确定要移除该用户',
onOk() {
delParentRole(record.role.id, that.roleId, { app_id: that.$route.name.split('_')[0] }).then((res) => {
that.$message.success('删除成功!')
that.loadRecords(that.roleId)
})
// .catch(err=>that.$httpError(err))
},
})
},
},
}
</script>

View File

@@ -0,0 +1,34 @@
<template>
<div :style="{ backgroundColor: '#fff', padding: '24px' }">
<a-tabs default-active-key="1">
<a-tab-pane key="1" tab="权限变更">
<permisson-table></permisson-table>
</a-tab-pane>
<a-tab-pane key="2" tab="角色变更">
<role-history-table></role-history-table>
</a-tab-pane>
<a-tab-pane key="3" tab="资源变更">
<resource-history-table></resource-history-table>
</a-tab-pane>
<a-tab-pane key="4" tab="资源类型变更">
<resource-type-history-table></resource-type-history-table>
</a-tab-pane>
<a-tab-pane key="5" tab="触发器变更">
<trigger-history-table></trigger-history-table>
</a-tab-pane>
</a-tabs>
</div>
</template>
<script>
import permissonTable from './modules/permissionTable.vue'
import resourceHistoryTable from './modules/resourceHistoryTable.vue'
import resourceTypeHistoryTable from './modules/resourceTypeHistoryTable.vue'
import roleHistoryTable from './modules/roleHistoryTable.vue'
import triggerHistoryTable from './modules/triggerHistoryTable.vue'
export default {
components: { permissonTable, resourceHistoryTable, resourceTypeHistoryTable, roleHistoryTable, triggerHistoryTable },
}
</script>
<style></style>

View File

@@ -0,0 +1,416 @@
<template>
<div>
<search-form
ref="child"
:attrList="permissionTableAttrList"
@search="handleSearch"
@expandChange="handleExpandChange"
@searchFormReset="searchFormReset"
@searchFormChange="searchFormChange"
@loadMoreData="loadMoreResources"
@fetchData="fetchResources"
@resourceClear="resourceClear"
></search-form>
<vxe-table
ref="xTable"
border
resizable
size="small"
:data="tableData"
:loading="loading"
:max-height="`${windowHeight - windowHeightMinus}px`"
:scroll-y="{ enabled: false }"
>
<vxe-column field="created_at" width="144px" title="操作时间">
<template #default="{ row }">
<span>{{ row.deleted_at || row.updated_at || row.created_at }}</span>
</template>
</vxe-column>
<vxe-column field="operate_uid" width="130px" title="操作员"></vxe-column>
<vxe-column field="operate_type" width="80px" title="操作">
<template #default="{ row }">
<a-tag :color="row.operate_type === 'grant' ? 'green' : 'red'">{{
operateTypeMap.get(row.operate_type)
}}</a-tag>
</template>
</vxe-column>
<vxe-column field="rid" title="用户"></vxe-column>
<vxe-column field="resource_type_id" title="资源类型"></vxe-column>
<vxe-column field="resources" title="资源">
<template #default="{ row }">
<template v-if="row.resource_ids.length > 0">
<a-tooltip placement="top">
<template slot="title">
<span>{{ row.resource_ids[0] }}</span>
</template>
<a-tag color="blue" v-for="(resource, index) in row.resource_ids" :key="'resources_' + resource + index">
{{ resource }}
</a-tag>
</a-tooltip>
</template>
<template v-else-if="row.group_ids.length > 0">
<a-tag color="blue" v-for="(group, index) in row.group_ids" :key="'groups_' + group + index">
{{ group }}
</a-tag>
</template>
</template>
</vxe-column>
<vxe-column title="权限">
<template #default="{ row }">
<a-tag v-for="(perm, index) in row.permission_ids" :key="'perms_' + perm + index">
{{ perm }}
</a-tag>
</template>
</vxe-column>
<vxe-column field="source" width="100px" title="来源"></vxe-column>
</vxe-table>
<pager
:current-page.sync="queryParams.page"
:page-size.sync="queryParams.page_size"
:page-sizes="[50, 100, 200]"
:total="tableDataLength"
:isLoading="loading"
@change="onChange"
@showSizeChange="onShowSizeChange"
></pager>
</div>
</template>
<script>
import debounce from 'lodash/debounce'
import Pager from '../../module/pager.vue'
import SearchForm from '../../module/searchForm.vue'
import { searchApp } from '@/modules/acl/api/app'
import { searchPermissonHistory } from '@/modules/acl/api/history'
import { searchRole } from '@/modules/acl/api/role'
import { searchUser } from '@/modules/acl/api/user'
import { searchResource, searchResourceType } from '@/modules/acl/api/resource'
export default {
components: { SearchForm, Pager },
data() {
this.fetchResources = debounce(this.fetchResources, 800)
return {
app_id: undefined,
resource_id: undefined,
isExpand: false,
loading: true,
resourcesPage: 1,
resourcesNum: 0,
tableData: [],
allRoles: [],
allUsers: [],
allResourceTypes: [],
allResources: [],
allApps: [],
allRolesMap: new Map(),
allUsersMap: new Map(),
allResourceTypesMap: new Map(),
operateTypeMap: new Map([
['grant', '授权'],
['revoke', '撤销'],
]),
queryParams: {
page: 1,
page_size: 50,
start: '',
end: '',
},
permissionTableAttrList: [
{
alias: '日期',
is_choice: false,
name: 'datetime',
value_type: '3',
},
{
alias: '应用',
is_choice: true,
name: 'app_id',
value_type: '2',
choice_value: [],
},
{
alias: '操作员',
is_choice: true,
name: 'operate_uid',
value_type: '2',
choice_value: [],
},
{
alias: '用户',
is_choice: true,
name: 'rid',
value_type: '2',
choice_value: [],
},
{
alias: '资源类型',
is_choice: true,
name: 'resource_type_id',
value_type: '2',
choice_value: [],
},
{
alias: '资源',
is_choice: true,
name: 'resource_id',
value_type: '2',
choice_value: [],
},
{
alias: '操作',
is_choice: true,
name: 'operate_type',
value_type: '2',
choice_value: [{ 授权: 'grant' }, { 撤销: 'revoke' }],
},
],
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
windowHeightMinus() {
return this.isExpand ? 396 : 331
},
tableDataLength() {
return this.tableData.length
},
},
async created() {
this.$watch(
function() {
return this.permissionTableAttrList[3].choice_value
},
function() {
delete this.$refs.child.queryParams.rid
delete this.$refs.child.queryParams.resource_type_id
delete this.$refs.child.queryParams.resource_id
}
)
await Promise.all([this.getAllApps(), this.getAllUsers()])
await this.getTable(this.queryParams)
},
updated() {
this.$refs.xTable.$el.querySelector('.vxe-table--body-wrapper').scrollTop = 0
},
methods: {
async getTable(queryParams) {
try {
this.loading = true
const { data, id2groups, id2perms, id2resources, id2roles, id2resource_types } = await searchPermissonHistory(
this.handleQueryParams(queryParams)
)
data.forEach((item) => {
item.rid = id2roles[item.rid].name
item.operate_uid = this.allUsersMap.get(item.operate_uid)
// 脏数据清除后可删
if (id2resource_types[item.resource_type_id]) {
item.resource_type_id = id2resource_types[item.resource_type_id].name
}
item.resource_ids.forEach((subItem, index) => {
item.resource_ids[index] = id2resources[subItem].name
})
item.group_ids.forEach((subItem, index) => {
item.group_ids[index] = id2groups[subItem].name
})
item.permission_ids.forEach((subItem, index) => {
item.permission_ids[index] = id2perms[subItem].name
})
})
this.tableData = data
} finally {
this.loading = false
}
},
async getAllApps() {
const res = await searchApp()
const allApps = []
res.apps.forEach((item) => {
allApps.push({ [item.name]: item.id })
})
this.allApps = allApps
this.permissionTableAttrList[1].choice_value = this.allApps
},
async getAllRoles(app_id) {
if (!app_id) {
this.permissionTableAttrList[3].choice_value = []
return
}
const { roles } = await searchRole({
page_size: 9999,
app_id: app_id,
})
const allRoles = []
roles.forEach((item) => {
allRoles.push({ [item.name]: item.id })
})
this.allRoles = allRoles
this.permissionTableAttrList[3].choice_value = this.allRoles
},
async getAllUsers() {
const { users } = await searchUser({ page_size: 10000, app_id: 'acl' })
const allUsers = []
const allUsersMap = new Map()
users.forEach((item) => {
allUsers.push({ [item.nickname]: item.uid })
allUsersMap.set(item.uid, item.nickname)
})
this.allUsers = allUsers
this.allUsersMap = allUsersMap
this.permissionTableAttrList[2].choice_value = this.allUsers
},
async getAllResourceTypes(app_id) {
if (!app_id) {
this.permissionTableAttrList[4].choice_value = []
return
}
const { groups } = await searchResourceType({
page_size: 9999,
page: 1,
app_id: app_id,
})
const allResourceTypes = []
groups.forEach((item) => {
allResourceTypes.push({ [item.name]: item.id })
})
this.allResourceTypes = allResourceTypes
this.permissionTableAttrList[4].choice_value = this.allResourceTypes
},
async getAllResources(app_id, page, value = undefined) {
if (!app_id) {
this.permissionTableAttrList[5].choice_value = []
return
}
const { resources, numfound } = await searchResource({
page: page,
page_size: 50,
app_id: app_id,
q: value,
})
this.resourcesNum = numfound
const allResources = this.allResources
resources.forEach((item) => {
allResources.push({ [item.name]: item.id })
})
this.allResources = allResources
this.permissionTableAttrList[5].choice_value = this.allResources
},
loadMoreResources(name, value) {
if (name === 'resource_id' && this.allResources.length < this.resourcesNum) {
let currentPage = this.resourcesPage
this.getAllResources(this.app_id, ++currentPage, value)
this.resourcesPage = currentPage
}
},
resourceClear() {
this.resourcesPage = 1
this.allResources = []
this.getAllResources(this.app_id, 1)
},
async fetchResources(value) {
this.allResources = []
const allResources = []
if (!this.app_id) {
this.permissionTableAttrList[5].choice_value = []
return
}
this.resourcesPage = 1
if (value === '') {
this.getAllResources(this.app_id, 1)
return
}
const { resources, numfound } = await searchResource({
page: 1,
page_size: 50,
app_id: this.app_id,
q: value,
})
this.resourcesNum = numfound
resources.forEach((item) => {
allResources.push({ [item.name]: item.id })
})
this.allResources = allResources
this.permissionTableAttrList[5].choice_value = this.allResources
},
searchFormReset() {
this.queryParams = {
page: 1,
page_size: 50,
}
this.resourcesPage = 1
this.resourcesNum = 0
this.getTable(this.queryParams)
},
handleSearch(queryParams) {
this.queryParams = queryParams
this.getTable(this.queryParams)
},
handleExpandChange(expand) {
this.isExpand = expand
},
async searchFormChange(queryParams) {
if (this.app_id !== queryParams.app_id) {
this.app_id = queryParams.app_id
this.allResources = []
this.resourcesPage = 1
this.resourcesNum = 0
await Promise.all([
this.getAllRoles(this.app_id),
this.getAllResourceTypes(this.app_id),
this.getAllResources(this.app_id, this.resourcesPage),
])
}
if (queryParams.app_id === undefined) {
this.app_id = undefined
this.$refs.child.queryParams.rid = undefined
this.$refs.child.queryParams.resource_type_id = undefined
this.$refs.child.queryParams.resource_id = undefined
this.allResources = []
this.resourcesPage = 1
this.resourcesNum = 0
}
},
onShowSizeChange(size) {
this.queryParams.page_size = size
this.queryParams.page = 1
this.getTable(this.queryParams)
},
onChange(pageNum) {
this.queryParams.page = pageNum
this.getTable(this.queryParams)
},
handleQueryParams(queryParams) {
let q = ''
for (const key in queryParams) {
if (
key !== 'page' &&
key !== 'page_size' &&
key !== 'app_id' &&
key !== 'start' &&
key !== 'end' &&
queryParams[key] !== undefined
) {
if (q) q += `,${key}:${queryParams[key]}`
else q += `${key}:${queryParams[key]}`
delete queryParams[key]
}
}
const newQueryParams = { ...queryParams, q }
return q ? newQueryParams : queryParams
},
},
}
</script>
<style lang="less" scoped>
.ant-tag {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
</style>

View File

@@ -0,0 +1,400 @@
<template>
<div>
<search-form
ref="child"
:attrList="resourceTableAttrList"
:hasSwitch="true"
switchValue=""
@onSwitchChange="onSwitchChange"
@expandChange="handleExpandChange"
@search="handleSearch"
@searchFormReset="searchFormReset"
@searchFormChange="searchFormChange"
@loadMoreData="loadMoreResources"
@fetchData="fetchResources"
@resourceClear="resourceClear"
></search-form>
<vxe-table
ref="xTable"
border
resizable
size="small"
:loading="loading"
:data="tableData"
:max-height="`${windowHeight - windowHeightMinus}px`"
>
<vxe-column field="created_at" width="144px" title="操作时间"></vxe-column>
<vxe-column field="operate_uid" width="130px" title="操作员"></vxe-column>
<vxe-column field="operate_type" width="80px" title="操作">
<template #default="{ row }">
<a-tag :color="handleTagColor(row.operate_type)">
{{ operateTypeMap.get(row.operate_type) }}
</a-tag>
</template>
</vxe-column>
<vxe-column field="link_id" title="资源名">
<template #default="{ row }">
<span>
{{ row.current.name || row.origin.name }}
</span>
</template>
</vxe-column>
<vxe-column title="描述">
<template #default="{ row }">
<p>
{{ row.description }}
</p>
</template>
</vxe-column>
<vxe-column field="source" width="100px" title="来源"></vxe-column>
</vxe-table>
<pager
:current-page.sync="queryParams.page"
:page-size.sync="queryParams.page_size"
:page-sizes="[50, 100, 200]"
:total="tableDataLength"
:isLoading="loading"
@change="onChange"
@showSizeChange="onShowSizeChange"
></pager>
</div>
</template>
<script>
import _ from 'lodash'
import Pager from '../../module/pager.vue'
import SearchForm from '../../module/searchForm.vue'
import { searchResourceHistory } from '@/modules/acl/api/history'
import { searchUser } from '@/modules/acl/api/user'
import { searchResource } from '@/modules/acl/api/resource'
import { searchApp } from '@/modules/acl/api/app'
export default {
components: { SearchForm, Pager },
data() {
this.fetchResources = _.debounce(this.fetchResources, 800)
return {
loading: true,
checked: false,
isExpand: false,
app_id: undefined,
resourcesPage: 1,
resourcesNum: 0,
tableData: [],
allResources: [],
allUsers: [],
allApps: [],
allUsersMap: new Map(),
allResourcesMap: new Map(),
resourceTableAttrList: [
{
alias: '日期',
is_choice: false,
name: 'datetime',
value_type: '3',
},
{
alias: '应用',
is_choice: true,
name: 'app_id',
value_type: '2',
choice_value: [],
},
{
alias: '操作员',
is_choice: true,
name: 'operate_uid',
value_type: '2',
choice_value: [],
},
{
alias: '资源名',
is_choice: true,
name: 'link_id',
value_type: '2',
choice_value: [],
},
{
alias: '操作',
is_choice: true,
name: 'operate_type',
value_type: '2',
choice_value: [{ 新建: 'create' }, { 修改: 'update' }, { 删除: 'delete' }],
},
],
operateTypeMap: new Map([
['create', '新建'],
['update', '修改'],
['delete', '删除'],
]),
colorMap: new Map([
['create', 'green'],
['update', 'orange'],
['delete', 'red'],
]),
queryParams: {
page: 1,
page_size: 50,
scope: 'resource',
start: '',
end: '',
},
}
},
async created() {
this.$watch(
function() {
return this.resourceTableAttrList[3].choice_value
},
function() {
delete this.$refs.child.queryParams.link_id
}
)
await Promise.all([this.getAllApps(), this.getAllUsers()])
await this.getTable(this.queryParams)
},
updated() {
this.$refs.xTable.$el.querySelector('.vxe-table--body-wrapper').scrollTop = 0
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
windowHeightMinus() {
return this.isExpand ? 396 : 331
},
tableDataLength() {
return this.tableData.length
},
},
methods: {
async getTable(queryParams) {
try {
this.loading = true
const { data } = await searchResourceHistory(this.handleQueryParams(queryParams))
data.forEach((item) => {
item.originResource_ids = item?.extra?.resource_ids?.origin
item.currentResource_ids = item?.extra?.resource_ids?.current
this.handleChangeDescription(item, item.operate_type)
item.operate_uid = this.allUsersMap.get(item.operate_uid)
})
this.tableData = data
} finally {
this.loading = false
}
},
async getAllApps() {
const { apps } = await searchApp()
const allApps = []
apps.forEach((item) => {
allApps.push({ [item.name]: item.id })
})
this.allApps = allApps
this.resourceTableAttrList[1].choice_value = this.allApps
},
async getAllUsers() {
const { users } = await searchUser({ page_size: 10000, app_id: 'acl' })
const allUsers = []
const allUsersMap = new Map()
users.forEach((item) => {
allUsers.push({ [item.nickname]: item.uid })
allUsersMap.set(item.uid, item.nickname)
})
this.allUsers = allUsers
this.allUsersMap = allUsersMap
this.resourceTableAttrList[2].choice_value = this.allUsers
},
async getAllResources(app_id, page, value = undefined) {
if (!app_id) {
this.resourceTableAttrList[3].choice_value = []
return
}
const { resources, numfound } = await searchResource({
page: page,
page_size: 50,
app_id: app_id,
q: value,
})
this.resourcesNum = numfound
const allResources = this.allResources
resources.forEach((item) => {
allResources.push({ [item.name]: item.id })
})
this.allResources = allResources
this.resourceTableAttrList[3].choice_value = this.allResources
},
loadMoreResources(name, value) {
if (name === 'link_id' && this.allResources.length < this.resourcesNum) {
let currentPage = this.resourcesPage
this.getAllResources(this.app_id, ++currentPage, value)
this.resourcesPage = currentPage
}
},
resourceClear() {
this.resourcesPage = 1
this.allResources = []
this.getAllResources(this.app_id, 1)
},
async fetchResources(value) {
this.allResources = []
const allResources = []
if (!this.app_id) {
this.resourceTableAttrList[3].choice_value = []
return
}
this.resourcesPage = 1
if (value === '') {
this.getAllResources(this.app_id, 1)
return
}
const { resources, numfound } = await searchResource({
page: 1,
page_size: 50,
app_id: this.app_id,
q: value,
})
this.resourcesNum = numfound
resources.forEach((item) => {
allResources.push({ [item.name]: item.id })
})
this.allResources = allResources
this.resourceTableAttrList[3].choice_value = this.allResources
},
// searchForm相关
handleExpandChange(expand) {
this.isExpand = expand
},
handleSearch(queryParams) {
this.queryParams = { ...queryParams, scope: this.checked ? 'resource_group' : 'resource' }
this.getTable(this.queryParams)
},
searchFormReset() {
this.$refs.child.checked = false
this.checked = false
this.queryParams = {
page: 1,
page_size: 50,
scope: this.checked ? 'resource_group' : 'resource',
}
this.resourcesPage = 1
this.resourcesNum = 0
this.getTable(this.queryParams)
},
onSwitchChange(checked) {
this.checked = checked
this.queryParams.scope = checked ? 'resource_group' : 'resource'
this.queryParams.page = 1
this.getTable(this.queryParams)
},
async searchFormChange(queryParams) {
if (this.app_id !== queryParams.app_id) {
this.app_id = queryParams.app_id
this.allResources = []
this.resourcesPage = 1
this.resourcesNum = 0
await this.getAllResources(this.app_id, this.resourcesPage)
}
if (queryParams.app_id === undefined) {
this.app_id = undefined
this.$refs.child.queryParams.link_id = undefined
this.allResources = []
this.resourcesPage = 1
this.resourcesNum = 0
}
},
// pager相关
onShowSizeChange(size) {
this.queryParams.page_size = size
this.queryParams.page = 1
this.getTable(this.queryParams)
},
onChange(pageNum) {
this.queryParams.page = pageNum
this.getTable(this.queryParams)
},
handleQueryParams(queryParams) {
let flag = false
let q = queryParams.q ? queryParams.q : ''
for (const key in queryParams) {
if (
key !== 'page' &&
key !== 'page_size' &&
key !== 'app_id' &&
key !== 'q' &&
key !== 'start' &&
key !== 'end' &&
queryParams[key] !== undefined
) {
flag = true
if (q) q += `,${key}:${queryParams[key]}`
else q += `${key}:${queryParams[key]}`
delete queryParams[key]
}
}
const newQueryParams = { ...queryParams, q }
return flag ? newQueryParams : queryParams
},
handleTagColor(operateType) {
return this.colorMap.get(operateType)
},
handleChangeDescription(item, operate_type) {
switch (operate_type) {
// create
case 'create': {
item.description = `新建资源${item.current.name}`
break
}
case 'update': {
item.description = ''
for (const key in item.origin) {
const newVal = item.current[key]
const oldVal = item.origin[key]
if (!_.isEqual(newVal, oldVal) && key !== 'updated_at' && key !== 'deleted_at' && key !== 'created_at') {
if (oldVal === null) {
const str = ` ${key} : 改为 ${newVal} `
item.description += str
} else {
const str = ` ${key} : ${oldVal} 改为 ${newVal} `
item.description += str
}
}
}
const originResource_ids = item.originResource_ids
const currentResource_ids = item.currentResource_ids
if (!_.isEqual(originResource_ids, currentResource_ids)) {
if (originResource_ids.length === 0) {
const str = ` resource_ids : 新增 ${currentResource_ids} `
item.description += str
} else {
const str = ` resource_ids : ${originResource_ids} 改为 ${currentResource_ids} `
item.description += str
}
}
if (!item.description) item.description = '没有修改'
break
}
case 'delete': {
item.description = `删除资源${item.origin.name}`
break
}
}
},
},
}
</script>
<style lang="less" scoped>
.row {
margin-top: 5px;
}
.ant-tag {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
p {
margin-bottom: 0;
}
</style>

View File

@@ -0,0 +1,324 @@
<template>
<div>
<search-form
ref="child"
:attrList="resourceTableAttrList"
@search="handleSearch"
@searchFormReset="searchFormReset"
@searchFormChange="searchFormChange"
@expandChange="handleExpandChange"
></search-form>
<vxe-table
ref="xTable"
border
resizable
size="small"
:data="tableData"
:loading="loading"
:max-height="`${windowHeight - windowHeightMinus}px`"
>
<vxe-column field="created_at" width="144px" title="操作时间"></vxe-column>
<vxe-column field="operate_uid" width="130px" title="操作员"></vxe-column>
<vxe-column field="operate_type" width="80px" title="操作">
<template #default="{ row }">
<a-tag :color="handleTagColor(row.operate_type)">
{{ operateTypeMap.get(row.operate_type) }}
</a-tag>
</template>
</vxe-column>
<vxe-column field="link_id" width="159px" title="资源类型名">
<template #default="{ row }">
<span>
{{ row.current.name || row.origin.name }}
</span>
</template>
</vxe-column>
<vxe-column title="描述">
<template #default="{ row }">
<p>
{{ row.changeDescription }}
</p>
</template>
</vxe-column>
<vxe-column field="source" width="100px" title="来源"></vxe-column>
</vxe-table>
<pager
:current-page.sync="queryParams.page"
:page-size.sync="queryParams.page_size"
:page-sizes="[50,100,200]"
:total="tableDataLength"
:isLoading="loading"
@change="onChange"
@showSizeChange="onShowSizeChange"
></pager>
</div>
</template>
<script>
import _ from 'lodash'
import Pager from '../../module/pager.vue'
import SearchForm from '../../module/searchForm.vue'
import { searchResourceHistory } from '@/modules/acl/api/history'
import { searchUser } from '@/modules/acl/api/user'
import { searchResourceType } from '@/modules/acl/api/resource'
import { searchApp } from '@/modules/acl/api/app'
export default {
components: { SearchForm, Pager },
data() {
return {
loading: true,
checked: false,
isExpand: false,
app_id: undefined,
tableData: [],
allResourceTypes: [],
allUsers: [],
allApps: [],
allUsersMap: new Map(),
allResourcesMap: new Map(),
resourceTableAttrList: [
{
alias: '日期',
is_choice: false,
name: 'datetime',
value_type: '3'
},
{
alias: '应用',
is_choice: true,
name: 'app_id',
value_type: '2',
choice_value: []
},
{
alias: '操作员',
is_choice: true,
name: 'operate_uid',
value_type: '2',
choice_value: []
},
{
alias: '资源类型',
is_choice: true,
name: 'link_id',
value_type: '2',
choice_value: []
},
{
alias: '操作',
is_choice: true,
name: 'operate_type',
value_type: '2',
choice_value: [
{ '新建': 'create' },
{ '修改': 'update' },
{ '删除': 'delete' },
]
}
],
operateTypeMap: new Map([
['create', '新建'],
['update', '修改'],
['delete', '删除']
]),
colorMap: new Map([
['create', 'green'],
['update', 'orange'],
['delete', 'red']
]),
queryParams: {
page: 1,
page_size: 50,
scope: 'resource_type',
start: '',
end: ''
},
}
},
async created() {
this.$watch(
function () {
return this.resourceTableAttrList[3].choice_value
},
function () {
delete this.$refs.child.queryParams.link_id
}
)
await Promise.all([ this.getAllApps(), this.getAllUsers() ])
await this.getTable(this.queryParams)
},
updated() {
this.$refs.xTable.$el.querySelector('.vxe-table--body-wrapper').scrollTop = 0
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
windowHeightMinus() {
return this.isExpand ? 396 : 331
},
tableDataLength() {
return this.tableData.length
}
},
methods: {
async getTable(queryParams) {
try {
this.loading = true
const { data } = await searchResourceHistory(this.handleQueryParams(queryParams))
data.forEach(item => {
this.handleChangeDescription(item, item.operate_type)
item.operate_uid = this.allUsersMap.get(item.operate_uid)
})
this.tableData = data
} finally {
this.loading = false
}
},
async getAllApps() {
const { apps } = await searchApp()
const allApps = []
apps.forEach(item => { allApps.push({ [item.name]: item.id }) })
this.allApps = allApps
this.resourceTableAttrList[1].choice_value = this.allApps
},
async getAllUsers() {
const { users } = await searchUser({ page_size: 10000, app_id: 'acl' })
const allUsers = []
const allUsersMap = new Map()
users.forEach(item => {
allUsers.push({ [item.nickname]: item.uid })
allUsersMap.set(item.uid, item.nickname)
})
this.allUsers = allUsers
this.allUsersMap = allUsersMap
this.resourceTableAttrList[2].choice_value = this.allUsers
},
async getAllResourceTypes(app_id) {
if (!app_id) {
this.resourceTableAttrList[3].choice_value = []
return
}
const { groups } = await searchResourceType({
page: 1,
page_size: 9999,
app_id: app_id
})
const allResourceTypes = []
groups.forEach(item => { allResourceTypes.push({ [item.name]: item.id }) })
this.allResourceTypes = allResourceTypes
this.resourceTableAttrList[3].choice_value = this.allResourceTypes
},
// searchForm相关
handleExpandChange(expand) {
this.isExpand = expand
},
handleSearch(queryParams) {
this.queryParams = { ...queryParams, scope: 'resource_type' }
this.getTable(this.queryParams)
},
searchFormReset() {
this.$refs.child.checked = false
this.checked = false
this.queryParams = {
page: 1,
page_size: 50,
scope: 'resource_type',
}
this.getTable(this.queryParams)
},
async searchFormChange(queryParams) {
if (this.app_id !== queryParams.app_id) {
this.app_id = queryParams.app_id
await this.getAllResourceTypes(this.app_id)
}
if (queryParams.app_id === undefined) {
this.app_id = undefined
this.$refs.child.queryParams.link_id = undefined
}
},
// pager相关
onShowSizeChange(size) {
this.queryParams.page_size = size
this.queryParams.page = 1
this.getTable(this.queryParams)
},
onChange(pageNum) {
this.queryParams.page = pageNum
this.getTable(this.queryParams)
},
handleQueryParams(queryParams) {
let flag = false
let q = queryParams.q ? queryParams.q : ''
for (const key in queryParams) {
if (key !== 'page' && key !== 'page_size' && key !== 'app_id' && key !== 'q' && key !== 'start' && key !== 'end' && queryParams[key] !== undefined) {
flag = true
if (q) q += `,${key}:${queryParams[key]}`
else q += `${key}:${queryParams[key]}`
delete queryParams[key]
}
}
const newQueryParams = { ...queryParams, q }
return flag ? newQueryParams : queryParams
},
handleTagColor(operateType) {
return this.colorMap.get(operateType)
},
handleChangeDescription(item, operate_type) {
switch (operate_type) {
// create
case 'create': {
const description = item.current?.description === undefined ? '' : item.current?.description
const permission = item.extra.permission_ids?.current === undefined ? '' : item.extra.permission_ids?.current
item.changeDescription = `新增资源类型${item.current.name}\n描述${description}\n权限${permission}`
break
}
case 'update': {
item.changeDescription = ''
for (const key in item.origin) {
const newVal = item.current[key]
const oldVal = item.origin[key]
if (!_.isEqual(newVal, oldVal) && key !== 'updated_at' && key !== 'deleted_at' && key !== 'created_at') {
if (oldVal === null || oldVal === '') {
const str = ` ${key} : 改为 ${newVal} \n`
item.changeDescription += str
} else {
const str = ` ${key} : ${oldVal} 改为 ${newVal} \n`
item.changeDescription += str
}
}
}
const currentPerms = item.extra.permission_ids?.current === undefined ? '' : item.extra.permission_ids?.current
const originPerms = item.extra.permission_ids?.origin === undefined ? '' : item.extra.permission_ids?.origin
if (!_.isEqual(currentPerms, originPerms)) item.changeDescription += ` permission_ids : ${originPerms} 改为 ${currentPerms} `
if (!item.changeDescription) item.changeDescription = '没有修改'
break
}
case 'delete': {
const description = item.origin?.description === undefined ? '' : item.origin?.description
const permission = item.extra.permission_ids?.origin === undefined ? '' : item.extra.permission_ids?.origin
item.changeDescription = `删除资源类型${item.origin.name}\n描述${description}\n权限${permission}`
break
}
}
}
}
}
</script>
<style lang="less" scoped>
.row {
margin-top: 5px;
}
.ant-tag {
max-width: 100%;
overflow: hidden;
text-overflow:ellipsis;
}
p {
margin-bottom: 0;
}
</style>

View File

@@ -0,0 +1,322 @@
<template>
<div>
<search-form
ref="child"
:attrList="roleTableAttrList"
:hasSwitch="true"
switchValue="角色关系"
@onSwitchChange="onSwitchChange"
@search="handleSearch"
@searchFormReset="searchFormReset"
></search-form>
<vxe-table
ref="xTable"
border
resizable
size="small"
:data="tableData"
:loading="loading"
:max-height="`${windowHeight - 331}px`"
>
<vxe-column field="created_at" width="144px" title="操作时间"></vxe-column>
<vxe-column field="operate_uid" width="130px" title="操作员"></vxe-column>
<vxe-column field="operate_type" width="112px" title="操作">
<template #default="{ row }">
<template>
<a-tag :color="handleTagColor(row.operate_type)">{{ operateTypeMap.get(row.operate_type) }}</a-tag>
</template>
</template>
</vxe-column>
<vxe-column :title="checked ? '角色' : '角色'">
<template #default="{ row }">
<template v-if="!checked">
<a-tag color="blue">{{ row.current.name || row.origin.name }}</a-tag>
</template>
<template v-else>
<a-tag v-for="(id,index) in row.extra.child_ids" :key="'child_ids_' + id + index" color="blue">{{ id }}</a-tag>
</template>
</template>
</vxe-column>
<vxe-column :title="checked ? '继承自' : '管理员'" :width="checked ? '350px' : '80px'">
<template #default="{ row }">
<template v-if="!checked">
<a-icon type="check" v-if="row.current.is_app_admin"/>
</template>
<template v-else>
<a-tag v-for="(id,index) in row.extra.parent_ids" :key="'parent_ids_' + id + index" color="cyan">{{ id }}</a-tag>
</template>
</template>
</vxe-column>
<vxe-column title="描述" v-if="!checked">
<template #default="{ row }">
<p>
{{ row.description }}
</p>
</template>
</vxe-column>
<vxe-column field="source" width="100px" title="来源"></vxe-column>
</vxe-table>
<pager
:current-page.sync="queryParams.page"
:page-size.sync="queryParams.page_size"
:page-sizes="[50,100,200]"
:total="tableDataLength"
:isLoading="loading"
@change="onChange"
@showSizeChange="onShowSizeChange"
></pager>
</div>
</template>
<script>
import _ from 'lodash'
import Pager from '../../module/pager.vue'
import SearchForm from '../../module/searchForm.vue'
import { searchRoleHistory } from '@/modules/acl/api/history'
import { searchApp } from '@/modules/acl/api/app'
import { searchUser } from '@/modules/acl/api/user'
export default {
components: { SearchForm, Pager },
data() {
return {
loading: true,
checked: false,
app_id: undefined,
tableData: [],
allApps: [],
allUsers: [],
allRoles: [],
allRolesMap: new Map(),
allUsersMap: new Map(),
operateTypeMap: new Map([
['create', '新建'],
['delete', '删除'],
['update', '修改'],
['role_relation_add', '添加角色关系'],
['role_relation_delete', '删除角色关系'],
]),
colorMap: new Map([
['create', 'green'],
['delete', 'red'],
['update', 'orange'],
['role_relation_add', 'green'],
['role_relation_delete', 'red']
]),
queryParams: {
page: 1,
page_size: 50,
scope: 'role',
start: '',
end: ''
},
roleTableAttrList: [
{
alias: '日期',
is_choice: false,
name: 'datetime',
value_type: '3'
},
{
alias: '应用',
is_choice: true,
name: 'app_id',
value_type: '2',
choice_value: []
},
{
alias: '操作员',
is_choice: true,
name: 'operate_uid',
value_type: '2',
choice_value: []
},
{
alias: '操作',
is_choice: true,
name: 'operate_type',
value_type: '2',
choice_value: [
{ '新建': 'create' },
{ '修改': 'update' },
{ '删除': 'delete' },
{ '添加角色关系': 'role_relation_add' },
{ '删除角色关系': 'role_relation_delete' },
]
}
]
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
tableDataLength() {
return this.tableData.length
}
},
async created() {
await Promise.all([ this.getAllApps(), this.getAllUsers() ])
await this.getTable(this.queryParams)
},
updated() {
this.$refs.xTable.$el.querySelector('.vxe-table--body-wrapper').scrollTop = 0
},
methods: {
async getTable(queryParams) {
try {
this.loading = true
const { data, id2roles, id2perms, id2resources } = await searchRoleHistory(this.handleQueryParams(queryParams))
data.forEach(item => {
item.operate_uid = this.allUsersMap.get(item.operate_uid)
if (item.operate_type === 'role_relation_add' || item.operate_type === 'role_relation_delete') {
item.extra.child_ids.forEach((subItem, index) => { item.extra.child_ids[index] = id2roles[subItem].name })
item.extra.parent_ids.forEach((subItem, index) => { item.extra.parent_ids[index] = id2roles[subItem].name })
} else {
this.handleChangeDescription(item, item.operate_type, id2roles, id2perms, id2resources)
}
})
this.tableData = data
} finally {
this.loading = false
}
},
async getAllApps() {
const { apps } = await searchApp()
const allApps = []
apps.forEach(item => { allApps.push({ [item.name]: item.id }) })
this.allApps = allApps
this.roleTableAttrList[1].choice_value = this.allApps
},
async getAllUsers() {
const { users } = await searchUser({ page_size: 10000, app_id: 'acl' })
const allUsers = []
const allUsersMap = new Map()
users.forEach(item => {
allUsers.push({ [item.nickname]: item.uid })
allUsersMap.set(item.uid, item.nickname)
})
this.allUsers = allUsers
this.allUsersMap = allUsersMap
this.roleTableAttrList[2].choice_value = this.allUsers
},
// pager相关
onShowSizeChange(size) {
this.queryParams.page_size = size
this.queryParams.page = 1
this.getTable(this.queryParams)
},
onChange(pageNum) {
this.queryParams.page = pageNum
this.getTable(this.queryParams)
},
// searchForm相关
onSwitchChange(checked) {
this.checked = checked
this.queryParams.scope = checked ? 'role_relation' : 'role'
this.queryParams.page = 1
this.getTable(this.queryParams)
},
handleSearch(queryParams) {
this.queryParams = { ...queryParams, scope: this.checked ? 'role_relation' : 'role' }
this.getTable(this.queryParams)
},
searchFormReset() {
this.$refs.child.checked = false
this.checked = false
this.queryParams = {
page: 1,
page_size: 50,
scope: this.checked ? 'role_relation' : 'role'
}
this.getTable(this.queryParams)
},
// 处理查询参数
handleQueryParams(queryParams) {
let flag = false
let q = queryParams.q ? queryParams.q : ''
for (const key in queryParams) {
if (key !== 'page' && key !== 'page_size' && key !== 'app_id' && key !== 'q' && key !== 'start' && key !== 'end' && queryParams[key] !== undefined) {
flag = true
if (q) q += `,${key}:${queryParams[key]}`
else q += `${key}:${queryParams[key]}`
delete queryParams[key]
}
}
const newQueryParams = { ...queryParams, q }
return flag ? newQueryParams : queryParams
},
// 处理tag颜色
handleTagColor(operateType) {
return this.colorMap.get(operateType)
},
handleChangeDescription(item, operate_type, id2roles, id2perms, id2resources) {
switch (operate_type) {
// create
case 'create': {
item.description = `新建角色${item.current.name}`
break
}
case 'update': {
item.description = ''
for (const key in item.origin) {
const newVal = item.current[key]
const oldVal = item.origin[key]
if (!_.isEqual(newVal, oldVal) && key !== 'updated_at' && key !== 'deleted_at' && key !== 'created_at') {
if (oldVal === null) {
const str = ` ${key} : 改为 ${newVal} `
item.description += str
} else {
const str = ` ${key} : ${oldVal} 改为 ${newVal} `
item.description += str
}
}
}
if (!item.description) item.description = '没有修改'
break
}
case 'delete': {
const { extra: { child_ids, parent_ids, role_permissions } } = item
child_ids.forEach((subItem, index) => { child_ids[index] = id2roles[subItem].name })
parent_ids.forEach((subItem, index) => { parent_ids[index] = id2roles[subItem].name })
const resourceMap = new Map()
const permsArr = []
role_permissions.forEach(subItem => {
const resource_id = subItem.resource_id
const perm_id = subItem.perm_id
if (resourceMap.has(resource_id)) {
let resource_perms = resourceMap.get(resource_id)
resource_perms += `,${id2perms[perm_id].name}`
resourceMap.set(resource_id, resource_perms)
} else {
resourceMap.set(resource_id, String(id2perms[perm_id].name))
}
})
resourceMap.forEach((value, key) => {
permsArr.push(`${id2resources[key].name}${value}`)
})
item.description = `继承者${child_ids}\n继承自${parent_ids}\n涉及资源及权限\n${permsArr.join(`\n`)}`
break
}
}
},
}
}
</script>
<style lang="less" scoped>
.row {
margin-top: 5px;
}
.ant-tag {
max-width: 100%;
overflow: hidden;
text-overflow:ellipsis;
}
p {
margin-bottom: 0;
}
</style>

View File

@@ -0,0 +1,346 @@
<template>
<div>
<search-form
ref="child"
:attrList="triggerTableAttrList"
@searchFormReset="searchFormReset"
@search="handleSearch"
@expandChange="handleExpandChange"
@searchFormChange="searchFormChange"
></search-form>
<vxe-table
ref="xTable"
border
resizable
size="small"
:data="tableData"
:loading="loading"
:max-height="`${windowHeight - windowHeightMinus}px`"
>
<vxe-column field="created_at" width="144px" title="操作时间"></vxe-column>
<vxe-column field="operate_uid" width="130px" title="操作员"></vxe-column>
<vxe-column field="operate_type" width="80px" title="操作">
<template #default="{ row }">
<a-tag :color="handleTagColor(row.operate_type)">
{{ operateTypeMap.get(row.operate_type) }}
</a-tag>
</template>
</vxe-column>
<vxe-column field="trigger_id" width="250px" title="触发器">
<template #default="{ row }">
<span>
{{ row.current.name || row.origin.name }}
</span>
</template>
</vxe-column>
<vxe-column title="描述">
<template #default="{ row }">
<p>
{{ row.changeDescription }}
</p>
</template>
</vxe-column>
</vxe-table>
<pager
:current-page.sync="queryParams.page"
:page-size.sync="queryParams.page_size"
:page-sizes="[50,100,200]"
:total="tableDataLength"
:isLoading="loading"
@change="onChange"
@showSizeChange="onShowSizeChange"
></pager>
</div>
</template>
<script>
import _ from 'lodash'
import Pager from '../../module/pager.vue'
import SearchForm from '../../module/searchForm.vue'
import { searchTriggerHistory } from '@/modules/acl/api/history'
import { getTriggers } from '@/modules/acl/api/trigger'
import { searchUser } from '@/modules/acl/api/user'
import { searchApp } from '@/modules/acl/api/app'
export default {
components: { SearchForm, Pager },
data() {
return {
app_id: undefined,
loading: true,
isExpand: false,
tableData: [],
allResourceTypes: [],
allResources: [],
allUsers: [],
allRoles: [],
allTriggers: [],
allApps: [],
allRolesMap: new Map(),
allUsersMap: new Map(),
allResourceTypesMap: new Map(),
allResourcesMap: new Map(),
allTriggersMap: new Map(),
operateTypeMap: new Map([
['create', '新建'],
['update', '修改'],
['delete', '删除'],
['trigger_apply', '应用'],
['trigger_cancel', '取消'],
]),
colorMap: new Map([
['create', 'green'],
['delete', 'red'],
['update', 'orange'],
['trigger_apply', 'green'],
['trigger_cancel', 'red'],
]),
triggerTableAttrList: [
{
alias: '日期',
is_choice: false,
name: 'datetime',
value_type: '3'
},
{
alias: '应用',
is_choice: true,
name: 'app_id',
value_type: '2',
choice_value: []
},
{
alias: '操作员',
is_choice: true,
name: 'operate_uid',
value_type: '2',
choice_value: []
},
{
alias: '触发器',
is_choice: true,
name: 'trigger_id',
value_type: '2',
choice_value: []
},
{
alias: '操作',
is_choice: true,
name: 'operate_type',
value_type: '2',
choice_value: [
{ '新建': 'create' },
{ '修改': 'update' },
{ '删除': 'delete' },
{ '应用': 'trigger_apply' },
{ '取消': 'trigger_cancel' },
]
},
],
queryParams: {
page: 1,
page_size: 50,
start: '',
end: ''
},
}
},
async created() {
this.$watch(
function () {
return this.triggerTableAttrList[3].choice_value
},
function () {
delete this.$refs.child.queryParams.trigger_id
}
)
await Promise.all([ this.getAllApps(), this.getAllUsers() ])
await this.getTable(this.queryParams)
},
updated() {
this.$refs.xTable.$el.querySelector('.vxe-table--body-wrapper').scrollTop = 0
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
windowHeightMinus() {
return this.isExpand ? 396 : 331
},
tableDataLength() {
return this.tableData.length
}
},
methods: {
async getTable(queryParams) {
try {
this.loading = true
const { data, id2resource_types, id2roles } = await searchTriggerHistory(this.handleQueryParams(queryParams))
data.forEach(item => {
this.handleChangeDescription(item, item.operate_type, id2resource_types, id2roles)
item.trigger_id = this.allTriggersMap.get(item.trigger_id)
item.operate_uid = this.allUsersMap.get(item.operate_uid)
})
this.tableData = data
} finally {
this.loading = false
}
},
async getAllApps() {
const { apps } = await searchApp()
const allApps = []
apps.forEach(item => { allApps.push({ [item.name]: item.id }) })
this.allApps = allApps
this.triggerTableAttrList[1].choice_value = this.allApps
},
async getAllUsers() {
const { users } = await searchUser({ page_size: 10000, app_id: 'acl' })
const allUsers = []
const allUsersMap = new Map()
users.forEach(item => {
allUsers.push({ [item.nickname]: item.uid })
allUsersMap.set(item.uid, item.nickname)
})
this.allUsers = allUsers
this.allUsersMap = allUsersMap
this.triggerTableAttrList[2].choice_value = this.allUsers
},
async getTriggers(app_id) {
if (!app_id) {
this.triggerTableAttrList[3].choice_value = []
return
}
const res = await getTriggers({ app_id: app_id })
const allTriggers = []
const allTriggersMap = new Map()
res.forEach(item => {
allTriggers.push({ [item.name]: item.id })
allTriggersMap.set(item.id, item.name)
})
this.allTriggers = allTriggers
this.allTriggersMap = allTriggersMap
this.triggerTableAttrList[3].choice_value = this.allTriggers
},
// pager相关
onShowSizeChange(size) {
this.queryParams.page_size = size
this.queryParams.page = 1
this.getTable(this.queryParams)
},
onChange(pageNum) {
this.queryParams.page = pageNum
this.getTable(this.queryParams)
},
// searchForm相关
handleExpandChange(expand) {
this.isExpand = expand
},
handleSearch(queryParams) {
this.queryParams = queryParams
this.getTable(this.queryParams)
},
searchFormReset() {
this.queryParams = {
page: 1,
page_size: 50,
}
this.getTable(this.queryParams)
},
async searchFormChange(queryParams) {
if (this.app_id !== queryParams.app_id) {
this.app_id = queryParams.app_id
await this.getTriggers(this.app_id)
}
if (queryParams.app_id === undefined) {
this.app_id = undefined
this.$refs.child.queryParams.trigger_id = undefined
}
},
handleChangeDescription(item, operate_type, id2resource_types, id2roles) {
switch (operate_type) {
// create
case 'create': {
const str = item.current.roles
const newArr = str.slice(1, str.length - 1).split(', ')
const newStr = newArr.map(i => id2roles[i].name).join('')
const { name, resource_type_id, wildcard, permissions, enabled } = item.current
item.changeDescription = `新增触发器${name}\n资源类型${id2resource_types[resource_type_id].name}资源名${wildcard || ''}角色[${newStr}]\n权限${permissions}\n状态${enabled}`
break
}
case 'update': {
item.changeDescription = ''
for (const key in item.origin) {
const newVal = item.current[key]
const oldVal = item.origin[key]
if (!_.isEqual(newVal, oldVal) && key !== 'updated_at' && key !== 'deleted_at' && key !== 'created_at') {
if (oldVal === null) {
const str = ` ${key} : 改为 ${newVal} `
item.changeDescription += str
} else {
const str = ` ${key} : ${oldVal} 改为 ${newVal} `
item.changeDescription += ` ${key} : ${oldVal} 改为 ${newVal} `
}
}
}
if (!item.changeDescription) item.changeDescription = '没有修改'
break
}
case 'delete': {
const str = item.origin.roles
const newArr = str.slice(1, str.length - 1).split(', ')
const newStr = newArr.map(i => id2roles[i].name).join('')
const { name, resource_type_id, wildcard, permissions, enabled } = item.origin
item.changeDescription = `删除触发器${name}\n资源类型${id2resource_types[resource_type_id].name}资源名${wildcard || ''}角色[${newStr}]\n权限${permissions}\n状态${enabled}`
break
}
case 'trigger_apply': {
const str = item.current.roles
const newArr = str.slice(1, str.length - 1).split(', ')
const newStr = newArr.map(i => id2roles[i].name).join('')
const { name, resource_type_id, wildcard, permissions, enabled } = item.current
item.changeDescription = `应用触发器${name}\n资源类型${id2resource_types[resource_type_id].name}资源名${wildcard || ''}角色[${newStr}]\n权限${permissions}\n状态${enabled}`
break
}
case 'trigger_cancel': {
const str = item.current.roles
const newArr = str.slice(1, str.length - 1).split(', ')
const newStr = newArr.map(i => id2roles[i].name).join('')
const { name, resource_type_id, wildcard, permissions, enabled } = item.current
item.changeDescription = `取消触发器${name}\n资源类型${id2resource_types[resource_type_id].name}资源名${wildcard || ''}角色[${newStr}]\n权限${permissions}\n状态${enabled}`
break
}
}
},
handleQueryParams(queryParams) {
let q = ''
for (const key in queryParams) {
if (key !== 'page' && key !== 'page_size' && key !== 'app_id' && key !== 'start' && key !== 'end' && queryParams[key] !== undefined) {
if (q) {
q += `,${key}:${queryParams[key]}`
} else {
q += `${key}:${queryParams[key]}`
}
}
}
const newQueryParams = { ...queryParams, q }
return q ? newQueryParams : queryParams
},
handleTagColor(operateType) {
return this.colorMap.get(operateType)
}
}
}
</script>
<style lang="less" scoped>
p {
margin-bottom: 0;
}
.ant-tag {
max-width: 100%;
overflow: hidden;
text-overflow:ellipsis;
}
</style>

View File

@@ -0,0 +1,197 @@
<template>
<div :style="{ backgroundColor: '#fff', padding: '24px' }">
<div class="resource-types-action-btn">
<a-button @click="handleCreate" type="primary" style="margin-right: 0.3rem;">{{ btnName }}</a-button>
<a-input-search
:style="{ display: 'inline', marginLeft: '10px', width: '200px' }"
placeholder="搜索 | 资源类型名"
v-model="searchName"
allowClear
@search="
() => {
this.tablePage.currentPage = 1
this.searchData()
}
"
></a-input-search>
</div>
<a-spin
:spinning="loading"
><vxe-grid :columns="tableColumns" :data="groups" :max-height="`${windowHeight - 185}px`" highlight-hover-row>
<template #id_default="{row}">
<a-tag color="cyan" v-for="perm in id2perms[row.id]" :key="perm.id">{{ perm.name }}</a-tag>
</template>
<template #action_default="{row}">
<a @click="handleEdit(row)"><a-icon type="edit"/></a>
<a-divider type="vertical" />
<a-popconfirm title="确认删除?" @confirm="handleDelete(row)" okText="" cancelText="">
<a style="color: red"><a-icon type="delete"/></a>
</a-popconfirm>
</template>
<template #pager>
<vxe-pager
:layouts="['Total', 'PrevPage', 'JumpNumber', 'NextPage', 'Sizes']"
:current-page.sync="tablePage.currentPage"
:page-size.sync="tablePage.pageSize"
:total="tablePage.total"
:page-sizes="pageSizeOptions"
@page-change="handlePageChange"
>
</vxe-pager>
</template> </vxe-grid
></a-spin>
<resourceTypeForm ref="resourceTypeForm" :handleOk="handleOk"> </resourceTypeForm>
</div>
</template>
<script>
import { mapState } from 'vuex'
import resourceTypeForm from './module/resourceTypeForm'
import { deleteResourceTypeById, searchResourceType } from '@/modules/acl/api/resource'
export default {
name: 'ResourceType',
components: {
resourceTypeForm,
},
data() {
return {
loading: false,
groups: [],
id2perms: {},
btnName: '新增资源类型',
pageSizeOptions: [10, 25, 50, 100],
tablePage: {
total: 0,
currentPage: 1,
pageSize: 50,
},
tableColumns: [
{
title: '资源类型名',
field: 'name',
minWidth: '175px',
fixed: 'left',
showOverflow: 'tooltip',
},
{
title: '描述',
field: 'description',
minWidth: '175px',
},
{
title: '权限',
field: 'id',
minWidth: '300px',
slots: {
default: 'id_default',
},
},
{
title: '操作',
field: 'action',
minWidth: '175px',
slots: { default: 'action_default' },
fixed: 'right',
},
],
searchName: '',
}
},
beforeCreate() {},
mounted() {
this.searchData()
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
},
watch: {
'$route.name': function(newName, oldName) {
this.tablePage = {
total: 0,
currentPage: 1,
pageSize: 50,
}
this.searchData()
},
searchName: {
immediate: true,
handler(newVal, oldVal) {
if (!newVal) {
this.tablePage.currentPage = 1
this.searchData()
}
},
},
},
inject: ['reload'],
methods: {
searchData() {
const { currentPage, pageSize } = this.tablePage
this.loading = true
const param = {
app_id: this.$route.name.split('_')[0],
page_size: pageSize,
page: currentPage,
q: this.searchName,
}
searchResourceType(param).then((res) => {
this.tablePage = { ...this.tablePage, total: res.numfound, currentPage: res.page }
this.groups = res.groups
this.id2perms = res.id2perms
this.loading = false
})
},
handleEdit(record) {
var perms = []
var permList = this.id2perms[record.id]
if (permList) {
for (var i = 0; i < permList.length; i++) {
perms.push(permList[i].name)
}
}
record.perms = perms
this.$refs.resourceTypeForm.handleEdit(record)
},
handleDelete(record) {
this.deleteResourceType(record.id)
},
handleOk() {
this.searchData()
},
handleCreate() {
this.$refs.resourceTypeForm.handleCreate()
},
deleteResourceType(id) {
deleteResourceTypeById(id).then((res) => {
this.$message.success(`删除成功`)
this.handleOk()
})
// .catch(err => this.requestFailed(err))
},
// requestFailed(err) {
// const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
// this.$message.error(`${msg}`)
// },
handlePageChange({ currentPage, pageSize }) {
this.tablePage.currentPage = currentPage
this.tablePage.pageSize = pageSize
this.searchData()
},
},
}
</script>
<style lang="less" scoped>
.resource-types-action-btn {
width: 100%;
display: inline-flex;
margin-bottom: 15px;
align-items: center;
}
</style>

View File

@@ -0,0 +1,378 @@
<template>
<div :style="{ backgroundColor: '#fff', padding: '24px' }">
<div v-if="allResourceTypes.length > 0">
<a-tabs default-active-key="1" @change="loadCurrentType">
<a-tab-pane v-for="rtype in allResourceTypes" :key="rtype.id" :tab="rtype.name"> </a-tab-pane>
</a-tabs>
<div class="resources-action-btn">
<a-space>
<a-button @click="handleCreate" type="primary">{{ btnName }}</a-button>
<a-input-search
placeholder="搜索 | 资源名"
v-model="searchName"
@search="
() => {
this.tablePage.currentPage = 1
this.searchData()
}
"
></a-input-search>
<a-dropdown v-if="selectedRows && selectedRows.length">
<a-button type="primary" ghost>
批量操作
</a-button>
<a-menu slot="overlay">
<a-menu-item @click="handleBatchPerm">
<a href="javascript:;">授权</a>
</a-menu-item>
<a-menu-item @click="handleBatchRevoke">
<a href="javascript:;">权限回收</a>
</a-menu-item>
</a-menu>
</a-dropdown>
<span
v-if="selectedRows && selectedRows.length"
>已选择<strong>{{ selectedRows.length }}</strong
></span
>
</a-space>
<a-space>
<a-button
type="primary"
ghost
@click="
() => {
$refs.resourceBatchPerm.open(currentType.id)
}
"
>便捷授权</a-button
>
<a-switch
v-model="isGroup"
@change="
() => {
searchName = ''
tablePage.currentPage = 1
searchData()
selectedRows = []
$refs.xTable && $refs.xTable.clearCheckboxRow()
$refs.xTable && $refs.xTable.clearCheckboxReserve()
}
"
un-checked-children=""
></a-switch>
</a-space>
</div>
<a-spin
:spinning="loading"
><vxe-grid
:columns="tableColumns"
:data="tableData"
highlight-hover-row
:max-height="`${windowHeight - 245}px`"
:checkbox-config="{ reserve: true }"
@checkbox-change="changeCheckbox"
@checkbox-all="changeCheckbox"
ref="xTable"
row-id="id"
show-overflow
>
<template #name_header>
{{ isGroup ? '资源组名' : '资源名' }}
</template>
<template #action_default="{row}">
<span v-show="isGroup">
<a @click="handleDisplayMember(row)">成员</a>
<a-divider type="vertical" />
<a @click="handleGroupEdit(row)">编辑</a>
<a-divider type="vertical" />
</span>
<a-tooltip title="查看授权">
<a @click="handlePerm(row)"><a-icon type="eye"/></a>
</a-tooltip>
<a-divider type="vertical" />
<a-tooltip title="授权">
<a :style="{ color: '#4bbb13' }" @click="handlePermManage(row)">
<a-icon type="usergroup-add" />
</a>
</a-tooltip>
<a-divider type="vertical" />
<a-popconfirm title="确认删除?" @confirm="handleDelete(row)" @cancel="cancel" okText="" cancelText="">
<a style="color: red"><a-icon type="delete"/></a>
</a-popconfirm>
</template>
<template #pager>
<vxe-pager
:layouts="['Total', 'PrevPage', 'JumpNumber', 'NextPage', 'Sizes']"
:current-page.sync="tablePage.currentPage"
:page-size.sync="tablePage.pageSize"
:total="tablePage.total"
:page-sizes="pageSizeOptions"
@page-change="handlePageChange"
>
</vxe-pager>
</template> </vxe-grid
></a-spin>
</div>
<div v-else style="text-align: center">
<a-icon style="font-size:50px; margin-bottom: 20px; color: orange" type="info-circle" />
<h3>暂无类型信息请先添加资源类型</h3>
</div>
<resourceForm ref="resourceForm" @fresh="handleOk"> </resourceForm>
<resourcePermForm ref="resourcePermForm"> </resourcePermForm>
<ResourcePermManageForm
ref="resourcePermManageForm"
:groupTypeMessage="currentType"
@close="closePerm"
></ResourcePermManageForm>
<resource-group-modal ref="resourceGroupModal"></resource-group-modal>
<resource-group-member ref="resourceGroupMember"></resource-group-member>
<ResourceBatchPerm ref="resourceBatchPerm" />
</div>
</template>
<script>
import { mapState } from 'vuex'
import resourceForm from './module/resourceForm'
import resourcePermForm from './module/resourcePermForm'
import ResourceGroupModal from './module/resourceGroupModal'
import ResourceGroupMember from './module/resourceGroupMember'
import ResourcePermManageForm from './module/resourcePermManageForm'
import ResourceBatchPerm from './module/resourceBatchPerm.vue'
import {
deleteResourceById,
searchResource,
searchResourceType,
getResourceGroups,
deleteResourceGroup,
} from '@/modules/acl/api/resource'
export default {
name: 'Resources',
components: {
resourceForm,
resourcePermForm,
ResourceGroupModal,
ResourceGroupMember,
ResourcePermManageForm,
ResourceBatchPerm,
},
data() {
return {
loading: false,
tablePage: {
total: 0,
currentPage: 1,
pageSize: 50,
},
tableData: [],
tableColumns: [
{
type: 'checkbox',
fixed: 'left',
},
{
title: '资源名',
field: 'name',
minWidth: '150px',
showOverflow: 'tooltip',
fixed: 'left',
slots: {
header: 'name_header',
},
},
{
title: '创建者',
minWidth: '100px',
field: 'user',
},
{
title: '创建时间',
minWidth: '220px',
field: 'created_at',
align: 'center',
},
{
title: '最后修改时间',
minWidth: '220px',
field: 'updated_at',
align: 'center',
},
{
title: '操作',
field: 'action',
width: '200px',
fixed: 'right',
align: 'center',
slots: {
default: 'action_default',
},
},
],
btnName: '新增资源',
isGroup: false,
allResourceTypes: [],
currentType: { id: 0 },
pageSizeOptions: [10, 25, 50, 100],
searchName: '',
selectedRows: [],
}
},
beforeCreate() {
this.form = this.$form.createForm(this)
},
computed: mapState({
windowHeight: (state) => state.windowHeight,
}),
mounted() {
// this.getAllResourceTypes()
},
inject: ['reload'],
beforeRouteEnter(to, from, next) {
next((vm) => {
vm.getAllResourceTypes()
})
},
methods: {
async searchData() {
const { currentPage, pageSize } = this.tablePage
const param = {
app_id: this.$route.name.split('_')[0],
resource_type_id: this.currentType.id,
page_size: pageSize,
page: currentPage,
q: this.searchName,
}
this.loading = true
if (!this.isGroup) {
await searchResource(param).then((res) => {
this.tablePage = { ...this.tablePage, total: res.numfound, currentPage: res.page }
this.tableData = res.resources
this.loading = false
})
} else {
await getResourceGroups(param).then((res) => {
this.tablePage = { ...this.tablePage, total: res.numfound, currentPage: res.page }
this.tableData = res.groups
this.loading = false
})
}
},
handleDisplayMember(record) {
this.$refs['resourceGroupMember'].handleEdit(record)
},
handleGroupEdit(record) {
this.$refs['resourceGroupModal'].handleEdit(record)
},
async getAllResourceTypes() {
await searchResourceType({ page_size: 9999, app_id: this.$route.name.split('_')[0] }).then((res) => {
this.allResourceTypes = res.groups
if (res.groups && res.groups.length) {
this.loadCurrentType(res.groups[0].id)
}
})
},
handlePermManage(record) {
this.$refs['resourcePermManageForm'].editPerm(record, this.isGroup) // todo fix
},
loadCurrentType(rtypeId) {
this.searchName = ''
this.selectedRows = []
this.tablePage.currentPage = 1
this.$refs.xTable && this.$refs.xTable.clearCheckboxRow()
this.$refs.xTable && this.$refs.xTable.clearCheckboxReserve()
if (rtypeId) {
this.currentType = this.allResourceTypes.find((item) => item.id === rtypeId)
}
this.searchData()
},
handlePerm(record) {
this.$refs.resourcePermForm.handlePerm(record, this.isGroup)
},
handleDelete(record) {
this.deleteResource(record.id)
},
handleOk() {
this.tablePage.currentPage = 1
this.searchData()
},
handleCreate() {
this.$refs.resourceForm.handleCreate(this.currentType)
},
deleteResource(id) {
if (!this.isGroup) {
deleteResourceById(id).then((res) => {
this.$message.success(`删除成功`)
this.handleOk()
})
// .catch(err => this.requestFailed(err))
} else {
deleteResourceGroup(id).then((res) => {
this.$message.success(`删除成功`)
this.handleOk()
})
// .catch(err => this.requestFailed(err))
}
},
// requestFailed(err) {
// const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
// this.$message.error(`${msg}`)
// },
cancel() {},
handlePageChange({ currentPage, pageSize }) {
this.tablePage.currentPage = currentPage
this.tablePage.pageSize = pageSize
this.searchData()
},
changeCheckbox({ records }) {
// console.log(records)
this.selectedRows = this.$refs.xTable.getCheckboxRecords().concat(this.$refs.xTable.getCheckboxReserveRecords())
},
handleBatchPerm() {
this.$refs['resourcePermManageForm'].editPerm(this.selectedRows, this.isGroup)
},
handleBatchRevoke() {
this.$refs['resourcePermManageForm'].editPerm(this.selectedRows, this.isGroup, 'revoke')
},
closePerm() {
this.$refs.xTable.clearCheckboxRow()
this.selectedRows = []
},
},
watch: {
'$route.name': function(newName, oldName) {
this.isGroup = false
this.tablePage = {
total: 0,
currentPage: 1,
pageSize: 50,
}
},
searchName: {
immediate: true,
handler(newVal) {
if (!newVal) {
this.tablePage.currentPage = 1
this.searchData()
}
},
},
},
}
</script>
<style lang="less" scoped>
.resources-action-btn {
width: 100%;
display: inline-flex;
margin-bottom: 15px;
align-items: center;
justify-content: space-between;
.ant-switch {
margin-left: auto;
}
}
</style>

View File

@@ -0,0 +1,311 @@
<template>
<div class="acl-role">
<div class="roles-action-btn">
<a-button @click="handleCreate" type="primary">{{ btnName }}</a-button>
<a-input-search
allowClear
:style="{ display: 'inline', marginLeft: '10px', width: '200px' }"
placeholder="搜索 | 角色名"
v-model="searchName"
@search="
() => {
this.tablePage.currentPage = 1
this.loadData()
}
"
></a-input-search>
<a-checkbox :checked="is_all" @click="handleClickBoxChange">所有角色</a-checkbox>
</div>
<a-spin :spinning="loading">
<vxe-grid
:columns="tableColumns"
:data="tableData"
:max-height="`${windowHeight - 185}px`"
highlight-hover-row
min-height="300px"
:filter-config="{ remote: true }"
@filter-change="filterTableMethod"
>
<template #is_app_admin_default="{row}">
<a-icon type="check" v-if="row.is_app_admin" />
</template>
<template #inherit_default="{row}">
<a-tag color="cyan" v-for="role in id2parents[row.id]" :key="role.id">{{ role.name }}</a-tag>
</template>
<template #isVisualRole_default="{row}">
{{ row.uid ? '' : '' }}
</template>
<template #action_default="{row}">
<div style="width:300px">
<span>
<a-tooltip title="资源列表">
<a
v-if="$route.name !== 'acl_roles'"
@click="handleDisplayUserResource(row)"
><a-icon
type="file-search"
/></a>
</a-tooltip>
<a-divider type="vertical" />
<div v-if="!row.uid" style="display: inline-block">
<a-tooltip
title="用户列表"
><a @click="handleDisplayUserUnderRole(row)"><a-icon type="team"/></a
></a-tooltip>
<a-divider type="vertical" />
</div>
<a @click="handleEdit(row)"><a-icon type="edit"/></a>
<a-divider type="vertical" />
<a-popconfirm title="确认删除?" @confirm="handleDelete(row)" @cancel="cancel" okText="" cancelText="">
<a style="color: red"><a-icon type="delete"/></a>
</a-popconfirm>
</span>
</div>
</template>
<template #pager>
<vxe-pager
:layouts="['Total', 'PrevPage', 'JumpNumber', 'NextPage', 'Sizes']"
:current-page.sync="tablePage.currentPage"
:page-size.sync="tablePage.pageSize"
:total="tablePage.total"
:page-sizes="pageSizeOptions"
@page-change="handlePageChange"
>
</vxe-pager>
</template>
</vxe-grid>
</a-spin>
<roleForm ref="roleForm" :allRoles="allRoles" :id2parents="id2parents" :handleOk="handleOk"></roleForm>
<resource-user-form ref="ruf"></resource-user-form>
<users-under-role-form ref="usersUnderRoleForm" :allRoles="allRoles"></users-under-role-form>
</div>
</template>
<script>
import { mapState } from 'vuex'
import roleForm from './module/roleForm'
import resourceUserForm from './module/resourceUserForm'
import usersUnderRoleForm from './module/usersUnderRoleForm'
import { deleteRoleById, searchRole } from '@/modules/acl/api/role'
export default {
name: 'Roles',
components: {
roleForm,
resourceUserForm,
usersUnderRoleForm,
},
data() {
return {
loading: false,
tablePage: {
total: 0,
currentPage: 1,
pageSize: 50,
},
tableColumns: [
{
title: '角色名',
field: 'name',
sortable: true,
minWidth: '150px',
fixed: 'left',
showOverflow: 'tooltip',
},
{
title: '管理员',
field: 'is_app_admin',
minWidth: '100px',
align: 'center',
slots: { default: 'is_app_admin_default' },
},
{
title: '继承自',
field: 'id',
minWidth: '150px',
slots: { default: 'inherit_default' },
},
{
title: '虚拟角色',
field: 'uid',
minWidth: '100px',
align: 'center',
filters: [
{ label: '', value: 1 },
{ label: '', value: 0 },
],
filterMultiple: false,
filterMethod: ({ value, row }) => {
return value === !row.uid
},
slots: { default: 'isVisualRole_default' },
},
{
title: '操作',
minWidth: '280px',
field: 'action',
fixed: 'right',
slots: { default: 'action_default' },
},
],
btnName: '新增虚拟角色',
is_all: this.$route.name === 'acl_roles',
tableData: [],
allRoles: [],
id2parents: {},
pageSizeOptions: [10, 25, 50, 100],
searchName: '',
filterTableValue: { user_role: 1, user_only: 0 },
}
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
},
watch: {
'$route.name': function(newName, oldName) {
this.tablePage = {
total: 0,
currentPage: 1,
pageSize: 50,
}
this.tableData = []
this.allRoles = []
this.loadData()
this.initData()
},
searchName: {
immediate: true,
handler(newVal) {
if (!newVal) {
this.tablePage.currentPage = 1
this.loadData()
}
},
},
},
mounted() {
this.initData()
},
inject: ['reload'],
methods: {
handleClickBoxChange() {
this.is_all = !this.is_all
this.$nextTick(() => {
this.tablePage.currentPage = 1
this.loadData()
})
},
initData() {
console.log(111)
searchRole({
app_id: this.$route.name.split('_')[0],
page_size: 9999,
}).then((res) => {
this.allRoles = res.roles
})
},
loadData() {
console.log(222)
this.loading = true
const { currentPage, pageSize } = this.tablePage
searchRole({
...this.filterTableValue,
app_id: this.$route.name.split('_')[0],
page_size: pageSize,
page: currentPage,
is_all: this.is_all,
q: this.searchName,
})
.then((res) => {
this.tableData = res.roles
this.id2parents = res.id2parents
this.tablePage.total = res.numfound
this.loading = false
})
.catch(() => {
this.loading = false
})
},
handleDisplayUserResource(record) {
this.$refs.ruf.loadUserResource(record)
},
handleDisplayUserUnderRole(record) {
this.$refs.usersUnderRoleForm.handleProcessRole(record.id)
},
handleEdit(record) {
this.$refs.roleForm.handleEdit(record)
},
handleDelete(record) {
this.deleteRole(record.id)
},
handleOk() {
this.loadData()
this.initData()
},
handleCreate() {
this.$refs.roleForm.handleCreate()
},
deleteRole(id) {
deleteRoleById(id, { app_id: this.$route.name.split('_')[0] }).then((res) => {
this.$message.success(`删除成功`)
this.handleOk()
})
// .catch(err => this.requestFailed(err))
},
// requestFailed(err) {
// const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
// this.$message.error(`${msg}`)
// },
cancel(e) {
return false
},
handlePageChange({ currentPage, pageSize }) {
this.tablePage.currentPage = currentPage
this.tablePage.pageSize = pageSize
this.loadData()
},
filterTableMethod({ column, property, values, datas, filterList, $event }) {
if (property === 'uid') {
if (values[0] === 1) {
this.filterTableValue = { user_role: 0 }
} else if (values[0] === 0) {
this.filterTableValue = {
user_only: 1,
}
} else {
this.filterTableValue = {
user_role: 1,
user_only: 0,
}
}
}
this.tablePage.currentPage = 1
this.loadData()
},
},
}
</script>
<style lang="less">
.acl-role {
background-color: #fff;
padding: 24px;
.roles-action-btn {
width: 100%;
display: inline-flex;
margin-bottom: 15px;
align-items: center;
.ant-checkbox-wrapper {
margin-left: auto;
}
}
.vxe-table--filter-body {
min-height: 60px;
}
}
</style>

View File

@@ -0,0 +1,98 @@
<template>
<div class="acl-secret-key">
<a-form-model
ref="secretKeyForm"
:model="displayForm"
:rules="rules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 12 }"
>
<a-form-model-item label="Key" prop="key">
<a-input disabled v-model="displayForm.key" />
</a-form-model-item>
<a-form-model-item label="Secret" prop="secret">
<a-input disabled v-model="displayForm.secret" />
</a-form-model-item>
<a-form-model-item label=" " :colon="false">
<a-space>
<a-button type="primary" @click="changeVisible">{{ !visible ? '查看' : '隐藏' }}</a-button>
<a-button type="danger" ghost @click="handleSumbit">重置</a-button>
<!-- <a-button @click="handleCancel">取消</a-button> -->
</a-space>
</a-form-model-item>
</a-form-model>
</div>
</template>
<script>
import { getSecret, updateSecret } from '../api/secretKey'
export default {
name: 'SecretKey',
data() {
return {
form: {
key: '',
secret: '',
},
rules: {
key: [{ required: true, message: 'key is required' }],
secret: [{ required: true, message: 'secret is required' }],
},
visible: false,
}
},
computed: {
displayForm() {
return {
key: this.visible ? this.form.key : this.form.key.replace(/./g, '*'),
secret: this.visible ? this.form.secret : this.form.secret.replace(/./g, '*'),
}
},
},
mounted() {
this.getOriginSecret()
},
methods: {
getOriginSecret() {
getSecret().then((res) => {
const { key, secret } = res
this.form = { key, secret }
})
},
handleSumbit() {
const that = this
this.$confirm({
title: '重置',
content: '确定重置用户密钥?',
onOk() {
that.$refs.secretKeyForm.validate((valid) => {
if (valid) {
updateSecret().then((res) => {
that.$message.success('重置成功')
const { key, secret } = res
that.form = { key, secret }
})
}
})
},
})
},
handleCancel() {
this.getOriginSecret()
},
changeVisible() {
this.visible = !this.visible
},
},
}
</script>
<style lang="less">
.acl-secret-key {
background-color: #fff;
padding: 24px;
.ant-input[disabled] {
color: rgba(0, 0, 0, 0.5);
}
}
</style>

View File

@@ -0,0 +1,318 @@
<template>
<div :style="{ backgroundColor: '#fff', padding: '24px' }">
<div class="trigger-action-btn">
<a-button type="primary" @click="handleCreateTrigger">新增触发器</a-button>
<a-input-search
:style="{ display: 'inline', marginLeft: '10px', width: '200px' }"
placeholder="搜索 | 名称"
v-model="searchName"
allowClear
@search="filter"
></a-input-search>
</div>
<vxe-grid
:columns="tableColumns"
:data="filterTriggers"
:max-height="`${windowHeight - 185}px`"
highlight-hover-row
>
<template #wildcard_default="{row}">
<div style="word-break: break-word">
<span>{{ row.wildcard }}</span>
</div>
</template>
<template #resourceTypeRender_default="{row}">
{{
resourceTypeList.filter((item) => item.id === row.resource_type_id)[0]
? resourceTypeList.filter((item) => item.id === row.resource_type_id)[0].name
: 'unkown'
}}
</template>
<template #users_default="{row}">
<span
v-for="(u, index) in row.users"
:key="index"
>{{ u }}<a-divider
v-if="index < row.users.length - 1"
type="vertical"
/></span>
</template>
<template #permissions_default="{row}">
<a-tag v-for="(p, index) in row.permissions" :key="index">{{ p }}</a-tag>
</template>
<template #enabled_default="{row}">
<a-tag v-if="row.enabled" color="#2db7f5">启用</a-tag>
<a-tag v-else color="grey">禁用</a-tag>
</template>
<template #action_default="{row}">
<a-space>
<a-tooltip title="应用">
<a @click="handleApplyTrigger(row)" :style="{ color: '#0f9d58' }"><a-icon type="appstore"/></a>
</a-tooltip>
<a-tooltip title="取消">
<a @click="handleCancelTrigger(row)" :style="{ color: 'orange' }"><a-icon type="stop"/></a>
</a-tooltip>
<a-tooltip title="查看正则匹配结果">
<a @click="handlePattern(row)" :style="{ color: 'purple' }"><a-icon type="eye"/></a>
</a-tooltip>
<a @click="handleEditTrigger(row)"><a-icon type="edit"/></a>
<a @click="handleDeleteTrigger(row)" :style="{ color: 'red' }"><a-icon type="delete"/></a>
</a-space>
</template>
</vxe-grid>
<trigger-form
ref="triggerForm"
:roles="roles"
:resourceTypeList="resourceTypeList"
:id2perms="id2perms"
@refresh="loadTriggers"
:app_id="app_id"
></trigger-form>
<TriggerPattern ref="triggerPattern" :roles="roles" />
</div>
</template>
<script>
import { mapState } from 'vuex'
import TriggerForm from './module/triggerForm'
import TriggerPattern from './module/triggerPattern'
import { getTriggers, deleteTrigger, applyTrigger, cancelTrigger } from '@/modules/acl/api/trigger'
import { searchRole } from '@/modules/acl/api/role'
import { searchResourceType } from '@/modules/acl/api/resource'
export default {
name: 'ACLTrigger',
components: { TriggerForm, TriggerPattern },
data() {
return {
roles: [],
searchName: '',
resourceTypeList: [],
filterTriggers: [],
triggers: [],
id2parents: [],
id2perms: {},
tableColumns: [
{
title: '名称',
field: 'name',
sortable: true,
minWidth: '150px',
fixed: 'left',
showOverflow: 'tooltip',
},
{
title: '资源名',
field: 'wildcard',
minWidth: '250px',
showOverflow: 'tooltip',
slots: {
default: 'wildcard_default',
},
},
{
title: '资源类型',
field: 'resource_type_id',
minWidth: '100px',
slots: {
default: 'resourceTypeRender_default',
},
},
{
title: '创建人',
field: 'users',
minWidth: '150px',
showOverflow: 'tooltip',
slots: {
default: 'users_default',
},
},
{
title: '角色',
field: 'roles',
minWidth: '150px',
slots: {
default: ({ row }) => {
const roleNameList = row.roles.map((item) => {
const temp = this.roles.find((a) => a.id === item)
if (temp) {
return temp.name
}
return 'unknown'
})
return [
<div>
{roleNameList.length <= 1 && roleNameList.map((item) => <span key={item}>{item}</span>)}
{roleNameList.length > 1 && (
<a-tooltip>
<template slot="title">
{roleNameList.map((item, idx) => (
<span>
<span key={item}>{item}</span>
{idx !== roleNameList.length - 1 && <a-divider type="vertical" />}
</span>
))}
</template>
<span>{roleNameList[0]}...</span>
</a-tooltip>
)}
</div>,
]
},
},
},
{
title: '权限',
field: 'permissions',
minWidth: '250px',
slots: {
default: 'permissions_default',
},
},
{
title: '状态',
field: 'enabled',
minWidth: '100px',
slots: {
default: 'enabled_default',
},
},
{
title: '操作',
field: 'action',
minWidth: '200px',
fixed: 'right',
slots: {
default: 'action_default',
},
},
],
}
},
created() {
this.loadRoles()
this.loadResourceTypeList()
},
beforeMount() {
this.loadTriggers()
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
app_id() {
return this.$route.name.split('_')[0]
},
},
methods: {
loadTriggers() {
this.searchName = ''
getTriggers({ app_id: this.app_id }).then((res) => {
this.triggers = res
this.filterTriggers = res
})
// .catch(err => this.$httpError(err))
},
loadRoles() {
searchRole({ app_id: this.app_id, page_size: 9999 }).then((res) => {
this.roles = res.roles
this.id2parents = res.id2parents
})
// .catch(err => this.$httpError(err))
},
loadResourceTypeList() {
searchResourceType({ app_id: this.app_id, page_size: 9999 }).then((res) => {
this.resourceTypeList = res['groups']
this.id2perms = res['id2perms']
})
// .catch(err => this.$httpError(err))
},
handleCreateTrigger() {
this.$refs.triggerForm.handleEdit(null)
},
handleDeleteTrigger(record) {
const that = this
this.$confirm({
title: '删除',
content: '确认删除该触发器吗?',
onOk() {
deleteTrigger(record.id).then((res) => {
that.$message.success('删除成功')
that.loadTriggers()
})
// .catch(err => that.$httpError(err))
},
})
},
handleApplyTrigger(record) {
const that = this
this.$confirm({
title: '规则应用',
content: '是否确定应用该触发器?',
onOk() {
applyTrigger(record.id).then((res) => {
that.$message.success('提交成功!')
})
// .catch(err => that.$httpError(err))
},
})
},
handleCancelTrigger(record) {
const that = this
this.$confirm({
title: '规则应用',
content: '是否取消应用该触发器?',
onOk() {
cancelTrigger(record.id).then((res) => {
that.$message.success('提交成功!')
})
// .catch(err => that.$httpError(err))
},
})
},
handleEditTrigger(record) {
this.$refs.triggerForm.handleEdit(record)
},
filter() {
if (this.searchName) {
this.filterTriggers = this.triggers.filter((item) =>
item.name.toLowerCase().includes(this.searchName.toLowerCase())
)
} else {
this.filterTriggers = this.triggers
}
},
handlePattern(row) {
const { wildcard, uid, resource_type_id } = row
this.$refs.triggerPattern.open({
resource_type_id,
app_id: this.app_id,
owner: uid,
pattern: wildcard,
})
},
},
watch: {
'$route.name': function(oldName, newName) {
this.loadTriggers()
},
searchName: {
immediate: true,
handler(newVal) {
if (!newVal) {
this.filter()
}
},
},
},
}
</script>
<style lang="less" scoped>
.trigger-action-btn {
width: 100%;
display: inline-flex;
margin-bottom: 15px;
align-items: center;
}
</style>

View File

@@ -0,0 +1,181 @@
<template>
<a-card :bordered="false">
<div class="user-action-btn">
<a-button v-if="isAclAdmin" @click="handleCreate" type="primary">{{ btnName }}</a-button>
<a-input-search
allowClear
:style="{ display: 'inline', marginLeft: '10px' }"
placeholder="搜索 | 用户名、中文名"
v-model="searchName"
></a-input-search>
</div>
<a-spin :spinning="loading">
<vxe-grid :columns="tableColumns" :data="tableData" highlight-hover-row :max-height="`${windowHeight - 185}px`">
<template #block_default="{row}">
<a-icon type="lock" v-if="row.block" />
</template>
<template #action_default="{row}">
<template>
<a :disabled="isAclAdmin ? false : true" @click="handleEdit(row)">
<a-icon type="edit" />
</a>
<a-divider type="vertical" />
<a-tooltip title="权限汇总">
<a @click="handlePermCollect(row)"><a-icon type="solution"/></a>
</a-tooltip>
</template>
</template>
</vxe-grid>
</a-spin>
<userForm ref="userForm" :handleOk="handleOk"> </userForm>
<perm-collect-form ref="permCollectForm"></perm-collect-form>
</a-card>
</template>
<script>
import { mapState } from 'vuex'
import userForm from './module/userForm'
import PermCollectForm from './module/permCollectForm'
import { deleteUserById, searchUser, getOnDutyUser } from '@/modules/acl/api/user'
export default {
name: 'Users',
components: {
userForm,
PermCollectForm,
},
data() {
return {
loading: false,
tableColumns: [
{
title: '用户名',
field: 'username',
sortable: true,
minWidth: '100px',
fixed: 'left',
},
{
title: '中文名',
field: 'nickname',
minWidth: '100px',
},
{
title: '加入时间',
field: 'date_joined',
minWidth: '160px',
},
{
title: '锁定',
field: 'block',
minWidth: '100px',
slots: {
default: 'block_default',
},
},
{
title: '操作',
field: 'action',
minWidth: '180px',
fixed: 'right',
slots: {
default: 'action_default',
},
},
],
onDutuUids: [],
btnName: '新增用户',
allUsers: [],
tableData: [],
searchName: '',
}
},
beforeCreate() {
this.form = this.$form.createForm(this)
},
async beforeMount() {
this.loading = true
await getOnDutyUser().then(res => {
this.onDutuUids = res.map(i => i.uid)
this.search()
})
},
computed: {
...mapState({
windowHeight: state => state.windowHeight,
}),
isAclAdmin: function() {
if (this.$store.state.user.roles.permissions.filter(item => item === 'acl_admin').length > 0) {
return true
} else {
return false
}
},
},
mounted() {},
inject: ['reload'],
methods: {
search() {
searchUser({ page_size: 10000 }).then(res => {
const ret = res.users.filter(u => this.onDutuUids.includes(u.uid))
this.allUsers = ret
this.tableData = ret
this.loading = false
})
},
handlePermCollect(record) {
this.$refs['permCollectForm'].collect(record)
},
handleEdit(record) {
this.$refs.userForm.handleEdit(record)
},
handleDelete(record) {
this.deleteUser(record.uid)
},
handleOk() {
this.searchName = ''
this.search()
},
handleCreate() {
this.$refs.userForm.handleCreate()
},
deleteUser(attrId) {
deleteUserById(attrId).then(res => {
this.$message.success(`删除成功`)
this.handleOk()
})
// .catch(err => this.requestFailed(err))
},
// requestFailed(err) {
// const msg = ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试'
// this.$message.error(`${msg}`)
// },
},
watch: {
searchName: {
immediate: true,
handler(newVal, oldVal) {
if (newVal) {
this.tableData = this.allUsers.filter(
item =>
item.username.toLowerCase().includes(newVal.toLowerCase()) ||
item.nickname.toLowerCase().includes(newVal.toLowerCase())
)
} else {
this.tableData = this.allUsers
}
},
},
},
}
</script>
<style lang="less" scoped>
.user-action-btn {
display: inline-flex;
margin-bottom: 15px;
}
</style>