支持快捷脚本&简化客户端安装脚本

This commit is contained in:
chaos-zhu 2024-07-31 18:05:39 +08:00
parent af9f762c25
commit 0c6ea82be5
23 changed files with 519 additions and 193 deletions

View File

@ -81,16 +81,10 @@ pm2 start index.js --name easynode-server
- 占用端口:**22022**
#### X86架构
> 安装
```shell
wget -qO- --no-check-certificate https://mirror.ghproxy.com/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install-x86.sh | bash
```
#### ARM架构
```shell
wget -qO- --no-check-certificate https://mirror.ghproxy.com/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install-arm.sh | bash
wget -qO- --no-check-certificate https://mirror.ghproxy.com/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-instal.sh | bash
```
> 卸载

View File

@ -1,90 +0,0 @@
#!/bin/bash
if [ "$(id -u)" != "0" ] ; then
echo "***********************请切换到root再尝试执行***********************"
exit 1
fi
SERVER_NAME=easynode-client
FILE_PATH=/root/local/easynode-client
SERVICE_PATH=/etc/systemd/system
SERVER_VERSION=v1.0
echo "***********************开始安装 easynode-client_${SERVER_VERSION}***********************"
systemctl status ${SERVER_NAME} > /dev/null 2>&1
if [ $? != 4 ]
then
echo "***********************停用旧服务***********************"
systemctl stop ${SERVER_NAME}
systemctl disable ${SERVER_NAME}
systemctl daemon-reload
fi
if [ -f "${SERVICE_PATH}/${SERVER_NAME}.service" ]
then
echo "***********************移除旧服务***********************"
chmod 777 ${SERVICE_PATH}/${SERVER_NAME}.service
rm -Rf ${SERVICE_PATH}/${SERVER_NAME}.service
systemctl daemon-reload
fi
if [ -d ${FILE_PATH} ]
then
echo "***********************移除旧文件***********************"
chmod 777 ${FILE_PATH}
rm -Rf ${FILE_PATH}
fi
# 开始安装
echo "***********************创建文件PATH***********************"
mkdir -p ${FILE_PATH}
echo "***********************下载开始***********************"
DOWNLOAD_FILE_URL="https://mirror.ghproxy.com/https://github.com/chaos-zhu/easynode/releases/download/v2.0.0/easynode-client-x86"
DOWNLOAD_SERVICE_URL="https://mirror.ghproxy.com/https://github.com/chaos-zhu/easynode/releases/download/v2.0.0/easynode-client.service"
# -O 指定路径和文件名(这里是二进制文件, 不需要扩展名)
wget -O ${FILE_PATH}/${SERVER_NAME} --no-check-certificate --no-cache ${DOWNLOAD_FILE_URL}
if [ $? != 0 ]
then
echo "***********************下载${SERVER_NAME}失败***********************"
exit 1
fi
wget -O ${FILE_PATH}/${SERVER_NAME}.service --no-check-certificate --no-cache ${DOWNLOAD_SERVICE_URL}
if [ $? != 0 ]
then
echo "***********************下载${SERVER_NAME}.service失败***********************"
exit 1
fi
echo "***********************下载成功***********************"
# echo "***********************设置权限***********************"
chmod +x ${FILE_PATH}/${SERVER_NAME}
chmod 777 ${FILE_PATH}/${SERVER_NAME}.service
# echo "***********************移动service&reload***********************"
mv ${FILE_PATH}/${SERVER_NAME}.service ${SERVICE_PATH}
# echo "***********************daemon-reload***********************"
systemctl daemon-reload
echo "***********************准备启动服务***********************"
systemctl start ${SERVER_NAME}
if [ $? != 0 ]
then
echo "***********************${SERVER_NAME}.service启动失败***********************"
echo "***********************可能是服务器开启了SELinux, 参见Q&A***********************"
exit 1
fi
echo "***********************服务启动成功***********************"
# echo "***********************设置开机启动***********************"
systemctl enable ${SERVER_NAME}
echo "***********************安装成功***********************"

View File

@ -42,9 +42,19 @@ echo "***********************创建文件PATH***********************"
mkdir -p ${FILE_PATH}
echo "***********************下载开始***********************"
DOWNLOAD_FILE_URL="https://mirror.ghproxy.com/https://github.com/chaos-zhu/easynode/releases/download/v2.0.0/easynode-client-arm64"
DOWNLOAD_SERVICE_URL="https://mirror.ghproxy.com/https://github.com/chaos-zhu/easynode/releases/download/v2.0.0/easynode-client.service"
ARCH=$(uname -m)
if [ "$ARCH" = "x86_64" ] ; then
DOWNLOAD_FILE_URL="https://mirror.ghproxy.com/https://github.com/chaos-zhu/easynode/releases/download/v2.0.0/easynode-client-x86"
elif [ "$ARCH" = "aarch64" ] ; then
DOWNLOAD_FILE_URL="https://mirror.ghproxy.com/https://github.com/chaos-zhu/easynode/releases/download/v2.0.0/easynode-client-arm64"
else
echo "未知的架构:$ARCH"
exit 1
fi
# -O 指定路径和文件名(这里是二进制文件, 不需要扩展名)
wget -O ${FILE_PATH}/${SERVER_NAME} --no-check-certificate --no-cache ${DOWNLOAD_FILE_URL}
if [ $? != 0 ]

View File

@ -14,6 +14,7 @@ module.exports = {
notifyConfDBPath: path.join(process.cwd(),'app/db/notify.db'),
groupConfDBPath: path.join(process.cwd(),'app/db/group.db'),
emailNotifyDBPath: path.join(process.cwd(),'app/db/email.db'),
scriptsDBPath: path.join(process.cwd(),'app/db/scripts.db'),
apiPrefix: '/api/v1',
logConfig: {
outDir: path.join(process.cwd(),'./app/logs'),

View File

@ -16,7 +16,7 @@ const addGroupList = async ({ res, request }) => {
let group = { name, index }
groupList.push(group)
await writeGroupList(groupList)
res.success({ data: '新增成功' })
res.success({ data: '添加成功' })
}
const updateGroupList = async ({ res, request }) => {

View File

@ -57,7 +57,7 @@ async function updateHost({
name: newName, host: newHost, index, expired, expiredNotify, group, consoleUrl, remark,
port, username, authType, password, privateKey, credential, command
}
if (!hostList.some(({ host }) => host === oldHost)) return res.fail({ msg: `原实例[${ oldHost }]不存在,请尝试新增实例` })
if (!hostList.some(({ host }) => host === oldHost)) return res.fail({ msg: `原实例[${ oldHost }]不存在,请尝试添加实例` })
let idx = hostList.findIndex(({ host }) => host === oldHost)
const oldRecord = hostList[idx]

View File

@ -0,0 +1,52 @@
const { readScriptList, writeScriptList } = require('../utils')
async function getScriptList({ res }) {
let data = await readScriptList()
data = data.map(item => {
return { ...item, id: item._id }
})
data?.sort((a, b) => Number(b.index || 0) - Number(a.index || 0))
res.success({ data })
}
const addScript = async ({ res, request }) => {
let { body: { name, remark, content, index } } = request
if (!name || !content) return res.fail({ data: false, msg: '参数错误' })
index = Number(index) || 0
let scriptsList = await readScriptList()
let record = { name, remark, content, index }
scriptsList.push(record)
await writeScriptList(scriptsList)
res.success({ data: '添加成功' })
}
const updateScriptList = async ({ res, request }) => {
let { params: { id } } = request
let { body: { name, remark, content, index } } = request
if (!name || !content) return res.fail({ data: false, msg: '参数错误' })
let scriptsList = await readScriptList()
let idx = scriptsList.findIndex(item => item._id === id)
if (idx === -1) return res.fail({ data: false, msg: `脚本ID${ id }不存在` })
const { _id } = scriptsList[idx]
let record = Object.assign({ _id }, { name, remark, content, index })
scriptsList.splice(idx, 1, record)
await writeScriptList(scriptsList)
res.success({ data: '修改成功' })
}
const removeScript = async ({ res, request }) => {
let { params: { id } } = request
let scriptsList = await readScriptList()
let idx = scriptsList.findIndex(item => item._id === id)
if (idx === -1) return res.fail({ msg: '脚本ID不存在' })
scriptsList.splice(idx, 1)
await writeScriptList(scriptsList)
res.success({ data: '移除成功' })
}
module.exports = {
addScript,
getScriptList,
updateScriptList,
removeScript
}

View File

@ -27,7 +27,7 @@ const addSSH = async ({ res, request }) => {
sshRecord.push({ ...record, date: Date.now() })
await writeSSHRecord(sshRecord)
consola.info('新增凭证:', name)
consola.info('添加凭证:', name)
res.success({ data: '保存成功' })
}

View File

@ -3,6 +3,7 @@ const { getHostList, addHost, updateHost, removeHost, importHost } = require('..
const { login, getpublicKey, updatePwd, getLoginRecord } = require('../controller/user')
const { getSupportEmailList, getUserEmailList, updateUserEmailList, removeUserEmail, pushEmail, getNotifyList, updateNotifyList } = require('../controller/notify')
const { getGroupList, addGroupList, updateGroupList, removeGroup } = require('../controller/group')
const { getScriptList, addScript, updateScriptList, removeScript } = require('../controller/scripts')
const ssh = [
{
@ -141,4 +142,27 @@ const group = [
}
]
module.exports = [].concat(ssh, host, user, notify, group)
const scripts = [
{
method: 'get',
path: '/script',
controller: getScriptList
},
{
method: 'post',
path: '/script',
controller: addScript
},
{
method: 'delete',
path: '/script/:id',
controller: removeScript
},
{
method: 'put',
path: '/script/:id',
controller: updateScriptList
}
]
module.exports = [].concat(ssh, host, user, notify, group, scripts)

View File

@ -37,7 +37,7 @@ async function getClientsInfo(socketId) {
})
})
.on('connect_error', (error) => {
consola.error('client connect fail:', host, name, error.message)
// consola.error('client connect fail:', host, name, error.message)
clientsData[host] = null
})
.on('disconnect', () => {

View File

@ -1,5 +1,5 @@
const Datastore = require('@seald-io/nedb')
const { credentialsDBPath, hostListDBPath, keyDBPath, emailNotifyDBPath, notifyConfDBPath, groupConfDBPath } = require('../config')
const { credentialsDBPath, hostListDBPath, keyDBPath, emailNotifyDBPath, notifyConfDBPath, groupConfDBPath, scriptsDBPath } = require('../config')
module.exports.KeyDB = class KeyDB {
constructor() {
@ -66,3 +66,14 @@ module.exports.EmailNotifyDB = class EmailNotifyDB {
return EmailNotifyDB.instance
}
}
module.exports.ScriptsDB = class ScriptsDB {
constructor() {
if (!ScriptsDB.instance) {
ScriptsDB.instance = new Datastore({ filename: scriptsDBPath, autoload: true })
}
}
getInstance() {
return ScriptsDB.instance
}
}

View File

@ -12,7 +12,9 @@ const {
getNotifySwByType,
writeNotifyList,
readGroupList,
writeGroupList
writeGroupList,
readScriptList,
writeScriptList
} = require('./storage')
const { RSADecryptSync, AESEncryptSync, AESDecryptSync, SHA1Encrypt } = require('./encrypt')
const { verifyAuthSync, isProd } = require('./verify-auth')
@ -47,5 +49,7 @@ module.exports = {
getNotifySwByType,
writeNotifyList,
readGroupList,
writeGroupList
writeGroupList,
readScriptList,
writeScriptList
}

View File

@ -1,4 +1,4 @@
const { KeyDB, HostListDB, SshRecordDB, NotifyDB, GroupDB, EmailNotifyDB } = require('./db-class')
const { KeyDB, HostListDB, SshRecordDB, NotifyDB, GroupDB, EmailNotifyDB, ScriptsDB } = require('./db-class')
const readKey = async () => {
return new Promise((resolve, reject) => {
@ -234,6 +234,42 @@ const writeGroupList = async (list = []) => {
})
}
const readScriptList = async () => {
return new Promise((resolve, reject) => {
const scriptsDB = new ScriptsDB().getInstance()
scriptsDB.find({}, (err, docs) => {
if (err) {
consola.error('读取scripts list错误: ', err)
reject(err)
} else {
resolve(docs)
}
})
})
}
const writeScriptList = async (list = []) => {
return new Promise((resolve, reject) => {
const scriptsDB = new ScriptsDB().getInstance()
scriptsDB.remove({}, { multi: true }, (err) => {
if (err) {
consola.error('清空group list出错:', err)
reject(err)
} else {
scriptsDB.insert(list, (err, newDocs) => {
if (err) {
consola.error('写入新的group list出错:', err)
reject(err)
} else {
scriptsDB.compactDatafile()
resolve(newDocs)
}
})
}
})
})
}
module.exports = {
readSSHRecord,
writeSSHRecord,
@ -248,5 +284,7 @@ module.exports = {
writeGroupList,
readSupportEmailList,
readUserEmailList,
writeUserEmailList
writeUserEmailList,
readScriptList,
writeScriptList
}

View File

@ -87,5 +87,17 @@ export default {
},
deleteGroup(id) {
return axios({ url: `/group/${ id }`, method: 'delete' })
},
getScriptList() {
return axios({ url: '/script', method: 'get' })
},
addScript(data) {
return axios({ url: '/script', method: 'post', data })
},
updateScript(id, data) {
return axios({ url: `/script/${ id }`, method: 'put', data })
},
deleteScript(id) {
return axios({ url: `/script/${ id }`, method: 'delete' })
}
}

View File

@ -62,16 +62,16 @@ let menuList = reactive([
icon: markRaw(FolderOpened),
index: '/group'
},
{
name: '脚本库',
icon: markRaw(ArrowRight),
index: '/scripts'
},
// {
// name: '',
// icon: markRaw(Pointer),
// index: '/onekey'
// },
// {
// name: '',
// icon: markRaw(ArrowRight),
// index: '/scripts'
// },
{
name: '系统设置',
icon: markRaw(Setting),

View File

@ -10,6 +10,7 @@ const useStore = defineStore({
hostList: [],
groupList: [],
sshList: [],
scriptList: [],
HostStatusSocket: null,
user: localStorage.getItem('user') || null,
token: sessionStorage.getItem('token') || localStorage.getItem('token') || null,
@ -37,6 +38,7 @@ const useStore = defineStore({
await this.getGroupList()
await this.getHostList()
await this.getSSHList()
await this.getScriptList()
},
async getHostList() {
const { data: hostList } = await $api.getHostList()
@ -54,6 +56,11 @@ const useStore = defineStore({
// console.log('sshList:', sshList)
this.$patch({ sshList })
},
async getScriptList() {
const { data: scriptList } = await $api.getScriptList()
// console.log('scriptList:', scriptList)
this.$patch({ scriptList })
},
getHostPing() {
setTimeout(() => {
this.hostList.forEach((item) => {

View File

@ -80,7 +80,7 @@
<template #footer>
<span>
<el-button @click="sshFormVisible = false">关闭</el-button>
<el-button type="primary" @click="updateForm">{{ isModify ? '修改' : '新增' }}</el-button>
<el-button type="primary" @click="updateForm">{{ isModify ? '修改' : '添加' }}</el-button>
</span>
</template>
</el-dialog>

View File

@ -44,7 +44,7 @@
<template #footer>
<span>
<el-button @click="groupFormVisible = false">关闭</el-button>
<el-button type="primary" @click="updateForm">{{ isModify ? '修改' : '新增' }}</el-button>
<el-button type="primary" @click="updateForm">{{ isModify ? '修改' : '添加' }}</el-button>
</span>
</template>
</el-dialog>

View File

@ -1,19 +1,175 @@
<template>
<div class="">
开发中...
<div class="scripts_container">
<div class="header">
<el-button type="primary" @click="addScript">添加脚本</el-button>
</div>
<el-table v-loading="loading" :data="scriptList">
<el-table-column prop="index" label="序号" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="remark" label="备注" />
<el-table-column prop="content" label="脚本内容" />
<el-table-column label="操作">
<template #default="{ row }">
<el-button type="primary" @click="handleChange(row)">修改</el-button>
<el-button v-show="row.id !== 'own'" type="danger" @click="handleRemove(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog
v-model="formVisible"
width="600px"
top="150px"
:title="isModify ? '修改脚本' : '添加脚本'"
:close-on-click-modal="false"
@close="clearFormInfo"
>
<el-form
ref="updateFormRef"
:model="formData"
:rules="rules"
:hide-required-asterisk="true"
label-suffix=""
label-width="100px"
:show-message="false"
>
<el-form-item label="脚本名称" prop="name">
<el-input
v-model.trim="formData.name"
clearable
placeholder=""
autocomplete="off"
/>
</el-form-item>
<el-form-item label="脚本备注" prop="remark">
<el-input
v-model.trim="formData.remark"
clearable
placeholder=""
autocomplete="off"
/>
</el-form-item>
<el-form-item label="脚本序号" prop="index">
<el-input
v-model.trim.number="formData.index"
clearable
placeholder=""
autocomplete="off"
/>
</el-form-item>
<el-form-item prop="content" label="脚本内容" show-overflow-tooltip>
<el-input
v-model.trim="formData.content"
type="textarea"
:rows="5"
clearable
autocomplete="off"
style="margin-top: 5px;"
placeholder="shell script"
/>
</el-form-item>
</el-form>
<template #footer>
<span>
<el-button @click="formVisible = false">关闭</el-button>
<el-button type="primary" @click="updateForm">{{ isModify ? '修改' : '添加' }}</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
export default {
<script setup>
import { ref, reactive, computed, nextTick, getCurrentInstance } from 'vue'
const { proxy: { $api, $message, $messageBox, $store } } = getCurrentInstance()
const loading = ref(false)
const formVisible = ref(false)
let isModify = ref(false)
let formData = reactive({
name: '',
data() {
return {
}
remark: '',
index: 0,
content: ''
})
const rules = computed(() => {
return {
name: { required: true, trigger: 'change' },
remark: { required: false, trigger: 'change' },
index: { required: false, type: 'number', trigger: 'change' },
content: { required: true, trigger: 'change' }
}
})
const updateFormRef = ref(null)
let scriptList = computed(() => $store.scriptList)
let addScript = () => {
formData.id = null
isModify.value = false
formVisible.value = true
}
const handleChange = (row) => {
Object.assign(formData, { ...row })
formVisible.value = true
isModify.value = true
}
function updateForm() {
updateFormRef.value.validate()
.then(async () => {
let data = { ...formData }
if (isModify.value) {
await $api.updateScript(data.id, data)
} else {
await $api.addScript(data)
}
formVisible.value = false
await $store.getScriptList()
$message.success('success')
})
}
const clearFormInfo = () => {
nextTick(() => updateFormRef.value.resetFields())
}
const handleRemove = ({ id, name }) => {
$messageBox.confirm(`确认删除该脚本:${ name }`, 'Warning', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
await $api.deleteScript(id)
await $store.getScriptList()
$message.success('success')
})
}
</script>
<style lang="scss" scoped>
.scripts_container {
padding: 20px;
.header {
padding: 15px;
display: flex;
align-items: center;
justify-content: end;
}
}
</style>
.host_count {
display: block;
width: 100px;
text-align: center;
font-size: 15px;
color: #87cf63;
cursor: pointer;
}
</style>

View File

@ -288,7 +288,7 @@ const visible = computed({
set: (newVal) => emit('update:show', newVal)
})
const title = computed(() => props.defaultData ? '修改实例' : '新增实例')
const title = computed(() => props.defaultData ? '修改实例' : '添加实例')
let groupList = computed(() => $store.groupList)
let sshList = computed(() => $store.sshList)

View File

@ -190,7 +190,7 @@
</el-button> -->
<el-button
:type="inputCommandStyle ? 'primary' : 'success'"
style="display: block;width: 80%;margin: 30px auto;"
style="display: block;width: 80%;margin: 15px auto;"
@click="clickInputCommand"
>
命令输入框

View File

@ -26,7 +26,7 @@ const props = defineProps({
}
})
const emit = defineEmits(['input',])
const emit = defineEmits(['inputCommand',])
const socket = ref(null)
const term = ref(null)
@ -206,8 +206,7 @@ const onData = () => {
let acsiiCode = key.codePointAt()
if (acsiiCode === 22) return handlePaste()
if (acsiiCode === 6) return searchBar.value.show()
emit('input', { idx: props.index, key })
// console.log('input:', key)
emit('inputCommand', key)
socket.value.emit('input', key)
})
}
@ -218,8 +217,7 @@ const handleClear = () => {
const handlePaste = async () => {
let key = await navigator.clipboard.readText()
emit('input', { idx: props.index, key })
// console.log('input:', key)
emit('inputCommand', key)
socket.value.emit('input', key)
term.value.focus()
}
@ -231,7 +229,7 @@ const focusTab = () => {
}, 200)
}
const handleInputCommand = (command) => {
const inputCommand = (command) => {
socket.value.emit('input', command)
}
@ -250,7 +248,7 @@ onBeforeUnmount(() => {
defineExpose({
focusTab,
handleResize,
handleInputCommand,
inputCommand,
handleClear
})
</script>

View File

@ -1,34 +1,99 @@
<template>
<div class="terminal_wrap">
<div class="terminal_top">
<el-dropdown trigger="click">
<span class="link_text">新建连接<el-icon><arrow-down /></el-icon></span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="(item, index) in hostList" :key="index" @click="handleCommandHost(item)">
{{ item.name }} {{ item.host }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-dropdown trigger="click">
<span class="link_text">会话同步<el-icon><arrow-down /></el-icon></span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleSyncSession">
<el-icon v-show="isSyncAllSession"><Select class="action_icon" /></el-icon>
<span>同步键盘输入到所有会话</span>
</el-dropdown-item>
<!-- <el-dropdown-item @click="handleSyncSession">
同步键盘输入到部分会话
</el-dropdown-item> -->
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- <div class="link_text fullscreen" @click="handleFullScreen">全屏</div> -->
<el-icon class="full_icon">
<FullScreen class="icon" @click="handleFullScreen" />
</el-icon>
<div class="left_menu">
<el-dropdown trigger="click">
<span class="link_text">新建连接<el-icon><arrow-down /></el-icon></span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="(item, index) in hostList" :key="index" @click="handleCommandHost(item)">
{{ item.name }} {{ item.host }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- <el-dropdown trigger="click">
<span class="link_text">会话同步<el-icon><arrow-down /></el-icon></span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleSyncSession">
<el-icon v-show="isSyncAllSession"><Select class="action_icon" /></el-icon>
<span>同步键盘输入到所有会话</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown> -->
<el-dropdown
trigger="click"
max-height="50vh"
:teleported="false"
class="scripts_menu"
>
<span class="link_text">快捷命令<el-icon><arrow-down /></el-icon></span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="item in scriptList" :key="item.id" @click="handleExecScript(item)">
<span>{{ item.name }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-dropdown trigger="click">
<span class="link_text">设置<el-icon><arrow-down /></el-icon></span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handleFullScreen">
<span>开启全屏</span>
</el-dropdown-item>
<el-dropdown-item disabled @click="handleFullScreen">
<span>终端设置(开发中)</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- <el-dropdown trigger="click">
<span class="link_text">设置
<el-icon class="hidden_icon"><arrow-down /></el-icon>
</span>
</el-dropdown> -->
</div>
<div class="right_overview">
<div class="switch_wrap">
<el-tooltip
effect="dark"
content="开启后发送键盘输入到所有会话"
placement="top"
>
<el-switch
v-model="isSyncAllSession"
class="swtich"
inline-prompt
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
active-text="同步"
inactive-text="同步"
/>
</el-tooltip>
</div>
<div class="switch_wrap">
<el-tooltip
effect="dark"
content="SFTP文件传输"
placement="top"
>
<el-switch
v-model="showSftp"
class="swtich"
inline-prompt
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
active-text="SFTP"
inactive-text="SFTP"
/>
</el-tooltip>
</div>
<!-- <el-icon class="full_icon">
<FullScreen class="icon" @click="handleFullScreen" />
</el-icon> -->
</div>
</div>
<div class="info_box">
<InfoSide
@ -56,12 +121,12 @@
>
<div class="tab_content_wrap" :style="{ height: mainHeight + 'px' }">
<TerminalTab
ref="terminalTabRefs"
ref="terminalRefs"
:index="index"
:host="item.host"
@input="terminalInput"
@input-command="terminalInput"
/>
<Sftp :host="item.host" @resize="resizeTerminal" />
<Sftp v-if="showSftp" :host="item.host" @resize="resizeTerminal" />
</div>
</el-tab-pane>
</el-tabs>
@ -98,9 +163,10 @@ const emit = defineEmits(['closed', 'removeTab', 'add-host',])
const showInputCommand = ref(false)
const infoSideRef = ref(null)
const terminalTabRefs = ref([])
const terminalRefs = ref([])
let activeTabIndex = ref(0)
let visible = ref(true)
let showSftp = ref(false)
let mainHeight = ref('')
let isSyncAllSession = ref(false)
let hostFormVisible = ref(false)
@ -110,6 +176,7 @@ const terminalTabs = computed(() => props.terminalTabs)
const terminalTabsLen = computed(() => props.terminalTabs.length)
const curHost = computed(() => terminalTabs.value[activeTabIndex.value])
let hostList = computed(() => $store.hostList)
let scriptList = computed(() => $store.scriptList)
// const closable = computed(() => terminalTabs.length > 1)
@ -135,7 +202,7 @@ const handleUpdateList = async ({ isConfig, host }) => {
}
}
function handleResizeTerminalSftp() {
const handleResizeTerminalSftp = () => {
$nextTick(() => {
mainHeight.value = document.querySelector('.terminals_sftp_wrap').offsetHeight - 45 // 45 is tab-header height+15
})
@ -157,25 +224,33 @@ const handleSyncSession = () => {
else $message.info('已关闭键盘输入到所有会话')
}
const terminalInput = ({ idx, key }) => {
if (!isSyncAllSession.value) return
let filterHostList = terminalTabRefs.value.filter((host, index) => {
return index !== idx
const handleExecScript = (scriptObj) => {
// console.log(scriptObj.content)
if (!isSyncAllSession.value) return handleInputCommand(scriptObj.content)
terminalRefs.value.forEach(terminalRef => {
terminalRef.inputCommand(scriptObj.content)
})
filterHostList.forEach(item => {
item.handleInputCommand(key)
}
const terminalInput = (command) => {
if (!isSyncAllSession.value) return
let filterTerminalRefs = terminalRefs.value.filter((host, index) => {
return index !== activeTabIndex.value
})
filterTerminalRefs.forEach(hostRef => {
hostRef.inputCommand(command)
})
}
const tabChange = async (index) => {
await $nextTick()
const curTabTerminal = terminalTabRefs.value[index]
curTabTerminal?.focusTab()
const curTerminalRef = terminalRefs.value[index]
curTerminalRef?.focusTab()
}
watch(terminalTabsLen, () => {
let len = terminalTabsLen.value
console.log('add tab:', len)
// console.log('add tab:', len)
if (len > 0) {
activeTabIndex.value = len - 1
// registryDbClick()
@ -222,23 +297,23 @@ const handleFullScreen = () => {
// removeTab(key)
// }
const handleVisibleSidebar = () => {
visible.value = !visible.value
resizeTerminal()
}
// const handleVisibleSidebar = () => {
// visible.value = !visible.value
// resizeTerminal()
// }
const resizeTerminal = () => {
for (let terminalTabRef of terminalTabRefs.value) {
for (let terminalTabRef of terminalRefs.value) {
const { handleResize } = terminalTabRef || {}
handleResize && handleResize()
}
}
const handleInputCommand = async (command) => {
const curTabTerminal = terminalTabRefs.value[activeTabIndex.value]
const curTerminalRef = terminalRefs.value[activeTabIndex.value]
await $nextTick()
curTabTerminal?.focusTab()
curTabTerminal.handleInputCommand(`${ command }\n`)
curTerminalRef?.focusTab()
curTerminalRef.inputCommand(`${ command }\n`)
showInputCommand.value = false
}
</script>
@ -265,34 +340,68 @@ const handleInputCommand = async (command) => {
}
$terminalTopHeight: 30px;
.terminal_top {
width: 100%;
height: $terminalTopHeight;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 15px;
border-bottom: 1px solid var(--el-color-primary);
position: sticky;
top: 0;
background-color: #fff;
border-bottom: 1px solid #fff;
// background-color: #fff;
background: var(--el-fill-color-light);
color: var(--el-text-color-regular);
z-index: 3;
:deep(.el-dropdown) {
margin-top: -2px;
user-select: none;
// :deep(.el-dropdown) {
// margin-top: -2px;
// }
.scripts_menu {
:deep(.el-dropdown-menu) {
min-width: 100px;
max-width: 300px;
}
}
.link_text {
font-size: var(--el-font-size-base);
color: var(--el-color-primary);
color: var(--el-text-color-regular);
// color: var(--el-color-primary);
cursor: pointer;
margin-right: 15px;
.hidden_icon {
opacity: 0;
}
}
.full_icon {
cursor: pointer;
margin-left: auto;
&:hover .icon {
color: var(--el-color-primary);
.left_menu {
display: flex;
align-items: center;
}
.right_overview {
display: flex;
align-items: center;
.switch_wrap {
display: flex;
align-items: center;
margin-right: 5px;
}
.full_icon {
cursor: pointer;
&:hover .icon {
color: var(--el-color-primary);
}
}
}
}
.info_box {
height: calc(100% - $terminalTopHeight);
overflow: auto;