1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-01-19 08:19:15 +08:00

feat: 网站分组修改

This commit is contained in:
zhengkunwang223 2022-11-25 18:59:49 +08:00 committed by ssongliu
parent 0e3d9d1f3f
commit a2fb8353de
14 changed files with 820 additions and 818 deletions

View File

@ -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

View File

@ -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:

View File

@ -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} 天后 面板密码即将过期过期后需要重新设置密码',

View File

@ -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,

View File

@ -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;
} }
} }

View File

@ -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);

View File

@ -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;
}
}); });
}; };

View File

@ -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>

View File

@ -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>();

View File

@ -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>&nbsp;{{ item.title }}&nbsp;&nbsp;</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;

View File

@ -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>&nbsp;{{ item.title }}&nbsp;&nbsp;</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>

View 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>

View File

@ -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);

View File

@ -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>