🔨 优化主机逻辑.

This commit is contained in:
lijiahang
2025-03-28 14:34:51 +08:00
parent 6b5c7fd409
commit 986f0974db
29 changed files with 1569 additions and 1051 deletions

View File

@@ -29,20 +29,17 @@
<template #empty>
<a-empty style="margin: 32px 0;" description="当前分组内无主机" />
</template>
<!-- 主机类型 -->
<template #type="{ record }">
<a-tag class="flex-center" :color="getDictValue(hostTypeKey, record.type, 'color')">
<!-- 系统类型图标 -->
<component v-if="getHostOsIcon(record.osType)"
:is="getHostOsIcon(record.osType)"
class="os-icon" />
<!-- 主机类型 -->
<span>{{ getDictValue(hostTypeKey, record.type) }}</span>
</a-tag>
</template>
<!-- 地址 -->
<template #address="{ record }">
{{ record.address }}:{{ record.port }}
<!-- 主机协议 -->
<template #protocols="{ record }">
<a-space v-if="record.types?.length"
style="margin-bottom: -8px;"
wrap>
<a-tag v-for="type in record.types"
:key="type"
:color="getDictValue(hostTypeKey, type, 'color')">
{{ getDictValue(hostTypeKey, type) }}
</a-tag>
</a-space>
</template>
</a-table>
</grant-layout>
@@ -65,7 +62,7 @@
import { flatNodeKeys } from '@/utils/tree';
import { Message } from '@arco-design/web-vue';
import { hostColumns } from '../types/table.columns';
import { getHostOsIcon, hostTypeKey } from '@/views/asset/host-list/types/const';
import { hostTypeKey } from '@/views/asset/host-list/types/const';
import { getHostGroupRelList } from '@/api/asset/host-group';
import HostGroupTree from '@/components/asset/host-group/tree/index.vue';
import GrantLayout from './grant-layout.vue';
@@ -169,19 +166,15 @@
</script>
<style lang="less" scoped>
@host-width: 560px;
.group-main-tree {
width: calc(100% - 496px);
width: calc(100% - 16px - @host-width);
height: 100%;
}
.group-main-hosts {
width: 480px;
width: @host-width;
height: 100%;
.os-icon {
width: 16px;
height: 16px;
margin-right: 6px;
}
}
</style>

View File

@@ -1,6 +1,9 @@
import HostGroupGrant from '../components/host-group-grant.vue';
import HostKeyGrant from '../components/host-key-grant.vue';
import HostIdentityGrant from '../components/host-identity-grant.vue';
import { defineAsyncComponent } from 'vue';
// 组件
const HostGroupGrant = defineAsyncComponent(() => import('../components/host-group-grant.vue'))
const HostKeyGrant = defineAsyncComponent(() => import('../components/host-key-grant.vue'))
const HostIdentityGrant = defineAsyncComponent(() => import('../components/host-identity-grant.vue'))
// 路由
export const GrantRouteName = 'assetGrant';

View File

@@ -11,11 +11,11 @@ export const hostColumns = [
align: 'left',
fixed: 'left',
}, {
title: '主机类型',
dataIndex: 'type',
slotName: 'type',
align: 'center',
width: 100,
title: '主机协议',
dataIndex: 'protocols',
slotName: 'protocols',
align: 'left',
width: 134,
}, {
title: '主机名称',
dataIndex: 'name',

View File

@@ -1,212 +0,0 @@
<template>
<a-card class="general-card" :body-style="{ padding: '0 16px 0 20px'}">
<!-- 标题 -->
<template #title>
<div class="config-title-wrapper">
<span class="title">SSH 配置</span>
<!-- 操作按钮 -->
<a-space>
<a-button size="small"
@click="emits('reset')">
重置
</a-button>
<a-button type="primary"
size="small"
@click="saveConfig">
保存
</a-button>
</a-space>
</div>
</template>
<!-- 表单 -->
<div class="config-form-wrapper full">
<!-- 表单 -->
<a-form :model="formModel"
ref="formRef"
label-align="right"
:auto-label-width="true"
:rules="rules">
<!-- 用户名 -->
<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>
<!-- 认证方式 -->
<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 class="mt4"
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>
</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 type { HostConfigQueryResponse } from '@/api/asset/host';
import { ref, watch } from 'vue';
import { sshAuthTypeKey, SshAuthType } from '../types/const';
import { useDictStore } from '@/store';
import rules from '../types/ssh-form.rules';
import HostKeySelector from '@/components/asset/host-key/selector/index.vue';
import HostIdentitySelector from '@/components/asset/host-identity/selector/index.vue';
const { toOptions, toRadioOptions } = useDictStore();
const props = defineProps<{
hostConfig: HostConfigQueryResponse;
}>();
const emits = defineEmits(['save', 'reset']);
const formRef = ref();
const formModel = ref<HostSshConfig>({
username: undefined,
password: undefined,
authType: SshAuthType.PASSWORD,
keyId: undefined,
identityId: undefined,
connectTimeout: undefined,
charset: undefined,
fileNameCharset: undefined,
fileContentCharset: undefined,
useNewPassword: false,
hasPassword: false,
});
// 监听数据变化
watch(() => props.hostConfig.current, () => {
formModel.value = Object.assign({}, props.hostConfig.config);
// 使用新密码默认为不包含密码
formModel.value.useNewPassword = !formModel.value.hasPassword;
});
// 用户名认证
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 saveConfig = async () => {
// 验证参数
const error = await formRef.value.validate();
if (error) {
return false;
}
// 回调
emits('save', { ...formModel.value });
};
</script>
<style lang="less" scoped>
.auth-type-group {
width: 100%;
display: flex;
justify-content: space-between;
}
.password-switch {
width: 148px;
margin-left: 8px;
}
</style>

View File

@@ -1,174 +0,0 @@
<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 class="host-config-wrapper"
:host-config="hostConfig"
@save="save"
@reset="reset" />
</a-spin>
</a-drawer>
</template>
<script lang="ts">
export default {
name: 'hostConfigDrawer'
};
</script>
<script lang="ts" setup>
import type { HostConfigQueryResponse } from '@/api/asset/host';
import { ref } from 'vue';
import useVisible from '@/hooks/visible';
import useLoading from '@/hooks/loading';
import { Message } from '@arco-design/web-vue';
import { useCacheStore, useDictStore } from '@/store';
import { dictKeys } from '../types/const';
import { getHostConfig, updateHostConfig } from '@/api/asset/host';
import { HostType } from '@/views/asset/host-list/types/const';
import { encrypt } from '@/utils/rsa';
import SshConfigForm from '../components/ssh-config-form.vue';
const { visible, setVisible } = useVisible();
const { loading, setLoading } = useLoading();
const cacheStore = useCacheStore();
const record = ref({} as any);
const hostConfig = ref<HostConfigQueryResponse>({} as HostConfigQueryResponse);
// 打开
const open = async (e: any) => {
record.value = { ...e };
hostConfig.value = { config: {} } as HostConfigQueryResponse;
try {
setLoading(true);
setVisible(true);
// 加载字典值
const dictStore = useDictStore();
await dictStore.loadKeys(dictKeys);
// 加载配置
const { data } = await getHostConfig(record.value.id);
data.current = Date.now();
hostConfig.value = data;
} catch ({ message }) {
Message.error(`配置加载失败 ${message}`);
setVisible(false);
} finally {
setLoading(false);
}
};
defineExpose({ open });
// 保存
const save = async (conf: Record<string, any>) => {
// 加密参数
const newConfig = { ...conf };
try {
// 加密 SSH
if (record.value.type === HostType.SSH.value) {
newConfig.password = await encrypt(conf.password);
}
} catch (e) {
return;
}
try {
setLoading(true);
// 更新
await updateHostConfig({
id: hostConfig.value.id,
config: JSON.stringify(newConfig),
});
// 设置参数
hostConfig.value.config = conf;
Message.success('修改成功');
} catch (e) {
} finally {
setLoading(false);
}
};
// 重置
const reset = () => {
// 修改 current 让子组件重新渲染
hostConfig.value.current = Date.now();
};
// 关闭
const handleCancel = () => {
setLoading(false);
setVisible(false);
};
</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%;
min-height: 100%;
background-color: var(--color-fill-2);
}
.host-config-wrapper {
margin: 18px;
}
:deep(.config-title-wrapper) {
display: flex;
align-items: center;
justify-content: space-between;
.title {
font-weight: 600;
user-select: none;
}
}
:deep(.config-button-group) {
display: flex;
align-items: center;
justify-content: flex-end;
}
</style>

View File

@@ -1,33 +0,0 @@
// 主机 SSH 配置
export interface HostSshConfig {
username?: string;
password?: string;
authType?: string;
keyId?: number;
identityId?: number;
connectTimeout?: number;
charset?: string;
fileNameCharset?: string;
fileContentCharset?: string;
useNewPassword?: boolean;
hasPassword?: boolean;
}
// 主机认证方式
export const SshAuthType = {
// 密码认证
PASSWORD: 'PASSWORD',
// 密钥认证
KEY: 'KEY',
// 身份认证
IDENTITY: 'IDENTITY'
};
// 主机认证方式 字典项
export const sshAuthTypeKey = 'hostSshAuthType';
// 主机系统类型 字典项
export const sshOsTypeKey = 'hostSshOsType';
// 加载的字典值
export const dictKeys = [sshAuthTypeKey, sshOsTypeKey];

View File

@@ -1,60 +0,0 @@
import type { FieldRule } from '@arco-design/web-vue';
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 {
authType,
keyId,
identityId,
connectTimeout,
charset,
fileNameCharset,
fileContentCharset,
} as Record<string, FieldRule | FieldRule[]>;

View File

@@ -73,11 +73,11 @@
<a-form-item field="address" label="主机地址">
<a-input v-model="formModel.address" placeholder="请输入主机地址" allow-clear />
</a-form-item>
<!-- 主机类型 -->
<a-form-item field="type" label="主机类型">
<!-- 主机协议 -->
<a-form-item field="type" label="主机协议">
<a-select v-model="formModel.type"
:options="toOptions(hostTypeKey)"
placeholder="请选择主机类型"
placeholder="请选择主机协议"
allow-clear />
</a-form-item>
<!-- 系统类型 -->
@@ -87,6 +87,13 @@
placeholder="请选择系统类型"
allow-clear />
</a-form-item>
<!-- 系统架构 -->
<a-form-item field="archType" label="系统架构">
<a-select v-model="formModel.archType"
:options="toOptions(hostArchTypeKey)"
placeholder="请选择系统架构"
allow-clear />
</a-form-item>
<!-- 主机状态 -->
<a-form-item field="status" label="主机状态">
<a-select v-model="formModel.status"
@@ -118,7 +125,7 @@
:is="getHostOsIcon(record.osType)"
class="os-icon" />
<!-- 主机名称 -->
<span>{{ record.name }}</span>
<span class="host-name">{{ record.name }}</span>
</div>
</template>
<!-- 编码 -->
@@ -131,24 +138,42 @@
{{ record.description || '-' }}
</span>
</template>
<!-- 主机类型 -->
<template #type="{ record }">
<a-tag :color="getDictValue(hostTypeKey, record.type, 'color')">
{{ getDictValue(hostTypeKey, record.type) }}
</a-tag>
<!-- 主机协议 -->
<template #protocols="{ record }">
<a-space v-if="record.types?.length"
style="margin-bottom: -8px;"
wrap>
<a-tag v-for="type in record.types"
:key="type"
:color="getDictValue(hostTypeKey, type, 'color')">
{{ getDictValue(hostTypeKey, type) }}
</a-tag>
</a-space>
</template>
<!-- 地址 -->
<!-- 主机地址 -->
<template #address="{ record }">
<span class="span-blue text-copy host-address"
title="复制"
@click="copy(record.address)">
{{ record.address }}
</span>
<span class="span-blue text-copy"
<span class="span-blue text-copy host-address"
title="复制"
@click="copy(record.port)">
{{ record.port }}
</span>
@click="copy(record.address)">
{{ record.address }}
</span>
</template>
<!-- 主机规格 -->
<template #hostSpec="{ record }">
<span v-if="record.spec">
{{
[
addSuffix(record.spec.cpuCore, 'C'),
addSuffix(record.spec.memorySize, 'G'),
addSuffix(record.spec.diskSize, 'G')
].filter(Boolean).join('/') || '-'
}}
</span>
<span v-else>-</span>
<!-- 系统架构 -->
<span class="ml4">
- {{ getDictValue(hostArchTypeKey, record.archType) }}
</span>
</template>
<!-- 主机状态 -->
<template #status="{ record }">
@@ -156,15 +181,31 @@
{{ getDictValue(hostStatusKey, record.status) }}
</a-tag>
</template>
<!-- 主机分组 -->
<template #groups="{ record }">
<a-space v-if="record.groupIdList?.length"
style="margin-bottom: -8px;"
:wrap="true">
<template v-for="groupId in record.groupIdList"
:key="groupId">
<a-tag color="green">
{{ hostGroupList.find(s => s.key === groupId)?.title || groupId }}
</a-tag>
</template>
</a-space>
</template>
<!-- 标签 -->
<template #tags="{ record }">
<a-space v-if="record.tags" wrap style="margin-bottom: -8px;">
<a-space v-if="record.tags?.length"
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>
<span v-else>-</span>
</template>
<!-- 拓展操作 -->
<template #extra="{ record }">
@@ -178,11 +219,6 @@
@click="emits('openUpdate', record)">
<span class="more-doption normal">修改</span>
</a-doption>
<!-- 配置 -->
<a-doption v-permission="['asset:host:update-config']"
@click="emits('openUpdateConfig', record)">
<span class="more-doption normal">配置</span>
</a-doption>
<!-- 修改状态 -->
<a-doption v-permission="['asset:host:update-status']"
@click="updateStatus(record as HostQueryResponse)">
@@ -203,13 +239,13 @@
<span class="more-doption error">删除</span>
</a-doption>
<!-- SSH -->
<a-doption v-if="record.type === HostType.SSH.value"
<a-doption v-if="record.types.includes(HostType.SSH.value)"
v-permission="['asset:terminal:access']"
@click="openNewRoute({ name: 'terminal', query: { connect: record.id, type: 'SSH' } })">
<span class="more-doption normal">SSH</span>
</a-doption>
<!-- SFTP -->
<a-doption v-if="record.type === HostType.SSH.value"
<a-doption v-if="record.types.includes(HostType.SSH.value)"
v-permission="['asset:terminal:access']"
@click="openNewRoute({ name: 'terminal', query: { connect: record.id, type: 'SFTP' } })">
<span class="more-doption normal">SFTP</span>
@@ -229,23 +265,24 @@
<script lang="ts" setup>
import type { HostQueryRequest, HostQueryResponse } from '@/api/asset/host';
import type { HostGroupQueryResponse } from '@/api/asset/host-group';
import { useCardPagination, useCardColLayout, useCardFieldConfig } from '@/hooks/card';
import { computed, reactive, ref, onMounted } from 'vue';
import { dataColor, objectTruthKeyCount, resetObject } from '@/utils';
import { addSuffix, dataColor, objectTruthKeyCount, resetObject } from '@/utils';
import { deleteHost, getHostPage, updateHostStatus } from '@/api/asset/host';
import { Message, Modal } from '@arco-design/web-vue';
import { getHostOsIcon, hostOsTypeKey, hostStatusKey, HostType, hostTypeKey, TableName, tagColor } from '../types/const';
import { getHostOsIcon, hostOsTypeKey, hostArchTypeKey, hostStatusKey, HostType, hostTypeKey, tagColor, TableName } from '../types/const';
import { copy } from '@/hooks/copy';
import { useCacheStore, useDictStore } from '@/store';
import { useQueryOrder, ASC } from '@/hooks/query-order';
import { GrantKey, GrantRouteName } from '@/views/asset/grant/types/const';
import { useRouter } from 'vue-router';
import useLoading from '@/hooks/loading';
import fieldConfig from '../types/card.fields';
import { ASC, useQueryOrder } from '@/hooks/query-order';
import { openNewRoute } from '@/router';
import TagMultiSelector from '@/components/meta/tag/multi-selector/index.vue';
const emits = defineEmits(['openAdd', 'openUpdate', 'openUpdateConfig', 'openHostGroup', 'openCopy']);
const emits = defineEmits(['openAdd', 'openUpdate', 'openHostGroup', 'openCopy']);
const router = useRouter();
const cacheStore = useCacheStore();
@@ -257,6 +294,7 @@
const { toOptions, getDictValue, toggleDictValue, toggleDict } = useDictStore();
const list = ref<HostQueryResponse[]>([]);
const hostGroupList = ref<Array<HostGroupQueryResponse>>([]);
const formRef = ref();
const formModel = reactive<HostQueryRequest>({
searchValue: undefined,
@@ -266,15 +304,18 @@
address: undefined,
type: undefined,
osType: undefined,
archType: undefined,
status: undefined,
tags: undefined,
description: undefined,
queryGroup: true,
queryTag: true,
querySpec: true,
});
// 条件数量
const filterCount = computed(() => {
return objectTruthKeyCount(formModel, ['searchValue', 'queryTag']);
return objectTruthKeyCount(formModel, ['searchValue', 'queryTag', 'queryGroup', 'querySpec']);
});
// 更新状态
@@ -340,7 +381,7 @@
// 重置条件
const reset = () => {
resetObject(formModel, ['queryTag']);
resetObject(formModel, ['queryTag', 'queryGroup', 'querySpec']);
fetchCardData();
};
@@ -364,7 +405,10 @@
doFetchCardData({ page, limit, ...form });
};
onMounted(() => {
onMounted(async () => {
// 加载分组数据
hostGroupList.value = await useCacheStore().loadHostGroupList();
// 加载主机数据
fetchCardData();
});
@@ -381,10 +425,9 @@
height: 20px;
margin-right: 6px;
}
}
.host-address::after {
content: ':';
user-select: text;
.host-name {
font-size: 16px;
}
}
</style>

View File

@@ -0,0 +1,179 @@
<template>
<a-drawer v-model:visible="visible"
:title="title"
:width="600"
:esc-to-close="false"
:mask-closable="false"
:unmount-on-close="true"
:footer="false"
@close="handleClose">
<div class="host-from-container">
<a-tabs v-model:active-key="activeTab"
class=""
type="rounded"
direction="vertical"
:justify="true"
:lazy-load="true">
<!-- 主机信息 -->
<a-tab-pane v-permission="['asset:host:update']"
key="info"
title="主机信息">
<host-form-info ref="infoRef"
class="form-panel"
@change-type="(ts: string[]) => types = ts"
@updated="updateHostInfo" />
</a-tab-pane>
<!-- 规格配置 -->
<a-tab-pane v-permission="['asset:host:update']"
key="spec"
title="规格配置"
:disabled="!hostId">
<host-form-spec v-if="hostId"
class="form-panel"
:hostId="hostId"
@updated="incrUpdatedCount" />
</a-tab-pane>
<!-- SSH 配置 -->
<a-tab-pane v-permission="['asset:host:update-config']"
key="ssh"
title="SSH"
:disabled="!hostId || !types.includes(HostType.SSH.value)">
<host-form-ssh v-if="hostId"
class="form-panel"
:hostId="hostId" />
</a-tab-pane>
</a-tabs>
</div>
</a-drawer>
</template>
<script lang="ts">
export default {
name: 'hostFormDrawer'
};
</script>
<script lang="ts" setup>
import type { HostUpdateRequest } from '@/api/asset/host';
import { ref, nextTick } from 'vue';
import useVisible from '@/hooks/visible';
import { Message } from '@arco-design/web-vue';
import { useCacheStore, useDictStore } from '@/store';
import { HostType } from '../types/const';
import { useCounter } from '@vueuse/core';
import HostFormInfo from './host-form-info.vue';
import HostFormSsh from './host-form-ssh.vue';
import HostFormSpec from './host-form-spec.vue';
const { toOptions } = useDictStore();
const { count: updatedCount, inc: incrUpdatedCount, reset: resetCounter } = useCounter();
const { visible, setVisible } = useVisible();
const activeTab = ref<string>('info');
const title = ref<string>();
const hostId = ref<number>();
const types = ref<string[]>([]);
const infoRef = ref();
const formRef = ref();
const formModel = ref<HostUpdateRequest>({});
const emits = defineEmits(['reload']);
// 打开新增
const openAdd = () => {
init('添加主机', undefined);
nextTick(() => {
infoRef.value.openAdd();
});
};
// 打开修改
const openUpdate = (id: number) => {
init('修改主机', id);
nextTick(() => {
infoRef.value.openUpdate(id);
});
};
// 打开复制
const openCopy = (id: number) => {
init('复制主机', id);
nextTick(() => {
infoRef.value.openCopy(id);
});
};
// 初始化
const init = (_title: string, id: number | undefined) => {
title.value = _title;
activeTab.value = 'info';
hostId.value = id;
types.value = [];
checkHostGroup();
resetCounter();
setVisible(true);
};
// 检查是否有主机分组
const checkHostGroup = () => {
useCacheStore().loadHostGroupTree().then(s => {
if (!s?.length) {
Message.warning('请先配置主机分组');
setVisible(false);
}
});
};
defineExpose({ openAdd, openUpdate, openCopy });
// 更新主机信息
const updateHostInfo = (id: number) => {
hostId.value = id;
incrUpdatedCount();
};
// 处理关闭
const handleClose = () => {
if (updatedCount.value) {
emits('reload');
}
};
</script>
<style lang="less" scoped>
@drawer-width: 600px;
@nav-width: 104px;
.host-from-container {
height: 100%;
width: 100%;
padding: 16px 0 16px 16px;
display: flex;
overflow: hidden;
:deep(.arco-tabs-pane) {
border-left: 1px var(--color-neutral-3) solid;
}
:deep(.arco-tabs-nav) {
user-select: none;
}
:deep(.arco-tabs-content) {
width: @drawer-width - @nav-width;
overflow: auto;
}
:deep(.extra-button) {
margin-left: 8px;
width: 168px;
}
}
.form-panel {
width: 100%;
height: 100%;
padding: 0 24px;
}
</style>

View File

@@ -0,0 +1,213 @@
<template>
<a-spin :loading="loading">
<a-form :model="formModel"
ref="formRef"
label-align="right"
:auto-label-width="true"
:rules="hostFormRules">
<!-- 主机协议 -->
<a-form-item field="types" label="主机协议">
<a-select v-model="formModel.types"
placeholder="请选择支持的主机协议"
:options="toOptions(hostTypeKey)"
multiple
allow-clear />
</a-form-item>
<!-- 系统类型 -->
<a-form-item field="osType" label="系统类型">
<a-select v-model="formModel.osType"
placeholder="请选择系统类型"
:options="toOptions(hostOsTypeKey)"
allow-clear />
</a-form-item>
<!-- 系统架构 -->
<a-form-item field="archType" label="系统架构">
<a-select v-model="formModel.archType"
placeholder="请选择系统架构"
:options="toOptions(hostArchTypeKey)"
allow-clear />
</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="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"
type="HOST"
:limit="5"
:tag-color="tagColor"
:allow-create="true"
placeholder="请选择主机标签"
@on-limited="onLimitedTag" />
</a-form-item>
<!-- 主机描述 -->
<a-form-item field="description" label="主机描述">
<a-textarea v-model="formModel.description"
placeholder="请输入主机描述"
:auto-size="{ minRows: 3, maxRows: 3}"
allow-clear />
</a-form-item>
<!-- 保存 -->
<a-form-item style="margin-bottom: 0;">
<a-button type="primary"
long
@click="saveHost">
保存
</a-button>
</a-form-item>
</a-form>
</a-spin>
</template>
<script lang="ts">
export default {
name: 'hostFormInfo'
};
</script>
<script lang="ts" setup>
import type { HostUpdateRequest } from '@/api/asset/host';
import { ref } from 'vue';
import useLoading from '@/hooks/loading';
import { hostFormRules } 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, hostTypeKey, hostOsTypeKey, HostOsType, hostArchTypeKey } from '../types/const';
import { useDictStore } from '@/store';
import TagMultiSelector from '@/components/meta/tag/multi-selector/index.vue';
import HostGroupTreeSelector from '@/components/asset/host-group/tree-selector/index.vue';
const emits = defineEmits(['changeType', 'updated']);
const { toOptions } = useDictStore();
const { loading, setLoading } = useLoading();
const defaultForm = (): HostUpdateRequest => {
return {
id: undefined,
osType: HostOsType.LINUX.value,
archType: 'AMD64',
types: [],
name: undefined,
code: undefined,
address: undefined,
tags: undefined,
groupIdList: undefined,
description: undefined,
};
};
const formRef = ref();
const formModel = ref<HostUpdateRequest>({});
// 打开新增
const openAdd = () => {
renderForm({ ...defaultForm() });
};
// 打开修改
const openUpdate = async (id: number) => {
renderForm({ ...defaultForm() });
await fetchHostRender(id);
};
// 打开复制
const openCopy = async (id: number) => {
renderForm({ ...defaultForm() });
await fetchHostRender(id);
};
defineExpose({ openAdd, openUpdate, openCopy });
// 渲染主机
const fetchHostRender = async (id: number) => {
try {
setLoading(true);
const { data } = await getHost(id);
const detail = Object.assign({} as Record<string, any>,
pick(data, 'id', 'types', 'osType', 'archType', 'name', 'code', 'address', 'port', 'status', 'groupIdList', 'description'));
// tag
const tags = (data.tags || []).map(s => s.id);
// 渲染
renderForm({ ...detail, tags });
// 响应类型
emits('changeType', data.types);
} catch (e) {
} finally {
setLoading(false);
}
};
// 渲染表单
const renderForm = (record: any) => {
// 渲染表单
formModel.value = Object.assign({}, record);
};
// tag 超出所选限制
const onLimitedTag = (count: number, message: string) => {
formRef.value.setFields({
tags: {
status: 'error',
message
}
});
// 因为输入框已经限制数量 这里只做提示
setTimeout(() => {
formRef.value.clearValidate('tags');
}, 3000);
};
// 保存
const saveHost = async () => {
setLoading(true);
try {
// 验证参数
const error = await formRef.value.validate();
if (error) {
return;
}
if (!formModel.value.id) {
// 新增
const { data } = await createHost(formModel.value);
Message.success('创建成功');
emits('updated', data);
} else {
// 修改
await updateHost(formModel.value);
Message.success('修改成功');
emits('updated', formModel.value.id);
}
emits('changeType', formModel.value.types);
} catch (e) {
} finally {
setLoading(false);
}
};
</script>
<style lang="less" scoped>
</style>

View File

@@ -1,243 +0,0 @@
<template>
<a-modal v-model:visible="visible"
modal-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 v-if="isAddHandle"
field="type"
label="主机类型"
help="主机创建后, 类型则无法修改">
<a-select v-model="formModel.type"
placeholder="请选择主机类型"
:options="toOptions(hostTypeKey)"
allow-clear />
</a-form-item>
<!-- 系统类型 -->
<a-form-item field="osType" label="系统类型">
<a-select v-model="formModel.osType"
placeholder="请选择系统类型"
:options="toOptions(hostOsTypeKey)"
allow-clear />
</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="port" label="主机端口">
<a-input-number v-model="formModel.port"
placeholder="请输入主机端口"
hide-button
allow-clear />
</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"
type="HOST"
:limit="5"
:tag-color="tagColor"
:allow-create="true"
placeholder="请选择主机标签"
@on-limited="onLimitedTag" />
</a-form-item>
<!-- 主机描述 -->
<a-form-item field="description" label="主机描述">
<a-textarea v-model="formModel.description"
placeholder="请输入主机描述"
allow-clear />
</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, HostType, hostTypeKey, hostOsTypeKey, HostOsType } from '../types/const';
import { useDictStore } from '@/store';
import TagMultiSelector from '@/components/meta/tag/multi-selector/index.vue';
import HostGroupTreeSelector from '@/components/asset/host-group/tree-selector/index.vue';
const { toOptions } = useDictStore();
const { visible, setVisible } = useVisible();
const { loading, setLoading } = useLoading();
const title = ref<string>();
const isAddHandle = ref<boolean>(true);
const defaultForm = (): HostUpdateRequest => {
return {
id: undefined,
type: HostType.SSH.value,
osType: HostOsType.LINUX.value,
name: undefined,
code: undefined,
address: undefined,
port: HostType.SSH.port,
tags: undefined,
groupIdList: undefined,
description: 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 openCopy = async (id: number) => {
title.value = '复制主机';
isAddHandle.value = true;
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', 'type', 'osType', 'name', 'code', 'address', 'port', 'status', 'groupIdList', 'description'));
// 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, openCopy });
// 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>

View File

@@ -0,0 +1,337 @@
<template>
<a-spin :loading="loading">
<!-- 表单 -->
<a-descriptions :column="1" bordered>
<!-- 系统名称 -->
<a-descriptions-item label="系统名称" :span="2">
<a-input v-if="editing"
v-model="formModel.osName"
class="input"
size="mini"
allow-clear />
<span v-else class="text">{{ formModel.osName }}</span>
</a-descriptions-item>
<!-- CPU型号 -->
<a-descriptions-item label="CPU型号" :span="2">
<a-input v-if="editing"
v-model="formModel.cpuModel"
class="input"
size="mini"
allow-clear />
<span v-else class="text">{{ formModel.cpuModel }}</span>
</a-descriptions-item>
<!-- CPU核心数 -->
<a-descriptions-item label="CPU核心数">
<a-input-number v-if="editing"
v-model="formModel.cpuCore"
class="input"
size="mini"
hide-button
allow-clear />
<span v-else class="text">{{ formModel.cpuCore }}</span>
</a-descriptions-item>
<!-- CPU速率 -->
<a-descriptions-item label="CPU速率">
<a-input-number v-if="editing"
v-model="formModel.cpuFrequency"
class="input"
size="mini"
hide-button
allow-clear>
<template #suffix>
GHz
</template>
</a-input-number>
<span v-else class="text">{{ addSuffix(formModel.cpuFrequency, 'GHz') }}</span>
</a-descriptions-item>
<!-- 内存 -->
<a-descriptions-item label="内存">
<a-input-number v-if="editing"
v-model="formModel.memorySize"
class="input"
size="mini"
hide-button
allow-clear>
<template #suffix>
G
</template>
</a-input-number>
<span v-else class="text">{{ addSuffix(formModel.memorySize, 'G') }}</span>
</a-descriptions-item>
<!-- 硬盘 -->
<a-descriptions-item label="硬盘">
<a-input-number v-if="editing"
v-model="formModel.diskSize"
class="input"
size="mini"
hide-button
allow-clear>
<template #suffix>
G
</template>
</a-input-number>
<span v-else class="text">{{ addSuffix(formModel.diskSize, 'G') }}</span>
</a-descriptions-item>
<!-- 上行带宽 -->
<a-descriptions-item label="上行带宽">
<a-input-number v-if="editing"
v-model="formModel.inBandwidth"
class="input"
size="mini"
hide-button
allow-clear>
<template #suffix>
M
</template>
</a-input-number>
<span v-else class="text">{{ addSuffix(formModel.inBandwidth, 'M') }}</span>
</a-descriptions-item>
<!-- 下行带宽 -->
<a-descriptions-item label="下行带宽">
<a-input-number v-if="editing"
v-model="formModel.outBandwidth"
class="input"
size="mini"
hide-button
allow-clear>
<template #suffix>
M
</template>
</a-input-number>
<span v-else class="text">{{ addSuffix(formModel.outBandwidth, 'M') }}</span>
</a-descriptions-item>
<!-- 公网地址 -->
<a-descriptions-item label="公网地址" :span="2">
<a-select v-if="editing"
v-model="formModel.publicIpAddress"
class="input"
size="mini"
multiple
hide-button
allow-create
allow-clear />
<a-space v-else-if="formModel.publicIpAddress?.length"
class="text"
style="margin-bottom: -8px;"
wrap>
<a-tag v-for="addr in formModel.publicIpAddress">
{{ addr }}
</a-tag>
</a-space>
<span v-else class="text" />
</a-descriptions-item>
<!-- 私网地址 -->
<a-descriptions-item label="私网地址" :span="2">
<a-select v-if="editing"
v-model="formModel.privateIpAddress"
class="input"
size="mini"
multiple
hide-button
allow-create
allow-clear />
<a-space v-else-if="formModel.privateIpAddress?.length"
class="text"
style="margin-bottom: -8px;"
wrap>
<a-tag v-for="addr in formModel.privateIpAddress">
{{ addr }}
</a-tag>
</a-space>
<span v-else class="text" />
</a-descriptions-item>
<!-- 负责人 -->
<a-descriptions-item label="负责人" :span="2">
<a-input v-if="editing"
v-model="formModel.chargePerson"
class="input"
size="mini"
allow-clear />
<span v-else class="text">{{ formModel.chargePerson }}</span>
</a-descriptions-item>
<!-- 创建时间 -->
<a-descriptions-item label="创建时间" :span="2">
<a-date-picker v-if="editing"
v-model="formModel.createdTime"
class="input"
size="mini"
placeholder=" "
allow-clear />
<span v-else-if="formModel.createdTime" class="text">{{ dateFormat(new Date(formModel.createdTime), 'yyyy-MM-dd') }}</span>
<span v-else class="text" />
</a-descriptions-item>
<!-- 到期时间 -->
<a-descriptions-item label="到期时间" :span="2">
<a-date-picker v-if="editing"
v-model="formModel.expiredTime"
value-format="timestamp"
class="input"
size="mini"
placeholder=" "
allow-clear />
<span v-else-if="formModel.expiredTime" class="text">{{ dateFormat(new Date(formModel.expiredTime), 'yyyy-MM-dd') }}</span>
<span v-else class="text" />
</a-descriptions-item>
<!-- 自定义规格 -->
<template v-if="formModel.items?.length">
<a-descriptions-item v-for="(item, index) in formModel.items"
:key="index"
:span="2">
<template #label>
<a-input v-if="editing"
v-model="item.label"
value-format="timestamp"
class="label"
size="mini"
allow-clear />
<span v-else class="label">{{ item.label }}</span>
</template>
<!-- -->
<div class="item-wrapper">
<a-input v-if="editing"
v-model="item.value"
class="input"
size="mini"
allow-clear />
<span v-else class="text">{{ item.value }}</span>
<!-- 移除 -->
<a-button v-if="editing"
class="ml4"
size="mini"
type="text"
@click="removeSpec(index)">
移除
</a-button>
</div>
</a-descriptions-item>
</template>
</a-descriptions>
<!-- 操作 -->
<div class="actions">
<!-- 编辑 -->
<a-button v-if="!editing"
type="primary"
long
@click="toggleEditing()">
编辑
</a-button>
<!-- 保存 -->
<a-button v-else
type="primary"
long
@click="saveSpec">
保存
</a-button>
<!-- 新增规格 -->
<a-button v-if="editing"
class="extra-button"
type="primary"
long
@click="addSpec">
新增规格
</a-button>
</div>
</a-spin>
</template>
<script lang="ts">
export default {
name: 'hostFormSpec'
};
</script>
<script lang="ts" setup>
import type { HostSpecExtraModel } from '@/api/asset/host-extra';
import { onMounted, ref } from 'vue';
import { addSuffix, dateFormat } from '@/utils';
import { useToggle } from '@vueuse/core';
import useLoading from '@/hooks/loading';
import { getHostExtraItem, updateHostExtra } from '@/api/asset/host-extra';
const props = defineProps<{
hostId: number;
}>();
const emits = defineEmits(['updated']);
const { loading, setLoading } = useLoading();
const [editing, toggleEditing] = useToggle();
const formRef = ref();
const formModel = ref<HostSpecExtraModel>({} as HostSpecExtraModel);
// 加载配置
const fetchHostSpec = async () => {
setLoading(true);
try {
const { data } = await getHostExtraItem<HostSpecExtraModel>({ hostId: props.hostId, item: 'SPEC' });
formModel.value = data;
} catch (e) {
} finally {
setLoading(false);
}
};
// 添加规格
const addSpec = () => {
if (!formModel.value.items) {
formModel.value.items = [];
}
formModel.value.items.push({ label: '', value: '' });
};
// 移除规格
const removeSpec = (index: number) => {
formModel.value.items.splice(index, 1);
};
// 保存规格
const saveSpec = async () => {
setLoading(true);
try {
await updateHostExtra({
hostId: props.hostId,
item: 'SPEC',
extra: JSON.stringify(formModel.value)
});
toggleEditing();
emits('updated');
} catch (e) {
} finally {
setLoading(false);
}
};
onMounted(fetchHostSpec);
</script>
<style lang="less" scoped>
:deep(.arco-descriptions-item-value) {
padding: 6px 8px !important;
}
.label {
display: flex;
overflow: hidden;
}
.input, .text {
min-height: 24px;
font-size: 13px;
}
.text {
display: flex;
}
.item-wrapper {
display: flex;
width: 306px;
}
.actions {
width: 100%;
margin-top: 16px;
display: flex;
}
</style>

View File

@@ -0,0 +1,250 @@
<template>
<a-spin :loading="loading">
<!-- 表单 -->
<a-form :model="formModel"
ref="formRef"
label-align="right"
:auto-label-width="true"
:rules="sshFormRules">
<!-- 端口 -->
<a-form-item field="port" label="端口">
<a-input-number v-model="formModel.port"
placeholder="请输入 SSH 端口"
hide-button />
</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>
<!-- 认证方式 -->
<a-form-item field="authType" label="认证方式">
<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="主机密钥">
<host-key-selector v-model="formModel.keyId" />
</a-form-item>
<!-- 主机身份 -->
<a-form-item v-if="SshAuthType.IDENTITY === formModel.authType"
field="identityId"
label="主机身份">
<host-identity-selector v-model="formModel.identityId" />
</a-form-item>
<!-- 连接超时时间 -->
<a-form-item field="connectTimeout" label="连接超时时间">
<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输出编码">
<a-input v-model="formModel.charset" placeholder="请输入 SSH 输出编码" />
</a-form-item>
<!-- 文件名称编码 -->
<a-form-item field="fileNameCharset" label="文件名称编码">
<a-input v-model="formModel.fileNameCharset" placeholder="请输入 SFTP 文件名称编码" />
</a-form-item>
<!-- 文件内容编码 -->
<a-form-item field="fileContentCharset" label="文件内容编码">
<a-input v-model="formModel.fileContentCharset" placeholder="请输入 SFTP 文件内容编码" />
</a-form-item>
<!-- 操作 -->
<a-form-item style="margin-bottom: 0;">
<!-- 保存 -->
<a-button type="primary"
long
@click="saveConfig">
保存
</a-button>
<!-- 测试连接 -->
<a-tooltip position="tr"
content="请先保存后测试连接"
mini>
<a-button class="extra-button"
type="primary"
:loading="connectLoading"
long
@click="testConnect">
测试连接
</a-button>
</a-tooltip>
</a-form-item>
</a-form>
</a-spin>
</template>
<script lang="ts">
export default {
name: 'hostFormSsh'
};
</script>
<script lang="ts" setup>
import type { FieldRule } from '@arco-design/web-vue';
import type { HostSshConfig } from '@/api/asset/host-config';
import { ref, onMounted } from 'vue';
import useLoading from '@/hooks/loading';
import { useDictStore } from '@/store';
import { sshAuthTypeKey, SshAuthType, HostType } from '../types/const';
import { sshFormRules } from '../types/form.rules';
import { Message } from '@arco-design/web-vue';
import { encrypt } from '@/utils/rsa';
import { testHostConnect } from '@/api/asset/host';
import { getHostSshConfig, updateHostConfig } from '@/api/asset/host-config';
import HostIdentitySelector from '@/components/asset/host-identity/selector/index.vue';
import HostKeySelector from '@/components/asset/host-key/selector/index.vue';
const props = defineProps<{
hostId: number;
}>();
const { loading, setLoading } = useLoading();
const { loading: connectLoading, setLoading: setConnectLoading } = useLoading();
const { toOptions, toRadioOptions } = useDictStore();
const formRef = ref();
const formModel = ref<HostSshConfig>({} as HostSshConfig);
// 用户名验证
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 fetchHostConfig = async () => {
try {
setLoading(true);
// 加载配置
const { data } = await getHostSshConfig<HostSshConfig>({
hostId: props.hostId,
type: HostType.SSH.value,
});
formModel.value = data;
// 使用新密码默认为不包含密码
formModel.value.useNewPassword = !formModel.value.hasPassword;
} catch ({ message }) {
Message.error(`配置加载失败 ${message}`);
} finally {
setLoading(false);
}
};
// 测试连接
const testConnect = async () => {
// 验证参数
const error = await formRef.value.validate();
if (error) {
return;
}
try {
setConnectLoading(true);
// 测试连接
await testHostConnect({
id: props.hostId,
type: HostType.SSH.value,
});
Message.success('连接成功');
} catch (e) {
} finally {
setConnectLoading(false);
}
};
// 保存配置
const saveConfig = async () => {
// 验证参数
const error = await formRef.value.validate();
if (error) {
return;
}
// 加密参数
const requestData = { ...formModel.value };
try {
requestData.password = await encrypt(formRef.value.password);
} catch (e) {
return;
}
try {
setLoading(true);
// 更新
await updateHostConfig({
hostId: props.hostId,
type: HostType.SSH.value,
config: JSON.stringify(requestData),
});
Message.success('修改成功');
} catch (e) {
} finally {
setLoading(false);
}
};
onMounted(fetchHostConfig);
</script>
<style lang="less" scoped>
.auth-type-group {
width: 100%;
display: flex;
justify-content: space-between;
}
.password-switch {
width: 148px;
margin-left: 8px;
}
</style>

View File

@@ -25,11 +25,11 @@
<a-form-item field="address" label="主机地址">
<a-input v-model="formModel.address" placeholder="请输入主机地址" allow-clear />
</a-form-item>
<!-- 主机类型 -->
<a-form-item field="type" label="主机类型">
<!-- 主机协议 -->
<a-form-item field="type" label="主机协议">
<a-select v-model="formModel.type"
:options="toOptions(hostTypeKey)"
placeholder="请选择主机类型"
placeholder="请选择主机协议"
allow-clear />
</a-form-item>
<!-- 系统类型 -->
@@ -39,6 +39,13 @@
placeholder="请选择系统类型"
allow-clear />
</a-form-item>
<!-- 系统架构 -->
<a-form-item field="archType" label="系统架构">
<a-select v-model="formModel.archType"
:options="toOptions(hostArchTypeKey)"
placeholder="请选择系统架构"
allow-clear />
</a-form-item>
<!-- 主机状态 -->
<a-form-item field="status" label="主机状态">
<a-select v-model="formModel.status"
@@ -147,33 +154,71 @@
:bordered="false"
@page-change="(page: number) => fetchTableData(page, pagination.pageSize)"
@page-size-change="(size: number) => fetchTableData(1, size)">
<!-- 主机类型 -->
<template #type="{ record }">
<a-tag class="flex-center" :color="getDictValue(hostTypeKey, record.type, 'color')">
<!-- 系统类型图标 -->
<component v-if="getHostOsIcon(record.osType)"
:is="getHostOsIcon(record.osType)"
class="os-icon" />
<!-- 主机类型 -->
<span>{{ getDictValue(hostTypeKey, record.type) }}</span>
</a-tag>
<!-- 主机信息 -->
<template #hostInfo="{ record }">
<div class="info-wrapper">
<div class="info-item">
<span class="info-label">主机名称</span>
<span class="info-value text-copy text-ellipsis"
:title="record.name"
@click="copy(record.name, true)">
{{ record.name }}
</span>
</div>
<div class="info-item">
<span class="info-label">主机编码</span>
<span class="info-value text-copy text-ellipsis"
:title="record.code"
@click="copy(record.code, true)">
{{ record.code }}
</span>
</div>
<div class="info-item">
<span class="info-label">主机地址</span>
<span class="info-value span-blue text-copy text-ellipsis"
:title="record.address"
@click="copy(record.address, true)">
{{ record.address }}
</span>
</div>
</div>
</template>
<!-- 主机编码 -->
<template #code="{ record }">
<a-tag>{{ record.code }}</a-tag>
<!-- 主机规格 -->
<template #hostSpec="{ record }">
<div class="info-wrapper">
<div class="info-item">
<span class="spec-label">规格</span>
<span v-if="record.spec" class="spec-value text-ellipsis">
{{
[
addSuffix(record.spec.cpuCore, 'C'),
addSuffix(record.spec.memorySize, 'G'),
addSuffix(record.spec.diskSize, 'G')
].filter(Boolean).join(' / ') || '-'
}}
</span>
<span v-else class="spec-value text-ellipsis">-</span>
</div>
<!-- 系统类型 -->
<div class="info-item">
<span class="spec-label">系统</span>
<span class="spec-value text-ellipsis">
{{ getDictValue(hostOsTypeKey, record.osType) }} - {{ getDictValue(hostArchTypeKey, record.archType) }}
</span>
</div>
</div>
</template>
<!-- 地址 -->
<template #address="{ record }">
<span class="span-blue text-copy host-address"
title="复制"
@click="copy(record.address)">
{{ record.address }}
</span>
<span class="span-blue text-copy"
title="复制"
@click="copy(record.port)">
{{ record.port }}
</span>
<!-- 主机协议 -->
<template #protocols="{ record }">
<a-space v-if="record.types?.length"
style="margin-bottom: -8px;"
wrap>
<a-tag v-for="type in record.types"
:key="type"
:color="getDictValue(hostTypeKey, type, 'color')">
{{ getDictValue(hostTypeKey, type) }}
</a-tag>
</a-space>
</template>
<!-- 主机状态 -->
<template #status="{ record }">
@@ -181,16 +226,30 @@
{{ getDictValue(hostStatusKey, record.status) }}
</a-tag>
</template>
<!-- 标签 -->
<template #tags="{ record }">
<a-space v-if="record.tags"
<!-- 主机分组 -->
<template #groups="{ record }">
<a-space v-if="record.groupIdList?.length"
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>
<template v-for="groupId in record.groupIdList"
:key="groupId">
<a-tag color="green">
{{ hostGroupList.find(s => s.key === groupId)?.title || groupId }}
</a-tag>
</template>
</a-space>
</template>
<!-- 主机标签 -->
<template #tags="{ record }">
<a-space v-if="record.tags?.length"
style="margin-bottom: -8px;"
:wrap="true">
<template v-for="tag in record.tags"
:key="tag.id">
<a-tag :color="dataColor(tag.name, tagColor)">
{{ tag.name }}
</a-tag>
</template>
</a-space>
</template>
<!-- 操作 -->
@@ -203,13 +262,6 @@
@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"
@@ -242,13 +294,13 @@
<span class="more-doption normal">复制</span>
</a-doption>
<!-- SSH -->
<a-doption v-if="record.type === HostType.SSH.value"
<a-doption v-if="record.types.includes(HostType.SSH.value)"
v-permission="['asset:terminal:access']"
@click="openNewRoute({ name: 'terminal', query: { connect: record.id, type: 'SSH' } })">
<span class="more-doption normal">SSH</span>
</a-doption>
<!-- SFTP -->
<a-doption v-if="record.type === HostType.SSH.value"
<a-doption v-if="record.types.includes(HostType.SSH.value)"
v-permission="['asset:terminal:access']"
@click="openNewRoute({ name: 'terminal', query: { connect: record.id, type: 'SFTP' } })">
<span class="more-doption normal">SFTP</span>
@@ -269,24 +321,25 @@
<script lang="ts" setup>
import type { HostQueryRequest, HostQueryResponse } from '@/api/asset/host';
import type { HostGroupQueryResponse } from '@/api/asset/host-group';
import { reactive, ref, onMounted } from 'vue';
import { deleteHost, batchDeleteHost, getHostPage, updateHostStatus } from '@/api/asset/host';
import { Message, Modal } from '@arco-design/web-vue';
import { TableName, tagColor, hostTypeKey, hostStatusKey, HostType, hostOsTypeKey, getHostOsIcon } from '../types/const';
import { tagColor, hostTypeKey, hostStatusKey, HostType, hostOsTypeKey, hostArchTypeKey, TableName } from '../types/const';
import { useTablePagination, useRowSelection, useTableColumns } from '@/hooks/table';
import { useQueryOrder, ASC } from '@/hooks/query-order';
import { useCacheStore, useDictStore } from '@/store';
import { copy } from '@/hooks/copy';
import { dataColor } from '@/utils';
import { addSuffix, dataColor } from '@/utils';
import { useRouter } from 'vue-router';
import useLoading from '@/hooks/loading';
import columns from '../types/table.columns';
import { GrantKey, GrantRouteName } from '@/views/asset/grant/types/const';
import { ASC, useQueryOrder } from '@/hooks/query-order';
import { openNewRoute } from '@/router';
import TagMultiSelector from '@/components/meta/tag/multi-selector/index.vue';
import TableAdjust from '@/components/app/table-adjust/index.vue';
import TagMultiSelector from '@/components/meta/tag/multi-selector/index.vue';
const emits = defineEmits(['openCopy', 'openAdd', 'openUpdate', 'openUpdateConfig', 'openHostGroup']);
const emits = defineEmits(['openCopy', 'openAdd', 'openUpdate', 'openHostGroup']);
const router = useRouter();
const cacheStore = useCacheStore();
@@ -297,6 +350,7 @@
const { loading, setLoading } = useLoading();
const { toOptions, getDictValue, toggleDictValue, toggleDict } = useDictStore();
const hostGroupList = ref<Array<HostGroupQueryResponse>>([]);
const tagSelector = ref();
const selectedKeys = ref<Array<number>>([]);
const tableRenderData = ref<Array<HostQueryResponse>>([]);
@@ -307,10 +361,13 @@
address: undefined,
type: undefined,
osType: undefined,
archType: undefined,
status: undefined,
tags: undefined,
description: undefined,
queryGroup: true,
queryTag: true,
querySpec: true,
});
// 更新状态
@@ -403,7 +460,10 @@
doFetchTableData({ page, limit, ...form });
};
onMounted(() => {
onMounted(async () => {
// 加载分组数据
hostGroupList.value = await useCacheStore().loadHostGroupList();
// 加载数据
fetchTableData();
});
@@ -411,10 +471,42 @@
<style lang="less" scoped>
.os-icon {
width: 16px;
height: 16px;
margin-right: 6px;
.info-wrapper {
padding: 4px 0;
.info-item {
display: flex;
&:not(:last-child) {
margin-bottom: 4px;
}
.info-label, .spec-label {
margin-right: 8px;
user-select: none;
font-weight: 600;
&::after {
content: ':';
}
}
.info-label {
width: 60px;
}
.spec-label {
width: 34px;
}
.info-value {
width: calc(100% - 68px);
}
.spec-value {
width: calc(100% - 42px);
}
}
}
.row-handle-wrapper {
@@ -422,9 +514,4 @@
align-items: center;
}
.host-address::after {
content: ':';
user-select: text;
}
</style>

View File

@@ -4,24 +4,18 @@
<host-table v-if="renderTable"
ref="table"
@open-host-group="() => hostGroup.open()"
@open-copy="(e) => modal.openCopy(e.id)"
@open-add="() => modal.openAdd()"
@open-update="(e) => modal.openUpdate(e.id)"
@open-update-config="(e) => hostConfig.open(e)" />
@open-copy="(e) => drawer.openCopy(e.id)"
@open-add="() => drawer.openAdd()"
@open-update="(e) => drawer.openUpdate(e.id)" />
<!-- 列表-卡片 -->
<host-card-list v-else
ref="card"
@open-host-group="() => hostGroup.open()"
@open-copy="(e) => modal.openCopy(e.id)"
@open-add="() => modal.openAdd()"
@open-update="(e) => modal.openUpdate(e.id)"
@open-update-config="(e) => hostConfig.open(e)" />
<!-- 添加修改模态框 -->
<host-form-modal ref="modal"
@added="reload"
@updated="reload" />
<!-- 配置面板 -->
<host-config-drawer ref="hostConfig" />
@open-copy="(e) => drawer.openCopy(e.id)"
@open-add="() => drawer.openAdd()"
@open-update="(e) => drawer.openUpdate(e.id)" />
<!-- 添加修改抽屉 -->
<host-form-drawer ref="drawer" @reload="reload" />
<!-- 分组配置 -->
<host-group-drawer ref="hostGroup" />
</div>
@@ -39,14 +33,12 @@
import { dictKeys } from './types/const';
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 '../host-config/drawer/index.vue';
import HostFormDrawer from './components/host-form-drawer.vue';
import HostGroupDrawer from '../host-group/drawer/index.vue';
const table = ref();
const card = ref();
const modal = ref();
const hostConfig = ref();
const drawer = ref();
const hostGroup = ref();
const appStore = useAppStore();
@@ -63,9 +55,8 @@
};
// 加载字典配置
onBeforeMount(async () => {
const dictStore = useDictStore();
await dictStore.loadKeys(dictKeys);
onBeforeMount(() => {
useDictStore().loadKeys(dictKeys);
});
</script>

View File

@@ -7,7 +7,7 @@ const fieldConfig = {
minHeight: '22px',
fields: [
{
label: 'id',
label: '主机ID',
dataIndex: 'id',
slotName: 'id',
default: true,
@@ -23,20 +23,30 @@ const fieldConfig = {
tooltip: true,
default: true,
}, {
label: '主机类型',
dataIndex: 'type',
slotName: 'type',
label: '主机规格',
dataIndex: 'hostSpec',
slotName: 'hostSpec',
tooltip: true,
default: true,
}, {
label: '主机状态',
dataIndex: 'status',
slotName: 'status',
default: true,
}, {
label: '主机协议',
dataIndex: 'protocols',
slotName: 'protocols',
default: true,
}, {
label: '主机分组',
dataIndex: 'groups',
slotName: 'groups',
default: true,
}, {
label: '主机标签',
dataIndex: 'tags',
slotName: 'tags',
rowAlign: 'start',
default: true,
}, {
label: '主机描述',

View File

@@ -1,5 +1,6 @@
import WindowsIcon from '@/assets/images/icon/os_windows.svg';
import LinuxIcon from '@/assets/images/icon/os_linux.svg';
import DarwinIcon from '@/assets/images/icon/os_darwin.svg';
// 表名称
export const TableName = 'host';
@@ -31,6 +32,20 @@ export const HostOsType = {
value: 'WINDOWS',
icon: WindowsIcon,
},
DARWIN: {
value: 'DARWIN',
icon: DarwinIcon,
},
};
// 主机认证方式
export const SshAuthType = {
// 密码认证
PASSWORD: 'PASSWORD',
// 密钥认证
KEY: 'KEY',
// 身份认证
IDENTITY: 'IDENTITY'
};
// 获取系统类型 icon
@@ -44,6 +59,9 @@ export const hostTypeKey = 'hostType';
// 主机系统类型 字典项
export const hostOsTypeKey = 'hostOsType';
// 主机系统架构 字典项
export const hostArchTypeKey = 'hostArchType';
// 主机状态 字典项
export const hostStatusKey = 'hostStatus';
@@ -51,4 +69,4 @@ export const hostStatusKey = 'hostStatus';
export const sshAuthTypeKey = 'hostSshAuthType';
// 加载的字典值
export const dictKeys = [hostTypeKey, hostOsTypeKey, hostStatusKey, sshAuthTypeKey];
export const dictKeys = [hostTypeKey, hostOsTypeKey, hostArchTypeKey, hostStatusKey, sshAuthTypeKey];

View File

@@ -1,72 +1,115 @@
import type { FieldRule } from '@arco-design/web-vue';
export const type = [{
required: true,
message: '请选择主机类型'
}] as FieldRule[];
export const osType = [{
required: true,
message: '请选择系统类型'
}] as FieldRule[];
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 port = [{
required: true,
message: '请输入主机端口'
}, {
type: 'number',
min: 1,
max: 65535,
message: '输入的端口不合法'
}] as FieldRule[];
export const groupIdList = [{
required: true,
message: '请选择主机分组'
}] as FieldRule[];
export const tags = [{
maxLength: 5,
message: '最多选择5个标签'
}] as FieldRule[];
export const description = [{
maxLength: 255,
message: '主机描述长度不能大于255位'
}] as FieldRule[];
export default {
type,
osType,
name,
code,
address,
port,
groupIdList,
tags,
description,
// 主机表单规则
export const hostFormRules = {
types: [{
required: true,
message: '请选择主机协议'
}],
osType: [{
required: true,
message: '请选择系统类型'
}],
archType: [{
required: true,
message: '请选择系统架构'
}],
name: [{
required: true,
message: '请输入主机名称'
}, {
maxLength: 64,
message: '主机名称长度不能大于64位'
}],
code: [{
required: true,
message: '请输入主机编码'
}, {
maxLength: 64,
message: '主机编码长度不能大于64位'
}],
address: [{
required: true,
message: '请输入主机地址'
}, {
maxLength: 128,
message: '主机地址长度不能大于128位'
}],
port: [{
required: true,
message: '请输入主机端口'
}, {
type: 'number',
min: 1,
max: 65535,
message: '输入的端口不合法'
}],
groupIdList: [{
required: true,
message: '请选择主机分组'
}],
tags: [{
maxLength: 5,
message: '最多选择5个标签'
}],
description: [{
maxLength: 255,
message: '主机描述长度不能大于255位'
}],
} as Record<string, FieldRule | FieldRule[]>;
// ssh 表单规则
export const sshFormRules = {
port: [{
required: true,
message: '请输入 SSH 端口'
}, {
min: 1,
max: 65535,
message: 'SSH 端口不合法'
}],
authType: [{
required: true,
message: '请选择认证方式'
}],
keyId: [{
required: true,
message: '请选择主机密钥'
}],
identityId: [{
required: true,
message: '请选择主机身份'
}],
connectTimeout: [{
required: true,
message: '请输入连接超时时间'
}, {
type: 'number',
min: 0,
max: 100000,
message: '连接超时时间需要在 0 - 100000 之间'
}],
charset: [{
required: true,
message: '请输入SSH输出编码'
}, {
maxLength: 12,
message: 'SSH输出编码长度不能超过12位'
}],
fileNameCharset: [{
required: true,
message: '请输入文件名称编码'
}, {
maxLength: 12,
message: '文件名称编码长度不能超过12位'
}],
fileContentCharset: [{
required: true,
message: '请输入SSH输出编码'
}, {
maxLength: 12,
message: '文件内容编码长度不能超过12位'
}],
} as Record<string, FieldRule | FieldRule[]>;
export default null;

View File

@@ -11,38 +11,26 @@ const columns = [
fixed: 'left',
default: true,
}, {
title: '主机名称',
dataIndex: 'name',
slotName: 'name',
ellipsis: true,
tooltip: true,
minWidth: 238,
default: true,
}, {
title: '主机编码',
dataIndex: 'code',
slotName: 'code',
minWidth: 120,
default: true,
}, {
title: '主机类型',
dataIndex: 'type',
slotName: 'type',
align: 'center',
width: 100,
default: true,
}, {
title: '主机地址',
dataIndex: 'address',
slotName: 'address',
minWidth: 238,
default: true,
}, {
title: '主机标签',
dataIndex: 'tags',
slotName: 'tags',
title: '主机信息',
dataIndex: 'hostInfo',
slotName: 'hostInfo',
minWidth: 328,
align: 'left',
minWidth: 148,
fixed: 'left',
default: true,
}, {
title: '主机规格',
dataIndex: 'hostSpec',
slotName: 'hostSpec',
width: 188,
align: 'left',
default: true,
}, {
title: '主机协议',
dataIndex: 'protocols',
slotName: 'protocols',
align: 'left',
width: 184,
default: true,
}, {
title: '主机状态',
@@ -50,7 +38,20 @@ const columns = [
slotName: 'status',
width: 88,
align: 'center',
fixed: 'right',
default: true,
}, {
title: '主机分组',
dataIndex: 'groups',
slotName: 'groups',
align: 'left',
minWidth: 148,
default: true,
}, {
title: '主机标签',
dataIndex: 'tags',
slotName: 'tags',
align: 'left',
minWidth: 148,
default: true,
}, {
title: '主机描述',
@@ -99,7 +100,7 @@ const columns = [
}, {
title: '操作',
slotName: 'handle',
width: 192,
width: 162,
align: 'center',
fixed: 'right',
default: true,

View File

@@ -35,7 +35,7 @@
</script>
<script lang="ts" setup>
import type { LabelExtraSettingModel } from '../../../types/define';
import type { HostLabelExtraSettingModel } from '@/api/asset/host-extra';
import { onMounted, ref } from 'vue';
import { tabColorKey } from '../../../types/const';
import { getHostExtraItem } from '@/api/asset/host-extra';
@@ -48,13 +48,13 @@
const { toOptions } = useDictStore();
const formModel = ref<LabelExtraSettingModel>({
const formModel = ref<Partial<HostLabelExtraSettingModel>>({
color: '',
});
// 渲染表单
const renderForm = async () => {
const { data } = await getHostExtraItem<LabelExtraSettingModel>({ hostId: props.hostId, item: props.item });
const { data } = await getHostExtraItem<HostLabelExtraSettingModel>({ hostId: props.hostId, item: props.item });
formModel.value = data;
};

View File

@@ -42,7 +42,7 @@
</script>
<script lang="ts" setup>
import type { SshExtraSettingModel } from '../../../types/define';
import type { HostSshExtraSettingModel } from '@/api/asset/host-extra';
import { onMounted, ref } from 'vue';
import { getHostExtraItem } from '@/api/asset/host-extra';
import { ExtraSshAuthType, extraSshAuthTypeKey } from '../../../types/const';
@@ -58,13 +58,13 @@
const { toRadioOptions } = useDictStore();
const formRef = ref();
const formModel = ref<SshExtraSettingModel>({
authType: ExtraSshAuthType.DEFAULT
const formModel = ref<Partial<HostSshExtraSettingModel>>({
authType: ExtraSshAuthType.DEFAULT,
});
// 渲染表单
const renderForm = async () => {
const { data } = await getHostExtraItem<SshExtraSettingModel>({ hostId: props.hostId, item: props.item });
const { data } = await getHostExtraItem<HostSshExtraSettingModel>({ hostId: props.hostId, item: props.item });
formModel.value = data;
};

View File

@@ -57,20 +57,6 @@ export interface ShortcutKeyItem {
type: number;
}
// SSH 额外配置
export interface SshExtraSettingModel {
authType?: string;
username?: string;
keyId?: number;
identityId?: number;
}
// 标签额外配置
export interface LabelExtraSettingModel {
alias?: string;
color?: string;
}
// session tab
export interface PanelSessionTabType {
type: string;