mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-19 08:19:15 +08:00
feat: 网站分组修改
This commit is contained in:
parent
0e3d9d1f3f
commit
a2fb8353de
@ -189,7 +189,11 @@ func (b *BaseApi) ContainerExec(c *gin.Context) {
|
|||||||
if wshandleError(wsConn, errors.WithMessage(err, "New docker client failed.")) {
|
if wshandleError(wsConn, errors.WithMessage(err, "New docker client failed.")) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
conf := types.ExecConfig{Tty: true, Cmd: []string{command}, AttachStderr: true, AttachStdin: true, AttachStdout: true, User: user}
|
|
||||||
|
conf := types.ExecConfig{Tty: true, Cmd: []string{command}, AttachStderr: true, AttachStdin: true, AttachStdout: true}
|
||||||
|
if len(user) != 0 {
|
||||||
|
conf.User = user
|
||||||
|
}
|
||||||
ir, err := client.ContainerExecCreate(context.TODO(), containerID, conf)
|
ir, err := client.ContainerExecCreate(context.TODO(), containerID, conf)
|
||||||
if wshandleError(wsConn, errors.WithMessage(err, "failed to set exec conf.")) {
|
if wshandleError(wsConn, errors.WithMessage(err, "failed to set exec conf.")) {
|
||||||
return
|
return
|
||||||
|
@ -301,6 +301,7 @@ export default {
|
|||||||
last10Min: 'Last 10 Minutes',
|
last10Min: 'Last 10 Minutes',
|
||||||
|
|
||||||
custom: 'Custom',
|
custom: 'Custom',
|
||||||
|
emptyUser: 'When empty, you will log in with the default user of container',
|
||||||
containerTerminal: 'Container terminal',
|
containerTerminal: 'Container terminal',
|
||||||
|
|
||||||
containerCreate: 'Container create',
|
containerCreate: 'Container create',
|
||||||
@ -610,6 +611,9 @@ export default {
|
|||||||
safeEntranceHelper:
|
safeEntranceHelper:
|
||||||
'Panel management portal. You can log in to the panel only through a specified security portal, for example: onepanel',
|
'Panel management portal. You can log in to the panel only through a specified security portal, for example: onepanel',
|
||||||
expirationTime: 'Expiration Time',
|
expirationTime: 'Expiration Time',
|
||||||
|
unSetting: 'Not set',
|
||||||
|
noneSetting:
|
||||||
|
'Set the expiration time for the panel password. After the expiration, you need to reset the password',
|
||||||
days: 'Expiration Days',
|
days: 'Expiration Days',
|
||||||
expiredHelper: 'The current password has expired. Please change the password again.',
|
expiredHelper: 'The current password has expired. Please change the password again.',
|
||||||
timeoutHelper:
|
timeoutHelper:
|
||||||
|
@ -307,6 +307,7 @@ export default {
|
|||||||
|
|
||||||
custom: '自定义',
|
custom: '自定义',
|
||||||
containerTerminal: '容器终端',
|
containerTerminal: '容器终端',
|
||||||
|
emptyUser: '为空时,将使用容器默认的用户登录',
|
||||||
|
|
||||||
containerCreate: '容器创建',
|
containerCreate: '容器创建',
|
||||||
port: '端口',
|
port: '端口',
|
||||||
@ -625,6 +626,8 @@ export default {
|
|||||||
safeEntrance: '安全入口',
|
safeEntrance: '安全入口',
|
||||||
safeEntranceHelper: '面板管理入口,设置后只能通过指定安全入口登录面板,如: onepanel',
|
safeEntranceHelper: '面板管理入口,设置后只能通过指定安全入口登录面板,如: onepanel',
|
||||||
expirationTime: '密码过期时间',
|
expirationTime: '密码过期时间',
|
||||||
|
unSetting: '未设置',
|
||||||
|
noneSetting: '为面板密码设置过期时间,过期后需要重新设置密码',
|
||||||
days: '过期天数',
|
days: '过期天数',
|
||||||
expiredHelper: '当前密码已过期,请重新修改密码:',
|
expiredHelper: '当前密码已过期,请重新修改密码:',
|
||||||
timeoutHelper: '【 {0} 天后 】面板密码即将过期,过期后需要重新设置密码',
|
timeoutHelper: '【 {0} 天后 】面板密码即将过期,过期后需要重新设置密码',
|
||||||
|
@ -10,14 +10,6 @@ const hostRouter = {
|
|||||||
title: 'menu.host',
|
title: 'menu.host',
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
// {
|
|
||||||
// path: '/hosts/security',
|
|
||||||
// name: 'Security',
|
|
||||||
// component: () => import('@/views/host/security/index.vue'),
|
|
||||||
// meta: {
|
|
||||||
// title: 'menu.security',
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
path: '/hosts/files',
|
path: '/hosts/files',
|
||||||
name: 'File',
|
name: 'File',
|
||||||
@ -35,9 +27,29 @@ const hostRouter = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/host/terminal',
|
path: '/hosts/terminal',
|
||||||
name: 'Terminal',
|
name: 'Terminal',
|
||||||
component: () => import('@/views/host/terminal/index.vue'),
|
component: () => import('@/views/host/terminal/terminal/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: 'menu.terminal',
|
||||||
|
keepAlive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/hosts/host',
|
||||||
|
name: 'TerminalHost',
|
||||||
|
hidden: true,
|
||||||
|
component: () => import('@/views/host/terminal/host/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: 'menu.terminal',
|
||||||
|
keepAlive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/hosts/command',
|
||||||
|
name: 'TerminalCommand',
|
||||||
|
hidden: true,
|
||||||
|
component: () => import('@/views/host/terminal/command/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: 'menu.terminal',
|
title: 'menu.terminal',
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
|
@ -67,32 +67,7 @@
|
|||||||
</ComplexTable>
|
</ComplexTable>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-dialog v-model="detailVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="70%">
|
<CodemirrorDialog ref="mydetail" />
|
||||||
<template #header>
|
|
||||||
<div class="card-header">
|
|
||||||
<span>{{ $t('commons.button.view') }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<codemirror
|
|
||||||
:autofocus="true"
|
|
||||||
placeholder="None data"
|
|
||||||
:indent-with-tab="true"
|
|
||||||
:tabSize="4"
|
|
||||||
style="max-height: 500px"
|
|
||||||
:lineWrapping="true"
|
|
||||||
:matchBrackets="true"
|
|
||||||
theme="cobalt"
|
|
||||||
:styleActiveLine="true"
|
|
||||||
:extensions="extensions"
|
|
||||||
v-model="detailInfo"
|
|
||||||
:readOnly="true"
|
|
||||||
/>
|
|
||||||
<template #footer>
|
|
||||||
<span class="dialog-footer">
|
|
||||||
<el-button @click="detailVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
@close="onCloseLog"
|
@close="onCloseLog"
|
||||||
@ -174,6 +149,7 @@ import ComplexTable from '@/components/complex-table/index.vue';
|
|||||||
import CreateDialog from '@/views/container/container/create/index.vue';
|
import CreateDialog from '@/views/container/container/create/index.vue';
|
||||||
import MonitorDialog from '@/views/container/container/monitor/index.vue';
|
import MonitorDialog from '@/views/container/container/monitor/index.vue';
|
||||||
import TerminalDialog from '@/views/container/container/terminal/index.vue';
|
import TerminalDialog from '@/views/container/container/terminal/index.vue';
|
||||||
|
import CodemirrorDialog from '@/components/codemirror-dialog/codemirror.vue';
|
||||||
import Submenu from '@/views/container/index.vue';
|
import Submenu from '@/views/container/index.vue';
|
||||||
import { Codemirror } from 'vue-codemirror';
|
import { Codemirror } from 'vue-codemirror';
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
@ -201,8 +177,9 @@ const props = withDefaults(defineProps<Filters>(), {
|
|||||||
filters: '',
|
filters: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const detailVisiable = ref<boolean>(false);
|
|
||||||
const detailInfo = ref();
|
const detailInfo = ref();
|
||||||
|
const mydetail = ref();
|
||||||
|
|
||||||
const extensions = [javascript(), oneDark];
|
const extensions = [javascript(), oneDark];
|
||||||
const logVisiable = ref<boolean>(false);
|
const logVisiable = ref<boolean>(false);
|
||||||
const logInfo = ref();
|
const logInfo = ref();
|
||||||
@ -275,7 +252,11 @@ const onTerminal = (containerID: string) => {
|
|||||||
const onInspect = async (id: string) => {
|
const onInspect = async (id: string) => {
|
||||||
const res = await inspect({ id: id, type: 'container' });
|
const res = await inspect({ id: id, type: 'container' });
|
||||||
detailInfo.value = JSON.stringify(JSON.parse(res.data), null, 2);
|
detailInfo.value = JSON.stringify(JSON.parse(res.data), null, 2);
|
||||||
detailVisiable.value = true;
|
let param = {
|
||||||
|
header: i18n.global.t('commons.button.view'),
|
||||||
|
detailInfo: detailInfo.value,
|
||||||
|
};
|
||||||
|
mydetail.value!.acceptParams(param);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onLog = async (row: Container.ContainerInfo) => {
|
const onLog = async (row: Container.ContainerInfo) => {
|
||||||
@ -336,14 +317,14 @@ const checkStatus = (operation: string) => {
|
|||||||
return false;
|
return false;
|
||||||
case 'stop':
|
case 'stop':
|
||||||
for (const item of selects.value) {
|
for (const item of selects.value) {
|
||||||
if (item.state === 'stopped') {
|
if (item.state === 'stopped' || item.state === 'exited') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
case 'pause':
|
case 'pause':
|
||||||
for (const item of selects.value) {
|
for (const item of selects.value) {
|
||||||
if (item.state === 'paused') {
|
if (item.state === 'paused' || item.state === 'exited') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,8 +12,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-form ref="formRef" :model="form" label-width="80px">
|
<el-form ref="formRef" :model="form" label-width="80px">
|
||||||
<el-form-item label="User" prop="user" :rules="Rules.requiredInput">
|
<el-form-item label="User" prop="user">
|
||||||
<el-input style="width: 30%" clearable placeholder="root" v-model="form.user" />
|
<el-input style="width: 30%" clearable v-model="form.user" />
|
||||||
|
<span class="input-help">{{ $t('container.emptyUser') }}</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('container.custom')" prop="custom">
|
<el-form-item :label="$t('container.custom')" prop="custom">
|
||||||
<el-switch v-model="form.isCustom" @change="form.command = ''" />
|
<el-switch v-model="form.isCustom" @change="form.command = ''" />
|
||||||
@ -77,7 +78,7 @@ const acceptParams = async (params: DialogProps): Promise<void> => {
|
|||||||
terminalVisiable.value = true;
|
terminalVisiable.value = true;
|
||||||
form.containerID = params.containerID;
|
form.containerID = params.containerID;
|
||||||
form.isCustom = false;
|
form.isCustom = false;
|
||||||
form.user = 'root';
|
form.user = '';
|
||||||
form.command = '/bin/bash';
|
form.command = '/bin/bash';
|
||||||
terminalOpen.value = false;
|
terminalOpen.value = false;
|
||||||
window.addEventListener('resize', changeTerminalSize);
|
window.addEventListener('resize', changeTerminalSize);
|
||||||
|
@ -58,9 +58,8 @@ const search = async () => {
|
|||||||
pageSize: paginationConfig.pageSize,
|
pageSize: paginationConfig.pageSize,
|
||||||
};
|
};
|
||||||
await searchImageRepo(params).then((res) => {
|
await searchImageRepo(params).then((res) => {
|
||||||
if (res.data) {
|
data.value = res.data.items || [];
|
||||||
data.value = res.data.items;
|
paginationConfig.total = res.data.total;
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div style="margin: 20px">
|
<div>
|
||||||
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" :data="data" @search="search">
|
<Submenu activeName="command" />
|
||||||
<template #toolbar>
|
<el-card style="margin-top: 20px">
|
||||||
<el-button @click="onCreate()">{{ $t('commons.button.create') }}</el-button>
|
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" :data="data" @search="search">
|
||||||
<el-button type="danger" plain :disabled="selects.length === 0" @click="batchDelete(null)">
|
<template #toolbar>
|
||||||
{{ $t('commons.button.delete') }}
|
<el-button @click="onCreate()">{{ $t('commons.button.create') }}</el-button>
|
||||||
</el-button>
|
<el-button type="danger" plain :disabled="selects.length === 0" @click="batchDelete(null)">
|
||||||
</template>
|
{{ $t('commons.button.delete') }}
|
||||||
<el-table-column type="selection" fix />
|
</el-button>
|
||||||
<el-table-column :label="$t('commons.table.name')" min-width="100" prop="name" fix />
|
</template>
|
||||||
<el-table-column :label="$t('terminal.command')" min-width="300" show-overflow-tooltip prop="command" />
|
<el-table-column type="selection" fix />
|
||||||
<fu-table-operations type="icon" :buttons="buttons" :label="$t('commons.table.operate')" fix />
|
<el-table-column :label="$t('commons.table.name')" min-width="100" prop="name" fix />
|
||||||
</ComplexTable>
|
<el-table-column :label="$t('terminal.command')" min-width="300" show-overflow-tooltip prop="command" />
|
||||||
|
<fu-table-operations type="icon" :buttons="buttons" :label="$t('commons.table.operate')" fix />
|
||||||
|
</ComplexTable>
|
||||||
|
</el-card>
|
||||||
<el-dialog v-model="cmdVisiable" :title="$t('terminal.addHost')" width="30%">
|
<el-dialog v-model="cmdVisiable" :title="$t('terminal.addHost')" width="30%">
|
||||||
<el-form ref="commandInfoRef" label-width="100px" label-position="left" :model="commandInfo" :rules="rules">
|
<el-form ref="commandInfoRef" label-width="100px" label-position="left" :model="commandInfo" :rules="rules">
|
||||||
<el-form-item :label="$t('commons.table.name')" prop="name">
|
<el-form-item :label="$t('commons.table.name')" prop="name">
|
||||||
@ -36,9 +38,10 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ComplexTable from '@/components/complex-table/index.vue';
|
import ComplexTable from '@/components/complex-table/index.vue';
|
||||||
|
import Submenu from '@/views/host/terminal/index.vue';
|
||||||
import { Command } from '@/api/interface/command';
|
import { Command } from '@/api/interface/command';
|
||||||
import { addCommand, editCommand, deleteCommand, getCommandPage } from '@/api/modules/command';
|
import { addCommand, editCommand, deleteCommand, getCommandPage } from '@/api/modules/command';
|
||||||
import { reactive, ref } from '@vue/runtime-core';
|
import { reactive, ref, onMounted } from 'vue';
|
||||||
import { useDeleteData } from '@/hooks/use-delete-data';
|
import { useDeleteData } from '@/hooks/use-delete-data';
|
||||||
import type { ElForm } from 'element-plus';
|
import type { ElForm } from 'element-plus';
|
||||||
import { Rules } from '@/global/form-rules';
|
import { Rules } from '@/global/form-rules';
|
||||||
@ -143,10 +146,7 @@ const search = async () => {
|
|||||||
paginationConfig.total = res.data.total;
|
paginationConfig.total = res.data.total;
|
||||||
};
|
};
|
||||||
|
|
||||||
function onInit() {
|
onMounted(() => {
|
||||||
search();
|
search();
|
||||||
}
|
|
||||||
defineExpose({
|
|
||||||
onInit,
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,119 +1,143 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-row style="margin-top: 20px" class="row-box" :gutter="20">
|
<div>
|
||||||
<el-col :span="8">
|
<Submenu activeName="host" />
|
||||||
<el-card class="el-card">
|
<el-row class="row-box" style="margin-top: 20px" :gutter="20">
|
||||||
<el-tooltip class="box-item" effect="dark" :content="$t('terminal.createConn')" placement="top-start">
|
<el-col :span="8">
|
||||||
<el-button icon="Plus" @click="restHostForm" />
|
<el-card class="el-card">
|
||||||
</el-tooltip>
|
<el-tooltip
|
||||||
<el-tooltip class="box-item" effect="dark" :content="$t('terminal.createGroup')" placement="top-start">
|
class="box-item"
|
||||||
<el-button icon="FolderAdd" @click="onGroupCreate" />
|
effect="dark"
|
||||||
</el-tooltip>
|
:content="$t('terminal.createConn')"
|
||||||
<el-tooltip class="box-item" effect="dark" :content="$t('terminal.expand')" placement="top-start">
|
placement="top-start"
|
||||||
<el-button icon="Expand" @click="setTreeStatus(true)" />
|
|
||||||
</el-tooltip>
|
|
||||||
<el-tooltip class="box-item" effect="dark" :content="$t('terminal.fold')" placement="top-start">
|
|
||||||
<el-button icon="Fold" @click="setTreeStatus(false)" />
|
|
||||||
</el-tooltip>
|
|
||||||
<el-input @input="loadHostTree" clearable style="margin-top: 5px" v-model="searcConfig.info">
|
|
||||||
<template #append><el-button icon="search" @click="loadHostTree" /></template>
|
|
||||||
</el-input>
|
|
||||||
<el-input v-if="groupInputShow" clearable style="margin-top: 5px" v-model="groupInputValue">
|
|
||||||
<template #append>
|
|
||||||
<el-button-group>
|
|
||||||
<el-button icon="Check" @click="onCreateGroup" />
|
|
||||||
<el-button icon="Close" @click="groupInputShow = false" />
|
|
||||||
</el-button-group>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
<el-tree
|
|
||||||
ref="tree"
|
|
||||||
:expand-on-click-node="false"
|
|
||||||
node-key="id"
|
|
||||||
:default-expand-all="true"
|
|
||||||
:data="hostTree"
|
|
||||||
:props="defaultProps"
|
|
||||||
>
|
|
||||||
<template #default="{ node, data }">
|
|
||||||
<span class="custom-tree-node" @mouseover="hover = data.id" @mouseleave="hover = null">
|
|
||||||
<span>
|
|
||||||
<a @click="onEdit(node, data)">{{ node.label }}</a>
|
|
||||||
</span>
|
|
||||||
<el-button-group
|
|
||||||
v-if="!(node.level === 1 && data.label === 'default') && data.id === hover"
|
|
||||||
>
|
|
||||||
<el-button icon="Edit" @click="onEdit(node, data)" />
|
|
||||||
<el-button icon="Delete" @click="onDelete(node, data)" />
|
|
||||||
</el-button-group>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</el-tree>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="16">
|
|
||||||
<el-card class="el-card">
|
|
||||||
<el-form ref="hostInfoRef" label-width="100px" label-position="left" :model="hostInfo" :rules="rules">
|
|
||||||
<el-form-item :label="$t('commons.table.name')" prop="name">
|
|
||||||
<el-input clearable v-model="hostInfo.name" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('commons.table.group')" prop="groupBelong">
|
|
||||||
<el-select filterable v-model="hostInfo.groupBelong" clearable style="width: 100%">
|
|
||||||
<el-option v-for="item in groupList" :key="item.id" :label="item.name" :value="item.name" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="IP" prop="addr">
|
|
||||||
<el-input clearable v-model="hostInfo.addr" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('terminal.port')" prop="port">
|
|
||||||
<el-input clearable v-model.number="hostInfo.port" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('terminal.user')" prop="user">
|
|
||||||
<el-input clearable v-model="hostInfo.user" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('terminal.authMode')" prop="authMode">
|
|
||||||
<el-radio-group v-model="hostInfo.authMode">
|
|
||||||
<el-radio label="password">{{ $t('terminal.passwordMode') }}</el-radio>
|
|
||||||
<el-radio label="key">{{ $t('terminal.keyMode') }}</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item
|
|
||||||
:label="$t('terminal.password')"
|
|
||||||
v-if="hostInfo.authMode === 'password'"
|
|
||||||
prop="password"
|
|
||||||
>
|
>
|
||||||
<el-input clearable show-password type="password" v-model="hostInfo.password" />
|
<el-button icon="Plus" @click="restHostForm" />
|
||||||
</el-form-item>
|
</el-tooltip>
|
||||||
<el-form-item :label="$t('terminal.key')" v-if="hostInfo.authMode === 'key'" prop="privateKey">
|
<el-tooltip
|
||||||
<el-input clearable type="textarea" v-model="hostInfo.privateKey" />
|
class="box-item"
|
||||||
</el-form-item>
|
effect="dark"
|
||||||
<el-form-item :label="$t('commons.table.description')" prop="description">
|
:content="$t('terminal.createGroup')"
|
||||||
<el-input clearable type="textarea" v-model="hostInfo.description" />
|
placement="top-start"
|
||||||
</el-form-item>
|
>
|
||||||
<el-form-item>
|
<el-button icon="FolderAdd" @click="onGroupCreate" />
|
||||||
<el-button @click="restHostForm">
|
</el-tooltip>
|
||||||
{{ $t('commons.button.reset') }}
|
<el-tooltip class="box-item" effect="dark" :content="$t('terminal.expand')" placement="top-start">
|
||||||
</el-button>
|
<el-button icon="Expand" @click="setTreeStatus(true)" />
|
||||||
<el-button @click="submitAddHost(hostInfoRef, 'testconn')">
|
</el-tooltip>
|
||||||
{{ $t('terminal.testConn') }}
|
<el-tooltip class="box-item" effect="dark" :content="$t('terminal.fold')" placement="top-start">
|
||||||
</el-button>
|
<el-button icon="Fold" @click="setTreeStatus(false)" />
|
||||||
<el-button
|
</el-tooltip>
|
||||||
v-if="hostOperation === 'create'"
|
<el-input @input="loadHostTree" clearable style="margin-top: 5px" v-model="searcConfig.info">
|
||||||
type="primary"
|
<template #append><el-button icon="search" @click="loadHostTree" /></template>
|
||||||
@click="submitAddHost(hostInfoRef, 'create')"
|
</el-input>
|
||||||
|
<el-input v-if="groupInputShow" clearable style="margin-top: 5px" v-model="groupInputValue">
|
||||||
|
<template #append>
|
||||||
|
<el-button-group>
|
||||||
|
<el-button icon="Check" @click="onCreateGroup" />
|
||||||
|
<el-button icon="Close" @click="groupInputShow = false" />
|
||||||
|
</el-button-group>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<el-tree
|
||||||
|
ref="tree"
|
||||||
|
:expand-on-click-node="false"
|
||||||
|
node-key="id"
|
||||||
|
:default-expand-all="true"
|
||||||
|
:data="hostTree"
|
||||||
|
:props="defaultProps"
|
||||||
|
>
|
||||||
|
<template #default="{ node, data }">
|
||||||
|
<span class="custom-tree-node" @mouseover="hover = data.id" @mouseleave="hover = null">
|
||||||
|
<span>
|
||||||
|
<a @click="onEdit(node, data)">{{ node.label }}</a>
|
||||||
|
</span>
|
||||||
|
<el-button-group
|
||||||
|
v-if="!(node.level === 1 && data.label === 'default') && data.id === hover"
|
||||||
|
>
|
||||||
|
<el-button icon="Edit" @click="onEdit(node, data)" />
|
||||||
|
<el-button icon="Delete" @click="onDelete(node, data)" />
|
||||||
|
</el-button-group>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-tree>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="16">
|
||||||
|
<el-card class="el-card">
|
||||||
|
<el-form
|
||||||
|
ref="hostInfoRef"
|
||||||
|
label-width="100px"
|
||||||
|
label-position="left"
|
||||||
|
:model="hostInfo"
|
||||||
|
:rules="rules"
|
||||||
|
>
|
||||||
|
<el-form-item :label="$t('commons.table.name')" prop="name">
|
||||||
|
<el-input clearable v-model="hostInfo.name" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('commons.table.group')" prop="groupBelong">
|
||||||
|
<el-select filterable v-model="hostInfo.groupBelong" clearable style="width: 100%">
|
||||||
|
<el-option
|
||||||
|
v-for="item in groupList"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.name"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="IP" prop="addr">
|
||||||
|
<el-input clearable v-model="hostInfo.addr" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('terminal.port')" prop="port">
|
||||||
|
<el-input clearable v-model.number="hostInfo.port" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('terminal.user')" prop="user">
|
||||||
|
<el-input clearable v-model="hostInfo.user" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('terminal.authMode')" prop="authMode">
|
||||||
|
<el-radio-group v-model="hostInfo.authMode">
|
||||||
|
<el-radio label="password">{{ $t('terminal.passwordMode') }}</el-radio>
|
||||||
|
<el-radio label="key">{{ $t('terminal.keyMode') }}</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item
|
||||||
|
:label="$t('terminal.password')"
|
||||||
|
v-if="hostInfo.authMode === 'password'"
|
||||||
|
prop="password"
|
||||||
>
|
>
|
||||||
{{ $t('commons.button.create') }}
|
<el-input clearable show-password type="password" v-model="hostInfo.password" />
|
||||||
</el-button>
|
</el-form-item>
|
||||||
<el-button
|
<el-form-item :label="$t('terminal.key')" v-if="hostInfo.authMode === 'key'" prop="privateKey">
|
||||||
v-if="hostOperation === 'edit'"
|
<el-input clearable type="textarea" v-model="hostInfo.privateKey" />
|
||||||
type="primary"
|
</el-form-item>
|
||||||
@click="submitAddHost(hostInfoRef, 'edit')"
|
<el-form-item :label="$t('commons.table.description')" prop="description">
|
||||||
>
|
<el-input clearable type="textarea" v-model="hostInfo.description" />
|
||||||
{{ $t('commons.button.confirm') }}
|
</el-form-item>
|
||||||
</el-button>
|
<el-form-item>
|
||||||
</el-form-item>
|
<el-button @click="restHostForm">
|
||||||
</el-form>
|
{{ $t('commons.button.reset') }}
|
||||||
</el-card>
|
</el-button>
|
||||||
</el-col>
|
<el-button @click="submitAddHost(hostInfoRef, 'testconn')">
|
||||||
</el-row>
|
{{ $t('terminal.testConn') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="hostOperation === 'create'"
|
||||||
|
type="primary"
|
||||||
|
@click="submitAddHost(hostInfoRef, 'create')"
|
||||||
|
>
|
||||||
|
{{ $t('commons.button.create') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="hostOperation === 'edit'"
|
||||||
|
type="primary"
|
||||||
|
@click="submitAddHost(hostInfoRef, 'edit')"
|
||||||
|
>
|
||||||
|
{{ $t('commons.button.confirm') }}
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
@ -128,6 +152,7 @@ import { useDeleteData } from '@/hooks/use-delete-data';
|
|||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import i18n from '@/lang';
|
import i18n from '@/lang';
|
||||||
import type Node from 'element-plus/es/components/tree/src/model/node';
|
import type Node from 'element-plus/es/components/tree/src/model/node';
|
||||||
|
import Submenu from '@/views/host/terminal/index.vue';
|
||||||
|
|
||||||
type FormInstance = InstanceType<typeof ElForm>;
|
type FormInstance = InstanceType<typeof ElForm>;
|
||||||
const hostInfoRef = ref<FormInstance>();
|
const hostInfoRef = ref<FormInstance>();
|
||||||
|
@ -1,455 +1,46 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<el-card class="topCard">
|
||||||
<el-card class="topCard">
|
<el-radio-group v-model="active">
|
||||||
<el-radio-group @change="handleChange" v-model="activeNames">
|
<el-radio-button class="topButton" size="large" @click="routerTo('/hosts/terminal')" label="terminal">
|
||||||
<el-radio-button class="topButton" size="large" label="terminal">
|
{{ $t('menu.terminal') }}
|
||||||
{{ $t('menu.terminal') }}
|
</el-radio-button>
|
||||||
</el-radio-button>
|
<el-radio-button class="topButton" size="large" @click="routerTo('/hosts/host')" label="host">
|
||||||
<el-radio-button class="topButton" size="large" label="host">
|
{{ $t('menu.host') }}
|
||||||
{{ $t('menu.host') }}
|
</el-radio-button>
|
||||||
</el-radio-button>
|
<el-radio-button class="topButton" size="large" @click="routerTo('/hosts/command')" label="command">
|
||||||
<el-radio-button class="topButton" size="large" label="command">
|
{{ $t('terminal.quickCommand') }}
|
||||||
{{ $t('terminal.quickCommand') }}
|
</el-radio-button>
|
||||||
</el-radio-button>
|
</el-radio-group>
|
||||||
</el-radio-group>
|
</el-card>
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
<div v-if="activeNames === 'terminal'">
|
|
||||||
<el-tabs
|
|
||||||
type="card"
|
|
||||||
class="terminal-tabs"
|
|
||||||
style="background-color: #efefef; margin-top: 20px"
|
|
||||||
v-model="terminalValue"
|
|
||||||
:before-leave="beforeLeave"
|
|
||||||
@edit="handleTabsRemove"
|
|
||||||
>
|
|
||||||
<el-tab-pane
|
|
||||||
:key="item.key"
|
|
||||||
v-for="item in terminalTabs"
|
|
||||||
:closable="true"
|
|
||||||
:label="item.title"
|
|
||||||
:name="item.key"
|
|
||||||
>
|
|
||||||
<template #label>
|
|
||||||
<span class="custom-tabs-label">
|
|
||||||
<el-icon style="margin-top: 1px" color="#67C23A" v-if="item.status === 'online'">
|
|
||||||
<circleCheck />
|
|
||||||
</el-icon>
|
|
||||||
<el-icon style="margin-top: 1px" color="#F56C6C" v-if="item.status === 'closed'">
|
|
||||||
<circleClose />
|
|
||||||
</el-icon>
|
|
||||||
<span> {{ item.title }} </span>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<Terminal
|
|
||||||
style="height: calc(100vh - 178px); background-color: #000"
|
|
||||||
:ref="'Ref' + item.key"
|
|
||||||
:wsID="item.wsID"
|
|
||||||
:terminalID="item.key"
|
|
||||||
></Terminal>
|
|
||||||
<div>
|
|
||||||
<el-select
|
|
||||||
v-model="quickCmd"
|
|
||||||
clearable
|
|
||||||
filterable
|
|
||||||
@change="quickInput"
|
|
||||||
style="width: 25%"
|
|
||||||
:placeholder="$t('terminal.quickCommand')"
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
v-for="cmd in commandList"
|
|
||||||
:key="cmd.id"
|
|
||||||
:label="cmd.name + ' [ ' + cmd.command + ' ] '"
|
|
||||||
:value="cmd.command"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
<el-input
|
|
||||||
:placeholder="$t('terminal.batchInput')"
|
|
||||||
v-model="batchVal"
|
|
||||||
@keyup.enter="batchInput"
|
|
||||||
style="width: 75%"
|
|
||||||
>
|
|
||||||
<template #append>
|
|
||||||
<el-switch v-model="isBatch" class="ml-2" />
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
</div>
|
|
||||||
</el-tab-pane>
|
|
||||||
<el-tab-pane :closable="false" name="newTabs">
|
|
||||||
<template #label>
|
|
||||||
<el-button
|
|
||||||
v-popover="popoverRef"
|
|
||||||
style="background-color: #ededed; border: 0"
|
|
||||||
icon="Plus"
|
|
||||||
></el-button>
|
|
||||||
<el-popover ref="popoverRef" width="250px" trigger="hover" virtual-triggering persistent>
|
|
||||||
<el-button-group style="width: 100%">
|
|
||||||
<el-button @click="onNewSsh">New ssh</el-button>
|
|
||||||
<el-button @click="onNewTab">New tab</el-button>
|
|
||||||
</el-button-group>
|
|
||||||
<el-input clearable style="margin-top: 5px" v-model="hostfilterInfo">
|
|
||||||
<template #append><el-button icon="search" /></template>
|
|
||||||
</el-input>
|
|
||||||
<el-tree
|
|
||||||
ref="treeRef"
|
|
||||||
:expand-on-click-node="false"
|
|
||||||
node-key="id"
|
|
||||||
:default-expand-all="true"
|
|
||||||
:data="hostTree"
|
|
||||||
:props="defaultProps"
|
|
||||||
:filter-node-method="filterHost"
|
|
||||||
>
|
|
||||||
<template #default="{ node, data }">
|
|
||||||
<span class="custom-tree-node">
|
|
||||||
<span>
|
|
||||||
<a @click="onConn(node, data)">{{ node.label }}</a>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</el-tree>
|
|
||||||
</el-popover>
|
|
||||||
</template>
|
|
||||||
</el-tab-pane>
|
|
||||||
<div v-if="terminalTabs.length === 0">
|
|
||||||
<el-empty
|
|
||||||
style="background-color: #000; height: calc(100vh - 150px)"
|
|
||||||
:description="$t('terminal.emptyTerminal')"
|
|
||||||
></el-empty>
|
|
||||||
</div>
|
|
||||||
</el-tabs>
|
|
||||||
<el-button @click="toggleFullscreen" class="fullScreen" icon="FullScreen"></el-button>
|
|
||||||
</div>
|
|
||||||
<div v-if="activeNames === 'host'"><HostTab ref="hostTabRef" /></div>
|
|
||||||
<div v-if="activeNames === 'command'"><CommandTab ref="commandTabRef" /></div>
|
|
||||||
|
|
||||||
<el-dialog v-model="connVisiable" :title="$t('terminal.addHost')" width="30%">
|
|
||||||
<el-form ref="hostInfoRef" label-width="100px" label-position="left" :model="hostInfo" :rules="rules">
|
|
||||||
<el-form-item :label="$t('commons.table.name')" prop="name">
|
|
||||||
<el-input clearable v-model="hostInfo.name" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="IP" prop="addr">
|
|
||||||
<el-input clearable v-model="hostInfo.addr" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('terminal.port')" prop="port">
|
|
||||||
<el-input clearable v-model.number="hostInfo.port" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('terminal.user')" prop="user">
|
|
||||||
<el-input clearable v-model="hostInfo.user" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('terminal.authMode')" prop="authMode">
|
|
||||||
<el-radio-group v-model="hostInfo.authMode">
|
|
||||||
<el-radio label="password">{{ $t('terminal.passwordMode') }}</el-radio>
|
|
||||||
<el-radio label="key">{{ $t('terminal.keyMode') }}</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('terminal.password')" v-if="hostInfo.authMode === 'password'" prop="password">
|
|
||||||
<el-input clearable show-password type="password" v-model="hostInfo.password" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('terminal.key')" v-if="hostInfo.authMode === 'key'" prop="privateKey">
|
|
||||||
<el-input clearable type="textarea" v-model="hostInfo.privateKey" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<span class="dialog-footer">
|
|
||||||
<el-button @click="connVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
|
|
||||||
<el-button @click="submitAddHost(hostInfoRef, 'testConn')">
|
|
||||||
{{ $t('terminal.testConn') }}
|
|
||||||
</el-button>
|
|
||||||
<el-button type="primary" @click="submitAddHost(hostInfoRef, 'saveAndConn')">
|
|
||||||
{{ $t('terminal.saveAndConn') }}
|
|
||||||
</el-button>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
import { onMounted, onBeforeMount, ref, watch, reactive, getCurrentInstance } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import { Rules } from '@/global/form-rules';
|
import { useRouter } from 'vue-router';
|
||||||
import { testConn, getHostTree, addHost } from '@/api/modules/host';
|
const router = useRouter();
|
||||||
import { getCommandList } from '@/api/modules/command';
|
interface MenuProps {
|
||||||
import i18n from '@/lang';
|
activeName: string;
|
||||||
import { ElForm } from 'element-plus';
|
|
||||||
import { Host } from '@/api/interface/host';
|
|
||||||
import { ElMessage } from 'element-plus';
|
|
||||||
import Terminal from '@/views/host/terminal/terminal/index.vue';
|
|
||||||
import HostTab from '@/views/host/terminal/host/index.vue';
|
|
||||||
import CommandTab from '@/views/host/terminal/command/index.vue';
|
|
||||||
import type Node from 'element-plus/es/components/tree/src/model/node';
|
|
||||||
import { ElTree } from 'element-plus';
|
|
||||||
import screenfull from 'screenfull';
|
|
||||||
|
|
||||||
let timer: NodeJS.Timer | null = null;
|
|
||||||
|
|
||||||
const activeNames = ref<string>('terminal');
|
|
||||||
const hostTabRef = ref();
|
|
||||||
const commandTabRef = ref();
|
|
||||||
|
|
||||||
const terminalValue = ref();
|
|
||||||
const terminalTabs = ref([]) as any;
|
|
||||||
let tabIndex = 0;
|
|
||||||
const commandList = ref();
|
|
||||||
let quickCmd = ref();
|
|
||||||
let batchVal = ref();
|
|
||||||
let isBatch = ref<boolean>(false);
|
|
||||||
const popoverRef = ref();
|
|
||||||
|
|
||||||
const connVisiable = ref<boolean>(false);
|
|
||||||
type FormInstance = InstanceType<typeof ElForm>;
|
|
||||||
|
|
||||||
const hostInfoRef = ref<FormInstance>();
|
|
||||||
const hostTree = ref<Array<Host.HostTree>>();
|
|
||||||
const treeRef = ref<InstanceType<typeof ElTree>>();
|
|
||||||
const defaultProps = {
|
|
||||||
label: 'label',
|
|
||||||
children: 'children',
|
|
||||||
};
|
|
||||||
const hostfilterInfo = ref('');
|
|
||||||
interface Tree {
|
|
||||||
id: number;
|
|
||||||
label: string;
|
|
||||||
children?: Tree[];
|
|
||||||
}
|
}
|
||||||
const rules = reactive({
|
const props = withDefaults(defineProps<MenuProps>(), {
|
||||||
name: [Rules.requiredInput, Rules.name],
|
activeName: 'terminal',
|
||||||
addr: [Rules.requiredInput, Rules.ip],
|
|
||||||
port: [Rules.requiredInput, Rules.port],
|
|
||||||
user: [Rules.requiredInput],
|
|
||||||
authMode: [Rules.requiredSelect],
|
|
||||||
password: [Rules.requiredInput],
|
|
||||||
privateKey: [Rules.requiredInput],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let hostInfo = reactive<Host.HostOperate>({
|
const active = ref('terminal');
|
||||||
id: 0,
|
|
||||||
name: '',
|
|
||||||
groupBelong: '',
|
|
||||||
addr: '',
|
|
||||||
port: 22,
|
|
||||||
user: '',
|
|
||||||
authMode: 'password',
|
|
||||||
password: '',
|
|
||||||
privateKey: '',
|
|
||||||
description: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const ctx = getCurrentInstance() as any;
|
|
||||||
|
|
||||||
function toggleFullscreen() {
|
|
||||||
if (screenfull.isEnabled) {
|
|
||||||
screenfull.toggle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const handleChange = (tab: any) => {
|
|
||||||
if (tab === 'host') {
|
|
||||||
if (ctx) {
|
|
||||||
ctx.refs[`hostTabRef`] && ctx.refs[`hostTabRef`].onInit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (tab === 'command') {
|
|
||||||
if (ctx) {
|
|
||||||
ctx.refs[`commandTabRef`] && ctx.refs[`commandTabRef`].onInit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTabsRemove = (targetName: string, action: 'remove' | 'add') => {
|
|
||||||
if (action !== 'remove') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (ctx) {
|
|
||||||
ctx.refs[`Ref${targetName}`] && ctx.refs[`Ref${targetName}`][0].onClose();
|
|
||||||
}
|
|
||||||
const tabs = terminalTabs.value;
|
|
||||||
let activeName = terminalValue.value;
|
|
||||||
if (activeName === targetName) {
|
|
||||||
tabs.forEach((tab: any, index: any) => {
|
|
||||||
if (tab.key === targetName) {
|
|
||||||
const nextTab = tabs[index + 1] || tabs[index - 1];
|
|
||||||
if (nextTab) {
|
|
||||||
activeName = nextTab.key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
terminalValue.value = activeName;
|
|
||||||
terminalTabs.value = tabs.filter((tab: any) => tab.key !== targetName);
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadHost = async () => {
|
|
||||||
const res = await getHostTree({});
|
|
||||||
hostTree.value = res.data;
|
|
||||||
};
|
|
||||||
watch(hostfilterInfo, (val: any) => {
|
|
||||||
treeRef.value!.filter(val);
|
|
||||||
});
|
|
||||||
const filterHost = (value: string, data: any) => {
|
|
||||||
if (!value) return true;
|
|
||||||
return data.label.includes(value);
|
|
||||||
};
|
|
||||||
const loadCommand = async () => {
|
|
||||||
const res = await getCommandList();
|
|
||||||
commandList.value = res.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
function quickInput(val: any) {
|
|
||||||
if (val !== '') {
|
|
||||||
if (ctx) {
|
|
||||||
ctx.refs[`Ref${terminalValue.value}`] && ctx.refs[`Ref${terminalValue.value}`][0].onSendMsg(val + '\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function batchInput() {
|
|
||||||
if (batchVal.value === '' || !ctx) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isBatch.value) {
|
|
||||||
for (const tab of terminalTabs.value) {
|
|
||||||
ctx.refs[`Ref${tab.key}`] && ctx.refs[`Ref${tab.key}`][0].onSendMsg(batchVal.value + '\n');
|
|
||||||
}
|
|
||||||
batchVal.value = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ctx.refs[`Ref${terminalValue.value}`] && ctx.refs[`Ref${terminalValue.value}`][0].onSendMsg(batchVal.value + '\n');
|
|
||||||
batchVal.value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function beforeLeave(activeName: string) {
|
|
||||||
if (activeName === 'newTabs') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onNewTab = () => {
|
|
||||||
terminalTabs.value.push({
|
|
||||||
key: `127.0.0.1-${++tabIndex}`,
|
|
||||||
title: '127.0.0.1',
|
|
||||||
wsID: 0,
|
|
||||||
status: 'online',
|
|
||||||
});
|
|
||||||
terminalValue.value = `127.0.0.1-${tabIndex}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onNewSsh = () => {
|
|
||||||
connVisiable.value = true;
|
|
||||||
if (hostInfoRef.value) {
|
|
||||||
hostInfoRef.value.resetFields();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onConn = (node: Node, data: Tree) => {
|
|
||||||
if (node.level === 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let addr = data.label.split('@')[1].split(':')[0];
|
|
||||||
terminalTabs.value.push({
|
|
||||||
key: `${addr}-${++tabIndex}`,
|
|
||||||
title: addr,
|
|
||||||
wsID: data.id,
|
|
||||||
status: 'online',
|
|
||||||
});
|
|
||||||
terminalValue.value = `${addr}-${tabIndex}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitAddHost = (formEl: FormInstance | undefined, ops: string) => {
|
|
||||||
if (!formEl) return;
|
|
||||||
formEl.validate(async (valid) => {
|
|
||||||
if (!valid) return;
|
|
||||||
hostInfo.groupBelong = 'default';
|
|
||||||
switch (ops) {
|
|
||||||
case 'testConn':
|
|
||||||
await testConn(hostInfo);
|
|
||||||
ElMessage.success(i18n.global.t('terminal.connTestOk'));
|
|
||||||
break;
|
|
||||||
case 'saveAndConn':
|
|
||||||
const res = await addHost(hostInfo);
|
|
||||||
terminalTabs.value.push({
|
|
||||||
key: `${res.data.addr}-${++tabIndex}`,
|
|
||||||
title: res.data.addr,
|
|
||||||
wsID: res.data.id,
|
|
||||||
status: 'online',
|
|
||||||
});
|
|
||||||
terminalValue.value = `${res.data.addr}-${tabIndex}`;
|
|
||||||
connVisiable.value = false;
|
|
||||||
loadHost();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onConnLocal = () => {
|
|
||||||
terminalTabs.value.push({
|
|
||||||
key: `127.0.0.1-${++tabIndex}`,
|
|
||||||
title: '127.0.0.1',
|
|
||||||
wsID: 0,
|
|
||||||
status: 'online',
|
|
||||||
});
|
|
||||||
terminalValue.value = `127.0.0.1-${tabIndex}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
function syncTerminal() {
|
|
||||||
for (const terminal of terminalTabs.value) {
|
|
||||||
if (ctx && ctx.refs[`Ref${terminal.key}`][0]) {
|
|
||||||
terminal.status = ctx.refs[`Ref${terminal.key}`][0].isWsOpen() ? 'online' : 'closed';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
onConnLocal();
|
if (props.activeName) {
|
||||||
loadHost();
|
active.value = props.activeName;
|
||||||
loadCommand();
|
}
|
||||||
timer = setInterval(() => {
|
|
||||||
syncTerminal();
|
|
||||||
}, 1000 * 8);
|
|
||||||
});
|
});
|
||||||
onBeforeMount(() => {
|
|
||||||
clearInterval(Number(timer));
|
|
||||||
timer = null;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.terminal-tabs {
|
|
||||||
:deep .el-tabs__header {
|
|
||||||
padding: 0;
|
|
||||||
position: relative;
|
|
||||||
margin: 0 0 3px 0;
|
|
||||||
}
|
|
||||||
::deep .el-tabs__nav {
|
|
||||||
white-space: nowrap;
|
|
||||||
position: relative;
|
|
||||||
transition: transform var(--el-transition-duration);
|
|
||||||
float: left;
|
|
||||||
z-index: calc(var(--el-index-normal) + 1);
|
|
||||||
}
|
|
||||||
:deep .el-tabs__item {
|
|
||||||
color: #575758;
|
|
||||||
padding: 0 0px;
|
|
||||||
}
|
|
||||||
:deep .el-tabs__item.is-active {
|
|
||||||
color: #ebeef5;
|
|
||||||
background-color: #575758;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.vertical-tabs > .el-tabs__content {
|
const routerTo = (path: string) => {
|
||||||
padding: 32px;
|
router.push({ path: path });
|
||||||
color: #6b778c;
|
};
|
||||||
font-size: 32px;
|
</script>
|
||||||
font-weight: 600;
|
|
||||||
}
|
<style>
|
||||||
.fullScreen {
|
|
||||||
position: absolute;
|
|
||||||
right: 50px;
|
|
||||||
top: 86px;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
.el-tabs--top.el-tabs--card > .el-tabs__header .el-tabs__item:last-child {
|
|
||||||
padding-right: 0px;
|
|
||||||
}
|
|
||||||
.topCard {
|
.topCard {
|
||||||
--el-card-border-color: var(--el-border-color-light);
|
--el-card-border-color: var(--el-border-color-light);
|
||||||
--el-card-border-radius: 4px;
|
--el-card-border-radius: 4px;
|
||||||
|
@ -1,176 +1,422 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :id="'terminal' + props.terminalID"></div>
|
<div>
|
||||||
|
<Submenu activeName="terminal" />
|
||||||
|
<el-card class="topCard" style="margin-top: 20px">
|
||||||
|
<el-tabs
|
||||||
|
type="card"
|
||||||
|
class="terminal-tabs"
|
||||||
|
style="background-color: #efefef"
|
||||||
|
v-model="terminalValue"
|
||||||
|
:before-leave="beforeLeave"
|
||||||
|
@edit="handleTabsRemove"
|
||||||
|
>
|
||||||
|
<el-tab-pane
|
||||||
|
:key="item.key"
|
||||||
|
v-for="item in terminalTabs"
|
||||||
|
:closable="true"
|
||||||
|
:label="item.title"
|
||||||
|
:name="item.key"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<span class="custom-tabs-label">
|
||||||
|
<el-icon style="margin-top: 1px" color="#67C23A" v-if="item.status === 'online'">
|
||||||
|
<circleCheck />
|
||||||
|
</el-icon>
|
||||||
|
<el-icon style="margin-top: 1px" color="#F56C6C" v-if="item.status === 'closed'">
|
||||||
|
<circleClose />
|
||||||
|
</el-icon>
|
||||||
|
<span> {{ item.title }} </span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<Terminal
|
||||||
|
style="height: calc(100vh - 178px); background-color: #000"
|
||||||
|
:ref="'Ref' + item.key"
|
||||||
|
:wsID="item.wsID"
|
||||||
|
:terminalID="item.key"
|
||||||
|
></Terminal>
|
||||||
|
<div>
|
||||||
|
<el-select
|
||||||
|
v-model="quickCmd"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
@blur="quickInput(quickCmd)"
|
||||||
|
style="width: 25%"
|
||||||
|
:placeholder="$t('terminal.quickCommand')"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="cmd in commandList"
|
||||||
|
:key="cmd.id"
|
||||||
|
:label="cmd.name + ' [ ' + cmd.command + ' ] '"
|
||||||
|
:value="cmd.command"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-input
|
||||||
|
:placeholder="$t('terminal.batchInput')"
|
||||||
|
v-model="batchVal"
|
||||||
|
@keyup.enter="batchInput"
|
||||||
|
style="width: 75%"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<el-switch v-model="isBatch" class="ml-2" />
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane :closable="false" name="newTabs">
|
||||||
|
<template #label>
|
||||||
|
<el-button
|
||||||
|
v-popover="popoverRef"
|
||||||
|
style="background-color: #ededed; border: 0"
|
||||||
|
icon="Plus"
|
||||||
|
></el-button>
|
||||||
|
<el-popover ref="popoverRef" width="250px" trigger="hover" virtual-triggering persistent>
|
||||||
|
<el-button-group style="width: 100%">
|
||||||
|
<el-button @click="onNewSsh">New ssh</el-button>
|
||||||
|
<el-button @click="onNewTab">New tab</el-button>
|
||||||
|
</el-button-group>
|
||||||
|
<el-input clearable style="margin-top: 5px" v-model="hostfilterInfo">
|
||||||
|
<template #append><el-button icon="search" /></template>
|
||||||
|
</el-input>
|
||||||
|
<el-tree
|
||||||
|
ref="treeRef"
|
||||||
|
:expand-on-click-node="false"
|
||||||
|
node-key="id"
|
||||||
|
:default-expand-all="true"
|
||||||
|
:data="hostTree"
|
||||||
|
:props="defaultProps"
|
||||||
|
:filter-node-method="filterHost"
|
||||||
|
>
|
||||||
|
<template #default="{ node, data }">
|
||||||
|
<span class="custom-tree-node">
|
||||||
|
<span>
|
||||||
|
<a @click="onConn(node, data)">{{ node.label }}</a>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-tree>
|
||||||
|
</el-popover>
|
||||||
|
</template>
|
||||||
|
</el-tab-pane>
|
||||||
|
<div v-if="terminalTabs.length === 0">
|
||||||
|
<el-empty
|
||||||
|
style="background-color: #000; height: calc(100vh - 150px)"
|
||||||
|
:description="$t('terminal.emptyTerminal')"
|
||||||
|
></el-empty>
|
||||||
|
</div>
|
||||||
|
</el-tabs>
|
||||||
|
<el-button @click="toggleFullscreen" class="fullScreen" icon="FullScreen"></el-button>
|
||||||
|
</el-card>
|
||||||
|
<el-dialog v-model="connVisiable" :title="$t('terminal.addHost')" width="30%">
|
||||||
|
<el-form ref="hostInfoRef" label-width="100px" label-position="left" :model="hostInfo" :rules="rules">
|
||||||
|
<el-form-item :label="$t('commons.table.name')" prop="name">
|
||||||
|
<el-input clearable v-model="hostInfo.name" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="IP" prop="addr">
|
||||||
|
<el-input clearable v-model="hostInfo.addr" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('terminal.port')" prop="port">
|
||||||
|
<el-input clearable v-model.number="hostInfo.port" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('terminal.user')" prop="user">
|
||||||
|
<el-input clearable v-model="hostInfo.user" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('terminal.authMode')" prop="authMode">
|
||||||
|
<el-radio-group v-model="hostInfo.authMode">
|
||||||
|
<el-radio label="password">{{ $t('terminal.passwordMode') }}</el-radio>
|
||||||
|
<el-radio label="key">{{ $t('terminal.keyMode') }}</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('terminal.password')" v-if="hostInfo.authMode === 'password'" prop="password">
|
||||||
|
<el-input clearable show-password type="password" v-model="hostInfo.password" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('terminal.key')" v-if="hostInfo.authMode === 'key'" prop="privateKey">
|
||||||
|
<el-input clearable type="textarea" v-model="hostInfo.privateKey" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="connVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||||
|
<el-button @click="submitAddHost(hostInfoRef, 'testConn')">
|
||||||
|
{{ $t('terminal.testConn') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" @click="submitAddHost(hostInfoRef, 'saveAndConn')">
|
||||||
|
{{ $t('terminal.saveAndConn') }}
|
||||||
|
</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
import Submenu from '@/views/host/terminal/index.vue';
|
||||||
import { Terminal } from 'xterm';
|
import Terminal from '@/views/host/terminal/terminal/terminal.vue';
|
||||||
import { AttachAddon } from 'xterm-addon-attach';
|
import { Host } from '@/api/interface/host';
|
||||||
import { Base64 } from 'js-base64';
|
import { getCommandList } from '@/api/modules/command';
|
||||||
import 'xterm/css/xterm.css';
|
import { addHost, getHostTree, testConn } from '@/api/modules/host';
|
||||||
import { FitAddon } from 'xterm-addon-fit';
|
import { Rules } from '@/global/form-rules';
|
||||||
|
import i18n from '@/lang';
|
||||||
|
import { ElForm, ElMessage, ElTree } from 'element-plus';
|
||||||
|
import screenfull from 'screenfull';
|
||||||
|
import { getCurrentInstance, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
|
||||||
|
import type Node from 'element-plus/es/components/tree/src/model/node';
|
||||||
|
|
||||||
interface WsProps {
|
let timer: NodeJS.Timer | null = null;
|
||||||
terminalID: string;
|
const terminalValue = ref();
|
||||||
wsID: number;
|
const terminalTabs = ref([]) as any;
|
||||||
}
|
let tabIndex = 0;
|
||||||
const props = withDefaults(defineProps<WsProps>(), {
|
const commandList = ref();
|
||||||
terminalID: '',
|
let quickCmd = ref();
|
||||||
wsID: 0,
|
let batchVal = ref();
|
||||||
});
|
let isBatch = ref<boolean>(false);
|
||||||
const fitAddon = new FitAddon();
|
const popoverRef = ref();
|
||||||
const loading = ref(true);
|
|
||||||
let terminalSocket = ref(null) as unknown as WebSocket;
|
|
||||||
let term = ref(null) as unknown as Terminal;
|
|
||||||
|
|
||||||
const runRealTerminal = () => {
|
const connVisiable = ref<boolean>(false);
|
||||||
loading.value = false;
|
type FormInstance = InstanceType<typeof ElForm>;
|
||||||
|
|
||||||
|
const hostInfoRef = ref<FormInstance>();
|
||||||
|
const hostTree = ref<Array<Host.HostTree>>();
|
||||||
|
const treeRef = ref<InstanceType<typeof ElTree>>();
|
||||||
|
const defaultProps = {
|
||||||
|
label: 'label',
|
||||||
|
children: 'children',
|
||||||
};
|
};
|
||||||
|
const hostfilterInfo = ref('');
|
||||||
|
interface Tree {
|
||||||
|
id: number;
|
||||||
|
label: string;
|
||||||
|
children?: Tree[];
|
||||||
|
}
|
||||||
|
const rules = reactive({
|
||||||
|
name: [Rules.requiredInput, Rules.name],
|
||||||
|
addr: [Rules.requiredInput, Rules.ip],
|
||||||
|
port: [Rules.requiredInput, Rules.port],
|
||||||
|
user: [Rules.requiredInput],
|
||||||
|
authMode: [Rules.requiredSelect],
|
||||||
|
password: [Rules.requiredInput],
|
||||||
|
privateKey: [Rules.requiredInput],
|
||||||
|
});
|
||||||
|
|
||||||
const onWSReceive = (message: any) => {
|
let hostInfo = reactive<Host.HostOperate>({
|
||||||
if (!isJson(message.data)) {
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
groupBelong: '',
|
||||||
|
addr: '',
|
||||||
|
port: 22,
|
||||||
|
user: '',
|
||||||
|
authMode: 'password',
|
||||||
|
password: '',
|
||||||
|
privateKey: '',
|
||||||
|
description: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const ctx = getCurrentInstance() as any;
|
||||||
|
|
||||||
|
function toggleFullscreen() {
|
||||||
|
if (screenfull.isEnabled) {
|
||||||
|
screenfull.toggle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handleTabsRemove = (targetName: string, action: 'remove' | 'add') => {
|
||||||
|
if (action !== 'remove') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = JSON.parse(message.data);
|
if (ctx) {
|
||||||
term.element && term.focus();
|
ctx.refs[`Ref${targetName}`] && ctx.refs[`Ref${targetName}`][0].onClose();
|
||||||
term.write(data.Data);
|
}
|
||||||
|
const tabs = terminalTabs.value;
|
||||||
|
let activeName = terminalValue.value;
|
||||||
|
if (activeName === targetName) {
|
||||||
|
tabs.forEach((tab: any, index: any) => {
|
||||||
|
if (tab.key === targetName) {
|
||||||
|
const nextTab = tabs[index + 1] || tabs[index - 1];
|
||||||
|
if (nextTab) {
|
||||||
|
activeName = nextTab.key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
terminalValue.value = activeName;
|
||||||
|
terminalTabs.value = tabs.filter((tab: any) => tab.key !== targetName);
|
||||||
};
|
};
|
||||||
|
|
||||||
function isJson(str: string) {
|
const loadHost = async () => {
|
||||||
try {
|
const res = await getHostTree({});
|
||||||
if (typeof JSON.parse(str) === 'object') {
|
hostTree.value = res.data;
|
||||||
return true;
|
for (let i = 0; i < hostTree.value.length; i++) {
|
||||||
|
if (!hostTree.value[i].children) {
|
||||||
|
hostTree.value.splice(i, 1);
|
||||||
|
} else if (hostTree.value[i].children.length === 0) {
|
||||||
|
hostTree.value.splice(i, 1);
|
||||||
}
|
}
|
||||||
} catch {
|
}
|
||||||
|
};
|
||||||
|
watch(hostfilterInfo, (val: any) => {
|
||||||
|
treeRef.value!.filter(val);
|
||||||
|
});
|
||||||
|
const filterHost = (value: string, data: any) => {
|
||||||
|
if (!value) return true;
|
||||||
|
return data.label.includes(value);
|
||||||
|
};
|
||||||
|
const loadCommand = async () => {
|
||||||
|
const res = await getCommandList();
|
||||||
|
commandList.value = res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
function quickInput(val: any) {
|
||||||
|
if (val !== '') {
|
||||||
|
if (ctx) {
|
||||||
|
ctx.refs[`Ref${terminalValue.value}`] && ctx.refs[`Ref${terminalValue.value}`][0].onSendMsg(val + '\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function batchInput() {
|
||||||
|
if (batchVal.value === '' || !ctx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isBatch.value) {
|
||||||
|
for (const tab of terminalTabs.value) {
|
||||||
|
ctx.refs[`Ref${tab.key}`] && ctx.refs[`Ref${tab.key}`][0].onSendMsg(batchVal.value + '\n');
|
||||||
|
}
|
||||||
|
batchVal.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.refs[`Ref${terminalValue.value}`] && ctx.refs[`Ref${terminalValue.value}`][0].onSendMsg(batchVal.value + '\n');
|
||||||
|
batchVal.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function beforeLeave(activeName: string) {
|
||||||
|
if (activeName === 'newTabs') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorRealTerminal = (ex: any) => {
|
const onNewTab = () => {
|
||||||
let message = ex.message;
|
terminalTabs.value.push({
|
||||||
if (!message) message = 'disconnected';
|
key: `127.0.0.1-${++tabIndex}`,
|
||||||
term.write(`\x1b[31m${message}\x1b[m\r\n`);
|
title: '127.0.0.1',
|
||||||
console.log('err');
|
wsID: 0,
|
||||||
};
|
status: 'online',
|
||||||
|
|
||||||
const closeRealTerminal = (ev: CloseEvent) => {
|
|
||||||
term.write(ev.reason);
|
|
||||||
};
|
|
||||||
|
|
||||||
const initTerm = () => {
|
|
||||||
let ifm = document.getElementById('terminal' + props.terminalID) as HTMLInputElement | null;
|
|
||||||
term = new Terminal({
|
|
||||||
lineHeight: 1.2,
|
|
||||||
fontSize: 12,
|
|
||||||
fontFamily: "Monaco, Menlo, Consolas, 'Courier New', monospace",
|
|
||||||
theme: {
|
|
||||||
background: '#000000',
|
|
||||||
},
|
|
||||||
cursorBlink: true,
|
|
||||||
cursorStyle: 'underline',
|
|
||||||
scrollback: 100,
|
|
||||||
tabStopWidth: 4,
|
|
||||||
});
|
});
|
||||||
if (ifm) {
|
terminalValue.value = `127.0.0.1-${tabIndex}`;
|
||||||
term.open(ifm);
|
};
|
||||||
if (props.wsID === 0) {
|
|
||||||
terminalSocket = new WebSocket(
|
const onNewSsh = () => {
|
||||||
`ws://localhost:9999/api/v1/terminals/local?cols=${term.cols}&rows=${term.rows}`,
|
connVisiable.value = true;
|
||||||
);
|
if (hostInfoRef.value) {
|
||||||
} else {
|
hostInfoRef.value.resetFields();
|
||||||
terminalSocket = new WebSocket(
|
}
|
||||||
`ws://localhost:9999/api/v1/terminals?id=${props.wsID}&cols=${term.cols}&rows=${term.rows}`,
|
};
|
||||||
);
|
|
||||||
|
const onConn = (node: Node, data: Tree) => {
|
||||||
|
if (node.level === 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let addr = data.label.split('@')[1].split(':')[0];
|
||||||
|
terminalTabs.value.push({
|
||||||
|
key: `${addr}-${++tabIndex}`,
|
||||||
|
title: addr,
|
||||||
|
wsID: data.id,
|
||||||
|
status: 'online',
|
||||||
|
});
|
||||||
|
terminalValue.value = `${addr}-${tabIndex}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitAddHost = (formEl: FormInstance | undefined, ops: string) => {
|
||||||
|
if (!formEl) return;
|
||||||
|
formEl.validate(async (valid) => {
|
||||||
|
if (!valid) return;
|
||||||
|
hostInfo.groupBelong = 'default';
|
||||||
|
switch (ops) {
|
||||||
|
case 'testConn':
|
||||||
|
await testConn(hostInfo);
|
||||||
|
ElMessage.success(i18n.global.t('terminal.connTestOk'));
|
||||||
|
break;
|
||||||
|
case 'saveAndConn':
|
||||||
|
const res = await addHost(hostInfo);
|
||||||
|
terminalTabs.value.push({
|
||||||
|
key: `${res.data.addr}-${++tabIndex}`,
|
||||||
|
title: res.data.addr,
|
||||||
|
wsID: res.data.id,
|
||||||
|
status: 'online',
|
||||||
|
});
|
||||||
|
terminalValue.value = `${res.data.addr}-${tabIndex}`;
|
||||||
|
connVisiable.value = false;
|
||||||
|
loadHost();
|
||||||
}
|
}
|
||||||
terminalSocket.onopen = runRealTerminal;
|
});
|
||||||
terminalSocket.onmessage = onWSReceive;
|
|
||||||
terminalSocket.onclose = closeRealTerminal;
|
|
||||||
terminalSocket.onerror = errorRealTerminal;
|
|
||||||
term.onData((data: any) => {
|
|
||||||
if (isWsOpen()) {
|
|
||||||
terminalSocket.send(
|
|
||||||
JSON.stringify({
|
|
||||||
type: 'cmd',
|
|
||||||
cmd: Base64.encode(data),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
term.loadAddon(new AttachAddon(terminalSocket));
|
|
||||||
term.loadAddon(fitAddon);
|
|
||||||
setTimeout(() => {
|
|
||||||
fitAddon.fit();
|
|
||||||
if (isWsOpen()) {
|
|
||||||
terminalSocket.send(
|
|
||||||
JSON.stringify({
|
|
||||||
type: 'resize',
|
|
||||||
cols: term.cols,
|
|
||||||
rows: term.rows,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, 30);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const fitTerm = () => {
|
const onConnLocal = () => {
|
||||||
fitAddon.fit();
|
terminalTabs.value.push({
|
||||||
|
key: `127.0.0.1-${++tabIndex}`,
|
||||||
|
title: '127.0.0.1',
|
||||||
|
wsID: 0,
|
||||||
|
status: 'online',
|
||||||
|
});
|
||||||
|
terminalValue.value = `127.0.0.1-${tabIndex}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isWsOpen = () => {
|
function syncTerminal() {
|
||||||
const readyState = terminalSocket && terminalSocket.readyState;
|
for (const terminal of terminalTabs.value) {
|
||||||
return readyState === 1;
|
if (ctx && ctx.refs[`Ref${terminal.key}`][0]) {
|
||||||
};
|
terminal.status = ctx.refs[`Ref${terminal.key}`][0].isWsOpen() ? 'online' : 'closed';
|
||||||
|
}
|
||||||
function onClose() {
|
|
||||||
window.removeEventListener('resize', changeTerminalSize);
|
|
||||||
terminalSocket && terminalSocket.close();
|
|
||||||
term && term.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSendMsg(command: string) {
|
|
||||||
terminalSocket.send(
|
|
||||||
JSON.stringify({
|
|
||||||
type: 'cmd',
|
|
||||||
cmd: Base64.encode(command),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeTerminalSize() {
|
|
||||||
fitTerm();
|
|
||||||
const { cols, rows } = term;
|
|
||||||
if (isWsOpen()) {
|
|
||||||
terminalSocket.send(
|
|
||||||
JSON.stringify({
|
|
||||||
type: 'resize',
|
|
||||||
cols: cols,
|
|
||||||
rows: rows,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
onClose,
|
|
||||||
isWsOpen,
|
|
||||||
onSendMsg,
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
nextTick(() => {
|
onConnLocal();
|
||||||
initTerm();
|
loadHost();
|
||||||
window.addEventListener('resize', changeTerminalSize);
|
loadCommand();
|
||||||
});
|
timer = setInterval(() => {
|
||||||
|
syncTerminal();
|
||||||
|
}, 1000 * 8);
|
||||||
});
|
});
|
||||||
|
onUnmounted(() => {
|
||||||
onBeforeUnmount(() => {
|
clearInterval(Number(timer));
|
||||||
onClose();
|
timer = null;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
#terminal {
|
.terminal-tabs {
|
||||||
width: 100%;
|
:deep .el-tabs__header {
|
||||||
height: 100%;
|
padding: 0;
|
||||||
|
position: relative;
|
||||||
|
margin: 0 0 3px 0;
|
||||||
|
}
|
||||||
|
::deep .el-tabs__nav {
|
||||||
|
white-space: nowrap;
|
||||||
|
position: relative;
|
||||||
|
transition: transform var(--el-transition-duration);
|
||||||
|
float: left;
|
||||||
|
z-index: calc(var(--el-index-normal) + 1);
|
||||||
|
}
|
||||||
|
:deep .el-tabs__item {
|
||||||
|
color: #575758;
|
||||||
|
padding: 0 0px;
|
||||||
|
}
|
||||||
|
:deep .el-tabs__item.is-active {
|
||||||
|
color: #ebeef5;
|
||||||
|
background-color: #575758;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical-tabs > .el-tabs__content {
|
||||||
|
padding: 32px;
|
||||||
|
color: #6b778c;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.fullScreen {
|
||||||
|
position: absolute;
|
||||||
|
right: 50px;
|
||||||
|
top: 86px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.el-tabs--top.el-tabs--card > .el-tabs__header .el-tabs__item:last-child {
|
||||||
|
padding-right: 0px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
176
frontend/src/views/host/terminal/terminal/terminal.vue
Normal file
176
frontend/src/views/host/terminal/terminal/terminal.vue
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
<template>
|
||||||
|
<div :id="'terminal' + props.terminalID"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||||
|
import { Terminal } from 'xterm';
|
||||||
|
import { AttachAddon } from 'xterm-addon-attach';
|
||||||
|
import { Base64 } from 'js-base64';
|
||||||
|
import 'xterm/css/xterm.css';
|
||||||
|
import { FitAddon } from 'xterm-addon-fit';
|
||||||
|
|
||||||
|
interface WsProps {
|
||||||
|
terminalID: string;
|
||||||
|
wsID: number;
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<WsProps>(), {
|
||||||
|
terminalID: '',
|
||||||
|
wsID: 0,
|
||||||
|
});
|
||||||
|
const fitAddon = new FitAddon();
|
||||||
|
const loading = ref(true);
|
||||||
|
let terminalSocket = ref(null) as unknown as WebSocket;
|
||||||
|
let term = ref(null) as unknown as Terminal;
|
||||||
|
|
||||||
|
const runRealTerminal = () => {
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onWSReceive = (message: any) => {
|
||||||
|
if (!isJson(message.data)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = JSON.parse(message.data);
|
||||||
|
term.element && term.focus();
|
||||||
|
term.write(data.Data);
|
||||||
|
};
|
||||||
|
|
||||||
|
function isJson(str: string) {
|
||||||
|
try {
|
||||||
|
if (typeof JSON.parse(str) === 'object') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorRealTerminal = (ex: any) => {
|
||||||
|
let message = ex.message;
|
||||||
|
if (!message) message = 'disconnected';
|
||||||
|
term.write(`\x1b[31m${message}\x1b[m\r\n`);
|
||||||
|
console.log('err');
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeRealTerminal = (ev: CloseEvent) => {
|
||||||
|
term.write(ev.reason);
|
||||||
|
};
|
||||||
|
|
||||||
|
const initTerm = () => {
|
||||||
|
let ifm = document.getElementById('terminal' + props.terminalID) as HTMLInputElement | null;
|
||||||
|
term = new Terminal({
|
||||||
|
lineHeight: 1.2,
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily: "Monaco, Menlo, Consolas, 'Courier New', monospace",
|
||||||
|
theme: {
|
||||||
|
background: '#000000',
|
||||||
|
},
|
||||||
|
cursorBlink: true,
|
||||||
|
cursorStyle: 'underline',
|
||||||
|
scrollback: 100,
|
||||||
|
tabStopWidth: 4,
|
||||||
|
});
|
||||||
|
if (ifm) {
|
||||||
|
term.open(ifm);
|
||||||
|
if (props.wsID === 0) {
|
||||||
|
terminalSocket = new WebSocket(
|
||||||
|
`ws://localhost:9999/api/v1/terminals/local?cols=${term.cols}&rows=${term.rows}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
terminalSocket = new WebSocket(
|
||||||
|
`ws://localhost:9999/api/v1/terminals?id=${props.wsID}&cols=${term.cols}&rows=${term.rows}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
terminalSocket.onopen = runRealTerminal;
|
||||||
|
terminalSocket.onmessage = onWSReceive;
|
||||||
|
terminalSocket.onclose = closeRealTerminal;
|
||||||
|
terminalSocket.onerror = errorRealTerminal;
|
||||||
|
term.onData((data: any) => {
|
||||||
|
if (isWsOpen()) {
|
||||||
|
terminalSocket.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'cmd',
|
||||||
|
cmd: Base64.encode(data),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
term.loadAddon(new AttachAddon(terminalSocket));
|
||||||
|
term.loadAddon(fitAddon);
|
||||||
|
setTimeout(() => {
|
||||||
|
fitAddon.fit();
|
||||||
|
if (isWsOpen()) {
|
||||||
|
terminalSocket.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'resize',
|
||||||
|
cols: term.cols,
|
||||||
|
rows: term.rows,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, 30);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fitTerm = () => {
|
||||||
|
fitAddon.fit();
|
||||||
|
};
|
||||||
|
|
||||||
|
const isWsOpen = () => {
|
||||||
|
const readyState = terminalSocket && terminalSocket.readyState;
|
||||||
|
return readyState === 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
function onClose() {
|
||||||
|
window.removeEventListener('resize', changeTerminalSize);
|
||||||
|
terminalSocket && terminalSocket.close();
|
||||||
|
term && term.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSendMsg(command: string) {
|
||||||
|
terminalSocket.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'cmd',
|
||||||
|
cmd: Base64.encode(command),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeTerminalSize() {
|
||||||
|
fitTerm();
|
||||||
|
const { cols, rows } = term;
|
||||||
|
if (isWsOpen()) {
|
||||||
|
terminalSocket.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'resize',
|
||||||
|
cols: cols,
|
||||||
|
rows: rows,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
onClose,
|
||||||
|
isWsOpen,
|
||||||
|
onSendMsg,
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
initTerm();
|
||||||
|
window.addEventListener('resize', changeTerminalSize);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
onClose();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
#terminal {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
@ -182,7 +182,7 @@ const passRules = reactive({
|
|||||||
Rules.requiredInput,
|
Rules.requiredInput,
|
||||||
{ min: 6, message: i18n.global.t('commons.rule.commonPassword'), trigger: 'blur' },
|
{ min: 6, message: i18n.global.t('commons.rule.commonPassword'), trigger: 'blur' },
|
||||||
],
|
],
|
||||||
newPasswordComplexity: [Rules.password],
|
newPasswordComplexity: [Rules.requiredInput, Rules.password],
|
||||||
retryPassword: [Rules.requiredInput, { validator: checkPassword, trigger: 'blur' }],
|
retryPassword: [Rules.requiredInput, { validator: checkPassword, trigger: 'blur' }],
|
||||||
});
|
});
|
||||||
const passwordVisiable = ref<boolean>(false);
|
const passwordVisiable = ref<boolean>(false);
|
||||||
|
@ -10,58 +10,6 @@
|
|||||||
<el-row>
|
<el-row>
|
||||||
<el-col :span="1"><br /></el-col>
|
<el-col :span="1"><br /></el-col>
|
||||||
<el-col :span="10">
|
<el-col :span="10">
|
||||||
<!-- <el-form-item
|
|
||||||
:label="$t('setting.panelPort')"
|
|
||||||
prop="settingInfo.serverPort"
|
|
||||||
:rules="Rules.number"
|
|
||||||
>
|
|
||||||
<el-input clearable v-model="form.settingInfo.serverPort">
|
|
||||||
<template #append>
|
|
||||||
<el-button
|
|
||||||
@click="onSave(panelFormRef, 'ServerPort', form.settingInfo.serverPort)"
|
|
||||||
icon="Collection"
|
|
||||||
>
|
|
||||||
{{ $t('commons.button.save') }}
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
<el-tooltip
|
|
||||||
class="box-item"
|
|
||||||
effect="dark"
|
|
||||||
content="Top Left prompts info"
|
|
||||||
placement="top-start"
|
|
||||||
>
|
|
||||||
<el-icon style="font-size: 14px; margin-top: 8px"><WarningFilled /></el-icon>
|
|
||||||
</el-tooltip>
|
|
||||||
</el-input>
|
|
||||||
<div>
|
|
||||||
<span class="input-help">
|
|
||||||
{{ $t('setting.portHelper') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</el-form-item> -->
|
|
||||||
<!-- <el-form-item
|
|
||||||
:label="$t('setting.safeEntrance')"
|
|
||||||
prop="settingInfo.securityEntrance"
|
|
||||||
:rules="Rules.requiredInput"
|
|
||||||
>
|
|
||||||
<el-input clearable v-model="form.settingInfo.securityEntrance">
|
|
||||||
<template #append>
|
|
||||||
<el-button
|
|
||||||
@click="
|
|
||||||
onSave(panelFormRef, 'SecurityEntrance', form.settingInfo.securityEntrance)
|
|
||||||
"
|
|
||||||
icon="Collection"
|
|
||||||
>
|
|
||||||
{{ $t('commons.button.save') }}
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
<div>
|
|
||||||
<span class="input-help">
|
|
||||||
{{ $t('setting.safeEntranceHelper') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</el-form-item> -->
|
|
||||||
<el-form-item
|
<el-form-item
|
||||||
:label="$t('setting.expirationTime')"
|
:label="$t('setting.expirationTime')"
|
||||||
prop="settingInfo.expirationTime"
|
prop="settingInfo.expirationTime"
|
||||||
@ -75,9 +23,15 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
<div>
|
<div>
|
||||||
<span class="input-help">
|
<span
|
||||||
|
class="input-help"
|
||||||
|
v-if="form.settingInfo.expirationTime !== $t('setting.unSetting')"
|
||||||
|
>
|
||||||
{{ $t('setting.timeoutHelper', [loadTimeOut()]) }}
|
{{ $t('setting.timeoutHelper', [loadTimeOut()]) }}
|
||||||
</span>
|
</span>
|
||||||
|
<span class="input-help" v-else>
|
||||||
|
{{ $t('setting.noneSetting') }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item
|
<el-form-item
|
||||||
@ -241,6 +195,7 @@ const submitTimeout = async (formEl: FormInstance | undefined) => {
|
|||||||
let time = new Date(new Date().getTime() + 3600 * 1000 * 24 * timeoutForm.days);
|
let time = new Date(new Date().getTime() + 3600 * 1000 * 24 * timeoutForm.days);
|
||||||
await updateSetting({ key: 'ExpirationDays', value: timeoutForm.days + '' });
|
await updateSetting({ key: 'ExpirationDays', value: timeoutForm.days + '' });
|
||||||
emit('search');
|
emit('search');
|
||||||
|
loadTimeOut();
|
||||||
form.settingInfo.expirationTime = dateFromat(0, 0, time);
|
form.settingInfo.expirationTime = dateFromat(0, 0, time);
|
||||||
timeoutVisiable.value = false;
|
timeoutVisiable.value = false;
|
||||||
});
|
});
|
||||||
@ -248,9 +203,14 @@ const submitTimeout = async (formEl: FormInstance | undefined) => {
|
|||||||
|
|
||||||
function loadTimeOut() {
|
function loadTimeOut() {
|
||||||
if (form.settingInfo.expirationDays === 0) {
|
if (form.settingInfo.expirationDays === 0) {
|
||||||
return '-';
|
form.settingInfo.expirationTime = i18n.global.t('setting.unSetting');
|
||||||
|
return i18n.global.t('setting.unSetting');
|
||||||
}
|
}
|
||||||
let staytimeGap = new Date(form.settingInfo.expirationTime).getTime() - new Date().getTime();
|
let staytimeGap = new Date(form.settingInfo.expirationTime).getTime() - new Date().getTime();
|
||||||
return staytimeGap < 0 ? '-' : Math.floor(staytimeGap / (3600 * 1000 * 24));
|
if (staytimeGap < 0) {
|
||||||
|
form.settingInfo.expirationTime = i18n.global.t('setting.unSetting');
|
||||||
|
return i18n.global.t('setting.unSetting');
|
||||||
|
}
|
||||||
|
return Math.floor(staytimeGap / (3600 * 1000 * 24));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user