🔨 重构主机模块.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import type { HostGroupQueryResponse } from '@/api/asset/host-group';
|
||||
import type { HostQueryResponse } from './host';
|
||||
import type { HostQueryResponse, HostType } from './host';
|
||||
import type { HostKeyQueryResponse } from './host-key';
|
||||
import type { HostIdentityQueryResponse } from './host-identity';
|
||||
import axios from 'axios';
|
||||
@@ -17,7 +17,7 @@ export interface AuthorizedHostQueryResponse {
|
||||
/**
|
||||
* 查询当前用户已授权的主机
|
||||
*/
|
||||
export function getCurrentAuthorizedHost(type: string) {
|
||||
export function getCurrentAuthorizedHost(type: HostType) {
|
||||
return axios.get<AuthorizedHostQueryResponse>('/asset/authorized-data/current-host', { params: { type } });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import axios from 'axios';
|
||||
|
||||
/**
|
||||
* 主机配置请求
|
||||
*/
|
||||
export interface HostConfigRequest {
|
||||
hostId?: number;
|
||||
type?: string;
|
||||
version?: number;
|
||||
status?: number;
|
||||
config?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主机配置查询响应
|
||||
*/
|
||||
export interface HostConfigQueryResponse {
|
||||
id: number;
|
||||
hostId: number;
|
||||
type: string;
|
||||
version: number;
|
||||
status: number;
|
||||
config: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询主机配置
|
||||
*/
|
||||
export function getHostConfig(params: HostConfigRequest) {
|
||||
return axios.get<HostConfigQueryResponse>('/asset/host-config/get', { params });
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询全部主机配置
|
||||
*/
|
||||
export function getHostConfigList(hostId: number) {
|
||||
return axios.get<Array<HostConfigQueryResponse>>('/asset/host-config/list', { params: { hostId } });
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新主机配置
|
||||
*/
|
||||
export function updateHostConfig(request: HostConfigRequest) {
|
||||
return axios.put('/asset/host-config/update', request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新主机配置状态
|
||||
*/
|
||||
export function updateHostConfigStatus(request: HostConfigRequest) {
|
||||
return axios.put('/asset/host-config/update-status', request);
|
||||
}
|
||||
@@ -3,13 +3,18 @@ import type { TableData } from '@arco-design/web-vue/es/table/interface';
|
||||
import axios from 'axios';
|
||||
import qs from 'query-string';
|
||||
|
||||
// 主机类型
|
||||
export type HostType = 'SSH' | string | undefined;
|
||||
|
||||
/**
|
||||
* 主机创建请求
|
||||
*/
|
||||
export interface HostCreateRequest {
|
||||
type?: string;
|
||||
name?: string;
|
||||
code?: string;
|
||||
address?: string;
|
||||
port?: number;
|
||||
tags?: Array<number>;
|
||||
groupIdList?: Array<number>;
|
||||
}
|
||||
@@ -21,15 +26,33 @@ export interface HostUpdateRequest extends HostCreateRequest {
|
||||
id?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主机更新状态请求
|
||||
*/
|
||||
export interface HostUpdateStatusRequest {
|
||||
id: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主机更新配置请求
|
||||
*/
|
||||
export interface HostUpdateConfigRequest {
|
||||
id: number;
|
||||
config: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主机查询请求
|
||||
*/
|
||||
export interface HostQueryRequest extends Pagination {
|
||||
searchValue?: string;
|
||||
id?: number;
|
||||
type?: string;
|
||||
name?: string;
|
||||
code?: string;
|
||||
address?: string;
|
||||
status?: string;
|
||||
tags?: Array<number>;
|
||||
queryTag?: boolean;
|
||||
}
|
||||
@@ -39,9 +62,12 @@ export interface HostQueryRequest extends Pagination {
|
||||
*/
|
||||
export interface HostQueryResponse extends TableData, HostQueryResponseExtra {
|
||||
id: number;
|
||||
type: string;
|
||||
name: string;
|
||||
code: string;
|
||||
address: string;
|
||||
port: string;
|
||||
status: string;
|
||||
createTime: number;
|
||||
updateTime: number;
|
||||
creator: string;
|
||||
@@ -62,6 +88,22 @@ export interface HostQueryResponseExtra {
|
||||
modCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主机 配置查询响应
|
||||
*/
|
||||
export interface HostConfigQueryResponse extends HostConfigQueryResponseExtra {
|
||||
id: number;
|
||||
type: string;
|
||||
config: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主机配置拓展
|
||||
*/
|
||||
export interface HostConfigQueryResponseExtra {
|
||||
current: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建主机
|
||||
*/
|
||||
@@ -77,7 +119,21 @@ export function updateHost(request: HostUpdateRequest) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 id 查询主机
|
||||
* 通过 id 更新主机状态
|
||||
*/
|
||||
export function updateHostStatus(request: HostUpdateStatusRequest) {
|
||||
return axios.put('/asset/host/update-status', request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 id 更新主机配置
|
||||
*/
|
||||
export function updateHostConfig(request: HostUpdateConfigRequest) {
|
||||
return axios.put('/asset/host/update-config', request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询主机
|
||||
*/
|
||||
export function getHost(id: number) {
|
||||
return axios.get<HostQueryResponse>('/asset/host/get', { params: { id } });
|
||||
@@ -86,8 +142,15 @@ export function getHost(id: number) {
|
||||
/**
|
||||
* 查询全部主机
|
||||
*/
|
||||
export function getHostList() {
|
||||
return axios.get<Array<HostQueryResponse>>('/asset/host/list');
|
||||
export function getHostList(type: HostType) {
|
||||
return axios.get<Array<HostQueryResponse>>('/asset/host/list', { params: { type } });
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 id 查询主机配置
|
||||
*/
|
||||
export function getHostConfig(id: number) {
|
||||
return axios.get<HostConfigQueryResponse>('/asset/host/get-config', { params: { id } });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,12 +30,20 @@
|
||||
border-radius: 50%;
|
||||
background-color: rgb(var(--blue-6));
|
||||
|
||||
&.normal {
|
||||
color: rgb(var(--arcoblue-6));
|
||||
}
|
||||
|
||||
&.pass {
|
||||
background-color: rgb(var(--green-6));
|
||||
color: rgb(var(--green-6));
|
||||
}
|
||||
|
||||
&.warn {
|
||||
color: rgb(var(--orange-6));
|
||||
}
|
||||
|
||||
&.error {
|
||||
background-color: rgb(var(--red-6));
|
||||
color: rgb(var(--red-6));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
}
|
||||
|
||||
// -- drawer
|
||||
.drawer-form-small{
|
||||
.drawer-form-small {
|
||||
padding: 20px 20px 2px 20px;
|
||||
}
|
||||
|
||||
@@ -185,3 +185,31 @@
|
||||
background: var(--color-bg-2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
// -- doption
|
||||
.more-doption {
|
||||
min-width: 42px;
|
||||
padding: 0 4px;
|
||||
font-size: 12px;
|
||||
display: inline-block;
|
||||
|
||||
svg {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
&.normal {
|
||||
color: rgb(var(--arcoblue-6));
|
||||
}
|
||||
|
||||
&.pass {
|
||||
color: rgb(var(--green-6));
|
||||
}
|
||||
|
||||
&.warn {
|
||||
color: rgb(var(--orange-6));
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: rgb(var(--red-6));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<host-table class="host-list"
|
||||
v-model:selected-keys="selectedKeysValue"
|
||||
:host-list="hostList"
|
||||
empty-message="当前分组内无授权主机/主机未启用 SSH 配置!" />
|
||||
empty-message="当前分组内无授权主机!" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
import type { SelectOptionData } from '@arco-design/web-vue';
|
||||
import type { AuthorizedHostQueryResponse } from '@/api/asset/asset-authorized-data';
|
||||
import type { HostQueryResponse } from '@/api/asset/host';
|
||||
import type { HostType } from '@/api/asset/host';
|
||||
import { onMounted, ref, watch, computed } from 'vue';
|
||||
import { dataColor } from '@/utils';
|
||||
import { dictKeys, NewConnectionType, newConnectionTypeKey } from './types/const';
|
||||
@@ -92,6 +93,12 @@
|
||||
import HostTable from './components/host-table.vue';
|
||||
import HostGroup from './components/host-group.vue';
|
||||
|
||||
const props = withDefaults(defineProps<Partial<{
|
||||
type: HostType;
|
||||
}>>(), {
|
||||
type: undefined,
|
||||
});
|
||||
|
||||
const emits = defineEmits(['selected']);
|
||||
|
||||
const { toRadioOptions, loadKeys } = useDictStore();
|
||||
@@ -110,10 +117,10 @@
|
||||
const emptyMessage = computed(() => {
|
||||
if (newConnectionType.value === NewConnectionType.LIST) {
|
||||
// 列表
|
||||
return '无授权主机/主机未启用 SSH 配置!';
|
||||
return '无授权主机!';
|
||||
} else if (newConnectionType.value === NewConnectionType.FAVORITE) {
|
||||
// 收藏
|
||||
return '无收藏主机/主机未启用 SSH 配置!';
|
||||
return '无收藏主机!';
|
||||
} else if (newConnectionType.value === NewConnectionType.LATEST) {
|
||||
// 最近连接
|
||||
return '暂无连接记录!';
|
||||
@@ -144,7 +151,7 @@
|
||||
setLoading(true);
|
||||
try {
|
||||
// 加载主机列表
|
||||
const { data } = await getCurrentAuthorizedHost('ssh');
|
||||
const { data } = await getCurrentAuthorizedHost(props.type);
|
||||
hosts.value = data;
|
||||
// 禁用别名
|
||||
data.hostList.forEach(s => s.alias = undefined as unknown as string);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<a-select v-model:model-value="value"
|
||||
:options="optionData"
|
||||
:loading="loading"
|
||||
:multiple="multiple"
|
||||
placeholder="请选择主机"
|
||||
allow-clear />
|
||||
</template>
|
||||
@@ -14,22 +15,30 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { SelectOptionData } from '@arco-design/web-vue';
|
||||
import type { HostType } from '@/api/asset/host';
|
||||
import { computed, onBeforeMount, ref } from 'vue';
|
||||
import { useCacheStore } from '@/store';
|
||||
import useLoading from '@/hooks/loading';
|
||||
|
||||
const props = defineProps<Partial<{
|
||||
modelValue: number;
|
||||
}>>();
|
||||
const props = withDefaults(defineProps<Partial<{
|
||||
type: HostType;
|
||||
status: string | undefined;
|
||||
modelValue: number | Array<number>;
|
||||
multiple: boolean;
|
||||
}>>(), {
|
||||
type: undefined,
|
||||
status: undefined,
|
||||
multiple: false,
|
||||
});
|
||||
|
||||
const emits = defineEmits(['update:modelValue']);
|
||||
|
||||
const { loading, setLoading } = useLoading();
|
||||
const cacheStore = useCacheStore();
|
||||
|
||||
const value = computed<number>({
|
||||
const value = computed({
|
||||
get() {
|
||||
return props.modelValue as number;
|
||||
return props.modelValue;
|
||||
},
|
||||
set(e) {
|
||||
if (e) {
|
||||
@@ -45,8 +54,9 @@
|
||||
onBeforeMount(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const hosts = await cacheStore.loadHosts();
|
||||
optionData.value = hosts.map(s => {
|
||||
const hosts = await cacheStore.loadHosts(props.type);
|
||||
optionData.value = hosts.filter(s => !props.status || s.status === props.status)
|
||||
.map(s => {
|
||||
return {
|
||||
label: `${s.name} - ${s.address}`,
|
||||
value: s.id,
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import type { IDisposable, ITerminalInitOnlyOptions, ITerminalOptions, Terminal } from '@xterm/xterm';
|
||||
import type { FitAddon } from '@xterm/addon-fit';
|
||||
import type { SearchAddon } from '@xterm/addon-search';
|
||||
import type { WebLinksAddon } from '@xterm/addon-web-links';
|
||||
import type { WebglAddon } from '@xterm/addon-webgl';
|
||||
import type { Unicode11Addon } from '@xterm/addon-unicode11';
|
||||
import type { XtermAddons } from '@/types/xterm';
|
||||
import { defaultFontFamily, defaultTheme } from '@/types/xterm';
|
||||
import type { ITerminalInitOnlyOptions, ITerminalOptions, Terminal } from '@xterm/xterm';
|
||||
|
||||
// 执行类型
|
||||
export type ExecType = 'BATCH' | 'JOB';
|
||||
@@ -47,11 +44,7 @@ export const dictKeys = [execStatusKey, execHostStatusKey];
|
||||
|
||||
// appender 配置
|
||||
export const LogAppenderOptions: ITerminalOptions & ITerminalInitOnlyOptions = {
|
||||
theme: {
|
||||
foreground: '#FFFFFF',
|
||||
background: '#1C1C1C',
|
||||
selectionBackground: '#444444',
|
||||
},
|
||||
theme: defaultTheme,
|
||||
cols: 30,
|
||||
rows: 8,
|
||||
rightClickSelectsWord: true,
|
||||
@@ -63,7 +56,7 @@ export const LogAppenderOptions: ITerminalOptions & ITerminalInitOnlyOptions = {
|
||||
lineHeight: 1.12,
|
||||
convertEol: true,
|
||||
allowProposedApi: true,
|
||||
fontFamily: 'Courier New, Monaco, courier, monospace',
|
||||
fontFamily: defaultFontFamily,
|
||||
};
|
||||
|
||||
// dom 引用
|
||||
@@ -79,16 +72,7 @@ export interface LogAppenderConf {
|
||||
el: HTMLElement;
|
||||
openSearch: () => {};
|
||||
terminal: Terminal;
|
||||
addons: LogAddons;
|
||||
}
|
||||
|
||||
// appender 插件
|
||||
export interface LogAddons extends Record<string, IDisposable> {
|
||||
fit: FitAddon;
|
||||
webgl: WebglAddon;
|
||||
search: SearchAddon;
|
||||
weblink: WebLinksAddon;
|
||||
unicode: Unicode11Addon;
|
||||
addons: XtermAddons;
|
||||
}
|
||||
|
||||
// 执行日志 appender 定义
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ExecType, ILogAppender, LogAddons, LogAppenderConf, LogDomRef } from '../const';
|
||||
import type { ExecType, ILogAppender, LogAppenderConf, LogDomRef } from '../const';
|
||||
import { LogAppenderOptions } from '../const';
|
||||
import type { XtermAddons } from '@/types/xterm';
|
||||
import type { ExecLogTailRequest } from '@/api/exec/exec-log';
|
||||
import { openExecLogChannel } from '@/api/exec/exec-log';
|
||||
import { getExecCommandLogTailToken } from '@/api/exec/exec-command-log';
|
||||
@@ -43,13 +44,13 @@ export default class LogAppender implements ILogAppender {
|
||||
// 初始化
|
||||
async init(logDomRefs: Array<LogDomRef>) {
|
||||
// 初始化 appender
|
||||
this.initAppender(logDomRefs);
|
||||
await this.initAppender(logDomRefs);
|
||||
// 初始化 client
|
||||
await this.openClient();
|
||||
}
|
||||
|
||||
// 初始化 appender
|
||||
initAppender(logDomRefs: Array<LogDomRef>) {
|
||||
async initAppender(logDomRefs: Array<LogDomRef>) {
|
||||
// 打开 log-view
|
||||
for (let logDomRef of logDomRefs) {
|
||||
// 初始化 terminal
|
||||
@@ -126,7 +127,7 @@ export default class LogAppender implements ILogAppender {
|
||||
}
|
||||
|
||||
// 初始化插件
|
||||
initAddons(terminal: Terminal): LogAddons {
|
||||
initAddons(terminal: Terminal): XtermAddons {
|
||||
const fit = new FitAddon();
|
||||
const search = new SearchAddon();
|
||||
const webgl = new WebglAddon();
|
||||
@@ -144,7 +145,7 @@ export default class LogAppender implements ILogAppender {
|
||||
webgl,
|
||||
weblink,
|
||||
unicode
|
||||
};
|
||||
} as XtermAddons;
|
||||
}
|
||||
|
||||
// 初始化 client
|
||||
|
||||
@@ -89,8 +89,8 @@
|
||||
import useEmitter from '@/hooks/emitter';
|
||||
|
||||
const props = defineProps<CardProps & {
|
||||
index: number,
|
||||
item: CardRecord,
|
||||
index: number;
|
||||
item: CardRecord;
|
||||
}>();
|
||||
const emits = defineEmits(['emitter']);
|
||||
|
||||
|
||||
@@ -40,15 +40,15 @@ export const builtinParams: Array<TemplateParam> = [
|
||||
}, {
|
||||
name: 'hostAddress',
|
||||
desc: '执行主机地址'
|
||||
}, {
|
||||
name: 'hostPort',
|
||||
desc: '主机端口'
|
||||
}, {
|
||||
name: 'hostUsername',
|
||||
desc: '执行主机用户名'
|
||||
}, {
|
||||
name: 'osType',
|
||||
desc: '执行主机系统版本'
|
||||
}, {
|
||||
name: 'port',
|
||||
desc: 'SSH 端口'
|
||||
}, {
|
||||
name: 'charset',
|
||||
desc: 'SSH 编码集'
|
||||
|
||||
@@ -2,6 +2,8 @@ import type { CacheState } from './types';
|
||||
import type { AxiosResponse } from 'axios';
|
||||
import type { TagType } from '@/api/meta/tag';
|
||||
import { getTagList } from '@/api/meta/tag';
|
||||
import type { HostType } from '@/api/asset/host';
|
||||
import { getHostList } from '@/api/asset/host';
|
||||
import type { PreferenceType } from '@/api/user/preference';
|
||||
import { getPreference } from '@/api/user/preference';
|
||||
import { defineStore } from 'pinia';
|
||||
@@ -10,7 +12,6 @@ import { getRoleList } from '@/api/user/role';
|
||||
import { getDictKeyList } from '@/api/system/dict-key';
|
||||
import { getHostKeyList } from '@/api/asset/host-key';
|
||||
import { getHostIdentityList } from '@/api/asset/host-identity';
|
||||
import { getHostList } from '@/api/asset/host';
|
||||
import { getHostGroupTree } from '@/api/asset/host-group';
|
||||
import { getMenuList } from '@/api/system/menu';
|
||||
import { getCurrentAuthorizedHostIdentity, getCurrentAuthorizedHostKey } from '@/api/asset/asset-authorized-data';
|
||||
@@ -19,7 +20,7 @@ import { getExecJobList } from '@/api/job/exec-job';
|
||||
import { getPathBookmarkGroupList } from '@/api/asset/path-bookmark-group';
|
||||
|
||||
export type CacheType = 'users' | 'menus' | 'roles'
|
||||
| 'hosts' | 'hostGroups' | 'hostKeys' | 'hostIdentities'
|
||||
| 'hostGroups' | 'hostKeys' | 'hostIdentities'
|
||||
| 'dictKeys'
|
||||
| 'authorizedHostKeys' | 'authorizedHostIdentities'
|
||||
| 'commandSnippetGroups' | 'pathBookmarkGroups'
|
||||
@@ -84,8 +85,8 @@ export default defineStore('cache', {
|
||||
},
|
||||
|
||||
// 获取主机列表
|
||||
async loadHosts(force = false) {
|
||||
return await this.load('hosts', getHostList, force);
|
||||
async loadHosts(type: HostType, force = false) {
|
||||
return await this.load(`host_${type}`, () => getHostList(type), force);
|
||||
},
|
||||
|
||||
// 获取主机密钥列表
|
||||
|
||||
@@ -128,7 +128,7 @@ export default defineStore('terminal', {
|
||||
if (this.hosts.hostList?.length) {
|
||||
return;
|
||||
}
|
||||
const { data } = await getCurrentAuthorizedHost('ssh');
|
||||
const { data } = await getCurrentAuthorizedHost('SSH');
|
||||
Object.keys(data).forEach(k => {
|
||||
this.hosts[k as keyof AuthorizedHostQueryResponse] = data[k as keyof AuthorizedHostQueryResponse] as any;
|
||||
});
|
||||
|
||||
@@ -98,8 +98,8 @@ export const useColLayout = (): ColResponsiveValue => {
|
||||
sm: 12,
|
||||
md: 8,
|
||||
lg: 8,
|
||||
xl: 6,
|
||||
xxl: 4,
|
||||
xl: 8,
|
||||
xxl: 6,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
242
orion-visor-ui/src/types/protocol/terminal.protocol.ts
Normal file
242
orion-visor-ui/src/types/protocol/terminal.protocol.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
// 终端协议
|
||||
export interface Protocol {
|
||||
type: string;
|
||||
template: string[];
|
||||
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// 终端输入消息内容
|
||||
export interface InputPayload {
|
||||
type?: string;
|
||||
sessionId?: string;
|
||||
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// 终端输出消息内容
|
||||
export interface OutputPayload {
|
||||
type: string;
|
||||
sessionId: string;
|
||||
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
// 分隔符
|
||||
export const SEPARATOR = '|';
|
||||
|
||||
// 输入协议
|
||||
export const InputProtocol = {
|
||||
// 主机连接检查
|
||||
CHECK: {
|
||||
type: 'ck',
|
||||
template: ['type', 'sessionId', 'hostId', 'connectType']
|
||||
},
|
||||
// 连接主机
|
||||
CONNECT: {
|
||||
type: 'co',
|
||||
template: ['type', 'sessionId', 'terminalType', 'cols', 'rows']
|
||||
},
|
||||
// 关闭连接
|
||||
CLOSE: {
|
||||
type: 'cl',
|
||||
template: ['type', 'sessionId']
|
||||
},
|
||||
// ping
|
||||
PING: {
|
||||
type: 'p',
|
||||
template: ['type']
|
||||
},
|
||||
// SSH 修改大小
|
||||
SSH_RESIZE: {
|
||||
type: 'rs',
|
||||
template: ['type', 'sessionId', 'cols', 'rows']
|
||||
},
|
||||
// SSH 输入
|
||||
SSH_INPUT: {
|
||||
type: 'i',
|
||||
template: ['type', 'sessionId', 'command']
|
||||
},
|
||||
// SFTP 文件列表
|
||||
SFTP_LIST: {
|
||||
type: 'ls',
|
||||
template: ['type', 'sessionId', 'showHiddenFile', 'path']
|
||||
},
|
||||
// SFTP 创建文件夹
|
||||
SFTP_MKDIR: {
|
||||
type: 'mk',
|
||||
template: ['type', 'sessionId', 'path']
|
||||
},
|
||||
// SFTP 创建文件
|
||||
SFTP_TOUCH: {
|
||||
type: 'to',
|
||||
template: ['type', 'sessionId', 'path']
|
||||
},
|
||||
// SFTP 移动文件
|
||||
SFTP_MOVE: {
|
||||
type: 'mv',
|
||||
template: ['type', 'sessionId', 'path', 'target']
|
||||
},
|
||||
// SFTP 删除文件
|
||||
SFTP_REMOVE: {
|
||||
type: 'rm',
|
||||
template: ['type', 'sessionId', 'path']
|
||||
},
|
||||
// SFTP 修改文件权限
|
||||
SFTP_CHMOD: {
|
||||
type: 'cm',
|
||||
template: ['type', 'sessionId', 'path', 'mod']
|
||||
},
|
||||
// SFTP 修改文件权限
|
||||
SFTP_DOWNLOAD_FLAT_DIRECTORY: {
|
||||
type: 'df',
|
||||
template: ['type', 'sessionId', 'currentPath', 'path']
|
||||
},
|
||||
// SFTP 获取内容
|
||||
SFTP_GET_CONTENT: {
|
||||
type: 'gc',
|
||||
template: ['type', 'sessionId', 'path']
|
||||
},
|
||||
// SFTP 修改内容
|
||||
SFTP_SET_CONTENT: {
|
||||
type: 'sc',
|
||||
template: ['type', 'sessionId', 'path', 'content']
|
||||
},
|
||||
};
|
||||
|
||||
// 输出协议
|
||||
export const OutputProtocol = {
|
||||
// 主机连接检查
|
||||
CHECK: {
|
||||
type: 'ck',
|
||||
template: ['type', 'sessionId', 'result', 'msg'],
|
||||
processMethod: 'processCheck'
|
||||
},
|
||||
// 主机连接
|
||||
CONNECT: {
|
||||
type: 'co',
|
||||
template: ['type', 'sessionId', 'result', 'msg'],
|
||||
processMethod: 'processConnect'
|
||||
},
|
||||
// 主机连接关闭
|
||||
CLOSE: {
|
||||
type: 'cl',
|
||||
template: ['type', 'sessionId', 'forceClose', 'msg'],
|
||||
processMethod: 'processClose'
|
||||
},
|
||||
// pong
|
||||
PONG: {
|
||||
type: 'p',
|
||||
template: ['type'],
|
||||
processMethod: 'processPong'
|
||||
},
|
||||
// SSH 输出
|
||||
SSH_OUTPUT: {
|
||||
type: 'o',
|
||||
template: ['type', 'sessionId', 'body'],
|
||||
processMethod: 'processSshOutput'
|
||||
},
|
||||
// SFTP 文件列表
|
||||
SFTP_LIST: {
|
||||
type: 'ls',
|
||||
template: ['type', 'sessionId', 'path', 'result', 'msg', 'body'],
|
||||
processMethod: 'processSftpList'
|
||||
},
|
||||
// SFTP 创建文件夹
|
||||
SFTP_MKDIR: {
|
||||
type: 'mk',
|
||||
template: ['type', 'sessionId', 'result', 'msg'],
|
||||
processMethod: 'processSftpMkdir'
|
||||
},
|
||||
// SFTP 创建文件
|
||||
SFTP_TOUCH: {
|
||||
type: 'to',
|
||||
template: ['type', 'sessionId', 'result', 'msg'],
|
||||
processMethod: 'processSftpTouch'
|
||||
},
|
||||
// SFTP 移动文件
|
||||
SFTP_MOVE: {
|
||||
type: 'mv',
|
||||
template: ['type', 'sessionId', 'result', 'msg'],
|
||||
processMethod: 'processSftpMove'
|
||||
},
|
||||
// SFTP 删除文件
|
||||
SFTP_REMOVE: {
|
||||
type: 'rm',
|
||||
template: ['type', 'sessionId', 'result', 'msg'],
|
||||
processMethod: 'processSftpRemove'
|
||||
},
|
||||
// SFTP 修改文件权限
|
||||
SFTP_CHMOD: {
|
||||
type: 'cm',
|
||||
template: ['type', 'sessionId', 'result', 'msg'],
|
||||
processMethod: 'processSftpChmod'
|
||||
},
|
||||
// SFTP 修改文件权限
|
||||
SFTP_DOWNLOAD_FLAT_DIRECTORY: {
|
||||
type: 'df',
|
||||
template: ['type', 'sessionId', 'currentPath', 'result', 'msg', 'body'],
|
||||
processMethod: 'processDownloadFlatDirectory'
|
||||
},
|
||||
// SFTP 获取文件内容
|
||||
SFTP_GET_CONTENT: {
|
||||
type: 'gc',
|
||||
template: ['type', 'sessionId', 'path', 'result', 'msg', 'content'],
|
||||
processMethod: 'processSftpGetContent'
|
||||
},
|
||||
// SFTP 修改文件内容
|
||||
SFTP_SET_CONTENT: {
|
||||
type: 'sc',
|
||||
template: ['type', 'sessionId', 'result', 'msg'],
|
||||
processMethod: 'processSftpSetContent'
|
||||
},
|
||||
};
|
||||
|
||||
// 解析参数
|
||||
export const parse = (payload: string) => {
|
||||
const protocols = Object.values(OutputProtocol);
|
||||
const useProtocol = protocols.find(p => payload.startsWith(p.type + SEPARATOR) || p.type === payload);
|
||||
if (!useProtocol) {
|
||||
return undefined;
|
||||
}
|
||||
const template = useProtocol.template;
|
||||
const res = {} as OutputPayload;
|
||||
let curr = 0;
|
||||
let len = payload.length;
|
||||
for (let i = 0, pl = template.length; i < pl; i++) {
|
||||
if (i == pl - 1) {
|
||||
// 最后一次
|
||||
res[template[i]] = payload.substring(curr, len);
|
||||
} else {
|
||||
// 非最后一次
|
||||
let tmp = '';
|
||||
for (; curr < len; curr++) {
|
||||
const c = payload.charAt(curr);
|
||||
if (c == SEPARATOR) {
|
||||
res[template[i]] = tmp;
|
||||
curr++;
|
||||
break;
|
||||
} else {
|
||||
tmp += c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
// 格式化参数
|
||||
export const format = (protocol: Protocol, payload: InputPayload | OutputPayload) => {
|
||||
payload.type = protocol.type;
|
||||
return protocol.template
|
||||
.map(i => getPayloadValueString(payload[i]))
|
||||
.join(SEPARATOR);
|
||||
};
|
||||
|
||||
// 获取默认值
|
||||
export const getPayloadValueString = (value: unknown): any => {
|
||||
if (value === undefined || value === null) {
|
||||
return '';
|
||||
}
|
||||
return value;
|
||||
};
|
||||
29
orion-visor-ui/src/types/xterm.ts
Normal file
29
orion-visor-ui/src/types/xterm.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { ITerminalAddon } from '@xterm/xterm';
|
||||
import type { FitAddon } from '@xterm/addon-fit';
|
||||
import type { WebglAddon } from '@xterm/addon-webgl';
|
||||
import type { CanvasAddon } from '@xterm/addon-canvas';
|
||||
import type { WebLinksAddon } from '@xterm/addon-web-links';
|
||||
import type { SearchAddon } from '@xterm/addon-search';
|
||||
import type { ImageAddon } from '@xterm/addon-image';
|
||||
import type { Unicode11Addon } from '@xterm/addon-unicode11';
|
||||
|
||||
// 默认字体
|
||||
export const defaultFontFamily = 'Courier New, Monaco, courier, monospace';
|
||||
|
||||
// 默认主题
|
||||
export const defaultTheme = {
|
||||
foreground: '#FFFFFF',
|
||||
background: '#1C1C1C',
|
||||
selectionBackground: '#444444',
|
||||
};
|
||||
|
||||
// xterm 插件
|
||||
export interface XtermAddons extends Record<string, ITerminalAddon> {
|
||||
fit: FitAddon;
|
||||
webgl: WebglAddon;
|
||||
canvas: CanvasAddon;
|
||||
weblink: WebLinksAddon;
|
||||
search: SearchAddon;
|
||||
image: ImageAddon;
|
||||
unicode: Unicode11Addon;
|
||||
}
|
||||
@@ -41,7 +41,7 @@
|
||||
// 重置缓存
|
||||
onUnmounted(() => {
|
||||
const cacheStore = useCacheStore();
|
||||
cacheStore.reset('users', 'hosts');
|
||||
cacheStore.reset('users');
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
// 重置缓存
|
||||
onUnmounted(() => {
|
||||
const cacheStore = useCacheStore();
|
||||
cacheStore.reset('users', 'hosts');
|
||||
cacheStore.reset('users');
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<!-- 操作主机 -->
|
||||
<a-form-item field="hostId" label="操作主机">
|
||||
<host-selector v-model="formModel.hostId"
|
||||
type="SSH"
|
||||
placeholder="请选择主机"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
// 重置缓存
|
||||
onUnmounted(() => {
|
||||
const cacheStore = useCacheStore();
|
||||
cacheStore.reset('users', 'hosts');
|
||||
cacheStore.reset('users');
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data } = await getHostGroupRelList(groupId as number);
|
||||
const hosts = await cacheStore.loadHosts();
|
||||
const hosts = await cacheStore.loadHosts(undefined);
|
||||
selectedGroupHosts.value = data.map(s => hosts.find(h => h.id === s) as HostQueryResponse)
|
||||
.filter(Boolean);
|
||||
} catch (e) {
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
|
||||
// 卸载时清除 cache
|
||||
onUnmounted(() => {
|
||||
cacheStore.reset('users', 'roles', 'hosts', 'hostGroups', 'hostKeys', 'hostIdentities');
|
||||
cacheStore.reset('users', 'roles', 'hostGroups', 'hostKeys', 'hostIdentities');
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
<template>
|
||||
<a-card class="general-card"
|
||||
:body-style="{ padding: config.status === EnabledStatus.ENABLED ? '' : '0' }">
|
||||
<a-card class="general-card" :body-style="{ padding: '0 16px 0 20px'}">
|
||||
<!-- 标题 -->
|
||||
<template #title>
|
||||
<div class="config-title-wrapper">
|
||||
<span>SSH 配置</span>
|
||||
<a-switch v-model="config.status"
|
||||
:disabled="loading"
|
||||
type="round"
|
||||
:checked-value="EnabledStatus.ENABLED"
|
||||
:unchecked-value="EnabledStatus.DISABLED"
|
||||
:before-change="s => updateStatus(s as number)" />
|
||||
<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>
|
||||
<!-- 表单 -->
|
||||
<a-spin v-show="config.status" :loading="loading" class="config-form-wrapper">
|
||||
<div class="config-form-wrapper full">
|
||||
<!-- 表单 -->
|
||||
<a-form :model="formModel"
|
||||
ref="formRef"
|
||||
@@ -29,14 +34,6 @@
|
||||
:options="toOptions(sshOsTypeKey)"
|
||||
placeholder="请选择系统类型" />
|
||||
</a-form-item>
|
||||
<!-- SSH 端口 -->
|
||||
<a-form-item field="port"
|
||||
label="SSH端口"
|
||||
:hide-asterisk="true">
|
||||
<a-input-number v-model="formModel.port"
|
||||
placeholder="请输入SSH端口"
|
||||
hide-button />
|
||||
</a-form-item>
|
||||
<!-- 用户名 -->
|
||||
<a-form-item field="username"
|
||||
label="用户名"
|
||||
@@ -85,7 +82,8 @@
|
||||
<host-identity-selector v-model="formModel.identityId" />
|
||||
</a-form-item>
|
||||
<!-- 连接超时时间 -->
|
||||
<a-form-item field="connectTimeout"
|
||||
<a-form-item class="mt4"
|
||||
field="connectTimeout"
|
||||
label="连接超时时间"
|
||||
:hide-asterisk="true">
|
||||
<a-input-number v-model="formModel.connectTimeout"
|
||||
@@ -96,15 +94,8 @@
|
||||
</template>
|
||||
</a-input-number>
|
||||
</a-form-item>
|
||||
<!-- 其他配置 -->
|
||||
<a-collapse :bordered="false">
|
||||
<a-collapse-item key="1">
|
||||
<template #header>
|
||||
<span class="usn">其他配置</span>
|
||||
</template>
|
||||
<!-- SSH 输出编码 -->
|
||||
<a-form-item class="mt4"
|
||||
field="charset"
|
||||
<a-form-item field="charset"
|
||||
label="SSH输出编码"
|
||||
:hide-asterisk="true">
|
||||
<a-input v-model="formModel.charset" placeholder="请输入 SSH 输出编码" />
|
||||
@@ -121,24 +112,8 @@
|
||||
:hide-asterisk="true">
|
||||
<a-input v-model="formModel.fileContentCharset" placeholder="请输入 SFTP 文件内容编码" />
|
||||
</a-form-item>
|
||||
</a-collapse-item>
|
||||
</a-collapse>
|
||||
</a-form>
|
||||
<!-- 操作按钮 -->
|
||||
<div class="config-button-group">
|
||||
<a-space>
|
||||
<a-button size="small"
|
||||
@click="resetConfig">
|
||||
重置
|
||||
</a-button>
|
||||
<a-button type="primary"
|
||||
size="small"
|
||||
@click="saveConfig">
|
||||
保存
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-spin>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
@@ -151,38 +126,26 @@
|
||||
<script lang="ts" setup>
|
||||
import type { FieldRule } from '@arco-design/web-vue';
|
||||
import type { HostSshConfig } from '../types/const';
|
||||
import { reactive, ref, watch } from 'vue';
|
||||
import { updateHostConfigStatus, updateHostConfig } from '@/api/asset/host-config';
|
||||
import { sshAuthTypeKey, sshOsTypeKey, SshAuthType, SshOsType } from '../types/const';
|
||||
import rules from '../types/ssh-form.rules';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import type { HostConfigQueryResponse } from '@/api/asset/host';
|
||||
import { ref, watch } from 'vue';
|
||||
import { sshAuthTypeKey, sshOsTypeKey, SshAuthType } from '../types/const';
|
||||
import { useDictStore } from '@/store';
|
||||
import { EnabledStatus } from '@/types/const';
|
||||
import { HostConfigType } from '../types/const';
|
||||
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 { loading, setLoading } = useLoading();
|
||||
const { toOptions, toRadioOptions } = useDictStore();
|
||||
|
||||
const props = defineProps<{
|
||||
content: any;
|
||||
hostId: number;
|
||||
hostConfig: HostConfigQueryResponse;
|
||||
}>();
|
||||
|
||||
const emits = defineEmits(['submitted']);
|
||||
|
||||
const config = reactive({
|
||||
status: undefined,
|
||||
version: undefined,
|
||||
});
|
||||
const emits = defineEmits(['save', 'reset']);
|
||||
|
||||
const formRef = ref();
|
||||
const formModel = ref<HostSshConfig>({
|
||||
osType: SshOsType.LINUX,
|
||||
osType: undefined,
|
||||
username: undefined,
|
||||
port: undefined,
|
||||
password: undefined,
|
||||
authType: SshAuthType.PASSWORD,
|
||||
keyId: undefined,
|
||||
@@ -196,10 +159,10 @@
|
||||
});
|
||||
|
||||
// 监听数据变化
|
||||
watch(() => props.content, (v: any) => {
|
||||
config.status = v?.status;
|
||||
config.version = v?.version;
|
||||
resetConfig();
|
||||
watch(() => props.hostConfig.current, () => {
|
||||
formModel.value = Object.assign({}, props.hostConfig.config);
|
||||
// 使用新密码默认为不包含密码
|
||||
formModel.value.useNewPassword = !formModel.value.hasPassword;
|
||||
});
|
||||
|
||||
// 用户名验证
|
||||
@@ -230,77 +193,20 @@
|
||||
}
|
||||
}] as FieldRule[];
|
||||
|
||||
// 修改状态
|
||||
const updateStatus = async (e: number) => {
|
||||
setLoading(true);
|
||||
return updateHostConfigStatus({
|
||||
hostId: props?.hostId,
|
||||
type: HostConfigType.SSH,
|
||||
status: e,
|
||||
version: config.version
|
||||
}).then(({ data }) => {
|
||||
config.version = data;
|
||||
setLoading(false);
|
||||
return true;
|
||||
}).catch(() => {
|
||||
setLoading(false);
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
// 重置配置
|
||||
const resetConfig = () => {
|
||||
formModel.value = Object.assign({}, props.content?.config);
|
||||
// 使用新密码默认为不包含密码
|
||||
formModel.value.useNewPassword = !formModel.value.hasPassword;
|
||||
};
|
||||
|
||||
// 保存配置
|
||||
const saveConfig = async () => {
|
||||
try {
|
||||
// 验证参数
|
||||
const error = await formRef.value.validate();
|
||||
if (error) {
|
||||
return false;
|
||||
}
|
||||
setLoading(true);
|
||||
// 更新
|
||||
const { data } = await updateHostConfig({
|
||||
hostId: props?.hostId,
|
||||
type: HostConfigType.SSH,
|
||||
version: config.version,
|
||||
config: JSON.stringify(formModel.value)
|
||||
});
|
||||
config.version = data;
|
||||
setLoading(false);
|
||||
Message.success('修改成功');
|
||||
// 回调 props
|
||||
emits('submitted', { ...props.content, ...config, config: { ...formModel.value } });
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
// 回调
|
||||
emits('save', { ...formModel.value });
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.config-title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.config-form-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.config-button-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.auth-type-group {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
@@ -312,13 +218,4 @@
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
:deep(.arco-collapse-item-content) {
|
||||
background: unset;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:deep(.arco-collapse-item-header) {
|
||||
border: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -11,15 +11,15 @@
|
||||
<!-- 标题 -->
|
||||
<template #title>
|
||||
<span class="host-title-text">
|
||||
主机配置 <span class="host-name-title-text">{{ record.name }}</span>
|
||||
主机配置 <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-id="record.id"
|
||||
:content="config.ssh"
|
||||
@submitted="(e) => config.ssh = e" />
|
||||
:host-config="hostConfig"
|
||||
@save="save"
|
||||
@reset="reset" />
|
||||
</a-spin>
|
||||
</a-drawer>
|
||||
</template>
|
||||
@@ -31,14 +31,14 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { HostConfigWrapper, HostSshConfig } from '../types/const';
|
||||
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 { getHostConfigList } from '@/api/asset/host-config';
|
||||
import { useCacheStore, useDictStore } from '@/store';
|
||||
import { dictKeys } from '../types/const';
|
||||
import { getHostConfig, updateHostConfig } from '@/api/asset/host';
|
||||
import SshConfigForm from '../components/ssh-config-form.vue';
|
||||
|
||||
const { visible, setVisible } = useVisible();
|
||||
@@ -46,13 +46,12 @@
|
||||
const cacheStore = useCacheStore();
|
||||
|
||||
const record = ref({} as any);
|
||||
const config = ref<HostConfigWrapper>({
|
||||
ssh: {} as HostSshConfig
|
||||
});
|
||||
const hostConfig = ref<HostConfigQueryResponse>({} as HostConfigQueryResponse);
|
||||
|
||||
// 打开
|
||||
const open = async (e: any) => {
|
||||
record.value = { ...e };
|
||||
hostConfig.value = { config: {} } as HostConfigQueryResponse;
|
||||
try {
|
||||
setLoading(true);
|
||||
setVisible(true);
|
||||
@@ -60,10 +59,9 @@
|
||||
const dictStore = useDictStore();
|
||||
await dictStore.loadKeys(dictKeys);
|
||||
// 加载配置
|
||||
const { data } = await getHostConfigList(record.value.id);
|
||||
data.forEach(s => {
|
||||
config.value[s.type] = s;
|
||||
});
|
||||
const { data } = await getHostConfig(record.value.id);
|
||||
data.current = Date.now();
|
||||
hostConfig.value = data;
|
||||
} catch ({ message }) {
|
||||
Message.error(`配置加载失败 ${message}`);
|
||||
setVisible(false);
|
||||
@@ -72,14 +70,38 @@
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({ open });
|
||||
|
||||
// 保存
|
||||
const save = async (conf: Record<string, any>) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 更新
|
||||
await updateHostConfig({
|
||||
id: hostConfig.value.id,
|
||||
config: JSON.stringify(conf)
|
||||
});
|
||||
// 设置参数
|
||||
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);
|
||||
};
|
||||
|
||||
defineExpose({ open });
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@@ -120,4 +142,21 @@
|
||||
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,19 +1,11 @@
|
||||
// 主机所有配置
|
||||
export interface HostConfigWrapper {
|
||||
ssh: HostSshConfig;
|
||||
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// 主机 SSH 配置
|
||||
export interface HostSshConfig {
|
||||
osType?: string;
|
||||
port?: number;
|
||||
username?: string;
|
||||
password?: string;
|
||||
authType?: string;
|
||||
identityId?: number;
|
||||
keyId?: number;
|
||||
identityId?: number;
|
||||
connectTimeout?: number;
|
||||
charset?: string;
|
||||
fileNameCharset?: string;
|
||||
@@ -32,17 +24,6 @@ export const SshAuthType = {
|
||||
IDENTITY: 'IDENTITY'
|
||||
};
|
||||
|
||||
// 主机系统版本
|
||||
export const SshOsType = {
|
||||
LINUX: 'LINUX',
|
||||
WINDOWS: 'WINDOWS',
|
||||
};
|
||||
|
||||
// 主机配置类型
|
||||
export const HostConfigType = {
|
||||
SSH: 'ssh'
|
||||
};
|
||||
|
||||
// 主机验证方式 字典项
|
||||
export const sshAuthTypeKey = 'hostSshAuthType';
|
||||
|
||||
|
||||
@@ -5,16 +5,6 @@ export const osType = [{
|
||||
message: '请选择系统类型'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const port = [{
|
||||
required: true,
|
||||
message: '请输入SSH端口'
|
||||
}, {
|
||||
type: 'number',
|
||||
min: 1,
|
||||
max: 65535,
|
||||
message: '输入的端口不合法'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const authType = [{
|
||||
required: true,
|
||||
message: '请选择认证方式'
|
||||
@@ -66,7 +56,6 @@ export const fileContentCharset = [{
|
||||
|
||||
export default {
|
||||
osType,
|
||||
port,
|
||||
authType,
|
||||
keyId,
|
||||
identityId,
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
cacheStore.loadHosts().then(hosts => {
|
||||
cacheStore.loadHosts(undefined).then(hosts => {
|
||||
data.value = hosts.map(s => {
|
||||
return {
|
||||
value: String(s.id),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<card-list v-model:searchValue="formModel.searchValue"
|
||||
search-input-placeholder="输入 id / 名称 / 用户名"
|
||||
:loading="loading"
|
||||
:fieldConfig="fieldConfig"
|
||||
:field-config="fieldConfig"
|
||||
:list="list"
|
||||
:pagination="pagination"
|
||||
:card-layout-cols="cardColLayout"
|
||||
@@ -119,36 +119,22 @@
|
||||
<!-- 修改 -->
|
||||
<a-doption v-permission="['asset:host-identity:update']"
|
||||
@click="emits('openUpdate', record)">
|
||||
<icon-edit />
|
||||
修改
|
||||
<span class="more-doption normal">
|
||||
<icon-edit /> 修改
|
||||
</span>
|
||||
</a-doption>
|
||||
<!-- 删除 -->
|
||||
<a-doption v-permission="['asset:host-identity:delete']"
|
||||
class="span-red"
|
||||
@click="deleteRow(record.id)">
|
||||
<icon-delete />
|
||||
删除
|
||||
<span class="more-doption error">
|
||||
<icon-delete /> 删除
|
||||
</span>
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</template>
|
||||
<!-- 右键菜单 -->
|
||||
<template #contextMenu="{ record }">
|
||||
<!-- 修改 -->
|
||||
<a-doption v-permission="['asset:host-identity:update']"
|
||||
@click="emits('openUpdate', record)">
|
||||
<icon-edit />
|
||||
修改
|
||||
</a-doption>
|
||||
<!-- 删除 -->
|
||||
<a-doption v-permission="['asset:host-identity:delete']"
|
||||
class="span-red"
|
||||
@click="deleteRow(record.id)">
|
||||
<icon-delete />
|
||||
删除
|
||||
</a-doption>
|
||||
</template>
|
||||
</card-list>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<card-list v-model:searchValue="formModel.searchValue"
|
||||
search-input-placeholder="输入 id / 名称"
|
||||
:loading="loading"
|
||||
:fieldConfig="fieldConfig"
|
||||
:field-config="fieldConfig"
|
||||
:list="list"
|
||||
:pagination="pagination"
|
||||
:card-layout-cols="cardColLayout"
|
||||
@@ -47,48 +47,29 @@
|
||||
<!-- 详情 -->
|
||||
<a-doption v-permission="['asset:host-key:detail', 'asset:host-key:update']"
|
||||
@click="emits('openView', record)">
|
||||
<icon-list />
|
||||
详情
|
||||
<span class="more-doption normal">
|
||||
<icon-list /> 详情
|
||||
</span>
|
||||
</a-doption>
|
||||
<!-- 修改 -->
|
||||
<a-doption v-permission="['asset:host-key:update']"
|
||||
@click="emits('openUpdate', record)">
|
||||
<icon-edit />
|
||||
修改
|
||||
<span class="more-doption normal">
|
||||
<icon-edit /> 修改
|
||||
</span>
|
||||
</a-doption>
|
||||
<!-- 删除 -->
|
||||
<a-doption v-permission="['asset:host-key:delete']"
|
||||
class="span-red"
|
||||
@click="deleteRow(record.id)">
|
||||
<icon-delete />
|
||||
删除
|
||||
<span class="more-doption error">
|
||||
<icon-delete /> 删除
|
||||
</span>
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</template>
|
||||
<!-- 右键菜单 -->
|
||||
<template #contextMenu="{ record }">
|
||||
<!-- 详情 -->
|
||||
<a-doption v-permission="['asset:host-key:detail', 'asset:host-key:update']"
|
||||
@click="emits('openView', record)">
|
||||
<icon-list />
|
||||
详情
|
||||
</a-doption>
|
||||
<!-- 修改 -->
|
||||
<a-doption v-permission="['asset:host-key:update']"
|
||||
@click="emits('openUpdate', record)">
|
||||
<icon-edit />
|
||||
修改
|
||||
</a-doption>
|
||||
<!-- 删除 -->
|
||||
<a-doption v-permission="['asset:host-key:delete']"
|
||||
class="span-red"
|
||||
@click="deleteRow(record.id)">
|
||||
<icon-delete />
|
||||
删除
|
||||
</a-doption>
|
||||
</template>
|
||||
</card-list>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<card-list v-model:searchValue="formModel.searchValue"
|
||||
search-input-placeholder="输入 id / 名称 / 编码 / 地址"
|
||||
:loading="loading"
|
||||
:fieldConfig="fieldConfig"
|
||||
:field-config="fieldConfig"
|
||||
:list="list"
|
||||
:pagination="pagination"
|
||||
:card-layout-cols="cardColLayout"
|
||||
@@ -70,6 +70,20 @@
|
||||
<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-select v-model="formModel.type"
|
||||
:options="toOptions(hostTypeKey)"
|
||||
placeholder="请选择主机类型"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 主机状态 -->
|
||||
<a-form-item field="status" label="主机状态">
|
||||
<a-select v-model="formModel.status"
|
||||
:options="toOptions(hostStatusKey)"
|
||||
placeholder="请选择主机状态"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 主机标签 -->
|
||||
<a-form-item field="tags" label="主机标签">
|
||||
<tag-multi-selector v-model="formModel.tags"
|
||||
@@ -88,11 +102,30 @@
|
||||
<template #code="{ record }">
|
||||
{{ record.code }}
|
||||
</template>
|
||||
<!-- 主机类型 -->
|
||||
<template #type="{ record }">
|
||||
<a-tag :color="getDictValue(hostTypeKey, record.type, 'color')">
|
||||
{{ getDictValue(hostTypeKey, record.type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<!-- 地址 -->
|
||||
<template #address="{ record }">
|
||||
<span class="span-blue text-copy" @click="copy(record.address)">
|
||||
<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>
|
||||
<!-- 主机状态 -->
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="getDictValue(hostStatusKey, record.status, 'color')">
|
||||
{{ getDictValue(hostStatusKey, record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<!-- 标签 -->
|
||||
<template #tags="{ record }">
|
||||
@@ -114,48 +147,44 @@
|
||||
<!-- 修改 -->
|
||||
<a-doption v-permission="['asset:host:update']"
|
||||
@click="emits('openUpdate', record)">
|
||||
<icon-edit />
|
||||
修改
|
||||
<span class="more-doption normal">
|
||||
<icon-edit /> 修改
|
||||
</span>
|
||||
</a-doption>
|
||||
<!-- 配置 -->
|
||||
<a-doption v-permission="['asset:host:update-config']"
|
||||
@click="emits('openUpdateConfig', record)">
|
||||
<icon-settings />
|
||||
配置
|
||||
<span class="more-doption normal">
|
||||
<icon-settings /> 配置
|
||||
</span>
|
||||
</a-doption>
|
||||
<!-- 修改状态 -->
|
||||
<a-doption v-permission="['asset:host:update-status']"
|
||||
@click="updateStatus(record as HostQueryResponse)">
|
||||
<span class="more-doption"
|
||||
:class="[toggleDictValue(hostStatusKey, record.status, 'status')]">
|
||||
<icon-sync /> {{ toggleDictValue(hostStatusKey, record.status, 'label') }}
|
||||
</span>
|
||||
</a-doption>
|
||||
<!-- 复制 -->
|
||||
<a-doption v-permission="['asset:host:create']"
|
||||
@click="emits('openCopy', record)">
|
||||
<span class="more-doption normal">
|
||||
<icon-copy /> 复制
|
||||
</span>
|
||||
</a-doption>
|
||||
<!-- 删除 -->
|
||||
<a-doption v-permission="['asset:host:delete']"
|
||||
class="span-red"
|
||||
@click="deleteRow(record.id)">
|
||||
<icon-delete />
|
||||
删除
|
||||
<span class="more-doption error">
|
||||
<icon-delete /> 删除
|
||||
</span>
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</template>
|
||||
<!-- 右键菜单 -->
|
||||
<template #contextMenu="{ record }">
|
||||
<!-- 修改 -->
|
||||
<a-doption v-permission="['asset:host:update']"
|
||||
@click="emits('openUpdate', record)">
|
||||
<icon-edit />
|
||||
修改
|
||||
</a-doption>
|
||||
<!-- 配置 -->
|
||||
<a-doption v-permission="['asset:host:update-config']"
|
||||
@click="emits('openUpdateConfig', record)">
|
||||
<icon-settings />
|
||||
配置
|
||||
</a-doption>
|
||||
<!-- 删除 -->
|
||||
<a-doption v-permission="['asset:host:delete']"
|
||||
class="span-red"
|
||||
@click="deleteRow(record.id)">
|
||||
<icon-delete />
|
||||
删除
|
||||
</a-doption>
|
||||
</template>
|
||||
</card-list>
|
||||
</template>
|
||||
|
||||
@@ -169,24 +198,25 @@
|
||||
import type { HostQueryRequest, HostQueryResponse } from '@/api/asset/host';
|
||||
import { usePagination, useColLayout } from '@/types/card';
|
||||
import { computed, reactive, ref, onMounted } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { dataColor, objectTruthKeyCount, resetObject } from '@/utils';
|
||||
import fieldConfig from '../types/card.fields';
|
||||
import { deleteHost, getHostPage } from '@/api/asset/host';
|
||||
import { deleteHost, getHostPage, updateHostStatus } from '@/api/asset/host';
|
||||
import { Message, Modal } from '@arco-design/web-vue';
|
||||
import { tagColor } from '../types/const';
|
||||
import { hostStatusKey, hostTypeKey, tagColor } from '../types/const';
|
||||
import { copy } from '@/hooks/copy';
|
||||
import { useDictStore } from '@/store';
|
||||
import { GrantKey, GrantRouteName } from '@/views/asset/grant/types/const';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import fieldConfig from '../types/card.fields';
|
||||
import TagMultiSelector from '@/components/meta/tag/multi-selector/index.vue';
|
||||
|
||||
const emits = defineEmits(['openAdd', 'openUpdate', 'openUpdateConfig', 'openHostGroup']);
|
||||
|
||||
const list = ref<HostQueryResponse[]>([]);
|
||||
const emits = defineEmits(['openAdd', 'openUpdate', 'openUpdateConfig', 'openHostGroup', 'openCopy']);
|
||||
|
||||
const cardColLayout = useColLayout();
|
||||
const pagination = usePagination();
|
||||
const { loading, setLoading } = useLoading();
|
||||
const { toOptions, getDictValue, toggleDictValue, toggleDict } = useDictStore();
|
||||
|
||||
const list = ref<HostQueryResponse[]>([]);
|
||||
const formRef = ref();
|
||||
const formModel = reactive<HostQueryRequest>({
|
||||
searchValue: undefined,
|
||||
@@ -194,6 +224,8 @@
|
||||
name: undefined,
|
||||
code: undefined,
|
||||
address: undefined,
|
||||
type: undefined,
|
||||
status: undefined,
|
||||
tags: undefined,
|
||||
queryTag: true
|
||||
});
|
||||
@@ -203,6 +235,33 @@
|
||||
return objectTruthKeyCount(formModel, ['searchValue', 'queryTag']);
|
||||
});
|
||||
|
||||
// 更新状态
|
||||
const updateStatus = async (record: HostQueryResponse) => {
|
||||
const dict = toggleDict(hostStatusKey, record.status);
|
||||
Modal.confirm({
|
||||
title: `${dict.label}确认`,
|
||||
titleAlign: 'start',
|
||||
content: `确定要${dict.label}该主机吗?`,
|
||||
okText: '确定',
|
||||
onOk: async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const newStatus = dict.value as string;
|
||||
// 调用修改接口
|
||||
await updateHostStatus({
|
||||
id: record.id,
|
||||
status: newStatus,
|
||||
});
|
||||
record.status = newStatus;
|
||||
Message.success(`已${dict.label}`);
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 删除当前行
|
||||
const deleteRow = (id: number) => {
|
||||
Modal.confirm({
|
||||
@@ -273,4 +332,8 @@
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.host-address::after {
|
||||
content: ':';
|
||||
user-select: text;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -18,6 +18,15 @@
|
||||
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)" />
|
||||
</a-form-item>
|
||||
<!-- 主机名称 -->
|
||||
<a-form-item field="name" label="主机名称">
|
||||
<a-input v-model="formModel.name" placeholder="请输入主机名称" />
|
||||
@@ -30,6 +39,12 @@
|
||||
<a-form-item field="address" label="主机地址">
|
||||
<a-input v-model="formModel.address" placeholder="请输入主机地址" />
|
||||
</a-form-item>
|
||||
<!-- 主机端口 -->
|
||||
<a-form-item field="port" label="主机端口">
|
||||
<a-input-number v-model="formModel.port"
|
||||
placeholder="请输入主机端口"
|
||||
hide-button />
|
||||
</a-form-item>
|
||||
<!-- 主机分组 -->
|
||||
<a-form-item field="groupIdList" label="主机分组">
|
||||
<host-group-tree-selector v-model="formModel.groupIdList"
|
||||
@@ -65,10 +80,12 @@
|
||||
import { createHost, getHost, updateHost } from '@/api/asset/host';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { pick } from 'lodash';
|
||||
import { tagColor } from '@/views/asset/host-list/types/const';
|
||||
import { tagColor, hostType, hostTypeKey } 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();
|
||||
|
||||
@@ -78,9 +95,11 @@
|
||||
const defaultForm = (): HostUpdateRequest => {
|
||||
return {
|
||||
id: undefined,
|
||||
type: hostType.SSH.type,
|
||||
name: undefined,
|
||||
code: undefined,
|
||||
address: undefined,
|
||||
port: hostType.SSH.default.port,
|
||||
tags: undefined,
|
||||
groupIdList: undefined,
|
||||
};
|
||||
@@ -108,13 +127,22 @@
|
||||
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', 'name', 'code', 'address', 'groupIdList'));
|
||||
pick(data, 'id', 'type', 'name', 'code', 'address', 'port', 'status', 'groupIdList'));
|
||||
// tag
|
||||
const tags = (data.tags || []).map(s => s.id);
|
||||
// 渲染
|
||||
@@ -130,7 +158,7 @@
|
||||
formModel.value = Object.assign({}, record);
|
||||
};
|
||||
|
||||
defineExpose({ openAdd, openUpdate });
|
||||
defineExpose({ openAdd, openUpdate, openCopy });
|
||||
|
||||
// tag 超出所选限制
|
||||
const onLimitedTag = (count: number, message: string) => {
|
||||
|
||||
@@ -25,6 +25,20 @@
|
||||
<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-select v-model="formModel.type"
|
||||
:options="toOptions(hostTypeKey)"
|
||||
placeholder="请选择主机类型"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 主机状态 -->
|
||||
<a-form-item field="status" label="主机状态">
|
||||
<a-select v-model="formModel.status"
|
||||
:options="toOptions(hostStatusKey)"
|
||||
placeholder="请选择主机状态"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 主机标签 -->
|
||||
<a-form-item field="tags" label="主机标签">
|
||||
<tag-multi-selector v-model="formModel.tags"
|
||||
@@ -115,17 +129,34 @@
|
||||
:bordered="false"
|
||||
@page-change="(page) => fetchTableData(page, pagination.pageSize)"
|
||||
@page-size-change="(size) => fetchTableData(1, size)">
|
||||
<!-- 编码 -->
|
||||
<!-- 主机类型 -->
|
||||
<template #type="{ record }">
|
||||
<a-tag :color="getDictValue(hostTypeKey, record.type, 'color')">
|
||||
{{ getDictValue(hostTypeKey, record.type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<!-- 主机编码 -->
|
||||
<template #code="{ record }">
|
||||
<a-tag>{{ record.code }}</a-tag>
|
||||
</template>
|
||||
<!-- 地址 -->
|
||||
<template #address="{ record }">
|
||||
<span class="span-blue text-copy"
|
||||
<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>
|
||||
<!-- 主机状态 -->
|
||||
<template #status="{ record }">
|
||||
<a-tag :color="getDictValue(hostStatusKey, record.status, 'color')">
|
||||
{{ getDictValue(hostStatusKey, record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<!-- 标签 -->
|
||||
<template #tags="{ record }">
|
||||
@@ -149,13 +180,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"
|
||||
@@ -168,6 +192,36 @@
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
<!-- 更多 -->
|
||||
<a-dropdown trigger="hover">
|
||||
<a-button type="text" size="mini">
|
||||
更多
|
||||
</a-button>
|
||||
<template #content>
|
||||
<!-- 配置 -->
|
||||
<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)">
|
||||
<span class="more-doption"
|
||||
:class="[toggleDictValue(hostStatusKey, record.status, 'status')]">
|
||||
{{ toggleDictValue(hostStatusKey, record.status, 'label') }}
|
||||
</span>
|
||||
</a-doption>
|
||||
<!-- 复制 -->
|
||||
<a-doption v-permission="['asset:host:create']"
|
||||
@click="emits('openCopy', record)">
|
||||
<span class="more-doption normal">
|
||||
复制
|
||||
</span>
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
</a-table>
|
||||
@@ -183,43 +237,72 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HostQueryRequest, HostQueryResponse } from '@/api/asset/host';
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import { deleteHost, batchDeleteHost, getHostPage } from '@/api/asset/host';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { tagColor } from '../types/const';
|
||||
import { deleteHost, batchDeleteHost, getHostPage, updateHostStatus } from '@/api/asset/host';
|
||||
import { Message, Modal } from '@arco-design/web-vue';
|
||||
import { tagColor, hostTypeKey, hostStatusKey } from '../types/const';
|
||||
import { usePagination, useRowSelection } from '@/types/table';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { useDictStore } from '@/store';
|
||||
import { copy } from '@/hooks/copy';
|
||||
import columns from '../types/table.columns';
|
||||
import { dataColor } from '@/utils';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import columns from '../types/table.columns';
|
||||
import { GrantKey, GrantRouteName } from '@/views/asset/grant/types/const';
|
||||
import TagMultiSelector from '@/components/meta/tag/multi-selector/index.vue';
|
||||
|
||||
const emits = defineEmits(['openAdd', 'openUpdate', 'openUpdateConfig', 'openHostGroup']);
|
||||
const emits = defineEmits(['openCopy', 'openAdd', 'openUpdate', 'openUpdateConfig', 'openHostGroup']);
|
||||
|
||||
const pagination = usePagination();
|
||||
const rowSelection = useRowSelection();
|
||||
const { loading, setLoading } = useLoading();
|
||||
const { toOptions, getDictValue, toggleDictValue, toggleDict } = useDictStore();
|
||||
|
||||
const tagSelector = ref();
|
||||
const selectedKeys = ref<number[]>([]);
|
||||
const tableRenderData = ref<HostQueryResponse[]>([]);
|
||||
const selectedKeys = ref<Array<number>>([]);
|
||||
const tableRenderData = ref<Array<HostQueryResponse>>([]);
|
||||
const formModel = reactive<HostQueryRequest>({
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
code: undefined,
|
||||
address: undefined,
|
||||
type: undefined,
|
||||
status: undefined,
|
||||
tags: undefined,
|
||||
queryTag: true
|
||||
});
|
||||
|
||||
// 更新状态
|
||||
const updateStatus = async (record: HostQueryResponse) => {
|
||||
const dict = toggleDict(hostStatusKey, record.status);
|
||||
Modal.confirm({
|
||||
title: `${dict.label}确认`,
|
||||
titleAlign: 'start',
|
||||
content: `确定要${dict.label}该主机吗?`,
|
||||
okText: '确定',
|
||||
onOk: async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const newStatus = dict.value as string;
|
||||
// 调用修改接口
|
||||
await updateHostStatus({
|
||||
id: record.id,
|
||||
status: newStatus,
|
||||
});
|
||||
record.status = newStatus;
|
||||
Message.success(`已${dict.label}`);
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 删除当前行
|
||||
const deleteRow = async ({ id }: {
|
||||
id: number
|
||||
}) => {
|
||||
const deleteRow = async (record: HostQueryResponse) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 调用删除接口
|
||||
await deleteHost(id);
|
||||
await deleteHost(record.id);
|
||||
Message.success('删除成功');
|
||||
// 重新加载数据
|
||||
fetchTableData();
|
||||
@@ -292,4 +375,10 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.host-address::after {
|
||||
content: ':';
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<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)" />
|
||||
@@ -11,6 +12,7 @@
|
||||
<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)" />
|
||||
@@ -32,8 +34,9 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, onUnmounted } from 'vue';
|
||||
import { useAppStore, useCacheStore } from '@/store';
|
||||
import { computed, ref, onUnmounted, onBeforeMount } from 'vue';
|
||||
import { useAppStore, useCacheStore, useDictStore } from '@/store';
|
||||
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';
|
||||
@@ -69,9 +72,15 @@
|
||||
}
|
||||
};
|
||||
|
||||
// 加载字典配置
|
||||
onBeforeMount(async () => {
|
||||
const dictStore = useDictStore();
|
||||
await dictStore.loadKeys(dictKeys);
|
||||
});
|
||||
|
||||
// 卸载时清除 cache
|
||||
onUnmounted(() => {
|
||||
cacheStore.reset('hosts', 'hostKeys', 'hostIdentities', 'hostGroups', 'HOST_Tags');
|
||||
cacheStore.reset('hostKeys', 'hostIdentities', 'hostGroups', 'HOST_Tags');
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -9,6 +9,10 @@ const fieldConfig = {
|
||||
label: 'id',
|
||||
dataIndex: 'id',
|
||||
slotName: 'id',
|
||||
}, {
|
||||
label: '主机类型',
|
||||
dataIndex: 'type',
|
||||
slotName: 'type',
|
||||
}, {
|
||||
label: '主机编码',
|
||||
dataIndex: 'code',
|
||||
@@ -18,6 +22,10 @@ const fieldConfig = {
|
||||
dataIndex: 'address',
|
||||
slotName: 'address',
|
||||
tooltip: true,
|
||||
}, {
|
||||
label: '主机状态',
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
}, {
|
||||
label: '主机标签',
|
||||
dataIndex: 'tags',
|
||||
|
||||
@@ -6,3 +6,22 @@ export const tagColor = [
|
||||
'pinkpurple',
|
||||
'magenta'
|
||||
];
|
||||
|
||||
// 主机类型
|
||||
export const hostType = {
|
||||
SSH: {
|
||||
type: 'SSH',
|
||||
default: {
|
||||
port: 22,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 主机类型 字典项
|
||||
export const hostTypeKey = 'hostType';
|
||||
|
||||
// 主机状态 字典项
|
||||
export const hostStatusKey = 'hostStatus';
|
||||
|
||||
// 加载的字典值
|
||||
export const dictKeys = [hostTypeKey, hostStatusKey];
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import type { FieldRule } from '@arco-design/web-vue';
|
||||
|
||||
export const type = [{
|
||||
required: true,
|
||||
message: '请选择主机类型'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const name = [{
|
||||
required: true,
|
||||
message: '请输入主机名称'
|
||||
@@ -24,14 +29,26 @@ export const address = [{
|
||||
message: '主机地址长度不能大于128位'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const port = [{
|
||||
required: true,
|
||||
message: '请输入主机端口'
|
||||
}, {
|
||||
type: 'number',
|
||||
min: 1,
|
||||
max: 65535,
|
||||
message: '输入的端口不合法'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const tags = [{
|
||||
maxLength: 5,
|
||||
message: '最多选择5个标签'
|
||||
}] as FieldRule[];
|
||||
|
||||
export default {
|
||||
type,
|
||||
name,
|
||||
code,
|
||||
address,
|
||||
port,
|
||||
tags,
|
||||
} as Record<string, FieldRule | FieldRule[]>;
|
||||
|
||||
@@ -5,7 +5,7 @@ const columns = [
|
||||
title: 'id',
|
||||
dataIndex: 'id',
|
||||
slotName: 'id',
|
||||
width: 100,
|
||||
width: 68,
|
||||
align: 'left',
|
||||
fixed: 'left',
|
||||
}, {
|
||||
@@ -13,20 +13,37 @@ const columns = [
|
||||
dataIndex: 'name',
|
||||
slotName: 'name',
|
||||
ellipsis: true,
|
||||
tooltip: true
|
||||
tooltip: true,
|
||||
minWidth: 238,
|
||||
}, {
|
||||
title: '主机编码',
|
||||
dataIndex: 'code',
|
||||
slotName: 'code',
|
||||
minWidth: 120,
|
||||
}, {
|
||||
title: '主机类型',
|
||||
dataIndex: 'type',
|
||||
slotName: 'type',
|
||||
align: 'center',
|
||||
width: 88,
|
||||
}, {
|
||||
title: '主机地址',
|
||||
dataIndex: 'address',
|
||||
slotName: 'address',
|
||||
minWidth: 238
|
||||
}, {
|
||||
title: '主机标签',
|
||||
dataIndex: 'tags',
|
||||
slotName: 'tags',
|
||||
align: 'left',
|
||||
minWidth: 148,
|
||||
}, {
|
||||
title: '主机状态',
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
width: 88,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
}, {
|
||||
title: '操作',
|
||||
slotName: 'handle',
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
</template>
|
||||
<!-- 主机模态框 -->
|
||||
<authorized-host-modal ref="hostModal"
|
||||
type="SSH"
|
||||
@selected="setSelectedHost" />
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
@selected="setWithTemplate" />
|
||||
<!-- 主机模态框 -->
|
||||
<authorized-host-modal ref="hostModal"
|
||||
type="SSH"
|
||||
@selected="setSelectedHost" />
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
@open-host="(e) => openHostModal('exec', e)" />
|
||||
<!-- 主机模态框 -->
|
||||
<authorized-host-modal ref="hostModal"
|
||||
type="SSH"
|
||||
@selected="hostSelected" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<!-- 主机列表 -->
|
||||
<host-list-view class="host-list"
|
||||
:hostList="hostList"
|
||||
empty-value="当前分组内无授权主机/主机未启用 SSH 配置!" />
|
||||
empty-value="当前分组内无授权主机!" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
const emptyMessage = computed(() => {
|
||||
if (props.newConnectionType === NewConnectionType.LIST) {
|
||||
// 列表
|
||||
return '无授权主机/主机未启用 SSH 配置!';
|
||||
return '无授权主机!';
|
||||
} else if (props.newConnectionType === NewConnectionType.FAVORITE) {
|
||||
// 收藏
|
||||
return '无收藏记录, 快去点击主机右侧的⭐进行收藏吧!';
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
const { toOptions } = useDictStore();
|
||||
|
||||
const formModel = ref<LabelExtraSettingModel>({
|
||||
color: ''
|
||||
color: '',
|
||||
});
|
||||
|
||||
// 渲染表单
|
||||
|
||||
@@ -216,7 +216,7 @@
|
||||
selectedFiles: Array<string>;
|
||||
}>();
|
||||
|
||||
const emits = defineEmits(['update:selectedFiles', 'loadFile', 'download', 'setLoading']);
|
||||
const emits = defineEmits(['loadFile', 'download', 'deleteFile', 'setLoading']);
|
||||
|
||||
const showHiddenFile = ref(false);
|
||||
const analysisPaths = ref<Array<PathAnalysis>>([]);
|
||||
@@ -295,9 +295,12 @@
|
||||
|
||||
// 删除选中文件
|
||||
const deleteSelectFiles = () => {
|
||||
if (props.selectedFiles?.length) {
|
||||
props.session?.remove(props.selectedFiles);
|
||||
}
|
||||
emits('deleteFile', [...props.selectedFiles]);
|
||||
};
|
||||
|
||||
// 下载文件
|
||||
const downloadFile = () => {
|
||||
emits('download', [...props.selectedFiles], true);
|
||||
};
|
||||
|
||||
// 重新连接
|
||||
@@ -309,12 +312,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
// 下载文件
|
||||
const downloadFile = () => {
|
||||
emits('download', [...props.selectedFiles]);
|
||||
emits('update:selectedFiles', []);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
selectedFiles: Array<string>;
|
||||
}>();
|
||||
|
||||
const emits = defineEmits(['update:selectedFiles', 'loadFile', 'editFile', 'download']);
|
||||
const emits = defineEmits(['update:selectedFiles', 'loadFile', 'editFile', 'deleteFile', 'download']);
|
||||
|
||||
const openSftpMoveModal = inject(openSftpMoveModalKey) as (sessionId: string, path: string) => void;
|
||||
const openSftpChmodModal = inject(openSftpChmodModalKey) as (sessionId: string, path: string, permission: number) => void;
|
||||
@@ -239,7 +239,7 @@
|
||||
if (!props.session?.connected) {
|
||||
return;
|
||||
}
|
||||
props.session?.remove([path]);
|
||||
emits('deleteFile', [path]);
|
||||
};
|
||||
|
||||
// 下载文件
|
||||
@@ -248,7 +248,7 @@
|
||||
if (!props.session?.connected) {
|
||||
return;
|
||||
}
|
||||
emits('download', [path]);
|
||||
emits('download', [path], false);
|
||||
};
|
||||
|
||||
// 移动文件
|
||||
|
||||
@@ -4,16 +4,17 @@
|
||||
title-align="start"
|
||||
title="文件上传"
|
||||
ok-text="上传"
|
||||
:body-style="{ padding: '20px' }"
|
||||
:body-style="{ padding: 0 }"
|
||||
:align-center="false"
|
||||
:mask-closable="false"
|
||||
:unmount-on-close="true"
|
||||
:on-before-ok="handlerOk"
|
||||
@cancel="handleClose">
|
||||
<div class="upload-container">
|
||||
<div class="parent-wrapper mb16">
|
||||
<span class="parent-label">上传至文件夹:</span>
|
||||
<a-input class="parent-input"
|
||||
<!-- 上传目录 -->
|
||||
<div class="item-wrapper">
|
||||
<div class="form-item">
|
||||
<span class="item-label">上传至文件夹</span>
|
||||
<a-input class="item-input"
|
||||
v-model="parentPath"
|
||||
placeholder="上传目录" />
|
||||
</div>
|
||||
@@ -80,8 +81,8 @@
|
||||
import { ref } from 'vue';
|
||||
import { useTerminalStore } from '@/store';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import { getFileSize } from '@/utils/file';
|
||||
import useVisible from '@/hooks/visible';
|
||||
|
||||
const { visible, setVisible } = useVisible();
|
||||
const { transferManager } = useTerminalStore();
|
||||
@@ -100,7 +101,7 @@
|
||||
defineExpose({ open });
|
||||
|
||||
// 确定
|
||||
const handlerOk = () => {
|
||||
const handlerOk = async () => {
|
||||
if (!parentPath.value) {
|
||||
Message.error('请输入上传目录');
|
||||
return false;
|
||||
@@ -109,8 +110,9 @@
|
||||
Message.error('请选择文件');
|
||||
return false;
|
||||
}
|
||||
// 添加到上传列表
|
||||
// 获取上传的文件
|
||||
const files = fileList.value.map(s => s.file as File);
|
||||
// 上传
|
||||
transferManager.addUpload(hostId.value, parentPath.value, files);
|
||||
Message.success('已开始上传, 点击右侧传输列表查看进度');
|
||||
// 清空
|
||||
@@ -132,25 +134,37 @@
|
||||
|
||||
<style lang="less" scoped>
|
||||
@file-size-width: 82px;
|
||||
@item-label: 104px;
|
||||
|
||||
.upload-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.parent-wrapper {
|
||||
.item-wrapper {
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.parent-label {
|
||||
width: 98px;
|
||||
}
|
||||
|
||||
.parent-input {
|
||||
width: 386px;
|
||||
.item-label {
|
||||
width: @item-label;
|
||||
padding-right: 8px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
user-select: none;
|
||||
|
||||
&:after {
|
||||
content: ':';
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.item-input {
|
||||
width: 376px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.file-list-uploader {
|
||||
margin-top: 24px;
|
||||
|
||||
@@ -160,7 +174,7 @@
|
||||
|
||||
:deep(.arco-upload-list) {
|
||||
padding: 0 12px 0 0;
|
||||
max-height: calc(100vh - 386px);
|
||||
max-height: calc(100vh - 496px);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -11,13 +11,14 @@
|
||||
:hide-icon="true">
|
||||
<!-- 表头 -->
|
||||
<sftp-table-header class="sftp-table-header"
|
||||
v-model:selected-files="selectFiles"
|
||||
:selected-files="selectFiles"
|
||||
:close-message="closeMessage"
|
||||
:current-path="currentPath"
|
||||
:session="session"
|
||||
@load-file="loadFiles"
|
||||
@download="downloadFiles"
|
||||
@set-loading="setTableLoading" />
|
||||
@set-loading="setTableLoading"
|
||||
@delete-file="deleteFile"
|
||||
@download="downloadFiles" />
|
||||
<!-- 表格 -->
|
||||
<sftp-table class="sftp-table-wrapper"
|
||||
v-model:selected-files="selectFiles"
|
||||
@@ -26,6 +27,7 @@
|
||||
:loading="tableLoading"
|
||||
@load-file="loadFiles"
|
||||
@edit-file="editFile"
|
||||
@delete-file="deleteFile"
|
||||
@download="downloadFiles" />
|
||||
</a-spin>
|
||||
</template>
|
||||
@@ -145,17 +147,30 @@
|
||||
editorFilePath.value = '';
|
||||
};
|
||||
|
||||
// 下载文件
|
||||
const downloadFiles = (paths: Array<string>) => {
|
||||
// 删除文件
|
||||
const deleteFile = (paths: Array<string>) => {
|
||||
if (!paths.length) {
|
||||
return;
|
||||
}
|
||||
// 删除
|
||||
selectFiles.value = [];
|
||||
session.value?.remove(paths);
|
||||
};
|
||||
|
||||
// 下载文件
|
||||
const downloadFiles = (paths: Array<string>, clear: boolean) => {
|
||||
if (!paths.length) {
|
||||
return;
|
||||
}
|
||||
Message.success('已开始下载, 点击右侧传输列表查看进度');
|
||||
// 映射为文件
|
||||
const files = fileList.value.filter(s => paths.includes(s.path))
|
||||
.map(s => {
|
||||
return { ...s };
|
||||
});
|
||||
if (clear) {
|
||||
selectFiles.value = [];
|
||||
}
|
||||
Message.success('已开始下载, 点击右侧传输列表查看进度');
|
||||
// 添加普通文件到下载队列
|
||||
const normalFiles = files.filter(s => !s.isDir);
|
||||
transferManager.addDownload(props.tab.hostId as number, currentPath.value, normalFiles);
|
||||
@@ -193,9 +208,9 @@
|
||||
};
|
||||
|
||||
// 接收列表回调
|
||||
const resolveList = (result: string, path: string, list: Array<SftpFile>) => {
|
||||
const resolveList = (path: string, result: string, msg: string, list: Array<SftpFile>) => {
|
||||
setTableLoading(false);
|
||||
if (!checkResult(result, '查询失败')) {
|
||||
if (!checkResult(result, msg)) {
|
||||
return;
|
||||
}
|
||||
currentPath.value = path;
|
||||
@@ -216,11 +231,11 @@
|
||||
};
|
||||
|
||||
// 接收获取文件内容响应
|
||||
const resolveSftpGetContent = (path: string, result: string, content: string) => {
|
||||
const resolveSftpGetContent = (path: string, result: string, msg: string, content: string) => {
|
||||
setTableLoading(false);
|
||||
setEditorLoading(false);
|
||||
// 检查结果
|
||||
if (!checkResult(result, '加载失败')) {
|
||||
if (!checkResult(result, msg)) {
|
||||
return;
|
||||
}
|
||||
editorRef.value?.setValue(content);
|
||||
@@ -237,8 +252,12 @@
|
||||
};
|
||||
|
||||
// 接收下载文件夹展开文件响应
|
||||
const resolveDownloadFlatDirectory = (currentPath: string, list: Array<SftpFile>) => {
|
||||
const resolveDownloadFlatDirectory = (currentPath: string, result: string, msg: string, list: Array<SftpFile>) => {
|
||||
setTableLoading(false);
|
||||
// 检查结果
|
||||
if (!checkResult(result, msg)) {
|
||||
return;
|
||||
}
|
||||
transferManager.addDownload(props.tab.hostId as number, currentPath, list);
|
||||
};
|
||||
|
||||
@@ -273,7 +292,7 @@
|
||||
|
||||
.sftp-container {
|
||||
width: 100%;
|
||||
height: calc(100vh - var(--header-height) - var(--panel-nav-height));
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
.split-view {
|
||||
|
||||
@@ -40,7 +40,7 @@ const columns = [
|
||||
sortable: {
|
||||
sortDirections: ['ascend', 'descend'],
|
||||
},
|
||||
width: 234,
|
||||
width: 264,
|
||||
cellClass: 'action-cell',
|
||||
},
|
||||
] as TableColumnData[];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ISftpSession, ISftpSessionResolver, ITerminalChannel, TerminalPanelTabItem } from '../types/terminal.type';
|
||||
import { InputProtocol } from '../types/terminal.protocol';
|
||||
import { InputProtocol } from '@/types/protocol/terminal.protocol';
|
||||
import { PanelSessionType } from '../types/terminal.const';
|
||||
import { Modal } from '@arco-design/web-vue';
|
||||
import BaseSession from './base-session';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SftpTransferItem } from '../types/terminal.type';
|
||||
import { TransferStatus, TransferType } from '../types/terminal.const';
|
||||
import { TransferStatus } from '../types/terminal.const';
|
||||
import { getFileName, openDownloadFile } from '@/utils/file';
|
||||
import { saveAs } from 'file-saver';
|
||||
import { getDownloadTransferUrl } from '@/api/asset/host-sftp';
|
||||
@@ -8,8 +8,8 @@ import SftpTransferHandler from './sftp-transfer-handler';
|
||||
// sftp 下载器实现
|
||||
export default class SftpTransferDownloader extends SftpTransferHandler {
|
||||
|
||||
constructor(item: SftpTransferItem, client: WebSocket) {
|
||||
super(TransferType.DOWNLOAD, item, client);
|
||||
constructor(type: string, item: SftpTransferItem, client: WebSocket) {
|
||||
super(type, item, client);
|
||||
}
|
||||
|
||||
// 开始回调
|
||||
|
||||
@@ -27,7 +27,7 @@ export default abstract class SftpTransferHandler implements ISftpTransferHandle
|
||||
operator: TransferOperator.START,
|
||||
type: this.type,
|
||||
path: getPath(this.item.parentPath + '/' + this.item.name),
|
||||
hostId: this.item.hostId
|
||||
hostId: this.item.hostId,
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -81,9 +81,14 @@ export default abstract class SftpTransferHandler implements ISftpTransferHandle
|
||||
};
|
||||
|
||||
// 进度回调
|
||||
onProgress(size: number) {
|
||||
if (this.item && size) {
|
||||
this.item.currentSize = size;
|
||||
onProgress(totalSize: number | undefined, currentSize: number | undefined) {
|
||||
if (this.item) {
|
||||
if (totalSize) {
|
||||
this.item.totalSize = totalSize;
|
||||
}
|
||||
if (currentSize) {
|
||||
this.item.currentSize = currentSize;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export default class SftpTransferManager implements ISftpTransferManager {
|
||||
|
||||
private run: boolean;
|
||||
|
||||
private progressIntervalId?: number;
|
||||
private progressIntervalId?: any;
|
||||
|
||||
private currentItem?: SftpTransferItem;
|
||||
|
||||
@@ -45,9 +45,7 @@ export default class SftpTransferManager implements ISftpTransferManager {
|
||||
});
|
||||
this.transferList.push(...items);
|
||||
// 开始传输
|
||||
if (!this.run) {
|
||||
this.openClient();
|
||||
}
|
||||
this.startTransfer(items);
|
||||
}
|
||||
|
||||
// 添加下载任务
|
||||
@@ -67,6 +65,12 @@ export default class SftpTransferManager implements ISftpTransferManager {
|
||||
status: TransferStatus.WAITING,
|
||||
};
|
||||
}) as Array<SftpTransferItem>;
|
||||
// 开始传输
|
||||
this.startTransfer(items);
|
||||
}
|
||||
|
||||
// 开始传输
|
||||
startTransfer(items: Array<SftpTransferItem>) {
|
||||
this.transferList.push(...items);
|
||||
// 开始传输
|
||||
if (!this.run) {
|
||||
@@ -154,13 +158,8 @@ export default class SftpTransferManager implements ISftpTransferManager {
|
||||
// 获取任务
|
||||
this.currentItem = this.transferList.find(s => s.status === TransferStatus.WAITING);
|
||||
if (this.currentItem) {
|
||||
if (this.currentItem.type === TransferType.UPLOAD) {
|
||||
// 上传
|
||||
this.currentTransfer = new SftpTransferUploader(this.currentItem, this.client as WebSocket);
|
||||
} else {
|
||||
// 下载
|
||||
this.currentTransfer = new SftpTransferDownloader(this.currentItem, this.client as WebSocket);
|
||||
}
|
||||
// 创建传输器
|
||||
this.currentTransfer = this.createTransfer();
|
||||
// 开始
|
||||
this.currentTransfer?.start();
|
||||
} else {
|
||||
@@ -169,6 +168,20 @@ export default class SftpTransferManager implements ISftpTransferManager {
|
||||
}
|
||||
}
|
||||
|
||||
// 创建传输器
|
||||
private createTransfer(): ISftpTransferHandler | undefined {
|
||||
if (!this.currentItem) {
|
||||
return undefined;
|
||||
}
|
||||
if (this.currentItem.type === TransferType.UPLOAD) {
|
||||
// 上传
|
||||
return new SftpTransferUploader(TransferType.UPLOAD, this.currentItem, this.client as WebSocket);
|
||||
} else if (this.currentItem.type === TransferType.DOWNLOAD) {
|
||||
// 下载
|
||||
return new SftpTransferDownloader(TransferType.DOWNLOAD, this.currentItem, this.client as WebSocket);
|
||||
}
|
||||
}
|
||||
|
||||
// 接收消息
|
||||
private async resolveMessage(message: MessageEvent) {
|
||||
// 文本消息
|
||||
@@ -181,7 +194,7 @@ export default class SftpTransferManager implements ISftpTransferManager {
|
||||
this.currentTransfer?.onStart(data.channelId as string, data.transferToken as string);
|
||||
} else if (data.type === TransferReceiver.PROGRESS) {
|
||||
// 进度回调
|
||||
this.currentTransfer?.onProgress(data.currentSize as number);
|
||||
this.currentTransfer?.onProgress(data.totalSize, data.currentSize);
|
||||
} else if (data.type === TransferReceiver.FINISH) {
|
||||
// 完成回调
|
||||
this.currentTransfer?.onFinish();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { SftpTransferItem } from '../types/terminal.type';
|
||||
import { TransferType } from '../types/terminal.const';
|
||||
import SftpTransferHandler from './sftp-transfer-handler';
|
||||
|
||||
// 512 KB
|
||||
@@ -12,8 +11,8 @@ export default class SftpTransferUploader extends SftpTransferHandler {
|
||||
private readonly totalPart: number;
|
||||
private file: File;
|
||||
|
||||
constructor(item: SftpTransferItem, client: WebSocket) {
|
||||
super(TransferType.UPLOAD, item, client);
|
||||
constructor(type: string, item: SftpTransferItem, client: WebSocket) {
|
||||
super(type, item, client);
|
||||
this.file = item.file;
|
||||
this.currentPart = 0;
|
||||
this.totalPart = Math.ceil(item.file.size / PART_SIZE);
|
||||
|
||||
@@ -2,10 +2,12 @@ import type { UnwrapRef } from 'vue';
|
||||
import type { ISearchOptions } from '@xterm/addon-search';
|
||||
import { SearchAddon } from '@xterm/addon-search';
|
||||
import type { TerminalPreference } from '@/store/modules/terminal/types';
|
||||
import type { ISshSession, ISshSessionHandler, ITerminalChannel, TerminalPanelTabItem, XtermAddons, XtermDomRef } from '../types/terminal.type';
|
||||
import type { ISshSession, ISshSessionHandler, ITerminalChannel, TerminalPanelTabItem, XtermDomRef } from '../types/terminal.type';
|
||||
import type { XtermAddons } from '@/types/xterm';
|
||||
import { defaultFontFamily } from '@/types/xterm';
|
||||
import { useTerminalStore } from '@/store';
|
||||
import { InputProtocol } from '../types/terminal.protocol';
|
||||
import { fontFamilySuffix, PanelSessionType, TerminalShortcutType, TerminalStatus, } from '../types/terminal.const';
|
||||
import { InputProtocol } from '@/types/protocol/terminal.protocol';
|
||||
import { PanelSessionType, TerminalShortcutType, TerminalStatus } from '../types/terminal.const';
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
import { FitAddon } from '@xterm/addon-fit';
|
||||
import { WebLinksAddon } from '@xterm/addon-web-links';
|
||||
@@ -44,6 +46,7 @@ export default class SshSession extends BaseSession implements ISshSession {
|
||||
// 初始化
|
||||
init(domRef: XtermDomRef): void {
|
||||
const { preference } = useTerminalStore();
|
||||
const fontFamily = preference.displaySetting.fontFamily;
|
||||
// 初始化实例
|
||||
this.inst = new Terminal({
|
||||
...(preference.displaySetting as any),
|
||||
@@ -52,7 +55,7 @@ export default class SshSession extends BaseSession implements ISshSession {
|
||||
altClickMovesCursor: !!preference.interactSetting.altClickMovesCursor,
|
||||
rightClickSelectsWord: !!preference.interactSetting.rightClickSelectsWord,
|
||||
wordSeparator: preference.interactSetting.wordSeparator,
|
||||
fontFamily: preference.displaySetting.fontFamily + fontFamilySuffix,
|
||||
fontFamily: fontFamily === '_' ? defaultFontFamily : `${fontFamily}, ${defaultFontFamily}`,
|
||||
scrollback: preference.sessionSetting.scrollBackLine,
|
||||
allowProposedApi: true,
|
||||
});
|
||||
@@ -219,6 +222,11 @@ export default class SshSession extends BaseSession implements ISshSession {
|
||||
this.inst.write(value);
|
||||
}
|
||||
|
||||
// 修改大小
|
||||
resize(cols: number, rows: number): void {
|
||||
this.inst.resize(cols, rows);
|
||||
}
|
||||
|
||||
// 聚焦
|
||||
focus(): void {
|
||||
this.inst.focus();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { InputPayload, ITerminalChannel, ITerminalOutputProcessor, ITerminalSessionManager, Protocol, } from '../types/terminal.type';
|
||||
import { format, OutputProtocol, parse } from '../types/terminal.protocol';
|
||||
import type { ITerminalChannel, ITerminalOutputProcessor, ITerminalSessionManager } from '../types/terminal.type';
|
||||
import type { InputPayload, Protocol } from '@/types/protocol/terminal.protocol';
|
||||
import { format, OutputProtocol, parse } from '@/types/protocol/terminal.protocol';
|
||||
import { sessionCloseMsg } from '../types/terminal.const';
|
||||
import { getTerminalAccessToken, openHostTerminalChannel } from '@/api/asset/host-terminal';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import type {
|
||||
ISftpSession,
|
||||
ISshSession,
|
||||
ITerminalChannel,
|
||||
ITerminalOutputProcessor,
|
||||
ITerminalSession,
|
||||
ITerminalSessionManager,
|
||||
OutputPayload
|
||||
} from '../types/terminal.type';
|
||||
import { InputProtocol } from '../types/terminal.protocol';
|
||||
import type { ISftpSession, ISshSession, ITerminalChannel, ITerminalOutputProcessor, ITerminalSession, ITerminalSessionManager } from '../types/terminal.type';
|
||||
import type { OutputPayload } from '@/types/protocol/terminal.protocol';
|
||||
import { InputProtocol } from '@/types/protocol/terminal.protocol';
|
||||
import { PanelSessionType, TerminalStatus } from '../types/terminal.const';
|
||||
import { useTerminalStore } from '@/store';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
@@ -43,7 +36,7 @@ export default class TerminalOutputProcessor implements ITerminalOutputProcessor
|
||||
});
|
||||
} else {
|
||||
// 未成功展示错误信息
|
||||
ssh.write(`[91m${msg || ''}\r\n输入回车重新连接...[0m\r\n\r\n`);
|
||||
ssh.write(`[91m${msg || ''}[0m\r\n[91m输入回车重新连接...[0m\r\n\r\n`);
|
||||
ssh.status = TerminalStatus.CLOSED;
|
||||
}
|
||||
}, sftp => {
|
||||
@@ -109,7 +102,7 @@ export default class TerminalOutputProcessor implements ITerminalOutputProcessor
|
||||
// ssh 拼接关闭消息
|
||||
ssh.write(`\r\n\r\n[91m${msg || ''}[0m\r\n`);
|
||||
if (!isForceClose) {
|
||||
ssh.write(`[91m${msg || ''}[0m\r\n[91m输入回车重新连接...[0m\r\n\r\n`);
|
||||
ssh.write('[91m输入回车重新连接...[0m\r\n\r\n');
|
||||
}
|
||||
// 设置状态
|
||||
ssh.status = TerminalStatus.CLOSED;
|
||||
@@ -135,10 +128,10 @@ export default class TerminalOutputProcessor implements ITerminalOutputProcessor
|
||||
}
|
||||
|
||||
// 处理 SFTP 文件列表
|
||||
processSftpList({ sessionId, result, path, body }: OutputPayload): void {
|
||||
processSftpList({ sessionId, result, path, msg, body }: OutputPayload): void {
|
||||
// 获取会话
|
||||
const session = this.sessionManager.getSession<ISftpSession>(sessionId);
|
||||
session && session.resolver.resolveList(result, path, JSON.parse(body));
|
||||
session && session.resolver.resolveList(path, result, msg, JSON.parse(body));
|
||||
}
|
||||
|
||||
// 处理 SFTP 创建文件夹
|
||||
@@ -177,17 +170,17 @@ export default class TerminalOutputProcessor implements ITerminalOutputProcessor
|
||||
}
|
||||
|
||||
// 处理 SFTP 下载文件夹展开文件
|
||||
processDownloadFlatDirectory({ sessionId, currentPath, body }: OutputPayload): void {
|
||||
processDownloadFlatDirectory({ sessionId, currentPath, result, msg, body }: OutputPayload): void {
|
||||
// 获取会话
|
||||
const session = this.sessionManager.getSession<ISftpSession>(sessionId);
|
||||
session && session.resolver.resolveDownloadFlatDirectory(currentPath, JSON.parse(body));
|
||||
session && session.resolver.resolveDownloadFlatDirectory(currentPath, result, msg, JSON.parse(body));
|
||||
}
|
||||
|
||||
// 处理 SFTP 获取文件内容
|
||||
processSftpGetContent({ sessionId, path, result, content }: OutputPayload): void {
|
||||
processSftpGetContent({ sessionId, path, result, msg, content }: OutputPayload): void {
|
||||
// 获取会话
|
||||
const session = this.sessionManager.getSession<ISftpSession>(sessionId);
|
||||
session && session.resolver.resolveSftpGetContent(path, result, content);
|
||||
session && session.resolver.resolveSftpGetContent(path, result, msg, content);
|
||||
}
|
||||
|
||||
// 处理 SFTP 修改文件内容
|
||||
|
||||
@@ -4,10 +4,9 @@ import TerminalTabManager from '../handler/terminal-tab-manager';
|
||||
// 终端面板管理器实现
|
||||
export default class TerminalPanelManager implements ITerminalPanelManager {
|
||||
|
||||
// 当前面板
|
||||
active: number;
|
||||
// 面板列表
|
||||
panels: Array<TerminalTabManager<TerminalPanelTabItem>>;
|
||||
public active: number;
|
||||
|
||||
public panels: Array<TerminalTabManager<TerminalPanelTabItem>>;
|
||||
|
||||
constructor() {
|
||||
this.active = 0;
|
||||
@@ -31,17 +30,18 @@ export default class TerminalPanelManager implements ITerminalPanelManager {
|
||||
|
||||
// 移除面板
|
||||
removePanel(index: number) {
|
||||
this.panels.splice(index, 1);
|
||||
this.panels[index].clear();
|
||||
this.active = index >= this.panels.length ? this.panels.length - 1 : index;
|
||||
};
|
||||
|
||||
// 重置
|
||||
reset() {
|
||||
this.active = 0;
|
||||
if (this.panels) {
|
||||
for (let panel of this.panels) {
|
||||
panel.clear();
|
||||
}
|
||||
this.active = 0;
|
||||
this.panels = [new TerminalTabManager()];
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
XtermDomRef
|
||||
} from '../types/terminal.type';
|
||||
import { sleep } from '@/utils';
|
||||
import { InputProtocol } from '../types/terminal.protocol';
|
||||
import { InputProtocol } from '@/types/protocol/terminal.protocol';
|
||||
import { PanelSessionType } from '../types/terminal.const';
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
import { addEventListen, removeEventListen } from '@/utils/event';
|
||||
@@ -132,7 +132,7 @@ export default class TerminalSessionManager implements ITerminalSessionManager {
|
||||
}
|
||||
|
||||
// 调度重置大小
|
||||
private dispatchResize() {
|
||||
dispatchResize() {
|
||||
// 对所有已连接的会话重置大小
|
||||
Object.values(this.sessions)
|
||||
.filter(s => s?.type === PanelSessionType.SSH.type)
|
||||
|
||||
@@ -56,8 +56,8 @@ export const NewConnectionType = {
|
||||
|
||||
// 主机额外配置项
|
||||
export const ExtraSettingItems = {
|
||||
SSH: 'ssh',
|
||||
LABEL: 'label',
|
||||
SSH: 'SSH',
|
||||
LABEL: 'LABEL',
|
||||
};
|
||||
|
||||
// 主机额外配置 ssh 认证方式
|
||||
@@ -324,7 +324,7 @@ export const TransferStatus = {
|
||||
// 传输类型
|
||||
export const TransferType = {
|
||||
UPLOAD: 'upload',
|
||||
DOWNLOAD: 'download'
|
||||
DOWNLOAD: 'download',
|
||||
};
|
||||
|
||||
// 传输操作
|
||||
@@ -363,9 +363,6 @@ export const openSftpChmodModalKey = Symbol();
|
||||
// 打开 sftpUploadModal key
|
||||
export const openSftpUploadModalKey = Symbol();
|
||||
|
||||
// 字体后缀 兜底
|
||||
export const fontFamilySuffix = ', Courier New, Monaco, courier, monospace';
|
||||
|
||||
// 终端字体样式
|
||||
export const fontFamilyKey = 'terminalFontFamily';
|
||||
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import type { Terminal } from '@xterm/xterm';
|
||||
import type { FitAddon } from '@xterm/addon-fit';
|
||||
import type { CanvasAddon } from '@xterm/addon-canvas';
|
||||
import type { WebglAddon } from '@xterm/addon-webgl';
|
||||
import type { WebLinksAddon } from '@xterm/addon-web-links';
|
||||
import type { ISearchOptions, SearchAddon } from '@xterm/addon-search';
|
||||
import type { ImageAddon } from '@xterm/addon-image';
|
||||
import type { Unicode11Addon } from '@xterm/addon-unicode11';
|
||||
import type { ISearchOptions } from '@xterm/addon-search';
|
||||
import type { CSSProperties } from 'vue';
|
||||
import type { HostQueryResponse } from '@/api/asset/host';
|
||||
import type { InputPayload, OutputPayload, Protocol } from '@/types/protocol/terminal.protocol';
|
||||
|
||||
// 终端 tab 元素
|
||||
export interface TerminalTabItem {
|
||||
@@ -81,30 +76,6 @@ export interface PanelSessionTabType {
|
||||
icon: string;
|
||||
}
|
||||
|
||||
// 终端协议
|
||||
export interface Protocol {
|
||||
type: string;
|
||||
template: string[];
|
||||
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// 终端输入消息内容
|
||||
export interface InputPayload {
|
||||
type?: string;
|
||||
sessionId?: string;
|
||||
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// 终端输出消息内容
|
||||
export interface OutputPayload {
|
||||
type: string;
|
||||
sessionId: string;
|
||||
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
// 终端 tab 管理器定义
|
||||
export interface ITerminalTabManager<T extends TerminalTabItem = TerminalTabItem> {
|
||||
// 当前 tab
|
||||
@@ -166,6 +137,8 @@ export interface ITerminalSessionManager {
|
||||
getSession: <T extends ITerminalSession>(sessionId: string) => T;
|
||||
// 关闭终端会话
|
||||
closeSession: (sessionId: string) => void;
|
||||
// 重置大小
|
||||
dispatchResize: () => void;
|
||||
// 重置
|
||||
reset: () => void;
|
||||
}
|
||||
@@ -221,17 +194,6 @@ export interface XtermDomRef {
|
||||
editorModal: any;
|
||||
}
|
||||
|
||||
// xterm 插件
|
||||
export interface XtermAddons {
|
||||
fit: FitAddon;
|
||||
webgl: WebglAddon;
|
||||
canvas: CanvasAddon;
|
||||
weblink: WebLinksAddon;
|
||||
search: SearchAddon;
|
||||
image: ImageAddon;
|
||||
unicode: Unicode11Addon;
|
||||
}
|
||||
|
||||
// 终端会话定义
|
||||
export interface ITerminalSession {
|
||||
type: string;
|
||||
@@ -269,6 +231,8 @@ export interface ISshSession extends ITerminalSession {
|
||||
init: (domRef: XtermDomRef) => void;
|
||||
// 写入数据
|
||||
write: (value: string) => void;
|
||||
// 修改大小
|
||||
resize: (cols: number, rows: number) => void;
|
||||
// 聚焦
|
||||
focus: () => void;
|
||||
// 失焦
|
||||
@@ -368,7 +332,7 @@ export interface ISftpSessionResolver {
|
||||
// 关闭回调
|
||||
onClose: (forceClose: boolean, msg: string) => void;
|
||||
// 接受文件列表响应
|
||||
resolveList: (result: string, path: string, list: Array<SftpFile>) => void;
|
||||
resolveList: (path: string, result: string, msg: string, list: Array<SftpFile>) => void;
|
||||
// 接收创建文件夹响应
|
||||
resolveSftpMkdir: (result: string, msg: string) => void;
|
||||
// 接收创建文件响应
|
||||
@@ -380,9 +344,9 @@ export interface ISftpSessionResolver {
|
||||
// 接收修改文件权限响应
|
||||
resolveSftpChmod: (result: string, msg: string) => void;
|
||||
// 接收下载文件夹展开文件响应
|
||||
resolveDownloadFlatDirectory: (currentPath: string, list: Array<SftpFile>) => void;
|
||||
resolveDownloadFlatDirectory: (currentPath: string, result: string, msg: string, list: Array<SftpFile>) => void;
|
||||
// 接收获取文件内容响应
|
||||
resolveSftpGetContent: (path: string, result: string, content: string) => void;
|
||||
resolveSftpGetContent: (path: string, result: string, msg: string, content: string) => void;
|
||||
// 接收修改文件内容响应
|
||||
resolveSftpSetContent: (result: string, msg: string) => void;
|
||||
}
|
||||
@@ -414,7 +378,6 @@ export interface ISftpTransferManager {
|
||||
cancelAllTransfer: () => void;
|
||||
}
|
||||
|
||||
|
||||
// sftp 传输处理回调定义
|
||||
export interface ISftpTransferCallback {
|
||||
// 下一分片回调
|
||||
@@ -422,7 +385,7 @@ export interface ISftpTransferCallback {
|
||||
// 开始回调
|
||||
onStart: (channelId: string, token: string) => void;
|
||||
// 进度回调
|
||||
onProgress: (size: number) => void;
|
||||
onProgress: (totalSize: number | undefined, currentSize: number | undefined) => void;
|
||||
// 失败回调
|
||||
onError: (msg: string | undefined) => void;
|
||||
// 完成回调
|
||||
@@ -475,4 +438,5 @@ export interface TransferOperatorResponse {
|
||||
transferToken?: string;
|
||||
success: boolean;
|
||||
msg?: string;
|
||||
totalSize?: number;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
@selected="(e) => drawer.setWithTemplate(e)" />
|
||||
<!-- 主机模态框 -->
|
||||
<authorized-host-modal ref="hostModal"
|
||||
type="SSH"
|
||||
@selected="(e) => drawer.setSelectedHost(e)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
<!-- 额外参数 -->
|
||||
<template #extraSchema="{ record }">
|
||||
<template v-if="record.extraSchema">
|
||||
<a-space>
|
||||
<a-space style="margin-bottom: -8px;" :wrap="true">
|
||||
<template v-for="item in JSON.parse(record.extraSchema)" :key="item.name">
|
||||
<a-tag :color="getDictValue(dictValueTypeKey, item.type, 'color')">
|
||||
{{ item.name }}
|
||||
|
||||
@@ -6,7 +6,7 @@ const columns = [
|
||||
title: 'id',
|
||||
dataIndex: 'id',
|
||||
slotName: 'id',
|
||||
width: 80,
|
||||
width: 68,
|
||||
align: 'left',
|
||||
fixed: 'left',
|
||||
}, {
|
||||
|
||||
Reference in New Issue
Block a user