🔖 项目重命名.
This commit is contained in:
151
orion-visor-ui/src/views/asset/grant/components/grant-layout.vue
Normal file
151
orion-visor-ui/src/views/asset/grant/components/grant-layout.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<a-spin :loading="loading" class="grant-container">
|
||||
<!-- 角色列表 -->
|
||||
<router-roles v-if="type === GrantType.ROLE"
|
||||
outer-class="router-wrapper"
|
||||
v-model="subjectId"
|
||||
@change="fetchAuthorizedData" />
|
||||
<!-- 角色列表 -->
|
||||
<router-users v-else-if="type === GrantType.USER"
|
||||
outer-class="router-wrapper"
|
||||
v-model="subjectId"
|
||||
@change="fetchAuthorizedData" />
|
||||
<!-- 数据列表 -->
|
||||
<div class="data-container">
|
||||
<!-- 顶部 -->
|
||||
<div class="data-header">
|
||||
<!-- 提示信息 -->
|
||||
<a-alert class="alert-wrapper" :show-icon="false">
|
||||
<span class="alert-message" v-if="currentSubject">
|
||||
<!-- 角色提示信息 -->
|
||||
<template v-if="type === GrantType.ROLE">
|
||||
当前选择的角色为 <span class="span-blue mr4">{{ currentSubject.text }}</span>
|
||||
</template>
|
||||
<!-- 用户提示信息 -->
|
||||
<template v-else-if="type === GrantType.USER">
|
||||
当前选择的用户为 <span class="span-blue mr4">{{ currentSubject.text }}</span>
|
||||
</template>
|
||||
</span>
|
||||
</a-alert>
|
||||
<!-- 操作按钮 -->
|
||||
<a-space>
|
||||
<!-- 全选 -->
|
||||
<a-button @click="emits('selectAll')">
|
||||
全选
|
||||
<template #icon>
|
||||
<icon-select-all />
|
||||
</template>
|
||||
</a-button>
|
||||
<!-- 反选 -->
|
||||
<a-button @click="emits('reverse')">
|
||||
反选
|
||||
<template #icon>
|
||||
<icon-list />
|
||||
</template>
|
||||
</a-button>
|
||||
<!-- 授权 -->
|
||||
<a-button type="primary" @click="doGrant">
|
||||
授权
|
||||
<template #icon>
|
||||
<icon-safe />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
<!-- 主体部分 -->
|
||||
<div class="data-main">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'grantLayout'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TabRouterItem } from '@/components/view/tab-router/types';
|
||||
import type { AssetAuthorizedDataQueryRequest, AssetDataGrantRequest } from '@/api/asset/asset-data-grant';
|
||||
import { ref } from 'vue';
|
||||
import { GrantType } from '../types/const';
|
||||
import RouterRoles from './router-roles.vue';
|
||||
import RouterUsers from './router-users.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
type: string;
|
||||
loading: boolean;
|
||||
}>();
|
||||
const emits = defineEmits(['fetch', 'grant', 'selectAll', 'reverse']);
|
||||
|
||||
const subjectId = ref();
|
||||
const currentSubject = ref();
|
||||
|
||||
// 获取授权列表
|
||||
const fetchAuthorizedData = async (id: number, subject: TabRouterItem) => {
|
||||
currentSubject.value = subject;
|
||||
if (props.type === GrantType.ROLE) {
|
||||
emits('fetch', { roleId: id } as AssetAuthorizedDataQueryRequest);
|
||||
} else if (props.type === GrantType.USER) {
|
||||
emits('fetch', { userId: id } as AssetAuthorizedDataQueryRequest);
|
||||
}
|
||||
};
|
||||
|
||||
// 授权
|
||||
const doGrant = async () => {
|
||||
if (props.type === GrantType.ROLE) {
|
||||
emits('grant', { roleId: subjectId.value } as AssetDataGrantRequest);
|
||||
} else if (props.type === GrantType.USER) {
|
||||
emits('grant', { userId: subjectId.value } as AssetDataGrantRequest);
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.grant-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
padding: 0 12px 12px 0;
|
||||
position: absolute;
|
||||
|
||||
.router-wrapper {
|
||||
margin-right: 16px;
|
||||
border-right: 1px var(--color-neutral-3) solid;
|
||||
}
|
||||
|
||||
.data-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.data-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
align-items: center;
|
||||
|
||||
.alert-wrapper {
|
||||
padding: 4px 16px;
|
||||
margin-right: 12px;
|
||||
|
||||
.alert-message {
|
||||
display: block;
|
||||
height: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.data-main {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: calc(100% - 48px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<grant-layout :type="type"
|
||||
:loading="loading"
|
||||
@fetch="fetchAuthorizedData"
|
||||
@grant="doGrant"
|
||||
@select-all="selectAll"
|
||||
@reverse="reverseSelect">
|
||||
<!-- 分组 -->
|
||||
<host-group-tree v-model:checked-keys="checkedGroups"
|
||||
ref="tree"
|
||||
outer-class="group-main-tree"
|
||||
:checkable="true"
|
||||
:editable="false"
|
||||
:loading="loading"
|
||||
@set-loading="setLoading"
|
||||
@selected-node="(e) => selectedGroup = e"
|
||||
@on-selected="clickGroup" />
|
||||
<!-- 主机列表 -->
|
||||
<host-list class="group-main-hosts sticky-list" :group="selectedGroup" />
|
||||
</grant-layout>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'hostGroupGrant'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TreeNodeData } from '@arco-design/web-vue';
|
||||
import type { AssetAuthorizedDataQueryRequest, AssetDataGrantRequest } from '@/api/asset/asset-data-grant';
|
||||
import { ref } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { getAuthorizedHostGroup, grantHostGroup } from '@/api/asset/asset-data-grant';
|
||||
import { useCacheStore } from '@/store';
|
||||
import { flatNodeKeys } from '@/utils/tree';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import HostGroupTree from '@/components/asset/host-group/tree/index.vue';
|
||||
import HostList from './host-list.vue';
|
||||
import GrantLayout from './grant-layout.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
type: string;
|
||||
}>();
|
||||
|
||||
const cacheStore = useCacheStore();
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
const tree = ref();
|
||||
const authorizedGroups = ref<Array<number>>([]);
|
||||
const checkedGroups = ref<Array<number>>([]);
|
||||
const selectedGroup = ref<TreeNodeData>({});
|
||||
|
||||
// 点击分组
|
||||
const clickGroup = (groups: Array<number>) => {
|
||||
if (groups && groups.length) {
|
||||
const group = groups[0];
|
||||
const index = checkedGroups.value.findIndex((s) => s === group);
|
||||
if (index === -1) {
|
||||
checkedGroups.value.push(group);
|
||||
} else {
|
||||
checkedGroups.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 获取授权列表
|
||||
const fetchAuthorizedData = async (request: AssetAuthorizedDataQueryRequest) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data } = await getAuthorizedHostGroup(request);
|
||||
authorizedGroups.value = data;
|
||||
checkedGroups.value = data;
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 授权
|
||||
const doGrant = async (request: AssetDataGrantRequest) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 执行授权
|
||||
await grantHostGroup({
|
||||
...request,
|
||||
idList: checkedGroups.value
|
||||
});
|
||||
Message.success('授权成功');
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
// 查询数据
|
||||
await fetchAuthorizedData(request);
|
||||
};
|
||||
|
||||
// 全选
|
||||
const selectAll = async () => {
|
||||
// 从缓存中查询全部分组
|
||||
const groups = await cacheStore.loadHostGroups();
|
||||
const groupKeys: number[] = [];
|
||||
flatNodeKeys(groups, groupKeys);
|
||||
checkedGroups.value = groupKeys;
|
||||
};
|
||||
|
||||
// 反选
|
||||
const reverseSelect = async () => {
|
||||
// 从缓存中查询全部分组
|
||||
const groups = await cacheStore.loadHostGroups();
|
||||
const groupKeys: number[] = [];
|
||||
flatNodeKeys(groups, groupKeys);
|
||||
checkedGroups.value = groupKeys.filter(s => !checkedGroups.value.includes(s));
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.group-main-tree {
|
||||
width: calc(60% - 16px);
|
||||
height: 100%;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.group-main-hosts {
|
||||
width: 40%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<grant-layout :type="type"
|
||||
:loading="loading"
|
||||
@fetch="fetchAuthorizedData"
|
||||
@grant="doGrant"
|
||||
@select-all="selectAll"
|
||||
@reverse="reverseSelect">
|
||||
<!-- 主机身份表格 -->
|
||||
<a-table row-key="id"
|
||||
class="host-identity-main-table"
|
||||
:columns="hostIdentityColumns"
|
||||
v-model:selected-keys="selectedKeys"
|
||||
:row-selection="rowSelection"
|
||||
row-class="pointer"
|
||||
:sticky-header="true"
|
||||
:data="hostIdentities"
|
||||
:pagination="false"
|
||||
:bordered="false"
|
||||
@row-click="clickRow">
|
||||
<!-- 类型 -->
|
||||
<template #type="{ record }">
|
||||
<a-tag :color="getDictValue(identityTypeKey, record.type, 'color')">
|
||||
{{ getDictValue(identityTypeKey, record.type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<!-- 秘钥名称 -->
|
||||
<template #keyId="{ record }">
|
||||
<!-- 有秘钥 -->
|
||||
<template v-if="record.keyId && record.type === 'KEY'">
|
||||
<a-tag color="arcoblue" v-if="record.keyId">
|
||||
{{ hostKeys.find(s => s.id === record.keyId)?.name }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<!-- 无秘钥 -->
|
||||
<template v-else>
|
||||
<span>-</span>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</grant-layout>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'hostIdentityGrant'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TableData } from '@arco-design/web-vue/es/table/interface';
|
||||
import type { AssetAuthorizedDataQueryRequest, AssetDataGrantRequest } from '@/api/asset/asset-data-grant';
|
||||
import type { HostIdentityQueryResponse } from '@/api/asset/host-identity';
|
||||
import type { HostKeyQueryResponse } from '@/api/asset/host-key';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { useRowSelection } from '@/types/table';
|
||||
import { getAuthorizedHostIdentity, grantHostIdentity } from '@/api/asset/asset-data-grant';
|
||||
import { useCacheStore, useDictStore } from '@/store';
|
||||
import { hostIdentityColumns } from '../types/table.columns';
|
||||
import { identityTypeKey } from '../types/const';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import GrantLayout from './grant-layout.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
type: string;
|
||||
}>();
|
||||
|
||||
const cacheStore = useCacheStore();
|
||||
const rowSelection = useRowSelection();
|
||||
const { getDictValue } = useDictStore();
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
const selectedKeys = ref<Array<number>>([]);
|
||||
const hostIdentities = ref<Array<HostIdentityQueryResponse>>([]);
|
||||
const hostKeys = ref<Array<HostKeyQueryResponse>>([]);
|
||||
|
||||
// 获取授权列表
|
||||
const fetchAuthorizedData = async (request: AssetAuthorizedDataQueryRequest) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data } = await getAuthorizedHostIdentity(request);
|
||||
selectedKeys.value = data;
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 授权
|
||||
const doGrant = async (request: AssetDataGrantRequest) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 执行授权
|
||||
await grantHostIdentity({
|
||||
...request,
|
||||
idList: selectedKeys.value
|
||||
});
|
||||
Message.success('授权成功');
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
// 查询数据
|
||||
await fetchAuthorizedData(request);
|
||||
};
|
||||
|
||||
// 全选
|
||||
const selectAll = () => {
|
||||
selectedKeys.value = hostIdentities.value.map(s => s.id);
|
||||
};
|
||||
|
||||
// 反选
|
||||
const reverseSelect = () => {
|
||||
selectedKeys.value = hostIdentities.value.map(s => s.id)
|
||||
.filter(s => !selectedKeys.value.includes(s));
|
||||
};
|
||||
|
||||
// 点击行
|
||||
const clickRow = ({ id }: TableData) => {
|
||||
const index = selectedKeys.value.indexOf(id);
|
||||
if (index === -1) {
|
||||
selectedKeys.value.push(id);
|
||||
} else {
|
||||
selectedKeys.value.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化身份数据
|
||||
onMounted(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 加载主机身份
|
||||
hostIdentities.value = await cacheStore.loadHostIdentities();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化秘钥数据
|
||||
onMounted(async () => {
|
||||
// 加载主机秘钥
|
||||
hostKeys.value = await cacheStore.loadHostKeys();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<grant-layout :type="type"
|
||||
:loading="loading"
|
||||
@fetch="fetchAuthorizedData"
|
||||
@grant="doGrant"
|
||||
@select-all="selectAll"
|
||||
@reverse="reverseSelect">
|
||||
<!-- 主机秘钥表格 -->
|
||||
<a-table row-key="id"
|
||||
class="host-key-main-table"
|
||||
:columns="hostKeyColumns"
|
||||
v-model:selected-keys="selectedKeys"
|
||||
:row-selection="rowSelection"
|
||||
row-class="pointer"
|
||||
:sticky-header="true"
|
||||
:data="hostKeys"
|
||||
:pagination="false"
|
||||
:bordered="false"
|
||||
@row-click="clickRow" />
|
||||
</grant-layout>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'hostKeyGrant'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TableData } from '@arco-design/web-vue/es/table/interface';
|
||||
import type { AssetAuthorizedDataQueryRequest, AssetDataGrantRequest } from '@/api/asset/asset-data-grant';
|
||||
import type { HostKeyQueryResponse } from '@/api/asset/host-key';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { getAuthorizedHostKey, grantHostKey } from '@/api/asset/asset-data-grant';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { useRowSelection } from '@/types/table';
|
||||
import { useCacheStore } from '@/store';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { hostKeyColumns } from '../types/table.columns';
|
||||
import GrantLayout from './grant-layout.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
type: string;
|
||||
}>();
|
||||
|
||||
const cacheStore = useCacheStore();
|
||||
const rowSelection = useRowSelection();
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
const selectedKeys = ref<Array<number>>([]);
|
||||
const hostKeys = ref<Array<HostKeyQueryResponse>>([]);
|
||||
|
||||
// 获取授权列表
|
||||
const fetchAuthorizedData = async (request: AssetAuthorizedDataQueryRequest) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data } = await getAuthorizedHostKey(request);
|
||||
selectedKeys.value = data;
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 授权
|
||||
const doGrant = async (request: AssetDataGrantRequest) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 执行授权
|
||||
await grantHostKey({
|
||||
...request,
|
||||
idList: selectedKeys.value
|
||||
});
|
||||
Message.success('授权成功');
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
// 查询数据
|
||||
await fetchAuthorizedData(request);
|
||||
};
|
||||
|
||||
// 全选
|
||||
const selectAll = () => {
|
||||
selectedKeys.value = hostKeys.value.map(s => s.id);
|
||||
};
|
||||
|
||||
// 反选
|
||||
const reverseSelect = () => {
|
||||
selectedKeys.value = hostKeys.value.map(s => s.id)
|
||||
.filter(s => !selectedKeys.value.includes(s));
|
||||
};
|
||||
|
||||
// 点击行
|
||||
const clickRow = ({ id }: TableData) => {
|
||||
const index = selectedKeys.value.indexOf(id);
|
||||
if (index === -1) {
|
||||
selectedKeys.value.push(id);
|
||||
} else {
|
||||
selectedKeys.value.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化数据
|
||||
onMounted(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
hostKeys.value = await cacheStore.loadHostKeys();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
||||
112
orion-visor-ui/src/views/asset/grant/components/host-list.vue
Normal file
112
orion-visor-ui/src/views/asset/grant/components/host-list.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<a-list size="small"
|
||||
max-height="100%"
|
||||
:hoverable="true"
|
||||
:data="selectedGroupHosts"
|
||||
:loading="loading">
|
||||
<!-- 表头 -->
|
||||
<template #header>
|
||||
<span class="hosts-header-title">组内数据</span>
|
||||
<span class="span-blue">{{ group?.title }}</span>
|
||||
</template>
|
||||
<!-- 空数据 -->
|
||||
<template #empty>
|
||||
<span class="host-list-empty">当前分组未配置主机</span>
|
||||
</template>
|
||||
<!-- 数据 -->
|
||||
<template #item="{ item }">
|
||||
<a-tooltip :content="`${item.name} - ${item.address}`">
|
||||
<a-list-item>
|
||||
<icon-desktop class="host-list-icon" />
|
||||
<span>{{ `${item.name} - ` }}</span>
|
||||
<span class="span-blue">{{ item.address }}</span>
|
||||
</a-list-item>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</a-list>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'hostList'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TreeNodeData } from '@arco-design/web-vue';
|
||||
import type { HostQueryResponse } from '@/api/asset/host';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { useCacheStore } from '@/store';
|
||||
import { ref, watch } from 'vue';
|
||||
import { getHostGroupRelList } from '@/api/asset/host-group';
|
||||
|
||||
const props = defineProps<Partial<{
|
||||
group: TreeNodeData;
|
||||
}>>();
|
||||
|
||||
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);
|
||||
const hosts = await cacheStore.loadHosts();
|
||||
selectedGroupHosts.value = data.map(s => 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: 16px 24px;
|
||||
text-align: center;
|
||||
color: var(--color-text-2);
|
||||
display: block;
|
||||
}
|
||||
|
||||
:deep(.arco-scrollbar) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
: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: var(--color-white);
|
||||
background: rgb(var(--blue-6));
|
||||
margin-right: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
109
orion-visor-ui/src/views/asset/grant/components/router-roles.vue
Normal file
109
orion-visor-ui/src/views/asset/grant/components/router-roles.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<a-scrollbar>
|
||||
<div class="role-container">
|
||||
<!-- 角色列表 -->
|
||||
<tab-router v-if="rolesRouter.length"
|
||||
class="role-router"
|
||||
v-model="value"
|
||||
:items="rolesRouter"
|
||||
@change="(key: number, item: any) => emits('change', key, item)" />
|
||||
<!-- 加载中 -->
|
||||
<a-skeleton v-else-if="loading"
|
||||
class="skeleton-wrapper"
|
||||
:animation="true">
|
||||
<a-skeleton-line :rows="4" />
|
||||
</a-skeleton>
|
||||
<!-- 暂无数据 -->
|
||||
<a-empty v-else class="role-empty">
|
||||
<div slot="description">
|
||||
暂无角色数据
|
||||
</div>
|
||||
</a-empty>
|
||||
</div>
|
||||
</a-scrollbar>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'routerRoles'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TabRouterItem } from '@/components/view/tab-router/types';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useCacheStore } from '@/store';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
|
||||
const props = defineProps<Partial<{
|
||||
modelValue: number;
|
||||
}>>();
|
||||
|
||||
const emits = defineEmits(['update:modelValue', 'change']);
|
||||
|
||||
const { loading, setLoading } = useLoading();
|
||||
const cacheStore = useCacheStore();
|
||||
|
||||
const rolesRouter = ref<Array<TabRouterItem>>([]);
|
||||
|
||||
const value = computed({
|
||||
get() {
|
||||
return props.modelValue;
|
||||
},
|
||||
set(e) {
|
||||
emits('update:modelValue', e);
|
||||
}
|
||||
});
|
||||
|
||||
// 加载角色列表
|
||||
const loadRoleList = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const roles = await cacheStore.loadRoles();
|
||||
rolesRouter.value = roles.map(s => {
|
||||
return {
|
||||
key: s.id,
|
||||
text: `${s.name} (${s.code})`,
|
||||
code: s.code
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
Message.error('角色列表加载失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载角色列表
|
||||
onMounted(() => {
|
||||
loadRoleList();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@width: 198px;
|
||||
@height: 198px;
|
||||
|
||||
:deep(.arco-scrollbar-container) {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.role-container {
|
||||
overflow: hidden;
|
||||
|
||||
.role-router {
|
||||
height: 100%;
|
||||
min-width: @width;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.skeleton-wrapper, .role-empty {
|
||||
width: @width;
|
||||
height: 200px;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
108
orion-visor-ui/src/views/asset/grant/components/router-users.vue
Normal file
108
orion-visor-ui/src/views/asset/grant/components/router-users.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<a-scrollbar>
|
||||
<div class="user-container">
|
||||
<!-- 用户列表 -->
|
||||
<tab-router v-if="usersRouter.length"
|
||||
class="user-router"
|
||||
v-model="value"
|
||||
:items="usersRouter"
|
||||
@change="(key: number, item: any) => emits('change', key, item)" />
|
||||
<!-- 加载中 -->
|
||||
<a-skeleton v-else-if="loading"
|
||||
class="skeleton-wrapper"
|
||||
:animation="true">
|
||||
<a-skeleton-line :rows="4" />
|
||||
</a-skeleton>
|
||||
<!-- 暂无数据 -->
|
||||
<a-empty v-else class="user-empty">
|
||||
<div slot="description">
|
||||
暂无用户数据
|
||||
</div>
|
||||
</a-empty>
|
||||
</div>
|
||||
</a-scrollbar>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'routerUsers'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TabRouterItem } from '@/components/view/tab-router/types';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useCacheStore } from '@/store';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
|
||||
const props = defineProps<Partial<{
|
||||
modelValue: number;
|
||||
}>>();
|
||||
|
||||
const emits = defineEmits(['update:modelValue', 'change']);
|
||||
|
||||
const { loading, setLoading } = useLoading();
|
||||
const cacheStore = useCacheStore();
|
||||
|
||||
const usersRouter = ref<Array<TabRouterItem>>([]);
|
||||
|
||||
const value = computed({
|
||||
get() {
|
||||
return props.modelValue;
|
||||
},
|
||||
set(e) {
|
||||
emits('update:modelValue', e);
|
||||
}
|
||||
});
|
||||
|
||||
// 加载用户列表
|
||||
const loadUserList = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const users = await cacheStore.loadUsers();
|
||||
usersRouter.value = users.map(s => {
|
||||
return {
|
||||
key: s.id,
|
||||
text: `${s.nickname} (${s.username})`
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
Message.error('用户列表加载失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载用户列表
|
||||
onMounted(() => {
|
||||
loadUserList();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@width: 198px;
|
||||
@height: 198px;
|
||||
|
||||
:deep(.arco-scrollbar-container) {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.user-container {
|
||||
overflow: hidden;
|
||||
|
||||
.user-router {
|
||||
height: 100%;
|
||||
min-width: @width;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.skeleton-wrapper, .user-empty {
|
||||
width: @width;
|
||||
height: @height;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
92
orion-visor-ui/src/views/asset/grant/index.vue
Normal file
92
orion-visor-ui/src/views/asset/grant/index.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<div class="view-container">
|
||||
<a-tabs v-model:active-key="activeKey"
|
||||
class="tabs-container simple-card"
|
||||
size="large"
|
||||
:justify="true"
|
||||
:lazy-load="true">
|
||||
<a-tab-pane v-for="tab in GrantTabs"
|
||||
v-permission="tab.permission"
|
||||
:key="tab.key">
|
||||
<template #title>
|
||||
<component :is="tab.icon" />
|
||||
{{ tab.title }}
|
||||
</template>
|
||||
<component :is="tab.component" :type="tab.type" />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'assetGrant'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onBeforeMount, onUnmounted, ref } from 'vue';
|
||||
import { useCacheStore, useDictStore } from '@/store';
|
||||
import { GrantTabs, dictKeys } from './types/const';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
const cacheStore = useCacheStore();
|
||||
|
||||
const activeKey = ref();
|
||||
|
||||
// 加载字典项
|
||||
onBeforeMount(async () => {
|
||||
const dictStore = useDictStore();
|
||||
await dictStore.loadKeys(dictKeys);
|
||||
});
|
||||
|
||||
// 跳转到指定页
|
||||
onBeforeMount(() => {
|
||||
const key = route.query.key;
|
||||
if (key) {
|
||||
activeKey.value = Number(key);
|
||||
}
|
||||
});
|
||||
|
||||
// 卸载时清除 cache
|
||||
onUnmounted(() => {
|
||||
cacheStore.reset('users', 'roles', 'hosts', 'hostGroups', 'hostKeys', 'hostIdentities');
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.view-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-pane) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-nav-type-line .arco-tabs-tab) {
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-tab-title) {
|
||||
user-select: none;
|
||||
font-size: 15px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-content) {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
81
orion-visor-ui/src/views/asset/grant/types/const.ts
Normal file
81
orion-visor-ui/src/views/asset/grant/types/const.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import HostGroupGrant from '../components/host-group-grant.vue';
|
||||
import HostKeyGrant from '../components/host-key-grant.vue';
|
||||
import HostIdentityGrant from '../components/host-identity-grant.vue';
|
||||
|
||||
// 路由
|
||||
export const GrantRouteName = 'assetGrant';
|
||||
|
||||
// 授权 key
|
||||
export const GrantKey = {
|
||||
// 主机分组-角色
|
||||
HOST_GROUP_ROLE: 1,
|
||||
// 主机分组-用户
|
||||
HOST_GROUP_USER: 2,
|
||||
// 主机秘钥-角色
|
||||
HOST_KEY_ROLE: 3,
|
||||
// 主机秘钥-用户
|
||||
HOST_KEY_USER: 4,
|
||||
// 主机身份-角色
|
||||
HOST_IDENTITY_ROLE: 5,
|
||||
// 主机身份-用户
|
||||
HOST_IDENTITY_USER: 6,
|
||||
};
|
||||
|
||||
// 授权类型
|
||||
export const GrantType = {
|
||||
ROLE: 'role',
|
||||
USER: 'user',
|
||||
};
|
||||
|
||||
// 授权 tab 组件
|
||||
export const GrantTabs = [
|
||||
{
|
||||
key: GrantKey.HOST_GROUP_ROLE,
|
||||
permission: ['asset:host-group:grant'],
|
||||
title: '主机分组授权 - 角色',
|
||||
icon: 'icon-desktop',
|
||||
type: GrantType.ROLE,
|
||||
component: HostGroupGrant
|
||||
}, {
|
||||
key: GrantKey.HOST_GROUP_USER,
|
||||
permission: ['asset:host-group:grant'],
|
||||
title: '主机分组授权 - 用户',
|
||||
icon: 'icon-desktop',
|
||||
type: GrantType.USER,
|
||||
component: HostGroupGrant
|
||||
}, {
|
||||
key: GrantKey.HOST_KEY_ROLE,
|
||||
permission: ['asset:host-key:grant'],
|
||||
title: '主机秘钥授权 - 角色',
|
||||
icon: 'icon-lock',
|
||||
type: GrantType.ROLE,
|
||||
component: HostKeyGrant
|
||||
}, {
|
||||
key: GrantKey.HOST_KEY_USER,
|
||||
permission: ['asset:host-key:grant'],
|
||||
title: '主机秘钥授权 - 用户',
|
||||
icon: 'icon-lock',
|
||||
type: GrantType.USER,
|
||||
component: HostKeyGrant
|
||||
}, {
|
||||
key: GrantKey.HOST_IDENTITY_ROLE,
|
||||
permission: ['asset:host-identity:grant'],
|
||||
title: '主机身份授权 - 角色',
|
||||
icon: 'icon-idcard',
|
||||
type: GrantType.ROLE,
|
||||
component: HostIdentityGrant
|
||||
}, {
|
||||
key: GrantKey.HOST_IDENTITY_USER,
|
||||
permission: ['asset:host-identity:grant'],
|
||||
title: '主机身份授权 - 用户',
|
||||
icon: 'icon-idcard',
|
||||
type: GrantType.USER,
|
||||
component: HostIdentityGrant
|
||||
},
|
||||
];
|
||||
|
||||
// 身份类型 字典项
|
||||
export const identityTypeKey = 'hostIdentityType';
|
||||
|
||||
// 加载的字典值
|
||||
export const dictKeys = [identityTypeKey];
|
||||
80
orion-visor-ui/src/views/asset/grant/types/table.columns.ts
Normal file
80
orion-visor-ui/src/views/asset/grant/types/table.columns.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface';
|
||||
import { dateFormat } from '@/utils';
|
||||
|
||||
// 主机秘钥列
|
||||
export const hostKeyColumns = [
|
||||
{
|
||||
title: 'id',
|
||||
dataIndex: 'id',
|
||||
slotName: 'id',
|
||||
width: 100,
|
||||
align: 'left',
|
||||
fixed: 'left',
|
||||
}, {
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
slotName: 'name',
|
||||
ellipsis: true,
|
||||
tooltip: true
|
||||
}, {
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
slotName: 'createTime',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
render: ({ record }) => {
|
||||
return dateFormat(new Date(record.createTime));
|
||||
},
|
||||
}, {
|
||||
title: '修改时间',
|
||||
dataIndex: 'updateTime',
|
||||
slotName: 'updateTime',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
render: ({ record }) => {
|
||||
return dateFormat(new Date(record.updateTime));
|
||||
},
|
||||
},
|
||||
] as TableColumnData[];
|
||||
|
||||
// 主机身份列
|
||||
export const hostIdentityColumns = [
|
||||
{
|
||||
title: 'id',
|
||||
dataIndex: 'id',
|
||||
slotName: 'id',
|
||||
width: 70,
|
||||
align: 'left',
|
||||
fixed: 'left',
|
||||
}, {
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
slotName: 'name',
|
||||
ellipsis: true,
|
||||
tooltip: true
|
||||
}, {
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
slotName: 'type',
|
||||
width: 98,
|
||||
}, {
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
slotName: 'username',
|
||||
ellipsis: true,
|
||||
tooltip: true
|
||||
}, {
|
||||
title: '主机秘钥',
|
||||
dataIndex: 'keyId',
|
||||
slotName: 'keyId',
|
||||
}, {
|
||||
title: '修改时间',
|
||||
dataIndex: 'updateTime',
|
||||
slotName: 'updateTime',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
render: ({ record }) => {
|
||||
return dateFormat(new Date(record.updateTime));
|
||||
},
|
||||
},
|
||||
] as TableColumnData[];
|
||||
@@ -0,0 +1,273 @@
|
||||
<template>
|
||||
<card-list v-model:searchValue="formModel.searchValue"
|
||||
search-input-placeholder="输入 id / 名称 / 用户名"
|
||||
:loading="loading"
|
||||
:fieldConfig="fieldConfig"
|
||||
:list="list"
|
||||
:pagination="pagination"
|
||||
:card-layout-cols="cardColLayout"
|
||||
:filter-count="filterCount"
|
||||
:add-permission="['asset:host-identity:create']"
|
||||
@add="emits('openAdd')"
|
||||
@reset="reset"
|
||||
@search="fetchCardData"
|
||||
@page-change="fetchCardData">
|
||||
<!-- 左侧操作 -->
|
||||
<template #leftHandle>
|
||||
<!-- 角色授权 -->
|
||||
<a-button v-permission="['asset:host-identity:grant']"
|
||||
class="card-header-icon-wrapper"
|
||||
@click="$router.push({ name: GrantRouteName, query: { key: GrantKey.HOST_IDENTITY_ROLE }})">
|
||||
角色授权
|
||||
<template #icon>
|
||||
<icon-user-group />
|
||||
</template>
|
||||
</a-button>
|
||||
<!-- 用户授权 -->
|
||||
<a-button v-permission="['asset:host-identity:grant']"
|
||||
class="card-header-icon-wrapper"
|
||||
@click="$router.push({ name: GrantRouteName, query: { key: GrantKey.HOST_IDENTITY_USER }})">
|
||||
用户授权
|
||||
<template #icon>
|
||||
<icon-user />
|
||||
</template>
|
||||
</a-button>
|
||||
</template>
|
||||
<!-- 过滤条件 -->
|
||||
<template #filterContent>
|
||||
<a-form :model="formModel"
|
||||
class="card-filter-form"
|
||||
size="small"
|
||||
ref="formRef"
|
||||
label-align="right"
|
||||
:auto-label-width="true"
|
||||
@keyup.enter="() => fetchCardData()">
|
||||
<!-- id -->
|
||||
<a-form-item field="id" label="id">
|
||||
<a-input-number v-model="formModel.id"
|
||||
placeholder="请输入id"
|
||||
allow-clear
|
||||
hide-button />
|
||||
</a-form-item>
|
||||
<!-- 名称 -->
|
||||
<a-form-item field="name" label="名称">
|
||||
<a-input v-model="formModel.name" placeholder="请输入名称" allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 类型 -->
|
||||
<a-form-item field="type" label="类型">
|
||||
<a-select v-model="formModel.type"
|
||||
placeholder="请选择类型"
|
||||
:options="toOptions(identityTypeKey)"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 用户名 -->
|
||||
<a-form-item field="username" label="用户名">
|
||||
<a-input v-model="formModel.username" placeholder="请输入用户名" allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 秘钥 -->
|
||||
<a-form-item field="keyId" label="秘钥">
|
||||
<host-key-selector v-model="formModel.keyId" allow-clear />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
<!-- 标题 -->
|
||||
<template #title="{ record }">
|
||||
{{ record.name }}
|
||||
</template>
|
||||
<!-- 类型 -->
|
||||
<template #type="{ record }">
|
||||
<a-tag :color="getDictValue(identityTypeKey, record.type, 'color')">
|
||||
{{ getDictValue(identityTypeKey, record.type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<!-- 用户名 -->
|
||||
<template #username="{ record }">
|
||||
<span class="span-blue text-copy" @click="copy(record.username)">
|
||||
{{ record.username }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- 秘钥名称 -->
|
||||
<template #keyId="{ record }">
|
||||
<!-- 有秘钥 -->
|
||||
<template v-if="record.keyId && record.type === IdentityType.KEY">
|
||||
<!-- 可查看详情 -->
|
||||
<a-tooltip v-if="hasAnyPermission(['asset:host-key:detail', 'asset:host-key:update'])"
|
||||
content="点击查看详情">
|
||||
<a-tag :checked="true"
|
||||
checkable
|
||||
@click="emits('openKeyView', { id: record.keyId })">
|
||||
{{ record.keyName }}
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
<!-- 不可查看详情 -->
|
||||
<a-tag v-else>
|
||||
{{ record.keyName }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<!-- 无秘钥 -->
|
||||
<template v-else>
|
||||
<span>-</span>
|
||||
</template>
|
||||
</template>
|
||||
<!-- 拓展操作 -->
|
||||
<template #extra="{ record }">
|
||||
<a-space>
|
||||
<!-- 更多操作 -->
|
||||
<a-dropdown trigger="hover">
|
||||
<icon-more class="card-extra-icon" />
|
||||
<template #content>
|
||||
<!-- 修改 -->
|
||||
<a-doption v-permission="['asset:host-identity:update']"
|
||||
@click="emits('openUpdate', record)">
|
||||
<icon-edit />
|
||||
修改
|
||||
</a-doption>
|
||||
<!-- 删除 -->
|
||||
<a-doption v-permission="['asset:host-identity:delete']"
|
||||
class="span-red"
|
||||
@click="deleteRow(record.id)">
|
||||
<icon-delete />
|
||||
删除
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</template>
|
||||
<!-- 右键菜单 -->
|
||||
<template #contextMenu="{ record }">
|
||||
<!-- 修改 -->
|
||||
<a-doption v-permission="['asset:host-identity:update']"
|
||||
@click="emits('openUpdate', record)">
|
||||
<icon-edit />
|
||||
修改
|
||||
</a-doption>
|
||||
<!-- 删除 -->
|
||||
<a-doption v-permission="['asset:host-identity:delete']"
|
||||
class="span-red"
|
||||
@click="deleteRow(record.id)">
|
||||
<icon-delete />
|
||||
删除
|
||||
</a-doption>
|
||||
</template>
|
||||
</card-list>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'hostIdentityCardList'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { HostIdentityQueryRequest, HostIdentityQueryResponse } from '@/api/asset/host-identity';
|
||||
import { usePagination, useColLayout } from '@/types/card';
|
||||
import { computed, reactive, ref, onMounted } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { objectTruthKeyCount, resetObject } from '@/utils';
|
||||
import fieldConfig from '../types/card.fields';
|
||||
import { deleteHostIdentity, getHostIdentityPage } from '@/api/asset/host-identity';
|
||||
import { Message, Modal } from '@arco-design/web-vue';
|
||||
import usePermission from '@/hooks/permission';
|
||||
import { useDictStore } from '@/store';
|
||||
import { copy } from '@/hooks/copy';
|
||||
import { GrantKey, GrantRouteName } from '@/views/asset/grant/types/const';
|
||||
import { IdentityType, identityTypeKey } from '../types/const';
|
||||
import HostKeySelector from '@/components/asset/host-key/selector/index.vue';
|
||||
|
||||
const emits = defineEmits(['openAdd', 'openUpdate', 'openKeyView']);
|
||||
|
||||
const list = ref<HostIdentityQueryResponse[]>([]);
|
||||
|
||||
const cardColLayout = useColLayout();
|
||||
const pagination = usePagination();
|
||||
const { toOptions, getDictValue } = useDictStore();
|
||||
const { loading, setLoading } = useLoading();
|
||||
const { hasAnyPermission } = usePermission();
|
||||
|
||||
const formRef = ref();
|
||||
const formModel = reactive<HostIdentityQueryRequest>({
|
||||
searchValue: undefined,
|
||||
id: undefined,
|
||||
type: undefined,
|
||||
name: undefined,
|
||||
username: undefined,
|
||||
keyId: undefined,
|
||||
});
|
||||
|
||||
// 条件数量
|
||||
const filterCount = computed(() => {
|
||||
return objectTruthKeyCount(formModel, ['searchValue']);
|
||||
});
|
||||
|
||||
// 删除当前行
|
||||
const deleteRow = (id: number) => {
|
||||
Modal.confirm({
|
||||
title: '删除前确认!',
|
||||
titleAlign: 'start',
|
||||
content: '确定要删除这条记录吗?',
|
||||
okText: '删除',
|
||||
onOk: async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 调用删除接口
|
||||
await deleteHostIdentity(id);
|
||||
Message.success('删除成功');
|
||||
// 重新加载数据
|
||||
fetchCardData();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 添加后回调
|
||||
const addedCallback = () => {
|
||||
fetchCardData();
|
||||
};
|
||||
|
||||
// 更新后回调
|
||||
const updatedCallback = () => {
|
||||
fetchCardData();
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
addedCallback, updatedCallback
|
||||
});
|
||||
|
||||
// 重置条件
|
||||
const reset = () => {
|
||||
resetObject(formModel);
|
||||
fetchCardData();
|
||||
};
|
||||
|
||||
// 加载数据
|
||||
const doFetchCardData = async (request: HostIdentityQueryRequest) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data } = await getHostIdentityPage(request);
|
||||
list.value = data.rows;
|
||||
pagination.total = data.total;
|
||||
pagination.current = request.page;
|
||||
pagination.pageSize = request.limit;
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 切换页码
|
||||
const fetchCardData = (page = 1, limit = pagination.pageSize, form = formModel) => {
|
||||
doFetchCardData({ page, limit, ...form });
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchCardData();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<a-modal v-model:visible="visible"
|
||||
body-class="modal-form-large"
|
||||
title-align="start"
|
||||
:title="title"
|
||||
:top="80"
|
||||
:align-center="false"
|
||||
:draggable="true"
|
||||
:mask-closable="false"
|
||||
:unmount-on-close="true"
|
||||
:ok-button-props="{ disabled: loading }"
|
||||
:cancel-button-props="{ disabled: loading }"
|
||||
:on-before-ok="handlerOk"
|
||||
@close="handleClose">
|
||||
<a-spin class="full" :loading="loading">
|
||||
<a-form :model="formModel"
|
||||
ref="formRef"
|
||||
label-align="right"
|
||||
:auto-label-width="true"
|
||||
:rules="formRules">
|
||||
<!-- 名称 -->
|
||||
<a-form-item field="name" label="名称">
|
||||
<a-input v-model="formModel.name" placeholder="请输入名称" />
|
||||
</a-form-item>
|
||||
<!-- 类型 -->
|
||||
<a-form-item field="type" label="类型">
|
||||
<a-radio-group v-model="formModel.type"
|
||||
type="button"
|
||||
class="usn"
|
||||
:options="toRadioOptions(identityTypeKey)" />
|
||||
</a-form-item>
|
||||
<!-- 用户名 -->
|
||||
<a-form-item field="username" label="用户名">
|
||||
<a-input v-model="formModel.username" placeholder="请输入用户名" />
|
||||
</a-form-item>
|
||||
<!-- 用户密码 -->
|
||||
<a-form-item v-if="formModel.type === IdentityType.PASSWORD"
|
||||
field="password"
|
||||
label="用户密码"
|
||||
:rules="passwordRules">
|
||||
<a-input-password v-model="formModel.password"
|
||||
:disabled="!isAddHandle && !formModel.useNewPassword"
|
||||
:class="[isAddHandle ? 'password-input-full' : 'password-input']"
|
||||
placeholder="请输入用户密码" />
|
||||
<a-switch v-model="formModel.useNewPassword"
|
||||
v-if="!isAddHandle"
|
||||
class="password-switch"
|
||||
type="round"
|
||||
checked-text="使用新密码"
|
||||
unchecked-text="使用原密码" />
|
||||
</a-form-item>
|
||||
<!-- 主机秘钥 -->
|
||||
<a-form-item v-if="formModel.type === IdentityType.KEY"
|
||||
field="keyId"
|
||||
label="主机秘钥">
|
||||
<host-key-selector v-model="formModel.keyId" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'hostIdentityFormModal'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { HostIdentityUpdateRequest } from '@/api/asset/host-identity';
|
||||
import type { FieldRule } from '@arco-design/web-vue';
|
||||
import { ref } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import formRules from '../types/form.rules';
|
||||
import { createHostIdentity, updateHostIdentity } from '@/api/asset/host-identity';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { IdentityType, identityTypeKey } from '../types/const';
|
||||
import { useDictStore } from '@/store';
|
||||
import HostKeySelector from '@/components/asset/host-key/selector/index.vue';
|
||||
|
||||
const { toRadioOptions, getDictValue } = useDictStore();
|
||||
const { visible, setVisible } = useVisible();
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
const title = ref<string>();
|
||||
const isAddHandle = ref<boolean>(true);
|
||||
|
||||
const defaultForm = (): HostIdentityUpdateRequest => {
|
||||
return {
|
||||
id: undefined,
|
||||
type: IdentityType.PASSWORD,
|
||||
name: undefined,
|
||||
username: undefined,
|
||||
password: undefined,
|
||||
useNewPassword: false,
|
||||
keyId: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const formRef = ref();
|
||||
const formModel = ref<HostIdentityUpdateRequest>({});
|
||||
|
||||
const emits = defineEmits(['added', 'updated']);
|
||||
|
||||
// 打开新增
|
||||
const openAdd = () => {
|
||||
title.value = '添加主机身份';
|
||||
isAddHandle.value = true;
|
||||
renderForm({ ...defaultForm() });
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
// 打开修改
|
||||
const openUpdate = (record: any) => {
|
||||
title.value = '修改主机身份';
|
||||
isAddHandle.value = false;
|
||||
renderForm({ ...defaultForm(), ...record });
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
// 渲染表单
|
||||
const renderForm = (record: any) => {
|
||||
formModel.value = Object.assign({}, record);
|
||||
};
|
||||
|
||||
defineExpose({ openAdd, openUpdate });
|
||||
|
||||
// 密码验证
|
||||
const passwordRules = [{
|
||||
validator: (value, cb) => {
|
||||
if (value && value.length > 512) {
|
||||
cb('密码长度不能大于512位');
|
||||
return;
|
||||
}
|
||||
if (formModel.value.useNewPassword && !value) {
|
||||
cb('请输入密码');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}] as FieldRule[];
|
||||
|
||||
// 确定
|
||||
const handlerOk = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 验证参数
|
||||
const error = await formRef.value.validate();
|
||||
if (error) {
|
||||
return false;
|
||||
}
|
||||
if (isAddHandle.value) {
|
||||
// 新增
|
||||
await createHostIdentity(formModel.value);
|
||||
Message.success('创建成功');
|
||||
emits('added');
|
||||
} else {
|
||||
// 修改
|
||||
await updateHostIdentity(formModel.value);
|
||||
Message.success('修改成功');
|
||||
emits('updated');
|
||||
}
|
||||
// 清空
|
||||
handlerClear();
|
||||
} catch (e) {
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭
|
||||
const handleClose = () => {
|
||||
handlerClear();
|
||||
};
|
||||
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@switch-width: 94px;
|
||||
|
||||
.password-input {
|
||||
width: calc(100% - @switch-width);
|
||||
}
|
||||
|
||||
.password-input-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.password-switch {
|
||||
width: @switch-width;
|
||||
margin-left: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,249 @@
|
||||
<template>
|
||||
<!-- 搜索 -->
|
||||
<a-card class="general-card table-search-card">
|
||||
<query-header :model="formModel"
|
||||
label-align="left"
|
||||
@submit="fetchTableData"
|
||||
@reset="fetchTableData"
|
||||
@keyup.enter="() => fetchTableData()">
|
||||
<!-- id -->
|
||||
<a-form-item field="id" label="id">
|
||||
<a-input-number v-model="formModel.id"
|
||||
placeholder="请输入id"
|
||||
allow-clear
|
||||
hide-button />
|
||||
</a-form-item>
|
||||
<!-- 名称 -->
|
||||
<a-form-item field="name" label="名称">
|
||||
<a-input v-model="formModel.name" placeholder="请输入名称" allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 类型 -->
|
||||
<a-form-item field="type" label="类型">
|
||||
<a-select v-model="formModel.type"
|
||||
placeholder="请选择类型"
|
||||
:options="toOptions(identityTypeKey)"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 用户名 -->
|
||||
<a-form-item field="username" label="用户名">
|
||||
<a-input v-model="formModel.username" placeholder="请输入用户名" allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 主机秘钥 -->
|
||||
<a-form-item field="keyId" label="主机秘钥">
|
||||
<host-key-selector v-model="formModel.keyId" allow-clear />
|
||||
</a-form-item>
|
||||
</query-header>
|
||||
</a-card>
|
||||
<!-- 表格 -->
|
||||
<a-card class="general-card table-card">
|
||||
<template #title>
|
||||
<!-- 左侧操作 -->
|
||||
<div class="table-left-bar-handle">
|
||||
<!-- 标题 -->
|
||||
<div class="table-title">
|
||||
身份列表
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧操作 -->
|
||||
<div class="table-right-bar-handle">
|
||||
<a-space>
|
||||
<!-- 角色授权 -->
|
||||
<a-button type="primary"
|
||||
v-permission="['asset:host-identity:grant']"
|
||||
@click="$router.push({ name: GrantRouteName, query: { key: GrantKey.HOST_IDENTITY_ROLE }})">
|
||||
角色授权
|
||||
<template #icon>
|
||||
<icon-user-group />
|
||||
</template>
|
||||
</a-button>
|
||||
<!-- 用户授权 -->
|
||||
<a-button type="primary"
|
||||
v-permission="['asset:host-identity:grant']"
|
||||
@click="$router.push({ name: GrantRouteName, query: { key: GrantKey.HOST_IDENTITY_USER }})">
|
||||
用户授权
|
||||
<template #icon>
|
||||
<icon-user />
|
||||
</template>
|
||||
</a-button>
|
||||
<!-- 新增 -->
|
||||
<a-button type="primary"
|
||||
v-permission="['asset:host-identity:create']"
|
||||
@click="emits('openAdd')">
|
||||
新增
|
||||
<template #icon>
|
||||
<icon-plus />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
<!-- table -->
|
||||
<a-table row-key="id"
|
||||
ref="tableRef"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="tableRenderData"
|
||||
:pagination="pagination"
|
||||
@page-change="(page) => fetchTableData(page, pagination.pageSize)"
|
||||
@page-size-change="(size) => fetchTableData(1, size)"
|
||||
:bordered="false">
|
||||
<!-- 类型 -->
|
||||
<template #type="{ record }">
|
||||
<a-tag :color="getDictValue(identityTypeKey, record.type, 'color')">
|
||||
{{ getDictValue(identityTypeKey, record.type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<!-- 用户名 -->
|
||||
<template #username="{ record }">
|
||||
<span class="span-blue text-copy" @click="copy(record.username)">
|
||||
{{ record.username }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- 秘钥名称 -->
|
||||
<template #keyId="{ record }">
|
||||
<!-- 有秘钥 -->
|
||||
<template v-if="record.keyId && record.type === IdentityType.KEY">
|
||||
<!-- 可查看详情 -->
|
||||
<a-tooltip v-if="hasAnyPermission(['asset:host-key:detail', 'asset:host-key:update'])"
|
||||
content="点击查看详情">
|
||||
<a-tag :checked="true"
|
||||
checkable
|
||||
@click="emits('openKeyView', { id: record.keyId })">
|
||||
{{ record.keyName }}
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
<!-- 不可查看详情 -->
|
||||
<a-tag v-else>
|
||||
{{ record.keyName }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<!-- 无秘钥 -->
|
||||
<template v-else>
|
||||
<span>-</span>
|
||||
</template>
|
||||
</template>
|
||||
<!-- 操作 -->
|
||||
<template #handle="{ record }">
|
||||
<div class="table-handle-wrapper">
|
||||
<!-- 修改 -->
|
||||
<a-button type="text"
|
||||
size="mini"
|
||||
v-permission="['asset:host-identity:update']"
|
||||
@click="emits('openUpdate', record)">
|
||||
修改
|
||||
</a-button>
|
||||
<!-- 删除 -->
|
||||
<a-popconfirm content="确认删除这条记录吗?"
|
||||
position="left"
|
||||
type="warning"
|
||||
@ok="deleteRow(record)">
|
||||
<a-button v-permission="['asset:host-identity:delete']"
|
||||
type="text"
|
||||
size="mini"
|
||||
status="danger">
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'hostIdentityTable'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { HostIdentityQueryRequest, HostIdentityQueryResponse } from '@/api/asset/host-identity';
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import { deleteHostIdentity, getHostIdentityPage } from '@/api/asset/host-identity';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import columns from '../types/table.columns';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import usePermission from '@/hooks/permission';
|
||||
import { copy } from '@/hooks/copy';
|
||||
import { useDictStore } from '@/store';
|
||||
import { usePagination } from '@/types/table';
|
||||
import { GrantKey, GrantRouteName } from '@/views/asset/grant/types/const';
|
||||
import { IdentityType, identityTypeKey } from '../types/const';
|
||||
import HostKeySelector from '@/components/asset/host-key/selector/index.vue';
|
||||
|
||||
const emits = defineEmits(['openAdd', 'openUpdate', 'openKeyView']);
|
||||
|
||||
const pagination = usePagination();
|
||||
const { toOptions, getDictValue } = useDictStore();
|
||||
const { loading, setLoading } = useLoading();
|
||||
const { hasAnyPermission } = usePermission();
|
||||
|
||||
const tableRenderData = ref<HostIdentityQueryResponse[]>([]);
|
||||
const formModel = reactive<HostIdentityQueryRequest>({
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
type: undefined,
|
||||
username: undefined,
|
||||
keyId: undefined,
|
||||
});
|
||||
|
||||
// 删除当前行
|
||||
const deleteRow = async ({ id }: {
|
||||
id: number
|
||||
}) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 调用删除接口
|
||||
await deleteHostIdentity(id);
|
||||
Message.success('删除成功');
|
||||
// 重新加载数据
|
||||
fetchTableData();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 添加后回调
|
||||
const addedCallback = () => {
|
||||
fetchTableData();
|
||||
};
|
||||
|
||||
// 更新后回调
|
||||
const updatedCallback = () => {
|
||||
fetchTableData();
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
addedCallback, updatedCallback
|
||||
});
|
||||
|
||||
// 加载数据
|
||||
const doFetchTableData = async (request: HostIdentityQueryRequest) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data } = await getHostIdentityPage(request);
|
||||
tableRenderData.value = data.rows;
|
||||
pagination.total = data.total;
|
||||
pagination.current = request.page;
|
||||
pagination.pageSize = request.limit;
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 切换页码
|
||||
const fetchTableData = (page = 1, limit = pagination.pageSize, form = formModel) => {
|
||||
doFetchTableData({ page, limit, ...form });
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchTableData();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
82
orion-visor-ui/src/views/asset/host-identity/index.vue
Normal file
82
orion-visor-ui/src/views/asset/host-identity/index.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<div class="layout-container">
|
||||
<!-- 列表-表格 -->
|
||||
<host-identity-table v-if="renderTable"
|
||||
ref="table"
|
||||
@open-add="() => modal.openAdd()"
|
||||
@open-update="(e) => modal.openUpdate(e)"
|
||||
@open-key-view="(e) => keyDrawer.openView(e) " />
|
||||
<!-- 列表-卡片 -->
|
||||
<host-identity-card-list v-else
|
||||
ref="card"
|
||||
@open-add="() => modal.openAdd()"
|
||||
@open-update="(e) => modal.openUpdate(e)"
|
||||
@open-key-view="(e) => keyDrawer.openView(e) " />
|
||||
<!-- 添加修改模态框 -->
|
||||
<host-identity-form-modal ref="modal"
|
||||
@added="modalAddCallback"
|
||||
@updated="modalUpdateCallback" />
|
||||
<!-- 主机秘钥抽屉 -->
|
||||
<host-key-form-drawer ref="keyDrawer" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'hostIdentity'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onUnmounted, onBeforeMount } from 'vue';
|
||||
import { useAppStore, useCacheStore, useDictStore } from '@/store';
|
||||
import { dictKeys } from './types/const';
|
||||
import HostIdentityCardList from './components/host-identity-card-list.vue';
|
||||
import HostIdentityTable from './components/host-identity-table.vue';
|
||||
import HostIdentityFormModal from './components/host-identity-form-modal.vue';
|
||||
import HostKeyFormDrawer from '../host-key/components/host-key-form-drawer.vue';
|
||||
|
||||
const table = ref();
|
||||
const card = ref();
|
||||
const modal = ref();
|
||||
const keyDrawer = ref();
|
||||
const appStore = useAppStore();
|
||||
const cacheStore = useCacheStore();
|
||||
|
||||
const renderTable = computed(() => appStore.hostIdentityView === 'table');
|
||||
|
||||
// 添加回调
|
||||
const modalAddCallback = () => {
|
||||
if (renderTable.value) {
|
||||
table.value.addedCallback();
|
||||
} else {
|
||||
card.value.addedCallback();
|
||||
}
|
||||
};
|
||||
|
||||
// 修改回调
|
||||
const modalUpdateCallback = () => {
|
||||
if (renderTable.value) {
|
||||
table.value.updatedCallback();
|
||||
} else {
|
||||
card.value.updatedCallback();
|
||||
}
|
||||
};
|
||||
|
||||
// 加载字典值
|
||||
onBeforeMount(async () => {
|
||||
const dictStore = useDictStore();
|
||||
await dictStore.loadKeys(dictKeys);
|
||||
});
|
||||
|
||||
// 卸载时清除 cache
|
||||
onUnmounted(() => {
|
||||
const cacheStore = useCacheStore();
|
||||
cacheStore.reset('hostKeys');
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { CardField, CardFieldConfig } from '@/types/card';
|
||||
import { dateFormat } from '@/utils';
|
||||
|
||||
const fieldConfig = {
|
||||
rowGap: '12px',
|
||||
labelSpan: 8,
|
||||
fields: [
|
||||
{
|
||||
label: 'id',
|
||||
dataIndex: 'id',
|
||||
slotName: 'id',
|
||||
}, {
|
||||
label: '类型',
|
||||
dataIndex: 'type',
|
||||
slotName: 'type',
|
||||
}, {
|
||||
label: '用户名',
|
||||
dataIndex: 'username',
|
||||
slotName: 'username',
|
||||
ellipsis: true,
|
||||
}, {
|
||||
label: '主机秘钥',
|
||||
dataIndex: 'keyId',
|
||||
slotName: 'keyId',
|
||||
height: '24px',
|
||||
}, {
|
||||
label: '修改时间',
|
||||
dataIndex: 'updateTime',
|
||||
slotName: 'updateTime',
|
||||
render: ({ record }) => {
|
||||
return dateFormat(new Date(record.updateTime));
|
||||
},
|
||||
}
|
||||
] as CardField[]
|
||||
} as CardFieldConfig;
|
||||
|
||||
export default fieldConfig;
|
||||
11
orion-visor-ui/src/views/asset/host-identity/types/const.ts
Normal file
11
orion-visor-ui/src/views/asset/host-identity/types/const.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
// 身份类型
|
||||
export const IdentityType = {
|
||||
PASSWORD: 'PASSWORD',
|
||||
KEY: 'KEY',
|
||||
};
|
||||
|
||||
// 身份类型 字典项
|
||||
export const identityTypeKey = 'hostIdentityType';
|
||||
|
||||
// 加载的字典值
|
||||
export const dictKeys = [identityTypeKey];
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { FieldRule } from '@arco-design/web-vue';
|
||||
|
||||
export const name = [{
|
||||
required: true,
|
||||
message: '请输入名称'
|
||||
}, {
|
||||
maxLength: 64,
|
||||
message: '名称长度不能大于64位'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const type = [{
|
||||
required: true,
|
||||
message: '请选择类型'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const keyId = [{
|
||||
required: true,
|
||||
message: '请选择秘钥'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const username = [{
|
||||
required: true,
|
||||
message: '请输入用户名'
|
||||
}, {
|
||||
maxLength: 128,
|
||||
message: '用户名长度不能大于128位'
|
||||
}] as FieldRule[];
|
||||
|
||||
export default {
|
||||
name,
|
||||
type,
|
||||
keyId,
|
||||
username,
|
||||
} as Record<string, FieldRule | FieldRule[]>;
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface';
|
||||
import { dateFormat } from '@/utils';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'id',
|
||||
dataIndex: 'id',
|
||||
slotName: 'id',
|
||||
width: 70,
|
||||
align: 'left',
|
||||
fixed: 'left',
|
||||
}, {
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
slotName: 'name',
|
||||
ellipsis: true,
|
||||
tooltip: true
|
||||
}, {
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
slotName: 'type',
|
||||
width: 138,
|
||||
}, {
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
slotName: 'username',
|
||||
}, {
|
||||
title: '主机秘钥',
|
||||
dataIndex: 'keyId',
|
||||
slotName: 'keyId',
|
||||
}, {
|
||||
title: '修改时间',
|
||||
dataIndex: 'updateTime',
|
||||
slotName: 'updateTime',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
render: ({ record }) => {
|
||||
return dateFormat(new Date(record.updateTime));
|
||||
},
|
||||
}, {
|
||||
title: '操作',
|
||||
slotName: 'handle',
|
||||
width: 130,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
},
|
||||
] as TableColumnData[];
|
||||
|
||||
export default columns;
|
||||
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<card-list v-model:searchValue="formModel.searchValue"
|
||||
search-input-placeholder="输入 id / 名称"
|
||||
:loading="loading"
|
||||
:fieldConfig="fieldConfig"
|
||||
:list="list"
|
||||
:pagination="pagination"
|
||||
:card-layout-cols="cardColLayout"
|
||||
:handle-visible="{disableFilter: true}"
|
||||
:add-permission="['asset:host-key:create']"
|
||||
@add="emits('openAdd')"
|
||||
@reset="reset"
|
||||
@search="fetchCardData"
|
||||
@page-change="fetchCardData">
|
||||
<!-- 左侧操作 -->
|
||||
<template #leftHandle>
|
||||
<!-- 角色授权 -->
|
||||
<a-button v-permission="['asset:host-key:grant']"
|
||||
class="card-header-icon-wrapper"
|
||||
@click="$router.push({ name: GrantRouteName, query: { key: GrantKey.HOST_KEY_ROLE }})">
|
||||
角色授权
|
||||
<template #icon>
|
||||
<icon-user-group />
|
||||
</template>
|
||||
</a-button>
|
||||
<!-- 用户授权 -->
|
||||
<a-button v-permission="['asset:host-key:grant']"
|
||||
class="card-header-icon-wrapper"
|
||||
@click="$router.push({ name: GrantRouteName, query: { key: GrantKey.HOST_KEY_USER }})">
|
||||
用户授权
|
||||
<template #icon>
|
||||
<icon-user />
|
||||
</template>
|
||||
</a-button>
|
||||
</template>
|
||||
<!-- 标题 -->
|
||||
<template #title="{ record }">
|
||||
{{ record.name }}
|
||||
</template>
|
||||
<!-- 拓展操作 -->
|
||||
<template #extra="{ record }">
|
||||
<a-space>
|
||||
<!-- 更多操作 -->
|
||||
<a-dropdown trigger="hover">
|
||||
<icon-more class="card-extra-icon" />
|
||||
<template #content>
|
||||
<!-- 详情 -->
|
||||
<a-doption v-permission="['asset:host-key:detail', 'asset:host-key:update']"
|
||||
@click="emits('openView', record)">
|
||||
<icon-list />
|
||||
详情
|
||||
</a-doption>
|
||||
<!-- 修改 -->
|
||||
<a-doption v-permission="['asset:host-key:update']"
|
||||
@click="emits('openUpdate', record)">
|
||||
<icon-edit />
|
||||
修改
|
||||
</a-doption>
|
||||
<!-- 删除 -->
|
||||
<a-doption v-permission="['asset:host-key:delete']"
|
||||
class="span-red"
|
||||
@click="deleteRow(record.id)">
|
||||
<icon-delete />
|
||||
删除
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</template>
|
||||
<!-- 右键菜单 -->
|
||||
<template #contextMenu="{ record }">
|
||||
<!-- 详情 -->
|
||||
<a-doption v-permission="['asset:host-key:detail', 'asset:host-key:update']"
|
||||
@click="emits('openView', record)">
|
||||
<icon-list />
|
||||
详情
|
||||
</a-doption>
|
||||
<!-- 修改 -->
|
||||
<a-doption v-permission="['asset:host-key:update']"
|
||||
@click="emits('openUpdate', record)">
|
||||
<icon-edit />
|
||||
修改
|
||||
</a-doption>
|
||||
<!-- 删除 -->
|
||||
<a-doption v-permission="['asset:host-key:delete']"
|
||||
class="span-red"
|
||||
@click="deleteRow(record.id)">
|
||||
<icon-delete />
|
||||
删除
|
||||
</a-doption>
|
||||
</template>
|
||||
</card-list>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'hostKeyCardList'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { HostKeyQueryRequest, HostKeyQueryResponse } from '@/api/asset/host-key';
|
||||
import { usePagination, useColLayout } from '@/types/card';
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { resetObject } from '@/utils';
|
||||
import fieldConfig from '../types/card.fields';
|
||||
import { deleteHostKey, getHostKeyPage } from '@/api/asset/host-key';
|
||||
import { Message, Modal } from '@arco-design/web-vue';
|
||||
import { GrantKey, GrantRouteName } from '@/views/asset/grant/types/const';
|
||||
|
||||
const emits = defineEmits(['openAdd', 'openUpdate', 'openView']);
|
||||
|
||||
const list = ref<HostKeyQueryResponse[]>([]);
|
||||
|
||||
const cardColLayout = useColLayout();
|
||||
const pagination = usePagination();
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
const formModel = reactive<HostKeyQueryRequest>({
|
||||
searchValue: undefined,
|
||||
});
|
||||
|
||||
// 删除当前行
|
||||
const deleteRow = (id: number) => {
|
||||
Modal.confirm({
|
||||
title: '删除前确认!',
|
||||
titleAlign: 'start',
|
||||
content: '确定要删除这条记录吗?',
|
||||
okText: '删除',
|
||||
onOk: async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 调用删除接口
|
||||
await deleteHostKey(id);
|
||||
Message.success('删除成功');
|
||||
// 重新加载数据
|
||||
fetchCardData();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 添加后回调
|
||||
const addedCallback = () => {
|
||||
fetchCardData();
|
||||
};
|
||||
|
||||
// 更新后回调
|
||||
const updatedCallback = () => {
|
||||
fetchCardData();
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
addedCallback, updatedCallback
|
||||
});
|
||||
|
||||
// 重置条件
|
||||
const reset = () => {
|
||||
resetObject(formModel);
|
||||
fetchCardData();
|
||||
};
|
||||
|
||||
// 加载数据
|
||||
const doFetchCardData = async (request: HostKeyQueryRequest) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data } = await getHostKeyPage(request);
|
||||
list.value = data.rows;
|
||||
pagination.total = data.total;
|
||||
pagination.current = request.page;
|
||||
pagination.pageSize = request.limit;
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 切换页码
|
||||
const fetchCardData = (page = 1, limit = pagination.pageSize, form = formModel) => {
|
||||
doFetchCardData({ page, limit, ...form });
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchCardData();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,258 @@
|
||||
<template>
|
||||
<a-drawer v-model:visible="visible"
|
||||
:title="title"
|
||||
:width="520"
|
||||
:mask-closable="false"
|
||||
:unmount-on-close="true"
|
||||
:ok-button-props="{ disabled: loading || isViewHandler }"
|
||||
:cancel-button-props="{ disabled: loading }"
|
||||
:on-before-ok="handlerOk"
|
||||
@cancel="handleClose">
|
||||
<a-spin class="full modal-form-small" :loading="loading">
|
||||
<a-alert class="keygen-alert">
|
||||
请使用 ssh-keygen -m PEM -t rsa 生成秘钥
|
||||
</a-alert>
|
||||
<a-form :model="formModel"
|
||||
ref="formRef"
|
||||
label-align="right"
|
||||
:auto-label-width="true"
|
||||
:rules="formRules">
|
||||
<!-- 名称 -->
|
||||
<a-form-item field="name" label="名称">
|
||||
<a-input v-model="formModel.name"
|
||||
:disabled="isViewHandler"
|
||||
placeholder="请输入名称" />
|
||||
</a-form-item>
|
||||
<!-- 公钥文本 -->
|
||||
<a-form-item field="publicKey" label="公钥">
|
||||
<a-upload :auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:draggable="true"
|
||||
:disabled="isViewHandler"
|
||||
@change="selectPublicFile"
|
||||
@click.prevent="() => {}">
|
||||
<template #upload-button>
|
||||
<a-textarea v-model="formModel.publicKey"
|
||||
:disabled="isViewHandler"
|
||||
placeholder="请输入公钥文本或将文件拖拽到此处"
|
||||
:auto-size="{ minRows: 8, maxRows: 8}" />
|
||||
</template>
|
||||
</a-upload>
|
||||
</a-form-item>
|
||||
<!-- 私钥文本 -->
|
||||
<a-form-item field="privateKey" label="私钥">
|
||||
<a-upload :auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:draggable="true"
|
||||
:disabled="isViewHandler"
|
||||
@change="selectPrivateFile"
|
||||
@click.prevent="() => {}">
|
||||
<template #upload-button>
|
||||
<a-textarea v-model="formModel.privateKey"
|
||||
:disabled="isViewHandler"
|
||||
placeholder="请输入私钥文本或将文件拖拽到此处"
|
||||
:auto-size="{ minRows: 8, maxRows: 8}" />
|
||||
</template>
|
||||
</a-upload>
|
||||
</a-form-item>
|
||||
<!-- 密码 -->
|
||||
<a-form-item v-if="!isViewHandler"
|
||||
field="password"
|
||||
label="密码"
|
||||
:rules="passwordRules">
|
||||
<a-input-password v-model="formModel.password"
|
||||
:disabled="!isAddHandle && !formModel.useNewPassword"
|
||||
:class="[isAddHandle ? 'password-input-full' : 'password-input']"
|
||||
class="password-input"
|
||||
placeholder="请输入私钥密码" />
|
||||
<a-switch v-model="formModel.useNewPassword"
|
||||
v-if="!isAddHandle"
|
||||
class="password-switch"
|
||||
type="round"
|
||||
checked-text="使用新密码"
|
||||
unchecked-text="使用原密码" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'hostKeyFormDrawer'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { HostKeyUpdateRequest } from '@/api/asset/host-key';
|
||||
import type { FieldRule, FileItem } from '@arco-design/web-vue';
|
||||
import { ref } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import formRules from '../types/form.rules';
|
||||
import { createHostKey, updateHostKey, getHostKey } from '@/api/asset/host-key';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { readFileText } from '@/utils/file';
|
||||
|
||||
const { visible, setVisible } = useVisible();
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
const title = ref<string>();
|
||||
const isAddHandle = ref<boolean>(true);
|
||||
const isViewHandler = ref<boolean>(false);
|
||||
|
||||
const defaultForm = (): HostKeyUpdateRequest => {
|
||||
return {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
publicKey: undefined,
|
||||
privateKey: undefined,
|
||||
password: undefined,
|
||||
useNewPassword: false
|
||||
};
|
||||
};
|
||||
|
||||
const formRef = ref();
|
||||
const formModel = ref<HostKeyUpdateRequest>({});
|
||||
|
||||
const emits = defineEmits(['added', 'updated']);
|
||||
|
||||
// 打开新增
|
||||
const openAdd = () => {
|
||||
title.value = '添加主机秘钥';
|
||||
isAddHandle.value = true;
|
||||
isViewHandler.value = false;
|
||||
renderForm({ ...defaultForm() });
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
// 打开修改
|
||||
const openUpdate = async (record: any) => {
|
||||
title.value = '修改主机秘钥';
|
||||
isAddHandle.value = false;
|
||||
isViewHandler.value = false;
|
||||
await render(record.id);
|
||||
};
|
||||
|
||||
// 打开查看
|
||||
const openView = async (record: any) => {
|
||||
title.value = '主机秘钥';
|
||||
isAddHandle.value = false;
|
||||
isViewHandler.value = true;
|
||||
await render(record.id);
|
||||
};
|
||||
|
||||
// 渲染数据
|
||||
const render = async (id: number) => {
|
||||
setVisible(true);
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data } = await getHostKey(id);
|
||||
renderForm({ ...data });
|
||||
} catch (e) {
|
||||
setVisible(false);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 渲染表单
|
||||
const renderForm = (record: any) => {
|
||||
formModel.value = Object.assign({}, record);
|
||||
};
|
||||
|
||||
defineExpose({ openAdd, openUpdate, openView });
|
||||
|
||||
// 密码验证
|
||||
const passwordRules = [{
|
||||
validator: (value, cb) => {
|
||||
if (value && value.length > 512) {
|
||||
cb('密码长度不能大于512位');
|
||||
return;
|
||||
}
|
||||
if (formModel.value.useNewPassword && !value) {
|
||||
cb('请输入密码');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}] as FieldRule[];
|
||||
|
||||
// 选择公钥文件
|
||||
const selectPublicFile = async (fileList: FileItem[]) => {
|
||||
formModel.value.publicKey = await readFileText(fileList[0].file as File);
|
||||
formRef.value.clearValidate('publicKey');
|
||||
};
|
||||
|
||||
// 选择私钥文件
|
||||
const selectPrivateFile = async (fileList: FileItem[]) => {
|
||||
formModel.value.privateKey = await readFileText(fileList[0].file as File);
|
||||
formRef.value.clearValidate('privateKey');
|
||||
};
|
||||
|
||||
// 确定
|
||||
const handlerOk = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 验证参数
|
||||
const error = await formRef.value.validate();
|
||||
if (error) {
|
||||
return false;
|
||||
}
|
||||
if (isAddHandle.value) {
|
||||
// 新增
|
||||
await createHostKey(formModel.value);
|
||||
Message.success('创建成功');
|
||||
emits('added');
|
||||
} else {
|
||||
// 修改
|
||||
await updateHostKey(formModel.value);
|
||||
Message.success('修改成功');
|
||||
emits('updated');
|
||||
}
|
||||
// 清空
|
||||
handlerClear();
|
||||
} catch (e) {
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭
|
||||
const handleClose = () => {
|
||||
handlerClear();
|
||||
};
|
||||
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@switch-width: 94px;
|
||||
|
||||
.form-wrapper {
|
||||
width: 100%;
|
||||
padding: 12px 12px 0 12px;
|
||||
}
|
||||
|
||||
.keygen-alert {
|
||||
margin-bottom: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.password-input {
|
||||
width: calc(100% - @switch-width);
|
||||
}
|
||||
|
||||
.password-input-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.password-switch {
|
||||
width: @switch-width;
|
||||
margin-left: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<!-- 搜索 -->
|
||||
<a-card class="general-card table-search-card">
|
||||
<query-header :model="formModel"
|
||||
label-align="left"
|
||||
@submit="fetchTableData"
|
||||
@reset="fetchTableData"
|
||||
@keyup.enter="() => fetchTableData()">
|
||||
<!-- id -->
|
||||
<a-form-item field="id" label="id">
|
||||
<a-input-number v-model="formModel.id"
|
||||
placeholder="请输入id"
|
||||
allow-clear
|
||||
hide-button />
|
||||
</a-form-item>
|
||||
<!-- 名称 -->
|
||||
<a-form-item field="name" label="名称">
|
||||
<a-input v-model="formModel.name" placeholder="请输入名称" allow-clear />
|
||||
</a-form-item>
|
||||
</query-header>
|
||||
</a-card>
|
||||
<!-- 表格 -->
|
||||
<a-card class="general-card table-card">
|
||||
<template #title>
|
||||
<!-- 左侧操作 -->
|
||||
<div class="table-left-bar-handle">
|
||||
<!-- 标题 -->
|
||||
<div class="table-title">
|
||||
秘钥列表
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧操作 -->
|
||||
<div class="table-right-bar-handle">
|
||||
<a-space>
|
||||
<!-- 角色授权 -->
|
||||
<a-button type="primary"
|
||||
v-permission="['asset:host-key:grant']"
|
||||
@click="$router.push({ name: GrantRouteName, query: { key: GrantKey.HOST_KEY_ROLE }})">
|
||||
角色授权
|
||||
<template #icon>
|
||||
<icon-user-group />
|
||||
</template>
|
||||
</a-button>
|
||||
<!-- 用户授权 -->
|
||||
<a-button type="primary"
|
||||
v-permission="['asset:host-key:grant']"
|
||||
@click="$router.push({ name: GrantRouteName, query: { key: GrantKey.HOST_KEY_USER }})">
|
||||
用户授权
|
||||
<template #icon>
|
||||
<icon-user />
|
||||
</template>
|
||||
</a-button>
|
||||
<!-- 新增 -->
|
||||
<a-button type="primary"
|
||||
v-permission="['asset:host-key:create']"
|
||||
@click="emits('openAdd')">
|
||||
新增
|
||||
<template #icon>
|
||||
<icon-plus />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
<!-- table -->
|
||||
<a-table row-key="id"
|
||||
ref="tableRef"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="tableRenderData"
|
||||
:pagination="pagination"
|
||||
@page-change="(page) => fetchTableData(page, pagination.pageSize)"
|
||||
@page-size-change="(size) => fetchTableData(1, size)"
|
||||
:bordered="false">
|
||||
<!-- 操作 -->
|
||||
<template #handle="{ record }">
|
||||
<div class="table-handle-wrapper">
|
||||
<!-- 详情 -->
|
||||
<a-button type="text"
|
||||
size="mini"
|
||||
v-permission="['asset:host-key:detail', 'asset:host-key:update']"
|
||||
@click="emits('openView', record)">
|
||||
详情
|
||||
</a-button>
|
||||
<!-- 修改 -->
|
||||
<a-button type="text"
|
||||
size="mini"
|
||||
v-permission="['asset:host-key:update']"
|
||||
@click="emits('openUpdate', record)">
|
||||
修改
|
||||
</a-button>
|
||||
<!-- 删除 -->
|
||||
<a-popconfirm content="确认删除这条记录吗?"
|
||||
position="left"
|
||||
type="warning"
|
||||
@ok="deleteRow(record)">
|
||||
<a-button v-permission="['asset:host-key:delete']"
|
||||
type="text"
|
||||
size="mini"
|
||||
status="danger">
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'hostKeyTable'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { HostKeyQueryRequest, HostKeyQueryResponse } from '@/api/asset/host-key';
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import { deleteHostKey, getHostKeyPage } from '@/api/asset/host-key';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import columns from '../types/table.columns';
|
||||
import { usePagination } from '@/types/table';
|
||||
import { GrantKey, GrantRouteName } from '@/views/asset/grant/types/const';
|
||||
|
||||
const emits = defineEmits(['openAdd', 'openUpdate', 'openView']);
|
||||
|
||||
const tableRenderData = ref<HostKeyQueryResponse[]>([]);
|
||||
|
||||
const pagination = usePagination();
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
const formModel = reactive<HostKeyQueryRequest>({
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
publicKey: undefined,
|
||||
privateKey: undefined,
|
||||
});
|
||||
|
||||
// 删除当前行
|
||||
const deleteRow = async ({ id }: {
|
||||
id: number
|
||||
}) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 调用删除接口
|
||||
await deleteHostKey(id);
|
||||
Message.success('删除成功');
|
||||
// 重新加载数据
|
||||
fetchTableData();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 添加后回调
|
||||
const addedCallback = () => {
|
||||
fetchTableData();
|
||||
};
|
||||
|
||||
// 更新后回调
|
||||
const updatedCallback = () => {
|
||||
fetchTableData();
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
addedCallback, updatedCallback
|
||||
});
|
||||
|
||||
// 加载数据
|
||||
const doFetchTableData = async (request: HostKeyQueryRequest) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data } = await getHostKeyPage(request);
|
||||
tableRenderData.value = data.rows;
|
||||
pagination.total = data.total;
|
||||
pagination.current = request.page;
|
||||
pagination.pageSize = request.limit;
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 切换页码
|
||||
const fetchTableData = (page = 1, limit = pagination.pageSize, form = formModel) => {
|
||||
doFetchTableData({ page, limit, ...form });
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchTableData();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
64
orion-visor-ui/src/views/asset/host-key/index.vue
Normal file
64
orion-visor-ui/src/views/asset/host-key/index.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div class="layout-container">
|
||||
<!-- 列表-表格 -->
|
||||
<host-key-table v-if="renderTable"
|
||||
ref="table"
|
||||
@open-view="(e) => drawer.openView(e)"
|
||||
@open-add="() => drawer.openAdd()"
|
||||
@open-update="(e) => drawer.openUpdate(e)" />
|
||||
<!-- 列表-卡片 -->
|
||||
<host-key-card-list v-else
|
||||
ref="card"
|
||||
@open-view="(e) => drawer.openView(e)"
|
||||
@open-add="() => drawer.openAdd()"
|
||||
@open-update="(e) => drawer.openUpdate(e)" />
|
||||
<!-- 添加修改模态框 -->
|
||||
<host-key-form-drawer ref="drawer"
|
||||
@added="modalAddCallback"
|
||||
@updated="modalUpdateCallback" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'hostKey'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { useAppStore } from '@/store';
|
||||
import HostKeyCardList from './components/host-key-card-list.vue';
|
||||
import HostKeyTable from './components/host-key-table.vue';
|
||||
import HostKeyFormDrawer from './components/host-key-form-drawer.vue';
|
||||
|
||||
const table = ref();
|
||||
const card = ref();
|
||||
const drawer = ref();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const renderTable = computed(() => appStore.hostKeyView === 'table');
|
||||
|
||||
// 添加回调
|
||||
const modalAddCallback = () => {
|
||||
if (renderTable.value) {
|
||||
table.value.addedCallback();
|
||||
} else {
|
||||
card.value.addedCallback();
|
||||
}
|
||||
};
|
||||
|
||||
// 修改回调
|
||||
const modalUpdateCallback = () => {
|
||||
if (renderTable.value) {
|
||||
table.value.updatedCallback();
|
||||
} else {
|
||||
card.value.updatedCallback();
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
30
orion-visor-ui/src/views/asset/host-key/types/card.fields.ts
Normal file
30
orion-visor-ui/src/views/asset/host-key/types/card.fields.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { CardField, CardFieldConfig } from '@/types/card';
|
||||
import { dateFormat } from '@/utils';
|
||||
|
||||
const fieldConfig = {
|
||||
rowGap: '12px',
|
||||
labelSpan: 8,
|
||||
fields: [
|
||||
{
|
||||
label: 'id',
|
||||
dataIndex: 'id',
|
||||
slotName: 'id',
|
||||
}, {
|
||||
label: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
slotName: 'createTime',
|
||||
render: ({ record }) => {
|
||||
return dateFormat(new Date(record.createTime));
|
||||
},
|
||||
}, {
|
||||
label: '修改时间',
|
||||
dataIndex: 'updateTime',
|
||||
slotName: 'updateTime',
|
||||
render: ({ record }) => {
|
||||
return dateFormat(new Date(record.updateTime));
|
||||
},
|
||||
}
|
||||
] as CardField[]
|
||||
} as CardFieldConfig;
|
||||
|
||||
export default fieldConfig;
|
||||
19
orion-visor-ui/src/views/asset/host-key/types/form.rules.ts
Normal file
19
orion-visor-ui/src/views/asset/host-key/types/form.rules.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { FieldRule } from '@arco-design/web-vue';
|
||||
|
||||
export const name = [{
|
||||
required: true,
|
||||
message: '请输入名称'
|
||||
}, {
|
||||
maxLength: 64,
|
||||
message: '名称长度不能大于64位'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const privateKey = [{
|
||||
required: true,
|
||||
message: '请输入私钥文本'
|
||||
}] as FieldRule[];
|
||||
|
||||
export default {
|
||||
name,
|
||||
privateKey,
|
||||
} as Record<string, FieldRule | FieldRule[]>;
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface';
|
||||
import { dateFormat } from '@/utils';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'id',
|
||||
dataIndex: 'id',
|
||||
slotName: 'id',
|
||||
width: 100,
|
||||
align: 'left',
|
||||
fixed: 'left',
|
||||
}, {
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
slotName: 'name',
|
||||
ellipsis: true,
|
||||
tooltip: true
|
||||
}, {
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
slotName: 'createTime',
|
||||
align: 'center',
|
||||
width: 198,
|
||||
render: ({ record }) => {
|
||||
return dateFormat(new Date(record.createTime));
|
||||
},
|
||||
}, {
|
||||
title: '修改时间',
|
||||
dataIndex: 'updateTime',
|
||||
slotName: 'updateTime',
|
||||
align: 'center',
|
||||
width: 198,
|
||||
render: ({ record }) => {
|
||||
return dateFormat(new Date(record.updateTime));
|
||||
},
|
||||
}, {
|
||||
title: '操作',
|
||||
slotName: 'handle',
|
||||
width: 180,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
},
|
||||
] as TableColumnData[];
|
||||
|
||||
export default columns;
|
||||
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<a-drawer v-model:visible="visible"
|
||||
:width="460"
|
||||
:esc-to-close="false"
|
||||
:mask-closable="false"
|
||||
:unmount-on-close="true"
|
||||
:ok-button-props="{ disabled: loading }"
|
||||
:cancel-button-props="{ disabled: loading }"
|
||||
:footer="false"
|
||||
@cancel="handleCancel">
|
||||
<!-- 标题 -->
|
||||
<template #title>
|
||||
<span class="host-title-text">
|
||||
主机配置 <span class="host-name-title-text">{{ record.name }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<a-spin :loading="loading" class="host-config-container">
|
||||
<!-- SSH 配置 -->
|
||||
<ssh-config-form :host-id="record.id"
|
||||
:content="config.ssh"
|
||||
@submitted="(e) => config.ssh = e" />
|
||||
</a-spin>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'hostConfigDrawer'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { HostConfigWrapper } from '../../types/const';
|
||||
import type { HostSshConfig } from './ssh/types/const';
|
||||
import { ref } from 'vue';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { getHostConfigList } from '@/api/asset/host-config';
|
||||
import { useCacheStore, useDictStore } from '@/store';
|
||||
import { dictKeys } from './ssh/types/const';
|
||||
import SshConfigForm from './ssh/ssh-config-form.vue';
|
||||
|
||||
const { visible, setVisible } = useVisible();
|
||||
const { loading, setLoading } = useLoading();
|
||||
const cacheStore = useCacheStore();
|
||||
|
||||
const record = ref({} as any);
|
||||
const config = ref<HostConfigWrapper>({
|
||||
ssh: {} as HostSshConfig
|
||||
});
|
||||
|
||||
// 打开
|
||||
const open = async (e: any) => {
|
||||
record.value = { ...e };
|
||||
try {
|
||||
setLoading(true);
|
||||
setVisible(true);
|
||||
// 加载字典值
|
||||
const dictStore = useDictStore();
|
||||
await dictStore.loadKeys(dictKeys);
|
||||
// 加载配置
|
||||
const { data } = await getHostConfigList(record.value.id);
|
||||
data.forEach(s => {
|
||||
config.value[s.type] = s;
|
||||
});
|
||||
} catch ({ message }) {
|
||||
Message.error(`配置加载失败 ${message}`);
|
||||
setVisible(false);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭
|
||||
const handleCancel = () => {
|
||||
setLoading(false);
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
defineExpose({ open });
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.host-title-text {
|
||||
width: 368px;
|
||||
display: flex;
|
||||
font-size: 16px;
|
||||
line-height: 16px;
|
||||
font-weight: 600;
|
||||
|
||||
.host-name-title-text {
|
||||
max-width: 288px;
|
||||
margin-left: 8px;
|
||||
font-size: 14px;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
color: rgb(var(--arcoblue-6));
|
||||
}
|
||||
|
||||
.host-name-title-text::before {
|
||||
content: '(';
|
||||
}
|
||||
|
||||
.host-name-title-text::after {
|
||||
content: ')';
|
||||
}
|
||||
}
|
||||
|
||||
.host-config-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--color-fill-2);
|
||||
}
|
||||
|
||||
.arco-card {
|
||||
margin: 18px 18px 0 18px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,304 @@
|
||||
<template>
|
||||
<a-card class="general-card"
|
||||
:body-style="{ padding: config.status === EnabledStatus.ENABLED ? '' : '0' }">
|
||||
<!-- 标题 -->
|
||||
<template #title>
|
||||
<div class="config-title-wrapper">
|
||||
<span>SSH 配置</span>
|
||||
<a-switch v-model="config.status"
|
||||
:disabled="loading"
|
||||
type="round"
|
||||
:checked-value="EnabledStatus.ENABLED"
|
||||
:unchecked-value="EnabledStatus.DISABLED"
|
||||
:before-change="s => updateStatus(s as number)" />
|
||||
</div>
|
||||
</template>
|
||||
<a-spin v-show="config.status" :loading="loading" class="config-form-wrapper">
|
||||
<!-- 表单 -->
|
||||
<a-form :model="formModel"
|
||||
ref="formRef"
|
||||
label-align="right"
|
||||
:auto-label-width="true"
|
||||
:rules="rules">
|
||||
<!-- 系统类型 -->
|
||||
<a-form-item field="osType"
|
||||
label="系统类型"
|
||||
:hide-asterisk="true">
|
||||
<a-select v-model="formModel.osType"
|
||||
:options="toOptions(sshOsTypeKey)"
|
||||
placeholder="请选择系统类型" />
|
||||
</a-form-item>
|
||||
<!-- 用户名 -->
|
||||
<a-form-item field="username"
|
||||
label="用户名"
|
||||
:rules="usernameRules"
|
||||
:help="SshAuthType.IDENTITY === formModel.authType ? '将使用主机身份的用户名' : undefined">
|
||||
<a-input v-model="formModel.username"
|
||||
:disabled="SshAuthType.IDENTITY === formModel.authType"
|
||||
placeholder="请输入用户名" />
|
||||
</a-form-item>
|
||||
<!-- SSH 端口 -->
|
||||
<a-form-item field="port"
|
||||
label="SSH端口"
|
||||
:hide-asterisk="true">
|
||||
<a-input-number v-model="formModel.port"
|
||||
placeholder="请输入SSH端口"
|
||||
hide-button />
|
||||
</a-form-item>
|
||||
<!-- 验证方式 -->
|
||||
<a-form-item field="authType"
|
||||
label="验证方式"
|
||||
:hide-asterisk="true">
|
||||
<a-radio-group type="button"
|
||||
class="auth-type-group usn"
|
||||
v-model="formModel.authType"
|
||||
:options="toRadioOptions(sshAuthTypeKey)" />
|
||||
</a-form-item>
|
||||
<!-- 主机密码 -->
|
||||
<a-form-item v-if="SshAuthType.PASSWORD === formModel.authType"
|
||||
field="password"
|
||||
label="主机密码"
|
||||
:rules="passwordRules">
|
||||
<a-input-password v-model="formModel.password"
|
||||
:disabled="!formModel.useNewPassword && formModel.hasPassword"
|
||||
placeholder="主机密码" />
|
||||
<a-switch v-if="formModel.hasPassword"
|
||||
v-model="formModel.useNewPassword"
|
||||
class="password-switch"
|
||||
type="round"
|
||||
checked-text="使用新密码"
|
||||
unchecked-text="使用原密码" />
|
||||
</a-form-item>
|
||||
<!-- 主机秘钥 -->
|
||||
<a-form-item v-if="SshAuthType.KEY === formModel.authType"
|
||||
field="keyId"
|
||||
label="主机秘钥"
|
||||
:hide-asterisk="true">
|
||||
<host-key-selector v-model="formModel.keyId" />
|
||||
</a-form-item>
|
||||
<!-- 主机身份 -->
|
||||
<a-form-item v-if="SshAuthType.IDENTITY === formModel.authType"
|
||||
field="identityId"
|
||||
label="主机身份"
|
||||
:hide-asterisk="true">
|
||||
<host-identity-selector v-model="formModel.identityId" />
|
||||
</a-form-item>
|
||||
<!-- 用户名 -->
|
||||
<a-form-item field="connectTimeout"
|
||||
label="连接超时时间"
|
||||
:hide-asterisk="true">
|
||||
<a-input-number v-model="formModel.connectTimeout"
|
||||
placeholder="请输入连接超时时间"
|
||||
hide-button>
|
||||
<template #suffix>
|
||||
ms
|
||||
</template>
|
||||
</a-input-number>
|
||||
</a-form-item>
|
||||
<!-- SSH输出编码 -->
|
||||
<a-form-item field="charset"
|
||||
label="SSH输出编码"
|
||||
:hide-asterisk="true">
|
||||
<a-input v-model="formModel.charset" placeholder="请输入 SSH 输出编码" />
|
||||
</a-form-item>
|
||||
<!-- 文件名称编码 -->
|
||||
<a-form-item field="fileNameCharset"
|
||||
label="文件名称编码"
|
||||
:hide-asterisk="true">
|
||||
<a-input v-model="formModel.fileNameCharset" placeholder="请输入 SFTP 文件名称编码" />
|
||||
</a-form-item>
|
||||
<!-- 文件内容编码 -->
|
||||
<a-form-item field="fileContentCharset"
|
||||
label="文件内容编码"
|
||||
:hide-asterisk="true">
|
||||
<a-input v-model="formModel.fileContentCharset" placeholder="请输入 SFTP 文件内容编码" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<!-- 操作按钮 -->
|
||||
<div class="config-button-group">
|
||||
<a-space>
|
||||
<a-button size="small"
|
||||
@click="resetConfig">
|
||||
重置
|
||||
</a-button>
|
||||
<a-button type="primary"
|
||||
size="small"
|
||||
@click="saveConfig">
|
||||
保存
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'sshConfigForm'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FieldRule } from '@arco-design/web-vue';
|
||||
import type { HostSshConfig } from './types/const';
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
import { updateHostConfigStatus, updateHostConfig } from '@/api/asset/host-config';
|
||||
import { sshAuthTypeKey, sshOsTypeKey, SshAuthType, SshOsType } from './types/const';
|
||||
import rules from './types/form.rules';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { useDictStore } from '@/store';
|
||||
import { EnabledStatus } from '@/types/const';
|
||||
import { HostConfigType } from '../../../types/const';
|
||||
import HostKeySelector from '@/components/asset/host-key/selector/index.vue';
|
||||
import HostIdentitySelector from '@/components/asset/host-identity/selector/index.vue';
|
||||
|
||||
const { loading, setLoading } = useLoading();
|
||||
const { toOptions, toRadioOptions } = useDictStore();
|
||||
|
||||
const props = defineProps<{
|
||||
content: any;
|
||||
hostId: number;
|
||||
}>();
|
||||
|
||||
const emits = defineEmits(['submitted']);
|
||||
|
||||
const config = reactive({
|
||||
status: undefined,
|
||||
version: undefined,
|
||||
});
|
||||
|
||||
const formRef = ref();
|
||||
const formModel = ref<HostSshConfig>({
|
||||
osType: SshOsType.LINUX,
|
||||
username: undefined,
|
||||
port: undefined,
|
||||
password: undefined,
|
||||
authType: SshAuthType.PASSWORD,
|
||||
keyId: undefined,
|
||||
identityId: undefined,
|
||||
connectTimeout: undefined,
|
||||
charset: undefined,
|
||||
fileNameCharset: undefined,
|
||||
fileContentCharset: undefined,
|
||||
useNewPassword: false,
|
||||
hasPassword: false,
|
||||
});
|
||||
|
||||
// 监听数据变化
|
||||
watch(() => props.content, (v: any) => {
|
||||
config.status = v?.status;
|
||||
config.version = v?.version;
|
||||
resetConfig();
|
||||
});
|
||||
|
||||
// 用户名验证
|
||||
const usernameRules = [{
|
||||
validator: (value, cb) => {
|
||||
if (value && value.length > 128) {
|
||||
cb('用户名长度不能大于128位');
|
||||
return;
|
||||
}
|
||||
if (formModel.value.authType !== SshAuthType.IDENTITY && !value) {
|
||||
cb('请输入用户名');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}] as FieldRule[];
|
||||
|
||||
// 密码验证
|
||||
const passwordRules = [{
|
||||
validator: (value, cb) => {
|
||||
if (value && value.length > 256) {
|
||||
cb('密码长度不能大于256位');
|
||||
return;
|
||||
}
|
||||
if (formModel.value.useNewPassword && !value) {
|
||||
cb('请输入密码');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}] as FieldRule[];
|
||||
|
||||
// 修改状态
|
||||
const updateStatus = async (e: number) => {
|
||||
setLoading(true);
|
||||
return updateHostConfigStatus({
|
||||
hostId: props?.hostId,
|
||||
type: HostConfigType.SSH,
|
||||
status: e,
|
||||
version: config.version
|
||||
}).then(({ data }) => {
|
||||
config.version = data;
|
||||
setLoading(false);
|
||||
return true;
|
||||
}).catch(() => {
|
||||
setLoading(false);
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
// 重置配置
|
||||
const resetConfig = () => {
|
||||
formModel.value = Object.assign({}, props.content?.config);
|
||||
// 使用新密码默认为不包含密码
|
||||
formModel.value.useNewPassword = !formModel.value.hasPassword;
|
||||
};
|
||||
|
||||
// 保存配置
|
||||
const saveConfig = async () => {
|
||||
try {
|
||||
// 验证参数
|
||||
const error = await formRef.value.validate();
|
||||
if (error) {
|
||||
return false;
|
||||
}
|
||||
setLoading(true);
|
||||
// 更新
|
||||
const { data } = await updateHostConfig({
|
||||
hostId: props?.hostId,
|
||||
type: HostConfigType.SSH,
|
||||
version: config.version,
|
||||
config: JSON.stringify(formModel.value)
|
||||
});
|
||||
config.version = data;
|
||||
setLoading(false);
|
||||
Message.success('修改成功');
|
||||
// 回调 props
|
||||
emits('submitted', { ...props.content, ...config, config: { ...formModel.value } });
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.config-title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.config-form-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.config-button-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.auth-type-group {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.password-switch {
|
||||
width: 148px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,41 @@
|
||||
// 主机 SSH 配置
|
||||
export interface HostSshConfig {
|
||||
osType?: string;
|
||||
port?: number;
|
||||
username?: string;
|
||||
password?: string;
|
||||
authType?: string;
|
||||
identityId?: number;
|
||||
keyId?: number;
|
||||
connectTimeout?: number;
|
||||
charset?: string;
|
||||
fileNameCharset?: string;
|
||||
fileContentCharset?: string;
|
||||
useNewPassword?: boolean;
|
||||
hasPassword?: boolean;
|
||||
}
|
||||
|
||||
// 主机验证方式
|
||||
export const SshAuthType = {
|
||||
// 密码验证
|
||||
PASSWORD: 'PASSWORD',
|
||||
// 秘钥验证
|
||||
KEY: 'KEY',
|
||||
// 身份验证
|
||||
IDENTITY: 'IDENTITY'
|
||||
};
|
||||
|
||||
// 主机系统版本
|
||||
export const SshOsType = {
|
||||
LINUX: 'LINUX',
|
||||
WINDOWS: 'WINDOWS',
|
||||
};
|
||||
|
||||
// 主机验证方式 字典项
|
||||
export const sshAuthTypeKey = 'hostSshAuthType';
|
||||
|
||||
// 主机系统类型 字典项
|
||||
export const sshOsTypeKey = 'hostSshOsType';
|
||||
|
||||
// 加载的字典值
|
||||
export const dictKeys = [sshAuthTypeKey, sshOsTypeKey];
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { FieldRule } from '@arco-design/web-vue';
|
||||
|
||||
export const osType = [{
|
||||
required: true,
|
||||
message: '请选择系统类型'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const port = [{
|
||||
required: true,
|
||||
message: '请输入SSH端口'
|
||||
}, {
|
||||
type: 'number',
|
||||
min: 1,
|
||||
max: 65535,
|
||||
message: '输入的端口不合法'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const authType = [{
|
||||
required: true,
|
||||
message: '请选择认证方式'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const keyId = [{
|
||||
required: true,
|
||||
message: '请选择主机秘钥'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const identityId = [{
|
||||
required: true,
|
||||
message: '请选择主机身份'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const connectTimeout = [{
|
||||
required: true,
|
||||
message: '请输入连接超时时间'
|
||||
}, {
|
||||
type: 'number',
|
||||
min: 0,
|
||||
max: 100000,
|
||||
message: '连接超时时间需要在 0 - 100000 之间'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const charset = [{
|
||||
required: true,
|
||||
message: '请输入SSH输出编码'
|
||||
}, {
|
||||
maxLength: 12,
|
||||
message: 'SSH输出编码长度不能超过12位'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const fileNameCharset = [{
|
||||
required: true,
|
||||
message: '请输入文件名称编码'
|
||||
}, {
|
||||
maxLength: 12,
|
||||
message: '文件名称编码长度不能超过12位'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const fileContentCharset = [{
|
||||
required: true,
|
||||
message: '请输入SSH输出编码'
|
||||
}, {
|
||||
maxLength: 12,
|
||||
message: '文件内容编码长度不能超过12位'
|
||||
}] as FieldRule[];
|
||||
|
||||
export default {
|
||||
osType,
|
||||
port,
|
||||
authType,
|
||||
keyId,
|
||||
identityId,
|
||||
connectTimeout,
|
||||
charset,
|
||||
fileNameCharset,
|
||||
fileContentCharset,
|
||||
} as Record<string, FieldRule | FieldRule[]>;
|
||||
@@ -0,0 +1,219 @@
|
||||
<template>
|
||||
<a-drawer v-model:visible="visible"
|
||||
class="host-group-drawer"
|
||||
width="70%"
|
||||
title="主机分组配置"
|
||||
:esc-to-close="false"
|
||||
:mask-closable="false"
|
||||
:unmount-on-close="true"
|
||||
ok-text="保存"
|
||||
cancel-text="关闭"
|
||||
:ok-button-props="{ disabled: loading }"
|
||||
:cancel-button-props="{ disabled: loading }"
|
||||
:on-before-ok="saveHost"
|
||||
@cancel="handleCancel">
|
||||
<a-spin :loading="loading"
|
||||
class="host-group-container">
|
||||
<div class="host-group-wrapper">
|
||||
<!-- 左侧菜单 -->
|
||||
<div class="simple-card tree-card">
|
||||
<!-- 主机分组头部 -->
|
||||
<div class="tree-card-header">
|
||||
<!-- 标题 -->
|
||||
<div class="tree-card-title">
|
||||
主机分组
|
||||
</div>
|
||||
<!-- 操作 -->
|
||||
<div class="tree-card-handler">
|
||||
<div v-permission="['asset:host-group:create']"
|
||||
class="click-icon-wrapper handler-icon-wrapper"
|
||||
title="根节点添加"
|
||||
@click="addRootNode">
|
||||
<icon-plus />
|
||||
</div>
|
||||
<div class="click-icon-wrapper handler-icon-wrapper"
|
||||
title="刷新"
|
||||
@click="refreshTree">
|
||||
<icon-refresh />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 主机分组树 -->
|
||||
<host-group-tree outer-class="tree-card-main"
|
||||
ref="tree"
|
||||
:loading="loading"
|
||||
:editable="true"
|
||||
@set-loading="setLoading"
|
||||
@selected-node="selectGroup" />
|
||||
</div>
|
||||
<!-- 身体部分 -->
|
||||
<a-spin class="simple-card transfer-body"
|
||||
:loading="hostLoading">
|
||||
<host-transfer v-model="currentGroupHost"
|
||||
:group="currentGroup"
|
||||
@loading="setHostLoading" />
|
||||
</a-spin>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'hostGroupDrawer'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TreeNodeData } from '@arco-design/web-vue';
|
||||
import { ref } from 'vue';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { useCacheStore } from '@/store';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { updateHostGroupRel } from '@/api/asset/host-group';
|
||||
import HostTransfer from './host-transfer.vue';
|
||||
import HostGroupTree from '@/components/asset/host-group/tree/index.vue';
|
||||
|
||||
const { visible, setVisible } = useVisible();
|
||||
const { loading, setLoading } = useLoading();
|
||||
const { loading: hostLoading, setLoading: setHostLoading } = useLoading();
|
||||
|
||||
const cacheStore = useCacheStore();
|
||||
|
||||
const tree = ref();
|
||||
const currentGroup = ref();
|
||||
const currentGroupHost = ref<Array<string>>([]);
|
||||
|
||||
// 打开
|
||||
const open = async () => {
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
defineExpose({ open });
|
||||
|
||||
// 添加根节点
|
||||
const addRootNode = () => {
|
||||
tree.value.addRootNode();
|
||||
};
|
||||
|
||||
// 刷新树
|
||||
const refreshTree = () => {
|
||||
tree.value.fetchTreeData(true);
|
||||
};
|
||||
|
||||
// 选中分组
|
||||
const selectGroup = (group: TreeNodeData) => {
|
||||
currentGroup.value = group;
|
||||
};
|
||||
|
||||
// 保存主机
|
||||
const saveHost = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await updateHostGroupRel({
|
||||
groupId: currentGroup.value?.key as number,
|
||||
hostIdList: currentGroupHost.value
|
||||
});
|
||||
Message.success('保存成功');
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// 关闭
|
||||
const handleCancel = () => {
|
||||
setLoading(false);
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@tree-width: 33%;
|
||||
|
||||
.host-group-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--color-fill-2);
|
||||
padding: 16px;
|
||||
|
||||
.host-group-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-card {
|
||||
width: @tree-width;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 8px 0 16px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
border-bottom: 1px var(--color-border-2) solid;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&-title {
|
||||
color: rgba(var(--gray-10), .95);
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&-handler {
|
||||
display: flex;
|
||||
|
||||
.handler-icon-wrapper {
|
||||
margin-left: 2px;
|
||||
padding: 4px;
|
||||
font-size: 16px;
|
||||
background: unset;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-fill-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-main {
|
||||
padding: 8px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: calc(100% - 44px);
|
||||
}
|
||||
}
|
||||
|
||||
.transfer-body {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
padding: 12px 16px 16px 16px;
|
||||
width: calc(100% - @tree-width - 16px);
|
||||
position: absolute;
|
||||
left: calc(@tree-width + 16px);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.host-group-drawer {
|
||||
.arco-drawer-footer {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.arco-drawer-body {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<div class="transfer-container">
|
||||
<!-- 传输框 -->
|
||||
<a-transfer v-model="value"
|
||||
:data="data"
|
||||
:source-input-search-props="{ placeholder: '请输入主机名称/编码/IP' }"
|
||||
:target-input-search-props="{ placeholder: '请输入主机名称/编码/IP' }"
|
||||
:disabled="!group.key"
|
||||
show-search
|
||||
one-way>
|
||||
<!-- 主机列表 -->
|
||||
<template #source-title="{ countTotal, countSelected, checked, indeterminate, onSelectAllChange }">
|
||||
<!-- 左侧标题 -->
|
||||
<div class="source-title-container">
|
||||
<a-checkbox style="margin-right: 8px;"
|
||||
:model-value="checked"
|
||||
:indeterminate="indeterminate"
|
||||
@change="onSelectAllChange" />
|
||||
<span>
|
||||
主机列表 {{ countSelected }} / {{ countTotal }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 右侧标题 -->
|
||||
<template #target-title="{ countTotal, countSelected, onClear }">
|
||||
<div class="target-title-container">
|
||||
<span>已选择 <span class="span-blue">{{ countTotal }}</span> 个</span>
|
||||
<span class="pointer"
|
||||
@click="onClear"
|
||||
title="清空">
|
||||
<icon-delete />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 内容 -->
|
||||
<template #item="{ label }">
|
||||
<a-tooltip position="top"
|
||||
:mini="true"
|
||||
:content="label">
|
||||
<span v-html="renderLabel(label)" />
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</a-transfer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'hostTransfer'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TransferItem } from '@arco-design/web-vue/es/transfer/interface';
|
||||
import type { TreeNodeData } from '@arco-design/web-vue';
|
||||
import { onMounted, ref, watch, computed } from 'vue';
|
||||
import { useCacheStore } from '@/store';
|
||||
import { getHostGroupRelList } from '@/api/asset/host-group';
|
||||
|
||||
const props = withDefaults(defineProps<Partial<{
|
||||
modelValue: Array<string>;
|
||||
group: TreeNodeData;
|
||||
}>>(), {
|
||||
group: () => {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['loading', 'update:modelValue']);
|
||||
|
||||
const cacheStore = useCacheStore();
|
||||
|
||||
const data = ref<Array<TransferItem>>([]);
|
||||
|
||||
const value = computed<Array<string>>({
|
||||
get() {
|
||||
return props.modelValue as Array<string>;
|
||||
},
|
||||
set(e) {
|
||||
if (e) {
|
||||
emits('update:modelValue', e);
|
||||
} else {
|
||||
emits('update:modelValue', []);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 渲染 label
|
||||
const renderLabel = (label: string) => {
|
||||
const last = label.lastIndexOf('-');
|
||||
const prefix = label.substring(0, last - 1);
|
||||
const address = label.substring(last + 2, label.length);
|
||||
return `${prefix} - <span class="span-blue">${address}</span>`;
|
||||
};
|
||||
|
||||
// 查询组内数据
|
||||
watch(() => props.group?.key, async (groupId) => {
|
||||
if (groupId) {
|
||||
// 加载组内数据
|
||||
try {
|
||||
emits('loading', true);
|
||||
const { data } = await getHostGroupRelList(groupId as number);
|
||||
value.value = data.map(String);
|
||||
} catch (e) {
|
||||
} finally {
|
||||
emits('loading', false);
|
||||
}
|
||||
} else {
|
||||
// 重置
|
||||
value.value = [];
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
cacheStore.loadHosts().then(hosts => {
|
||||
data.value = hosts.map(s => {
|
||||
return {
|
||||
value: String(s.id),
|
||||
label: `${s.name} - ${s.address}`,
|
||||
disabled: false
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.transfer-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.arco-transfer) {
|
||||
height: 100%;
|
||||
|
||||
.arco-transfer-view {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.arco-transfer-view-source {
|
||||
|
||||
.arco-transfer-list-item .arco-checkbox {
|
||||
width: calc(100% - 24px);
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
.arco-transfer-view-target {
|
||||
|
||||
.arco-transfer-list-item-content {
|
||||
margin-left: 4px;
|
||||
position: absolute;
|
||||
width: calc(100% - 52px);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.arco-transfer-list-item-remove-btn {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.source-title-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.target-title-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,276 @@
|
||||
<template>
|
||||
<card-list v-model:searchValue="formModel.searchValue"
|
||||
search-input-placeholder="输入 id / 名称 / 编码 / 地址"
|
||||
:loading="loading"
|
||||
:fieldConfig="fieldConfig"
|
||||
:list="list"
|
||||
:pagination="pagination"
|
||||
:card-layout-cols="cardColLayout"
|
||||
:filter-count="filterCount"
|
||||
:add-permission="['asset:host:create']"
|
||||
@add="emits('openAdd')"
|
||||
@reset="reset"
|
||||
@search="fetchCardData"
|
||||
@page-change="fetchCardData">
|
||||
<!-- 左侧操作 -->
|
||||
<template #leftHandle>
|
||||
<!-- 主机分组 -->
|
||||
<a-button v-permission="['asset:host-group:update']"
|
||||
class="card-header-icon-wrapper"
|
||||
@click="emits('openHostGroup')">
|
||||
主机分组
|
||||
<template #icon>
|
||||
<icon-layers />
|
||||
</template>
|
||||
</a-button>
|
||||
<!-- 角色授权 -->
|
||||
<a-button v-permission="['asset:host-group:grant']"
|
||||
class="card-header-icon-wrapper"
|
||||
@click="$router.push({ name: GrantRouteName, query: { key: GrantKey.HOST_GROUP_ROLE }})">
|
||||
角色授权
|
||||
<template #icon>
|
||||
<icon-user-group />
|
||||
</template>
|
||||
</a-button>
|
||||
<!-- 用户授权 -->
|
||||
<a-button v-permission="['asset:host-group:grant']"
|
||||
class="card-header-icon-wrapper"
|
||||
@click="$router.push({ name: GrantRouteName, query: { key: GrantKey.HOST_GROUP_USER }})">
|
||||
用户授权
|
||||
<template #icon>
|
||||
<icon-user />
|
||||
</template>
|
||||
</a-button>
|
||||
</template>
|
||||
<!-- 过滤条件 -->
|
||||
<template #filterContent>
|
||||
<a-form :model="formModel"
|
||||
class="card-filter-form"
|
||||
size="small"
|
||||
ref="formRef"
|
||||
label-align="right"
|
||||
:auto-label-width="true"
|
||||
@keyup.enter="() => fetchCardData()">
|
||||
<!-- id -->
|
||||
<a-form-item field="id" label="主机id">
|
||||
<a-input-number v-model="formModel.id"
|
||||
placeholder="请输入主机id"
|
||||
allow-clear
|
||||
hide-button />
|
||||
</a-form-item>
|
||||
<!-- 主机名称 -->
|
||||
<a-form-item field="name" label="主机名称">
|
||||
<a-input v-model="formModel.name" placeholder="请输入主机名称" allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 主机编码 -->
|
||||
<a-form-item field="code" label="主机编码">
|
||||
<a-input v-model="formModel.code" placeholder="请输入主机编码" allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 主机地址 -->
|
||||
<a-form-item field="address" label="主机地址">
|
||||
<a-input v-model="formModel.address" placeholder="请输入主机地址" allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 主机标签 -->
|
||||
<a-form-item field="tags" label="主机标签">
|
||||
<tag-multi-selector v-model="formModel.tags"
|
||||
:limit="0"
|
||||
type="HOST"
|
||||
:tagColor="tagColor"
|
||||
placeholder="请选择主机标签" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
<!-- 标题 -->
|
||||
<template #title="{ record }">
|
||||
{{ record.name }}
|
||||
</template>
|
||||
<!-- 编码 -->
|
||||
<template #code="{ record }">
|
||||
{{ record.code }}
|
||||
</template>
|
||||
<!-- 地址 -->
|
||||
<template #address="{ record }">
|
||||
<span class="span-blue text-copy" @click="copy(record.address)">
|
||||
{{ record.address }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- 标签 -->
|
||||
<template #tags="{ record }">
|
||||
<a-space v-if="record.tags" wrap style="margin-bottom: -8px;">
|
||||
<a-tag v-for="tag in record.tags"
|
||||
:key="tag.id"
|
||||
:color="dataColor(tag.name, tagColor)">
|
||||
{{ tag.name }}
|
||||
</a-tag>
|
||||
</a-space>
|
||||
</template>
|
||||
<!-- 拓展操作 -->
|
||||
<template #extra="{ record }">
|
||||
<a-space>
|
||||
<!-- 更多操作 -->
|
||||
<a-dropdown trigger="hover">
|
||||
<icon-more class="card-extra-icon" />
|
||||
<template #content>
|
||||
<!-- 修改 -->
|
||||
<a-doption v-permission="['asset:host:update']"
|
||||
@click="emits('openUpdate', record)">
|
||||
<icon-edit />
|
||||
修改
|
||||
</a-doption>
|
||||
<!-- 配置 -->
|
||||
<a-doption v-permission="['asset:host:update-config']"
|
||||
@click="emits('openUpdateConfig', record)">
|
||||
<icon-settings />
|
||||
配置
|
||||
</a-doption>
|
||||
<!-- 删除 -->
|
||||
<a-doption v-permission="['asset:host:delete']"
|
||||
class="span-red"
|
||||
@click="deleteRow(record.id)">
|
||||
<icon-delete />
|
||||
删除
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</template>
|
||||
<!-- 右键菜单 -->
|
||||
<template #contextMenu="{ record }">
|
||||
<!-- 修改 -->
|
||||
<a-doption v-permission="['asset:host:update']"
|
||||
@click="emits('openUpdate', record)">
|
||||
<icon-edit />
|
||||
修改
|
||||
</a-doption>
|
||||
<!-- 配置 -->
|
||||
<a-doption v-permission="['asset:host:update-config']"
|
||||
@click="emits('openUpdateConfig', record)">
|
||||
<icon-settings />
|
||||
配置
|
||||
</a-doption>
|
||||
<!-- 删除 -->
|
||||
<a-doption v-permission="['asset:host:delete']"
|
||||
class="span-red"
|
||||
@click="deleteRow(record.id)">
|
||||
<icon-delete />
|
||||
删除
|
||||
</a-doption>
|
||||
</template>
|
||||
</card-list>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'hostCardList'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { HostQueryRequest, HostQueryResponse } from '@/api/asset/host';
|
||||
import { usePagination, useColLayout } from '@/types/card';
|
||||
import { computed, reactive, ref, onMounted } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { dataColor, objectTruthKeyCount, resetObject } from '@/utils';
|
||||
import fieldConfig from '../types/card.fields';
|
||||
import { deleteHost, getHostPage } from '@/api/asset/host';
|
||||
import { Message, Modal } from '@arco-design/web-vue';
|
||||
import { tagColor } from '../types/const';
|
||||
import { copy } from '@/hooks/copy';
|
||||
import { GrantKey, GrantRouteName } from '@/views/asset/grant/types/const';
|
||||
import TagMultiSelector from '@/components/meta/tag/multi-selector/index.vue';
|
||||
|
||||
const emits = defineEmits(['openAdd', 'openUpdate', 'openUpdateConfig', 'openHostGroup']);
|
||||
|
||||
const list = ref<HostQueryResponse[]>([]);
|
||||
|
||||
const cardColLayout = useColLayout();
|
||||
const pagination = usePagination();
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
const formRef = ref();
|
||||
const formModel = reactive<HostQueryRequest>({
|
||||
searchValue: undefined,
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
code: undefined,
|
||||
address: undefined,
|
||||
tags: undefined,
|
||||
queryTag: true
|
||||
});
|
||||
|
||||
// 条件数量
|
||||
const filterCount = computed(() => {
|
||||
return objectTruthKeyCount(formModel, ['searchValue', 'queryTag']);
|
||||
});
|
||||
|
||||
// 删除当前行
|
||||
const deleteRow = (id: number) => {
|
||||
Modal.confirm({
|
||||
title: '删除前确认!',
|
||||
titleAlign: 'start',
|
||||
content: '确定要删除这条记录吗?',
|
||||
okText: '删除',
|
||||
onOk: async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 调用删除接口
|
||||
await deleteHost(id);
|
||||
Message.success('删除成功');
|
||||
// 重新加载数据
|
||||
fetchCardData();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 添加后回调
|
||||
const addedCallback = () => {
|
||||
fetchCardData();
|
||||
};
|
||||
|
||||
// 更新后回调
|
||||
const updatedCallback = () => {
|
||||
fetchCardData();
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
addedCallback, updatedCallback
|
||||
});
|
||||
|
||||
// 重置条件
|
||||
const reset = () => {
|
||||
resetObject(formModel, ['queryTag']);
|
||||
fetchCardData();
|
||||
};
|
||||
|
||||
// 加载数据
|
||||
const doFetchCardData = async (request: HostQueryRequest) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data } = await getHostPage(request);
|
||||
list.value = data.rows;
|
||||
pagination.total = data.total;
|
||||
pagination.current = request.page;
|
||||
pagination.pageSize = request.limit;
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 切换页码
|
||||
const fetchCardData = (page = 1, limit = pagination.pageSize, form = formModel) => {
|
||||
doFetchCardData({ page, limit, ...form });
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchCardData();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<a-modal v-model:visible="visible"
|
||||
body-class="modal-form-large"
|
||||
title-align="start"
|
||||
:title="title"
|
||||
:top="80"
|
||||
:align-center="false"
|
||||
:draggable="true"
|
||||
:mask-closable="false"
|
||||
:unmount-on-close="true"
|
||||
:ok-button-props="{ disabled: loading }"
|
||||
:cancel-button-props="{ disabled: loading }"
|
||||
:on-before-ok="handlerOk"
|
||||
@close="handleClose">
|
||||
<a-spin class="full" :loading="loading">
|
||||
<a-form :model="formModel"
|
||||
ref="formRef"
|
||||
label-align="right"
|
||||
:auto-label-width="true"
|
||||
:rules="formRules">
|
||||
<!-- 主机名称 -->
|
||||
<a-form-item field="name" label="主机名称">
|
||||
<a-input v-model="formModel.name" placeholder="请输入主机名称" />
|
||||
</a-form-item>
|
||||
<!-- 主机编码 -->
|
||||
<a-form-item field="code" label="主机编码">
|
||||
<a-input v-model="formModel.code" placeholder="请输入主机编码" />
|
||||
</a-form-item>
|
||||
<!-- 主机地址 -->
|
||||
<a-form-item field="address" label="主机地址">
|
||||
<a-input v-model="formModel.address" placeholder="请输入主机地址" />
|
||||
</a-form-item>
|
||||
<!-- 主机分组 -->
|
||||
<a-form-item field="groupIdList" label="主机分组">
|
||||
<host-group-tree-selector v-model="formModel.groupIdList"
|
||||
placeholder="请选择主机分组" />
|
||||
</a-form-item>
|
||||
<!-- 主机标签 -->
|
||||
<a-form-item field="tags" label="主机标签">
|
||||
<tag-multi-selector v-model="formModel.tags"
|
||||
:allowCreate="true"
|
||||
:limit="5"
|
||||
type="HOST"
|
||||
:tagColor="tagColor"
|
||||
placeholder="请选择主机标签"
|
||||
@on-limited="onLimitedTag" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'hostFormModal'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { HostUpdateRequest } from '@/api/asset/host';
|
||||
import { ref } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import formRules from '../types/form.rules';
|
||||
import { createHost, getHost, updateHost } from '@/api/asset/host';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { pick } from 'lodash';
|
||||
import { tagColor } from '@/views/asset/host-list/types/const';
|
||||
import TagMultiSelector from '@/components/meta/tag/multi-selector/index.vue';
|
||||
import HostGroupTreeSelector from '@/components/asset/host-group/tree-selector/index.vue';
|
||||
|
||||
const { visible, setVisible } = useVisible();
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
const title = ref<string>();
|
||||
const isAddHandle = ref<boolean>(true);
|
||||
|
||||
const defaultForm = (): HostUpdateRequest => {
|
||||
return {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
code: undefined,
|
||||
address: undefined,
|
||||
tags: undefined,
|
||||
groupIdList: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const formRef = ref();
|
||||
const formModel = ref<HostUpdateRequest>({});
|
||||
|
||||
const emits = defineEmits(['added', 'updated']);
|
||||
|
||||
// 打开新增
|
||||
const openAdd = () => {
|
||||
title.value = '添加主机';
|
||||
isAddHandle.value = true;
|
||||
renderForm({ ...defaultForm() });
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
// 打开修改
|
||||
const openUpdate = async (id: number) => {
|
||||
title.value = '修改主机';
|
||||
isAddHandle.value = false;
|
||||
renderForm({ ...defaultForm() });
|
||||
setVisible(true);
|
||||
await fetchHostRender(id);
|
||||
};
|
||||
|
||||
// 渲染主机
|
||||
const fetchHostRender = async (id: number) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data } = await getHost(id);
|
||||
const detail = Object.assign({} as Record<string, any>,
|
||||
pick(data, 'id', 'name', 'code', 'address', 'groupIdList'));
|
||||
// tag
|
||||
const tags = (data.tags || []).map(s => s.id);
|
||||
// 渲染
|
||||
renderForm({ ...detail, tags });
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 渲染表单
|
||||
const renderForm = (record: any) => {
|
||||
formModel.value = Object.assign({}, record);
|
||||
};
|
||||
|
||||
defineExpose({ openAdd, openUpdate });
|
||||
|
||||
// tag 超出所选限制
|
||||
const onLimitedTag = (count: number, message: string) => {
|
||||
formRef.value.setFields({
|
||||
tags: {
|
||||
status: 'error',
|
||||
message
|
||||
}
|
||||
});
|
||||
// 因为输入框已经限制数量 这里只做提示
|
||||
setTimeout(() => {
|
||||
formRef.value.clearValidate('tags');
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
// 确定
|
||||
const handlerOk = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 验证参数
|
||||
const error = await formRef.value.validate();
|
||||
if (error) {
|
||||
return false;
|
||||
}
|
||||
if (isAddHandle.value) {
|
||||
// 新增
|
||||
await createHost(formModel.value);
|
||||
Message.success('创建成功');
|
||||
emits('added');
|
||||
} else {
|
||||
// 修改
|
||||
await updateHost(formModel.value);
|
||||
Message.success('修改成功');
|
||||
emits('updated');
|
||||
}
|
||||
// 清空
|
||||
handlerClear();
|
||||
} catch (e) {
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭
|
||||
const handleClose = () => {
|
||||
handlerClear();
|
||||
};
|
||||
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,259 @@
|
||||
<template>
|
||||
<!-- 搜索 -->
|
||||
<a-card class="general-card table-search-card">
|
||||
<query-header :model="formModel"
|
||||
label-align="left"
|
||||
@submit="fetchTableData"
|
||||
@reset="fetchTableData"
|
||||
@keyup.enter="() => fetchTableData()">
|
||||
<!-- id -->
|
||||
<a-form-item field="id" label="主机id">
|
||||
<a-input-number v-model="formModel.id"
|
||||
placeholder="请输入主机id"
|
||||
allow-clear
|
||||
hide-button />
|
||||
</a-form-item>
|
||||
<!-- 主机名称 -->
|
||||
<a-form-item field="name" label="主机名称">
|
||||
<a-input v-model="formModel.name" placeholder="请输入主机名称" allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 主机编码 -->
|
||||
<a-form-item field="code" label="主机编码">
|
||||
<a-input v-model="formModel.code" placeholder="请输入主机编码" allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 主机地址 -->
|
||||
<a-form-item field="address" label="主机地址">
|
||||
<a-input v-model="formModel.address" placeholder="请输入主机地址" allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 主机标签 -->
|
||||
<a-form-item field="tags" label="主机标签">
|
||||
<tag-multi-selector v-model="formModel.tags"
|
||||
ref="tagSelector"
|
||||
:limit="0"
|
||||
type="HOST"
|
||||
:tagColor="tagColor"
|
||||
placeholder="请选择主机标签" />
|
||||
</a-form-item>
|
||||
</query-header>
|
||||
</a-card>
|
||||
<!-- 表格 -->
|
||||
<a-card class="general-card table-card">
|
||||
<template #title>
|
||||
<!-- 左侧操作 -->
|
||||
<div class="table-left-bar-handle">
|
||||
<!-- 标题 -->
|
||||
<div class="table-title">
|
||||
主机列表
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧操作 -->
|
||||
<div class="table-right-bar-handle">
|
||||
<a-space>
|
||||
<!-- 主机分组 -->
|
||||
<a-button type="primary"
|
||||
v-permission="['asset:host-group:update']"
|
||||
@click="emits('openHostGroup')">
|
||||
主机分组
|
||||
<template #icon>
|
||||
<icon-layers />
|
||||
</template>
|
||||
</a-button>
|
||||
<!-- 角色授权 -->
|
||||
<a-button type="primary"
|
||||
v-permission="['asset:host-group:grant']"
|
||||
@click="$router.push({ name: GrantRouteName, query: { key: GrantKey.HOST_GROUP_ROLE }})">
|
||||
角色授权
|
||||
<template #icon>
|
||||
<icon-user-group />
|
||||
</template>
|
||||
</a-button>
|
||||
<!-- 用户授权 -->
|
||||
<a-button type="primary"
|
||||
v-permission="['asset:host-group:grant']"
|
||||
@click="$router.push({ name: GrantRouteName, query: { key: GrantKey.HOST_GROUP_USER }})">
|
||||
用户授权
|
||||
<template #icon>
|
||||
<icon-user />
|
||||
</template>
|
||||
</a-button>
|
||||
<!-- 新增 -->
|
||||
<a-button type="primary"
|
||||
v-permission="['asset:host:create']"
|
||||
@click="emits('openAdd')">
|
||||
新增
|
||||
<template #icon>
|
||||
<icon-plus />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
<!-- table -->
|
||||
<a-table row-key="id"
|
||||
ref="tableRef"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="tableRenderData"
|
||||
:pagination="pagination"
|
||||
@page-change="(page) => fetchTableData(page, pagination.pageSize)"
|
||||
@page-size-change="(size) => fetchTableData(1, size)"
|
||||
:bordered="false">
|
||||
<!-- 编码 -->
|
||||
<template #code="{ record }">
|
||||
<a-tag>{{ record.code }}</a-tag>
|
||||
</template>
|
||||
<!-- 地址 -->
|
||||
<template #address="{ record }">
|
||||
<span class="span-blue text-copy"
|
||||
title="复制"
|
||||
@click="copy(record.address)">
|
||||
{{ record.address }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- 标签 -->
|
||||
<template #tags="{ record }">
|
||||
<a-space v-if="record.tags"
|
||||
style="margin-bottom: -8px;"
|
||||
:wrap="true">
|
||||
<a-tag v-for="tag in record.tags"
|
||||
:key="tag.id"
|
||||
:color="dataColor(tag.name, tagColor)">
|
||||
{{ tag.name }}
|
||||
</a-tag>
|
||||
</a-space>
|
||||
</template>
|
||||
<!-- 操作 -->
|
||||
<template #handle="{ record }">
|
||||
<div class="table-handle-wrapper row-handle-wrapper">
|
||||
<!-- 修改 -->
|
||||
<a-button type="text"
|
||||
size="mini"
|
||||
v-permission="['asset:host:update']"
|
||||
@click="emits('openUpdate', record)">
|
||||
修改
|
||||
</a-button>
|
||||
<!-- 配置 -->
|
||||
<a-button type="text"
|
||||
size="mini"
|
||||
v-permission="['asset:host:update-config']"
|
||||
@click="emits('openUpdateConfig', record)">
|
||||
配置
|
||||
</a-button>
|
||||
<!-- 删除 -->
|
||||
<a-popconfirm content="确认删除这条记录吗?"
|
||||
position="left"
|
||||
type="warning"
|
||||
@ok="deleteRow(record)">
|
||||
<a-button v-permission="['asset:host:delete']"
|
||||
type="text"
|
||||
size="mini"
|
||||
status="danger">
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'hostTable'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { HostQueryRequest, HostQueryResponse } from '@/api/asset/host';
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import { deleteHost, getHostPage } from '@/api/asset/host';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { tagColor } from '../types/const';
|
||||
import { usePagination } from '@/types/table';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { copy } from '@/hooks/copy';
|
||||
import columns from '../types/table.columns';
|
||||
import { dataColor } from '@/utils';
|
||||
import { GrantKey, GrantRouteName } from '@/views/asset/grant/types/const';
|
||||
import TagMultiSelector from '@/components/meta/tag/multi-selector/index.vue';
|
||||
|
||||
const tagSelector = ref();
|
||||
const tableRenderData = ref<HostQueryResponse[]>([]);
|
||||
const { loading, setLoading } = useLoading();
|
||||
const emits = defineEmits(['openAdd', 'openUpdate', 'openUpdateConfig', 'openHostGroup']);
|
||||
|
||||
const pagination = usePagination();
|
||||
|
||||
const formModel = reactive<HostQueryRequest>({
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
code: undefined,
|
||||
address: undefined,
|
||||
tags: undefined,
|
||||
queryTag: true
|
||||
});
|
||||
|
||||
// 删除当前行
|
||||
const deleteRow = async ({ id }: {
|
||||
id: number
|
||||
}) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 调用删除接口
|
||||
await deleteHost(id);
|
||||
Message.success('删除成功');
|
||||
// 重新加载数据
|
||||
fetchTableData();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 添加后回调
|
||||
const addedCallback = () => {
|
||||
fetchTableData();
|
||||
};
|
||||
|
||||
// 更新后回调
|
||||
const updatedCallback = () => {
|
||||
fetchTableData();
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
addedCallback, updatedCallback
|
||||
});
|
||||
|
||||
// 加载数据
|
||||
const doFetchTableData = async (request: HostQueryRequest) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data } = await getHostPage(request);
|
||||
tableRenderData.value = data.rows;
|
||||
pagination.total = data.total;
|
||||
pagination.current = request.page;
|
||||
pagination.pageSize = request.limit;
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 切换页码
|
||||
const fetchTableData = (page = 1, limit = pagination.pageSize, form = formModel) => {
|
||||
doFetchTableData({ page, limit, ...form });
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchTableData();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.row-handle-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
81
orion-visor-ui/src/views/asset/host-list/index.vue
Normal file
81
orion-visor-ui/src/views/asset/host-list/index.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div class="layout-container">
|
||||
<!-- 列表-表格 -->
|
||||
<host-table v-if="renderTable"
|
||||
ref="table"
|
||||
@open-host-group="() => hostGroup.open()"
|
||||
@open-add="() => modal.openAdd()"
|
||||
@open-update="(e) => modal.openUpdate(e.id)"
|
||||
@open-update-config="(e) => hostConfig.open(e)" />
|
||||
<!-- 列表-卡片 -->
|
||||
<host-card-list v-else
|
||||
ref="card"
|
||||
@open-host-group="() => hostGroup.open()"
|
||||
@open-add="() => modal.openAdd()"
|
||||
@open-update="(e) => modal.openUpdate(e.id)"
|
||||
@open-update-config="(e) => hostConfig.open(e)" />
|
||||
<!-- 添加修改模态框 -->
|
||||
<host-form-modal ref="modal"
|
||||
@added="modalAddCallback"
|
||||
@updated="modalUpdateCallback" />
|
||||
<!-- 配置面板 -->
|
||||
<host-config-drawer ref="hostConfig" />
|
||||
<!-- 分组配置 -->
|
||||
<host-group-drawer ref="hostGroup" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'hostList'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, onUnmounted } from 'vue';
|
||||
import { useAppStore, useCacheStore } from '@/store';
|
||||
import HostTable from './components/host-table.vue';
|
||||
import HostCardList from './components/host-card-list.vue';
|
||||
import HostFormModal from './components/host-form-modal.vue';
|
||||
import HostConfigDrawer from './components/config/host-config-drawer.vue';
|
||||
import HostGroupDrawer from './components/group/host-group-drawer.vue';
|
||||
|
||||
const table = ref();
|
||||
const card = ref();
|
||||
const modal = ref();
|
||||
const hostConfig = ref();
|
||||
const hostGroup = ref();
|
||||
|
||||
const appStore = useAppStore();
|
||||
const cacheStore = useCacheStore();
|
||||
|
||||
const renderTable = computed(() => appStore.hostView === 'table');
|
||||
|
||||
// 添加回调
|
||||
const modalAddCallback = () => {
|
||||
if (renderTable.value) {
|
||||
table.value.addedCallback();
|
||||
} else {
|
||||
card.value.addedCallback();
|
||||
}
|
||||
};
|
||||
|
||||
// 修改回调
|
||||
const modalUpdateCallback = () => {
|
||||
if (renderTable.value) {
|
||||
table.value.updatedCallback();
|
||||
} else {
|
||||
card.value.updatedCallback();
|
||||
}
|
||||
};
|
||||
|
||||
// 卸载时清除 cache
|
||||
onUnmounted(() => {
|
||||
cacheStore.reset('hosts', 'hostKeys', 'hostIdentities', 'hostGroups', 'HOST_Tags');
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { CardField, CardFieldConfig } from '@/types/card';
|
||||
|
||||
const fieldConfig = {
|
||||
rowGap: '10px',
|
||||
labelSpan: 8,
|
||||
minHeight: '22px',
|
||||
fields: [
|
||||
{
|
||||
label: 'id',
|
||||
dataIndex: 'id',
|
||||
slotName: 'id',
|
||||
}, {
|
||||
label: '主机编码',
|
||||
dataIndex: 'code',
|
||||
slotName: 'code',
|
||||
}, {
|
||||
label: '主机地址',
|
||||
dataIndex: 'address',
|
||||
slotName: 'address',
|
||||
tooltip: true,
|
||||
}, {
|
||||
label: '主机标签',
|
||||
dataIndex: 'tags',
|
||||
slotName: 'tags',
|
||||
rowAlign: 'start',
|
||||
},
|
||||
] as CardField[]
|
||||
} as CardFieldConfig;
|
||||
export default fieldConfig;
|
||||
22
orion-visor-ui/src/views/asset/host-list/types/const.ts
Normal file
22
orion-visor-ui/src/views/asset/host-list/types/const.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { HostSshConfig } from '../components/config/ssh/types/const';
|
||||
|
||||
// 主机所有配置
|
||||
export interface HostConfigWrapper {
|
||||
ssh: HostSshConfig;
|
||||
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// 主机配置类型
|
||||
export const HostConfigType = {
|
||||
SSH: 'ssh'
|
||||
};
|
||||
|
||||
// tag 颜色
|
||||
export const tagColor = [
|
||||
'arcoblue',
|
||||
'green',
|
||||
'purple',
|
||||
'pinkpurple',
|
||||
'magenta'
|
||||
];
|
||||
37
orion-visor-ui/src/views/asset/host-list/types/form.rules.ts
Normal file
37
orion-visor-ui/src/views/asset/host-list/types/form.rules.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { FieldRule } from '@arco-design/web-vue';
|
||||
|
||||
export const name = [{
|
||||
required: true,
|
||||
message: '请输入主机名称'
|
||||
}, {
|
||||
maxLength: 64,
|
||||
message: '主机名称长度不能大于64位'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const code = [{
|
||||
required: true,
|
||||
message: '请输入主机编码'
|
||||
}, {
|
||||
maxLength: 64,
|
||||
message: '主机编码长度不能大于64位'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const address = [{
|
||||
required: true,
|
||||
message: '请输入主机地址'
|
||||
}, {
|
||||
maxLength: 128,
|
||||
message: '主机地址长度不能大于128位'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const tags = [{
|
||||
maxLength: 5,
|
||||
message: '最多选择5个标签'
|
||||
}] as FieldRule[];
|
||||
|
||||
export default {
|
||||
name,
|
||||
code,
|
||||
address,
|
||||
tags,
|
||||
} as Record<string, FieldRule | FieldRule[]>;
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'id',
|
||||
dataIndex: 'id',
|
||||
slotName: 'id',
|
||||
width: 70,
|
||||
align: 'left',
|
||||
fixed: 'left',
|
||||
}, {
|
||||
title: '主机名称',
|
||||
dataIndex: 'name',
|
||||
slotName: 'name',
|
||||
ellipsis: true,
|
||||
tooltip: true
|
||||
}, {
|
||||
title: '主机编码',
|
||||
dataIndex: 'code',
|
||||
slotName: 'code',
|
||||
}, {
|
||||
title: '主机地址',
|
||||
dataIndex: 'address',
|
||||
slotName: 'address',
|
||||
}, {
|
||||
title: '主机标签',
|
||||
dataIndex: 'tags',
|
||||
slotName: 'tags',
|
||||
align: 'left',
|
||||
}, {
|
||||
title: '操作',
|
||||
slotName: 'handle',
|
||||
width: 162,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
},
|
||||
] as TableColumnData[];
|
||||
|
||||
export default columns;
|
||||
Reference in New Issue
Block a user