授权主机选择模态框.

This commit is contained in:
lijiahang
2024-03-07 15:54:51 +08:00
parent 079622d776
commit 6a7615e294
20 changed files with 680 additions and 189 deletions

View File

@@ -1,82 +0,0 @@
<template>
<a-modal v-model:visible="visible"
title-align="start"
title="授权主机"
:top="80"
:width="900"
: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-skeleton v-if="loading"
style="padding: 0 12px"
:animation="true">
<a-skeleton-line :rows="6"
:line-height="48"
:line-spacing="12" />
</a-skeleton>
<!-- 主机列表 -->
<div v-else class="layout-container">
11
</div>
</a-modal>
</template>
<script lang="ts">
export default {
name: 'authorizedHostModal'
};
</script>
<script lang="ts" setup>
import useLoading from '@/hooks/loading';
import useVisible from '@/hooks/visible';
import { Message } from '@arco-design/web-vue';
const { loading, setLoading } = useLoading();
const { visible, setVisible } = useVisible(true);
// 打开
const open = () => {
setVisible(true);
};
defineExpose({ open });
// 确定
const handlerOk = async () => {
setLoading(true);
try {
Message.success('修改成功');
// 清空
handlerClear();
} catch (e) {
return false;
} finally {
setLoading(false);
}
};
// 关闭
const handleClose = () => {
handlerClear();
};
// 清空
const handlerClear = () => {
setLoading(false);
};
</script>
<style lang="less" scoped>
.layout-container {
width: 100%;
max-height: calc(100vh - 300px);
}
</style>

View File

@@ -0,0 +1,124 @@
<template>
<div class="group-view-container">
<!-- 主机分组 -->
<div class="host-group-container">
<a-scrollbar>
<a-tree v-model:selected-keys="selectedGroup"
class="host-tree block-tree"
:data="groups"
:blockNode="true">
<!-- 组内数量 -->
<template #extra="node">
<span class="node-host-count span-blue">{{ nodes[node.key]?.length || 0 }}</span>
</template>
</a-tree>
</a-scrollbar>
</div>
<!-- 主机列表 -->
<host-table class="host-list"
v-model:selected-keys="selectedKeysValue"
:host-list="hostList"
empty-message="当前分组内无授权主机/主机未启用 SSH 配置!" />
</div>
</template>
<script lang="ts">
export default {
name: 'hostGroup'
};
</script>
<script lang="ts" setup>
import type { HostQueryResponse } from '@/api/asset/host';
import type { HostGroupQueryResponse } from '@/api/asset/host-group';
import { computed } from 'vue';
import HostTable from './host-table.vue';
const props = defineProps<{
selectedKeys: Array<number>;
selectedGroup: number,
hostList: Array<HostQueryResponse>;
groups: Array<HostGroupQueryResponse>;
nodes: Record<string, Array<number>>;
}>();
const emits = defineEmits(['update:selectedKeys', 'update:selectedGroup']);
// 选中数据
const selectedKeysValue = computed<Array<number>>({
get() {
return props.selectedKeys;
},
set(e) {
if (e) {
emits('update:selectedKeys', e);
} else {
emits('update:selectedKeys', []);
}
}
});
// 选中分组
const selectedGroup = computed({
get() {
return [props.selectedGroup];
},
set(e) {
emits('update:selectedGroup', e[0]);
}
});
</script>
<style lang="less" scoped>
@tree-width: 298px;
@tree-gap: 24px;
.group-view-container {
display: flex;
justify-content: space-between;
height: 100%;
width: 100%;
}
.host-group-container {
:deep(.arco-scrollbar) {
width: @tree-width;
height: 100%;
margin-right: @tree-gap;
border-radius: 4px;
&-container {
width: 100%;
max-height: 100%;
overflow: auto;
}
}
.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;
font-weight: bold;
}
}
}
.host-list {
width: calc(100% - @tree-width - @tree-gap);
border-radius: 4px;
max-height: 100%;
overflow: auto;
position: relative;
}
</style>

View File

@@ -0,0 +1,101 @@
<template>
<a-table row-key="id"
ref="tableRef"
:columns="columns"
v-model:selected-keys="selectedKeysValue"
:row-selection="rowSelection"
row-class="pointer"
:data="hostList"
:scroll="{ y: '100%' }"
:pagination="false"
:bordered="true"
@row-click="clickRow">
<!-- -->
<template #empty>
<a-empty>
{{ emptyMessage }}
</a-empty>
</template>
<!-- 名称 -->
<template #name="{ record }">
{{ record.alias || `${record.name} (${record.code})` }}
</template>
<!-- 编码 -->
<template #code="{ record }">
<a-tag>{{ record.code }}</a-tag>
</template>
<!-- 地址 -->
<template #address="{ record }">
<span class="span-blue">{{ 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>
</a-table>
</template>
<script lang="ts">
export default {
name: 'hostTable'
};
</script>
<script lang="ts" setup>
import type { TableData } from '@arco-design/web-vue';
import type { HostQueryResponse } from '@/api/asset/host';
import { dataColor } from '@/utils';
import { tagColor } from '@/views/asset/host-list/types/const';
import { useRowSelection } from '@/types/table';
import columns from '../types/table.columns';
import { computed } from 'vue';
const rowSelection = useRowSelection();
const props = defineProps<{
selectedKeys: Array<number>;
hostList: Array<HostQueryResponse>;
emptyMessage: string;
}>();
const emits = defineEmits(['update:selectedKeys']);
// 选中数据
const selectedKeysValue = computed<Array<number>>({
get() {
return props.selectedKeys;
},
set(e) {
if (e) {
emits('update:selectedKeys', e);
} else {
emits('update:selectedKeys', []);
}
}
});
// 点击行
const clickRow = ({ id }: TableData) => {
const result = [...props.selectedKeys];
const index = result.indexOf(id);
if (index === -1) {
result.push(id);
} else {
result.splice(index, 1);
}
emits('update:selectedKeys', result);
};
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,273 @@
<template>
<a-modal v-model:visible="visible"
title-align="start"
title="授权主机"
:top="60"
:width="968"
:align-center="false"
:draggable="true"
:mask-closable="false"
:unmount-on-close="true"
:body-style="{ padding: '0' }"
:ok-button-props="{ disabled: loading }"
:cancel-button-props="{ disabled: loading }"
:on-before-ok="handlerOk"
@close="handleClose">
<!-- 加载中 -->
<a-skeleton v-if="loading"
style="padding: 16px"
:animation="true">
<a-skeleton-line :rows="6"
:line-height="42"
:line-spacing="12" />
</a-skeleton>
<!-- 主机列表容器 -->
<div v-else class="host-layout-container">
<!-- 顶部操作 -->
<div class="top-side-container">
<!-- 视图类型 -->
<a-radio-group v-model="newConnectionType"
type="button"
class="usn"
:options="toRadioOptions(newConnectionTypeKey)" />
<!-- 过滤 -->
<a-auto-complete v-model="filterValue"
class="host-filter"
placeholder="别名/名称/编码/IP @标签"
:allow-clear="true"
:data="filterOptions"
:filter-option="tagLabelFilter">
<template #option="{ data: { raw: { label, isTag } } }">
<!-- tag -->
<a-tag v-if="isTag" :color="dataColor(label, tagColor)">
{{ label }}
</a-tag>
<!-- 文本 -->
<template v-else>
{{ label }}
</template>
</template>
</a-auto-complete>
</div>
<!-- 主机列表 -->
<div class="host-container">
<!-- 分组视图 -->
<host-group v-if="newConnectionType === NewConnectionType.GROUP"
v-model:selected-keys="selectedKeys"
v-model:selected-group="selectedGroup"
:host-list="hostList"
:groups="hosts?.groupTree as any"
:nodes="treeNodes" />
<!-- 列表视图 -->
<host-table v-else
v-model:selected-keys="selectedKeys"
:host-list="hostList"
:empty-message="emptyMessage" />
</div>
</div>
</a-modal>
</template>
<script lang="ts">
export default {
name: 'authorizedHostModal'
};
</script>
<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 { onMounted, ref, watch, computed } from 'vue';
import { dataColor } from '@/utils';
import { dictKeys, NewConnectionType, newConnectionTypeKey } from './types/const';
import { useDictStore } from '@/store';
import { tagLabelFilter } from '@/types/form';
import { tagColor } from '@/views/asset/host-list/types/const';
import useLoading from '@/hooks/loading';
import useVisible from '@/hooks/visible';
import { getCurrentAuthorizedHost } from '@/api/asset/asset-authorized-data';
import { getAuthorizedHostOptions } from '@/types/options';
import HostTable from './components/host-table.vue';
import HostGroup from './components/host-group.vue';
const emits = defineEmits(['selected']);
const { toRadioOptions, loadKeys } = useDictStore();
const { loading, setLoading } = useLoading();
const { visible, setVisible } = useVisible();
const hosts = ref<AuthorizedHostQueryResponse>();
const hostList = ref<Array<HostQueryResponse>>([]);
const treeNodes = ref<Record<string, Array<number>>>({});
const selectedGroup = ref<number>(0);
const selectedKeys = ref<Array<number>>([]);
const newConnectionType = ref(NewConnectionType.GROUP);
const filterValue = ref('');
const filterOptions = ref<Array<SelectOptionData>>([]);
const emptyMessage = computed(() => {
if (newConnectionType.value === NewConnectionType.LIST) {
// 列表
return '无授权主机/主机未启用 SSH 配置!';
} else if (newConnectionType.value === NewConnectionType.FAVORITE) {
// 收藏
return '无收藏主机/主机未启用 SSH 配置!';
} else if (newConnectionType.value === NewConnectionType.LATEST) {
// 最近连接
return '暂无连接记录!';
}
return '';
});
// 打开
const open = async (hostIdList: Array<number>) => {
setVisible(true);
// 加载主机列表
await fetchHosts();
// 设置选中分组
selectedGroup.value = hosts.value?.groupTree?.length ? hosts.value.groupTree[0].key : 0;
// 设置主机数据
shuffleHosts();
// 设置选中项
selectedKeys.value = hosts.value?.hostList
.map(s => s.id)
.filter(s => hostIdList.includes(s)) || [];
};
// 加载主机列表
const fetchHosts = async () => {
if (hosts.value) {
return;
}
setLoading(true);
try {
// 加载主机列表
const { data } = await getCurrentAuthorizedHost('ssh');
hosts.value = data;
// 设置主机搜索选项
filterOptions.value = getAuthorizedHostOptions(data.hostList);
} catch (e) {
} finally {
setLoading(false);
}
};
defineExpose({ open });
// 主机数据处理
const shuffleHosts = () => {
if (!hosts.value) {
return;
}
let list = [...hosts.value.hostList];
// 过滤
const filterVal = filterValue.value.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.alias as string)?.toLowerCase().indexOf(filterVal) > -1
|| (item.address as string)?.toLowerCase().indexOf(filterVal) > -1;
});
}
// 判断类型
if (NewConnectionType.GROUP === newConnectionType.value) {
// 过滤-分组
const groupNodes = { ...hosts.value.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 === newConnectionType.value) {
// 过滤-个人收藏
list = list.filter(item => item.favorite);
} else if (NewConnectionType.LATEST === newConnectionType.value) {
// 过滤-最近连接
list = hosts.value.latestHosts
.map(s => list.find(item => item.id === s) as HostQueryResponse)
.filter(Boolean);
}
// 非最近连接排序
if (NewConnectionType.LATEST !== newConnectionType.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;
}
});
} else {
// 最近连接不排序
hostList.value = list;
}
};
// 监听搜索值变化
watch(filterValue, shuffleHosts);
// 监听类型变化
watch(newConnectionType, shuffleHosts);
// 监听分组变化
watch(selectedGroup, shuffleHosts);
// 确定
const handlerOk = async () => {
emits('selected', selectedKeys.value);
// 清空
handlerClear();
};
// 关闭
const handleClose = () => {
handlerClear();
};
// 清空
const handlerClear = () => {
setLoading(false);
};
// 加载字典值
onMounted(async () => {
await loadKeys([...dictKeys]);
});
</script>
<style lang="less" scoped>
.host-layout-container {
padding: 16px;
width: 100%;
height: calc(100vh - 248px);
overflow: hidden;
position: relative;
}
.top-side-container {
height: 32px;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
:deep(.host-filter) {
width: 42%;
}
}
.host-container {
height: calc(100% - 48px);
}
</style>

View File

@@ -0,0 +1,15 @@
// 新建连接类型
export const NewConnectionType = {
GROUP: 'group',
LIST: 'list',
FAVORITE: 'favorite',
LATEST: 'latest'
};
// 新建连接类型
export const newConnectionTypeKey = 'hostNewConnectionType';
// 加载的字典值
export const dictKeys = [
newConnectionTypeKey,
];

View File

@@ -0,0 +1,22 @@
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface';
const columns = [
{
title: '主机名称',
dataIndex: 'name',
slotName: 'name',
ellipsis: true,
tooltip: true
}, {
title: '主机地址',
dataIndex: 'address',
slotName: 'address',
}, {
title: '主机标签',
dataIndex: 'tags',
slotName: 'tags',
align: 'left',
},
] as TableColumnData[];
export default columns;

View File

@@ -1,4 +1,4 @@
import type { TreeNodeData } from '@arco-design/web-vue';
import type { SelectOptionData, TreeNodeData } from '@arco-design/web-vue';
// 通过 label 进行过滤
export const labelFilter = (searchValue: string, option: { label: string }) => {
@@ -9,3 +9,14 @@ export const labelFilter = (searchValue: string, option: { label: string }) => {
export const titleFilter = (searchValue: string, option: TreeNodeData) => {
return (option.title as string).toLowerCase().indexOf(searchValue.toLowerCase()) > -1;
};
// 通过 tag label 进行过滤
export const tagLabelFilter = (searchValue: string, option: SelectOptionData) => {
if (searchValue.startsWith('@')) {
// tag 过滤
return option.isTag && (option.label as string).toLowerCase().startsWith(searchValue.substring(1, searchValue.length).toLowerCase());
} else {
// 文本过滤
return !option.isTag && (option.label as string).toLowerCase().indexOf(searchValue.toLowerCase()) > -1;
}
};

View File

@@ -0,0 +1,24 @@
import type { SelectOptionData } from '@arco-design/web-vue';
import type { HostQueryResponse } from '@/api/asset/host';
// 获取认证主机过滤器选项
export const getAuthorizedHostOptions = (list: Array<HostQueryResponse>): Array<SelectOptionData> => {
const options: Array<SelectOptionData> = [];
// 添加 tags
const tagNames = list.map(s => s.tags)
.filter(s => s?.length)
.flat(1)
.sort((o1, o2) => o1.id - o2.id)
.map(s => s.name);
[...new Set(tagNames)].map(value => {
return { label: value, value: `@${value}`, isTag: true };
}).forEach(s => options.push(s));
// 添加主机信息
const hostMeta = list.map(s => {
return [s.name, s.code, s.address, s.alias];
}).filter(Boolean).flat(1);
[...new Set(hostMeta)].map(value => {
return { label: value, value };
}).forEach(s => options.push(s));
return options;
};

View File

@@ -9,10 +9,12 @@
:columns="hostIdentityColumns"
v-model:selected-keys="selectedKeys"
:row-selection="rowSelection"
row-class="pointer"
:sticky-header="true"
:data="hostIdentities"
:pagination="false"
:bordered="false">
:bordered="false"
@row-click="clickRow">
<!-- 秘钥名称 -->
<template #keyId="{ record }">
<a-tag color="arcoblue" v-if="record.keyId">
@@ -30,6 +32,7 @@
</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';
@@ -81,6 +84,16 @@
}
};
// 点击行
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);

View File

@@ -9,10 +9,12 @@
:columns="hostKeyColumns"
v-model:selected-keys="selectedKeys"
:row-selection="rowSelection"
row-class="pointer"
:sticky-header="true"
:data="hostKeys"
:pagination="false"
:bordered="false" />
:bordered="false"
@row-click="clickRow" />
</grant-layout>
</template>
@@ -23,6 +25,7 @@
</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';
@@ -71,6 +74,16 @@
}
};
// 点击行
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);

View File

@@ -176,13 +176,13 @@
import { computed, reactive, ref, onMounted } from 'vue';
import useLoading from '@/hooks/loading';
import { dataColor, objectTruthKeyCount, resetObject } from '@/utils';
import fieldConfig from '../types/host.card.fields';
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 TagMultiSelector from '@/components/meta/tag/tag-multi-selector.vue';
import useCopy from '@/hooks/copy';
import { GrantKey, GrantRouteName } from '@/views/asset/grant/types/const';
import TagMultiSelector from '@/components/meta/tag/tag-multi-selector.vue';
const emits = defineEmits(['openAdd', 'openUpdate', 'openUpdateConfig', 'openHostGroup']);

View File

@@ -63,13 +63,13 @@
import { ref } from 'vue';
import useLoading from '@/hooks/loading';
import useVisible from '@/hooks/visible';
import formRules from '../types/host.form.rules';
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/tag-multi-selector.vue';
import HostGroupTreeSelector from '@/components/asset/host-group/host-group-tree-selector.vue';
import { tagColor } from '@/views/asset/host-list/types/const';
const { visible, setVisible } = useVisible();
const { loading, setLoading } = useLoading();

View File

@@ -112,7 +112,9 @@
</template>
<!-- 标签 -->
<template #tags="{ record }">
<a-space v-if="record.tags">
<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)">
@@ -166,14 +168,14 @@
import { reactive, ref, onMounted } from 'vue';
import { deleteHost, getHostPage } from '@/api/asset/host';
import { Message } from '@arco-design/web-vue';
import useLoading from '@/hooks/loading';
import columns from '../types/host.table.columns';
import { tagColor } from '../types/const';
import { usePagination } from '@/types/table';
import useLoading from '@/hooks/loading';
import useCopy from '@/hooks/copy';
import columns from '../types/table.columns';
import { dataColor } from '@/utils';
import TagMultiSelector from '@/components/meta/tag/tag-multi-selector.vue';
import { GrantKey, GrantRouteName } from '@/views/asset/grant/types/const';
import TagMultiSelector from '@/components/meta/tag/tag-multi-selector.vue';
const tagSelector = ref();
const tableRenderData = ref<HostQueryResponse[]>([]);

View File

@@ -31,7 +31,6 @@
</a-grid-item>
</a-grid>
</div>
<authorized-host-modal />
</div>
</template>
@@ -40,7 +39,6 @@
import QuickOperation from './components/quick-operation.vue';
import Docs from './components/docs.vue';
import OperatorLogSimpleTable from '@/views/user/operator-log/components/operator-log-simple-table.vue';
import AuthorizedHostModal from '@/components/asset/authorized-host-modal/index.vue';
</script>
<script lang="ts">

View File

@@ -27,55 +27,55 @@
<span class="host-item-left-name">
<!-- 名称文本 -->
<template v-if="!item.editable">
<!-- 文本 -->
<a-tooltip position="top"
:mini="true"
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
:content="item.alias || `${item.name} (${item.code})`">
<span class="host-item-text host-item-left-name-text">
<template v-if="item.alias">
{{ item.alias }}
</template>
<template v-else>
{{ `${item.name} (${item.code})` }}
</template>
</span>
</a-tooltip>
<!-- 文本 -->
<a-tooltip position="top"
:mini="true"
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
:content="item.alias || `${item.name} (${item.code})`">
<span class="host-item-text host-item-left-name-text">
<template v-if="item.alias">
{{ item.alias }}
</template>
<template v-else>
{{ `${item.name} (${item.code})` }}
</template>
</span>
</a-tooltip>
<!-- 修改别名 -->
<a-tooltip position="top"
:mini="true"
:auto-fix-position="false"
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="修改别名">
<icon-edit class="host-item-left-name-edit"
@click="clickEditAlias(item)" />
</a-tooltip>
</template>
<a-tooltip position="top"
:mini="true"
:auto-fix-position="false"
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-content"
content="修改别名">
<icon-edit class="host-item-left-name-edit"
@click="clickEditAlias(item)" />
</a-tooltip>
</template>
<!-- 名称输入框 -->
<template v-else>
<a-input v-model="item.alias"
ref="aliasNameInput"
class="host-item-left-name-input"
:max-length="32"
:disabled="item.loading"
size="mini"
:placeholder="item.name"
@blur="saveAlias(item)"
@pressEnter="saveAlias(item)"
@change="saveAlias(item)">
<template #suffix>
<!-- 加载中 -->
<icon-loading v-if="item.loading" />
<!-- 保存 -->
<icon-check v-else
class="pointer"
title="保存"
@click="saveAlias(item)" />
</template>
</a-input>
</template>
<a-input v-model="item.alias"
ref="aliasNameInput"
class="host-item-left-name-input"
:max-length="32"
:disabled="item.loading"
size="mini"
:placeholder="item.name"
@blur="saveAlias(item)"
@pressEnter="saveAlias(item)"
@change="saveAlias(item)">
<template #suffix>
<!-- 加载中 -->
<icon-loading v-if="item.loading" />
<!-- 保存 -->
<icon-check v-else
class="pointer"
title="保存"
@click="saveAlias(item)" />
</template>
</a-input>
</template>
</span>
</div>
<!-- 中间ip -->

View File

@@ -8,19 +8,9 @@
:host-list="hostList"
:filter-value="filterValue" />
<!-- 列表视图 -->
<host-list-view v-if="NewConnectionType.LIST === newConnectionType"
:hostList="hostList"
empty-value="无授权主机/主机未启用 SSH 配置!" />
<!-- 我的收藏 -->
<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="暂无连接记录, 快去体验吧!" />
<host-list-view v-else
:host-list="hostList"
:empty-value="emptyMessage" />
<!-- 主机设置模态框 -->
<host-setting-modal ref="settingModal" />
</div>
@@ -33,7 +23,7 @@
</script>
<script lang="ts" setup>
import { onMounted, provide, ref, watch } from 'vue';
import { computed, onMounted, provide, ref, watch } from 'vue';
import { NewConnectionType, openSettingModalKey } from '../../types/terminal.const';
import { AuthorizedHostQueryResponse } from '@/api/asset/asset-authorized-data';
import { HostQueryResponse } from '@/api/asset/host';
@@ -56,6 +46,20 @@
);
const settingModal = ref();
const emptyMessage = computed(() => {
if (props.newConnectionType === NewConnectionType.LIST) {
// 列表
return '无授权主机/主机未启用 SSH 配置!';
} else if (props.newConnectionType === NewConnectionType.FAVORITE) {
// 收藏
return '无收藏记录, 快去点击主机右侧的⭐进行收藏吧!';
} else if (props.newConnectionType === NewConnectionType.LATEST) {
// 最近连接
return '暂无连接记录, 快去体验吧!';
}
return '';
});
// 暴露打开 ssh 配置模态框
provide(openSettingModalKey, (record: any) => {
settingModal.value?.open(record);

View File

@@ -17,8 +17,8 @@
placeholder="别名/名称/编码/IP @标签"
:allow-clear="true"
:data="filterOptions"
:filter-option="searchFilter">
<template #option="{ data: { raw: { label, isTag} } }">
:filter-option="tagLabelFilter">
<template #option="{ data: { raw: { label, isTag } } }">
<!-- tag -->
<a-tag v-if="isTag" :color="dataColor(label, tagColor)">
{{ label }}
@@ -72,7 +72,9 @@
import { useDictStore, useTerminalStore } from '@/store';
import { TerminalPreferenceItem } from '@/store/modules/terminal';
import { dataColor } from '@/utils';
import { tagLabelFilter } from '@/types/form';
import { tagColor } from '@/views/asset/host-list/types/const';
import { getAuthorizedHostOptions } from '@/types/options';
import HostsView from './hosts-view.vue';
const { toRadioOptions } = useDictStore();
@@ -82,39 +84,10 @@
const filterValue = ref('');
const filterOptions = ref<Array<SelectOptionData>>([]);
// 过滤输入
const searchFilter = (searchValue: string, option: SelectOptionData) => {
if (searchValue.startsWith('@')) {
// tag 过滤
return option.isTag && (option.label as string).toLowerCase().startsWith(searchValue.substring(1, searchValue.length).toLowerCase());
} else {
// 文本过滤
return !option.isTag && (option.label as string).toLowerCase().indexOf(searchValue.toLowerCase()) > -1;
}
};
// 初始化过滤器项
const initFilterOptions = () => {
// 添加 tags
const tagNames = hosts.hostList?.map(s => s.tags)
.filter(s => s?.length)
.flat(1)
.sort((o1, o2) => o1.id - o2.id)
.map(s => s.name);
[...new Set(tagNames)].map(value => {
return { label: value, value: `@${value}`, isTag: true };
}).forEach(s => filterOptions.value.push(s));
// 添加主机信息
const hostMeta = hosts.hostList?.map(s => {
return [s.name, s.code, s.address, s.alias];
}).filter(Boolean).flat(1);
[...new Set(hostMeta)].map(value => {
return { label: value, value };
}).forEach(s => filterOptions.value.push(s));
};
// 初始化过滤器项
onBeforeMount(initFilterOptions);
onBeforeMount(() => {
filterOptions.value = getAuthorizedHostOptions(hosts.hostList);
});
</script>