feat: 主机分组授权.

This commit is contained in:
lijiahang
2023-11-24 14:42:58 +08:00
parent ba446ba508
commit 51f0e83b3a
10 changed files with 332 additions and 49 deletions

View File

@@ -29,7 +29,7 @@
]
},
"dependencies": {
"@arco-design/web-vue": "^2.44.7",
"@arco-design/web-vue": "^2.53.0",
"@dangojs/a-query-header": "^0.0.31",
"@sanqi377/arco-vue-icon-picker": "^1.0.7",
"@vueuse/core": "^9.3.0",

View File

@@ -11,11 +11,11 @@ overrides:
dependencies:
'@arco-design/web-vue':
specifier: ^2.44.7
version: 2.51.1(vue@3.3.4)
specifier: ^2.53.0
version: 2.53.0(vue@3.3.4)
'@dangojs/a-query-header':
specifier: ^0.0.31
version: 0.0.31(@arco-design/web-vue@2.51.1)(@dangojs/digitforce-ui-utils@0.0.9)(lodash@4.17.21)(vue@3.3.4)
version: 0.0.31(@arco-design/web-vue@2.53.0)(@dangojs/digitforce-ui-utils@0.0.9)(lodash@4.17.21)(vue@3.3.4)
'@sanqi377/arco-vue-icon-picker':
specifier: ^1.0.7
version: 1.0.7(vue@3.3.4)
@@ -224,8 +224,8 @@ packages:
color: 3.2.1
dev: false
/@arco-design/web-vue@2.51.1(vue@3.3.4):
resolution: {integrity: sha512-YsNF1hrl700rshZPrJ7haNEN6pc3dQwpLbYzpE49jrLdF0mALGU9aP2A8q0FNPH2Rt4rxvxkw1o0gsL7my8H+Q==}
/@arco-design/web-vue@2.53.0(vue@3.3.4):
resolution: {integrity: sha512-IQtxtIJNmH038EdMecAdeLVayr4r+Txnk51RIVAz28PpdEdjf4J0wnLyirDb41YB0fV7eubZOFNodq5uTYqPTA==}
peerDependencies:
vue: ^3.1.0
dependencies:
@@ -720,7 +720,7 @@ packages:
postcss-selector-parser: 6.0.13
dev: true
/@dangojs/a-query-header@0.0.31(@arco-design/web-vue@2.51.1)(@dangojs/digitforce-ui-utils@0.0.9)(lodash@4.17.21)(vue@3.3.4):
/@dangojs/a-query-header@0.0.31(@arco-design/web-vue@2.53.0)(@dangojs/digitforce-ui-utils@0.0.9)(lodash@4.17.21)(vue@3.3.4):
resolution: {integrity: sha512-hXLAU8oIQ1ULDY+qXKoKstqo3/m4HTjXcxNaFbPatTVMHwox4tBAIc5Nox1crvlWkNagUyk7PpnQuzLH73j9AQ==}
peerDependencies:
'@arco-design/web-vue': ^2.47.1
@@ -728,7 +728,7 @@ packages:
lodash: ^4.17.21
vue: ^3.3.2
dependencies:
'@arco-design/web-vue': 2.51.1(vue@3.3.4)
'@arco-design/web-vue': 2.53.0(vue@3.3.4)
'@dangojs/digitforce-ui-utils': 0.0.9
lodash: 4.17.21
vue: 3.3.4

View File

@@ -90,6 +90,10 @@ body {
}
}
.arco-trigger-menu {
user-select: none;
}
.arco-dropdown-option-content {
user-select: none;
}

View File

@@ -6,6 +6,7 @@
<!-- FIXME -->
<!-- LOGO -->
<img alt="logo"
draggable="false"
src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/dfdba5317c0c20ce20e64fac803d52bc.svg~tplv-49unhts6dw-image.image" />
<!-- 标头 -->
<a-typography-title :heading="5"
@@ -161,8 +162,9 @@
<li>
<a-dropdown trigger="click">
<!-- 头像 -->
<a-avatar :size="32"
:style="{ cursor: 'pointer', backgroundColor: '#3370ff' }">
<a-avatar draggable="false"
:size="32"
:style="{ cursor: 'pointer', backgroundColor: '#3370ff', userSelect: 'none' }">
{{ nickname }}
</a-avatar>
<template #content>
@@ -316,6 +318,7 @@
}
.left-side {
user-select: none;
display: flex;
align-items: center;
padding-left: 20px;
@@ -374,5 +377,4 @@
display: flex;
justify-content: flex-end;
}
</style>

View File

@@ -37,7 +37,14 @@
<TabBar v-if="appStore.tabBar" />
<!-- 页面 -->
<a-layout-content>
<PageLayout />
<!-- 水印 -->
<a-watermark :grayscale="true"
:alpha=".6"
:z-index="9999"
style="width: 100%; height: 100%;"
:content="userStore.username || ''">
<PageLayout />
</a-watermark>
</a-layout-content>
<!-- 页脚 -->
<Footer v-if="footer" />
@@ -49,7 +56,7 @@
<script lang="ts" setup>
import { computed, onMounted, provide, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useAppStore } from '@/store';
import { useAppStore, useUserStore } from '@/store';
import Menu from '@/components/system/menu/tree/index.vue';
import NavBar from '@/components/app/navbar/index.vue';
import Footer from '@/components/app/footer/index.vue';
@@ -60,6 +67,7 @@
const isInit = ref(false);
const appStore = useAppStore();
const userStore = useUserStore();
const router = useRouter();
const route = useRoute();
useResponsive(true);

View File

@@ -21,7 +21,7 @@
<!-- 提示信息 -->
<a-alert class="alert-wrapper" :show-icon="false">
<span v-if="currentRole">
当前选择的角色为 <span class="span-blue mr4">{{ currentRole.text }}</span>
当前选择的角色为 <span class="span-blue mr4">{{ currentRole?.text }}</span>
<span class="span-blue ml4" v-if="currentRole.code === AdminRoleCode">管理员拥有全部权限, 无需配置</span>
</span>
</a-alert>
@@ -41,23 +41,12 @@
:draggable="false"
:loading="loading"
@loading="setLoading"
@select-node="fetchGroupHost"
@select-node="e => selectedGroup = e"
@update:checked-keys="updateCheckedGroups" />
</div>
<!-- 主机列表 -->
<div class="group-main-hosts">
<a-list size="smail" :title="'组内数据'" :split="false">
<a-list-item v-for="idx in 4" :key="idx">
<a-list-item-meta
title="host"
>
<template #avatar>
<icon-desktop />
</template>
</a-list-item-meta>
</a-list-item>
</a-list>
</div>
<host-list class="group-main-hosts"
:group="selectedGroup" />
</div>
</div>
</a-spin>
@@ -71,13 +60,15 @@
<script lang="ts" setup>
import type { TabRouterItem } from '@/components/view/tab-router/types';
import type { TreeNodeData } from '@arco-design/web-vue';
import { ref, onMounted, watch } from 'vue';
import { useCacheStore } from '@/store';
import useLoading from '@/hooks/loading';
import { getAuthorizedHostGroup, grantHostGroup } from '@/api/asset/host-group';
import { AdminRoleCode } from '@/types/const';
import HostGroupTree from './host-group-tree.vue';
import { Message } from '@arco-design/web-vue';
import HostGroupTree from './host-group-tree.vue';
import HostList from './host-list.vue';
const { loading, setLoading } = useLoading();
const cacheStore = useCacheStore();
@@ -87,6 +78,7 @@
const rolesRouter = ref<Array<TabRouterItem>>([]);
const authorizedGroups = ref<Array<number>>([]);
const checkedGroups = ref<Array<number>>([]);
const selectedGroup = ref<TreeNodeData>({});
// 监听角色变更 获取授权列表
watch(roleId, async (value) => {
@@ -107,10 +99,6 @@
}
});
// 加载组内数据
const fetchGroupHost = () => {
};
// 选择分组
const updateCheckedGroups = (e: Array<number>) => {
checkedGroups.value = e;
@@ -141,6 +129,7 @@
};
});
});
</script>
<style lang="less" scoped>
@@ -173,7 +162,7 @@
align-items: center;
.alert-wrapper {
height: 32px;
padding: 4px 16px;
}
.save-button {
@@ -185,28 +174,19 @@
display: flex;
&-tree {
width: calc(100% - 240px - 16px);
width: calc(60% - 16px);
margin-right: 16px;
}
&-hosts {
width: 240px;
width: 40%;
}
}
}
}
:deep(.tab-item) {
margin-left: 0;
}
:deep(.arco-avatar-image) {
font-size: 24px;
display: flex;
align-items: center;
justify-content: center;
color: #FFF;
background: rgb(var(--blue-6));
}
</style>

View File

@@ -1,7 +1,55 @@
<template>
<div>
host-group-view-user-grant
</div>
<a-spin :loading="loading" class="grant-container">
<!-- 左侧用户列表 -->
<div class="user-container">
<!-- 用户列表 -->
<tab-router v-if="usersRouter.length"
class="user-router"
v-model="userId"
:items="usersRouter" />
<!-- 暂无数据 -->
<a-empty v-else class="user-empty">
<div slot="description">
暂无用户数据
</div>
</a-empty>
</div>
<!-- 右侧菜单列表 -->
<div class="group-container">
<!-- 顶部 -->
<div class="group-header">
<!-- 提示信息 -->
<a-alert class="alert-wrapper" :show-icon="false">
<span v-if="currentUser">
当前选择的用户为 <span class="span-blue mr4">{{ currentUser?.text }}</span>
<span class="ml4">若当前选择的用户角色包含管理员则无需配置 (管理员拥有全部权限)</span>
</span>
</a-alert>
<!-- 保存 -->
<a-button class="save-button"
type="primary"
@click="save">
保存
</a-button>
</div>
<!-- 主题部分 -->
<div class="group-main">
<!-- 菜单 -->
<div class="group-main-tree">
<host-group-tree :checkable="true"
:checked-keys="checkedGroups"
:draggable="false"
:loading="loading"
@loading="setLoading"
@select-node="e => selectedGroup = e"
@update:checked-keys="updateCheckedGroups" />
</div>
<!-- 主机列表 -->
<host-list class="group-main-hosts"
:group="selectedGroup" />
</div>
</div>
</a-spin>
</template>
<script lang="ts">
@@ -11,9 +59,133 @@
</script>
<script lang="ts" setup>
import type { TabRouterItem } from '@/components/view/tab-router/types';
import type { TreeNodeData } from '@arco-design/web-vue';
import { ref, onMounted, watch } from 'vue';
import { useCacheStore } from '@/store';
import useLoading from '@/hooks/loading';
import { getAuthorizedHostGroup, grantHostGroup } from '@/api/asset/host-group';
import { Message } from '@arco-design/web-vue';
import HostGroupTree from './host-group-tree.vue';
import HostList from './host-list.vue';
const { loading, setLoading } = useLoading();
const cacheStore = useCacheStore();
const userId = ref();
const currentUser = ref();
const usersRouter = ref<Array<TabRouterItem>>([]);
const authorizedGroups = ref<Array<number>>([]);
const checkedGroups = ref<Array<number>>([]);
const selectedGroup = ref<TreeNodeData>({});
// 监听用户变更 获取授权列表
watch(userId, async (value) => {
if (!value) {
return;
}
currentUser.value = usersRouter.value.find(s => s.key === value);
setLoading(true);
try {
const { data } = await getAuthorizedHostGroup({
userId: userId.value
});
authorizedGroups.value = data;
checkedGroups.value = data;
} catch (e) {
} finally {
setLoading(false);
}
});
// 选择分组
const updateCheckedGroups = (e: Array<number>) => {
checkedGroups.value = e;
};
// 保存
const save = async () => {
setLoading(true);
try {
await grantHostGroup({
userId: userId.value,
groupIdList: checkedGroups.value
});
Message.success('保存成功');
} catch (e) {
} finally {
setLoading(false);
}
};
// 加载主机
onMounted(() => {
usersRouter.value = cacheStore.users.map(s => {
return {
key: s.id,
text: `${s.nickname} (${s.username})`
};
});
});
</script>
<style lang="less" scoped>
.grant-container {
width: 100%;
padding: 16px;
display: flex;
.user-container {
margin-right: 16px;
.user-router {
height: 100%;
min-width: max-content;
border-right: 1px var(--color-neutral-3) solid;
}
.user-empty {
width: 198px;
}
}
.group-container {
width: 100%;
.group-header {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
align-items: center;
.alert-wrapper {
padding: 4px 16px;
}
.save-button {
width: 60px;
margin-left: 16px;
}
}
.group-main {
display: flex;
&-tree {
width: calc(60% - 16px);
margin-right: 16px;
}
&-hosts {
width: 40%;
}
}
}
}
:deep(.tab-item) {
margin-left: 0;
}
</style>

View File

@@ -0,0 +1,113 @@
<template>
<a-list size="small"
:hoverable="true"
:data="selectedGroupHosts"
:loading="loading">
<!-- 表头 -->
<template #header>
<span class="hosts-header-title">组内数据</span>
<span class="span-blue">{{ props.group?.title }}</span>
</template>
<!-- 空数据 -->
<template #empty>
<span class="host-list-empty">当前分组未配置主机</span>
</template>
<!-- 数据 -->
<template #item="{ item }">
<a-list-item :title="`${item.name}(${item.code}) - ${ item.address}`">
<icon-desktop class="host-list-icon" />
<span>{{ `${item.name}(${item.code}) - ` }}</span>
<span class="span-blue">{{ item.address }}</span>
</a-list-item>
</template>
</a-list>
</template>
<script lang="ts">
export default {
name: 'host-list'
};
</script>
<script lang="ts" setup>
import type { TreeNodeData } from '@arco-design/web-vue';
import type { HostQueryResponse } from '@/api/asset/host';
import type { PropType } from 'vue';
import useLoading from '@/hooks/loading';
import { useCacheStore } from '@/store';
import { ref, watch } from 'vue';
import { getHostGroupRelList } from '@/api/asset/host-group';
const props = defineProps({
group: {
type: Object as PropType<TreeNodeData>,
default: () => {
return {};
}
}
});
const cacheStore = useCacheStore();
const { loading, setLoading } = useLoading();
const selectedGroupHosts = ref<Array<HostQueryResponse>>([]);
// 监听分组变化 加载组内数据
watch(() => props.group?.key, async (groupId) => {
if (!groupId) {
return;
}
// 加载组内数据
try {
setLoading(true);
const { data } = await getHostGroupRelList(groupId as number);
selectedGroupHosts.value = data.map(s => cacheStore.hosts.find(h => h.id === s) as HostQueryResponse)
.filter(Boolean);
} catch (e) {
} finally {
setLoading(false);
}
});
</script>
<style lang="less" scoped>
.hosts-header-title {
&:after {
content: '-';
margin: 0 8px;
}
}
.host-list-empty {
padding: 24px;
text-align: center;
color: var(--color-text-2);
display: block;
}
:deep(.arco-list-wrapper .arco-list-spin) {
height: unset;
}
:deep(.arco-list-item-content) {
display: flex;
align-items: center;
color: var(--color-text-1);
overflow: hidden;
word-break: keep-all;
white-space: pre;
.host-list-icon {
font-size: 24px;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
color: #FFF;
background: rgb(var(--blue-6));
margin-right: 8px;
border-radius: 4px;
}
}
</style>

View File

@@ -161,7 +161,7 @@
align-items: center;
.alert-wrapper {
height: 32px;
padding: 4px 16px;
}
.save-button {

View File

@@ -129,6 +129,10 @@
background: #FFF;
}
:deep(.arco-tabs-tab-title){
user-select: none;
}
:deep(.arco-tabs-content) {
padding-top: 0;
}