feat: 新建连接视图.

This commit is contained in:
lijiahangmax
2023-12-17 17:56:10 +08:00
parent 5f485172e7
commit 856c80c28c
7 changed files with 608 additions and 529 deletions

View File

@@ -4,21 +4,20 @@
<div class="host-group-container">
<a-scrollbar>
<a-tree v-model:selected-keys="selectedGroup"
:data="hosts.groupTree"
:blockNode="true"
class="host-tree block-tree"
@select="chooseGroup">
:data="groupTree"
:blockNode="true">
<!-- 组内数量 -->
<template #extra="node">
<span class="node-host-count span-blue">{{ hosts?.treeNodes[node.key]?.length || 0 }}</span>
<span class="node-host-count span-blue">{{ treeNodes[node.key]?.length || 0 }}</span>
</template>
</a-tree>
</a-scrollbar>
</div>
<!-- 主机列表 -->
<host-list class="host-list"
:hostList="hosts.hostList"
empty-value="当前分组内无授权主机!" />
<host-list-view class="host-list"
:hostList="hostList"
empty-value="当前分组内无授权主机!" />
</div>
</template>
@@ -29,19 +28,28 @@
</script>
<script lang="ts" setup>
import type { AuthorizedHostQueryResponse } from '@/api/asset/asset-authorized-data';
import { ref } from 'vue';
import HostList from './host-list.vue';
import { computed } from 'vue';
import { HostQueryResponse } from '@/api/asset/host';
import { HostGroupQueryResponse } from '@/api/asset/host-group';
import HostListView from './host-list-view.vue';
const props = defineProps<{
hosts: AuthorizedHostQueryResponse
modelValue: number,
groupTree: Array<HostGroupQueryResponse>;
hostList: Array<HostQueryResponse>;
treeNodes: Record<string, Array<number>>;
}>();
const selectedGroup = ref([0]);
const emits = defineEmits(['update:modelValue']);
const chooseGroup = () => {
console.log(selectedGroup.value[0]);
};
const selectedGroup = computed({
get() {
return [props.modelValue];
},
set(e) {
emits('update:modelValue', e[0]);
}
});
</script>

View File

@@ -1,8 +1,115 @@
<template>
<!-- 主机列表 -->
<host-list class="list-view-container"
:hostList="hostList"
:empty-value="emptyValue" />
<div class="hosts-list-container">
<a-list size="large"
max-height="100%"
:hoverable="true"
:data="hostList">
<!-- 空数据 -->
<template #empty>
<a-empty>
<template #image>
<icon-desktop />
</template>
{{ emptyValue }}
</a-empty>
</template>
<!-- 数据 -->
<template #item="{ item }">
<a-list-item class="host-item-wrapper" @click="openTerminal(item)">
<div class="host-item">
<!-- 左侧图标-名称 -->
<div class="flex-center host-item-left">
<!-- 图标 -->
<span class="host-item-left-icon">
<icon-desktop />
</span>
<!-- 名称 -->
<a-tooltip position="top"
:mini="true"
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
:content="`${item.name} (${item.code})`">
<span class="host-item-text host-item-left-name">
{{ `${item.name} (${item.code})` }}
</span>
</a-tooltip>
</div>
<!-- 中间ip -->
<div class="flex-center host-item-center">
<!-- ip -->
<a-tooltip position="top"
:mini="true"
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
:content="item.address">
<span class="host-item-text host-item-center-address">
{{ item.address }}
</span>
</a-tooltip>
</div>
<!-- 右侧tag-操作 -->
<div class="flex-center host-item-right">
<!-- tags -->
<div class="host-item-right-tags">
<template v-if="item.tags?.length">
<a-tag v-for="(tag, i) in item.tags"
class="host-item-text"
:key="tag.id"
:style="{
maxWidth: `calc(${100 / item.tags.length}% - ${i !== item.tags.length - 1 ? '8px' : '0px'})`,
marginRight: `${i !== item.tags.length - 1 ? '8px' : '0'}`,
}"
:color="dataColor(tag.name, tagColor)">
{{ tag.name }}
</a-tag>
</template>
</div>
<!-- 操作 -->
<div class="host-item-right-actions">
<!-- 连接主机 -->
<a-tooltip position="top"
:mini="true"
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="连接主机">
<div class="terminal-sidebar-icon-wrapper">
<div class="terminal-sidebar-icon" @click.stop="openTerminal(item)">
<icon-thunderbolt />
</div>
</div>
</a-tooltip>
<!-- 连接设置 -->
<a-tooltip position="top"
:mini="true"
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="连接设置">
<div class="terminal-sidebar-icon-wrapper">
<div class="terminal-sidebar-icon" @click.stop="openSetting(item)">
<icon-settings />
</div>
</div>
</a-tooltip>
<!-- 收藏 -->
<a-tooltip position="top"
:mini="true"
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="收藏">
<div class="terminal-sidebar-icon-wrapper">
<div class="terminal-sidebar-icon" @click.stop="setFavorite(item)">
<icon-star-fill class="favorite" v-if="item.favorite" />
<icon-star v-else />
</div>
</div>
</a-tooltip>
</div>
</div>
</div>
</a-list-item>
</template>
</a-list>
</div>
</template>
<script lang="ts">
@@ -13,20 +120,148 @@
<script lang="ts" setup>
import type { HostQueryResponse } from '@/api/asset/host';
import HostList from './host-list.vue';
import useFavorite from '@/hooks/favorite';
import { dataColor } from '@/utils';
import { tagColor } from '@/views/asset/host-list/types/const';
const props = defineProps<{
hostList: Array<HostQueryResponse>,
emptyValue: string
}>();
const { toggle: toggleFavorite, loading: favoriteLoading } = useFavorite('HOST');
// 打开终端
const openTerminal = (item: any) => {
console.log('ter', item);
};
// 打开配置
const openSetting = (item: any) => {
console.log('set', item);
};
// 设置收藏
const setFavorite = async (item: any) => {
if (favoriteLoading.value) {
return;
}
await toggleFavorite(item, item.id);
};
</script>
<style lang="less" scoped>
.list-view-container {
max-height: 100%;
width: 100%;
overflow: auto;
position: relative;
@host-item-height: 56px;
:deep(.arco-list-bordered) {
border: 1px solid var(--color-fill-3);
.arco-empty {
padding: 16px 0;
flex-direction: column;
.arco-empty-image {
margin-bottom: 0;
}
}
.arco-list-item:not(:last-child) {
border-bottom: 1px solid var(--color-fill-3);
}
.arco-list-item:hover {
background-color: var(--color-fill-2);
}
}
.host-item-wrapper {
padding: 0 !important;
height: @host-item-height;
cursor: pointer;
font-size: 12px;
color: var(--color-content-text-2);
.host-item {
width: 100%;
padding: 0 18px;
display: flex;
justify-content: space-between;
align-items: center;
height: @host-item-height;
&-text {
display: inline-block;
white-space: pre;
word-break: break-all;
overflow: hidden;
text-overflow: ellipsis;
}
}
.host-item-left {
width: 35%;
&-icon {
width: 32px;
height: 32px;
border-radius: 32px;
margin-right: 10px;
font-size: 16px;
display: flex;
justify-content: center;
align-items: center;
color: var(--color-text-3);
background: var(--color-fill-3);
}
&-name {
max-width: calc(100% - 32px - 12px - 8px);
}
}
.host-item-center {
width: 25%;
&-address {
max-width: 100%;
}
}
.host-item-right {
width: 40%;
height: 100%;
flex-direction: column;
justify-content: center;
position: relative;
&-tags {
// 必须设置 最外层用的是 min-width
position: absolute;
width: 100%;
}
&-actions {
position: absolute;
display: none;
width: 100%;
justify-content: flex-end;
}
}
&:hover {
.host-item-right-tags {
display: none;
}
.host-item-right-actions {
display: flex;
}
}
}
.favorite {
color: rgb(var(--yellow-6));
}
</style>

View File

@@ -1,268 +0,0 @@
<template>
<div class="hosts-list-container">
<a-list size="large"
max-height="100%"
:hoverable="true"
:data="hostList">
<!-- 空数据 -->
<template #empty>
<a-empty>
<template #image>
<icon-desktop />
</template>
{{ emptyValue }}
</a-empty>
</template>
<!-- 数据 -->
<template #item="{ item }">
<a-list-item class="host-item-wrapper" @click="openTerminal(item)">
<div class="host-item">
<!-- 左侧图标-名称 -->
<div class="flex-center host-item-left">
<!-- 图标 -->
<span class="host-item-left-icon">
<icon-desktop />
</span>
<!-- 名称 -->
<a-tooltip position="top"
:mini="true"
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
:content="`${item.name} (${item.code})`">
<span class="host-item-text host-item-left-name">
{{ `${item.name} (${item.code})` }}
</span>
</a-tooltip>
</div>
<!-- 中间ip -->
<div class="flex-center host-item-center">
<!-- ip -->
<a-tooltip position="top"
:mini="true"
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
:content="item.address">
<span class="host-item-text host-item-center-address">
{{ item.address }}
</span>
</a-tooltip>
</div>
<!-- 右侧tag-操作 -->
<div class="flex-center host-item-right">
<!-- tags -->
<div class="host-item-right-tags">
<template v-if="item.tags?.length">
<a-tag v-for="(tag, i) in item.tags"
class="host-item-text"
:key="tag.id"
:style="{
maxWidth: `calc(${100 / item.tags.length}% - ${i !== item.tags.length - 1 ? '8px' : '0px'})`,
marginRight: `${i !== item.tags.length - 1 ? '8px' : '0'}`,
}"
:color="dataColor(tag.name, tagColor)">
{{ tag.name }}
</a-tag>
</template>
</div>
<!-- 操作 -->
<div class="host-item-right-actions">
<!-- 连接主机 -->
<a-tooltip position="top"
:mini="true"
:popup-visible="true"
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="连接主机">
<div class="terminal-sidebar-icon-wrapper">
<div class="terminal-sidebar-icon" @click.stop="openTerminal(item)">
<icon-thunderbolt />
</div>
</div>
</a-tooltip>
<!-- 连接设置 -->
<a-tooltip position="top"
:mini="true"
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="连接设置">
<div class="terminal-sidebar-icon-wrapper">
<div class="terminal-sidebar-icon" @click.stop="openSetting(item)">
<icon-settings />
</div>
</div>
</a-tooltip>
<!-- 收藏 -->
<a-tooltip position="top"
:mini="true"
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="收藏">
<div class="terminal-sidebar-icon-wrapper">
<div class="terminal-sidebar-icon" @click.stop="setFavorite(item)">
<icon-star-fill class="favorite" v-if="item.favorite" />
<icon-star v-else />
</div>
</div>
</a-tooltip>
</div>
</div>
</div>
</a-list-item>
</template>
</a-list>
</div>
</template>
<script lang="ts">
export default {
name: 'hostList'
};
</script>
<script lang="ts" setup>
import type { HostQueryResponse } from '@/api/asset/host';
import useFavorite from '@/hooks/favorite';
import { dataColor } from '@/utils';
import { tagColor } from '@/views/asset/host-list/types/const';
const props = defineProps<{
hostList: Array<HostQueryResponse>,
emptyValue: string
}>();
const { toggle: toggleFavorite, loading: favoriteLoading } = useFavorite('HOST');
// 打开终端
const openTerminal = (item: any) => {
console.log('ter', item);
};
// 打开配置
const openSetting = (item: any) => {
console.log('set', item);
};
// 设置收藏
const setFavorite = async (item: any) => {
if (favoriteLoading.value) {
return;
}
await toggleFavorite(item, item.id);
};
</script>
<style lang="less" scoped>
@host-item-height: 56px;
:deep(.arco-list-bordered) {
border: 1px solid var(--color-fill-3);
.arco-empty {
padding: 16px 0;
flex-direction: column;
.arco-empty-image {
margin-bottom: 0;
}
}
.arco-list-item:not(:last-child) {
border-bottom: 1px solid var(--color-fill-3);
}
.arco-list-item:hover {
background-color: var(--color-fill-2);
}
}
.host-item-wrapper {
padding: 0 !important;
height: @host-item-height;
cursor: pointer;
font-size: 12px;
color: var(--color-content-text-2);
.host-item {
width: 100%;
padding: 0 18px;
display: flex;
justify-content: space-between;
align-items: center;
height: @host-item-height;
&-text {
display: inline-block;
white-space: pre;
word-break: break-all;
overflow: hidden;
text-overflow: ellipsis;
}
}
.host-item-left {
width: 35%;
&-icon {
width: 32px;
height: 32px;
border-radius: 32px;
margin-right: 10px;
font-size: 16px;
display: flex;
justify-content: center;
align-items: center;
color: var(--color-text-3);
background: var(--color-fill-3);
}
&-name {
max-width: calc(100% - 32px - 12px - 8px);
}
}
.host-item-center {
width: 25%;
&-address {
max-width: 100%;
}
}
.host-item-right {
width: 40%;
height: 100%;
flex-direction: column;
justify-content: center;
position: relative;
&-tags {
// 必须设置 最外层用的是 min-width
position: absolute;
width: 100%;
}
&-actions {
position: absolute;
display: none;
width: 100%;
justify-content: flex-end;
}
}
&:hover {
.host-item-right-tags {
display: none;
}
.host-item-right-actions {
display: flex;
}
}
}
.favorite {
color: rgb(var(--yellow-6));
}
</style>

View File

@@ -0,0 +1,119 @@
<template>
<div>
<!-- 分组视图列表 -->
<host-group-view v-if="NewConnectionType.GROUP === newConnectionType"
v-model="selectedGroup"
:group-tree="hosts.groupTree"
:tree-nodes="treeNodes"
:host-list="hostList"
:filter-value="filterValue" />
<!-- 列表视图 -->
<host-list-view v-if="NewConnectionType.LIST === newConnectionType"
:hostList="hostList"
empty-value="无授权主机!" />
<!-- 我的收藏 -->
<host-list-view v-if="NewConnectionType.FAVORITE === newConnectionType"
class="list-view-container"
:hostList="hostList"
empty-value="无收藏记录, 快去点击主机右侧的进行收藏吧!" />
<!-- 最近连接 -->
<host-list-view v-if="NewConnectionType.LATEST === newConnectionType"
class="list-view-container"
:hostList="hostList"
empty-value="暂无连接记录, 快去体验吧!" />
</div>
</template>
<script lang="ts">
export default {
name: 'hostsView'
};
</script>
<script lang="ts" setup>
import { onMounted, ref, watch } from 'vue';
import { NewConnectionType } from '../../types/terminal.const';
import { AuthorizedHostQueryResponse } from '@/api/asset/asset-authorized-data';
import { HostQueryResponse } from '@/api/asset/host';
import HostGroupView from './host-group-view.vue';
import HostListView from './host-list-view.vue';
const props = defineProps<{
hosts: AuthorizedHostQueryResponse,
filterValue: string,
newConnectionType: string
}>();
const hostList = ref<Array<HostQueryResponse>>([]);
const treeNodes = ref<Record<string, Array<number>>>({});
const selectedGroup = ref(
props.hosts?.groupTree?.length
? props.hosts.groupTree[0].key
: 0
);
// 主机数据处理
const shuffleHosts = () => {
let list = [...props.hosts?.hostList];
// 过滤
const filterVal = props.filterValue.toLowerCase();
if (filterVal) {
list = filterVal.startsWith('@')
// tag 过滤
? list.filter(item => item.tags.some(tag => (tag.name as string).toLowerCase().startsWith(filterVal.substring(1, filterVal.length))))
// 名称/编码/地址 过滤
: list.filter(item => {
return (item.name as string)?.toLowerCase().indexOf(filterVal) > -1
|| (item.code as string)?.toLowerCase().indexOf(filterVal) > -1
|| (item.address as string)?.toLowerCase().indexOf(filterVal) > -1;
});
}
// 判断类型
if (NewConnectionType.GROUP === props.newConnectionType) {
// 过滤-分组
const groupNodes = { ...props.hosts.treeNodes };
Object.keys(groupNodes).forEach(k => {
groupNodes[k] = (groupNodes[k] || []).filter(item => list.some(host => host.id === item));
});
treeNodes.value = groupNodes;
// 当前组内数据
list = list.filter(item => groupNodes[selectedGroup.value]?.some(id => id === item.id));
} else if (NewConnectionType.FAVORITE === props.newConnectionType) {
// 过滤-个人收藏
list = list.filter(item => item.favorite);
}
// 排序
hostList.value = list?.sort((o1, o2) => {
if (o1.favorite || o2.favorite) {
if (o1.favorite && o2.favorite) {
return o2.id < o1.id ? 1 : -1;
}
return o2.favorite ? 1 : -1;
} else {
return o2.id < o1.id ? 1 : -1;
}
});
};
// 监听搜索值变化
watch(() => props.filterValue, shuffleHosts);
// 监听类型变化
watch(() => props.newConnectionType, shuffleHosts);
// 监听分组变化
watch(selectedGroup, shuffleHosts);
// 初始化 加载主机
onMounted(shuffleHosts);
</script>
<style lang="less" scoped>
.list-view-container {
max-height: 100%;
width: 100%;
overflow: auto;
position: relative;
}
</style>

View File

@@ -17,16 +17,15 @@
placeholder="输入名称/编码/IP @标签"
:allow-clear="true"
:data="filterOptions"
:filter-option="searchFilter"
@change="shuffleHosts">
<template #option="{ data: { raw: { value, isTag} } }">
:filter-option="searchFilter">
<template #option="{ data: { raw: { label, isTag} } }">
<!-- tag -->
<a-tag v-if="isTag" :color="dataColor(value, tagColor)">
{{ value }}
<a-tag v-if="isTag" :color="dataColor(label, tagColor)">
{{ label }}
</a-tag>
<!-- 文本 -->
<template v-else>
{{ value }}
{{ label }}
</template>
</template>
</a-auto-complete>
@@ -57,24 +56,11 @@
Oops! 无授权主机 请联系管理员授权后重试!
</a-empty>
<!-- 主机列表 -->
<div v-else class="host-view-container">
<!-- 分组视图列表 -->
<host-group-view v-if="NewConnectionType.GROUP === newConnectionType"
:hosts="hosts"
:filter-value="filterValue" />
<!-- 列表视图 -->
<host-list-view v-if="NewConnectionType.LIST === newConnectionType"
:host-list="hostList"
empty-value="无授权主机!" />
<!-- 我的收藏 -->
<host-list-view v-if="NewConnectionType.FAVORITE === newConnectionType"
:host-list="[]"
empty-value="无收藏记录, 快去点击主机右侧的进行收藏吧!" />
<!-- 最近连接 -->
<host-list-view v-if="NewConnectionType.LATEST === newConnectionType"
:host-list="[]"
empty-value="暂无连接记录, 快去体验吧!" />
</div>
<hosts-view v-else
class="host-view-container"
:hosts="hosts"
:filter-value="filterValue"
:new-connection-type="newConnectionType" />
</div>
</div>
</div>
@@ -90,62 +76,34 @@
<script lang="ts" setup>
import type { SelectOptionData } from '@arco-design/web-vue';
import type { AuthorizedHostQueryResponse } from '@/api/asset/asset-authorized-data';
import type { HostQueryResponse } from '@/api/asset/host';
import { getCurrentAuthorizedHost } from '@/api/asset/asset-authorized-data';
import { onBeforeMount, ref, watch } from 'vue';
import { onBeforeMount, ref } from 'vue';
import { NewConnectionType, NewConnectionTypeKey } from '../../types/terminal.const';
import useLoading from '@/hooks/loading';
import { useDictStore } from '@/store';
import { dataColor } from '@/utils';
import { tagColor } from '@/views/asset/host-list/types/const';
import HostGroupView from './host-group-view.vue';
import HostListView from './host-list-view.vue';
import HostsView from './hosts-view.vue';
const { loading, setLoading } = useLoading();
const { toOptions } = useDictStore();
const newConnectionType = ref(NewConnectionType.LIST);
const filterValue = ref();
const newConnectionType = ref(NewConnectionType.GROUP);
const filterValue = ref('');
const filterOptions = ref<Array<SelectOptionData>>([]);
const hosts = ref<AuthorizedHostQueryResponse>({} as AuthorizedHostQueryResponse);
const hostList = ref<Array<HostQueryResponse>>([]);
// 修改连接类型
const changeConnectionType = () => {
// FIXME 持久化
};
// 过滤输入
const searchFilter = (searchValue: string, option: SelectOptionData) => {
if (searchValue.startsWith('@')) {
// tag 过滤
return option.isTag && (option.value as string).toLowerCase().startsWith(searchValue.substring(1, searchValue.length).toLowerCase());
return option.isTag && (option.label as string).toLowerCase().startsWith(searchValue.substring(1, searchValue.length).toLowerCase());
} else {
// 文本过滤
return !option.isTag && (option.value as string).toLowerCase().indexOf(searchValue.toLowerCase()) > -1;
return !option.isTag && (option.label as string).toLowerCase().indexOf(searchValue.toLowerCase()) > -1;
}
};
// 数据处理
const shuffleHosts = () => {
let list = [...hosts.value?.hostList];
// 过滤
if (filterValue.value) {
console.log(filterValue.value);
}
// 排序
hostList.value = list?.sort((o1, o2) => {
if (o1.favorite || o2.favorite) {
if (o1.favorite && o2.favorite) {
return o2.id < o1.id ? 1 : -1;
}
return o2.favorite ? 1 : -1;
} else {
return o2.id < o1.id ? 1 : -1;
}
});
};
// 初始化过滤器项
const initFilterOptions = () => {
// 添加 tags
@@ -155,21 +113,20 @@
.sort((o1, o2) => o1.id - o2.id)
.map(s => s.name);
[...new Set(tagNames)].map(value => {
return { value: value, isTag: true };
return { label: value, value: `@${value}`, isTag: true };
}).forEach(s => filterOptions.value.push(s));
// 添加主机信息
const hostMeta = hosts.value.hostList?.map(s => {
return [s.name, s.code, s.address];
}).flat(1);
[...new Set(hostMeta)].map(value => {
return { value };
return { label: value, value };
}).forEach(s => filterOptions.value.push(s));
// // 添加主机信息
// hosts.value.hostList?.map(s => {
// return `${s.name} (${s.code}) - ${s.address}`;
// }).map(value => {
// return { value };
// }).forEach(s => filterOptions.value.push(s));
};
// 修改连接类型
const changeConnectionType = () => {
// FIXME 持久化类型
};
// 初始化
@@ -180,8 +137,6 @@
hosts.value = data;
// 初始化过滤项
initFilterOptions();
// 处理数据
shuffleHosts();
} finally {
setLoading(false);
}