前后端全面升级

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>

View File

@@ -0,0 +1,55 @@
<template>
<div :style="{'margin-topd':(graphSetting.viewSize.height-height)+'px', height: height+'px', 'margin-top': -height+'px' }" class="c-rg-bottom-panel">
<slot name="bottomPanel" />
</div>
</template>
<script>
// import SeeksRGStore from './core4vue/SeeksRGStore'
// import { mapState } from 'vuex'
// var _parent = this.$parent
// console.log('GraphSettingPanel.vue:', _parent)
export default {
name: 'GraphBottomPanel',
props: {
graphSetting: {
mustUseProp: true,
default: () => { return {} },
type: Object
}
},
data() {
return {
height: 50,
search_text: ''
}
},
mounted() {
this.$nextTick(() => {
this.height = this.$slots['bottomPanel'][0].elm.offsetHeight
if (window.SeeksGraphDebug) console.log('SeeksGraph bootomPanel height:', this.height)
})
},
methods: {
}
}
</script>
<style scoped>
.c-rg-bottom-panel{
width:100%;
margin-left:0px;
font-size: 12px;
color: #1890ff;
padding:0px;
overflow: hidden;
border-radius: 0px;
position: absolute;
z-index: 99;
opacity: 1;
}
.c-fixedLayout{
position: fixed;
bottom:0px;
}
</style>

View File

@@ -0,0 +1,53 @@
<template>
<div :style="{'margin-left':(graphSetting.viewELSize.width-350)+'px'}" class="c-mini-namefilter">
<!-- <el-autocomplete-->
<!-- v-model="$parent.search_text"-->
<!-- :fetch-suggestions="$parent.querySearchAsync"-->
<!-- :trigger-on-focus="false"-->
<!-- :label="'xxxx'"-->
<!-- size="small"-->
<!-- placeholder="图谱节点定位,请输入节点名称"-->
<!-- clearable-->
<!-- style="width: 320px;box-shadow: 0px 0px 8px #cccccc;"-->
<!-- @select="$parent.handleSelect"-->
<!-- >-->
<!-- <template slot-scope="{ item }">-->
<!-- <div class="c-suggestion-name">{{ item.text }}</div>-->
<!-- </template>-->
<!-- <el-button slot="append" style="color: #2E4E8F;" icon="el-icon-search" />-->
<!-- </el-autocomplete>-->
</div>
</template>
<script>
export default {
name: 'GraphMiniNameFilter',
props: {
graphSetting: {
mustUseProp: true,
default: () => { return {} },
type: Object
}
},
data() {
return {
}
},
mounted() {
},
methods: {
}
}
</script>
<style scoped>
.c-mini-namefilter{
height:60px;
position: absolute;
margin-top:10px;
z-index: 999;
}
.c-fixedLayout{
position: fixed;
top:145px;
}
</style>

View File

@@ -0,0 +1,377 @@
<template>
<div
:style="{
'margin-left': graphSetting.viewELSize.width - 50 + 'px',
'margin-top': (graphSetting.viewELSize.height - 260) / 2 + 'px',
}"
class="c-mini-toolbar"
>
<div class="c-mb-button" style="margin-top: 0px;" @click="graphSetting.fullscreen = !graphSetting.fullscreen">
<svg class="rg-icon" aria-hidden="true"><use xlink:href="#icon-resize-"></use></svg>
<span class="c-mb-text">{{ graphSetting.fullscreen ? '退出' : '全屏' }}</span>
</div>
<div v-if="graphSetting.allowShowZoomMenu" class="c-mb-button" @click="$parent.zoom(20)">
<svg class="rg-icon" aria-hidden="true"><use xlink:href="#icon-fangda"></use></svg>
<span class="c-mb-text">放大</span>
</div>
<div
v-if="graphSetting.allowShowZoomMenu"
style="float:left;margin-top:0px;height:20px;width:40px;border-top:0px;border-bottom:0px;background-color: #ffffff;color: #262626;font-size: 10px;background-color: #efefef;text-align: center;line-height: 20px;"
@click="printGraphJsonData"
>
{{ graphSetting.canvasZoom }}%
</div>
<!--<div style="float:left;margin-top:0px;height:20px;width:40px;border-top:0px;border-bottom:0px;background-color: #ffffff;color: #262626;font-size: 10px;background-color: #efefef;text-align: center;line-height: 20px;">{{ hits }}</div>-->
<div v-if="graphSetting.allowShowZoomMenu" class="c-mb-button" style="margin-top:0px;" @click="$parent.zoom(-20)">
<svg class="rg-icon" aria-hidden="true"><use xlink:href="#icon-suoxiao"></use></svg>
<span class="c-mb-text">缩小</span>
</div>
<div v-if="graphSetting.layouts.length > 1" class="c-mb-button">
<svg class="rg-icon" aria-hidden="true"><use xlink:href="#icon-yuanquanfenxiang"></use></svg>
<span class="c-mb-text">布局</span>
<div
:style="{
width: graphSetting.layouts.length * 70 + 6 + 'px',
'margin-left': graphSetting.layouts.length * -70 - 5 + 'px',
}"
class="c-mb-child-panel"
>
<div
v-for="thisLayoutSetting in graphSetting.layouts"
:key="thisLayoutSetting.label"
class="c-mb-button c-mb-button-c"
:class="{ 'c-mb-button-on': graphSetting.layoutLabel === thisLayoutSetting.label }"
style="width: 70px;"
@click="switchLayout(thisLayoutSetting)"
>
<svg class="rg-icon" aria-hidden="true"><use xlink:href="#icon-yuanquanfenxiang"></use></svg>
<span class="c-mb-text">{{ thisLayoutSetting.label }}</span>
</div>
</div>
</div>
<div v-if="graphSetting.allowSwitchLineShape" class="c-mb-button">
<svg class="rg-icon" aria-hidden="true"><use xlink:href="#icon-hj2"></use></svg>
<span class="c-mb-text">线条</span>
<div class="c-mb-child-panel" style="width:256px;margin-left:-255px;">
<div
:class="{ 'c-mb-button-on': graphSetting.defaultLineShape === 1 }"
class="c-mb-button c-mb-button-c"
style="width: 50px;"
@click="graphSetting.defaultLineShape = 1"
>
<svg class="rg-icon" aria-hidden="true"><use xlink:href="#icon-hj2"></use></svg>
<span class="c-mb-text">直线</span>
</div>
<div
:class="{ 'c-mb-button-on': graphSetting.defaultLineShape === 2 }"
class="c-mb-button c-mb-button-c"
style="width: 50px;"
@click="graphSetting.defaultLineShape = 2"
>
<svg class="rg-icon" aria-hidden="true"><use xlink:href="#icon-lianjieliu"></use></svg>
<span class="c-mb-text">简洁</span>
</div>
<div
:class="{ 'c-mb-button-on': graphSetting.defaultLineShape === 6 }"
class="c-mb-button c-mb-button-c"
style="width: 50px;"
@click="graphSetting.defaultLineShape = 6"
>
<svg class="rg-icon" aria-hidden="true"><use xlink:href="#icon-lianjieliu"></use></svg>
<span class="c-mb-text">生动</span>
</div>
<div
:class="{ 'c-mb-button-on': graphSetting.defaultLineShape === 5 }"
class="c-mb-button c-mb-button-c"
style="width: 50px;"
@click="graphSetting.defaultLineShape = 5"
>
<svg class="rg-icon" aria-hidden="true"><use xlink:href="#icon-lianjieliu"></use></svg>
<span class="c-mb-text">鱼尾</span>
</div>
<div
:class="{ 'c-mb-button-on': graphSetting.defaultLineShape === 4 }"
class="c-mb-button c-mb-button-c"
style="width: 50px;"
@click="graphSetting.defaultLineShape = 4"
>
<svg class="rg-icon" aria-hidden="true"><use xlink:href="#icon-hj2"></use></svg>
<span class="c-mb-text">折线</span>
</div>
</div>
</div>
<div v-if="graphSetting.allowSwitchJunctionPoint" class="c-mb-button">
<svg class="rg-icon" aria-hidden="true"><use xlink:href="#icon-lianjie_connecting5"></use></svg>
<span class="c-mb-text">连接点</span>
<div class="c-mb-child-panel" style="width:206px;margin-left:-205px;">
<div
:class="{ 'c-mb-button-on': graphSetting.defaultJunctionPoint === 'border' }"
class="c-mb-button c-mb-button-c"
style="width: 50px;"
@click="graphSetting.defaultJunctionPoint = 'border'"
>
<svg class="rg-icon" aria-hidden="true"><use xlink:href="#icon-lianjie_connecting5"></use></svg>
<span class="c-mb-text">边缘</span>
</div>
<div
:class="{ 'c-mb-button-on': graphSetting.defaultJunctionPoint === 'ltrb' }"
class="c-mb-button c-mb-button-c"
style="width: 50px;"
@click="graphSetting.defaultJunctionPoint = 'ltrb'"
>
<svg class="rg-icon" aria-hidden="true"><use xlink:href="#icon-lianjie_connecting5"></use></svg>
<span class="c-mb-text">四点</span>
</div>
<div
:class="{ 'c-mb-button-on': graphSetting.defaultJunctionPoint === 'tb' }"
class="c-mb-button c-mb-button-c"
style="width: 50px;"
@click="graphSetting.defaultJunctionPoint = 'tb'"
>
<svg class="rg-icon" aria-hidden="true"><use xlink:href="#icon-lianjie_connecting5"></use></svg>
<span class="c-mb-text">上下</span>
</div>
<div
:class="{ 'c-mb-button-on': graphSetting.defaultJunctionPoint === 'lr' }"
class="c-mb-button c-mb-button-c"
style="width: 50px;"
@click="graphSetting.defaultJunctionPoint = 'lr'"
>
<svg class="rg-icon" aria-hidden="true"><use xlink:href="#icon-lianjie_connecting5"></use></svg>
<span class="c-mb-text">左右</span>
</div>
</div>
</div>
<div
v-if="graphSetting.isNeedShowAutoLayoutButton"
:title="graphSetting.autoLayouting ? '点击停止自动布局' : '点击开始自动调整布局'"
:class="{ 'c-mb-button-on': graphSetting.autoLayouting }"
class="c-mb-button"
@click="toggleAutoLayout"
>
<svg v-if="!graphSetting.autoLayouting" class="rg-icon" aria-hidden="true">
<use xlink:href="#icon-zidong"></use>
</svg>
<svg v-else class="c-loading-icon rg-icon" aria-hidden="true"><use xlink:href="#icon-lianjiezhong"></use></svg>
<span class="c-mb-text">自动</span>
</div>
<div class="c-mb-button" @click="refresh">
<svg class="rg-icon" aria-hidden="true"><use xlink:href="#icon-ico_reset"></use></svg>
<span class="c-mb-text">刷新</span>
</div>
<div class="c-mb-button">
<svg class="rg-icon" aria-hidden="true"><use xlink:href="#icon-ziyuan"></use></svg>
<span class="c-mb-text">下载</span>
<div
:style="{ width: downloadPanelWidth + 'px', 'margin-left': downloadPanelWidth * -1 + 'px' }"
class="c-mb-child-panel"
>
<div class="c-mb-button c-mb-button-c" style="width: 50px;" @click="$parent.downloadAsImage('png')">
<svg class="rg-icon" aria-hidden="true"><use xlink:href="#icon-tupian"></use></svg>
<span class="c-mb-text">PNG</span>
</div>
<div class="c-mb-button c-mb-button-c" style="width: 50px;" @click="$parent.downloadAsImage('jpg')">
<svg class="rg-icon" aria-hidden="true"><use xlink:href="#icon-tupian"></use></svg>
<span class="c-mb-text">JPG</span>
</div>
<div
v-if="typeof $parent.onDownloadExcel === 'function'"
class="c-mb-button c-mb-button-c"
style="width: 50px;"
@click="$parent.onDownloadExcel()"
>
<svg class="rg-icon" aria-hidden="true"><use xlink:href="#icon-ziyuan"></use></svg>
<span class="c-mb-text">Excel</span>
</div>
</div>
</div>
</div>
</template>
<script>
/* eslint-disable */
import SeeksRGLayouters from './core4vue/SeeksRGLayouters'
export default {
name: 'GraphMiniToolBar',
props: {
graphSetting: {
mustUseProp: true,
default: () => {
return {}
},
type: Object,
},
},
data() {
return {
height: 275,
hits: 0,
downloadPanelWidth: 106,
}
},
// watch: {
// 'graphSetting.layoutName': function(newV, oldV) {
// console.log('change layout:', newV, oldV)
// SeeksRGLayouters.switchLayout(newV, this.graphSetting)
// this.refresh()
// }
// },
mounted() {
if (this.$parent.onDownloadExcel !== null) {
this.downloadPanelWidth += 50
}
if (this.graphSetting.layouts.length > 1) {
this.height -= 40
}
},
methods: {
refresh() {
this.$parent.refresh()
},
switchLayout(layoutConfig) {
if (window.SeeksGraphDebug) console.log('change layout:', layoutConfig)
SeeksRGLayouters.switchLayout(layoutConfig, this.graphSetting)
this.refresh()
},
toggleAutoLayout() {
// console.log('this.graphSetting.autoLayouting', this.graphSetting.autoLayouting)
this.graphSetting.autoLayouting = !this.graphSetting.autoLayouting
if (this.graphSetting.autoLayouting) {
if (!this.graphSetting.layouter.autoLayout) {
console.log('当前布局不支持自动布局!')
} else {
this.graphSetting.layouter.autoLayout(true)
}
} else {
if (!this.graphSetting.layouter.stop) {
console.log('当前布局不支持自动布局stop')
} else {
this.graphSetting.layouter.stop()
}
}
},
printGraphJsonData() {
this.hits++
setTimeout(() => {
if (this.hits > 0) this.hits--
}, 2000)
if (this.hits > 5) {
this.hits = 0
this.$parent.printGraphJsonData()
}
},
},
}
</script>
<style scoped>
.rg-icon {
width: 16px;
height: 16px;
vertical-align: -3px;
fill: currentColor;
overflow: hidden;
}
.c-mini-toolbar {
width: 44px;
position: absolute;
margin-top: 170px;
margin-right: 10px;
z-index: 999;
border: #bbbbbb solid 1px;
background-color: #ffffff;
box-shadow: 0px 0px 8px #cccccc;
box-sizing: border-box;
}
.c-fixedLayout {
position: fixed;
top: 100px;
}
.c-mb-button {
height: 44px;
width: 42px;
margin-top: 0px;
background-color: #ffffff;
border-top: #efefef solid 1px;
opacity: 1;
text-align: center;
padding-top: 3px;
cursor: pointer;
color: #999999;
font-size: 18px;
float: left;
box-sizing: border-box;
line-height: 21px;
}
.c-mb-button .c-mb-text {
display: inline-block;
height: 14px;
width: 42px;
font-size: 12px;
line-height: 12px;
margin-top: 24px;
margin-left: -28px;
position: absolute;
color: #262626;
}
.c-mb-button-on {
background-color: #2e74b5;
border-top: #2e4e8f solid 1px;
color: #ffffff;
}
.c-mb-button:hover {
background-color: #2e4e8f;
border-top: #2e4e8f solid 1px;
color: #ffffff;
}
.c-mb-button:hover .c-mb-text,
.c-mb-button-on .c-mb-text {
color: #ffffff;
}
.c-mb-button .c-mb-child-panel {
height: 46px;
position: absolute;
margin-top: -26px;
background-color: #ffffff;
display: none;
border: #bbbbbb solid 1px;
box-shadow: 0px 0px 8px #cccccc;
box-sizing: border-box;
}
.c-mb-button:hover .c-mb-child-panel {
display: block;
}
.c-mb-button .c-mb-button {
height: 44px;
width: 42px;
margin: 0px;
border: none;
}
.c-mb-button-c .c-mb-text {
color: #262626 !important;
}
.c-mb-button-c:hover .c-mb-text,
.c-mb-button-on .c-mb-text {
color: #ffffff !important;
}
.c-loading-icon {
animation: turn 1s linear infinite;
}
@keyframes turn {
0% {
-webkit-transform: rotate(0deg);
}
25% {
-webkit-transform: rotate(90deg);
}
50% {
-webkit-transform: rotate(180deg);
}
75% {
-webkit-transform: rotate(270deg);
}
100% {
-webkit-transform: rotate(360deg);
}
}
</style>

View File

@@ -0,0 +1,103 @@
<template>
<div ref="miniView" class="c-mini-graph">
<div :style="{width:(100 * zoom) + 'px',height:(graphSetting.canvasNVInfo.height * 100/graphSetting.canvasNVInfo.width * zoom)+'px'}" class="c-mini-canvas">
<template v-for="thisNode in $parent.nodeViewList">
<div v-if="isAllowShowNode(thisNode)" :key="thisNode.id" :style="{'margin-left':(thisNode.x * 100/graphSetting.canvasSize.width * zoom)+'px','margin-top':(thisNode.y * 100/graphSetting.canvasSize.width * zoom)+'px'}" class="c-mini-node" />
</template>
</div>
<div :style="getPositionData()" class="c-mini-view">
<i class="el-icon-view" />
</div>
</div>
</template>
<script>
import SeeksGraphMath from './core4vue/SeeksGraphMath'
export default {
name: 'GraphMiniView',
props: {
graphSetting: {
mustUseProp: true,
default: () => { return {} },
type: Object
}
},
data() {
return {
zoom: 1
}
},
mounted() {
},
methods: {
getPositionData() {
var _c_width = 100
var _r = _c_width / this.graphSetting.canvasNVInfo.width
var _width = this.graphSetting.viewNVInfo.width * _r
var _height = this.graphSetting.viewNVInfo.height * _r
var _view_x = (this.graphSetting.viewNVInfo.x - this.graphSetting.canvasNVInfo.x) * _r
var _view_y = (this.graphSetting.viewNVInfo.y - this.graphSetting.canvasNVInfo.y) * _r
if (_width > 100) {
_height = _height * 100 / _width
_view_x = _view_x * 100 / _width
_view_y = _view_y * 100 / _width
this.zoom = 100 / _width
_width = 100
} else {
this.zoom = 1
}
// console.log('Mini View style:', _view_center_x, _canvas_center_x)
var style = {
width: _width + 'px',
height: _height + 'px',
'margin-left': _view_x + 'px',
'margin-top': _view_y + 'px'
}
return style
},
isAllowShowNode(nodeData) {
return SeeksGraphMath.isAllowShowNode(nodeData)
}
}
}
</script>
<style scoped>
.c-mini-graph{
height:100px;
width:100px;
position: absolute;
margin-left: 60px;
margin-top:100px;
z-index: 999;
}
.c-fixedLayout{
position: fixed;
top:100px;
}
.c-mini-canvas{
background-color: #AACBFF;
border: #7BA8FF solid 1px;
opacity: 0.8;
position: absolute;
}
.c-mini-view{
background-color: #F5A565;
border: #C03639 solid 1px;
opacity: 0.5;
color: #ffffff;
font-size: 14px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
}
.c-mini-node{
position: absolute;
width:2px;
height:2px;
background-color: #000000;
border-radius: 1px;
}
</style>

View File

@@ -0,0 +1,232 @@
<template>
<div>
<div :class="[$parent.isNeedFixedTools?'c-fixedLayout':'']" class="c-setting-panel-button" @click="toggleSettingPanel">
<i class="el-icon-setting" />
</div>
<div v-if="showSettingPanel" :class="[$parent.isNeedFixedTools?'c-fixedLayout':'']" class="c-setting-panel">
<!-- <div style="padding:3px;">-->
<!-- 搜索节点-->
<!-- <el-autocomplete-->
<!-- v-model="$parent.search_text"-->
<!-- :fetch-suggestions="$parent.querySearchAsync"-->
<!-- :trigger-on-focus="true"-->
<!-- :label="'xxxx'"-->
<!-- size="small"-->
<!-- placeholder="输入节点名称"-->
<!-- clearable-->
<!-- style="width: 220px;"-->
<!-- @select="$parent.handleSelect">-->
<!-- <template slot-scope="{ item }">-->
<!-- <div class="c-suggestion-name">{{ item.text }}</div>-->
<!-- </template>-->
<!-- </el-autocomplete>-->
<!-- </div>-->
<!-- <div style="padding:3px;">-->
<!-- 布局方式-->
<!-- <el-radio-group v-model="currentlayoutName" size="mini" @change="switchLayout">-->
<!-- <el-radio-button label="center">中心</el-radio-button>-->
<!-- <el-radio-button label="circle">环形</el-radio-button>-->
<!-- <el-radio-button label="tree">树状(L)</el-radio-button>-->
<!-- <el-radio-button label="tree-plus-r">树状(R)</el-radio-button>-->
<!-- <el-radio-button label="tree-plus-t">树状(T)</el-radio-button>-->
<!-- <el-radio-button label="tree-plus-b">树状(B)</el-radio-button>-->
<!-- </el-radio-group>-->
<!-- </div>-->
<!-- <div style="padding:3px;">-->
<!-- 线条样式-->
<!-- <el-radio-group v-model="graphSetting.defaultLineShape" size="small">-->
<!-- <el-radio-button :label="1">直线</el-radio-button>-->
<!-- <el-radio-button :label="2">简洁</el-radio-button>-->
<!-- <el-radio-button :label="3">生动</el-radio-button>-->
<!-- <el-radio-button :label="5">鱼尾</el-radio-button>-->
<!-- <el-radio-button :label="4">折线</el-radio-button>-->
<!-- </el-radio-group>-->
<!-- </div>-->
<!-- <div style="padding:3px;">-->
<!-- 节点样式-->
<!-- <el-radio-group v-model="graphSetting.defaultNodeShape" size="small">-->
<!-- <el-radio-button :label="0">圆形</el-radio-button>-->
<!-- <el-radio-button :label="1">长方形</el-radio-button>-->
<!-- </el-radio-group>-->
<!-- </div>-->
<!-- <div style="padding:3px;">-->
<!-- 图像缩放-->
<!-- <el-input-number v-model="graphSetting.canvasZoom" :max="500" :min="10" :step="20" size="small"/>-->
<!-- </div>-->
<!-- <div style="padding:3px;">-->
<!-- 连线标签-->
<!-- <el-switch-->
<!-- v-model="graphSetting.defaultShowLineLabel"-->
<!-- active-color="#13ce66"-->
<!-- inactive-color="#ff4949"/>-->
<!-- </div>-->
<!-- <div style="padding:3px;">-->
<!-- 联动拖拽-->
<!-- <el-switch-->
<!-- v-model="graphSetting.isMoveByParentNode"-->
<!-- active-color="#13ce66"-->
<!-- inactive-color="#ff4949"/>-->
<!-- 拖动父节点后子节点跟随-->
<!-- </div>-->
<!-- <div style="padding:3px;display: none;">-->
<!-- 最大层级-->
<!-- <el-radio-group v-model="graphSetting.maxLevel" size="small">-->
<!-- <el-radio-button :label="1">1</el-radio-button>-->
<!-- <el-radio-button :label="2">2</el-radio-button>-->
<!-- <el-radio-button :label="3">3</el-radio-button>-->
<!-- <el-radio-button :label="4">4</el-radio-button>-->
<!-- <el-radio-button :label="5">5</el-radio-button>-->
<!-- </el-radio-group>-->
<!-- </div>-->
<!-- <div style="padding:3px;">-->
<!-- 自动布局-->
<!-- <el-switch-->
<!-- v-model="graphSetting.autoLayouting"-->
<!-- active-color="#13ce66"-->
<!-- inactive-color="#ff4949"-->
<!-- @change="toggleAutoLayout" />-->
<!-- </div>-->
<!-- <div style="padding:3px;">-->
<!-- 全屏展示-->
<!-- <el-switch-->
<!-- v-model="graphSetting.fullscreen"-->
<!-- active-color="#13ce66"-->
<!-- inactive-color="#ff4949"/>-->
<!-- </div>-->
<!-- <div style="padding:3px;">-->
<!-- 刷新布局-->
<!-- <el-button size="small" type="primary" icon="el-icon-refresh" @click="refresh">刷新</el-button>-->
<!-- </div>-->
<!-- <div style="padding:3px;">-->
<!-- 图片下载-->
<!-- <el-button size="small" type="primary" icon="el-icon-download" @click="$parent.downloadAsImage('png')">下载png</el-button>-->
<!-- <el-button size="small" type="primary" icon="el-icon-download" @click="$parent.downloadAsImage('jpg')">下载jpg</el-button>-->
<!-- </div>-->
<!-- <slot :setting="graphSetting" name="settingPanelPlus"/>-->
</div>
</div>
</template>
<script>
import SeeksRGLayouters from './core4vue/SeeksRGLayouters'
// import SeeksRGStore from './core4vue/SeeksRGStore'
// import { mapState } from 'vuex'
// var _parent = this.$parent
// console.log('GraphSettingPanel.vue:', _parent)
export default {
name: 'GraphSettingPanel',
props: {
graphSetting: {
mustUseProp: true,
default: () => { return {} },
type: Object
}
},
data() {
return {
search_text: '',
showSettingPanel: false,
currentLayoutName: ''
}
},
// computed: mapState({
// graphSetting: () => _parent.graphSetting
// }),
// watch: {
// 'graphSetting.layoutName': function(newV, oldV) {
// console.log('change layout:', newV, oldV)
// SeeksRGLayouters.switchLayout(newV, this.graphSetting)
// this.$parent.refresh()
// }
// },
methods: {
toggleSettingPanel() {
this.showSettingPanel = !this.showSettingPanel
},
toggleAutoLayout() {
if (this.graphSetting.autoLayouting) {
if (!this.graphSetting.layouter.autoLayout) {
console.log('当前布局不支持自动布局!')
} else {
this.graphSetting.layouter.autoLayout(true)
}
} else {
if (!this.graphSetting.layouter.stop) {
console.log('当前布局不支持自动布局stop')
} else {
this.graphSetting.layouter.stop()
}
}
},
switchLayout() {
if (window.SeeksGraphDebug) console.log('change layout:', this.currentLayoutName)
SeeksRGLayouters.switchLayout(this.currentLayoutName, this.graphSetting)
this.refresh()
},
refresh() {
this.$parent.refresh()
}
}
}
</script>
<style scoped>
.c-setting-panel{
--height:500px;
--width:500px;
width:500px;
height:500px;
position: absolute;
margin-left:10px;
margin-top:5px;
font-size: 12px;
color: rgb(58, 91, 178);
padding:10px;
overflow: hidden;
box-shadow: 0px 0px 5px rgb(58, 91, 178);
border-radius: 5px;
z-index: 1000;
background-color: #ffffff;
border: rgb(58, 91, 178) solid 1px;
padding-top:60px;
}
.c-setting-panel-button{
height:45px;
width:45px;
font-size: 20px;
line-height: 45px;
text-align: center;
border-radius: 50%;
position: absolute;
margin-left:25px;
margin-top:20px;
background-color: rgb(58, 91, 178);
color: #ffffff;
cursor: pointer;
z-index: 1001;
box-shadow: 0px 0px 8px rgb(46, 78, 143);
}
.c-setting-panel-button:hover{
box-shadow: 0px 0px 20px #FFA20A;
border:#ffffff solid 1px;
color: #FFA20A;
-moz-transform: rotate(-89deg) translateX(-190px);
animation-timing-function:linear;
animation: flashButton 2s infinite;
}
.c-fixedLayout{
position: fixed;
top: 125px
}
@keyframes flashButton {
from {
box-shadow: 0px 0px 8px rgb(46, 78, 143);
}
30% {
box-shadow: 0px 0px 20px #FFA20A;
}
to {
box-shadow: 0px 0px 8px rgb(46, 78, 143);
}
}
</style>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,543 @@
var STAITC_MAP_ANGLE = 0
var SeeksGraphMath = {
getRectPoint: function(x1, y1, x2, y2, n1w, n1h, n2w, n2h) {
var fx = x1 + n1w / 2
var fy = y1 + n1h / 2
var tx = x2 + n2w / 2
var ty = y2 + n2h / 2
var _ar_x = fx < tx ? 1 : -1
var _ar_y = fy < ty ? 1 : -1
if (ty === fy) {
return { x: fx + _ar_x * n1w / 2, y: fy }
}
var __tan = Math.abs((tx - fx) / (ty - fy))
var rectAngle = n1w / n1h
var __w = 0
var __h = 0
if (__tan < rectAngle) {
__w = _ar_x * n1h / 2 * __tan
__h = _ar_y * n1h / 2
} else {
__w = _ar_x * n1w / 2
__h = _ar_y * n1w / 2 / __tan
}
return { x: fx + __w, y: fy + __h }
},
getRectPointBasic: function(x1, y1, x2, y2, n1w, n1h, n2w, n2h) {
var fx = x1 + n1w / 2
var fy = y1 + n1h / 2
var tx = x2 + n2w / 2
var ty = y2 + n2h / 2
// var x = fx - tx
// var y = fy - ty
// var z = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))
// var cos = y / z
// var radina = Math.acos(cos)// 用反三角函数求弧度
// var angle = Math.floor(180 / (Math.PI / radina))// 将弧度转换成角度
// n1h = n1h + 10
// n1w = n1w + 10
var __tanA = ty === fy ? 0 : (tx - fx) / (ty - fy)
if (__tanA === 0)__tanA = (tx - fx) / (ty - fy + 1)
var rectAngle = n1w / n2h
var __w = 0
var __h = 0
var _case = '1'
// var __A_angle = Math.atan(__tanA) / (Math.PI / 180)
if ((-1 * rectAngle) < __tanA && __tanA < rectAngle) {
_case = '2'
if (fy < ty) {
__w = n1h / 2 * __tanA
__h = n1h / 2
} else {
__w = -1 * n1h / 2 * __tanA
__h = -1 * n1h / 2
}
} else {
if (fx < tx) {
__w = 1 * n1w / 2
__h = 1 * n1w / 2 / __tanA
} else {
__w = -1 * n1w / 2
__h = -1 * n1w / 2 / __tanA
}
_case = '3'
}
// var __w = (n1h / 2) * Math.tan(__A_angle)
// var __w = ty === fy ? parseInt(n1w / 2) : parseInt(((n1h / 2) * (tx - fx)) / (ty - fy))
// var __h = tx === fx ? parseInt(n1h / 2) : parseInt((__w * (ty - fy)) / (tx - fx))
return { x: fx + __w, y: fy + __h, _case }
},
getRectJoinPoint: function(x1, y1, x2, y2, n1w, n1h, n2w, n2h) {
var _from_c_x = x1 + n1w / 2
var _from_c_y = y1 + n1h / 2
var _to_c_x = x2 + n2w / 2
var _to_c_y = y2 + n2h / 2
var _atan2 = parseInt(Math.atan2(_to_c_y - _from_c_y, _to_c_x - _from_c_x) * 180 / 3.14) + 135
if (_atan2 >= 0 && _atan2 < 90) { // top
return { x: x1 + n1w / 2, y: y1 - 5 }
} else if (_atan2 >= 90 && _atan2 < 180) { // right
return { x: x1 + n1w + 5, y: y1 + n1h / 2 }
} else if (_atan2 >= 180 && _atan2 < 270) { // bottom
return { x: x1 + n1w / 2, y: y1 + n1h + 5 }
} else { // left
return { x: x1 - 5, y: y1 + n1h / 2 }
}
},
getRectHJoinPoint: function(x1, y1, x2, y2, n1w, n1h, n2w) {
var _hH = n1h / 2
// var _hW = n1w / 2
if ((x1 + n1w) < x2) {
return { x: x1 + n1w + 5, y: y1 + _hH }
} else if ((x1 + n1w) < (x2 + n2w)) {
return { x: x1 - 5, y: y1 + _hH }
} else {
return { x: x1 - 5, y: y1 + _hH }
}
},
getRectVJoinPoint: function(x1, y1, x2, y2, n1w, n1h, n2w, n2h) {
var _hW = n1w / 2
// var _hW = n1w / 2
if ((y1 + n1h) < y2) {
return { y: y1 + n1h + 5, x: x1 + _hW }
} else if ((y1 + n1h) < (y2 + n2h)) {
return { y: y1 - 5, x: x1 + _hW }
} else {
return { y: y1 - 5, x: x1 + _hW }
}
},
getBorderPoint: function(x1, y1, x2, y2, n1w, n1h, n2w, n2h, n1style) {
if (n1style === 0) {
return this.getCirclePoint(x1, y1, x2, y2, n1w, n1h, n2w, n2h)
} else {
return this.getRectPoint(x1, y1, x2, y2, n1w, n1h, n2w, n2h)
}
},
getBorderPoint4MultiLine: function(x1, y1, x2, y2, n1w, n1h, n2w, n2h, n1style, isReserve, allSize, indexOfAll) {
if (n1style === 0) {
return this.getCirclePoint4MultiLine(x1, y1, x2, y2, n1w, n1h, n2w, n2h, isReserve, allSize, indexOfAll)
} else {
return this.getRectPoint(x1, y1, x2, y2, n1w, n1h, n2w, n2h)
}
},
getCirclePoint: function(x1, y1, x2, y2, n1w, n1h, n2w, n2h) {
var fx = x2 + n2w / 2
var fy = y2 + n2h / 2
var tx = x1 + n1w / 2
var ty = y1 + n1h / 2
var buff_h = fx - tx
if (buff_h === 0) {
return { x: tx, y: ty - (n1h / 2) * (fy < ty ? 1 : -1) }
}
var buff_v = fy - ty
var k = buff_v / buff_h
// var m = ty - tx * k
var __x = Math.sqrt(1 / ((1 / Math.pow(n1w / 2, 2)) + (k ** 2 / Math.pow(n1h / 2, 2)))) * (fx < tx ? 1 : -1)
var __y = k * __x
// this.c = Math.sqrt(this.h * this.h + this.s * this.s)
// // this.l = this.c - radius
// this.v = (this.c - n1w / 2) * this.h / this.c * -1
// this.t = (this.c - n1h / 2) * this.s / this.c * -1
// alert(this.h+","+this.s+","+this.c+","+this.l+","+this.v+","+this.t);
return { x: tx - __x, y: ty - __y }
},
getCirclePoint4MultiLine: function(x1, y1, x2, y2, n1w, n1h, n2w, n2h, isReserve, allSize, indexOfAll) {
// console.log(indexOfAll, 'of', allSize, isReserve)
if (isReserve) {
indexOfAll = allSize - indexOfAll - 1
// console.log(indexOfAll, 'of', allSize, '|indexOfAll changed!')
}
var to_x = x2 + n2w / 2
var to_y = y2 + n2h / 2
var from_x = x1 + n1w / 2
var from_y = y1 + n1h / 2
var buff_h = to_x - from_x
if (buff_h === 0) {
return { x: from_x, y: from_y - (n1h / 2) * (to_y < from_y ? 1 : -1) }
}
var distance = ((40 / (allSize + 1)) * (indexOfAll + 1)) - 20
var buff_v = to_y - from_y
var b = Math.sqrt(Math.pow(buff_h, 2) + Math.pow(buff_v, 2)) * distance / buff_h
var k = buff_v / buff_h
var m = n1w / 2
var n = n1h / 2
var __wow = (to_x < from_x ? 1 : -1)
var __x = (-1 * (m ** 2) * k * b + (m * n * Math.sqrt((n ** 2 + (k ** 2) * (m ** 2) - b ** 2), 2)) / __wow) / (n ** 2 + m ** 2 * k ** 2)
// var __x = Math.sqrt(1 / ((1 / Math.pow(n1w / 2, 2)) + (k ** 2 / Math.pow(n1h / 2, 2)))) * (to_x < from_x ? 1 : -1)
var __y = k * __x + b
// this.c = Math.sqrt(this.h * this.h + this.s * this.s)
// // this.l = this.c - radius
// this.v = (this.c - n1w / 2) * this.h / this.c * -1
// this.t = (this.c - n1h / 2) * this.s / this.c * -1
// alert(this.h+","+this.s+","+this.c+","+this.l+","+this.v+","+this.t);
return { x: from_x - __x, y: from_y - __y }
},
getCirclePointBasic: function(x1, y1, x2, y2, n1w, n1h, n2w, n2h, radius) {
var fx = x1 + n1w / 2
var fy = y1 + n1h / 2
var tx = x2 + n2w / 2
var ty = y2 + n2h / 2
this.h = tx - fx
this.s = ty - fy
this.c = Math.sqrt(this.h * this.h + this.s * this.s)
this.l = this.c - radius
this.v = this.l * this.h / this.c * -1
this.t = this.l * this.s / this.c * -1
// alert(this.h+","+this.s+","+this.c+","+this.l+","+this.v+","+this.t);
return { x: tx + this.v, y: ty + this.t }
},
getCirclePointPlus: function(x1, y1, x2, y2, n1w, n1h, n2w, n2h) {
var fx = x1 + n1w / 2
var fy = y1 + n1h / 2
var tx = x2 + n2w / 2
var ty = y2 + n2h / 2
this.h = tx - fx
this.s = ty - fy
this.c = Math.sqrt(this.h * this.h + this.s * this.s)
// this.l = this.c - radius
this.v = (this.c - n1w / 2) * this.h / this.c * -1
this.t = (this.c - n1h / 2) * this.s / this.c * -1
// alert(this.h+","+this.s+","+this.c+","+this.l+","+this.v+","+this.t);
return { x: tx + this.v, y: ty + this.t }
},
getOvalPoint: function(c_x, c_y, c_r, c_i, c_n) {
return {
x: c_x + c_r * Math.sin((STAITC_MAP_ANGLE + (c_i * (360 / c_n)) + 0) * Math.PI / 180),
y: c_y + c_r * Math.cos((STAITC_MAP_ANGLE + (c_i * (360 / c_n)) + 0) * Math.PI / 180) * -1
}
},
getAngleType: function(buffer_x, buffer_y) {
if (buffer_x >= 0 && buffer_y >= 0) { // 第一象限
return 1
} else if (buffer_x < 0 && buffer_y >= 0) { // 第二象限
return 2
} else if (buffer_x < 0 && buffer_y < 0) { // 第三象限
return 3
} else if (buffer_x >= 0 && buffer_y < 0) { // 第三象限
return 4
}
},
getTextAngle: function(fx, fy, tx, ty) {
// 除数不能为0
var tan = Math.atan(Math.abs((ty - fy) / (tx - fx))) * 180 / Math.PI
// return tan
if (tx > fx && ty > fy) { // 第一象限
} else if (tx > fx && ty < fy) { // 第二象限
tan = -tan
} else if (tx < fx && ty > fy) { // 第三象限
tan = 180 - tan
} else {
tan = tan - 180
}
if (Math.abs(tan) > 90) {
tan = tan + 180
}
// if (tan > 90 && tan < 270) {
// tan = 0
// }
return parseInt(tan)
},
getTreePointFromTop: function(c_x, c_y, c_height, c_i, c_n, sizehelper) {
if (!c_x) { // if root
return {
x: (sizehelper.canvas_width - sizehelper.node_width) / 2,
y: (sizehelper.canvas_height - sizehelper.node_height) / 2 - 200
}
}
var sssss = {
x: c_x - 300 + (Math.max(600 / ((c_n === 1 ? 2 : c_n) - 1), 80)) * c_i,
y: c_y + c_height
}
return sssss
},
getTreePointFromRight: function(c_x, c_y, c_height, c_i, c_n, sizehelper) {
if (!c_x) { // if root
return {
x: (sizehelper.canvas_width - sizehelper.node_width) / 2 + 300,
y: (sizehelper.canvas_height - sizehelper.node_height) / 2
}
}
return {
x: c_x - c_height,
y: c_y - 200 + (Math.max(400 / ((c_n === 1 ? 2 : c_n) - 1), 80)) * c_i
}
},
getTreePointFromBottom: function(c_x, c_y, c_height, c_i, c_n, sizehelper) {
if (!c_x) { // if root
return {
x: (sizehelper.canvas_width - sizehelper.node_width) / 2,
y: (sizehelper.canvas_height - sizehelper.node_height) / 2 + 200
}
}
return {
x: c_x - 300 + (Math.max(600 / ((c_n === 1 ? 2 : c_n) - 1), 80)) * c_i,
y: c_y - c_height
}
},
getTreePointFromLeft: function(c_x, c_y, c_height, c_i, c_n, sizehelper) {
if (!c_x) { // if root
return {
x: (sizehelper.canvas_width - sizehelper.node_width) / 2 - 300,
y: (sizehelper.canvas_height - sizehelper.node_height) / 2
}
}
return {
x: c_x + c_height,
y: c_y - 200 + (Math.max(400 / ((c_n === 1 ? 2 : c_n) - 1), 80)) * c_i
}
},
analysisNodes: function(willLayoutNodes, thisLevelNodes, thisDeep, analyticResult, config) {
if (thisLevelNodes.length > analyticResult.max_length) {
analyticResult.max_length = thisLevelNodes.length
}
if (thisDeep > analyticResult.max_deep) {
analyticResult.max_deep = thisDeep
}
var __thisLOT_subling = {
level: thisDeep,
all_size: thisLevelNodes.length,
all_strength: 0
}
var newLevelNodes = []
thisLevelNodes.forEach(thisNode => {
if (!thisNode.lot)thisNode.lot = {}
thisNode.lot.eached = true
thisNode.lot.subling = __thisLOT_subling
thisNode.lot.level = thisDeep
willLayoutNodes.push(thisNode)
})
var __thisLevel_index = 0
// var __prev_node
thisLevelNodes.forEach(thisNode => {
// console.log('Build node::', thisNode.name, thisNode.targetNodes.length)
var __thisNode_child_size = 0
if (thisNode.targetNodes) {
let __thisTargetIndex = 0
thisNode.targetNodes.forEach((thisTarget) => {
if (!thisTarget.lot)thisTarget.lot = { eached: false }
if (!thisTarget.lot.eached) {
if (SeeksGraphMath.isAllowShowNode(thisTarget)) {
thisTarget.lot.eached = true
thisTarget.lot.parent = thisNode
thisTarget.lot.index_of_parent = __thisTargetIndex++
// thisTarget.lot.prevNode = __prev_node
// if (__prev_node)__prev_node.lot.nextNode = thisTarget
// __prev_node = thisTarget
thisNode.lot.childs.push(thisTarget)
newLevelNodes.push(thisTarget)
__thisNode_child_size++
} else {
thisNode.lot.childs.push(thisTarget)
// console.log('hide node:', thisTarget.name, 'from:', thisNode.text)
}
}
})
}
thisNode.lot.strength = __thisNode_child_size > 0 ? __thisNode_child_size : 1
__thisLOT_subling.all_strength += thisNode.lot.strength
thisNode.lot.strength_plus = __thisLOT_subling.all_strength
thisNode.lot.index_of_level = __thisLevel_index
thisNode.lot.childs_size = __thisNode_child_size
__thisLevel_index++
})
if (__thisLOT_subling.all_strength > analyticResult.max_strength) {
analyticResult.max_strength = __thisLOT_subling.all_strength
}
// console.log(thisDeep, 'next level nodes:', newLevelNodes.length)
if (newLevelNodes.length > 0) {
// console.log('thisLevelNodes.length:', thisLevelNodes, thisLevelNodes.length)
this.analysisNodes(willLayoutNodes, newLevelNodes, thisDeep + 1, analyticResult, config)
} else {
willLayoutNodes.forEach(thisNode => {
if (thisNode.lot.childs_size > 0) {
thisNode.lot.strengthWithChilds = 0
}
})
willLayoutNodes.forEach(thisNode => {
if (thisNode.lot.childs_size === 0) {
thisNode.lot.strengthWithChilds = 1
this.conductStrengthToParents(thisNode)
}
})
this.analysisDataTree([willLayoutNodes[0]], 0)
// willLayoutNodes.forEach(thisNode => {
// thisNode.text = thisNode.lot.strengthWithChilds_from + ':' + thisNode.lot.strengthWithChilds + '/' + thisNode.lot.strength
// })
}
},
analysisNodes4Didirectional: function(willLayoutNodes, thisLevelNodes, thisDeep, analyticResult, levelDirect) {
if (thisLevelNodes.length > analyticResult.max_length) {
analyticResult.max_length = thisLevelNodes.length
}
if (thisDeep > analyticResult.max_deep) {
analyticResult.max_deep = thisDeep
}
var __thisLOT_subling = {
level: thisDeep,
all_size: thisLevelNodes.length,
all_strength: 0
}
var newLevelNodes = []
thisLevelNodes.forEach(thisNode => {
if (!thisNode.lot)thisNode.lot = {}
thisNode.lot.eached = true
thisNode.lot.subling = __thisLOT_subling
thisNode.lot.level = thisDeep
willLayoutNodes.push(thisNode)
})
var __thisLevel_index = 0
// var __prev_node
thisLevelNodes.forEach(thisNode => {
var __thisNode_child_size = 0
if (levelDirect === 0) {
// console.log('Build node::from::', thisNode.name, thisNode.targetNodes.length)
let __thisTargetIndex = 0
thisNode.targetNodes.forEach((thisTarget) => {
if (!thisTarget.lot)thisTarget.lot = { eached: false }
if (!thisTarget.lot.eached) {
if (SeeksGraphMath.isAllowShowNode(thisTarget)) {
thisTarget.lot.eached = true
thisTarget.lot.parent = thisNode
thisTarget.lot.index_of_parent = __thisTargetIndex++
// thisTarget.lot.prevNode = __prev_node
// if (__prev_node)__prev_node.lot.nextNode = thisTarget
// __prev_node = thisTarget
thisNode.lot.childs.push(thisTarget)
newLevelNodes.push(thisTarget)
__thisNode_child_size++
} else {
thisNode.lot.childs.push(thisTarget)
// console.log('hide node:', thisTarget.text, 'from:', thisNode.text)
}
} else {
// console.log('solved node:', thisTarget.text, 'from:', thisNode.text)
}
})
} else if (levelDirect === -1) {
// console.log('Build node::from::', thisNode.name, thisNode.targetFrom.length)
let __thisTargetIndex = 0
thisNode.targetFrom.forEach((thisTarget) => {
if (!thisTarget.lot)thisTarget.lot = { eached: false }
if (!thisTarget.lot.eached) {
if (SeeksGraphMath.isAllowShowNode(thisTarget)) {
thisTarget.lot.eached = true
thisTarget.lot.parent = thisNode
thisTarget.lot.index_of_parent = __thisTargetIndex++
// thisTarget.lot.prevNode = __prev_node
// if (__prev_node)__prev_node.lot.nextNode = thisTarget
// __prev_node = thisTarget
thisNode.lot.childs.push(thisTarget)
newLevelNodes.push(thisTarget)
__thisNode_child_size++
} else {
thisNode.lot.childs.push(thisTarget)
// console.log('hide node:', thisTarget.name, 'from:', thisNode.text)
}
}
})
} else {
// console.log('Build node::to::', thisNode.name, thisNode.targetTo.length)
let __thisTargetIndex = 0
thisNode.targetTo.forEach((thisTarget) => {
if (!thisTarget.lot)thisTarget.lot = { eached: false }
if (!thisTarget.lot.eached) {
if (SeeksGraphMath.isAllowShowNode(thisTarget)) {
thisTarget.lot.eached = true
thisTarget.lot.parent = thisNode
thisTarget.lot.index_of_parent = __thisTargetIndex++
// thisTarget.lot.prevNode = __prev_node
// if (__prev_node)__prev_node.lot.nextNode = thisTarget
// __prev_node = thisTarget
thisNode.lot.childs.push(thisTarget)
newLevelNodes.push(thisTarget)
__thisNode_child_size++
} else {
thisNode.lot.childs.push(thisTarget)
// console.log('hide node:', thisTarget.name, 'from:', thisNode.text)
}
}
})
}
thisNode.lot.strength = __thisNode_child_size > 0 ? __thisNode_child_size : 1
__thisLOT_subling.all_strength += thisNode.lot.strength
thisNode.lot.strength_plus = __thisLOT_subling.all_strength
thisNode.lot.index_of_level = __thisLevel_index
thisNode.lot.childs_size = __thisNode_child_size
__thisLevel_index++
})
if (__thisLOT_subling.all_strength > analyticResult.max_strength) {
analyticResult.max_strength = __thisLOT_subling.all_strength
}
// console.log(thisDeep, 'next level nodes:', newLevelNodes.length)
if (newLevelNodes.length > 0) {
// console.log('thisLevelNodes.length:', thisLevelNodes, thisLevelNodes.length)
SeeksGraphMath.analysisNodes4Didirectional(willLayoutNodes, newLevelNodes, thisDeep + (levelDirect === -1 ? -1 : 1), analyticResult, levelDirect)
} else {
willLayoutNodes.forEach(thisNode => {
if (thisNode.lot.childs_size > 0) {
thisNode.lot.strengthWithChilds = 0
}
})
willLayoutNodes.forEach(thisNode => {
if (thisNode.lot.childs_size === 0) {
thisNode.lot.strengthWithChilds = 1
SeeksGraphMath.conductStrengthToParents(thisNode)
}
})
SeeksGraphMath.analysisDataTree([willLayoutNodes[0]], 0, levelDirect)
// willLayoutNodes.forEach(thisNode => {
// thisNode.text = thisNode.lot.strengthWithChilds_from + ':' + thisNode.lot.strengthWithChilds + '/' + thisNode.lot.strength
// })
}
},
conductStrengthToParents(node) {
if (node.lot.parent) {
node.lot.parent.lot.strengthWithChilds += 1
this.conductStrengthToParents(node.lot.parent)
}
},
analysisDataTree: function(thisLevelNodes, thisDeep, levelDirect) {
if (levelDirect === undefined) levelDirect = 1
var newLevelNodes = []
var currentLevelStrengthWidthChilds = 0
thisLevelNodes.forEach(thisNode => {
// console.log('Place node aaaaaa:', levelDirect, thisNode.text, (thisNode.lot.level < 0 ? -1 : 1))
if (thisNode.lot.level === 0 || levelDirect === (thisNode.lot.level < 0 ? -1 : 1)) {
if (thisNode.lot.childs_size > 0) {
thisNode.lot.childs.forEach((thisTarget) => {
newLevelNodes.push(thisTarget)
})
}
if (thisNode.lot.parent && currentLevelStrengthWidthChilds < thisNode.lot.parent.lot.strengthWithChilds_from) {
currentLevelStrengthWidthChilds = thisNode.lot.parent.lot.strengthWithChilds_from
}
thisNode.lot.strengthWithChilds_from = currentLevelStrengthWidthChilds
currentLevelStrengthWidthChilds += thisNode.lot.strengthWithChilds
}
})
// console.log(thisDeep, 'next level nodes:', newLevelNodes.length)
if (newLevelNodes.length > 0) {
this.analysisDataTree(newLevelNodes, thisDeep + levelDirect, levelDirect)
}
},
// conductStrengthToParents(node) {
// if (node.lot.childs_size === 0) {
// return 1
// } else {
// var _sum = 0
// node.lot.childs.forEach(thisChild => {
// thisChild.lot.strengthWithChilds = this.conductStrengthToParents(thisChild)
// _sum += thisChild.lot.strengthWithChilds
// })
// return _sum
// }
// },
isAllowShowNode: function(thisNode) {
const _r = thisNode.isShow !== false && thisNode.isHide !== true && (!thisNode.lot.parent || this.isAllowShowNode(thisNode.lot.parent, false) === true)
// if (derict !== false && _r === false) console.log('hide node by:', thisNode.isShow !== false, thisNode.isHide !== true)
return _r
}
}
export default SeeksGraphMath

View File

@@ -0,0 +1,63 @@
import SeeksBidirectionalTreeLayouter from './layouters/SeeksBidirectionalTreeLayouter'
import SeeksCenterLayouter from './layouters/SeeksCenterLayouter'
import SeeksCircleLayouter from './layouters/SeeksCircleLayouter'
import SeeksAutoLayouter from './layouters/SeeksAutoLayouter'
import SeeksFixedLayouter from './layouters/SeeksFixedLayouter'
var SeeksRGLayouters = {
createLayout(layoutSetting, _graphSetting) {
_graphSetting.canvasZoom = 100
_graphSetting.layoutClassName = layoutSetting.layoutClassName
_graphSetting.layoutLabel = layoutSetting.label
_graphSetting.layoutName = layoutSetting.layoutName
_graphSetting.layoutDirection = layoutSetting.layoutDirection
if (layoutSetting.useLayoutStyleOptions === true) {
_graphSetting.defaultExpandHolderPosition = layoutSetting.defaultExpandHolderPosition
_graphSetting.defaultJunctionPoint = layoutSetting.defaultJunctionPoint
_graphSetting.defaultNodeColor = layoutSetting.defaultNodeColor
_graphSetting.defaultNodeFontColor = layoutSetting.defaultNodeFontColor
_graphSetting.defaultNodeBorderColor = layoutSetting.defaultNodeBorderColor
_graphSetting.defaultNodeBorderWidth = layoutSetting.defaultNodeBorderWidth
_graphSetting.defaultLineColor = layoutSetting.defaultLineColor
_graphSetting.defaultLineWidth = layoutSetting.defaultLineWidth
_graphSetting.defaultLineShape = layoutSetting.defaultLineShape
_graphSetting.defaultNodeShape = layoutSetting.defaultNodeShape
_graphSetting.defaultNodeWidth = layoutSetting.defaultNodeWidth
_graphSetting.defaultNodeHeight = layoutSetting.defaultNodeHeight
_graphSetting.defaultLineMarker = layoutSetting.defaultLineMarker
_graphSetting.defaultShowLineLabel = layoutSetting.defaultShowLineLabel
}
var _layout = null
if (layoutSetting.layoutName === 'SeeksBidirectionalTreeLayouter' || layoutSetting.layoutName === 'tree') {
_layout = new SeeksBidirectionalTreeLayouter(layoutSetting, _graphSetting)
} else if (layoutSetting.layoutName === 'SeeksCenterLayouter' || layoutSetting.layoutName === 'center') {
_layout = new SeeksCenterLayouter(layoutSetting, _graphSetting)
} else if (layoutSetting.layoutName === 'SeeksCircleLayouter' || layoutSetting.layoutName === 'circle') {
_layout = new SeeksCircleLayouter(layoutSetting, _graphSetting)
} else if (layoutSetting.layoutName === 'SeeksAutoLayouter' || layoutSetting.layoutName === 'force') {
_layout = new SeeksAutoLayouter(layoutSetting, _graphSetting)
} else if (layoutSetting.layoutName === 'SeeksFixedLayouter' || layoutSetting.layoutName === 'fixed') {
_layout = new SeeksFixedLayouter(layoutSetting, _graphSetting)
}
_graphSetting.isNeedShowAutoLayoutButton = layoutSetting.allowAutoLayoutIfSupport !== false && _layout.autoLayout !== undefined
return _layout
},
switchLayout(layoutLabelOrSetting, _graphSetting) {
const __origin_nodes = _graphSetting.layouter ? _graphSetting.layouter.__origin_nodes : []
const __rootNode = _graphSetting.layouter ? _graphSetting.layouter.rootNode : null
if ((typeof layoutLabelOrSetting) === 'string') {
for (var thisLayoutSetting in _graphSetting.layouts) {
if (thisLayoutSetting.label === layoutLabelOrSetting) {
layoutLabelOrSetting = thisLayoutSetting
break
}
}
}
_graphSetting.layouter = SeeksRGLayouters.createLayout(layoutLabelOrSetting, _graphSetting)
_graphSetting.layouter.__origin_nodes = __origin_nodes
_graphSetting.layouter.rootNode = __rootNode
}
}
export default SeeksRGLayouters

View File

@@ -0,0 +1,571 @@
<template>
<g
v-if="lineProps.isHide !== true && isAllowShowNode(lineProps.fromNode) && isAllowShowNode(lineProps.toNode)"
ref="seeksRGLink"
transform="translate(0,0)"
>
<!--<path :d="createLinePath(lineProps.fromNode, lineProps.toNode)" :class="[lineProps.styleClass,graphSetting.checkedLineId==lineProps.seeks_id?'c-rg-line-checked':'']" :stroke="lineProps.color?linkProps.color:graphSetting.defaultLineColor" :marker-end="'url(\'#' + (linkProps.arrow?linkProps.arrow:'arrow-default') + '\')'" fill="none" class="c-rg-line" @click="onClick($event)" />-->
<!--<g v-if="graphSetting.defaultShowLineLabel" :transform="getTextTransform(textPositon.x,textPositon.y,textPositon.rotate)">-->
<!--<text v-for="thisRelation in lineProps.relations" :key="'t-'+thisRelation.id" :x="0" :y="0" :style="{fill:(thisRelation.fontColor?thisRelation.fontColor:(thisRelation.color?thisRelation.color:undefined))}" class="c-rg-link-text" @click="onClick($event)">-->
<!--&lt;!&ndash;<textPath :xlink:href="'#'+lineProps.id">{{ lineProps.text }}</textPath>&ndash;&gt;-->
<!--{{ thisRelation.text }}-->
<!--</text>-->
<!--</g>-->
<template v-for="(thisRelation, ri) in lineProps.relations">
<g v-if="thisRelation.isHide === false" :key="'l-' + thisRelation.id">
<path
:d="createLinePath(lineProps.fromNode, lineProps.toNode, ri, thisRelation)"
:class="[
thisRelation.styleClass,
graphSetting.checkedLineId == lineProps.seeks_id ? 'c-rg-line-checked' : '',
]"
:stroke="thisRelation.color ? thisRelation.color : graphSetting.defaultLineColor"
:style="{
'stroke-width': (thisRelation.lineWidth ? thisRelation.lineWidth : graphSetting.defaultLineWidth) + 'px',
}"
:marker-end="getArrow(thisRelation.isHideArrow, thisRelation.arrow, thisRelation.color)"
fill="none"
class="c-rg-line"
@click="onClick($event)"
/>
<g
v-if="graphSetting.defaultShowLineLabel && graphSetting.canvasZoom > 40"
:transform="
getTextTransform(
thisRelation,
thisRelation.textPositon.x,
thisRelation.textPositon.y,
thisRelation.textPositon.rotate
)
"
>
<text
:key="'t-' + thisRelation.id"
:x="0"
:y="0"
:style="{
fill: thisRelation.fontColor
? thisRelation.fontColor
: thisRelation.color
? thisRelation.color
: undefined,
}"
class="c-rg-link-text"
@click="onClick($event)"
>
<!--<textPath :xlink:href="'#'+lineProps.id">{{ lineProps.text }}</textPath>-->
{{ thisRelation.text }}
</text>
</g>
</g>
</template>
</g>
</template>
<script>
/* eslint-disable */
// import SeeksRGStore from './SeeksRGStore'
import SeeksGraphMath from './SeeksGraphMath'
// import Velocity from 'velocity-animate'
// import { mapState } from 'vuex'
// var _parent = this.$parent
const JUNCTION_POINT_STYLE = {
border: 'border',
ltrb: 'ltrb',
tb: 'tb',
lr: 'lr',
}
export default {
name: 'SeeksRGLink',
props: {
graphSetting: {
mustUseProp: true,
default: () => {
return {}
},
type: Object,
},
lineProps: {
mustUseProp: true,
default: () => {
return {}
},
type: Object,
},
onLineClick: {
mustUseProp: false,
default: () => {
return () => {}
},
type: Function,
},
},
data() {
return {
is_flashing: false,
}
},
show() {
this.isShow = true
},
watch: {},
mounted() {
// this.refresh()
// var __this = this
// setInterval(this.onLineClick, 1000)
},
// beforeDestroy() {
// const elx = this.$refs.seeksRGLink
// elx.remove()
// },
methods: {
getTextTransform(thisRelation, x, y, rotate) {
if (!x || !y) {
return 'translate(0,0)'
}
var __lineShape =
thisRelation.lineShape === undefined ? this.graphSetting.defaultLineShape : thisRelation.lineShape
if (__lineShape === 1 || __lineShape === 4) {
return 'translate(' + x + ',' + y + ')rotate(' + (rotate || 0) + ')'
} else {
return 'translate(' + x + ',' + y + ')'
}
},
getArrow(isHideArrow, arrow, color) {
// console.log('xxxxxxxxxxxx')
if (isHideArrow) {
return 'none'
} else {
var _arrow = this.$parent.getLineArrow(color)
return "url('#" + _arrow + "')"
}
},
createLinePath(from, to, ri, relationData) {
// console.log('redrawLine:', this.lineProps.fromNode.id, this.lineProps.toNode.id, ri)
// console.log('_point:', _point)
if (!ri) ri = 0
var __lineShape =
relationData.lineShape === undefined ? this.graphSetting.defaultLineShape : relationData.lineShape
var __lineDirection =
relationData.lineDirection === undefined ? this.graphSetting.layoutDirection : relationData.lineDirection
var from_x = from.x
var from_y = from.y
var to_x = to.x
var to_y = to.y
if (isNaN(from_x) || isNaN(from_y)) {
console.error('error start node:', from)
relationData.textPositon.x = 50
relationData.textPositon.y = 50
relationData.textPositon.rotate = 0
return 'M 0 0 L 100 100'
}
if (isNaN(to_x) || isNaN(to_y)) {
console.error('error start point:', from)
relationData.textPositon.x = 50
relationData.textPositon.y = 50
relationData.textPositon.rotate = 0
return 'M 0 0 L 100 100'
}
var f_W = from.el.offsetWidth || from.width || from.w
var f_H = from.el.offsetHeight || from.height || from.h
if (isNaN(f_W) || isNaN(f_H)) {
// console.log('error from node size:', f_W, f_H)
relationData.textPositon.x = 50
relationData.textPositon.y = 50
relationData.textPositon.rotate = 0
return 'M 0 0 L 100 100'
}
var t_W = to.el.offsetWidth || to.width || to.w
var t_H = to.el.offsetHeight || to.height || to.h
if (isNaN(t_W) || isNaN(t_H)) {
// console.log('error to node size:', f_W, f_H)
relationData.textPositon.x = 50
relationData.textPositon.y = 50
relationData.textPositon.rotate = 0
return 'M 0 0 L 100 100'
}
if (relationData.isReverse) {
;[from_x, from_y, to_x, to_y, f_W, f_H, t_W, t_H] = [to_x, to_y, from_x, from_y, t_W, t_H, f_W, f_H]
}
var __params4start = [
from_x,
from_y,
to_x,
to_y,
f_W,
f_H,
t_W,
t_H,
this.graphSetting.defaultNodeShape,
relationData.isReverse,
this.lineProps.relations.length,
ri,
]
var __params4end = [
to_x,
to_y,
from_x,
from_y,
t_W,
t_H,
f_W,
f_H,
this.graphSetting.defaultNodeShape,
!relationData.isReverse,
this.lineProps.relations.length,
ri,
]
var __start, __end
var _junctionPointStyle = this.graphSetting.defaultJunctionPoint
if (!_junctionPointStyle) {
_junctionPointStyle = JUNCTION_POINT_STYLE.border
}
if (_junctionPointStyle === JUNCTION_POINT_STYLE.border) {
__start = SeeksGraphMath.getBorderPoint4MultiLine(...__params4start)
__end = SeeksGraphMath.getBorderPoint4MultiLine(...__params4end)
} else if (_junctionPointStyle === JUNCTION_POINT_STYLE.ltrb) {
__start = SeeksGraphMath.getRectJoinPoint(...__params4start)
__end = SeeksGraphMath.getRectJoinPoint(...__params4end)
} else if (_junctionPointStyle === JUNCTION_POINT_STYLE.tb) {
__start = SeeksGraphMath.getRectVJoinPoint(...__params4start)
__end = SeeksGraphMath.getRectVJoinPoint(...__params4end)
} else if (_junctionPointStyle === JUNCTION_POINT_STYLE.lr) {
__start = SeeksGraphMath.getRectHJoinPoint(...__params4start)
__end = SeeksGraphMath.getRectHJoinPoint(...__params4end)
}
var fx = __start.x
var fy = __start.y
var tx = __end.x
var ty = __end.y
if (isNaN(fx) || isNaN(fy)) {
console.error('error start point:', from)
relationData.textPositon.x = 50
relationData.textPositon.y = 50
relationData.textPositon.rotate = 0
return 'M 0 0 L 100 100'
}
if (isNaN(tx) || isNaN(ty)) {
console.error('error end point:', to)
relationData.textPositon.x = 50
relationData.textPositon.y = 50
relationData.textPositon.rotate = 0
return 'M 0 0 L 100 100'
}
var __buff_x = __end.x - __start.x
var __buff_y = __end.y - __start.y
var __buff_type = __end.x > __start.x ? 1 : -1
if (__lineDirection === 'v') {
__buff_type = __end.y > __start.y ? 1 : -1
}
var __path = ''
if (__lineShape === 4) {
const distanceRate = (60 / (this.lineProps.relations.length + 1)) * (ri + 1) - 30
if (__lineDirection === 'v') {
__buff_y = __buff_y - (__buff_type * 33 + distanceRate)
relationData.textPositon.x = fx + __buff_x + 5
relationData.textPositon.y = fy + __buff_type * 40 + distanceRate
relationData.textPositon.rotate = 90
__path =
'M ' +
fx +
' ' +
fy +
' v' +
(__buff_type * 33 + distanceRate) +
' h' +
(__buff_x + distanceRate) +
' v' +
__buff_y
} else {
if (relationData.reverseText === true) {
relationData.textPositon.x = fx + __buff_type * 10 - (__buff_type < 0 ? 30 : 0)
relationData.textPositon.y = fy - 5
__buff_x = __buff_x - __buff_type * 120
__path = 'M ' + fx + ' ' + fy + ' h' + __buff_type * 120 + ' v' + __buff_y + ' h' + __buff_x
} else {
relationData.textPositon.x = fx + __buff_type * 50 - (__buff_type < 0 ? 30 : 0)
relationData.textPositon.y = fy + __buff_y - 5 + distanceRate
__buff_x = __buff_x - (__buff_type * 33 + distanceRate)
__buff_y = __buff_y + __buff_type * distanceRate
__path = 'M ' + fx + ' ' + fy + ' h' + (__buff_type * 33 + distanceRate) + ' v' + __buff_y + ' h' + __buff_x
}
}
} else if (__lineShape === 2) {
// var __buff_type_x = __end.x > __start.x ? 1 : -1
const __buff_type_y = __end.y > __start.y ? 1 : -1
const _base = Math.abs(__buff_x) + Math.abs(__buff_y)
relationData.textPositon.x = parseInt(__end.x - (__buff_x / _base) * 60 - 20)
relationData.textPositon.y = parseInt(__end.y - (__buff_y / _base) * 60 - 20 * __buff_type_y)
const distanceRate = (1 / (this.lineProps.relations.length + 1)) * (ri + 1) - 0.5 + 0.5
if (__lineDirection === 'v') {
__path =
'M' +
fx +
',' +
fy +
' c' +
0 +
',' +
__buff_type * 30 +
' ' +
__buff_x * distanceRate +
',' +
__buff_type * -10 +
' ' +
__buff_x +
',' +
__buff_y
} else {
__path =
'M' +
fx +
',' +
fy +
' c' +
__buff_type * 30 +
',' +
0 +
' ' +
__buff_type * -10 +
',' +
__buff_y * distanceRate +
' ' +
__buff_x +
',' +
__buff_y
}
} else if (__lineShape === 6) {
// const __buff_type_x = __end.x > __start.x ? 1 : -1
const __buff_type_y = __end.y > __start.y ? 1 : -1
const _base = Math.abs(__buff_x) + Math.abs(__buff_y)
relationData.textPositon.x = parseInt(__end.x - (__buff_x / _base) * 60 - 20)
relationData.textPositon.y = parseInt(__end.y - (__buff_y / _base) * 60 - 20 * __buff_type_y)
if (__lineDirection === 'v') {
__path =
'M' +
fx +
',' +
fy +
' c' +
0 +
',' +
__buff_y / 2 +
' ' +
__buff_x +
',' +
__buff_y / 2 +
' ' +
__buff_x +
',' +
__buff_y
} else {
__path =
'M' +
fx +
',' +
fy +
' c' +
__buff_x / 2 +
',' +
0 +
' ' +
__buff_x / 2 +
',' +
__buff_y +
' ' +
__buff_x +
',' +
__buff_y
}
} else if (__lineShape === 3) {
relationData.textPositon.x = __end.x - __buff_type * 63
relationData.textPositon.y = __end.y + 3
const distanceRate = (1 / (this.lineProps.relations.length + 1)) * (ri + 1) - 0.5 + 0.5
if (__lineDirection === 'v') {
__path =
'M' +
fx +
',' +
fy +
' c' +
0 +
',' +
__buff_y * distanceRate +
' ' +
0 +
',' +
0 +
' ' +
__buff_x +
',' +
__buff_y
} else {
// console.log('start:', __start, __end, __buff_x, __buff_y)
__path =
'M' +
fx +
',' +
fy +
' c' +
__buff_type * 30 +
',' +
0 +
' ' +
__buff_type * -10 +
',' +
__buff_y * distanceRate +
' ' +
__buff_x +
',' +
__buff_y
}
} else if (__lineShape === 5) {
// relationData.text.x = __start.x + __buff_x / 2 - 33
// relationData.text.y = __start.y + __buff_y / 2 - 3
relationData.textPositon.x = __end.x - __buff_type * 63
relationData.textPositon.y = __end.y + 3
const distanceRate = (1 / (this.lineProps.relations.length + 1)) * (ri + 1) - 0.5 + 0.5
if (__lineDirection === 'v') {
__path =
'M' +
fx +
',' +
fy +
' c' +
0 +
',' +
0 +
' ' +
0 +
',' +
__buff_y * distanceRate +
' ' +
__buff_x +
',' +
__buff_y // 鱼尾
} else {
__path =
'M' +
fx +
',' +
fy +
' c' +
0 +
',' +
0 +
' ' +
__buff_x * distanceRate +
',' +
0 +
' ' +
__buff_x +
',' +
__buff_y // 鱼尾
}
// __path = 'M' + fx + ',' + fy + ' c' + (0) + ',' + (0) + ' ' + (0) + ',' + (__buff_y * 0.5) + ' ' + __buff_x + ',' + __buff_y
// __path = 'M' + fx + ',' + fy + ' c' + (0) + ',' + (0) + ' ' + (-100) + ',' + (__buff_y * 0.5) + ' ' + __buff_x + ',' + __buff_y
// __path = 'M' + fx + ',' + fy + ' c' + (30) + ',' + (0) + ' ' + (-10) + ',' + (__buff_y * 0.5) + ' ' + __buff_x + ',' + __buff_y
// __path = 'M' + fx + ',' + fy + ' c' + (50) + ',' + (0) + ' ' + (-50) + ',' + (__buff_y * 0.5) + ' ' + __buff_x + ',' + __buff_y
// __path = 'M' + fx + ',' + fy + ' c' + (100) + ',' + (0) + ' ' + (10) + ',' + (__buff_y * 0.5) + ' ' + __buff_x + ',' + __buff_y
// __path = 'M' + fx + ',' + fy + ' c' + (0) + ',' + (0) + ' ' + (__buff_x * 0.5) + ',' + (0) + ' ' + __buff_x + ',' + __buff_y // 类似鱼尾
// __path = 'M' + fx + ',' + fy + ' c' + (__buff_x * 0.5) + ',' + (0) + ' ' + (0) + ',' + (0) + ' ' + __buff_x + ',' + __buff_y // 三角
// __path = 'M' + fx + ',' + fy + ' c' + (0) + ',' + (0) + ' ' + (__buff_x * 0.5) + ',' + (0) + ' ' + __buff_x + ',' + __buff_y // 鱼尾
// __path = 'M' + fx + ',' + fy + ' c' + (50) + ',' + (__buff_y * 0.5) + ' ' + (0) + ',' + (0) + ' ' + __buff_x + ',' + __buff_y //
// __path = 'M' + fx + ',' + fy + ' c' + (50) + ',' + (__buff_y * 0.5) + ' ' + (0) + ',' + (0) + ' ' + __buff_x + ',' + __buff_y
} else {
var _angle_type = SeeksGraphMath.getAngleType(__buff_x, __buff_y)
relationData.textPositon.rotate = SeeksGraphMath.getTextAngle(fx, fy, tx, ty)
var _xxx = _angle_type === 2 || _angle_type === 4 ? -1 : 1
var _x = (__buff_y === 0 ? -50 : (Math.sin(Math.atan(__buff_x / __buff_y)) * 10) / Math.sin(90)) * _xxx
var _y = __buff_x === 0 ? -50 : (Math.sin(Math.atan(__buff_y / __buff_x)) * 10) / Math.sin(90)
relationData.textPositon.x = parseInt(__start.x + __buff_x / 2 - _x)
relationData.textPositon.y = parseInt(__start.y + __buff_y / 2 - _y)
if (isNaN(relationData.textPositon.rotate)) {
relationData.textPositon.rotate = 0
console.log('NaN rotate:', relationData)
}
// this.lineProps.text = relationData.text.rotate
__path = 'M ' + fx + ' ' + fy + ' L ' + tx + ' ' + ty
}
return __path
},
onClick(e) {
// RGStore.commit('setCurrentLineId', this.lineProps.id)
this.graphSetting.checkedLineId = this.lineProps.seeks_id
this.lineProps.fromNode.selected = true
this.lineProps.toNode.selected = true
// Velocity(this.$refs.seeksRGLink, { strokDashoffset: 50 }, { duration: 3000, loop: 5 })
setTimeout(
function() {
this.lineProps.fromNode.selected = false
this.lineProps.toNode.selected = false
}.bind(this),
2000
)
if (this.onLineClick) {
this.onLineClick(this.lineProps, e)
}
},
isAllowShowNode: function(thisNode) {
const _r =
thisNode.isShow !== false &&
thisNode.isHide !== true &&
(!thisNode.lot.parent || this.isAllowShowNode(thisNode.lot.parent, false) === true)
// if (derict !== false && _r === false) console.log('be hide node:', thisNode.text)
return _r
},
flash() {},
},
}
</script>
<style type="">
/*.RGLine-enter-active {*/
/*transition: all .3s ease;*/
/*}*/
/*.RGLine-leave-active {*/
/*transition: all .8s cubic-bezier(1.0, 0.5, 0.8, 1.0);*/
/*}*/
.c-rg-link-text {
fill: #888888;
font-size: 12px;
}
.c-rg-line {
z-index: 1000;
fill-rule: nonzero;
/*marker-end: url('#arrow');*/
/* firefox bug fix - won't rotate at 90deg angles */
/*-moz-transform: rotate(-89deg) translateX(-190px);*/
/*animation-timing-function:linear;*/
/*animation: ACTRGLineInit 1s;*/
}
.c-rg-line-tool {
stroke-dasharray: 5, 5, 5;
}
.c-rg-line-flash {
/* firefox bug fix - won't rotate at 90deg angles */
-moz-transform: rotate(-89deg) translateX(-190px);
animation-timing-function: linear;
animation: ACTRGLineChecked 10s;
}
@keyframes ACTRGLineInit {
from {
stroke-dashoffset: 10px;
stroke-dasharray: 20, 20, 20;
}
to {
stroke-dashoffset: 0;
stroke-dasharray: 0, 0, 0;
}
}
@keyframes ACTRGLineChecked {
from {
stroke-dashoffset: 352px;
}
to {
stroke-dashoffset: 0;
}
}
</style>

View File

@@ -0,0 +1,479 @@
<template>
<div
v-show="isAllowShowNode(nodeProps)"
ref="seeksRGNode"
:style="{'left':nodeProps.x + 'px','top':nodeProps.y + 'px', 'opacity': (nodeProps.opacity>1?nodeProps.opacity/100:nodeProps.opacity) }"
class="rel-node-peel"
@mousedown.left.stop="onDragStart($event)"
@mouseover.stop="onMouseHover($event)"
@mouseout.stop="onMouseOut($event)"
@click.stop="onclick($event)"
>
<div v-if="(nodeProps.expandHolderPosition&&nodeProps.expandHolderPosition!=='hide')||(graphSetting.defaultExpandHolderPosition&&graphSetting.defaultExpandHolderPosition!=='hide'&&nodeProps.lot.childs&&nodeProps.lot.childs.length>0)" :class="[('c-expand-positon-'+(nodeProps.expandHolderPosition||graphSetting.defaultExpandHolderPosition))]" class="c-btn-open-close">
<span :class="expandButtonClass" :style="{'background-color':(nodeProps.color||graphSetting.defaultNodeColor)}" @click.stop="expandOrCollapseNode">
</span>
</div>
<div v-if="nodeProps.html" v-html="nodeProps.html" />
<div
v-else
:class="['rel-node-shape-'+(nodeProps.nodeShape===undefined?graphSetting.defaultNodeShape:nodeProps.nodeShape),'rel-node-type-'+nodeProps.type, (nodeProps.id===graphSetting.checkedNodeId?'rel-node-checked':''), (nodeProps.selected?'rel-node-selected':''), nodeProps.styleClass, (hovering?'rel-node-hover':''), (nodeProps.innerHTML?'rel-diy-node':'')]"
:style="{'background-color':(nodeProps.color===undefined?graphSetting.defaultNodeColor:nodeProps.color),'color':(nodeProps.fontColor===undefined?graphSetting.defaultNodeFontColor:nodeProps.fontColor),'border': (nodeProps.borderColor || graphSetting.defaultNodeBorderColor) + ' solid '+(nodeProps.borderWidth || graphSetting.defaultNodeBorderWidth)+'px', 'width':(nodeProps.width || graphSetting.defaultNodeWidth)+'px', 'height':(nodeProps.height||graphSetting.defaultNodeHeight)+'px'}"
class="rel-node"
>
<template v-if="!(graphSetting.hideNodeContentByZoom === true && graphSetting.canvasZoom<40)">
<slot :node="nodeProps" name="node">
<div v-if="!nodeProps.innerHTML" :style="{'color':(nodeProps.fontColor || graphSetting.defaultNodeFontColor)}" class="c-node-text">
<span v-html="getNodeName()" />
</div>
<div v-else v-html="nodeProps.innerHTML" />
</slot>
</template>
</div>
</div>
</template>
<script>
/* eslint-disable */
// import SeeksRGStore from './SeeksRGStore'
// import SeeksGraphMath from './SeeksGraphMath'
import SeeksRGUtils from './SeeksRGUtils'
// import Velocity from 'velocity-animate'
// import { mapState } from 'vuex'
// var _parent = this.$parent
// function isAllowShowNode(isShow, isHide, parent) {
// const _r = isShow !== false && isHide !== true && (!parent || isAllowShowNode(parent.isShow, parent.isHide, parent.lot.parent) === true)
// return _r
// }
export default {
name: 'SeeksRGNode',
components: { },
props: {
graphSetting: {
mustUseProp: true,
default: () => { return {} },
type: Object
},
nodeProps: {
mustUseProp: true,
default: () => { return {} },
type: Object
},
onNodeClick: {
mustUseProp: false,
default: () => { return () => {} },
type: Function
}
},
data() {
return {
hovering: false,
borderColor: '',
dragging: false
}
},
computed: {
expandButtonClass() {
return this.nodeProps.expanded===false ? 'c-expanded' : 'c-collapsed'
}
},
// show() {
//
// },
watch: {
// 'nodeProps.isShow': function(v) {
// console.log('nodeProps.isShow:', v)
// if (v === true) {
// this.$nextTick(() => {
// this.nodeProps.el.offsetWidth = this.$refs.seeksRGNode.offsetWidth
// this.nodeProps.el.offsetHeight = this.$refs.seeksRGNode.offsetHeight
// console.log('node 挂载 el size:', this.$refs.seeksRGNode.offsetWidth, this.$refs.seeksRGNode.offsetHeight)
// })
// }
// }
},
created() {
// Vue.version
},
mounted() {
this.refreshNodeProperties()
// this.leave(this.$refs.seeksRGNode)
// console.log('node show:', this.nodeProps.text, this.$parent.$slots.node)
},
beforeDestroy() {
const elx = this.$refs.seeksRGNode
elx.remove()
},
methods: {
refreshNodeProperties() {
this.nodeProps.el = this.$refs.seeksRGNode
// console.log('node 挂载 el:', this.nodeProps.text, this.nodeProps.el.offsetWidth, this.nodeProps.el.offsetHeight)
// this.$nextTick(() => {
// this.nodeProps.el.offsetWidth = this.$refs.seeksRGNode.offsetWidth
// this.nodeProps.el.offsetHeight = this.$refs.seeksRGNode.offsetHeight
// console.log('node 挂载 el size:', this.$refs.seeksRGNode.offsetWidth, this.$refs.seeksRGNode.offsetHeight)
// })
// this.nodeProps.em = true
// if (this.nodeProps.style === 0) {
// this.nodeProps.name = SeeksRGUtils.transName4Circle(this.nodeProps.name, this.nodeProps.el.offsetWidth)
// console.log('resize node name:', this.name)
// }
// this.nodeProps.el_width = this.$refs.seeksRGNode.offsetWidth
// this.nodeProps.el_height = this.$refs.seeksRGNode.offsetHeight
// var __this = this
// setInterval(function() {
// __this.nodeProps.x = __this.nodeProps.x
// __this.nodeProps.y = __this.nodeProps.y
// }, 1000)
},
getNodeName() {
// if (this.hovering) return 'N-' + this.nodeProps.seeks_id
if (this.hovering) {
return this.nodeProps.text
}
if (this.nodeProps.width === undefined && this.nodeProps.nodeShape !== 0) {
return this.nodeProps.text
}
var _w = this.nodeProps.el.offsetWidth
var _h = this.nodeProps.el.offsetHeight
var _num_l = parseInt((_w - 20) / 20)
var _num_c = parseInt((_h - 20) / 20)
if (_num_l === -1 || _num_c === -1) {
return this.nodeProps.text
}
var _length = _num_l * _num_c * 2
var _text_arr = []
var _current_length = 0
for (var i = 0; i < this.nodeProps.text.length; i++) {
var _thisChar = this.nodeProps.text[i]
var _charCode = _thisChar.charCodeAt(0)
var _charLength = 1
if (_charCode < 0 || _charCode > 255) {
_charLength = 2
}
if ((_current_length + _charLength) > _length) {
return _text_arr.join('') + '...'
} else {
_current_length += _charLength
_text_arr.push(_thisChar)
}
}
return _text_arr.join('')
// return _num_l + '/' + _num_c
// return this.nodeProps.text
},
expandOrCollapseNode(e) {
if (this.nodeProps.expanded === false) {
this.nodeProps.expanded = true
this.nodeProps.lot.childs.forEach(thisNode => {
thisNode.isShow = true
})
this.$parent.onNodeExpandEvent(this.nodeProps, e)
} else {
this.nodeProps.expanded = false
this.nodeProps.lot.childs.forEach(thisNode => {
thisNode.isShow = false
})
this.$parent.onNodeCollapseEvent(this.nodeProps, e)
}
},
onDragStart(e) {
if (this.graphSetting.disableDragNode || this.nodeProps.disableDrag) {
return
}
this.dragging = true
this.hovering = false
SeeksRGUtils.startDrag(e, this.nodeProps, this.onNodeDraged)
},
onNodeDraged(x, y) {
if (this.graphSetting.isMoveByParentNode) {
this.nodeProps.lot.childs.forEach(thisCnode => {
thisCnode.x += x
thisCnode.y += y
})
}
if (Math.abs(x) + Math.abs(y) > 6) {
setTimeout(function() {
if (window.SeeksGraphDebug) console.log('delay end dragging', this.dragging)
this.dragging = false
}.bind(this), 100)
} else {
this.dragging = false
}
},
onMouseHover() {
if (this.dragging) {
return
}
this.hovering = true
},
onMouseOut() {
this.hovering = false
},
onclick(e) {
if (this.dragging) {
return
}
if (!this.nodeProps.disableDefaultClickEffect) {
this.graphSetting.checkedNodeId = this.nodeProps.id
}
if (this.onNodeClick) {
this.onNodeClick(this.nodeProps, e)
}
},
// beforeEnter(el) {
// console.log('beforeEnter')
// el.style.opacity = 0
// el.style.transformOrigin = 'left'
// },
// enter(el, done) {
// console.log('enter')
// Velocity(el, { opacity: 1, fontSize: '1.4em' }, { duration: 300 })
// Velocity(el, { fontSize: '1em' }, { complete: done })
// },
// leave(el, done) {
// console.log('leave')
// Velocity(el, { translateX: '0px', rotateZ: '360deg' }, { duration: 600 })
// // Velocity(el, { rotateZ: '180deg' }, { loop: 1 })
// // Velocity(el, {
// // rotateZ: '45deg',
// // translateY: '30px',
// // translateX: '30px',
// // opacity: 0
// // }, { complete: done })
// },
getLightColor(col) {
// if (this.borderColor !== '') {
// return this.borderColor
// }
if (col[0] === '#') {
var _s = col.substring(1)
if (_s.length === 3) {
_s = _s[0] + _s[0] + _s[1] + _s[1] + _s[2] + _s[2]
}
var _rgb_arr = [
parseInt(_s[0] + '' + _s[1], 16),
parseInt(_s[2] + '' + _s[3], 16),
parseInt(_s[4] + '' + _s[5], 16)
]
if (window.SeeksGraphDebug) console.log('getLightColor1:', col, ':', _rgb_arr.join(','))
col = 'rgb(' + _rgb_arr.join(',') + ')'
}
var _st = col.substring(col.indexOf('(') + 1)
_st = _st.substring(0, _st.indexOf(')'))
var _rgb_string = _st.split(',')
// console.log('getLightColor444:', _st, ':', _rgb_string.join(','))
if (_rgb_string.length >= 3) {
var _rgb_number = [
parseInt(parseInt(_rgb_string[0]) * 0.9),
parseInt(parseInt(_rgb_string[1]) * 0.9),
parseInt(parseInt(_rgb_string[2]) * 0.9)
]
if (window.SeeksGraphDebug) console.log('getLightColor2:', col, ':', _rgb_number.join(','))
this.borderColor = 'rgb(' + _rgb_number.join(',') + ', 0.3)'
return this.borderColor
} else {
this.borderColor = col
return col
}
},
isAllowShowNode(thisNode) {
const _r = thisNode.isShow !== false && thisNode.isHide !== true && (!thisNode.lot.parent || this.isAllowShowNode(thisNode.lot.parent, false) === true)
return _r
}
}
}
</script>
<style>
.rg-icon {
width: 19px;
height: 19px;
vertical-align: 0px;
fill: currentColor;
overflow: hidden;
}
.el-icon-remove,.el-icon-circle-plus{
cursor: pointer;
}
.rel-node-peel{
clear: both;
padding:8px;
position: absolute;
font-size: 14px;
/*border:green solid 1px;*/
}
.rel-node{
text-align: center;
}
.rel-node-shape-1{
/*border: #FD8B37 solid 1px;*/
border-radius: 8px;
padding:5px;
padding-left:15px;
padding-right:15px;
}
.c-node-text{
height:100%;
width:100%;
display: flex;
align-items: center;
justify-content: center;
}
.rel-node-shape-0{
padding:10px;
}
.rel-node-shape-0{
width:80px;
height:80px;
border-radius: 50%;
/*border: #FD8B37 solid 2px;*/
/*text-align: left;*/
/*padding:10px;*/
/*white-space: nowrap;*/
/*text-overflow: ellipsis;*/
/*overflow: hidden;*/
/*word-break: break-all;*/
}
.rel-node-shape-0:hover{
/*overflow: visible;*/
/*text-overflow: inherit;*/
box-shadow: 0px 0px 5px #FFC5A6;
}
/*.rel-node{*/
/*display: table;*/
/*}*/
/*.rel-node span{*/
/*display: table-cell;*/
/*vertical-align:middle;*/
/*}*/
.rel-node-type-button{
border-radius: 25px;
color: blue;
cursor: pointer;
}
.rel-node-hover{
}
.rel-node-checked{
box-shadow: 0px 0px 10px #FD8B37;
/*border-color: #BA7909;*/
/*background-color: #FD8B37;*/
/*color: #ffffff;*/
/* firefox bug fix - won't rotate at 90deg angles */
-moz-transform: rotate(-89deg) translateX(-190px);
animation-timing-function:linear;
animation: ACTRGNodeInit 2s;
}
.rel-node-selected{
box-shadow: 0px 0px 10px #FD8B37;
/*border-color: #BA7909;*/
/*background-color: #FD8B37;*/
/*color: #ffffff;*/
/* firefox bug fix - won't rotate at 90deg angles */
-moz-transform: rotate(-89deg) translateX(-190px);
animation-timing-function:linear;
animation: ACTRGNodeInit 2s;
}
.rel-node-vtree-2 {
transform-origin:0 0;/* 设置旋转中心为左上角*/
/*transform-origin:50% 50%;!* 设置放大中心为元素中心 *!*/
transform: rotate(30deg) translateX(0px);
}
.rel-node-vtree {
width:130px;
height:45px;
text-align: left;
}
/*.c-node-text{*/
/*font-size: 12px;*/
/*display: inline-block;*/
/*width:100px;*/
/*height:16px;*/
/*margin-top:40px;*/
/*margin-left:-25px;*/
/*position:absolute;*/
/*word-break:keep-all;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;*/
/*text-align:center;*/
/*}*/
.c-btn-open-close{
position: absolute;
height:100%;
width:19px;
/*border:red solid 1px;*/
display: flex;
align-items: center;
justify-content: center;
/*border:#ff0000 solid 1px;*/
}
.c-btn-open-close span{
width: 19px;
height:19px;
display: inline-block;
text-align: center;
border-radius: 15px;
color: #ffffff;
cursor: pointer;
font-size: 19px;
line-height: 16px;
background-size: 100% 100%;
}
.c-expanded{
background-image: url(data:image/svg+xml;%20charset=utf8,%3Csvg%20t=%221606310217820%22%20viewBox=%220%200%201024%201024%22%20version=%221.1%22%20xmlns=%22http://www.w3.org/2000/svg%22%20p-id=%223373%22%20width=%2232%22%20height=%2232%22%3E%3Cpath%20d=%22M853.333333%20480H544V170.666667c0-17.066667-14.933333-32-32-32s-32%2014.933333-32%2032v309.333333H170.666667c-17.066667%200-32%2014.933333-32%2032s14.933333%2032%2032%2032h309.333333V853.333333c0%2017.066667%2014.933333%2032%2032%2032s32-14.933333%2032-32V544H853.333333c17.066667%200%2032-14.933333%2032-32s-14.933333-32-32-32z%22%20p-id=%223374%22%20fill=%22white%22%3E%3C/path%3E%3C/svg%3E);
}
.c-collapsed{
background-image: url(data:image/svg+xml;%20charset=utf8,%3Csvg%20t=%221606310454619%22%20class=%22icon%22%20viewBox=%220%200%201024%201024%22%20version=%221.1%22%20xmlns=%22http://www.w3.org/2000/svg%22%20p-id=%223662%22%20width=%22128%22%20height=%22128%22%3E%3Cpath%20d=%22M853.333333%20554.666667H170.666667c-23.466667%200-42.666667-19.2-42.666667-42.666667s19.2-42.666667%2042.666667-42.666667h682.666666c23.466667%200%2042.666667%2019.2%2042.666667%2042.666667s-19.2%2042.666667-42.666667%2042.666667z%22%20p-id=%223663%22%20fill=%22white%22%3E%3C/path%3E%3C/svg%3E);
}
.c-expand-positon-left{
margin-top:-8px;
margin-left:-18px;
}
.c-expand-positon-right{
height:100%;
width:100%;
justify-content: center;
}
.c-expand-positon-right span{
margin-top:-18px;
margin-right:-18px;
margin-left:100%;
}
.c-expand-positon-bottom{
height:100%;
width:100%;
margin-top:7px;
margin-left:-8px;
align-items: flex-end;
justify-content: center;
}
.c-expand-positon-top{
height:18px;
width:100%;
margin-top:-20px;
margin-left:-6px;
align-items: flex-end;
justify-content: center;
}
@keyframes ACTRGNodeInit {
from {
box-shadow: 0px 0px 15px #FD8B37;
}
15% {
box-shadow: 0px 0px 1px rgb(46, 78, 143);
}
30% {
box-shadow: 0px 0px 15px #FD8B37;
}
45% {
box-shadow: 0px 0px 1px rgb(46, 78, 143);
}
60% {
box-shadow: 0px 0px 15px #FD8B37;
}
to {
box-shadow: 0px 0px 1px rgb(46, 78, 143);
}
}
.rel-diy-node{
padding:0px;
}
</style>

View File

@@ -0,0 +1,249 @@
/* eslint-disable */
const SeeksStoreManager = {
createDefaultConfig(userGraphSetting) {
var _graphSetting = {
instanceId: 'SeeksGraph',
debug: true,
allowShowSettingPanel: false,
backgrounImage: '',
disableZoom: false,
disableDragNode: false,
moveToCenterWhenResize: true,
defaultFocusRootNode: true,
allowShowZoomMenu: true,
backgrounImageNoRepeat: false,
allowShowMiniToolBar: true,
allowShowMiniView: false,
allowShowMiniNameFilter: true,
fullscreen: false,
allowSwitchLineShape: false,
allowSwitchJunctionPoint: false,
isMoveByParentNode: false,
checkedNodeId: '',
checkedLineId: '',
layouts: [],
layoutLabel: '',
layoutName: 'tree',
layoutClassName: '',
layoutDirection: 'h',
defaultExpandHolderPosition: 'hide',
autoLayouting: false,
layouter: undefined,
allowAutoLayoutIfSupport: true,
isNeedShowAutoLayoutButton: false,
canvasZoom: 100,
defaultNodeColor: '#67C23A',
defaultNodeFontColor: '#ffffff',
defaultNodeBorderColor: '#90EE90',
defaultNodeBorderWidth: 5,
defaultLineColor: '#dddddd',
defaultLineWidth: 1,
defaultLineShape: 1,
defaultNodeShape: 0,
defaultNodeWidth: undefined,
defaultNodeHeight: undefined,
defaultShowLineLabel: true,
showSingleNode: true,
showNodeLabel: true,
showNodeShortLabel: true,
hideNodeContentByZoom: false,
defaultJunctionPoint: 'border',
viewSize: { width: 300, height: 300 },
viewELSize: { width: 1300, height: 800, left: 0, top: 100 },
viewNVInfo: { width: 1300, height: 800, x: 0, y: 100 },
canvasNVInfo: { width: 1300, height: 800, x: 0, y: 100 },
// NMViewCenter: { x: 0, y: 0 },
// NMCanvasCenter: { x: 0, y: 0 },
defaultLineMarker: {
markerWidth: 12,
markerHeight: 12,
refX: 6,
refY: 6,
color: undefined,
data: 'M2,2 L10,6 L2,10 L6,6 L2,2'
},
// defaultLineMarker: {
// markerWidth: 6,
// markerHeight: 6,
// refX: 3,
// refY: 3,
// color: undefined,
// data: 'M 0 0, V 6, L 4 3, Z'
// },
// defaultLineMarker: { // 另一种箭头样式
// markerWidth: 15,
// markerHeight: 15,
// refX: 50,
// refY: 7,
// color: '#128bed',
// data: 'M 14 7 L 1 .3 L 4 7 L .4 13 L 14 7, Z'
// },
canvasSize: {
width: 2000,
height: 2000
},
canvasOffset: {
x: 25,
y: 27,
zoom_buff_x: 0,
zoom_buff_y: 0
},
resetViewSize: (config) => {
// config.canvasOffset.x = parseInt(config.viewSize.width - config.canvasSize.width) / 2
// config.canvasOffset.y = parseInt(config.viewSize.height - config.canvasSize.height) / 2
config.canvasOffset.x = config.viewNVInfo.width / 2 - 100
config.canvasOffset.y = config.viewNVInfo.height / 2 - 100
}
}
var _debug = userGraphSetting.debug !== true ? false : true
if (_debug) console.log('user instance graphSetting:', userGraphSetting)
if (window) {
window.SeeksGraphDebug = _debug
}
if (userGraphSetting) {
Object.keys(userGraphSetting).forEach(key => {
var _thisUserValue = userGraphSetting[key]
if (typeof _thisUserValue === 'object') {
if (window.SeeksGraphDebug) console.log('user setting object:', key, _thisUserValue)
var _objectValue = _graphSetting[key]
if (_objectValue) {
if (_objectValue && !Array.isArray(_objectValue) && _thisUserValue) {
Object.keys(_objectValue).forEach(l2Key => {
if (window.SeeksGraphDebug) console.log(' user setting:', key + '.' + l2Key, _thisUserValue[l2Key])
_objectValue[l2Key] = _thisUserValue[l2Key]
})
} else if(Array.isArray(_objectValue)) {
if (window.SeeksGraphDebug) console.log(' user setting array:', key, 'size:', _thisUserValue.length)
var _new_arr = []
_thisUserValue.forEach(thisItem => {
if (window.SeeksGraphDebug) console.log(' user setting array:', key, 'push:', thisItem)
if (thisItem && typeof thisItem === 'object') {
_new_arr.push(JSON.parse(JSON.stringify(thisItem)))
} else {
_new_arr.push(thisItem)
}
})
_graphSetting[key] = _new_arr
// if (window.SeeksGraphDebug) console.log(' user setting array:', key, 'copy size:', _new_arr.length)
} else {
if (window.SeeksGraphDebug) console.log('user setting value:', key)
_graphSetting[key] = _thisUserValue
}
} else {
console.log('ignore option:', key)
}
} else {
if (window.SeeksGraphDebug) console.log('user setting:', key, _thisUserValue)
_graphSetting[key] = _thisUserValue
}
})
}
if (!_graphSetting.layouts || _graphSetting.layouts.length === 0) {
_graphSetting.layouts = [{
label: '中心',
layoutName: 'center',
layoutDirection: 'v',
defaultExpandHolderPosition: 'hide',
defaultNodeShape: 0,
defaultLineShape: 1,
defaultJunctionPoint: 'border'
}]
}
if (!Array.isArray(_graphSetting.layouts)) {
_graphSetting.layouts = [_graphSetting.layouts]
}
_graphSetting.layouts.forEach(thisLayout => {
SeeksStoreManager.appendDefaultOptions4Layout(thisLayout)
})
return _graphSetting
},
appendDefaultOptions4Layout(thisLayout) {
if (thisLayout.useLayoutStyleOptions === undefined) thisLayout.useLayoutStyleOptions = false
if (thisLayout.defaultNodeColor === undefined) thisLayout.defaultNodeColor = '#FFC5A6'
if (thisLayout.defaultNodeFontColor === undefined) thisLayout.defaultNodeFontColor = '#000000'
if (thisLayout.defaultNodeBorderColor === undefined) thisLayout.defaultNodeBorderColor = '#efefef'
if (thisLayout.defaultNodeBorderWidth === undefined) thisLayout.defaultNodeBorderWidth = 1
if (thisLayout.defaultLineColor === undefined) thisLayout.defaultLineColor = '#FD8B37'
if (thisLayout.defaultLineWidth === undefined) thisLayout.defaultLineWidth = 1
// if (thisLayout.defaultLineShape === undefined) thisLayout.defaultLineShape = 2
// if (thisLayout.defaultNodeShape === undefined) thisLayout.defaultNodeShape = 1
if (thisLayout.defaultNodeWidth === undefined) thisLayout.defaultNodeWidth = undefined
if (thisLayout.defaultNodeHeight === undefined) thisLayout.defaultNodeHeight = undefined
if (thisLayout.defaultShowLineLabel === undefined) thisLayout.defaultShowLineLabel = true
if (thisLayout.defaultExpandHolderPosition === undefined) thisLayout.defaultExpandHolderPosition = undefined
if (thisLayout.defaultJunctionPoint === undefined) thisLayout.defaultJunctionPoint = undefined
if (thisLayout.defaultLineMarker === undefined) {
thisLayout.defaultLineMarker = {
markerWidth: 12,
markerHeight: 12,
refX: 6,
refY: 6,
color: undefined,
data: 'M2,2 L10,6 L2,10 L6,6 L2,2'
}
}
if (thisLayout.layoutName === 'SeeksCenterLayouter' || thisLayout.layoutName === 'center') {
if (thisLayout.label === undefined) thisLayout.label = '中心'
if (thisLayout.layoutClassName === undefined) thisLayout.layoutClassName = 'seeks-layout-' + thisLayout.layoutName
if (thisLayout.defaultNodeShape === undefined) thisLayout.defaultNodeShape = 0
if (thisLayout.defaultLineShape === undefined) thisLayout.defaultLineShape = 1
if (thisLayout.defaultExpandHolderPosition === undefined) thisLayout.defaultExpandHolderPosition = 'hide'
if (thisLayout.defaultJunctionPoint === undefined) thisLayout.defaultJunctionPoint = 'border'
if (thisLayout.layoutDirection === undefined) thisLayout.layoutDirection = 'h'
if (thisLayout.centerOffset_x === undefined) thisLayout.centerOffset_x = 0
if (thisLayout.centerOffset_y === undefined) thisLayout.centerOffset_y = 0
if (thisLayout.levelDistance === undefined) thisLayout.levelDistance = ''
if (thisLayout.min_per_width === undefined) thisLayout.min_per_width = 30
if (thisLayout.max_per_width === undefined) thisLayout.max_per_width = 200
if (thisLayout.min_per_height === undefined) thisLayout.min_per_height = 100
if (thisLayout.max_per_height === undefined) thisLayout.max_per_height = 500
} else if (thisLayout.layoutName === 'SeeksBidirectionalTreeLayouter' || thisLayout.layoutName === 'tree') {
if (thisLayout.label === undefined) thisLayout.label = '树状'
if (thisLayout.layoutClassName === undefined) thisLayout.layoutClassName = 'seeks-layout-' + thisLayout.layoutName
if (thisLayout.defaultNodeShape === undefined) thisLayout.defaultNodeShape = 1
if (thisLayout.defaultLineShape === undefined) thisLayout.defaultLineShape = 2
if (thisLayout.defaultExpandHolderPosition === undefined) thisLayout.defaultExpandHolderPosition = 'hide'
if (thisLayout.defaultJunctionPoint === undefined) thisLayout.defaultJunctionPoint = 'ltrb'
if (thisLayout.layoutDirection === undefined) thisLayout.layoutDirection = 'h'
if (thisLayout.centerOffset_x === undefined) thisLayout.centerOffset_x = 0
if (thisLayout.centerOffset_y === undefined) thisLayout.centerOffset_y = 0
if (thisLayout.from === undefined) thisLayout.from = 'top'
if (thisLayout.levelDistance === undefined) thisLayout.levelDistance = ''
if (thisLayout.min_per_width === undefined) thisLayout.min_per_width = 30
if (thisLayout.max_per_width === undefined) thisLayout.max_per_width = 200
if (thisLayout.min_per_height === undefined) thisLayout.min_per_height = 100
if (thisLayout.max_per_height === undefined) thisLayout.max_per_height = 500
if (thisLayout.from === 'top' || thisLayout.from === 'bottom') thisLayout.layoutDirection = 'v'
}
},
createNewStore(userGraphSetting) {
if (window.SeeksGraphDebug) console.log('Create new GraphSetting:')
var _graphSetting = SeeksStoreManager.createDefaultConfig(userGraphSetting)
return new SeeksRGStore(_graphSetting)
}
}
function SeeksRGStore(_graphSetting) {
this.graphSetting = _graphSetting
this.resetViewSize = function() {
// state.graphSetting.canvasOffset.x = parseInt(state.graphSetting.viewSize.width - state.graphSetting.canvasSize.width) / 2
// state.graphSetting.canvasOffset.y = parseInt(state.graphSetting.viewSize.height - state.graphSetting.canvasSize.height) / 2
this.graphSetting.canvasOffset.x = 0 // state.graphSetting.viewNVInfo.width / 2 - 100
this.graphSetting.canvasOffset.y = 0 // state.graphSetting.viewNVInfo.height / 2 - 100
// console.log('resetViewSize:', state.graphSetting.viewSize.width, state.graphSetting.canvasSize.width, state.graphSetting.canvasZoom / 100, state.graphSetting.canvasSize.width * (state.graphSetting.canvasZoom / 100), state.graphSetting.canvasOffset.x)
}
this.getOptions = function() {
var _options = {}
var _ignore = [
'layouter', 'autoLayouting', 'canvasNVInfo', 'canvasOffset', 'canvasZoom', 'fullscreen', 'instanceId', 'layoutClassName', 'layoutDirection',
'layoutLabel', 'layoutName', 'resetViewSize', 'viewELSize', 'viewNVInfo', 'viewSize', 'canvasSize'
]
Object.keys(this.graphSetting).forEach(thisKey => {
if (_ignore.indexOf(thisKey) === -1) {
_options[thisKey] = this.graphSetting[thisKey]
}
})
return _options
}
console.log('relation-graph instance full option:', this.getOptions())
}
export default SeeksStoreManager

View File

@@ -0,0 +1,170 @@
/* eslint-disable */
var __tmp_basePosition = { x: 0, y: 0 }
var __tmp_positionModel = { x: 0, y: 0 }
var __ondraged
var __start_info = { x: 0, y: 0 }
var SeeksRGUtils = {
startDrag(e, positionModel, ondraged) {
__ondraged = ondraged
// console.log('startDrag:', __tmp_basePosition, e.clientX, e.clientY)
__tmp_positionModel = positionModel
__start_info.x = __tmp_positionModel.x
__start_info.y = __tmp_positionModel.y
__tmp_basePosition.x = parseInt(__tmp_positionModel.x) - e.clientX
__tmp_basePosition.y = parseInt(__tmp_positionModel.y) - e.clientY
document.body.addEventListener('mousemove', SeeksRGUtils.onNodeMove)
document.body.addEventListener('mouseup', SeeksRGUtils.onNodeDragend)
},
onNodeMove(e) {
// console.log('move', __tmp_basePosition, e.clientX, e.clientY)
__tmp_positionModel.x = e.clientX + __tmp_basePosition.x
__tmp_positionModel.y = e.clientY + __tmp_basePosition.y
},
onNodeDragend() {
// console.log('onNodeDragend', __tmp_positionModel.x - __start_info.x, __tmp_positionModel.y - __start_info.y)
document.body.removeEventListener('mousemove', SeeksRGUtils.onNodeMove)
document.body.removeEventListener('mouseup', SeeksRGUtils.onNodeDragend)
if (__ondraged) {
__ondraged(__tmp_positionModel.x - __start_info.x, __tmp_positionModel.y - __start_info.y)
}
},
transName4Circle(name) {
var _thisLevel = 0
var _thisLevelCharsArr = []
var result = []
for (var i = 0; i < name.length; i++) {
_thisLevelCharsArr.push(name[i])
if (_thisLevelCharsArr.length === circle_node_text_set[_thisLevel]) {
result.push(_thisLevelCharsArr.join(''))
_thisLevel++
_thisLevelCharsArr = []
}
}
if (_thisLevelCharsArr.length > 0) {
result.push(_thisLevelCharsArr.join(''))
}
// if (result.length < 3) {
// result.unshift('')
// if (result.length < 3) {
// result.unshift('')
// if (result.length < 3) {
// result.unshift('')
// }
// }
// }
return result.join('<br>')
},
getColorId(color) {
color = color.replace('#', '')
color = color.replace('(', '')
color = color.replace(')', '')
color = color.replace(/,/, '-')
return color
}
}
SeeksRGUtils.json2Node = function(originData) {
if (originData.id === undefined) throw Error('node must has option[id]:', originData)
originData.text = originData.text || originData.name || originData.id
var jsonData = {
id: originData.id,
text: originData.text !== undefined ? originData.text : '',
type: originData.type !== undefined ? originData.type : 'node',
isShow: originData.isShow !== undefined ? originData.isShow : true,
isHide: originData.isHide !== undefined ? originData.isHide : false,
expanded: originData.expanded !== undefined ? originData.expanded : true,
selected: originData.selected !== undefined ? originData.selected : false,
styleClass: originData.styleClass !== undefined ? originData.styleClass : '',
targetNodes: originData.targetNodes !== undefined ? originData.targetNodes : [],
targetFrom: originData.targetFrom !== undefined ? originData.targetFrom : [],
targetTo: originData.targetTo !== undefined ? originData.targetTo : [],
nodeShape: originData.nodeShape !== undefined ? originData.nodeShape : undefined,
borderWidth: originData.borderWidth !== undefined ? originData.borderWidth : undefined,
borderColor: originData.borderColor !== undefined ? originData.borderColor : undefined,
fontColor: originData.fontColor !== undefined ? originData.fontColor : undefined,
color: originData.color !== undefined ? originData.color : undefined,
opacity: originData.opacity !== undefined ? originData.opacity : 1,
fixed: originData.fixed !== undefined ? originData.fixed : false,
width: originData.width !== undefined ? originData.width : undefined,
height: originData.height !== undefined ? originData.height : undefined,
x: originData.x !== undefined ? originData.x : 0,
y: originData.y !== undefined ? originData.y : 0,
Fx: originData.Fx !== undefined ? originData.Fx : 0,
Fy: originData.Fy !== undefined ? originData.Fy : 0,
offset_x: originData.offset_x !== undefined ? originData.offset_x : 0,
offset_y: originData.offset_y !== undefined ? originData.offset_y : 0,
expandHolderPosition: originData.expandHolderPosition !== undefined ? originData.expandHolderPosition : undefined,
innerHTML: originData.innerHTML !== undefined ? originData.innerHTML : undefined,
html: originData.html !== undefined ? originData.html : undefined,
disableDefaultClickEffect: originData.disableDefaultClickEffect !== undefined ? originData.disableDefaultClickEffect : undefined,
disableDrag: originData.disableDrag !== undefined ? originData.disableDrag : false,
data: originData.data !== undefined ? originData.data : {}
}
if(jsonData.lot === undefined) jsonData.lot = { childs: [], parent: undefined, eached: false, strength: 0 }
if(jsonData.lot.childs === undefined) jsonData.lot.childs = []
if(jsonData.lot.parent === undefined) jsonData.lot.parent = undefined
if(jsonData.lot.eached === undefined) jsonData.lot.eached = false
if(jsonData.lot.strength === undefined) jsonData.lot.strength = 0
if(jsonData.el === undefined) jsonData.el = { offsetWidth: 50, offsetHeight: 50 }
if(jsonData.width !== undefined) jsonData.el.offsetWidth = jsonData.width
if(jsonData.height !== undefined) jsonData.el.offsetHeight = jsonData.height
return jsonData
}
SeeksRGUtils.json2Link = function(originData) {
if (originData.from === undefined) throw Error('error,link must has option[from]:', originData)
if (originData.to === undefined) throw Error('error,link must has option[to]:', originData)
if (typeof originData.from !== 'string') throw Error('error link from, must be string:', originData)
if (typeof originData.to !== 'string') throw Error('error link to, must be string:', originData)
var jsonData = {
text: originData.text !== undefined ? originData.text : '',
color: originData.color !== undefined ? originData.color : undefined,
fontColor: originData.fontColor !== undefined ? originData.fontColor : undefined,
lineWidth: originData.lineWidth !== undefined ? originData.lineWidth : undefined,
lineShape: originData.lineShape !== undefined ? originData.lineShape : undefined,
styleClass: originData.styleClass !== undefined ? originData.styleClass : undefined,
isHide: originData.isHide !== undefined ? originData.isHide : false,
arrow: originData.arrow !== undefined ? originData.arrow : undefined,
isHideArrow: originData.isHideArrow !== undefined ? originData.isHideArrow : undefined,
hidden: originData.hidden !== undefined ? originData.hidden : false,
lineDirection: originData.lineDirection !== undefined ? originData.lineDirection : undefined,
reverseText: originData.reverseText !== undefined ? originData.reverseText : undefined,
data: originData.data !== undefined ? originData.data : {},
}
return jsonData
}
SeeksRGUtils.getPosition = function(el) {
if (el.parentElement) {
return SeeksRGUtils.getPosition(el.parentElement) + el.offsetTop
}
return el.offsetTop
}
var _ignore_node_keys = [ 'Fx', 'Fy', 'appended', 'el', 'targetFrom', 'targetNodes', 'targetTo', 'type', 'lot', 'seeks_id' ]
SeeksRGUtils.transNodeToJson = function(node, nodes) {
if (!node) return
var _node_json = {}
Object.keys(node).forEach(thisKey => {
if (_ignore_node_keys.indexOf(thisKey) === -1) {
if (node[thisKey] !== undefined) {
_node_json[thisKey] = node[thisKey]
}
}
})
nodes.push(_node_json)
}
var _ignore_link_keys = [ 'arrow', 'id', 'reverseText', 'isReverse' ]
SeeksRGUtils.transLineToJson = function(line, links) {
if (!line) return
line.relations.forEach(thisRelation => {
var _link_json = {}
Object.keys(thisRelation).forEach(thisKey => {
if (_ignore_link_keys.indexOf(thisKey) === -1) {
if (thisRelation[thisKey] !== undefined) {
_link_json[thisKey] = thisRelation[thisKey]
}
}
})
links.push(_link_json)
})
}
var circle_node_text_set = [4, 5, 6, 4, 2, 100]
export default SeeksRGUtils

View File

@@ -0,0 +1,281 @@
import SeeksGraphMath from '../SeeksGraphMath'
function SeeksAutoLayouter(layoutSetting, graphSetting) {
this.graphSetting = graphSetting
this.config = layoutSetting || {}
this.rootNode = null
this.allNodes = []
this.__origin_nodes = []
this.refresh = function() {
this.placeNodes(this.__origin_nodes, this.rootNode)
}
this.placeNodes = function(allNodes, rootNode) {
if (!rootNode) {
console.log('root is null:', rootNode)
return
} else {
if (window.SeeksGraphDebug) console.log('layout by root:', rootNode)
}
this.__origin_nodes = allNodes
this.rootNode = rootNode
allNodes.forEach(thisNode => {
// thisNode.lot = { eached: false }
thisNode.lot.eached = false
thisNode.lot.notLeafNode = false
thisNode.lot.childs = []
// thisNode.lot.parent = undefined
thisNode.lot.index_of_parent = 0
thisNode.lot.strength = 0
thisNode.lot.prevNode = undefined
thisNode.lot.nextNode = undefined
thisNode.lot.placed = false
})
this.allNodes = []
var analyticResult = {
max_deep: 1,
max_length: 1
}
SeeksGraphMath.analysisNodes4Didirectional(this.allNodes, [this.rootNode], 0, analyticResult, 0)
// console.log('analysisNodes:', analyticResult)
// if (this.graphSetting.heightByContent) {
// console.log('根据内容调整高度')
// var __suitableHeight = analyticResult.max_deep * 2 * 300 + 500
// this.graphSetting.viewSize.height = __suitableHeight
// }
if (window.SeeksGraphDebug) console.log('调整画布大小')
// var __per_width = parseInt((__mapWidth - 10) / (analyticResult.max_deep + 2))
// var __per_height = parseInt((__mapHeight - 10) / (analyticResult.max_length + 1))
// console.log('per:', __per_width, __per_height)
// var __level2_current_length = 0
// this.allNodes.forEach(thisNode => {
// if (thisNode.lot.subling.level === 1 && thisNode.lot.childs_size > 0) {
// __level2_current_length += thisNode.lot.childs_size
// var __thisNodeLength = __level2_current_length + parseInt((thisNode.lot.childs_size / 2).toFixed(0))
// thisNode.lot.strength_plus = __level2_current_length
// console.log('level2 parents:', thisNode.name, thisNode.lot.childs_size, { strength_plus: thisNode.lot.strength_plus, __thisNodeLength, strength: thisNode.lot.childs_size, __level2_current_length })
// }
// })
// var __currentLevel = 0
var __mapWidth = this.graphSetting.viewSize.width
var __mapHeight = this.graphSetting.viewSize.height
rootNode.lot.x = parseInt((__mapWidth - rootNode.el.offsetWidth) / 2)
rootNode.lot.y = parseInt((__mapHeight - rootNode.el.offsetHeight) / 2)
// this.rootNode.lot.x = 0
// this.rootNode.lot.y = 0
// if (this.rootNode.lot.y > 400) {
// this.rootNode.lot.y = 400
// }
if (window.SeeksGraphDebug) console.log('[layout canvasOffset]', this.graphSetting.viewSize, this.graphSetting.canvasSize)
this.placeRelativePosition(this.rootNode)
this.allNodes.forEach(thisNode => {
if (thisNode.fixed === true) return
if (!SeeksGraphMath.isAllowShowNode(thisNode)) return
thisNode.x = thisNode.lot.x
thisNode.y = thisNode.lot.y
thisNode.lot.placed = true
})
// var __graphIndex = 1
// allNodes.forEach(thisNode => {
// // thisNode.lot = { eached: false }
// if (!SeeksGraphMath.isAllowShowNode(thisNode)) return
// if (thisNode.lot.placed === false) {
// this.allNodes = []
// var analyticResult = {
// max_deep: 1,
// max_length: 1
// }
// SeeksGraphMath.analysisNodes(this.allNodes, [thisNode], 0, analyticResult, { prettyLevelPosition: this.graphSetting.prettyLevelPosition })
// thisNode.lot.x = this.rootNode.lot.x
// thisNode.lot.y = this.rootNode.lot.y + (__graphIndex++ * 1200)
// this.graphSetting.canvasSize.height += 1200
// this.placeRelativePosition(thisNode)
// this.allNodes.forEach(thisNode => {
// thisNode.x = thisNode.lot.x - __offsetX
// thisNode.y = thisNode.lot.y - __offsetY
// thisNode.lot.placed = true
// })
// }
// })
if (window.SeeksGraphDebug) console.log('Start Auto Layout.....')
this.autoLayout(true)
// console.log('layout from root:', analyticResult.max_deep, analyticResult.max_length)
// rootNode.x = (this.graphSetting.canvasSize.width - this.graphSetting.nodeSize.width) / 2
// rootNode.y = (this.graphSetting.canvasSize.height - this.graphSetting.nodeSize.height) / 2
// rootNode.placed = true
// // rootNode.name = rootNode.x + ',' + rootNode.y
// var newLevelNodes = []
// newLevelNodes.push(rootNode)
// this.setPlace(newLevelNodes, 0, rootNode)
}
this.placeRelativePosition = function(rootNode) {
var __level1_r = 80
this.allNodes.forEach(thisNode => {
if (thisNode.lot.subling.level === 1) {
__level1_r = thisNode.lot.subling.all_size * 20 / Math.PI / 2
if (__level1_r < 80)__level1_r = 80
// if (__level1_r > 500)__level1_r = 500
const _point = SeeksGraphMath.getOvalPoint(rootNode.lot.x, rootNode.lot.y, thisNode.lot.subling.level * __level1_r, thisNode.lot.strength_plus - (thisNode.lot.strength / 2), thisNode.lot.subling.all_strength)
// const _point = SeeksGraphMath.getOvalPoint(this.rootNode.x, this.rootNode.y, thisNode.lot.subling.level * __level1_r, thisNode.lot.index_of_level, thisNode.lot.subling.all_size)
thisNode.lot.x = _point.x
thisNode.lot.y = _point.y
}
})
this.allNodes.forEach(thisNode => {
if (thisNode.lot.subling.level > 1) {
var __area_start = thisNode.lot.parent.lot.strength_plus - thisNode.lot.parent.lot.strength
var __area_end = thisNode.lot.parent.lot.strength_plus
var __buff = (__area_end - __area_start) / (thisNode.lot.parent.lot.childs_size + 1) * (thisNode.lot.index_of_parent + 1)
const _point = SeeksGraphMath.getOvalPoint(rootNode.lot.x, rootNode.lot.y, (thisNode.lot.subling.level - 1) * 80 + __level1_r, __area_start + __buff, thisNode.lot.parent.lot.subling.all_strength)
thisNode.lot.x = _point.x
thisNode.lot.y = _point.y
}
})
}
this.layoutTimes = 0
// var ___this = this
this.autoLayout = function(forceLayout) {
if (forceLayout) {
this.layoutTimes = 0
}
if (window.SeeksGraphDebug) console.log('this.layoutTimes:', this.layoutTimes)
if (this.layoutTimes > 300) {
this.graphSetting.autoLayouting = false
return
}
this.layoutTimes++
this.__origin_nodes.forEach(thisNode => {
thisNode.Fx = 0
thisNode.Fy = 0
})
var __by_node = true // parseInt(this.layoutTimes / 10) % 2 === 1
var __by_line = true // parseInt(this.layoutTimes / 10) % 2 === 0
if (__by_node) {
for (const i in this.__origin_nodes) {
var __node1 = this.__origin_nodes[i]
// if (__node1.text === '宣洁')console.log('宣洁:', __node1.x, __node1.y)
if (__node1.lot.placed === true) {
// var __thisNode = this.__origin_nodes[i]
// __thisNode.targetNodes.forEach(thisTN_level1 => {
// this.addGravityByNode(__thisNode, thisTN_level1)
// thisTN_level1.targetNodes.forEach(thisTN_level2 => {
// this.addGravityByNode(__thisNode, thisTN_level2)
// })
// })
// 循环点综合点与其他所有点点斥力及方向
for (var j in this.__origin_nodes) {
var __node2 = this.__origin_nodes[j]
if (__node2.lot.placed === true) {
// 循环点计算i点与j点点斥力及方向
if (i !== j) {
// if (this.allNodes[i].lot.level === this.allNodes[j].lot.level) {
this.addGravityByNode(__node1, __node2)
// }
}
}
}
}
}
}
if (__by_line) {
for (const i in this.__origin_nodes) {
// 循环线,设置每个点承受点力及力点方向
if (this.__origin_nodes[i].lot.parent) {
this.addElasticByLine(this.__origin_nodes[i].lot.parent, this.__origin_nodes[i])
// break
}
}
}
// if (this.layoutTimes % 5 === 0) { // 为提高布局效率计算五次后更新位置
for (const i in this.__origin_nodes) {
this.applyToNodePosition(this.__origin_nodes[i])
}
// }
window.setTimeout(function() { this.autoLayout() }.bind(this), 30)
}
this.stop = function() {
this.layoutTimes = 1000
}
this.addElasticByLine = function(node1, node2) {
var length = Math.sqrt(Math.pow((node1.y - node2.y), 2) + Math.pow((node1.x - node2.x), 2))
if (length > 1000) {
length = 1000
}
var Kf = length < 30 ? 0 : ((length - 30) * 0.05)
var Kf_1 = Kf
var Kf_2 = Kf
// var Kf_1 = Kf / node1.lot.childs.length
// var Kf_2 = Kf / node2.lot.childs.length
var _buff_x = (node1.x - node2.x) / length
var _buff_y = (node1.y - node2.y) / length
this.addFtoNode(node1, _buff_x * Kf_1 * -1, _buff_y * Kf_1 * -1, 1)
this.addFtoNode(node2, _buff_x * Kf_2, _buff_y * Kf_2, 1)
}
this.addGravityByNode = function(node1, node2) {
var length = Math.sqrt(Math.pow((node1.y - node2.y), 2) + Math.pow((node1.x - node2.x), 2))
var zero_length = 300
var Kf = length > zero_length ? 0 : ((zero_length - length) * 0.03)
if (zero_length < 30) {
Kf = Kf * 100
}
// if (length < 100)Kf = Kf * 2
var _buff_x = (node1.x - node2.x) / length
var _buff_y = (node1.y - node2.y) / length
// if (_buff_x < 30)_buff_x = 1
// if (_buff_y < 30)_buff_y = 1
// console.log({ Kf, _buff_x, _buff_y, zero_length })
this.addFtoNode(node1, _buff_x * Kf, _buff_y * Kf, 0)
this.addFtoNode(node2, _buff_x * Kf * -1, _buff_y * Kf * -1, 0)
}
this.getNodeFWeight = function(node) {
var level = node.lot.level
if (level > 7)level = 7
if (level < 0)level = 0
return (8 - level) / 8
}
this.addFtoNode = function(node, x, y) {
// console.log('Add F:', node.text, type, parseInt(x), parseInt(y))
if (isNaN(x) || isNaN(y)) {
return
}
x = x / node.lot.strength
y = y / node.lot.strength
if (x > 50)x = 50
if (y > 50)y = 50
if (x < -50)x = -50
if (y < -50)y = -50
// if (isNaN(node.Fx)) {
// if (node.text === '宣洁')console.log('宣洁!!!NaN B buff x:', x, node.lot.strength, node)
// }
node.Fx += x
node.Fy += y
// if (isNaN(node.Fx)) {
// if (node.text === '宣洁')console.log('宣洁!!!NaN A buff x:', x, node.lot.strength, node)
// }
}
this.applyToNodePosition = function(node) {
// if (!node.lot.childs || node.lot.childs.length === 0) {
// return
// }
// if (node.lot.level === 0) {
// return
// }
// console.log('F add:', node.name, node.Fx, node.Fy)
const __buff_x = parseInt(node.Fx)
const __buff_y = parseInt(node.Fy)
// console.log('F add:2:', node.name, __buff_x, __buff_y)
node.x = node.x + __buff_x
node.y = node.y + __buff_y
// if (isNaN(node.x)) {
// if (node.text === '宣洁')console.log('!!!NaN x:', node.text, __buff_x, node.Fx, node)
// }
// node.name = __buff_x + ',' + __buff_y
// if (node.id === '8') {
// console.log(node.id, __buff_x, __buff_y)
// // console.log(node.x, node.y)
// }
node.Fx = 0
node.Fy = 0
}
}
export default SeeksAutoLayouter

View File

@@ -0,0 +1,360 @@
import SeeksGraphMath from '../SeeksGraphMath'
function SeeksBidirectionalTreeLayouter(layoutSetting, graphSetting) {
this.graphSetting = graphSetting
this.config = layoutSetting || {}
console.log('new SeeksBidirectionalTreeLayouter:', this.config)
if (!this.config.from) this.config.from = 'left'
if (this.config.levelDistance && typeof this.config.levelDistance === 'string') {
this.config.levelDistanceArr = this.config.levelDistance.split(',').map(thisNum => parseInt(thisNum))
}
this.rootNode = null
this.allNodes = []
this.__origin_nodes = []
this.refresh = function() {
console.log('SeeksBidirectionalTreeLayouter:refresh:nodes:', this.__origin_nodes.length)
this.placeNodes(this.__origin_nodes, this.rootNode)
}
this.analysisNodes4Didirectional = function(willLayoutNodes, thisLevelNodes, thisDeep, analyticResult, levelDirect) {
if (thisLevelNodes.length > analyticResult.max_length) {
analyticResult.max_length = thisLevelNodes.length
}
if (thisDeep > analyticResult.max_deep) {
analyticResult.max_deep = thisDeep
}
var __thisLOT_subling = {
level: thisDeep,
all_size: thisLevelNodes.length,
all_strength: 0
}
var newLevelNodes = []
thisLevelNodes.forEach(thisNode => {
if (!thisNode.lot)thisNode.lot = {}
thisNode.lot.eached = true
thisNode.lot.subling = __thisLOT_subling
thisNode.lot.level = thisDeep
willLayoutNodes.push(thisNode)
})
var __thisLevel_index = 0
// var __prev_node
thisLevelNodes.forEach(thisNode => {
var __thisNode_child_size = 0
// console.log('Build node::', thisNode.text, thisNode.targetNodes.length)
if (levelDirect === -1) {
// console.log('Build node::from::', thisNode.text, thisNode.targetFrom.length)
let __thisTargetIndex = 0
thisNode.targetFrom.forEach((thisTarget) => {
if (!thisTarget.lot)thisTarget.lot = { eached: false }
if (!thisTarget.lot.eached) {
if (SeeksGraphMath.isAllowShowNode(thisTarget)) {
thisTarget.lot.eached = true
thisTarget.lot.parent = thisNode
thisTarget.lot.index_of_parent = __thisTargetIndex++
// thisTarget.lot.prevNode = __prev_node
// if (__prev_node)__prev_node.lot.nextNode = thisTarget
// __prev_node = thisTarget
thisNode.lot.childs.push(thisTarget)
newLevelNodes.push(thisTarget)
__thisNode_child_size++
} else {
thisNode.lot.childs.push(thisTarget)
// console.log('hide node:', thisTarget.name, 'from:', thisNode.text)
}
}
})
} else {
// console.log('Build node::to::', thisNode.text, thisNode.targetTo.length)
let __thisTargetIndex = 0
thisNode.targetTo.forEach((thisTarget) => {
if (!thisTarget.lot)thisTarget.lot = { eached: false }
if (!thisTarget.lot.eached) {
if (SeeksGraphMath.isAllowShowNode(thisTarget)) {
thisTarget.lot.eached = true
thisTarget.lot.parent = thisNode
thisTarget.lot.index_of_parent = __thisTargetIndex++
// thisTarget.lot.prevNode = __prev_node
// if (__prev_node)__prev_node.lot.nextNode = thisTarget
// __prev_node = thisTarget
thisNode.lot.childs.push(thisTarget)
newLevelNodes.push(thisTarget)
__thisNode_child_size++
} else {
thisNode.lot.childs.push(thisTarget)
// console.log('hide node:', thisTarget.name, 'from:', thisNode.text)
}
}
})
}
thisNode.lot.strength = __thisNode_child_size > 0 ? __thisNode_child_size : 1
__thisLOT_subling.all_strength += thisNode.lot.strength
thisNode.lot.strength_plus = __thisLOT_subling.all_strength
thisNode.lot.index_of_level = __thisLevel_index
thisNode.lot.childs_size = __thisNode_child_size
__thisLevel_index++
})
if (__thisLOT_subling.all_strength > analyticResult.max_strength) {
analyticResult.max_strength = __thisLOT_subling.all_strength
}
// console.log(thisDeep, 'next level nodes:', newLevelNodes.length)
if (newLevelNodes.length > 0) {
// console.log('thisLevelNodes.length:', thisLevelNodes, thisLevelNodes.length)
this.analysisNodes4Didirectional(willLayoutNodes, newLevelNodes, thisDeep + levelDirect, analyticResult, levelDirect)
} else {
willLayoutNodes.forEach(thisNode => {
if (thisNode.lot.childs_size > 0) {
thisNode.lot.strengthWithChilds = 0
}
})
willLayoutNodes.forEach(thisNode => {
if (thisNode.lot.childs_size === 0) {
thisNode.lot.strengthWithChilds = 1
SeeksGraphMath.conductStrengthToParents(thisNode)
}
})
SeeksGraphMath.analysisDataTree([willLayoutNodes[0]], 0, levelDirect)
// willLayoutNodes.forEach(thisNode => {
// thisNode.text = thisNode.lot.strengthWithChilds_from + ':' + thisNode.lot.strengthWithChilds + '/' + thisNode.lot.strength
// })
}
}
this.placeNodes = function(allNodes, rootNode) {
console.log('SeeksBidirectionalTreeLayouter:placeNodes')
if (!rootNode) {
console.error('root is null')
return
} else {
console.log('layout by root:', rootNode)
}
this.__origin_nodes = allNodes
this.rootNode = rootNode
allNodes.forEach(thisNode => {
// thisNode.lot = { eached: false }
thisNode.lot.eached = false
thisNode.lot.notLeafNode = false
thisNode.lot.childs = []
// thisNode.lot.parent = undefined
thisNode.lot.index_of_parent = 0
thisNode.lot.strength = 0
thisNode.lot.strengthWithChilds_from = 0
thisNode.lot.strengthWithChilds = 0
thisNode.lot.prevNode = undefined
thisNode.lot.nextNode = undefined
thisNode.lot.placed = false
})
// this.rootNode.fixed = true
this.allNodes = []
var analyticResult = {
max_deep: 1,
max_length: 1,
max_strength: 1
}
this.analysisNodes4Didirectional(this.allNodes, [this.rootNode], 0, analyticResult, -1)
this.placeNodesPosition(this.rootNode, this.allNodes, analyticResult)
this.allNodes = []
analyticResult = {
max_deep: 1,
max_length: 1,
max_strength: 1
}
this.analysisNodes4Didirectional(this.allNodes, [this.rootNode], 0, analyticResult, 1)
this.placeNodesPosition(this.rootNode, this.allNodes, analyticResult)
// console.log('根据数据调整画板高度')
// if (this.config.from === 'left' || this.config.from === 'right') {
// let __suitableHeight = analyticResult.max_strength * 50 + 100
// if (__suitableHeight < this.graphSetting.viewSize.height + 300)__suitableHeight = this.graphSetting.viewSize.height + 300
// this.graphSetting.canvasSize.height = __suitableHeight
// let __suitableWidth = analyticResult.max_deep * 1000 + 600
// if (__suitableWidth < this.graphSetting.viewSize.width + 500)__suitableWidth = this.graphSetting.viewSize.width + 500
// this.graphSetting.canvasSize.width = __suitableWidth
// } else {
// let __suitableWidth = analyticResult.max_strength * 320 + 1000
// if (__suitableWidth < this.graphSetting.viewSize.width + 500)__suitableWidth = this.graphSetting.viewSize.width + 500
// this.graphSetting.canvasSize.width = __suitableWidth
// let __suitableHeight = analyticResult.max_deep * 400 + 200
// if (__suitableHeight < this.graphSetting.viewSize.height + 300)__suitableHeight = this.graphSetting.viewSize.height + 300
// this.graphSetting.canvasSize.height = __suitableHeight
// }
// if (this.graphSetting.heightByContent) {
// console.log('根据数据调整视窗高度')
// if (this.config.from === 'left' || this.config.from === 'right') {
// this.graphSetting.viewSize.height = this.graphSetting.canvasSize.height
// } else {
// this.graphSetting.viewSize.height = analyticResult.max_deep * 500 + 300
// }
// }
// this.graphSetting.canvasOffset.x = this.graphSetting.viewNVInfo.width / 2 - 100
// this.graphSetting.canvasOffset.y = this.graphSetting.viewNVInfo.height / 2 - 100
}
this.placeNodesPosition = function(rootNode, allNodes, analyticResult) {
var __mapWidth = this.graphSetting.viewSize.width
var __mapHeight = this.graphSetting.viewSize.height
// console.log('analysisNodes:', analyticResult, allNodes)
// this.graphSetting.canvasOffset.x = 0
// this.graphSetting.canvasOffset.y = 0
var __offsetX = rootNode.offset_x || 0
var __offsetY = rootNode.offset_y || 0
// console.log('#############Seeks graph viewSize:Tree layout:', this.graphSetting.viewSize.width, this.graphSetting.viewSize.height)
// console.log('[layout canvasOffset]', __mapHeight, this.graphSetting, this.graphSetting.canvasSize, this.config)
// console.log('[Layout:AnalyticResult]', analyticResult)
if (rootNode.fixed !== true) {
var _center_offset_x = parseInt(this.config.centerOffset_x) || 0
var _center_offset_y = parseInt(this.config.centerOffset_y) || 0
if (this.config.from === 'top') {
rootNode.lot.x = (__mapWidth - rootNode.el.offsetWidth) / 2 + _center_offset_x
rootNode.lot.y = parseInt(__mapHeight * 0.3 - rootNode.el.offsetHeight) + _center_offset_y
} else if (this.config.from === 'bottom') {
rootNode.lot.x = (__mapWidth - rootNode.el.offsetWidth) / 2 + _center_offset_x
rootNode.lot.y = parseInt(__mapHeight * 0.7 - rootNode.el.offsetHeight) + _center_offset_y
} else if (this.config.from === 'right') {
rootNode.lot.x = parseInt(__mapWidth * 0.7 - rootNode.el.offsetWidth) / 2 + _center_offset_x
rootNode.lot.y = parseInt(__mapHeight / 2 - rootNode.el.offsetHeight / 2) + _center_offset_y
} else {
rootNode.lot.x = parseInt(__mapWidth * 0.3 - rootNode.el.offsetWidth) / 2 + _center_offset_x
rootNode.lot.y = parseInt(__mapHeight / 2 - rootNode.el.offsetHeight / 2) + _center_offset_y
}
console.log('设置根节点位置:', rootNode.text, rootNode.x, rootNode.y, this.graphSetting.canvasSize.width, this.graphSetting.canvasSize.height, this.graphSetting.canvasOffset.x, this.graphSetting.canvasOffset.y)
rootNode.x = rootNode.lot.x + __offsetX
rootNode.y = rootNode.lot.y + __offsetY
} else {
console.log('固定位置的rootNode:', rootNode.text, rootNode.x, rootNode.y)
if (rootNode.origin_x === undefined) {
rootNode.origin_x = rootNode.x
rootNode.origin_y = rootNode.y
}
rootNode.lot.x = rootNode.origin_x
rootNode.lot.y = rootNode.origin_y
rootNode.x = rootNode.lot.x + __offsetX
rootNode.y = rootNode.lot.y + __offsetY
console.log('固定位置的rootNode:', rootNode.text, rootNode.x, rootNode.y)
}
rootNode.lot.placed = true
var dynamicSizeConfig = {
__mapWidth,
__mapHeight
}
this.placeRelativePosition(rootNode, analyticResult, dynamicSizeConfig)
allNodes.forEach(thisNode => {
// if (rootNode === thisNode) {
// var _root_offset_x = this.config.root_offset_x || 0
// thisNode.x = thisNode.x + _root_offset_x
// return
// }
if (thisNode.fixed === true) {
thisNode.lot.placed = true
return
}
if (!SeeksGraphMath.isAllowShowNode(thisNode)) return
// console.log(thisNode.text, thisNode.offset_x, thisNode.offset_y)
var __offsetX = thisNode.offset_x || 0
var __offsetY = thisNode.offset_y || 0
thisNode.x = thisNode.offset_x + thisNode.lot.x + __offsetX
thisNode.y = thisNode.offset_y + thisNode.lot.y + __offsetY
thisNode.lot.placed = true
})
}
this.placeRelativePosition = function(rootNode, analyticResult, dynamicSizeConfig) {
if (this.config.from === 'left' || this.config.from === 'right') {
const __min_per_height = this.config.min_per_height || 80
const __max_per_height = this.config.max_per_height || 400
const __min_per_width = this.config.min_per_width || 430
const __max_per_width = this.config.max_per_width || 650
let __per_width = parseInt((dynamicSizeConfig.__mapWidth - 10) / (analyticResult.max_deep + 2))
if (__per_width < __min_per_width)__per_width = __min_per_width
if (__per_width > __max_per_width)__per_width = __max_per_width
let __per_height = parseInt(dynamicSizeConfig.__mapHeight / (analyticResult.max_strength + 1))
if (__per_height < __min_per_height)__per_height = __min_per_height
if (__per_height > __max_per_height)__per_height = __max_per_height
this.allNodes.forEach(thisNode => {
if (thisNode.fixed === true) return
if (thisNode.lot.placed === true) return
if (thisNode === rootNode) return
// console.log('Place node:', thisNode.text, thisNode)
// console.log('Place node lot:', thisNode.lot.subling.level, thisNode.lot.index_of_level, 'of', thisNode.lot.subling.all_size, thisNode.lot.subling.all_strength)
if (this.config.from === 'right') {
thisNode.lot.x = rootNode.lot.x - this.getLevelDistance(thisNode, thisNode.lot.subling.level, __per_width)
} else {
thisNode.lot.x = rootNode.lot.x + this.getLevelDistance(thisNode, thisNode.lot.subling.level, __per_width)
}
})
this.allNodes.forEach(thisNode => {
if (thisNode.fixed === true) return
if (thisNode.lot.level !== 0) {
thisNode.lot.y = rootNode.lot.y + __per_height * ((analyticResult.max_strength / -2) + thisNode.lot.strengthWithChilds_from + thisNode.lot.strengthWithChilds / 2)
}
})
// this.allNodes.forEach(thisNode => {
// if (thisNode.fixed === true) return
// if (thisNode.lot.level === 1) {
// thisNode.lot.y = __per_height * (thisNode.lot.strength_plus - thisNode.lot.strength + (thisNode.lot.strength - 1) / 2)
// }
// })
// this.allNodes.forEach(thisNode => {
// if (thisNode.fixed === true) return
// if (thisNode.lot.level > 1) {
// // thisNode.lot.y = __per_height * (thisNode.lot.strength_plus - thisNode.lot.strength + (thisNode.lot.strength - 1) / 2)
// thisNode.lot.y = __per_height * (thisNode.lot.parent.lot.strength_plus - thisNode.lot.parent.lot.strength + thisNode.lot.index_of_parent)
// // thisNode.text = thisNode.lot.parent.lot.strength_plus + '-' + thisNode.lot.parent.lot.strength + '+' + thisNode.lot.index_of_parent
// }
// })
} else {
const __min_per_height = this.config.min_per_height || 250
const __max_per_height = this.config.max_per_height || 400
const __min_per_width = this.config.min_per_width || 250
const __max_per_width = this.config.max_per_width || 500
var __per_width = parseInt((dynamicSizeConfig.__mapWidth - 10) / (analyticResult.max_strength + 2))
if (__per_width < __min_per_width)__per_width = __min_per_width
if (__per_width > __max_per_width)__per_width = __max_per_width
var __per_height = parseInt((dynamicSizeConfig.__mapHeight - 10) / (analyticResult.max_deep + 2))
if (__per_height < __min_per_height)__per_height = __min_per_height
if (__per_height > __max_per_height)__per_height = __max_per_height
this.allNodes.forEach(thisNode => {
if (thisNode.fixed === true) return
if (thisNode.lot.placed === true) return
if (thisNode === rootNode) return
// console.log('Place node:', thisNode.text, thisNode)
// console.log('Place node lot:', thisNode.lot.subling.level, thisNode.lot.index_of_level, 'of', thisNode.lot.subling.all_size, thisNode.lot.subling.all_strength)
if (this.config.from === 'bottom') {
thisNode.lot.y = rootNode.lot.y - this.getLevelDistance(thisNode, thisNode.lot.subling.level, __per_height)
} else {
// console.log('Place node xxxx:', rootNode.lot.y, thisNode.lot.subling.level, __per_height)
thisNode.lot.y = rootNode.lot.y + this.getLevelDistance(thisNode, thisNode.lot.subling.level, __per_height)
}
})
this.allNodes.forEach(thisNode => {
if (thisNode.fixed === true) return
if (thisNode.lot.level !== 0) {
// console.log('Place node xxxx:', thisNode.lot.strengthWithChilds_from, thisNode.lot.strengthWithChilds, __per_width)
thisNode.lot.x = -58 + rootNode.lot.x + __per_width * ((analyticResult.max_strength / -2) + thisNode.lot.strengthWithChilds_from + thisNode.lot.strengthWithChilds / 2)
// thisNode.lot.x = rootNode.lot.x
}
})
// this.allNodes.forEach(thisNode => {
// if (thisNode.fixed === true) return
// if (thisNode.lot.level === 1) {
// thisNode.lot.x = __per_width * (thisNode.lot.strength_plus - thisNode.lot.strength + thisNode.lot.strength / 2)
// }
// })
// this.allNodes.forEach(thisNode => {
// if (thisNode.fixed === true) return
// if (thisNode.lot.level > 1) {
// thisNode.lot.x = __per_width * (thisNode.lot.parent.lot.strength_plus - thisNode.lot.parent.lot.strength + thisNode.lot.index_of_parent)
// }
// })
}
}
this.getLevelDistance = function(node, level, perSize) {
if (this.config.levelDistanceArr && this.config.levelDistanceArr.length > 0) {
var _distance = 0
for (let i = 0; i < level; i++) {
var _thisLevelDistance = this.config.levelDistanceArr[i] || 100
_distance += _thisLevelDistance
}
return _distance
} else {
return level * perSize
}
}
}
export default SeeksBidirectionalTreeLayouter

View File

@@ -0,0 +1,290 @@
import SeeksGraphMath from '../SeeksGraphMath'
function SeeksCenterLayouter(layoutSetting, graphSetting) {
this.graphSetting = graphSetting
this.config = layoutSetting || {}
this.rootNode = null
this.allNodes = []
this.__origin_nodes = []
this.refresh = function() {
if (window.SeeksGraphDebug) console.log('SeeksCenterLayouter:refresh')
this.placeNodes(this.__origin_nodes, this.rootNode)
}
this.placeNodes = function(allNodes, rootNode) {
if (window.SeeksGraphDebug) console.log('SeeksCenterLayouter:placeNodes')
if (!rootNode) {
console.log('root is null:', rootNode)
return
} else {
if (window.SeeksGraphDebug) console.log('layout by root:', rootNode)
}
this.__origin_nodes = allNodes
this.rootNode = rootNode
allNodes.forEach(thisNode => {
// thisNode.lot = { eached: false }
thisNode.lot.eached = false
thisNode.lot.notLeafNode = false
thisNode.lot.childs = []
// thisNode.lot.parent = undefined
thisNode.lot.index_of_parent = 0
thisNode.lot.strength = 0
thisNode.lot.prevNode = undefined
thisNode.lot.nextNode = undefined
thisNode.lot.placed = false
})
this.allNodes = []
var analyticResult = {
max_deep: 1,
max_length: 1
}
SeeksGraphMath.analysisNodes4Didirectional(this.allNodes, [this.rootNode], 0, analyticResult, 0)
// console.log('analysisNodes:', analyticResult)
// if (this.graphSetting.heightByContent) {
// console.log('根据内容调整高度')
// var __suitableHeight = analyticResult.max_deep * 2 * 300 + 500
// this.graphSetting.viewSize.height = __suitableHeight
// }
if (window.SeeksGraphDebug) console.log('调整画布大小')
// var __per_width = parseInt((__mapWidth - 10) / (analyticResult.max_deep + 2))
// var __per_height = parseInt((__mapHeight - 10) / (analyticResult.max_length + 1))
// console.log('per:', __per_width, __per_height)
// var __level2_current_length = 0
// this.allNodes.forEach(thisNode => {
// if (thisNode.lot.subling.level === 1 && thisNode.lot.childs_size > 0) {
// __level2_current_length += thisNode.lot.childs_size
// var __thisNodeLength = __level2_current_length + parseInt((thisNode.lot.childs_size / 2).toFixed(0))
// thisNode.lot.strength_plus = __level2_current_length
// console.log('level2 parents:', thisNode.name, thisNode.lot.childs_size, { strength_plus: thisNode.lot.strength_plus, __thisNodeLength, strength: thisNode.lot.childs_size, __level2_current_length })
// }
// })
// var __currentLevel = 0
var __mapWidth = this.graphSetting.viewSize.width
var __mapHeight = this.graphSetting.viewSize.height
rootNode.lot.x = parseInt((__mapWidth - rootNode.el.offsetWidth) / 2)
rootNode.lot.y = parseInt((__mapHeight - rootNode.el.offsetHeight) / 2)
// this.rootNode.lot.x = 0
// this.rootNode.lot.y = 0
// if (this.rootNode.lot.y > 400) {
// this.rootNode.lot.y = 400
// }
// console.log('[layout canvasOffset]', this.graphSetting.viewSize, this.graphSetting.canvasSize)
this.placeRelativePosition(this.rootNode, analyticResult)
this.allNodes.forEach(thisNode => {
if (thisNode.fixed === true) return
if (!SeeksGraphMath.isAllowShowNode(thisNode)) return
var __offsetX = thisNode.offset_x || 0
var __offsetY = thisNode.offset_y || 0
thisNode.x = thisNode.lot.x + __offsetX
thisNode.y = thisNode.lot.y + __offsetY
thisNode.lot.placed = true
})
// var __graphIndex = 1
// allNodes.forEach(thisNode => {
// // thisNode.lot = { eached: false }
// if (!SeeksGraphMath.isAllowShowNode(thisNode)) return
// if (thisNode.lot.placed === false) {
// this.allNodes = []
// var analyticResult = {
// max_deep: 1,
// max_length: 1
// }
// SeeksGraphMath.analysisNodes(this.allNodes, [thisNode], 0, analyticResult, { prettyLevelPosition: this.graphSetting.prettyLevelPosition })
// thisNode.lot.x = this.rootNode.lot.x
// thisNode.lot.y = this.rootNode.lot.y + (__graphIndex++ * 1200)
// this.graphSetting.canvasSize.height += 1200
// this.placeRelativePosition(thisNode)
// this.allNodes.forEach(thisNode => {
// thisNode.x = thisNode.lot.x - __offsetX
// thisNode.y = thisNode.lot.y - __offsetY
// thisNode.lot.placed = true
// })
// }
// })
console.log('Start Auto Layout.....')
// this.autoLayout(true)
// console.log('layout from root:', analyticResult.max_deep, analyticResult.max_length)
// rootNode.x = (this.graphSetting.canvasSize.width - this.graphSetting.nodeSize.width) / 2
// rootNode.y = (this.graphSetting.canvasSize.height - this.graphSetting.nodeSize.height) / 2
// rootNode.placed = true
// // rootNode.name = rootNode.x + ',' + rootNode.y
// var newLevelNodes = []
// newLevelNodes.push(rootNode)
// this.setPlace(newLevelNodes, 0, rootNode)
}
this.placeRelativePosition = function(rootNode, analyticResult) {
var distance_coefficient = this.config.distance_coefficient === undefined ? 1 : this.config.distance_coefficient
var __leve1_min_r = parseInt(((this.graphSetting.viewSize.height + this.graphSetting.viewSize.width) / analyticResult.max_deep * 0.2)) * distance_coefficient
if (window.SeeksGraphDebug) console.log('analyticResult:', analyticResult, __leve1_min_r, this.config.distance_coefficient)
if (__leve1_min_r < 150 * distance_coefficient) __leve1_min_r = 150 * distance_coefficient
var __level1_r = 0
this.allNodes.forEach(thisNode => {
if (thisNode.lot.subling.level === 1) {
__level1_r = parseInt(thisNode.lot.subling.all_size * 50 / Math.PI / 2)
if (__level1_r < __leve1_min_r)__level1_r = __leve1_min_r
// if (__level1_r > 500)__level1_r = 500
const _point = SeeksGraphMath.getOvalPoint(rootNode.lot.x, rootNode.lot.y, thisNode.lot.subling.level * __level1_r, thisNode.lot.strength_plus - (thisNode.lot.strength / 2), thisNode.lot.subling.all_strength)
// const _point = SeeksGraphMath.getOvalPoint(this.rootNode.x, this.rootNode.y, thisNode.lot.subling.level * __level1_r, thisNode.lot.index_of_level, thisNode.lot.subling.all_size)
thisNode.lot.x = _point.x
thisNode.lot.y = _point.y
}
})
var __level_r = parseInt(300 * distance_coefficient)
this.allNodes.forEach(thisNode => {
if (thisNode.lot.subling.level > 1) {
var __area_start = thisNode.lot.parent.lot.strength_plus - thisNode.lot.parent.lot.strength
var __area_end = thisNode.lot.parent.lot.strength_plus
var __buff = (__area_end - __area_start) / (thisNode.lot.parent.lot.childs_size + 1) * (thisNode.lot.index_of_parent + 1)
const _point = SeeksGraphMath.getOvalPoint(rootNode.lot.x, rootNode.lot.y, (thisNode.lot.subling.level - 1) * __level_r + __level1_r, __area_start + __buff, thisNode.lot.parent.lot.subling.all_strength)
thisNode.lot.x = _point.x
thisNode.lot.y = _point.y
}
})
}
this.layoutTimes = 0
// var ___this = this
this.autoLayout = function(forceLayout) {
if (forceLayout) {
this.layoutTimes = 0
}
console.log('this.layoutTimes:', this.layoutTimes)
if (this.layoutTimes > 300) {
this.graphSetting.autoLayouting = false
return
}
this.layoutTimes++
this.__origin_nodes.forEach(thisNode => {
thisNode.Fx = 0
thisNode.Fy = 0
})
var __by_node = true // parseInt(this.layoutTimes / 10) % 2 === 1
var __by_line = true // parseInt(this.layoutTimes / 10) % 2 === 0
if (__by_node) {
for (const i in this.__origin_nodes) {
var __node1 = this.__origin_nodes[i]
// if (__node1.text === '宣洁')console.log('宣洁:', __node1.x, __node1.y)
if (__node1.lot.placed === true) {
// var __thisNode = this.__origin_nodes[i]
// __thisNode.targetNodes.forEach(thisTN_level1 => {
// this.addGravityByNode(__thisNode, thisTN_level1)
// thisTN_level1.targetNodes.forEach(thisTN_level2 => {
// this.addGravityByNode(__thisNode, thisTN_level2)
// })
// })
// 循环点综合点与其他所有点点斥力及方向
for (var j in this.__origin_nodes) {
var __node2 = this.__origin_nodes[j]
if (__node2.lot.placed === true) {
// 循环点计算i点与j点点斥力及方向
if (i !== j) {
// if (this.allNodes[i].lot.level === this.allNodes[j].lot.level) {
this.addGravityByNode(__node1, __node2)
// }
}
}
}
}
}
}
if (__by_line) {
for (const i in this.__origin_nodes) {
// 循环线,设置每个点承受点力及力点方向
if (this.__origin_nodes[i].lot.parent) {
this.addElasticByLine(this.__origin_nodes[i].lot.parent, this.__origin_nodes[i])
// break
}
}
}
// if (this.layoutTimes % 5 === 0) { // 为提高布局效率计算五次后更新位置
for (const i in this.__origin_nodes) {
this.applyToNodePosition(this.__origin_nodes[i])
}
// }
window.setTimeout(function() { this.autoLayout() }.bind(this), 30)
}
this.stop = function() {
this.layoutTimes = 1000
}
this.addElasticByLine = function(node1, node2) {
var length = Math.sqrt(Math.pow((node1.y - node2.y), 2) + Math.pow((node1.x - node2.x), 2))
if (length > 1000) {
length = 1000
}
var Kf = length < 30 ? 0 : ((length - 30) * 0.05)
var Kf_1 = Kf
var Kf_2 = Kf
// var Kf_1 = Kf / node1.lot.childs.length
// var Kf_2 = Kf / node2.lot.childs.length
var _buff_x = (node1.x - node2.x) / length
var _buff_y = (node1.y - node2.y) / length
this.addFtoNode(node1, _buff_x * Kf_1 * -1, _buff_y * Kf_1 * -1, 1)
this.addFtoNode(node2, _buff_x * Kf_2, _buff_y * Kf_2, 1)
}
this.addGravityByNode = function(node1, node2) {
var length = Math.sqrt(Math.pow((node1.y - node2.y), 2) + Math.pow((node1.x - node2.x), 2))
var zero_length = 300
var Kf = length > zero_length ? 0 : ((zero_length - length) * 0.03)
if (zero_length < 30) {
Kf = Kf * 100
}
// if (length < 100)Kf = Kf * 2
var _buff_x = (node1.x - node2.x) / length
var _buff_y = (node1.y - node2.y) / length
// if (_buff_x < 30)_buff_x = 1
// if (_buff_y < 30)_buff_y = 1
// console.log({ Kf, _buff_x, _buff_y, zero_length })
this.addFtoNode(node1, _buff_x * Kf, _buff_y * Kf, 0)
this.addFtoNode(node2, _buff_x * Kf * -1, _buff_y * Kf * -1, 0)
}
this.getNodeFWeight = function(node) {
var level = node.lot.level
if (level > 7)level = 7
if (level < 0)level = 0
return (8 - level) / 8
}
this.addFtoNode = function(node, x, y) {
// console.log('Add F:', node.text, type, parseInt(x), parseInt(y))
if (isNaN(x) || isNaN(y)) {
return
}
x = x / node.lot.strength
y = y / node.lot.strength
if (x > 50)x = 50
if (y > 50)y = 50
if (x < -50)x = -50
if (y < -50)y = -50
// if (isNaN(node.Fx)) {
// if (node.text === '宣洁')console.log('宣洁!!!NaN B buff x:', x, node.lot.strength, node)
// }
node.Fx += x
node.Fy += y
// if (isNaN(node.Fx)) {
// if (node.text === '宣洁')console.log('宣洁!!!NaN A buff x:', x, node.lot.strength, node)
// }
}
this.applyToNodePosition = function(node) {
// if (!node.lot.childs || node.lot.childs.length === 0) {
// return
// }
// if (node.lot.level === 0) {
// return
// }
// console.log('F add:', node.name, node.Fx, node.Fy)
const __buff_x = parseInt(node.Fx)
const __buff_y = parseInt(node.Fy)
// console.log('F add:2:', node.name, __buff_x, __buff_y)
node.x = node.x + __buff_x
node.y = node.y + __buff_y
// if (isNaN(node.x)) {
// if (node.text === '宣洁')console.log('!!!NaN x:', node.text, __buff_x, node.Fx, node)
// }
// node.name = __buff_x + ',' + __buff_y
// if (node.id === '8') {
// console.log(node.id, __buff_x, __buff_y)
// // console.log(node.x, node.y)
// }
node.Fx = 0
node.Fy = 0
}
}
export default SeeksCenterLayouter

View File

@@ -0,0 +1,307 @@
import SeeksGraphMath from '../SeeksGraphMath'
function SeeksCenterLayouter(layoutSetting, setting) {
this.setting = setting
this.config = layoutSetting || {}
this.__max_deep = 1
this.__max_length = 1
this.checkMaxDeepAndLength = function(thisLevelNodes, thisDeep) {
if (thisLevelNodes.length > this.__max_length) {
this.__max_length = thisLevelNodes.length
}
if (thisDeep > this.__max_deep) {
this.__max_deep = thisDeep
}
var __thisLOT_subling = {
level: thisDeep,
all_size: thisLevelNodes.length,
all_strength: 0
}
var newLevelNodes = []
thisLevelNodes.forEach(thisNode => {
if (!thisNode.lot)thisNode.lot = {}
thisNode.lot.eached = true
thisNode.lot.subling = __thisLOT_subling
this.allNodes.push(thisNode)
})
var __thisLevel_index = 0
var __prev_node
thisLevelNodes.forEach(thisNode => {
// console.log('Build node::', thisNode.name, thisNode.targetNodes.length)
var __thisNode_child_size = 0
if (thisNode.targetNodes) {
thisNode.targetNodes.forEach(thisTarget => {
console.log('child node::', thisTarget.type, thisTarget.lot.eached)
if (!thisTarget.lot)thisTarget.lot = { eached: false }
if (thisTarget.type === 'node' && thisTarget.targetNodes.length <= 1) {
if (!thisTarget.lot.eached) {
thisTarget.lot.parent = thisNode
thisTarget.lot.index_of_p_childs = __thisNode_child_size
thisTarget.lot.prevNode = __prev_node
if (__prev_node)__prev_node.lot.nextNode = thisTarget
__prev_node = thisTarget
thisNode.lot.childs.push(thisTarget)
thisNode.lot.eached = true
newLevelNodes.push(thisTarget)
__thisNode_child_size++
}
} else {
thisTarget.lot.notLeafNode = true
}
})
thisNode.targetNodes.forEach(thisTarget => {
if (thisTarget.lot.notLeafNode) {
if (!thisTarget.lot)thisTarget.lot = { eached: false }
if (!thisTarget.lot.eached) {
thisTarget.lot.parent = thisNode
thisTarget.lot.index_of_p_childs = __thisNode_child_size
thisTarget.lot.prevNode = __prev_node
if (__prev_node)__prev_node.lot.nextNode = thisTarget
__prev_node = thisTarget
thisNode.lot.childs.push(thisTarget)
thisNode.lot.eached = true
newLevelNodes.push(thisTarget)
__thisNode_child_size++
}
}
})
}
thisNode.lot.strength = __thisNode_child_size > 0 ? __thisNode_child_size : 1
__thisLOT_subling.all_strength += thisNode.lot.strength
thisNode.lot.strength_plus = __thisLOT_subling.all_strength
thisNode.lot.level_index = __thisLevel_index
thisNode.lot.childs_size = __thisNode_child_size
__thisLevel_index++
})
// console.log('next level nodes:', newLevelNodes.length)
if (newLevelNodes.length > 0) {
// console.log('thisLevelNodes.length:', thisLevelNodes, thisLevelNodes.length)
this.checkMaxDeepAndLength(newLevelNodes, thisDeep + 1)
}
}
this.rootNode = null
this.allNodes = []
this.__origin_nodes = []
this.refresh = function() {
this.placeNodes(this.__origin_nodes, this.rootNode)
}
this.placeNodes = function(allNodes, rootNode) {
if (!rootNode) {
console.log('root is null:', rootNode)
return
} else {
console.log('layout by root:', rootNode)
}
this.__origin_nodes = allNodes
this.rootNode = rootNode
allNodes.forEach(thisNode => {
// thisNode.lot = { eached: false }
thisNode.lot.eached = false
thisNode.lot.notLeafNode = false
thisNode.lot.childs = []
thisNode.lot.parent = undefined
thisNode.lot.index_of_p_childs = 0
thisNode.lot.strength = 0
thisNode.lot.prevNode = undefined
thisNode.lot.nextNode = undefined
})
this.allNodes = []
console.log('max before:', this.__max_deep, this.__max_length)
this.checkMaxDeepAndLength([this.rootNode], 0)
console.log('max after:', this.__max_deep, this.__max_length)
// if (this.setting.heightByContent) {
// console.log('根据内容调整高度')
// var __suitableHeight = this.__max_length * 40 + 100
// if (__suitableHeight > this.setting.viewSize.height) {
// this.setting.viewSize.height = __suitableHeight
// }
// if (setting.viewSize.height > this.setting.canvasSize.height / 2) {
// this.setting.canvasSize.height = this.setting.viewSize.height * 2
// }
// }
this.setting.canvasSize.width = 4000
this.setting.canvasSize.height = 4000
if (this.setting.heightByContent) {
console.log('根据数据调整视窗高度')
this.setting.viewSize.height = 1600
}
this.setting.resetViewSize(this.setting)
var __mapWidth = this.setting.viewSize.width
var __mapHeight = this.setting.viewSize.height
var __offsetX = this.setting.canvasOffset.x
var __offsetY = this.setting.canvasOffset.y
// var __per_width = parseInt((__mapWidth - 10) / (this.__max_deep + 2))
// var __per_height = parseInt((__mapHeight - 10) / (this.__max_length + 1))
// console.log('per:', __per_width, __per_height)
// var __level2_current_length = 0
// this.allNodes.forEach(thisNode => {
// if (thisNode.lot.subling.level === 1 && thisNode.lot.childs_size > 0) {
// __level2_current_length += thisNode.lot.childs_size
// var __thisNodeLength = __level2_current_length + parseInt((thisNode.lot.childs_size / 2).toFixed(0))
// thisNode.lot.strength_plus = __level2_current_length
// console.log('level2 parents:', thisNode.name, thisNode.lot.childs_size, { strength_plus: thisNode.lot.strength_plus, __thisNodeLength, strength: thisNode.lot.childs_size, __level2_current_length })
// }
// })
// var __currentLevel = 0
var __center = {
x: (__mapWidth) / 2 - __offsetX,
y: (__mapHeight) / 2 - __offsetY
}
if (__center.y > 800 - __offsetY) {
__center.y = 800 - __offsetY
}
var __all_size = this.allNodes.length
var __circle_r = __all_size * 90 / Math.PI / 2
if (__circle_r < 200)__circle_r = 200
if (__circle_r > 800)__circle_r = 800
this.allNodes.forEach((thisNode, _index) => {
const _point = SeeksGraphMath.getOvalPoint(__center.x, __center.y, __circle_r, _index, __all_size)
thisNode.x = _point.x
thisNode.y = _point.y
// console.log('Place node:', thisNode.name, thisNode.x, thisNode.y)
// thisNode.name = (thisNode.lp_level_index + 1) + '/' + subling_size
})
// this.allNodes.forEach(thisNode => {
// if (thisNode.lot.subling.level === 1 && thisNode.lot.childs_size > 0) {
// var _c_first = thisNode.lot.childs[0]
// var _c_last = thisNode.lot.childs[thisNode.lot.childs.length - 1]
// var _new_y = parseInt((_c_first.y + _c_last.y) / 2)
// thisNode.y = _new_y
// }
// })
// this.adjustLevel2Y(__mapHeight)
console.log('Start Auto Layout.....')
// this.autoLayout(true)
// console.log('layout from root:', this.__max_deep, this.__max_length)
// rootNode.x = (this.setting.canvasSize.width - this.setting.nodeSize.width) / 2
// rootNode.y = (this.setting.canvasSize.height - this.setting.nodeSize.height) / 2
// rootNode.placed = true
// // rootNode.name = rootNode.x + ',' + rootNode.y
// var newLevelNodes = []
// newLevelNodes.push(rootNode)
// this.setPlace(newLevelNodes, 0, rootNode)
}
this.adjustLevel2Y = function(__mapHeight) {
for (let i = 0; i < this.allNodes.length; i++) {
var thisNode = this.allNodes[i]
if (thisNode.lot.subling.level === 1 && thisNode.lot.childs_size === 0) {
var __per_height = parseInt(__mapHeight / (thisNode.lot.subling.all_size + 1))
if (__per_height > 70)__per_height = 70
console.log(__per_height, __mapHeight, thisNode.lot.subling.all_size, thisNode.lot.subling.all_strength, thisNode.lot.strength)
for (let j = 0; j < this.allNodes.length; j++) {
var thisLevel2Node = this.allNodes[j]
if (thisLevel2Node.lot.subling.level === 1 && thisLevel2Node !== thisNode) {
var __y_diff = Math.abs(thisNode.y - thisLevel2Node.y)
if (__y_diff < __per_height - 2) {
console.log('__y_diff', thisNode.name, thisLevel2Node.name, __y_diff)
// if (thisLevel2Node.lot.childs_size > 0 && i > 0) {
// thisLevel2Node.y = this.allNodes[i - 1].y + __per_height
// }
thisNode.y = thisLevel2Node.y + __per_height
i--
break
}
}
}
}
}
}
this.layoutTimes = 0
// var ___this = this
this.autoLayout = function(forceLayout) {
if (forceLayout) {
this.layoutTimes = 0
}
console.log('this.layoutTimes:', this.layoutTimes)
if (this.layoutTimes > 300) {
setting.autoLayouting = false
return
}
this.layoutTimes++
this.allNodes.forEach(thisNode => {
thisNode.Fx = 0
thisNode.Fy = 0
})
var __by_node = true // parseInt(this.layoutTimes / 10) % 2 === 1
var __by_line = true // parseInt(this.layoutTimes / 10) % 2 === 0
if (__by_node) {
for (const i in this.allNodes) {
// 循环点综合点与其他所有点点斥力及方向
for (var j in this.allNodes) {
// 循环点计算i点与j点点斥力及方向
if (i !== j) {
// if (this.allNodes[i].lot.subling.level === this.allNodes[j].lot.subling.level) {
this.addGravityByNode(this.allNodes[i], this.allNodes[j])
// }
}
}
}
}
if (__by_line) {
for (const i in this.allNodes) {
// 循环线,设置每个点承受点力及力点方向
if (this.allNodes[i].lot.parent) {
this.addElasticByLine(this.allNodes[i].lot.parent, this.allNodes[i])
// break
}
}
}
// if (this.layoutTimes % 1 === 0) { // 为提高布局效率计算五次后更新位置
for (const i in this.allNodes) {
this.applyToNodePosition(this.allNodes[i])
}
// }
window.setTimeout(function() { this.autoLayout() }.bind(this), 30)
}
this.stop = function() {
this.layoutTimes = 1000
}
this.addElasticByLine = function(n1, n2) {
var length = Math.sqrt(Math.pow((n1.y - n2.y), 2) + Math.pow((n1.x - n2.x), 2))
var Kf = length < 30 ? 0 : ((length - 30) * 0.01)
this.addFtoNode(n1, (n1.x - n2.x) * Kf * -1, (n1.y - n2.y) * Kf * -1)
this.addFtoNode(n2, (n2.x - n1.x) * Kf * -1, (n2.y - n1.y) * Kf * -1)
}
this.addGravityByNode = function(node1, node2) {
var length = Math.sqrt(Math.pow((node1.y - node2.y), 2) + Math.pow((node1.x - node2.x), 2))
var Kf = length > 300 ? 0 : ((300 - length) * 0.02)
// if (length < 100)Kf = Kf * 2
var _buff_x = node1.x - node2.x
var _buff_y = node1.y - node2.y
if (_buff_x === 0)_buff_x = 1
if (_buff_y === 0)_buff_y = 1
this.addFtoNode(node1, _buff_x * Kf, _buff_y * Kf)
this.addFtoNode(node2, _buff_x * -1 * Kf, _buff_y * -1 * Kf)
}
this.addFtoNode = function(node, x, y) {
node.Fx += x
node.Fy += y
}
this.applyToNodePosition = function(node) {
// if (!node.lot.childs || node.lot.childs.length === 0) {
// return
// }
if (this.rootNode === node) {
return
}
// console.log('F add:', node.name, node.Fx, node.Fy)
if (node.Fx > 1000)node.Fx = 3000
if (node.Fy > 1000)node.Fy = 3000
if (node.Fx < -1000)node.Fx = -3000
if (node.Fy < -1000)node.Fy = -3000
const __buff_x = parseInt(node.Fx * 0.02)
const __buff_y = parseInt(node.Fy * 0.02)
// console.log('F add:2:', node.name, __buff_x, __buff_y)
node.x = node.x + __buff_x
node.y = node.y + __buff_y
// node.name = __buff_x + ',' + __buff_y
// if (node.id === '8') {
// console.log(node.id, __buff_x, __buff_y)
// // console.log(node.x, node.y)
// }
node.Fx = 0
node.Fy = 0
}
}
export default SeeksCenterLayouter

View File

@@ -0,0 +1,43 @@
import SeeksGraphMath from '../SeeksGraphMath'
function SeeksFixedLayouter(layoutSetting, graphSetting) {
this.graphSetting = graphSetting
this.config = layoutSetting || {}
this.rootNode = null
this.allNodes = []
this.__origin_nodes = []
this.refresh = function() {
this.placeNodes(this.__origin_nodes, this.rootNode)
}
this.placeNodes = function(allNodes, rootNode) {
if (!rootNode) {
console.log('root is null:', rootNode)
return
} else {
if (window.SeeksGraphDebug) console.log('layout by root:', rootNode)
}
this.__origin_nodes = allNodes
this.rootNode = rootNode
allNodes.forEach(thisNode => {
// thisNode.lot = { eached: false }
thisNode.lot.eached = false
thisNode.lot.notLeafNode = false
thisNode.lot.childs = []
// thisNode.lot.parent = undefined
thisNode.lot.index_of_parent = 0
thisNode.lot.strength = 0
thisNode.lot.prevNode = undefined
thisNode.lot.nextNode = undefined
thisNode.lot.placed = false
})
this.allNodes = []
var analyticResult = {
max_deep: 1,
max_length: 1
}
SeeksGraphMath.analysisNodes4Didirectional(this.allNodes, [this.rootNode], 0, analyticResult, 0)
if (window.SeeksGraphDebug) console.log('[layout canvasOffset]', this.graphSetting.viewSize, this.graphSetting.canvasSize)
}
}
export default SeeksFixedLayouter

View File

@@ -0,0 +1,7 @@
/* eslint-disable */
import RelationGraph from './index.vue'
RelationGraph.install = function(Vue) {
Vue.component('relation-graph', RelationGraph);
Vue.component('seeks-relation-graph', RelationGraph);
};
export default RelationGraph

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,73 @@
import { axios } from '@/utils/request'
export function getFirstCIs(ciId) {
return axios({
url: '/v0.1/ci_relations/' + ciId + '/first_cis',
method: 'GET'
})
}
export function getSecondCIs(ciId) {
return axios({
url: '/v0.1/ci_relations/' + ciId + '/second_cis',
method: 'GET'
})
}
export function searchCIRelation(params) {
return axios({
url: `/v0.1/ci_relations/s?${params}`,
method: 'GET'
})
}
export function statisticsCIRelation(params) {
return axios({
url: '/v0.1/ci_relations/statistics',
method: 'GET',
params: params
})
}
// 批量添加子节点
export function batchUpdateCIRelationChildren(ciIds, parents) {
return axios({
url: '/v0.1/ci_relations/batch',
method: 'POST',
data: { ci_ids: ciIds, parents: parents }
})
}
// 批量添加父节点
export function batchUpdateCIRelationParents(ciIds, children) {
return axios({
url: '/v0.1/ci_relations/batch',
method: 'POST',
data: { ci_ids: ciIds, children: children }
})
}
// 批量删除
export function batchDeleteCIRelation(ciIds, parents) {
return axios({
url: '/v0.1/ci_relations/batch',
method: 'DELETE',
data: { ci_ids: ciIds, parents: parents }
})
}
// 单个添加
export function addCIRelationView(firstCiId, secondCiId) {
return axios({
url: `/v0.1/ci_relations/${firstCiId}/${secondCiId}`,
method: 'POST',
})
}
// 单个删除
export function deleteCIRelationView(firstCiId, secondCiId) {
return axios({
url: `/v0.1/ci_relations/${firstCiId}/${secondCiId}`,
method: 'DELETE',
})
}

View File

@@ -0,0 +1,207 @@
import { axios } from '@/utils/request'
/**
* 获取 所有的 ci_types
* @param parameter
* @returns {AxiosPromise}
*/
export function getCITypes(parameter) {
return axios({
url: '/v0.1/ci_types',
method: 'GET',
params: parameter
})
}
/**
* 获取 某个 ci_types
* @param CITypeName
* @param parameter
* @returns {AxiosPromise}
*/
export function getCIType(CITypeName, parameter) {
return axios({
url: `/v0.1/ci_types/${CITypeName}`,
method: 'GET',
params: parameter
})
}
/**
* 创建 ci_type
* @param data
* @returns {AxiosPromise}
*/
export function createCIType(data) {
return axios({
url: '/v0.1/ci_types',
method: 'POST',
data: data
})
}
/**
* 更新 ci_type
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function updateCIType(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}`,
method: 'PUT',
data: data
})
}
/**
* 删除 ci_type
* @param CITypeId
* @returns {AxiosPromise}
*/
export function deleteCIType(CITypeId) {
return axios({
url: `/v0.1/ci_types/${CITypeId}`,
method: 'DELETE'
})
}
/**
* 获取 某个 ci_type 的分组
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function getCITypeGroupById(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attribute_groups`,
method: 'GET',
params: data
})
}
/**
* 保存 某个 ci_type 的分组
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function createCITypeGroupById(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attribute_groups`,
method: 'POST',
data: data
})
}
/**
* 修改 某个 ci_type 的分组
* @param groupId
* @param data
* @returns {AxiosPromise}
*/
export function updateCITypeGroupById(groupId, data) {
return axios({
url: `/v0.1/ci_types/attribute_groups/${groupId}`,
method: 'PUT',
data: data
})
}
/**
* 删除 某个 ci_type 的分组
* @param groupId
* @param data
* @returns {AxiosPromise}
*/
export function deleteCITypeGroupById(groupId, data) {
return axios({
url: `/v0.1/ci_types/attribute_groups/${groupId}`,
method: 'delete',
data: data
})
}
export function getUniqueConstraintList(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint`,
method: 'get',
})
}
export function addUniqueConstraint(type_id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint`,
method: 'post',
data: data
})
}
export function updateUniqueConstraint(type_id, id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint/${id}`,
method: 'put',
data: data
})
}
export function deleteUniqueConstraint(type_id, id) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint/${id}`,
method: 'delete',
})
}
export function getTriggerList(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers`,
method: 'get',
})
}
export function addTrigger(type_id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers`,
method: 'post',
data: data
})
}
export function updateTrigger(type_id, id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers/${id}`,
method: 'put',
data: data
})
}
export function deleteTrigger(type_id, id) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers/${id}`,
method: 'delete',
})
}
// CMDB的模型和实例的授权接口
export function grantCiType(type_id, rid, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/roles/${rid}/grant`,
method: 'post',
data
})
}
// CMDB的模型和实例的删除授权接口
export function revokeCiType(type_id, rid, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/roles/${rid}/revoke`,
method: 'post',
data
})
}
// CMDB的模型和实例的过滤的权限
export function ciTypeFilterPermissions(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/filters/permissions`,
method: 'get',
})
}

View File

@@ -0,0 +1,155 @@
import { axios } from '@/utils/request'
/**
* 获取 ci_type 的属性
* @param CITypeName
* @param parameter
* @returns {AxiosPromise}
*/
export function getCITypeAttributesByName(CITypeName, parameter) {
return axios({
url: `/v0.1/ci_types/${CITypeName}/attributes`,
method: 'get',
params: parameter
})
}
/**
* 获取 ci_type 的属性
* @param CITypeId
* @param parameter
* @returns {AxiosPromise}
*/
export function getCITypeAttributesById(CITypeId, parameter) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attributes`,
method: 'get',
params: parameter
})
}
/**
* 更新属性
* @param attrId
* @param data
* @returns {AxiosPromise}
*/
export function updateAttributeById(attrId, data) {
return axios({
url: `/v0.1/attributes/${attrId}`,
method: 'put',
data: data
})
}
/**
* 添加属性
* @param data
* @returns {AxiosPromise}
*/
export function createAttribute(data) {
return axios({
url: `/v0.1/attributes`,
method: 'post',
data: data
})
}
/**
* 搜索属性/ 获取所有的属性
* @param data
* @returns {AxiosPromise}
*/
export function searchAttributes(params) {
return axios({
url: `/v0.1/attributes/s`,
method: 'get',
params: params
})
}
export function getCITypeAttributesByTypeIds(params) {
return axios({
url: `/v0.1/ci_types/attributes`,
method: 'get',
params: params
})
}
/**
* 删除属性
* @param attrId
* @returns {AxiosPromise}
*/
export function deleteAttributesById(attrId) {
return axios({
url: `/v0.1/attributes/${attrId}`,
method: 'delete'
})
}
/**
* 绑定ci_type 属性
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function createCITypeAttributes(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attributes`,
method: 'post',
data: data
})
}
/**
* 更新ci_type 属性
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function updateCITypeAttributesById(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attributes`,
method: 'put',
data: data
})
}
/**
* 删除ci_type 属性
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function deleteCITypeAttributesById(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attributes`,
method: 'delete',
data: data
})
}
export function transferCITypeAttrIndex(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attributes/transfer`,
method: 'POST',
data: data
})
}
export function transferCITypeGroupIndex(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attribute_groups/transfer`,
method: 'POST',
data: data
})
}
export function canDefineComputed() {
return axios({
url: `/v0.1/ci_types/can_define_computed`,
method: 'HEAD',
})
}

View File

@@ -0,0 +1,63 @@
import { axios } from '@/utils/request'
export function getCITypeChildren(CITypeID, parameter) {
return axios({
url: '/v0.1/ci_type_relations/' + CITypeID + '/children',
method: 'get',
params: parameter
})
}
export function getCITypeParent(CITypeID) {
return axios({
url: '/v0.1/ci_type_relations/' + CITypeID + '/parents',
method: 'get'
})
}
export function getCITypeRelations() {
return axios({
url: '/v0.1/ci_type_relations',
method: 'GET'
})
}
export function getRelationTypes(CITypeID, parameter) {
return axios({
url: '/v0.1/relation_types',
method: 'get',
params: parameter
})
}
export function createRelation(parentId, childrenId, relationTypeId, constraint) {
return axios({
url: `/v0.1/ci_type_relations/${parentId}/${childrenId}`,
method: 'post',
data: { relation_type_id: relationTypeId, constraint }
})
}
export function deleteRelation(parentId, childrenId) {
return axios({
url: `/v0.1/ci_type_relations/${parentId}/${childrenId}`,
method: 'delete'
})
}
export function grantTypeRelation(first_type_id, second_type_id, rid, data) {
return axios({
url: `/v0.1/ci_type_relations/${first_type_id}/${second_type_id}/roles/${rid}/grant`,
method: 'post',
data
})
}
export function revokeTypeRelation(first_type_id, second_type_id, rid, data) {
return axios({
url: `/v0.1/ci_type_relations/${first_type_id}/${second_type_id}/roles/${rid}/revoke`,
method: 'post',
data
})
}

View File

@@ -0,0 +1,67 @@
import XLSX from 'xlsx'
import { axios } from '@/utils/request'
export function processFile(fileObj) {
return new Promise(function (resolve) {
const reader = new FileReader()
reader.readAsBinaryString(fileObj)
reader.onload = function (e) {
const data = e.target.result
const workbook = XLSX.read(data, { type: 'binary' })
const sheet = workbook.Sheets[workbook.SheetNames[0]]
const lt = XLSX.utils.sheet_to_json(sheet, { header: 1 })
resolve(lt)
}
})
}
export function uploadData(ciId, data) {
data.ci_type = ciId
data.exist_policy = 'replace'
return axios({
url: '/v0.1/ci',
method: 'POST',
data,
isShowMessage: false
})
}
export function writeCsv(columns) {
const { Parser } = require('json2csv')
const opts = { columns }
const p = new Parser(opts)
return p.parse([])
}
export function writeExcel(columns, name) {
const worksheet = XLSX.utils.aoa_to_sheet([columns])
const newWorkBoot = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(newWorkBoot, worksheet, name)
const s = XLSX.write(newWorkBoot, { type: 'array' })
console.log(s)
return s
}
// 判断一个数组元素是否都为空的
export function any(ArrayList) {
let flag = false
for (let i = 0; i < ArrayList.length; i++) {
if (ArrayList[i]) {
flag = true
return flag
}
}
return false
}
// 去除一个二维数组 底下为空的部分
export function filterNull(twoDimArray) {
console.log(twoDimArray)
const newArray = []
for (let i = 0; i < twoDimArray.length; i++) {
if (any(twoDimArray[i])) {
newArray.push(twoDimArray[i])
}
}
return newArray
}

View File

@@ -0,0 +1,58 @@
import { axios } from '@/utils/request'
const urlPrefix = '/v0.1'
export function searchCI(params) {
return axios({
url: urlPrefix + `/ci/s`,
method: 'GET',
params: params
})
}
export function searchCI2(params) {
return axios({
url: urlPrefix + `/ci/s?${params}`,
method: 'GET'
})
}
export function addCI(params) {
return axios({
url: urlPrefix + '/ci',
method: 'POST',
data: params
})
}
export function updateCI(id, params) {
return axios({
url: urlPrefix + `/ci/${id}`,
method: 'PUT',
data: params
})
}
export function deleteCI(ciId) {
return axios({
url: urlPrefix + `/ci/${ciId}`,
method: 'DELETE'
})
}
// 获取单个ci实例
export function getCIById(ciId) {
return axios({
// url: urlPrefix + `/ci/${ciId}`,
url: urlPrefix + `/ci/s?q=_id:${ciId}`,
method: 'GET'
})
}
// 获取自动发现占比
export function getCIAdcStatistics() {
return axios({
url: urlPrefix + `/ci/adc/statistics`,
method: 'GET'
})
}

View File

@@ -0,0 +1,52 @@
import { axios } from '@/utils/request'
const urlPrefix = '/v0.1'
export function getCITypeGroups(params) {
return axios({
url: `${urlPrefix}/ci_types/groups`,
method: 'GET',
params: params
})
}
export function postCITypeGroup(data) {
return axios({
url: `${urlPrefix}/ci_types/groups`,
method: 'POST',
data: data
})
}
export function putCITypeGroupByGId(gid, data) {
return axios({
url: `${urlPrefix}/ci_types/groups/${gid}`,
method: 'PUT',
data: data
})
}
export function deleteCITypeGroup(gid, data) {
return axios({
url: `${urlPrefix}/ci_types/groups/${gid}`,
method: 'DELETE',
data: data
})
}
export function getCITypeGroupsConfig(params) {
return axios({
url: `${urlPrefix}/ci_types/groups/config`,
method: 'GET',
params: params
})
}
// 更新模型配置分组的排序
export const putCITypeGroups = (data) => {
return axios({
url: `${urlPrefix}/ci_types/groups/order`,
method: 'PUT',
data: data
})
}

View File

@@ -0,0 +1,39 @@
import { axios } from '@/utils/request'
export function getCustomDashboard() {
return axios({
url: '/v0.1/custom_dashboard',
method: 'get',
})
}
export function postCustomDashboard(data) {
return axios({
url: '/v0.1/custom_dashboard',
method: 'post',
data
})
}
export function putCustomDashboard(id, data) {
return axios({
url: `/v0.1/custom_dashboard/${id}`,
method: 'put',
data
})
}
export function deleteCustomDashboard(id) {
return axios({
url: `/v0.1/custom_dashboard/${id}`,
method: 'delete',
})
}
export function batchUpdateCustomDashboard(data) {
return axios({
url: `/v0.1/custom_dashboard/batch`,
method: 'put',
data
})
}

View File

@@ -0,0 +1,120 @@
import { axios } from '@/utils/request'
export function getDiscovery() {
return axios({
url: `/v0.1/adr`,
method: 'GET'
})
}
export function postDiscovery(data) {
return axios({
url: `/v0.1/adr`,
method: 'POST',
data
})
}
export function putDiscovery(id, data) {
return axios({
url: `/v0.1/adr/${id}`,
method: 'PUT',
data
})
}
export function deleteDiscovery(id) {
return axios({
url: `/v0.1/adr/${id}`,
method: 'DELETE'
})
}
export function getHttpCategories(name) {
return axios({
url: `/v0.1/adr/http/${name}/categories`,
method: 'GET',
})
}
export function getHttpAttributes(name, params) {
return axios({
url: `/v0.1/adr/http/${name}/attributes`,
method: 'GET',
params
})
}
export function getSnmpAttributes(name) {
return axios({
url: `/v0.1/adr/snmp/${name}/attributes`,
method: 'GET',
})
}
export function getCITypeDiscovery(type_id) {
return axios({
url: `/v0.1/adt/ci_types/${type_id}`,
method: 'GET',
})
}
export function postCITypeDiscovery(type_id, data) {
return axios({
url: `/v0.1/adt/ci_types/${type_id}`,
method: 'POST',
data
})
}
export function putCITypeDiscovery(adt_id, data) {
return axios({
url: `/v0.1/adt/${adt_id}`,
method: 'PUT',
data
})
}
export function deleteCITypeDiscovery(id) {
return axios({
url: `/v0.1/adt/${id}`,
method: 'DELETE'
})
}
export function getADCCiTypes(params) {
return axios({
url: `/v0.1/adc/ci_types`,
method: 'GET',
params
})
}
export function getADCCiTypesAttrs(type_id) {
return axios({
url: `/v0.1/adc/ci_types/${type_id}/attributes`,
method: 'GET'
})
}
export function updateADCAccept(adc_id) {
return axios({
url: `/v0.1/adc/${adc_id}/accept`,
method: 'PUT'
})
}
export function getAdc(params) {
return axios({
url: `v0.1/adc`,
method: 'GET',
params
})
}
export function deleteAdc(adc_id) {
return axios({
url: `v0.1/adc/${adc_id}`,
method: 'DELETE',
})
}

View File

@@ -0,0 +1,40 @@
import { axios } from '@/utils/request'
export function getCIHistory (ciId) {
return axios({
url: `/v0.1/history/ci/${ciId}`,
method: 'GET'
})
}
export function getCIHistoryTable (params) {
return axios({
url: `/v0.1/history/records/attribute`,
method: 'GET',
params: params
})
}
export function getRelationTable (params) {
return axios({
url: `/v0.1/history/records/relation`,
method: 'GET',
params: params
})
}
export function getCITypesTable (params) {
return axios({
url: `/v0.1/history/ci_types`,
method: 'GET',
params: params
})
}
export function getUsers (params) {
return axios({
url: `/v1/acl/users/employee`,
method: 'GET',
params: params
})
}

View File

@@ -0,0 +1,8 @@
import { axios } from '@/utils/request'
export function getWX() {
return axios({
url: '/v1/acl/users',
method: 'GET'
})
}

View File

@@ -0,0 +1,116 @@
import { axios } from '@/utils/request'
export function getPreference(instance = true, tree = null) {
return axios({
url: '/v0.1/preference/ci_types',
method: 'GET',
params: { instance: instance, tree: tree }
})
}
export function getPreference2(instance = true, tree = null) {
return axios({
url: '/v0.1/preference/ci_types2',
method: 'GET',
params: { instance: instance, tree: tree }
})
}
export function getSubscribeAttributes(ciTypeId) {
return axios({
url: `/v0.1/preference/ci_types/${ciTypeId}/attributes`,
method: 'GET'
})
}
export function getSubscribeTreeView() {
return axios({
url: '/v0.1/preference/tree/view',
method: 'GET'
})
}
export function subscribeCIType(ciTypeId, attrs) {
return axios({
url: `/v0.1/preference/ci_types/${ciTypeId}/attributes`,
method: 'POST',
data: {
attr: attrs
}
})
}
export function subscribeTreeView(ciTypeId, levels) {
return axios({
url: `/v0.1/preference/tree/view`,
method: 'POST',
data: { type_id: ciTypeId, levels: levels }
})
}
export function getRelationView() {
return axios({
url: `/v0.1/preference/relation/view`,
method: 'GET'
})
}
export function deleteRelationView(viewName) {
return axios({
url: `/v0.1/preference/relation/view`,
method: 'DELETE',
data: { name: viewName }
})
}
export function subscribeRelationView(payload) {
return axios({
url: `/v0.1/preference/relation/view`,
method: 'POST',
data: payload
})
}
// 用户保存条件过滤选项
export function getPreferenceSearch(payload) {
// 参数有prv_id: 关系视图的id ptv_id: 层级视图的id, type_id: 模型id
return axios({
url: `/v0.1/preference/search/option`,
method: 'GET',
params: payload
})
}
export function savePreferenceSearch(payload) {
// 参数包括GET的参数 必须参数nameoption option是个json
return axios({
url: `/v0.1/preference/search/option`,
method: 'POST',
data: payload
})
}
export function deletePreferenceSearch(id) {
return axios({
url: `/v0.1/preference/search/option/${id}`,
method: 'DELETE',
})
}
// 服务树授权
export function grantRelationView(rid, data) {
return axios({
url: `/v0.1/preference/relation/view/roles/${rid}/grant`,
method: 'POST',
data: data
})
}
// 服务树权限回收
export function revokeRelationView(rid, data) {
return axios({
url: `/v0.1/preference/relation/view/roles/${rid}/revoke`,
method: 'POST',
data: data
})
}

View File

@@ -0,0 +1,31 @@
import { axios } from '@/utils/request'
export function getRelationTypes () {
return axios({
url: '/v0.1/relation_types',
method: 'GET'
})
}
export function addRelationType (payload) {
return axios({
url: `/v0.1/relation_types`,
method: 'POST',
data: payload
})
}
export function updateRelationType (rtId, payload) {
return axios({
url: `/v0.1/relation_types/${rtId}`,
method: 'PUT',
data: payload
})
}
export function deleteRelationType (rtId) {
return axios({
url: `/v0.1/relation_types/${rtId}`,
method: 'DELETE'
})
}

View File

@@ -0,0 +1,8 @@
import { axios } from '@/utils/request'
export function getStatistics() {
return axios({
url: '/v0.1/statistics',
method: 'GET'
})
}

View File

@@ -0,0 +1,19 @@
import { axios } from '@/utils/request'
// 保存布局
export function saveSystemConfig(data) {
return axios({
url: '/v0.1/system_config',
method: 'POST',
data
})
}
// 获取布局
export function getSystemConfig(params) {
return axios({
url: '/v0.1/system_config',
method: 'GET',
params
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@@ -0,0 +1,95 @@
<template>
<a-modal
:visible="visible"
wrapClassName="ci-json-editor"
:closable="false"
:maskClosable="false"
@cancel="handleCancel"
width="50%"
@ok="handleOk"
>
<vue-json-editor
:style="{ '--custom-height': `${windowHeight - 300}px` }"
v-model="jsonData"
:showBtns="false"
:mode="'code'"
lang="zh"
@has-error="onJsonError"
@json-change="onJsonChange"
/>
</a-modal>
</template>
<script>
import vueJsonEditor from 'vue-json-editor'
import { updateCI } from '@/modules/cmdb/api/ci'
export default {
name: 'JsonEditor',
components: { vueJsonEditor },
data() {
return {
visible: false,
jsonData: {},
row: {},
column: {},
default_value_json_right: true,
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
},
methods: {
open(column, row, jsonData) {
this.visible = true
if (row && row[column.property]) {
this.jsonData = JSON.parse(row[column.property]) || {}
} else {
this.jsonData = {}
}
if (jsonData) {
this.jsonData = jsonData
}
this.row = row
this.column = column
},
handleCancel() {
this.visible = false
},
handleOk() {
if (this.row && this.column) {
updateCI(this.row.ci_id || this.row._id, {
[`${this.column.property}`]: this.default_value_json_right ? this.jsonData : {},
}).then(() => {
this.$message.success('保存成功!')
this.handleCancel()
this.$emit('jsonEditorOk', this.row, this.column, this.default_value_json_right ? this.jsonData : {})
})
} else {
this.$emit('jsonEditorOk', this.jsonData)
this.handleCancel()
}
},
onJsonChange(value) {
this.default_value_json_right = true
},
onJsonError() {
this.default_value_json_right = false
},
},
}
</script>
<style lang="less">
.ci-json-editor {
.jsoneditor-outer {
height: var(--custom-height) !important;
border: 1px solid #2f54eb;
}
div.jsoneditor-menu {
background-color: #2f54eb;
}
}
</style>

View File

@@ -0,0 +1,298 @@
<template>
<div>
<a-transfer
:dataSource="dataSource"
:showSearch="true"
:listStyle="{
width: '200px',
height: `${height}px`,
}"
:titles="['未选属性', '已选属性']"
:render="(item) => item.title"
:targetKeys="targetKeys"
@change="handleChange"
@selectChange="selectChange"
:selectedKeys="selectedKeys"
:filterOption="filterOption"
class="cmdb-transfer"
>
<span slot="notFoundContent">暂无数据</span>
<template slot="children" slot-scope="{ props: { direction, filteredItems } }">
<div class="ant-transfer-list-content" v-if="direction === 'right'">
<draggable :value="targetKeys" animation="300" @end="dragEnd" :disabled="!isSortable">
<div
@dblclick="changeSingleItem(item)"
v-for="item in filteredItems"
:key="item.key"
:style="{ height: '38px' }"
>
<li
:class="{
'ant-transfer-list-content-item': true,
'ant-transfer-list-content-item-selected': selectedKeys.includes(item.key),
}"
@click="setSelectedKeys(item)"
>
<OpsMoveIcon class="move-icon" />
<div :class="`ant-transfer-list-content-item-text`" style="display: inline">
{{ item.title }}
<span
:style="{ position: 'absolute', top: '15px', left: '34px', fontSize: '11px', color: '#a3a3a3' }"
>{{ item.name }}</span
>
</div>
<div v-if="isFixable" @click="(e) => changeFixed(e, item)" class="ant-transfer-list-lock-icon">
<a-icon
:type="fixedList.includes(item.key) ? 'lock' : 'unlock'"
:theme="fixedList.includes(item.key) ? 'filled' : 'outlined'"
/>
</div>
<div class="ant-transfer-list-icon" :style="{ left: '17px' }" @click="changeSingleItem(item)">
<a-icon type="left" />
</div>
</li>
</div>
</draggable>
</div>
<div v-if="direction === 'left'" class="ant-transfer-list-content">
<div
@dblclick="changeSingleItem(item)"
v-for="item in filteredItems"
:key="item.key"
:style="{ height: '38px' }"
>
<li
:class="`ant-transfer-list-content-item ${
selectedKeys.includes(item.key) ? 'ant-transfer-list-content-item-selected' : ''
}`"
@click="setSelectedKeys(item)"
>
<div class="ant-transfer-list-content-item-text" style="display: inline">
{{ item.title }}
<span
:style="{ position: 'absolute', top: '15px', left: '34px', fontSize: '11px', color: '#a3a3a3' }"
>{{ item.name }}</span
>
</div>
<div @click="changeSingleItem(item)" :style="{ left: '4px' }" class="ant-transfer-list-icon">
<a-icon type="right" />
</div>
</li>
</div>
</div>
</template>
</a-transfer>
<div v-if="hasFooter" :style="{ marginTop: '5px', height: '20px' }">
<a-button :style="{ float: 'right' }" size="small" @click="handleSubmit" type="primary">确定</a-button>
</div>
</div>
</template>
<script>
import _ from 'lodash'
import draggable from 'vuedraggable'
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
export default {
name: 'AttributesTransfer',
components: { draggable, OpsMoveIcon },
props: {
dataSource: {
type: Array,
default: () => [],
},
targetKeys: {
type: Array,
default: () => [],
},
hasFooter: {
type: Boolean,
default: true,
},
isSortable: {
// 右侧是否可排序
type: Boolean,
default: true,
},
isFixable: {
// 右侧是否可固定
type: Boolean,
default: true,
},
fixedList: {
type: Array,
default: () => [],
},
height: {
type: Number,
default: 400,
},
},
data() {
return {
selectedKeys: [],
}
},
methods: {
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
}
}
})
},
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)
}
},
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.$emit('setTargetKeys', targetKeys)
},
handleSubmit() {
this.$emit('handleSubmit')
},
changeSingleItem(item) {
this.$emit('changeSingleItem', item)
},
dragEnd(e) {
const { newIndex, oldIndex } = e
const _targetKeys = _.cloneDeep(this.targetKeys)
const _item = _targetKeys.splice(oldIndex, 1)[0]
_targetKeys.splice(newIndex, 0, _item)
this.$emit('setTargetKeys', _targetKeys)
},
filterOption(inputValue, option) {
return (
option.title.toLowerCase().includes(inputValue.toLowerCase()) ||
option.name.toLowerCase().includes(inputValue.toLowerCase())
)
},
changeFixed(e, item) {
e.stopPropagation()
e.preventDefault()
const _fixedList = _.cloneDeep(this.fixedList)
const idx = _fixedList.findIndex((key) => key === item.key)
if (idx > -1) {
_fixedList.splice(idx, 1)
} else {
_fixedList.push(item.key)
}
this.$emit('setFixedList', _fixedList)
},
},
}
</script>
<style lang="less">
@import '~@/style/static.less';
.cmdb-transfer {
.ant-transfer-list {
background-color: #f9fbff;
border-color: #e4e7ed;
.ant-transfer-list-header {
background-color: #f9fbff;
border-bottom: none;
.ant-transfer-list-header-title {
color: #custom_colors[color_1];
font-weight: 400;
font-size: 14px;
}
}
.ant-transfer-list-body-search-wrapper {
padding-top: 0;
padding-bottom: 5px;
.ant-transfer-list-search-action {
top: 0;
}
}
.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;
border-left: 2px solid #f9fbff;
padding: 0 12px 8px 25px;
position: relative;
.ant-transfer-list-icon {
position: absolute;
top: 6px;
display: none;
cursor: pointer;
font-size: 12px;
background-color: #fff;
color: #custom_colors[color_1];
border-radius: 4px;
width: 12px;
}
.ant-transfer-list-lock-icon {
position: absolute;
top: 6px;
right: 4px;
display: inline;
cursor: pointer;
font-size: 12px;
color: #cacdd9;
&:hover {
color: #custom_colors[color_1];
}
}
.move-icon {
position: absolute;
left: 0;
top: 6px;
display: none;
width: 14px;
height: 20px;
}
&:hover .ant-transfer-list-icon {
display: inline;
}
&:hover .move-icon {
display: inline !important;
cursor: move;
}
}
.ant-transfer-list-content-item-selected {
background-color: #custom_colors[color_2];
border-color: #custom_colors[color_1];
}
}
}
}
</style>

View File

@@ -0,0 +1,180 @@
<template>
<a-modal :visible="visible" title="导出数据" @cancel="handleCancel" okText="导出" @ok="handleOk">
<a-form :form="form" :label-col="{ span: 6 }" :wrapper-col="{ span: 15 }">
<a-form-item label="文件名">
<a-input
placeholder="请输入文件名"
v-decorator="['filename', { rules: [{ required: true, message: '请输入文件名' }] }]"
/>
</a-form-item>
<a-form-item label="保存类型">
<a-select
placeholder="请选择保存类型"
v-decorator="['type', { rules: [{ required: true, message: '请选择保存类型' }], initialValue: 'xlsx' }]"
>
<a-select-option v-for="item in typeList" :key="item.id" :values="item.id">{{ item.label }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="选择字段">
<div
:style="{
paddingLeft: '26px',
backgroundColor: '#e9e9e9',
borderTopLeftRadius: '5px',
borderTopRightRadius: '5px',
}"
>
<a-checkbox
:indeterminate="indeterminate"
:checked="checkAll"
@change="onCheckAllChange"
:style="{ marginRight: '10px' }"
/>全选
</div>
<div
:style="{
height: '200px',
overflow: 'auto',
borderLeft: '1px solid #e9e9e9',
borderBottom: '1px solid #e9e9e9',
}"
>
<a-tree
checkable
defaultExpandAll
:selectable="false"
:auto-expand-parent="true"
:tree-data="preferenceAttrList"
:replaceFields="replaceFields"
:checkedKeys="checkedKeys"
@check="check"
/>
</div>
</a-form-item>
</a-form>
</a-modal>
</template>
<script>
import { mapMutations } from 'vuex'
import _ from 'lodash'
import moment from 'moment'
import Treeselect from '@riophae/vue-treeselect'
export default {
name: 'BatchDownload',
components: { Treeselect },
props: {
replaceFields: {
type: Object,
default: () => {
return { children: 'children', title: 'alias', key: 'name' }
},
},
treeType: {
type: String,
default: 'default',
},
},
data() {
const typeList = [
{
id: 'xlsx',
label: 'Excel工作簿(*.xlsx)',
},
{
id: 'csv',
label: 'CSV(逗号分隔)(*.csv)',
},
{
id: 'html',
label: '网页(*.html)',
},
{
id: 'xml',
label: 'XML数据(*.xml)',
},
{
id: 'txt',
label: '文本文件(制表符分隔)(*.txt)',
},
]
return {
typeList,
visible: false,
form: this.$form.createForm(this),
preferenceAttrList: [],
checkedKeys: [],
checkAll: false,
indeterminate: false,
defaultChecked: [],
}
},
methods: {
...mapMutations('cmdbStore', ['SET_IS_TABLE_LOADING']),
open({ preferenceAttrList }) {
this.preferenceAttrList = preferenceAttrList
this.visible = true
this.$nextTick((res) => {
this.form.setFieldsValue({
filename: `cmdb-${moment().format('YYYYMMDDHHmmss')}`,
})
if (this.treeType === 'tree') {
const _check = ['ci_type_alias']
preferenceAttrList.forEach((colGroup) => {
if (colGroup.children && colGroup.children.length) {
_check.push(...colGroup.children.map((attr) => attr[`${this.replaceFields.key}`]))
}
})
this.defaultChecked = _check
this.checkedKeys = _check
} else {
this.checkedKeys = preferenceAttrList.map((attr) => attr[`${this.replaceFields.key}`])
}
this.checkAll = true
this.indeterminate = false
})
},
check(checkedKeys) {
if (this.treeType === 'tree') {
this.checkedKeys = checkedKeys.filter((item) => !item.startsWith('parent-'))
} else {
this.checkedKeys = checkedKeys
}
if (this.treeType === 'tree') {
const isEqual = _.isEqual(this.checkedKeys.length, this.defaultChecked.length)
this.checkAll = isEqual
this.indeterminate = !!checkedKeys.length && !isEqual
return
}
this.checkAll = this.checkedKeys.length === this.preferenceAttrList.length
this.indeterminate = !!checkedKeys.length && checkedKeys.length < this.preferenceAttrList.length
},
handleCancel() {
this.visible = false
},
handleOk() {
this.form.validateFields((err, values) => {
if (!err) {
this.SET_IS_TABLE_LOADING(true)
this.$nextTick(() => {
this.$emit('batchDownload', { ...values, checkedKeys: this.checkedKeys })
setTimeout(() => {
this.SET_IS_TABLE_LOADING(false)
this.handleCancel()
}, 2000)
})
}
})
},
onCheckAllChange(e) {
Object.assign(this, {
checkedKeys: e.target.checked ? this.preferenceAttrList.map((attr) => attr[`${this.replaceFields.key}`]) : [],
indeterminate: false,
checkAll: e.target.checked,
})
},
},
}
</script>
<style></style>

View File

@@ -0,0 +1,139 @@
<template>
<div class="ci-type-grant">
<vxe-table size="mini" stripe class="ops-stripe-table" :data="filterTableData" :max-height="`${tableHeight}px`">
<vxe-column field="name"></vxe-column>
<vxe-column v-for="col in columns" :key="col" :field="col" :title="permMap[col]">
<template #default="{row}">
<ReadCheckbox
v-if="['read'].includes(col.split('_')[0])"
:value="row[col.split('_')[0]]"
:valueKey="col"
:rid="row.rid"
@openReadGrantModal="() => openReadGrantModal(col, row)"
/>
<a-checkbox v-else-if="col === 'grant'" :checked="row[col]" @click="clickGrant(col, row)"></a-checkbox>
<a-checkbox @change="(e) => handleChange(e, col, row)" v-else v-model="row[col]"></a-checkbox>
</template>
</vxe-column>
<template #empty>
<div v-if="loading()" style="height: 200px; line-height: 200px;color:#2F54EB">
<a-icon type="loading" /> 加载中...
</div>
<div v-else>
<img :style="{ width: '100px' }" :src="require('@/assets/data_empty.png')" />
<div>暂无数据</div>
</div>
</template>
</vxe-table>
<a-space>
<span class="grant-button" @click="grantDepart">授权用户/部门</span>
<!-- <span class="grant-button" @click="grantRole">授权角色</span> -->
</a-space>
</div>
</template>
<script>
import _ from 'lodash'
import { permMap } from './constants.js'
import { grantCiType, revokeCiType } from '../../api/CIType'
import ReadCheckbox from './readCheckbox.vue'
export default {
name: 'CiTypeGrant',
components: { ReadCheckbox },
inject: ['loading', 'isModal'],
props: {
CITypeId: {
type: Number,
default: null,
},
tableData: {
type: Array,
default: () => [],
},
grantType: {
type: String,
default: 'ci_type',
},
},
computed: {
filterTableData() {
console.log(_.cloneDeep(this.tableData))
const _tableData = this.tableData.filter((data) => {
const _intersection = _.intersection(
Object.keys(data),
this.columns.map((col) => col.split('_')[0])
)
return _intersection && _intersection.length
})
return _.uniqBy(_tableData, (item) => item.rid)
},
columns() {
if (this.grantType === 'ci_type') {
return ['config', 'grant']
}
return ['read_attr', 'read_ci', 'create', 'update', 'delete']
},
windowHeight() {
return this.$store.state.windowHeight
},
tableHeight() {
if (this.isModal) {
return (this.windowHeight - 104) / 2
}
return (this.windowHeight - 104) / 2 - 116
},
},
data() {
return {
permMap,
}
},
methods: {
async handleChange(e, col, row) {
if (e.target.checked) {
await grantCiType(this.CITypeId, row.rid, { perms: [col] }).catch(() => {
this.$emit('getTableData')
})
} else {
await revokeCiType(this.CITypeId, row.rid, { perms: [col] }).catch(() => {
this.$emit('getTableData')
})
}
},
grantDepart() {
this.$emit('grantDepart', this.grantType)
},
grantRole() {
this.$emit('grantRole', this.grantType)
},
openReadGrantModal(col, row) {
this.$emit('openReadGrantModal', col, row)
},
clickGrant(col, row, rowIndex) {
if (!row[col]) {
this.handleChange({ target: { checked: true } }, col, row)
const _idx = this.tableData.findIndex((item) => item.rid === row.rid)
this.$set(this.tableData, _idx, { ...this.tableData[_idx], grant: true })
} else {
const that = this
this.$confirm({
title: '警告',
content: `确认删除 ${row.name} 授权 权限`,
onOk() {
that.handleChange({ target: { checked: false } }, col, row)
const _idx = that.tableData.findIndex((item) => item.rid === row.rid)
that.$set(that.tableData, _idx, { ...that.tableData[_idx], grant: false })
},
})
}
},
},
}
</script>
<style lang="less" scoped>
.ci-type-grant {
padding: 10px 0;
}
</style>

View File

@@ -0,0 +1,11 @@
export const permMap = {
read: '查看',
add: '新增',
create: '新增',
update: '修改',
delete: '删除',
config: '配置',
grant: '授权',
'read_attr': '查看字段',
'read_ci': '查看实例'
}

View File

@@ -0,0 +1,319 @@
<template>
<div class="cmdb-grant" :style="{ maxHeight: `${windowHeight - 104}px` }">
<template v-if="cmdbGrantType.includes('ci_type')">
<div class="cmdb-grant-title">模型权限</div>
<CiTypeGrant
:CITypeId="CITypeId"
:tableData="tableData"
grantType="ci_type"
@grantDepart="grantDepart"
@grantRole="grantRole"
@getTableData="getTableData"
/>
</template>
<template
v-if="
cmdbGrantType.includes('ci_type,ci') || (cmdbGrantType.includes('ci') && !cmdbGrantType.includes('ci_type'))
"
>
<div class="cmdb-grant-title">实例权限</div>
<CiTypeGrant
:CITypeId="CITypeId"
:tableData="tableData"
grantType="ci"
@grantDepart="grantDepart"
@grantRole="grantRole"
@getTableData="getTableData"
@openReadGrantModal="openReadGrantModal"
/>
</template>
<template v-if="cmdbGrantType.includes('type_relation')">
<div class="cmdb-grant-title">关系权限</div>
<TypeRelationGrant
:typeRelationIds="typeRelationIds"
:tableData="tableData"
grantType="type_relation"
@grantDepart="grantDepart"
@grantRole="grantRole"
@getTableData="getTableData"
/>
</template>
<template v-if="cmdbGrantType.includes('relation_view')">
<div class="cmdb-grant-title">{{ resourceTypeName }}权限</div>
<RelationViewGrant
:resourceTypeName="resourceTypeName"
:tableData="tableData"
grantType="relation_view"
@grantDepart="grantDepart"
@grantRole="grantRole"
@getTableData="getTableData"
/>
</template>
<GrantModal ref="grantModal" @handleOk="handleOk" />
<ReadGrantModal ref="readGrantModal" :CITypeId="CITypeId" @updateTableDataRead="updateTableDataRead" />
</div>
</template>
<script>
import { mapState } from 'vuex'
import CiTypeGrant from './ciTypeGrant.vue'
import TypeRelationGrant from './typeRelationGrant.vue'
import { searchResource } from '@/modules/acl/api/resource'
import { getResourcePerms } from '@/modules/acl/api/permission'
import GrantModal from './grantModal.vue'
import ReadGrantModal from './readGrantModal'
import RelationViewGrant from './relationViewGrant.vue'
import { getCITypeGroupById, ciTypeFilterPermissions } from '../../api/CIType'
export default {
name: 'GrantComp',
components: { CiTypeGrant, TypeRelationGrant, RelationViewGrant, GrantModal, ReadGrantModal },
props: {
CITypeId: {
type: Number,
default: null,
},
resourceTypeName: {
type: String,
default: '',
},
resourceType: {
type: String,
default: 'CIType',
},
app_id: {
type: String,
default: 'cmdb',
},
cmdbGrantType: {
type: String,
default: 'ci_type,ci',
},
typeRelationIds: {
type: Array,
default: null,
},
isModal: {
type: Boolean,
default: false,
},
},
inject: ['resource_type'],
data() {
return {
tableData: [],
grantType: '',
resource_id: null,
attrGroup: [],
filerPerimissions: {},
loading: false,
}
},
computed: {
...mapState({
allEmployees: (state) => state.user.allEmployees,
allDepartments: (state) => state.user.allDepartments,
}),
child_resource_type() {
return this.resource_type()
},
windowHeight() {
return this.$store.state.windowHeight
},
},
provide() {
return {
attrGroup: () => {
return this.attrGroup
},
filerPerimissions: () => {
return this.filerPerimissions
},
loading: () => {
return this.loading
},
isModal: this.isModal,
}
},
watch: {
resourceTypeName: {
immediate: true,
handler() {
this.init()
},
},
CITypeId: {
immediate: true,
handler() {
if (this.CITypeId && this.cmdbGrantType.includes('ci')) {
this.getFilterPermissions()
this.getAttrGroup()
}
},
},
},
mounted() {},
methods: {
getAttrGroup() {
getCITypeGroupById(this.CITypeId, { need_other: true }).then((res) => {
this.attrGroup = res
})
},
getFilterPermissions() {
ciTypeFilterPermissions(this.CITypeId).then((res) => {
this.filerPerimissions = res
})
},
async init() {
const _find = this.child_resource_type.groups.find((item) => item.name === this.resourceType)
const resource_type_id = _find?.id ?? 0
const res = await searchResource({
app_id: this.app_id,
resource_type_id,
page_size: 9999,
})
const _tempFind = res.resources.find((item) => item.name === this.resourceTypeName)
console.log(this.resourceTypeName)
this.resource_id = _tempFind?.id || 0
this.getTableData()
},
async getTableData() {
this.loading = true
const _tableData = await getResourcePerms(this.resource_id, { need_users: 0 })
const perms = []
for (const key in _tableData) {
const obj = {}
obj.name = key
_tableData[key].perms.forEach((perm) => {
obj[`${perm.name}`] = true
obj.rid = perm?.rid ?? null
})
perms.push(obj)
}
this.tableData = perms
this.loading = false
},
// 授权common-setting中的部门 从中拿到roleid
grantDepart(grantType) {
this.$refs.grantModal.open('depart')
this.grantType = grantType
},
// 授权common-setting中的角色 从中拿到roleid
grantRole(grantType) {
this.$refs.grantModal.open('role')
this.grantType = grantType
},
handleOk(params) {
const { grantType } = this
console.log(params)
const rids = [
...params.department.map((rid) => {
const _find = this.allDepartments.find((dep) => dep.acl_rid === rid)
return { rid, name: _find?.department_name ?? rid }
}),
...params.user.map((rid) => {
const _find = this.allEmployees.find((dep) => dep.acl_rid === rid)
return { rid, name: _find?.nickname ?? rid }
}),
]
if (grantType === 'ci_type') {
this.tableData.unshift(
...rids.map(({ rid, name }) => {
const _find = this.tableData.find((item) => item.rid === rid)
return {
rid,
name,
conifg: false,
grant: false,
..._find,
}
})
)
}
if (grantType === 'ci') {
this.tableData.unshift(
...rids.map(({ rid, name }) => {
const _find = this.tableData.find((item) => item.rid === rid)
return {
rid,
name,
read_attr: false,
read_ci: false,
create: false,
update: false,
delete: false,
..._find,
}
})
)
}
if (grantType === 'type_relation') {
this.tableData.unshift(
...rids.map(({ rid, name }) => {
return {
rid,
name,
create: false,
grant: false,
delete: false,
}
})
)
}
if (grantType === 'relation_view') {
this.tableData.unshift(
...rids.map(({ rid, name }) => {
return {
rid,
name,
read: false,
grant: false,
}
})
)
}
},
openReadGrantModal(col, row) {
this.$refs.readGrantModal.open(col, row)
},
updateTableDataRead(row, hasRead) {
const _idx = this.tableData.findIndex((item) => item.rid === row.rid)
this.$set(this.tableData, _idx, { ...this.tableData[_idx], read: hasRead })
this.getFilterPermissions()
},
},
}
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.cmdb-grant {
position: relative;
padding: 24px 24px 0 24px;
overflow: auto;
.cmdb-grant-title {
border-left: 4px solid #custom_colors[color_1];
padding-left: 10px;
}
}
</style>
<style lang="less">
@import '~@/style/static.less';
.cmdb-grant {
.grant-button {
padding: 6px 8px;
color: #custom_colors[color_1];
background-color: #custom_colors[color_2];
border-radius: 2px;
cursor: pointer;
margin: 15px 0;
display: inline-block;
transition: all 0.3s;
&:hover {
box-shadow: 2px 3px 4px #custom_colors[color_2];
}
}
}
</style>

View File

@@ -0,0 +1,49 @@
<template>
<a-modal :title="title" :visible="visible" @ok="handleOk" @cancel="handleCancel" destroyOnClose>
<EmployeeTransfer
:isDisabledAllCompany="true"
v-if="type === 'depart'"
uniqueKey="acl_rid"
ref="employeeTransfer"
:height="350"
/>
</a-modal>
</template>
<script>
import EmployeeTransfer from '@/components/EmployeeTransfer'
export default {
name: 'GrantModal',
components: { EmployeeTransfer },
data() {
return {
visible: false,
type: 'depart',
}
},
computed: {
title() {
if (this.type === 'depart') {
return '授权用户/部门'
}
return '授权角色'
},
},
methods: {
open(type) {
this.visible = true
this.type = type
},
handleOk() {
const params = this.$refs.employeeTransfer.getValues()
this.handleCancel()
this.$emit('handleOk', params)
},
handleCancel() {
this.visible = false
},
},
}
</script>
<style></style>

View File

@@ -0,0 +1,57 @@
<template>
<a-modal width="800px" :visible="visible" @ok="handleOk" @cancel="handleCancel" :bodyStyle="{ padding: 0 }">
<GrantComp
:resourceType="resourceType"
:app_id="app_id"
:cmdbGrantType="cmdbGrantType"
:resourceTypeName="resourceTypeName"
:typeRelationIds="typeRelationIds"
:CITypeId="CITypeId"
:isModal="true"
/>
</a-modal>
</template>
<script>
import GrantComp from './grantComp.vue'
export default {
name: 'CMDBGrant',
components: { GrantComp },
props: {
resourceType: {
type: String,
default: 'CIType',
},
app_id: {
type: String,
default: '',
},
},
data() {
return {
visible: false,
resourceTypeName: '',
typeRelationIds: [],
cmdbGrantType: '',
CITypeId: null,
}
},
methods: {
open({ name, typeRelationIds = [], cmdbGrantType, CITypeId }) {
this.visible = true
this.resourceTypeName = name
this.typeRelationIds = typeRelationIds
this.cmdbGrantType = cmdbGrantType
this.CITypeId = CITypeId
},
handleOk() {
this.handleCancel()
},
handleCancel() {
this.visible = false
},
},
}
</script>
<style></style>

View File

@@ -0,0 +1,89 @@
<template>
<div :class="{ 'read-checkbox': true, 'ant-checkbox-wrapper': isHalfChecked }" @click="openReadGrantModal">
<a-tooltip
v-if="value && isHalfChecked"
:title="valueKey === 'read_ci' ? filerPerimissions[this.rid].name || '' : ''"
>
<div v-if="value && isHalfChecked" :class="{ 'read-checkbox-half-checked': true, 'ant-checkbox': true }"></div>
</a-tooltip>
<a-checkbox v-else :checked="value" />
</div>
</template>
<script>
export default {
name: 'ReadCheckbox',
inject: {
provide_filerPerimissions: {
from: 'filerPerimissions',
},
},
props: {
value: {
type: Boolean,
default: false,
},
valueKey: {
type: String,
default: 'read_attr',
},
rid: {
type: Number,
default: 0,
},
},
computed: {
filerPerimissions() {
return this.provide_filerPerimissions()
},
filterKey() {
if (this.valueKey === 'read_attr') {
return 'attr_filter'
}
return 'ci_filter'
},
isHalfChecked() {
if (this.filerPerimissions[this.rid]) {
const _tempValue = this.filerPerimissions[this.rid][this.filterKey]
return !!(_tempValue && _tempValue.length)
}
return false
},
},
methods: {
openReadGrantModal() {
this.$emit('openReadGrantModal')
},
},
}
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.read-checkbox {
.read-checkbox-half-checked {
width: 16px;
height: 16px;
border: 1px solid #d9d9d9;
border-radius: 2px;
cursor: pointer;
margin: 0;
padding: 0;
position: relative;
overflow: hidden;
&::after {
content: '';
position: absolute;
width: 0;
height: 0;
// background-color: #custom_colors[color_1];
border-radius: 2px;
border: 14px solid transparent;
border-left-color: #custom_colors[color_1];
transform: rotate(225deg);
top: -16px;
left: -17px;
}
}
}
</style>

View File

@@ -0,0 +1,205 @@
<template>
<a-modal :width="680" :title="title" :visible="visible" @ok="handleOk" @cancel="handleCancel">
<CustomRadio
:radioList="[
{ value: 1, label: '全部' },
{ value: 2, label: '自定义', layout: 'vertical' },
{ value: 3, label: '' },
]"
v-model="radioValue"
>
<template slot="extra_2" v-if="radioValue === 2">
<treeselect
v-if="colType === 'read_attr'"
v-model="selectedAttr"
:multiple="true"
:clearable="true"
searchable
:options="attrGroup"
placeholder="请选择属性字段"
value-consists-of="LEAF_PRIORITY"
:limit="10"
:limitText="(count) => `+ ${count}`"
:normalizer="
(node) => {
return {
id: node.name || -1,
label: node.alias || node.name || '其他',
title: node.alias || node.name || '其他',
children: node.attributes,
}
}
"
appendToBody
zIndex="1050"
>
</treeselect>
<a-form-model
:model="form"
:rules="rules"
v-if="colType === 'read_ci'"
:labelCol="{ span: 2 }"
:wrapperCol="{ span: 10 }"
ref="form"
>
<a-form-model-item label="名称" prop="name">
<a-input v-model="form.name" />
</a-form-model-item>
<FilterComp
ref="filterComp"
:isDropdown="false"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList"
@setExpFromFilter="setExpFromFilter"
:expression="expression"
/>
</a-form-model>
</template>
</CustomRadio>
</a-modal>
</template>
<script>
import { grantCiType, revokeCiType } from '../../api/CIType'
import { getCITypeAttributesByTypeIds } from '../../api/CITypeAttr'
import FilterComp from '@/components/CMDBFilterComp'
export default {
name: 'ReadGrantModal',
components: { FilterComp },
props: {
CITypeId: {
type: Number,
default: null,
},
},
inject: {
provide_attrGroup: {
from: 'attrGroup',
},
provide_filerPerimissions: {
from: 'filerPerimissions',
},
},
data() {
return {
visible: false,
colType: '',
row: {},
radioValue: 1,
radioStyle: {
display: 'block',
height: '30px',
lineHeight: '30px',
},
selectedAttr: [],
ruleList: [],
canSearchPreferenceAttrList: [],
expression: '',
form: {
name: '',
},
rules: {
name: [{ required: true, message: '请输入自定义筛选条件名' }],
},
}
},
computed: {
title() {
if (this.colType === 'read_attr') {
return '字段权限'
}
return '实例权限'
},
attrGroup() {
return this.provide_attrGroup()
},
filerPerimissions() {
return this.provide_filerPerimissions()
},
filterKey() {
if (this.colType === 'read_attr') {
return 'attr_filter'
}
return 'ci_filter'
},
},
methods: {
async open(colType, row) {
this.visible = true
this.colType = colType
this.row = row
if (this.colType === 'read_ci') {
await getCITypeAttributesByTypeIds({ type_ids: this.CITypeId }).then((res) => {
this.canSearchPreferenceAttrList = res.attributes.filter((item) => item.value_type !== '6')
})
}
if (this.filerPerimissions[row.rid]) {
const _tempValue = this.filerPerimissions[row.rid][this.filterKey]
if (_tempValue && _tempValue.length) {
this.radioValue = 2
if (this.colType === 'read_attr') {
this.selectedAttr = _tempValue
} else {
this.expression = `q=${_tempValue}`
this.form = {
name: this.filerPerimissions[row.rid].name || '',
}
this.$nextTick(() => {
this.$refs.filterComp.visibleChange(true)
})
}
}
} else {
this.form = {
name: '',
}
}
},
async handleOk() {
if (this.radioValue === 1) {
await grantCiType(this.CITypeId, this.row.rid, {
perms: ['read'],
attr_filter: this.colType === 'read_attr' ? [] : undefined,
ci_filter: this.colType === 'read_ci' ? '' : undefined,
})
} else if (this.radioValue === 2) {
if (this.colType === 'read_ci') {
this.$refs.filterComp.handleSubmit()
}
await grantCiType(this.CITypeId, this.row.rid, {
perms: ['read'],
attr_filter: this.colType === 'read_attr' ? this.selectedAttr : undefined,
ci_filter: this.colType === 'read_ci' ? this.expression.slice(2) : undefined,
name: this.colType === 'read_ci' ? this.form.name : undefined,
})
} else {
const _tempValue = this.filerPerimissions?.[this.row.rid]?.[this.filterKey]
await revokeCiType(this.CITypeId, this.row.rid, {
perms: ['read'],
attr_filter: this.colType === 'read_attr' ? _tempValue : undefined,
ci_filter: this.colType === 'read_ci' ? _tempValue : undefined,
})
}
this.$emit('updateTableDataRead', this.row, this.radioValue === 1 || this.radioValue === 2)
this.handleCancel()
},
handleCancel() {
this.radioValue = 1
this.selectedAttr = []
if (this.$refs.form) {
this.$refs.form.resetFields()
}
this.visible = false
},
setExpFromFilter(filterExp) {
let expression = ''
if (filterExp) {
expression = `q=${filterExp}`
}
this.expression = expression
},
},
}
</script>
<style></style>

View File

@@ -0,0 +1,81 @@
<template>
<div class="ci-relation-grant">
<vxe-table size="mini" stripe class="ops-stripe-table" :data="tableData" :max-height="`${tableHeight}px`">
<vxe-column field="name"></vxe-column>
<vxe-column v-for="col in columns" :key="col" :field="col" :title="permMap[col]">
<template #default="{row}">
<a-checkbox @change="(e) => handleChange(e, col, row)" v-model="row[col]"></a-checkbox>
</template>
</vxe-column>
</vxe-table>
<a-space>
<span class="grant-button" @click="grantDepart">授权用户/部门</span>
<!-- <span class="grant-button" @click="grantRole">授权角色</span> -->
</a-space>
</div>
</template>
<script>
import { permMap } from './constants.js'
import { grantRelationView, revokeRelationView } from '../../api/preference.js'
export default {
name: 'RelationViewGrant',
inject: ['loading', 'isModal'],
props: {
resourceTypeName: {
type: String,
default: '',
},
tableData: {
type: Array,
default: () => [],
},
grantType: {
type: String,
default: 'relation_view',
},
},
data() {
return {
permMap,
columns: ['read', 'grant'],
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
tableHeight() {
if (this.isModal) {
return (this.windowHeight - 104) / 2
}
return (this.windowHeight - 104) / 2 - 116
},
},
methods: {
grantDepart() {
this.$emit('grantDepart', this.grantType)
},
grantRole() {
this.$emit('grantRole', this.grantType)
},
handleChange(e, col, row) {
if (e.target.checked) {
grantRelationView(row.rid, { perms: [col], name: this.resourceTypeName }).catch(() => {
this.$emit('getTableData')
})
} else {
revokeRelationView(row.rid, { perms: [col], name: this.resourceTypeName }).catch(() => {
this.$emit('getTableData')
})
}
},
},
}
</script>
<style lang="less" scoped>
.ci-relation-grant {
padding: 10px 0;
}
</style>

View File

@@ -0,0 +1,83 @@
<template>
<div class="ci-relation-grant">
<vxe-table size="mini" stripe class="ops-stripe-table" :data="tableData" :max-height="`${tableHeight}px`">
<vxe-column field="name"></vxe-column>
<vxe-column v-for="col in columns" :key="col" :field="col" :title="permMap[col]">
<template #default="{row}">
<a-checkbox @change="(e) => handleChange(e, col, row)" v-model="row[col]"></a-checkbox>
</template>
</vxe-column>
</vxe-table>
<a-space>
<span class="grant-button" @click="grantDepart">授权用户/部门</span>
<!-- <span class="grant-button" @click="grantRole">授权角色</span> -->
</a-space>
</div>
</template>
<script>
import { permMap } from './constants.js'
import { grantTypeRelation, revokeTypeRelation } from '../../api/CITypeRelation.js'
export default {
name: 'TypeRelationGrant',
inject: ['loading', 'isModal'],
props: {
tableData: {
type: Array,
default: () => [],
},
grantType: {
type: String,
default: 'type_relation',
},
typeRelationIds: {
type: Array,
default: null,
},
},
data() {
return {
permMap,
columns: ['create', 'grant', 'delete'],
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
tableHeight() {
if (this.isModal) {
return (this.windowHeight - 104) / 2
}
return (this.windowHeight - 104) / 2 - 116
},
},
methods: {
grantDepart() {
this.$emit('grantDepart', this.grantType)
},
grantRole() {
this.$emit('grantRole', this.grantType)
},
handleChange(e, col, row) {
const first = this.typeRelationIds[0]
const second = this.typeRelationIds[1]
if (e.target.checked) {
grantTypeRelation(first, second, row.rid, { perms: [col] }).catch(() => {
this.$emit('getTableData')
})
} else {
revokeTypeRelation(first, second, row.rid, { perms: [col] }).catch(() => {
this.$emit('getTableData')
})
}
},
},
}
</script>
<style lang="less" scoped>
.ci-relation-grant {
padding: 10px 0;
}
</style>

View File

@@ -0,0 +1,565 @@
<template>
<div class="color-picker">
<!-- 选中的颜色 -->
<!-- <div class="color-button">
<div class="back-ground">
<div class="contain" :style="{ backgroundColor: realShowColor }" @click="isShowDropDown">
<a-icon type="down" />
</div>
</div>
</div> -->
<!-- 颜色选择器 -->
<div class="color-dropdown">
<div class="color-dropdown-picker">
<!-- 颜色面板 -->
<div
ref="colorPannel"
class="color-pannel-box"
@click="pannelMosueClick($event)"
:style="{ backgroundColor: colorPannel.backgroundColor }"
>
<div
:style="{ top: colorPannel.top + 'px', left: colorPannel.left + 'px' }"
class="color-select-circle"
@mousedown="pannelMosueHandler($event)"
></div>
</div>
<!-- 色相柱 -->
<div ref="colorBar" class="color-slider-box">
<div class="color-slider"></div>
<div class="color-thumb" :style="{ top: colorBar.top + 'px' }" @mousedown="thumbMouseHandler($event)"></div>
</div>
</div>
<!-- 透明条 -->
<!-- <div v-if="showAlpha" ref="alphaBar" class="color-alpha">
<div
class="color-alpha-bar"
:style="{
background: `linear-gradient(to right, ${alphaColorBar.barColor}, ${rgbToRgba(alphaColorBar.barColor, 0)})`,
}"
></div>
<div
class="color-alpha-thumb"
:style="{ left: alphaColorBar.thumbLeft + 'px' }"
@mousedown="alphaBarMouseHandler($event)"
></div>
</div> -->
<div class="color-input">
<a-input
size="small"
v-model="realShowColor"
class="color-input-box"
type="text"
style="width:130px"
@pressEnter="changeInputColor"
/>
<!-- <button @click="submitColor">确定</button> -->
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ColorPicker',
props: {
// 是否开启透明度
// showAlpha: {
// type: Boolean,
// default() {
// return true
// },
// },
// 初始化颜色使用该组件时传入的默认颜色支持hexrgb格式
initColor: {
type: String,
default() {
return '#f00'
},
},
// input中展示的颜色格式: hexrgb
colorFormat: {
type: String,
default() {
return 'hex'
},
},
},
data() {
return {
colorConfig: {
h: 360,
s: 100,
v: 100,
alpha: 1,
value: '',
// 底色
basicColor: '',
},
alphaColorBar: {
barColor: 'rgb(255, 0, 0)',
thumbLeft: 0,
width: 0,
},
colorBar: {
top: 0,
height: 0,
},
colorPannel: {
top: 0,
left: 300,
backgroundColor: '#f00',
height: 0,
width: 0,
},
// 默认红色
realShowColor: '#f00',
isShow: true,
isApply: false,
}
},
mounted() {
this.initShowColor(this.initColor)
},
methods: {
// 初始化
initShowColor(color) {
// 初始化hsv颜色格式化的颜色值
let hsvObj, initRgb
if (color.indexOf('#') !== -1) {
initRgb = this.hexToRGB(color)
hsvObj = this.rgbToHSV(initRgb)
} else if (color.indexOf('rgb') !== -1) {
hsvObj = this.rgbToHSV(color)
} else {
throw new Error('初始化颜色格式错误,使用#fff或rgb格式')
// this.$message.error('颜色格式错误使用16进制格式')
}
if (hsvObj) {
this.colorConfig.h = hsvObj.h
this.colorConfig.s = hsvObj.s
this.colorConfig.v = hsvObj.v
}
// 获取容器高宽
this.colorBar.height = this.$refs.colorBar.getBoundingClientRect().height
this.colorPannel.height = this.$refs.colorPannel.getBoundingClientRect().height
this.colorPannel.width = this.$refs.colorPannel.getBoundingClientRect().width
if (this.showAlpha) {
this.alphaColorBar.width = this.$refs.alphaBar.getBoundingClientRect().width
// 根据alpha获取滑块位置
this.alphaToPosition(this.colorConfig.alpha, this.alphaColorBar.width)
this.alphaColorBar.barColor = initRgb || color
}
// 根据hsv获取位置
this.colorPannel.backgroundColor = this.hueToRGB(this.colorConfig.h)
this.hsvToPosition(this.colorConfig.s, this.colorConfig.v, this.colorPannel.width, this.colorPannel.height)
this.hueToPosition(this.colorConfig.h, this.colorBar.height)
// 根据colorFormat和showAlpha格式化颜色
this.colorForamtTransform()
this.realShowColor = this.colorConfig.value || this.initColor
},
// isShowDropDown() {
// this.isShow = !this.isShow
// },
submitColor() {
// 如果颜色为rgba形式将转换为rgb
let initColor
if (this.realShowColor.indexOf('rgba') !== -1) {
initColor = this.realShowColor.replace(/,\d{1,3}(?=\))/, '')
// 获取输入的alpha
this.colorConfig.alpha = parseFloat(this.realShowColor.split(',')[3].replace(')', ''))
this.colorConfig.alpha = Math.max(0, this.colorConfig.alpha)
this.colorConfig.alpha = Math.min(this.colorConfig.alpha, 1)
} else {
initColor = this.realShowColor
}
this.initShowColor(initColor)
this.isShow = false
},
// 色相柱的拖拽事件
thumbMouseHandler(e) {
if (e.type === 'mousedown') {
document.body.addEventListener('mousemove', this.thumbMouseHandler)
document.body.addEventListener('mouseup', this.thumbMouseHandler)
} else if (e.type === 'mousemove') {
const elemInfo = this.$refs.colorBar.getBoundingClientRect()
this.colorBar.top = e.clientY - elemInfo.top
this.colorBar.top = Math.max(0, this.colorBar.top)
this.colorBar.top = Math.min(this.colorBar.top, elemInfo.height)
this.colorConfig.h = ((parseInt(this.colorBar.top) / elemInfo.height) * 360 * 100) / 100
// 色相[0,360)
if (this.colorConfig.h === 360) {
this.colorConfig.h = 0
}
// 获取颜色面板背景色
this.colorPannel.backgroundColor = this.hueToRGB(this.colorConfig.h)
this.colorForamtTransform()
this.alphaColorBar.barColor = this.colorConfig.basicColor
this.realShowColor = this.colorConfig.value
} else if (e.type === 'mouseup') {
// 当释放鼠标键时删除鼠标移动事件和删除鼠标释放事件
document.body.removeEventListener('mousemove', this.thumbMouseHandler)
document.body.removeEventListener('mouseup', this.thumbMouseHandler)
}
this.$emit('changColorPicker', this.realShowColor)
},
// 颜色面板点击事件
pannelMosueClick(e) {
const elemInfo = this.$refs.colorPannel.getBoundingClientRect()
// 在颜色面板容器范围内移动
this.colorPannel.top = e.clientY - elemInfo.top
this.colorPannel.left = e.clientX - elemInfo.left
// 使取色圈移动更加顺滑且不超过取色面板容器范围
this.colorPannel.left = Math.max(0, this.colorPannel.left)
this.colorPannel.left = Math.min(this.colorPannel.left, elemInfo.width)
this.colorPannel.top = Math.max(0, this.colorPannel.top)
this.colorPannel.top = Math.min(this.colorPannel.top, elemInfo.height)
// 计算饱和度(0 -> 100)和亮度 (0 -> 100)
this.colorConfig.s = (parseInt(this.colorPannel.left) / elemInfo.width) * 100
this.colorConfig.v = (1 - parseInt(this.colorPannel.top) / elemInfo.height) * 100
this.colorForamtTransform()
// 将hsv转换为rgb
this.alphaColorBar.barColor = this.colorConfig.basicColor
this.realShowColor = this.colorConfig.value
this.$emit('changColorPicker', this.realShowColor)
},
// 颜色面板的拖拽事件
pannelMosueHandler(e) {
if (e.type === 'mousedown') {
document.body.addEventListener('mousemove', this.pannelMosueHandler)
document.body.addEventListener('mouseup', this.pannelMosueHandler)
} else if (e.type === 'mousemove') {
const elemInfo = this.$refs.colorPannel.getBoundingClientRect()
// 在颜色面板容器范围内移动
this.colorPannel.top = e.clientY - elemInfo.top
this.colorPannel.left = e.clientX - elemInfo.left
// 使取色圈移动更加顺滑且不超过取色面板容器范围
this.colorPannel.left = Math.max(0, this.colorPannel.left)
this.colorPannel.left = Math.min(this.colorPannel.left, elemInfo.width)
this.colorPannel.top = Math.max(0, this.colorPannel.top)
this.colorPannel.top = Math.min(this.colorPannel.top, elemInfo.height)
// 计算饱和度(0 -> 100)和亮度 (0 -> 100)
this.colorConfig.s = (parseInt(this.colorPannel.left) / elemInfo.width) * 100
this.colorConfig.v = (1 - parseInt(this.colorPannel.top) / elemInfo.height) * 100
this.colorForamtTransform()
// 将hsv转换为rgb
this.alphaColorBar.barColor = this.colorConfig.basicColor
this.realShowColor = this.colorConfig.value
} else if (e.type === 'mouseup') {
// 当释放鼠标键时删除鼠标移动事件和删除鼠标释放事件
document.body.removeEventListener('mousemove', this.pannelMosueHandler)
document.body.removeEventListener('mouseup', this.pannelMosueHandler)
}
this.$emit('changColorPicker', this.realShowColor)
},
// 透明柱的拖拽事件
// alphaBarMouseHandler(e) {
// if (e.type === 'mousedown') {
// document.body.addEventListener('mousemove', this.alphaBarMouseHandler)
// document.body.addEventListener('mouseup', this.alphaBarMouseHandler)
// } else if (e.type === 'mousemove') {
// const elemInfo = this.$refs.alphaBar.getBoundingClientRect()
// this.alphaColorBar.thumbLeft = e.clientX - elemInfo.left
// this.alphaColorBar.thumbLeft = Math.max(0, this.alphaColorBar.thumbLeft)
// this.alphaColorBar.thumbLeft = Math.min(this.alphaColorBar.thumbLeft, elemInfo.width)
// // 获取颜色透明度0 -> 1
// this.colorConfig.alpha = (1 - this.alphaColorBar.thumbLeft / elemInfo.width).toFixed(2)
// this.colorForamtTransform()
// this.realShowColor = this.colorConfig.value
// } else if (e.type === 'mouseup') {
// // 当释放鼠标键时删除鼠标移动事件和删除鼠标释放事件
// document.body.removeEventListener('mousemove', this.alphaBarMouseHandler)
// document.body.removeEventListener('mouseup', this.alphaBarMouseHandler)
// }
// },
// 颜色格式
colorForamtTransform() {
if (this.showAlpha) {
// 如果开启透明度那么颜色一定为rgba格式
this.colorConfig.basicColor = this.hsvToRGB(this.colorConfig.h, this.colorConfig.s, this.colorConfig.v)
this.colorConfig.value = this.rgbToRgba(this.colorConfig.basicColor, this.colorConfig.alpha)
} else {
if (this.colorFormat === 'hex') {
this.colorConfig.basicColor = this.hsvToRGB(this.colorConfig.h, this.colorConfig.s, this.colorConfig.v)
this.colorConfig.value = this.rgbToHex(this.colorConfig.basicColor)
}
if (this.colorFormat === 'rgb') {
this.colorConfig.basicColor = this.hsvToRGB(this.colorConfig.h, this.colorConfig.s, this.colorConfig.v)
this.colorConfig.value = this.colorConfig.basicColor
}
}
},
// 从hue to rgb
hueToRGB(h) {
if (h === 360) {
h = 0
}
const doHandle = (num) => {
if (num > 255) {
return 255
} else if (num < 0) {
return 0
} else {
return Math.round(num)
}
}
const hueRGB = (h / 60) * 255
const r = doHandle(Math.abs(hueRGB - 765) - 255)
const g = doHandle(510 - Math.abs(hueRGB - 510))
const b = doHandle(510 - Math.abs(hueRGB - 1020))
return 'rgb(' + r + ',' + g + ',' + b + ')'
},
// 从HSV(色相饱和度亮度) to rgb
hsvToRGB(h, s, v) {
s = s / 100
v = v / 100
let r = 0
let g = 0
let b = 0
const i = Math.floor(h / 60)
const f = h / 60 - i
const p = v * (1 - s)
const q = v * (1 - f * s)
const t = v * (1 - (1 - f) * s)
switch (i) {
case 0:
r = v
g = t
b = p
break
case 1:
r = q
g = v
b = p
break
case 2:
r = p
g = v
b = t
break
case 3:
r = p
g = q
b = v
break
case 4:
r = t
g = p
b = v
break
case 5:
r = v
g = p
b = q
break
}
return `rgb(${Math.round(r * 255)},${Math.round(g * 255)},${Math.round(b * 255)})`
},
// rgb to hsv
rgbToHSV(rgbStr) {
let { r, g, b } = this.getRGB(rgbStr)
r = parseFloat(parseFloat(r / 255).toFixed(4))
g = parseFloat(parseFloat(g / 255).toFixed(4))
b = parseFloat(parseFloat(b / 255).toFixed(4))
const max = Math.max(r, g, b)
const min = Math.min(r, g, b)
let h
const v = max
const d = max - min
const s = max === 0 ? 0 : d / max
if (max === min) {
h = 0 // achromatic
} else {
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0)
break
case g:
h = (b - r) / d + 2
break
case b:
h = (r - g) / d + 4
break
}
h /= 6
}
return { h: h * 360, s: s * 100, v: v * 100 }
},
// 根据hsv获取取色圈位置
hsvToPosition(s, v, width, height) {
this.colorPannel.top = height - (v * height) / 100
this.colorPannel.left = (s * width) / 100
},
hueToPosition(h, height) {
this.colorBar.top = (h * height) / 360
},
alphaToPosition(alpha, width) {
this.alphaColorBar.thumbLeft = (1 - alpha) * width
},
// 拆解rgb为r,g,b
getRGB(rgbStr) {
const matchArr = rgbStr.match(/\(.+?\)/g)[0].match(/\w+/g)
const r = parseInt(matchArr[0])
const g = parseInt(matchArr[1])
const b = parseInt(matchArr[2])
return { r, g, b }
},
// rgb 16进制
rgbToHex(rgbStr) {
// 拆解rgb为[255,255,255]形式
const { r, g, b } = this.getRGB(rgbStr)
return `#${this.zeroFill(r.toString(16))}${this.zeroFill(g.toString(16))}${this.zeroFill(b.toString(16))}`
},
rgbToRgba(rgbStr, alpha) {
return rgbStr.replace(')', `,${alpha})`)
},
hexToRGB(hexStr) {
if (hexStr.length === 4) {
const hexArr = hexStr.match(/\w{1}/g)
return `rgb(${parseInt(hexArr[0] + hexArr[0], 16)},${parseInt(hexArr[1] + hexArr[1], 16)},${parseInt(
hexArr[2] + hexArr[2],
16
)})`
}
if (hexStr.length === 7) {
const hexArr = hexStr.match(/\w{2}/g)
return `rgb(${parseInt(hexArr[0], 16)},${parseInt(hexArr[1], 16)},${parseInt(hexArr[2], 16)})`
}
},
// 补零
zeroFill(val) {
return val.length > 1 ? val : '0' + val
},
changeInputColor(e) {
this.initShowColor(e.target.value)
this.$emit('changColorPicker', e.target.value)
},
},
}
</script>
<style lang="less" scoped>
.color-picker {
width: 150px;
margin: auto;
height: 100px;
margin-top: 0px;
position: relative;
// .color-button {
// height: 36px;
// width: 36px;
// border: 1px solid rgba(0, 0, 0, 0.15);
// border-radius: 4px;
// .back-ground {
// height: 26px;
// width: 26px;
// margin: 4px;
// background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);
// .contain {
// border: 1px solid rgba(0, 0, 0, 0.5);
// border-radius: 2px;
// }
// }
// }
.color-dropdown {
margin: auto;
width: 140px;
height: 92px;
position: absolute;
top: 0;
left: 0;
// background-color: rgba(0, 0, 0, 0.2);
overflow: hidden;
.color-dropdown-picker {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-around;
.color-pannel-box {
position: relative;
width: 110px;
height: 64px;
background: linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, transparent);
.color-select-circle {
position: absolute;
transform: translate(-4px, -4px);
border: 1px solid #fff;
width: 8px;
height: 8px;
border-radius: 50%;
}
}
.color-slider-box {
cursor: pointer;
width: 10px;
position: relative;
.color-slider {
background: linear-gradient(180deg, #f00, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00);
width: 10px;
height: 64px;
}
.color-thumb {
width: 10px;
height: 7px;
position: absolute;
left: 0;
transform: translate(0, -3px);
border-radius: 2px;
background-color: rgb(10, 10, 10);
border: 3px solid #fff;
margin-left: 0;
}
}
}
.color-alpha {
position: relative;
height: 12px;
box-shadow: 2px 2px 2px 2px 2px rgba(0, 0, 0, 0.1);
margin-left: 10px;
width: 300px;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);
.color-alpha-bar {
height: 100%;
width: 100%;
}
.color-alpha-thumb {
width: 7px;
height: 18px;
position: absolute;
top: -3px;
transform: translate(-3px, 0);
border-radius: 2px;
background-color: rgb(10, 10, 10);
border: 3px solid #fff;
}
}
.color-input {
width: 100%;
margin: auto;
display: flex;
padding: 3px 6px;
justify-content: space-between;
button {
color: #000;
}
.color-input-box {
color: #000;
border: 1px solid rgba(0, 0, 0, 0.1);
}
}
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More