mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-09-10 08:35:29 +08:00
459 lines
16 KiB
Vue
459 lines
16 KiB
Vue
<template>
|
|
<div>
|
|
<el-tabs tab-position="left" @tab-click="handleClick" v-model="routeTab" class="demo-tabs">
|
|
<el-tab-pane name="terminal">
|
|
<template #label>
|
|
<el-tooltip class="box-item" effect="dark" :content="$t('terminal.conn')" placement="right">
|
|
<el-icon><Connection /></el-icon>
|
|
</el-tooltip>
|
|
</template>
|
|
<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 - 210px); 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')"
|
|
size="small"
|
|
>
|
|
<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%"
|
|
size="small"
|
|
>
|
|
<template #append>
|
|
<el-switch size="small" 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 size="small" 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 - 210px)"
|
|
:description="$t('terminal.emptyTerminal')"
|
|
></el-empty>
|
|
</div>
|
|
</el-tabs>
|
|
<el-button @click="toggleFullscreen" class="fullScreen" icon="FullScreen"></el-button>
|
|
</el-tab-pane>
|
|
<el-tab-pane name="host">
|
|
<template #label>
|
|
<el-tooltip class="box-item" effect="dark" :content="$t('terminal.hostList')" placement="right">
|
|
<el-icon><Platform /></el-icon>
|
|
</el-tooltip>
|
|
</template>
|
|
<HostTab ref="hostTabRef" />
|
|
</el-tab-pane>
|
|
<el-tab-pane name="command">
|
|
<template #label>
|
|
<el-tooltip class="box-item" effect="dark" :content="$t('terminal.quickCmd')" placement="right">
|
|
<el-icon><Reading /></el-icon>
|
|
</el-tooltip>
|
|
</template>
|
|
<CommandTab ref="commandTabRef" />
|
|
</el-tab-pane>
|
|
</el-tabs>
|
|
|
|
<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>
|
|
|
|
<script setup lang="ts">
|
|
import { onMounted, onBeforeMount, ref, watch, reactive, getCurrentInstance } from 'vue';
|
|
import { Rules } from '@/global/form-rues';
|
|
import { testConn, getHostTree, addHost } from '@/api/modules/host';
|
|
import { getCommandList } from '@/api/modules/command';
|
|
import i18n from '@/lang';
|
|
import { ElForm } from 'element-plus';
|
|
import { Host } from '@/api/interface/host';
|
|
import { ElMessage } from 'element-plus';
|
|
import Terminal from '@/views/terminal/terminal/index.vue';
|
|
import HostTab from '@/views/terminal/host/index.vue';
|
|
import CommandTab from '@/views/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 routeTab = 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({
|
|
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],
|
|
});
|
|
|
|
let hostInfo = reactive<Host.HostOperate>({
|
|
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 handleClick = (tab: any) => {
|
|
if (tab.paneName === 'host') {
|
|
if (ctx) {
|
|
ctx.refs[`hostTabRef`] && ctx.refs[`hostTabRef`].onInit();
|
|
}
|
|
}
|
|
if (tab.paneName === '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;
|
|
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(() => {
|
|
onConnLocal();
|
|
loadHost();
|
|
loadCommand();
|
|
timer = setInterval(() => {
|
|
syncTerminal();
|
|
}, 1000 * 8);
|
|
});
|
|
onBeforeMount(() => {
|
|
clearInterval(Number(timer));
|
|
});
|
|
</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 {
|
|
padding: 32px;
|
|
color: #6b778c;
|
|
font-size: 32px;
|
|
font-weight: 600;
|
|
}
|
|
.fullScreen {
|
|
position: absolute;
|
|
right: 50px;
|
|
top: 6px;
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
}
|
|
.el-tabs--top.el-tabs--card > .el-tabs__header .el-tabs__item:last-child {
|
|
padding-right: 0px;
|
|
}
|
|
</style>
|