🔨 优化主机逻辑.
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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];
|
||||
@@ -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[]>;
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: '主机描述',
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user