🔖 项目重命名.

This commit is contained in:
lijiahangmax
2024-05-16 00:03:30 +08:00
parent f7189e34cb
commit d3a045ec20
1511 changed files with 4199 additions and 4128 deletions

View File

@@ -0,0 +1,5 @@
// 创建前缀
export const createGroupGroupPrefix = 'create-';
// 根id
export const rootId = 0;

View File

@@ -0,0 +1,61 @@
<template>
<a-tree-select v-model="value"
:multiple="true"
:data="treeData"
:loading="loading"
placeholder="请选择主机分组"
:allow-clear="true"
:allow-search="true" />
</template>
<script lang="ts">
export default {
name: 'hostGroupTreeSelector'
};
</script>
<script lang="ts" setup>
import type { TreeNodeData } from '@arco-design/web-vue';
import { computed, onBeforeMount, ref } from 'vue';
import { useCacheStore } from '@/store';
import useLoading from '@/hooks/loading';
const props = defineProps<Partial<{
modelValue: Array<number>;
}>>();
const emits = defineEmits(['update:modelValue']);
const { loading, setLoading } = useLoading();
const cacheStore = useCacheStore();
const value = computed<Array<number>>({
get() {
return props.modelValue as Array<number>;
},
set(e) {
if (e) {
emits('update:modelValue', e);
} else {
emits('update:modelValue', null);
}
}
});
const treeData = ref<Array<TreeNodeData>>([]);
// 初始化选项
onBeforeMount(async () => {
setLoading(true);
try {
treeData.value = await cacheStore.loadHostGroups();
} catch (e) {
} finally {
setLoading(false);
}
});
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,381 @@
<template>
<a-scrollbar>
<!-- 分组树 -->
<a-tree v-if="treeData.length"
v-model:checked-keys="checkedKeys"
ref="tree"
class="tree-container block-tree"
:class="[ editable ? 'editable-tree' : '' ]"
:blockNode="true"
:data="treeData"
:draggable="editable"
:checkable="checkable"
:check-strictly="true"
@drop="moveGroup"
@select="(s) => emits('onSelected', s)">
<!-- 标题 -->
<template #title="node">
<!-- 修改名称输入框 -->
<template v-if="node.editable">
<a-input size="mini"
ref="renameInput"
v-model="node.title"
style="width: 138px;"
placeholder="名称"
:max-length="32"
:disabled="node.loading"
@blur="saveNode(node)"
@press-enter="saveNode(node)"
@change="saveNode(node)">
<template #suffix>
<!-- 加载中 -->
<icon-loading v-if="node.loading" />
<!-- 保存 -->
<icon-check v-else
class="pointer"
title="保存"
@click="saveNode(node)" />
</template>
</a-input>
</template>
<!-- 名称 -->
<span v-else
class="node-title-wrapper"
@click="() => emits('selectedNode', node)">
{{ node.title }}
</span>
</template>
<!-- 操作图标 -->
<template #drag-icon="{ node }">
<a-space v-if="!node.editable">
<!-- 重命名 -->
<span v-permission="['asset:host-group:update']"
class="tree-icon"
title="重命名"
@click="rename(node.title, node.key)">
<icon-edit />
</span>
<!-- 删除 -->
<a-popconfirm content="确认删除这条记录吗?"
position="left"
type="warning"
@ok="deleteNode(node.key)">
<span v-permission="['asset:host-group:delete']"
class="tree-icon"
title="删除">
<icon-delete />
</span>
</a-popconfirm>
<!-- 新增 -->
<span v-permission="['asset:host-group:create']"
class="tree-icon"
title="新增"
@click="addChildren(node)">
<icon-plus />
</span>
</a-space>
</template>
</a-tree>
<!-- 无数据 -->
<a-empty v-else-if="!loading" class="empty-container">
<span>暂无数据</span><br>
<span v-if="editable">点击上方 '<icon-plus />' 添加一个分组吧~</span>
</a-empty>
</a-scrollbar>
</template>
<script lang="ts">
export default {
name: 'hostGroupTree'
};
</script>
<script lang="ts" setup>
import type { TreeNodeData } from '@arco-design/web-vue';
import { computed, nextTick, onMounted, ref } from 'vue';
import { createGroupGroupPrefix, rootId } from '../const';
import { findNode, findParentNode, moveNode } from '@/utils/tree';
import { createHostGroup, deleteHostGroup, updateHostGroupName, moveHostGroup } from '@/api/asset/host-group';
import { isString } from '@/utils/is';
import { useCacheStore } from '@/store';
const props = withDefaults(defineProps<Partial<{
loading: boolean;
editable: boolean;
checkable: boolean;
checkedKeys: Array<number>;
}>>(), {
editable: false,
checkable: false,
});
const emits = defineEmits(['setLoading', 'onSelected', 'selectedNode', 'update:checkedKeys']);
const cacheStore = useCacheStore();
const tree = ref();
const renameInput = ref();
const treeData = ref<Array<TreeNodeData>>([]);
const checkedKeys = computed<Array<number>>({
get() {
return props.checkedKeys as Array<number>;
},
set(e) {
if (e) {
emits('update:checkedKeys', e);
} else {
emits('update:checkedKeys', []);
}
}
});
// 重命名
const rename = (title: number, key: number) => {
const node = findNode<TreeNodeData>(key, treeData.value);
if (!node) {
return;
}
node.editable = true;
node.originTitle = node.title;
nextTick(() => {
renameInput.value?.focus();
});
};
// 删除节点
const deleteNode = async (key: number) => {
try {
emits('setLoading', true);
// 删除
await deleteHostGroup(key);
// 页面删除
const parentNode = findParentNode<TreeNodeData>(key, treeData.value);
if (!parentNode) {
return;
}
const children = parentNode.root ? treeData.value : parentNode.children;
if (children) {
// 删除
for (let i = 0; i < children.length; i++) {
if (children[i].key === key) {
children.splice(i, 1);
break;
}
}
}
} catch (e) {
} finally {
emits('setLoading', false);
}
};
// 新增根节点
const addRootNode = () => {
const newKey = `${createGroupGroupPrefix}${Date.now()}`;
treeData.value.push({
title: '',
key: newKey
});
// 编辑子节点
const newNode = findNode<TreeNodeData>(newKey, treeData.value);
if (newNode) {
newNode.editable = true;
nextTick(() => {
renameInput.value?.focus();
});
}
};
// 新增子节点
const addChildren = (parentNode: TreeNodeData) => {
const newKey = `${createGroupGroupPrefix}${Date.now()}`;
const children = parentNode.children || [];
children.push({
title: '',
key: newKey
});
parentNode.children = children;
treeData.value = [...treeData.value];
nextTick(() => {
// 展开
tree.value.expandNode(parentNode.key);
// 编辑子节点
const newNode = findNode<TreeNodeData>(newKey, treeData.value);
if (newNode) {
newNode.editable = true;
nextTick(() => {
renameInput.value?.focus();
});
}
});
};
// 保存节点
const saveNode = async (node: TreeNodeData) => {
const key = node.key;
const newTitle = node.title;
node.modCount = (node.modCount || 0) + 1;
if (node.modCount != 1) {
return;
}
if (newTitle) {
node.loading = true;
try {
// 创建节点
if (isString(key) && key.startsWith(createGroupGroupPrefix)) {
const parent = findParentNode<TreeNodeData>(key, treeData.value);
if (parent.root) {
parent.key = rootId;
}
// 创建
const { data } = await createHostGroup({
parentId: parent.key as number,
name: newTitle
});
node.key = data;
} else {
// 重命名节点
await updateHostGroupName({
id: key as unknown as number,
name: newTitle
});
}
node.editable = false;
} catch (e) {
// 重复 重新聚焦
setTimeout(() => {
renameInput.value?.focus();
}, 100);
} finally {
node.loading = false;
}
} else {
// 未输入数据 并且为创建则移除节点
if (isString(key) && key.startsWith(createGroupGroupPrefix)) {
// 寻找父节点
const parent = findParentNode(key, treeData.value);
if (parent) {
// 根节点
if (parent.root) {
parent.children = treeData.value;
}
// 移除子节点
if (parent.children) {
for (let i = 0; i < parent.children.length; i++) {
if (parent.children[i].key === key) {
parent.children.splice(i, 1);
}
}
}
}
} else {
// 修改为空则设置为之前的值
node.title = node.originTitle;
}
node.editable = false;
}
// 重置 modCount
setTimeout(() => {
node.modCount = 0;
node.originTitle = undefined;
}, 50);
};
// 移动分组
const moveGroup = async (
{
dragNode, dropNode, dropPosition
}: {
dragNode: TreeNodeData,
dropNode: TreeNodeData,
dropPosition: number
}) => {
try {
emits('setLoading', true);
// 移动
await moveHostGroup({
id: dragNode.key as number,
targetId: dropNode.key as number,
position: dropPosition
});
// 移动分组
moveNode(treeData.value, dragNode, dropNode, dropPosition);
} catch (e) {
} finally {
emits('setLoading', false);
}
};
// 加载数据
const fetchTreeData = async (force = false) => {
try {
emits('setLoading', true);
const groups = await cacheStore.loadHostGroups(force);
treeData.value = groups || [];
} catch (e) {
} finally {
emits('setLoading', false);
}
// 未选择则选择首个
if (!tree.value?.getSelectedNodes()?.length && treeData.value.length) {
await nextTick(() => {
tree.value?.selectNode(treeData.value[0].key);
emits('selectedNode', treeData.value[0]);
});
}
};
defineExpose({ addRootNode, fetchTreeData });
onMounted(() => {
fetchTreeData();
});
</script>
<style lang="less" scoped>
:deep(.arco-scrollbar-container) {
height: 100%;
overflow-y: auto;
}
.tree-container {
min-width: 100%;
width: max-content;
user-select: none;
overflow: hidden;
:deep(.arco-tree-node-title) {
padding: 0;
}
.node-title-wrapper {
width: 100%;
height: 100%;
display: flex;
align-items: center;
padding-left: 8px;
}
.tree-icon {
font-size: 12px;
color: rgb(var(--primary-6));
}
}
.editable-tree {
:deep(.arco-tree-node-title) {
padding: 0 80px 0 0;
}
}
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
padding-top: 25px;
color: var(--color-text-3);
}
</style>

View File

@@ -0,0 +1,70 @@
<template>
<a-select v-model:model-value="value"
:options="optionData"
:loading="loading"
placeholder="请选择主机身份"
allow-clear />
</template>
<script lang="ts">
export default {
name: 'hostIdentitySelector'
};
</script>
<script lang="ts" setup>
import type { SelectOptionData } from '@arco-design/web-vue';
import { computed, onBeforeMount, ref } from 'vue';
import { useCacheStore } from '@/store';
import useLoading from '@/hooks/loading';
const props = withDefaults(defineProps<Partial<{
modelValue: number;
authorized: boolean;
}>>(), {
authorized: false
});
const emits = defineEmits(['update:modelValue']);
const { loading, setLoading } = useLoading();
const cacheStore = useCacheStore();
const value = computed<number>({
get() {
return props.modelValue as number;
},
set(e) {
if (e) {
emits('update:modelValue', e);
} else {
emits('update:modelValue', null);
}
}
});
const optionData = ref<Array<SelectOptionData>>([]);
// 初始化选项
onBeforeMount(async () => {
setLoading(true);
try {
const hostIdentities = props.authorized
? await cacheStore.loadAuthorizedHostIdentities()
: await cacheStore.loadHostIdentities();
optionData.value = hostIdentities.map(s => {
return {
label: `${s.name} (${s.username})`,
value: s.id,
};
});
} catch (e) {
} finally {
setLoading(false);
}
});
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,69 @@
<template>
<a-select v-model:model-value="value"
:options="optionData"
:loading="loading"
placeholder="请选择主机秘钥"
allow-clear />
</template>
<script lang="ts">
export default {
name: 'hostKeySelector'
};
</script>
<script lang="ts" setup>
import type { SelectOptionData } from '@arco-design/web-vue';
import { computed, onBeforeMount, ref } from 'vue';
import { useCacheStore } from '@/store';
import useLoading from '@/hooks/loading';
const props = withDefaults(defineProps<Partial<{
modelValue: number;
authorized: boolean;
}>>(), {
authorized: false
});
const emits = defineEmits(['update:modelValue']);
const { loading, setLoading } = useLoading();
const cacheStore = useCacheStore();
const value = computed<number>({
get() {
return props.modelValue as number;
},
set(e) {
if (e) {
emits('update:modelValue', e);
} else {
emits('update:modelValue', null);
}
}
});
const optionData = ref<Array<SelectOptionData>>([]);
// 初始化选项
onBeforeMount(async () => {
setLoading(true);
try {
const hostKeys = props.authorized
? await cacheStore.loadAuthorizedHostKeys()
: await cacheStore.loadHostKeys();
optionData.value = hostKeys.map(s => {
return {
label: s.name,
value: s.id,
};
});
} catch (e) {
} finally {
setLoading(false);
}
});
</script>
<style lang="less" scoped>
</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;
width: 100%;
height: 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.name }}
</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,274 @@
<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;
// 禁用别名
data.hostList.forEach(s => s.alias = undefined as unknown as string);
// 设置主机搜索选项
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?.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 === 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

@@ -0,0 +1,65 @@
<template>
<a-select v-model:model-value="value"
:options="optionData"
:loading="loading"
placeholder="请选择主机"
allow-clear />
</template>
<script lang="ts">
export default {
name: 'hostSelector'
};
</script>
<script lang="ts" setup>
import type { SelectOptionData } from '@arco-design/web-vue';
import { computed, onBeforeMount, ref } from 'vue';
import { useCacheStore } from '@/store';
import useLoading from '@/hooks/loading';
const props = defineProps<Partial<{
modelValue: number;
}>>();
const emits = defineEmits(['update:modelValue']);
const { loading, setLoading } = useLoading();
const cacheStore = useCacheStore();
const value = computed<number>({
get() {
return props.modelValue as number;
},
set(e) {
if (e) {
emits('update:modelValue', e);
} else {
emits('update:modelValue', null);
}
}
});
const optionData = ref<Array<SelectOptionData>>([]);
// 初始化选项
onBeforeMount(async () => {
setLoading(true);
try {
const hosts = await cacheStore.loadHosts();
optionData.value = hosts.map(s => {
return {
label: `${s.name} - ${s.address}`,
value: s.id,
};
});
} catch (e) {
} finally {
setLoading(false);
}
});
</script>
<style lang="less" scoped>
</style>