✨ 支持快捷脚本&简化客户端安装脚本
This commit is contained in:
parent
af9f762c25
commit
0c6ea82be5
10
README.md
10
README.md
@ -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
|
||||
```
|
||||
|
||||
> 卸载
|
||||
|
@ -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 "***********************安装成功***********************"
|
@ -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 ]
|
@ -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'),
|
||||
|
@ -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 }) => {
|
||||
|
@ -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]
|
||||
|
52
server/app/controller/scripts.js
Normal file
52
server/app/controller/scripts.js
Normal 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
|
||||
}
|
@ -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: '保存成功' })
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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', () => {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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' })
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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) => {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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)
|
||||
|
@ -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"
|
||||
>
|
||||
命令输入框
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user