feat: 主机分组显示

This commit is contained in:
lijiahang
2023-12-14 19:17:25 +08:00
parent 305bdfc008
commit 49697b2c87
9 changed files with 232 additions and 57 deletions

View File

@@ -38,7 +38,7 @@
// 垂直滚动 // 垂直滚动
.arco-scrollbar-track-direction-vertical { .arco-scrollbar-track-direction-vertical {
width: 9px; width: 6px;
.arco-scrollbar-thumb-bar { .arco-scrollbar-thumb-bar {
margin: 0; margin: 0;
@@ -47,7 +47,7 @@
// 水平滚动 // 水平滚动
.arco-scrollbar-track-direction-horizontal { .arco-scrollbar-track-direction-horizontal {
height: 9px; height: 6px;
.arco-scrollbar-thumb-bar { .arco-scrollbar-thumb-bar {
margin: 0; margin: 0;
@@ -81,31 +81,42 @@
// 块状树 // 块状树
.block-tree { .block-tree {
@tree-node-hover-color: var(--color-fill-1);
@tree-node-selected-color: var(--color-fill-2);
@tree-node-selected-hover-color: var(--color-fill-2);
.arco-tree-node { .arco-tree-node {
cursor: unset; cursor: unset;
margin-bottom: 2px;
&-switcher { &-switcher {
margin-left: 8px; margin-left: 8px;
} }
&:hover { &:hover {
background-color: var(--color-fill-1); background-color: @tree-node-hover-color;
} }
&-selected { &-selected {
background-color: var(--color-fill-2); background-color: @tree-node-selected-color;
&:hover { &:hover {
background-color: var(--color-fill-1); background-color: @tree-node-selected-hover-color;
} }
} }
&-selected .arco-tree-node-title:hover {
background-color: unset;
//background-color: @tree-node-selected-hover-color;
}
&-title { &-title {
padding: 0; padding: 0 16px 0 0;
height: 32px; height: 32px;
&:hover { &:hover {
background-color: var(--color-fill-1); background-color: unset;
//background-color: @tree-node-hover-color;
} }
&-text { &-text {

View File

@@ -213,7 +213,7 @@ body {
::-webkit-scrollbar { ::-webkit-scrollbar {
-webkit-appearance: none; -webkit-appearance: none;
width: 6px; width: 6px;
height: 4px; height: 6px;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {

View File

@@ -15,7 +15,7 @@
</template> </template>
<!-- 数据 --> <!-- 数据 -->
<template #item="{ item }"> <template #item="{ item }">
<a-list-item :title="`${item.name}(${item.code}) - ${ item.address}`"> <a-list-item :title="`${item.name}(${item.code}) - ${item.address}`">
<icon-desktop class="host-list-icon" /> <icon-desktop class="host-list-icon" />
<span>{{ `${item.name}(${item.code}) - ` }}</span> <span>{{ `${item.name}(${item.code}) - ` }}</span>
<span class="span-blue">{{ item.address }}</span> <span class="span-blue">{{ item.address }}</span>

View File

@@ -192,7 +192,6 @@
position: relative; position: relative;
width: 100%; width: 100%;
height: calc(100% - 44px); height: calc(100% - 44px);
overflow: auto;
} }
} }

View File

@@ -1,7 +1,7 @@
// 亮色主题配色常量 // 亮色主题配色常量
body { body {
--color-bg-header: #232323; --color-bg-header: #232323;
--color-bg-sidebar: #F2F3F5; --color-bg-sidebar: #F2F3F4;
--color-bg-content: #FEFEFE; --color-bg-content: #FEFEFE;
--color-sidebar-icon: #737070; --color-sidebar-icon: #737070;
--color-sidebar-icon-bg: #D7D8DB; --color-sidebar-icon-bg: #D7D8DB;
@@ -16,7 +16,7 @@ body {
body[terminal-theme='dark'] { body[terminal-theme='dark'] {
--color-bg-header: #232323; --color-bg-header: #232323;
--color-bg-sidebar: #2C2E31; --color-bg-sidebar: #2C2E31;
--color-bg-content: #1A1B1F; --color-bg-content: #1A1B1C;
--color-sidebar-icon: #C3C8CE; --color-sidebar-icon: #C3C8CE;
--color-sidebar-icon-bg: #43444C; --color-sidebar-icon-bg: #43444C;
--color-sidebar-tooltip-text: rgba(255, 255, 255, .9); --color-sidebar-tooltip-text: rgba(255, 255, 255, .9);
@@ -213,6 +213,7 @@ body[terminal-theme='dark'] .host-layout {
.terminal-setting-wrapper { .terminal-setting-wrapper {
min-width: 932px; min-width: 932px;
position: relative;
} }
.terminal-setting-title { .terminal-setting-title {

View File

@@ -55,6 +55,7 @@
import { useFullscreen } from '@vueuse/core'; import { useFullscreen } from '@vueuse/core';
import { computed } from 'vue'; import { computed } from 'vue';
import IconActions from '../layout/icon-actions.vue'; import IconActions from '../layout/icon-actions.vue';
import { useTerminalStore } from '@/store';
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
@@ -67,24 +68,25 @@
} }
}); });
const emits = defineEmits(['update:modelValue', 'clickTab', 'deleteTab', 'split', 'share']); const emits = defineEmits(['update:modelValue', 'clickTab', 'deleteTab', 'share']);
const { isFullscreen, toggle: toggleFullScreen } = useFullscreen(); const { isFullscreen, toggle: toggleFullScreen } = useFullscreen();
const terminalStore = useTerminalStore();
// 顶部操作 // 顶部操作
const actions = computed<Array<SidebarAction>>(() => [ const actions = computed<Array<SidebarAction>>(() => [
{
icon: 'icon-interaction',
content: '分屏',
visible: false,
click: () => emits('split')
},
{ {
icon: 'icon-share-alt', icon: 'icon-share-alt',
content: '分享链接', content: '分享链接',
visible: false, visible: false,
click: () => emits('share') click: () => emits('share')
}, },
{
// FIXME 持久化
icon: terminalStore.isDarkTheme ? 'icon-sun-fill' : 'icon-moon-fill',
content: terminalStore.isDarkTheme ? '点击切换为亮色模式' : '点击切换为暗色模式',
click: () => terminalStore.changeDarkTheme(!terminalStore.isDarkTheme)
},
{ {
icon: isFullscreen.value ? 'icon-fullscreen-exit' : 'icon-fullscreen', icon: isFullscreen.value ? 'icon-fullscreen-exit' : 'icon-fullscreen',
content: isFullscreen.value ? '点击退出全屏模式' : '点击切换全屏模式', content: isFullscreen.value ? '点击退出全屏模式' : '点击切换全屏模式',

View File

@@ -0,0 +1,106 @@
<template>
<div class="group-view-container">
<!-- 主机分组 -->
<a-scrollbar>
<a-tree v-model:selected-keys="selectedGroup"
:data="hosts.groupTree"
:blockNode="true"
class="host-tree block-tree"
@select="chooseGroup">
<!-- 标题 -->
<template #extra="node">
<span class="node-host-count span-blue">{{ hosts?.treeNodes[node.key]?.length || 0 }}</span>
</template>
</a-tree>
</a-scrollbar>
<!-- 主机列表 -->
<div class="host-list">
<a-list size="large"
max-height="100%"
:hoverable="true"
:data="[{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}]">
<!-- 空数据 -->
<template #empty>
<span class="host-list-empty">当前分组未配置主机</span>
</template>
<!-- 数据 -->
<template #item="{ item }">
<a-list-item :title="`${item.name}(${item.code}) - ${item.address}`">
{{ hosts?.treeNodes[selectedGroup[0]] }}
<icon-desktop class="host-list-icon" />
<span>{{ `${item.name}(${item.code}) - ` }}</span>
<span class="span-blue">{{ item.address }}</span>
</a-list-item>
</template>
</a-list>
</div>
</div>
</template>
<script lang="ts">
export default {
name: 'hostGroupView'
};
</script>
<script lang="ts" setup>
import type { AuthorizedHostQueryResponse } from '@/api/asset/asset-authorized-data';
import { ref } from 'vue';
const props = defineProps<{
hosts: AuthorizedHostQueryResponse
}>();
const selectedGroup = ref([0]);
const chooseGroup = () => {
console.log(selectedGroup.value[0]);
};
</script>
<style lang="less" scoped>
@tree-width: 298px;
@tree-gap: 32px;
.group-view-container {
display: flex;
justify-content: space-between;
max-height: 100%;
width: 100%;
.host-tree {
min-width: 100%;
width: max-content;
user-select: none;
overflow: hidden;
.node-host-count {
margin-right: 10px;
font-size: 13px;
user-select: none;
display: flex;
justify-content: flex-end;
align-items: center;
}
}
.host-list {
width: calc(100% - @tree-width - @tree-gap);
border-radius: 4px;
}
}
:deep(.arco-scrollbar) {
width: @tree-width;
margin-right: @tree-gap;
border-radius: 4px;
&-container {
width: 100%;
max-height: 100%;
overflow: auto;
}
}
</style>

View File

@@ -5,16 +5,20 @@
<h2 class="terminal-setting-title">新建连接</h2> <h2 class="terminal-setting-title">新建连接</h2>
<!-- 操作栏 --> <!-- 操作栏 -->
<div class="terminal-setting-block header-actions"> <div class="terminal-setting-block header-actions">
<a-radio-group type="button"> <!-- 视图类型 -->
<a-radio value="1">分组</a-radio> <a-radio-group v-model="newConnectionType"
<a-radio value="2">列表</a-radio> type="button"
<a-radio value="3">最近连接</a-radio> class="usn"
</a-radio-group> :options="toOptions(NewConnectionTypeKey)"
<a-input-search class="host-filter" @change="changeConnectionType" />
placeholder="输入名称/编码/IP 进行过滤" /> <!-- 过滤 -->
<a-input-search v-model="filterValue"
class="host-filter"
placeholder="输入名称/编码/IP @标签"
:allow-clear="true" />
</div> </div>
<!-- 授权主机 --> <!-- 授权主机 -->
<div class="terminal-setting-block"> <div class="terminal-setting-block" style="margin: 0;">
<!-- 顶部 --> <!-- 顶部 -->
<div class="terminal-setting-subtitle-wrapper"> <div class="terminal-setting-subtitle-wrapper">
<h3 class="terminal-setting-subtitle"> <h3 class="terminal-setting-subtitle">
@@ -22,17 +26,33 @@
</h3> </h3>
</div> </div>
<!-- 内容区域 --> <!-- 内容区域 -->
<div class="terminal-setting-body hosts-container"> <div class="terminal-setting-body body-container">
<div class="host-tree"> <!-- 加载中 -->
<a-tree :data="hosts.groupTree" <a-skeleton v-if="loading"
:blockNode="true" class="hosts-skeleton"
> :animation="true">
<a-skeleton-line :rows="6"
:line-height="40"
:line-spacing="20" />
</a-skeleton>
<!-- 无数据 -->
<a-empty v-else-if="!hosts.hostList?.length">
<template #image>
<icon-desktop />
</template>
Oops! 无授权主机 请联系管理员授权后重试!
</a-empty>
<!-- 主机列表 -->
<div v-else class="host-view-container">
<!-- 分组视图列表 -->
<host-group-view v-if="NewConnectionType.GROUP === newConnectionType"
:hosts="hosts" />
<!-- 列表视图 -->
<!-- 我的收藏 -->
<!-- 最近连接 -->
</a-tree>
{{ }}
</div>
<div class="host-list">
{{ hosts.treeNodes }}
</div> </div>
</div> </div>
</div> </div>
@@ -49,13 +69,33 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { AuthorizedHostQueryResponse } from '@/api/asset/asset-authorized-data'; import type { AuthorizedHostQueryResponse } from '@/api/asset/asset-authorized-data';
import { getCurrentAuthorizedHost } from '@/api/asset/asset-authorized-data'; import { getCurrentAuthorizedHost } from '@/api/asset/asset-authorized-data';
import { onMounted, ref } from 'vue'; import { onBeforeMount, ref } from 'vue';
import { NewConnectionType, NewConnectionTypeKey } from '../../types/terminal.const';
import useLoading from '@/hooks/loading';
import HostGroupView from './host-group-view.vue';
import { useDictStore } from '@/store';
const { loading, setLoading } = useLoading();
const { toOptions } = useDictStore();
const newConnectionType = ref(NewConnectionType.GROUP);
const filterValue = ref();
const hosts = ref<AuthorizedHostQueryResponse>({} as AuthorizedHostQueryResponse); const hosts = ref<AuthorizedHostQueryResponse>({} as AuthorizedHostQueryResponse);
onMounted(async () => { // 修改连接类型
const { data } = await getCurrentAuthorizedHost(); const changeConnectionType = () => {
hosts.value = data; // FIXME 持久化
};
// 加载主机信息
onBeforeMount(async () => {
try {
setLoading(true);
const { data } = await getCurrentAuthorizedHost();
hosts.value = data;
} finally {
setLoading(false);
}
}); });
</script> </script>
@@ -66,20 +106,21 @@
justify-content: space-between; justify-content: space-between;
.host-filter { .host-filter {
width: 40%; width: 36%;
} }
} }
.hosts-container { .body-container {
justify-content: space-between; justify-content: space-between;
.host-tree { .hosts-skeleton {
margin-right: 16px; width: 100%;
width: 350px;
} }
.host-list { .host-view-container {
width: 490px; width: 100%;
height: calc(100vh - 240px);
position: relative;
} }
} }

View File

@@ -9,6 +9,15 @@ export interface SidebarAction {
click: () => void; click: () => void;
} }
// tab 元素
export interface TabItem {
key: string;
title: string;
type: string;
[key: string]: unknown;
}
// tab 类型 // tab 类型
export const TabType = { export const TabType = {
SETTING: 'setting', SETTING: 'setting',
@@ -34,14 +43,13 @@ export const InnerTabs = {
}, },
}; };
// tab 元素 // 新建连接类型
export interface TabItem { export const NewConnectionType = {
key: string; GROUP: 'group',
title: string; LIST: 'list',
type: string; FAVORITE: 'favorite',
LATEST: 'latest'
[key: string]: unknown; };
}
// 字体后缀 兜底 // 字体后缀 兜底
export const fontFamilySuffix = ',courier-new, courier, monospace'; export const fontFamilySuffix = ',courier-new, courier, monospace';
@@ -61,5 +69,12 @@ export const fontWeightKey = 'terminalFontWeight';
// 终端光标样式 // 终端光标样式
export const cursorStyleKey = 'terminalCursorStyle'; export const cursorStyleKey = 'terminalCursorStyle';
// 终端新建连接类型
export const NewConnectionTypeKey = 'terminalNewConnectionType';
// 加载的字典值 // 加载的字典值
export const dictKeys = [darkThemeKey, fontFamilyKey, fontSizeKey, fontWeightKey, cursorStyleKey]; export const dictKeys = [
darkThemeKey, fontFamilyKey,
fontSizeKey, fontWeightKey,
cursorStyleKey, NewConnectionTypeKey
];