✨ 增强脚本输入功能&优化与修复若干项

This commit is contained in:
chaos-zhu
2025-03-20 00:57:54 +08:00
parent ab3d98da32
commit def573c06b
35 changed files with 1500 additions and 352 deletions
+17
View File
@@ -1,3 +1,20 @@
## [3.0.5](https://github.com/chaos-zhu/easynode/releases) (2025-03-18)
* 新增AI问答&集成脚本生成功能(TODO
* 新增服务器定时任务功能(TODO
* 连接服务器下拉菜单分组展示(TODO
* 增加升级更新内容展示(TODO
## [3.0.4](https://github.com/chaos-zhu/easynode/releases) (2025-03-15)
* 新增脚本库分组功能
* 终端页新增脚本库布局
* 优化多行命令编辑器
* 优化终端工具栏布局
* 优化多项新增序号自动累加
* 修复前端连接断开但未释放服务端ssh连接的bug
* 修复终端页面同步会话在自动执行脚本开关为true时自动添加回车的bug
## [3.0.3](https://github.com/chaos-zhu/easynode/releases) (2024-12-22)
* 支持keyboard-interactive服务器验证(serv00验证通过)
+1
View File
@@ -13,6 +13,7 @@ module.exports = {
hostListDBPath: path.join(process.cwd(),'app/db/host.db'),
groupConfDBPath: path.join(process.cwd(),'app/db/group.db'),
scriptsDBPath: path.join(process.cwd(),'app/db/scripts.db'),
scriptGroupDBPath: path.join(process.cwd(),'app/db/script-group.db'),
notifyDBPath: path.join(process.cwd(),'app/db/notify.db'),
notifyConfigDBPath: path.join(process.cwd(),'app/db/notify-config.db'),
onekeyDBPath: path.join(process.cwd(),'app/db/onekey.db'),
+7
View File
@@ -0,0 +1,7 @@
const prompt = 'You are an expert at using shell commands.\nI need you to provide a response in the format \n`[{"command": "your_shell_command_here", "desc": "命令描述(永远使用中文描述命令的用途,不要超过20个字)"}]`\nOnly provide a single executable line of shell code as the value for the "command" and "desc" key.\nNever output any text outside the JSON structure.\nThe command will be directly executed in a shell.\nFor example, if I ask to display the message abc, you should respond with ```json\\n{"command": "echo abc", "desc": "打印 abc 到终端"}\\n```.\nif I ask to Debian sets swap memory to 2GB, you should respond with: ```json\\n[ {"command": "sudo fallocate -l 2G /swapfile", "desc": "创建2GB大小的交换文件"}, {"command": "sudo chmod 600 /swapfile", "desc": "设置交换文件权限为600"}, {"command": "sudo mkswap /swapfile", "desc": "将文件设置为交换分区"}, {"command": "sudo swapon /swapfile", "desc": "启用交换分区"}, {"command": "echo \'/swapfile none swap sw 0 0\' | sudo tee -a /etc/fstab", "desc": "开机自动挂载交换分区"} ]```\nMake sure the output is valid JSON.'
// const response = await openai.chat.completions.create({
// model: "gpt-4o-mini",
// messages: [{ role: "user", content: prompt }],
// response_format: { type: "json_object" },
// });
+1 -1
View File
@@ -1 +1 @@
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
+45
View File
@@ -0,0 +1,45 @@
const path = require('path')
const { ScriptGroupDB } = require('../utils/db-class')
const decryptAndExecuteAsync = require('../utils/decrypt-file')
const scriptGroupDB = new ScriptGroupDB().getInstance()
async function getScriptGroupList({ res }) {
let data = await scriptGroupDB.findAsync({})
data = data.map(item => ({ ...item, id: item._id }))
data?.sort((a, b) => Number(b.index || 0) - Number(a.index || 0))
res.success({ data })
}
const addScriptGroup = async ({ res, request }) => {
let { addScriptGroup } = (await decryptAndExecuteAsync(path.join(__dirname, 'plus.js'))) || {}
if (addScriptGroup) {
await addScriptGroup({ res, request })
} else {
return res.fail({ data: false, msg: 'Plus专属功能!' })
}
}
const updateScriptGroup = async ({ res, request }) => {
let { updateScriptGroup } = (await decryptAndExecuteAsync(path.join(__dirname, 'plus.js'))) || {}
if (updateScriptGroup) {
await updateScriptGroup({ res, request })
} else {
return res.fail({ data: false, msg: 'Plus专属功能!' })
}
}
const removeScriptGroup = async ({ res, request }) => {
let { removeScriptGroup } = (await decryptAndExecuteAsync(path.join(__dirname, 'plus.js'))) || {}
if (removeScriptGroup) {
await removeScriptGroup({ res, request })
} else {
return res.fail({ data: false, msg: 'Plus专属功能!' })
}
}
module.exports = {
addScriptGroup,
getScriptGroupList,
updateScriptGroup,
removeScriptGroup
}
+6 -6
View File
@@ -6,13 +6,13 @@ const localShellJson = require('../config/shell.json')
const scriptsDB = new ScriptsDB().getInstance()
let localShell = JSON.parse(JSON.stringify(localShellJson)).map((item) => {
return { ...item, id: randomStr(10), index: '--', description: item.description + '|内置脚本' }
return { ...item, id: randomStr(10), index: '--', description: item.description, group: 'builtin' }
})
async function getScriptList({ res }) {
let data = await scriptsDB.findAsync({})
data = data.map(item => {
return { ...item, id: item._id }
return { ...item, id: item._id, group: item.group || 'default' }
})
data?.sort((a, b) => Number(b.index || 0) - Number(a.index || 0))
data.push(...localShell)
@@ -24,19 +24,19 @@ async function getLocalScriptList({ res }) {
}
const addScript = async ({ res, request }) => {
let { body: { name, description, command, index } } = request
let { body: { name, description, command, index, group } } = request
if (!name || !command) return res.fail({ data: false, msg: '参数错误' })
index = Number(index) || 0
let record = { name, description, command, index }
let record = { name, description, command, index, group }
await scriptsDB.insertAsync(record)
res.success({ data: '添加成功' })
}
const updateScriptList = async ({ res, request }) => {
let { params: { id } } = request
let { body: { name, description, command, index } } = request
let { body: { name, description, command, index, group } } = request
if (!name || !command) return res.fail({ data: false, msg: '参数错误' })
await scriptsDB.updateAsync({ _id: id }, { name, description, command, index })
await scriptsDB.updateAsync({ _id: id }, { name, description, command, index, group })
res.success({ data: '修改成功' })
}
+16 -1
View File
@@ -1,7 +1,7 @@
const NodeRSA = require('node-rsa')
const { randomStr } = require('./utils/tools')
const { AESEncryptAsync } = require('./utils/encrypt')
const { KeyDB, GroupDB, NotifyDB, NotifyConfigDB } = require('./utils/db-class')
const { KeyDB, GroupDB, NotifyDB, NotifyConfigDB, ScriptGroupDB } = require('./utils/db-class')
async function initKeyDB() {
const keyDB = new KeyDB().getInstance()
@@ -96,9 +96,24 @@ async function initNotifyConfigDB() {
return notifyConfigDB.insertAsync(defaultData)
}
async function initScriptGroupDB() {
const scriptGroupDB = new ScriptGroupDB().getInstance()
let count = await scriptGroupDB.countAsync({})
if (count === 0) {
consola.log('初始化ScriptGroupDB✔')
const defaultData = [
{ '_id': 'default', 'name': '默认分组', 'index': 0 },
{ '_id': 'builtin', 'name': '内置脚本', 'index': -1 }
]
return scriptGroupDB.insertAsync(defaultData)
}
return Promise.resolve()
}
module.exports = async () => {
await initKeyDB()
await initNotifyDB()
await initGroupDB()
await initScriptGroupDB()
await initNotifyConfigDB()
}
+25 -1
View File
@@ -4,6 +4,7 @@ const { login, getpublicKey, updatePwd, getEasynodeVersion, getMFA2Status, getMF
const { getNotifyConfig, updateNotifyConfig, getNotifyList, updateNotifyList } = require('../controller/notify')
const { getGroupList, addGroupList, updateGroupList, removeGroup } = require('../controller/group')
const { getScriptList, getLocalScriptList, addScript, updateScriptList, removeScript, batchRemoveScript, importScript } = require('../controller/scripts')
const { getScriptGroupList, addScriptGroup, removeScriptGroup, updateScriptGroup } = require('../controller/script-group')
const { getOnekeyRecord, removeOnekeyRecord } = require('../controller/onekey')
const { getLog } = require('../controller/log')
@@ -217,6 +218,29 @@ const scripts = [
}
]
const scriptGroup = [
{
method: 'get',
path: '/script-group',
controller: getScriptGroupList
},
{
method: 'post',
path: '/script-group',
controller: addScriptGroup
},
{
method: 'delete',
path: '/script-group/:id',
controller: removeScriptGroup
},
{
method: 'put',
path: '/script-group/:id',
controller: updateScriptGroup
}
]
const onekey = [
{
method: 'get',
@@ -237,4 +261,4 @@ const log = [
controller: getLog
}
]
module.exports = [].concat(ssh, host, user, notify, group, scripts, onekey, log)
module.exports = [].concat(ssh, host, user, notify, group, scripts, scriptGroup, onekey, log)
+1 -1
View File
@@ -1 +1 @@
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
+10 -3
View File
@@ -57,8 +57,10 @@ async function createTerminal(hostId, socket, targetSSHClient) {
try {
let { authInfo: targetConnectionOptions } = await getConnectionOptions(hostId)
let jumpHostResult = connectByJumpHosts && (await connectByJumpHosts(jumpHosts, targetConnectionOptions.host, targetConnectionOptions.port, socket))
let jumpSshClients = []
if (jumpHostResult) {
targetConnectionOptions.sock = jumpHostResult.sock
jumpSshClients = jumpHostResult.sshClients
}
socket.emit('terminal_print_info', `准备连接目标终端: ${ name } - ${ host }`)
@@ -74,7 +76,7 @@ async function createTerminal(hostId, socket, targetSSHClient) {
consola.success('终端连接成功:', host)
socket.emit('connect_terminal_success', `终端连接成功:${ host }`)
let stream = await createInteractiveShell(socket, targetSSHClient)
resolve(stream)
resolve({ stream, jumpSshClients })
})
.on('close', (err) => {
if (closeNoticeFlag) return closeNoticeFlag = false
@@ -124,6 +126,7 @@ module.exports = (httpServer) => {
}
consola.success('terminal websocket 已连接')
let targetSSHClient = null
let jumpSshClients = []
socket.on('create', async ({ hostId, token }) => {
const { code } = await verifyAuthSync(token, requestIP)
if (code !== 1) {
@@ -132,7 +135,8 @@ module.exports = (httpServer) => {
return
}
targetSSHClient = new SSHClient()
let stream = null
let { stream = null, jumpSshClients: jumpSshClientsFromCreate } = await createTerminal(hostId, socket, targetSSHClient)
jumpSshClients = jumpSshClientsFromCreate
function listenerInput(key) {
if (targetSSHClient._sock.writable === false) return consola.info('终端连接已关闭,禁止输入')
stream && stream.write(key)
@@ -142,7 +146,6 @@ module.exports = (httpServer) => {
}
socket.on('input', listenerInput)
socket.on('resize', resizeShell)
stream = await createTerminal(hostId, socket, targetSSHClient)
})
socket.on('get_ping', async (ip) => {
@@ -155,6 +158,10 @@ module.exports = (httpServer) => {
socket.on('disconnect', (reason) => {
connectionCount--
targetSSHClient && targetSSHClient.end()
jumpSshClients?.forEach(sshClient => sshClient && sshClient.end())
targetSSHClient = null
jumpSshClients = null
consola.info(`终端socket连接断开: ${ reason } - 当前连接数: ${ connectionCount }`)
})
})
+12
View File
@@ -7,6 +7,7 @@ const {
notifyConfigDBPath,
groupConfDBPath,
scriptsDBPath,
scriptGroupDBPath,
onekeyDBPath,
logDBPath,
plusDBPath
@@ -96,6 +97,17 @@ module.exports.ScriptsDB = class ScriptsDB {
}
}
module.exports.ScriptGroupDB = class ScriptGroupDB {
constructor() {
if (!ScriptGroupDB.instance) {
ScriptGroupDB.instance = new Datastore({ filename: scriptGroupDBPath, autoload: true })
}
}
getInstance() {
return ScriptGroupDB.instance
}
}
module.exports.OnekeyDB = class OnekeyDB {
constructor() {
if (!OnekeyDB.instance) {
+1 -1
View File
@@ -1 +1 @@
U2FsdGVkX1+Gus2FIC0WsNp0rUXPA+Ui1NQUjtnqP6Ycb1pyHglCADvKu51oxYaGJ0ZdoRZYo7YP3tQgIhp3f96WxP1/QFdypVrVlS7+jbAH6Gzc4CPlD3UeFsCm1j32ArFX60tPSSkq6+DJ3OF6pIVxstGIbCkmv5NQaf0J95zCxgqGm+fo/nZmZ6oj21uspGWZjhHssFRol0KpzINFDSWE9+/hJ43ybT5G6OHvEiaF83YH6h3CXAa6zz2zV18LKvnO8A4nTYR2/EBmGiP6NE3YqQ7hTE7SFmEDtRaxKJfyBxs0bHDCcFifVZh8GE25VyDwvOihUHztgvIRMh9vkgehzx9YN3sZdAsBJqcWyqi1mEPZU/l+zq2tbO+EczCvz6JQ77RZToQxm0vXzJc/ctcCEoVvjDx1pJhsQiTj5tJirFgcYz4VC7ihFYIq2XUQNISZaLynpYUUPdjvIfXGcvk0500SK9VAKb6603Z3fABdsENDGuxl2UKXMed4sL/PFwLy9siEX3BgMg1hFFiwoqqEp/x75341BoeRavEIJBEv8BdTS66mel1lUa/L3so7LyjGpdgfzOZlv+0t6Uhzy82HwYkAWmvuYpK6s6JItsG1ftYrOBzHZbpu36wn0e4N4NLqBnm6Hx1+tQJY7lTmgokgUy+5sVtp4LEsTbgE64HbDLYhME4m/3Yw5ij5D1OhoNwm/9r6MEYyJOyv8j8nDjudLRe1YQ0D2JLQsr04LYpVrjU1+Tsg780K0j0JdnFfVhe/SdkVU8nbkIIfRkv/86N6U2ZQaCYaScZmKYdBQmsK//I2yuYym0tM5q2d5kesYTxy8uAtVIXL1rE065eZFPlg/7Mgu0sqUsspG+EeDJE=
U2FsdGVkX19h9PKGSt7KlCThmnjjqplBn5bV/KsYSEXHkRdn8GKHcu6wcjCsayQ/Ebm8ImN4TSA1PuU4ZAiz+ZtN0H7nT9ti8yvhfHE+EyAktgFjUv/NiOXGHhUEKykksuy8CNss1F1hV9dRfjGn15tpJwL/nU/aqgcScoHxMZo5IebjQycAio52IQkS2ikguaj687/8Ib4Ah38HMleo8/wuZYpaDQ3jkBgZAwFU9OqB4c7voO6r3G5cQfu4GZI/0cCNsath8CIQ1JPaqz3C5xrmAOcKBgJgw1GbgPZ2XneRZY92xRINDv7c3DvgH+qvtTg20Cmz77Dk/xEYyEBTWDLzkp6hXjg/ywT6m3WDcIMleg+Ze+pIWTPgKo2xzRgx2LUflm88iAWLHg2p6jCJvqVrqpsT9ciSEbj6duu5T1uqo+r1PzDovzqEv+b+/+KYFrWgB31XCe8ZvAv+OVPqY2DBubrxaikdstJELTFL7gZn3+SBBke18rOzkFCvu8PW2d+DoMWahNcoDpVxwAyaR7GH0UmEr3XXUn9o+g29Jf5u3U9y5hYWDLiJsvOMd2CeUpw1Q0ub8V1yLrSf8I53PxPOdeUF77p3f+zulqzjKqiUbDR7B6s4WGf0JEmN5eYEY2ND+SToOOHygwwwRxh5jgI6vzEUygG73bZ609jer47aMbwGs7mIbE8FF1Q5yjNPC+Z/o8XrMu4zCGbWmMTqJpmizd8oL6ZLKe0rwWRVF/7Yew1wJ0Vk+lLJOIcnBRC2JNIj54Q7V0TSnzEWDsPzaP/vx/x9S4YGC6A6RIYSismgNXz0GZm519UgoO33pSkRtJTVguXgVH9GfT7xgOeP3Y/1hLO/7OGj0zkbzzQoqHo=
+1 -1
View File
@@ -108,7 +108,7 @@ const getLocalNetIP = async () => {
if (ip) return ip
}
if (freeipapi.status === 'fulfilled') {
let ip = pconline.value?.data?.ipAddress
let ip = freeipapi.value?.data?.ipAddress
if (ip) return ip
}
return null
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "server",
"version": "3.0.3",
"version": "3.0.4",
"description": "easynode-server",
"bin": "./bin/www",
"scripts": {
+15
View File
@@ -0,0 +1,15 @@
[
{
"version": "v3.0.4",
"date": "2025-03-21",
"features": [
"新增脚本库分组功能",
"终端页新增脚本库布局",
"优化多行命令编辑器",
"优化终端工具栏布局",
"优化多项新增序号自动累加",
"修复前端连接断开但未释放服务端ssh连接的bug",
"修复终端页面同步会话在自动执行脚本开关为true时自动添加回车的bug"
]
}
]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "3.0.3",
"version": "3.0.4",
"description": "easynode-web",
"private": true,
"scripts": {
+12
View File
@@ -118,6 +118,18 @@ export default {
batchRemoveScript(data) {
return axios({ url: '/batch-remove-script', method: 'post', data })
},
getScriptGroupList() {
return axios({ url: '/script-group', method: 'get' })
},
addScriptGroup(data) {
return axios({ url: '/script-group', method: 'post', data })
},
updateScriptGroup(id, data) {
return axios({ url: `/script-group/${ id }`, method: 'put', data })
},
deleteScriptGroup(id) {
return axios({ url: `/script-group/${ id }`, method: 'delete' })
},
getOnekeyRecord() {
return axios({ url: '/onekey', method: 'get' })
},
+5 -36
View File
@@ -1,37 +1,6 @@
// $--colors: (
// "primary": (
// "base": green,
// ),
// "success": (
// "base": #21ba45,
// ),
// "warning": (
// "base": #f2711c,
// ),
// "danger": (
// "base": #db2828,
// ),
// "error": (
// "base": #db2828,
// ),
// "info": (
// "base": #42b8dd,
// ),
// );
// You should use them in scss, because we calculate it by sass.
// comment next lines to use default color
// @forward "element-plus/theme-chalk/src/common/var.scss" with (
// // do not use same name, it will override.
// // $colors: $--colors,
// // $button-padding-horizontal: ("default": 50px)
// );
// if you want to import all
// @use "element-plus/theme-chalk/src/index.scss" as *;
// You can comment it to hide debug info.
// @debug $--colors;
// custom dark variables
@use "./dark.scss";
@use "./dark.scss";
.el-tabs {
--el-tabs-header-height: 32px;
}
+1
View File
@@ -97,6 +97,7 @@ const plusFeatures = [
'服务器跳板机功能,支持任意数量服务器的连续跳板',
'批量修改实例配置(优化版)',
'脚本库批量导出导入',
'脚本库分组与终端工具栏支持',
'凭据管理支持解密带密码保护的密钥',
'通知方式无限制',
'本地socket断开自动重连',
+113 -16
View File
@@ -68,20 +68,31 @@
<p v-if="isNew" class="conspicuous">
新版本可用: {{ latestVersion }} -> <a
class="link"
href="https://github.com/chaos-zhu/easynode/releases"
href="https://github.com/chaos-zhu/easynode?tab=readme-ov-file#%E9%A1%B9%E7%9B%AE%E9%83%A8%E7%BD%B2"
target="_blank"
>https://github.com/chaos-zhu/easynode/releases</a>
</p>
<p>
功能更新日志:<a
class="link"
href="https://github.com/chaos-zhu/easynode/blob/main/CHANGELOG.md"
target="_blank"
>https://github.com/chaos-zhu/easynode/blob/main/CHANGELOG.md</a>
>https://github.com/chaos-zhu/easynode</a>
</p>
<template v-if="features.length > 0 && isNew">
<div class="version_features_title">
<el-icon><Document /></el-icon>
新版本更新了以下内容:
</div>
<ul class="conspicuous feature_list">
<li v-for="feature in features" :key="feature">
{{ feature }}
</li>
</ul>
</template>
<p>
TG更新通知频道:<a class="link" href="https://t.me/easynode_notify" target="_blank">https://t.me/easynode_notify</a>
</p>
<p>
项目地址:<a
class="link"
href="https://github.com/chaos-zhu/easynode"
target="_blank"
>https://github.com/chaos-zhu/easynode</a>
</p>
<div class="about_footer">
<el-button type="info" @click="visible = false">关闭</el-button>
</div>
@@ -106,7 +117,7 @@
<script setup>
import { ref, getCurrentInstance, computed, onMounted, onBeforeUnmount } from 'vue'
import { useRouter } from 'vue-router'
import { User, Sunny, Moon, Fold } from '@element-plus/icons-vue'
import { User, Sunny, Moon, Fold, Document } from '@element-plus/icons-vue'
import packageJson from '../../package.json'
import MenuList from './menuList.vue'
@@ -116,6 +127,7 @@ const visible = ref(false)
const checkVersionErr = ref(false)
const currentVersion = ref(`v${ packageJson.version }`)
const latestVersion = ref(null)
const features = ref([])
const menuCollapse = ref(false)
const discount = ref(false)
@@ -145,25 +157,22 @@ const gotoPlusPage = () => {
router.push('/setting?tabKey=plus')
}
async function checkLatestVersion() {
const timeout = 3000
async function checkLatestVersionByGitRelease() {
const timeout = 5000
try {
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('请求超时')), timeout)
)
const url = `https://api.github.com/repos/chaos-zhu/easynode/releases?ts=${ new Date().getTime() }`
const fetchPromise = fetch(url, {
headers: {
'Accept': 'application/vnd.github.v3+json'
}
})
const response = await Promise.race([fetchPromise, timeoutPromise,])
if (!response.ok) {
throw new Error('版本信息请求失败: ' + response.statusText)
}
const releases = await response.json()
// console.log('releases:', releases)
const filteredReleases = releases.filter(release => !release.tag_name.startsWith('client'))
@@ -176,7 +185,48 @@ async function checkLatestVersion() {
}
}
checkLatestVersion()
async function checkLatestVersionByJson() {
const timeout = 5000
try {
let date = new Date()
let lastGetVersionTime = localStorage.getItem('lastGetVersionTime')
// 10分钟内不重复请求
if (lastGetVersionTime && (date.getTime() - Number(lastGetVersionTime) < 1000 * 60 * 10)) {
let latestVersionFromLocal = localStorage.getItem('latestVersion')
let featuresFromLocal = localStorage.getItem('features')
if (latestVersionFromLocal && featuresFromLocal) {
latestVersion.value = latestVersionFromLocal
features.value = JSON.parse(featuresFromLocal)
return
}
}
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('请求超时')), timeout)
)
const url = `https://git.221022.xyz/https://raw.githubusercontent.com/chaos-zhu/easynode/refs/heads/main/server/version.json?ts=${ new Date().getTime() }`
const fetchPromise = fetch(url)
const response = await Promise.race([fetchPromise, timeoutPromise,])
if (!response.ok) {
throw new Error('版本信息请求失败: ' + response.statusText)
}
const releases = await response.json()
// console.log('releases:', releases)
if (Array.isArray(releases) && releases.length > 0) {
latestVersion.value = releases[0].version
features.value = releases[0].features
localStorage.setItem('lastGetVersionTime', date.getTime())
localStorage.setItem('latestVersion', latestVersion.value)
localStorage.setItem('features', JSON.stringify(features.value))
}
} catch (error) {
checkVersionErr.value = true
console.error('版本信息请求失败:', error.message)
checkLatestVersionByGitRelease()
}
}
checkLatestVersionByJson()
let timer = null
const checkFirstVisit = () => {
@@ -237,6 +287,7 @@ onBeforeUnmount(() => {
.new_version {
color: red;
text-decoration: underline;
}
}
@@ -299,6 +350,52 @@ onBeforeUnmount(() => {
color: #F56C6C;
font-weight: 500;
}
.feature_list {
li {
list-style: none;
margin-left: 20px;
padding: 6px 0;
line-height: 1.5;
position: relative;
counter-increment: feature-counter;
&::before {
content: counter(feature-counter);
position: absolute;
left: -25px;
top: 50%;
transform: translateY(-50%);
width: 18px;
height: 18px;
background: #F56C6C;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
}
&:not(:last-child) {
border-bottom: 1px dashed rgba(245, 108, 108, 0.2);
}
}
}
.version_features_title {
display: flex;
align-items: center;
color: #F56C6C;
font-weight: 500;
margin: 15px 0 10px;
padding-bottom: 10px;
border-bottom: 2px solid rgba(245, 108, 108, 0.2);
.el-icon {
margin-right: 6px;
font-size: 16px;
}
}
}
.about_footer {
+6
View File
@@ -15,6 +15,7 @@ const useStore = defineStore({
groupList: [],
sshList: [],
scriptList: [],
scriptGroupList: [],
localScriptList: [],
HostStatusSocket: null,
user: localStorage.getItem('user') || null,
@@ -71,6 +72,7 @@ const useStore = defineStore({
await this.getHostList()
await this.getSSHList()
await this.getScriptList()
await this.getScriptGroupList()
await this.getPlusInfo()
this.wsClientsStatus()
},
@@ -96,6 +98,10 @@ const useStore = defineStore({
const { data: scriptList } = await $api.getScriptList()
this.$patch({ scriptList })
},
async getScriptGroupList() {
const { data: scriptGroupList } = await $api.getScriptGroupList()
this.$patch({ scriptGroupList })
},
async getLocalScriptList() {
const { data: localScriptList } = await $api.getLocalScriptList()
this.$patch({ localScriptList })
+1 -1
View File
@@ -238,7 +238,7 @@ const handleDecryptKey = async () => {
<style lang="scss" scoped>
.credentials_container {
padding: 20px;
padding: 0 20px 20px 20px;
.header {
padding: 15px;
display: flex;
+3 -2
View File
@@ -120,6 +120,8 @@ const list = computed(() => {
let addGroup = () => {
groupForm.id = null
const maxIndex = Math.max(...groupList.value.map(item => item.index), 0)
groupForm.index = maxIndex + 1
groupFormVisible.value = true
isModify.value = false
}
@@ -167,10 +169,9 @@ const deleteGroup = ({ id, name }) => {
<style lang="scss" scoped>
.group_container {
padding: 20px;
padding:0 20px 20px 20px;
.header {
padding: 15px;
display: flex;
align-items: center;
justify-content: end;
+1 -1
View File
@@ -444,7 +444,7 @@ onActivated(async () => {
<style lang="scss" scoped>
.onekey_container {
padding: 20px;
padding: 0 20px 20px 20px;
.header {
padding: 15px;
display: flex;
@@ -0,0 +1,192 @@
<template>
<el-dialog
v-model="visible"
width="600px"
top="150px"
:title="isModify ? '修改脚本' : '添加脚本'"
:close-on-click-modal="false"
@open="handleOpen"
@close="handleClose"
>
<el-form
ref="formRef"
:model="formData"
:rules="rules"
:hide-required-asterisk="true"
label-suffix=":"
label-width="100px"
:show-message="false"
>
<el-form-item key="group" label="分组" prop="group">
<el-select
v-model="formData.group"
placeholder=""
clearable
style="width: 100%;"
>
<el-option
v-for="item in groupList"
:key="item.id"
:label="item.name"
:value="item.id"
:disabled="item.id === 'builtin'"
/>
</el-select>
</el-form-item>
<el-form-item label="名称" prop="name">
<el-input
v-model="formData.name"
clearable
placeholder=""
autocomplete="off"
/>
</el-form-item>
<!-- <el-form-item label="描述" prop="description">
<el-input
v-model="formData.description"
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="command" label="内容">
<el-input
v-model="formData.command"
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="visible = false">关闭</el-button>
<el-button type="primary" @click="handleSubmit">{{ isModify ? '修改' : '添加' }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ref, computed, reactive, watch } from 'vue'
import { getCurrentInstance } from 'vue'
const props = defineProps({
show: {
type: Boolean,
default: false
},
defaultData: {
type: Object,
default: () => ({})
},
defaultScript: {
type: String,
default: ''
},
defaultGroup: {
type: String,
default: 'default'
}
})
const emit = defineEmits(['update:show', 'success',])
const { proxy: { $api, $message, $store } } = getCurrentInstance()
const visible = computed({
get: () => props.show,
set: (val) => emit('update:show', val)
})
const formRef = ref(null)
const groupList = computed(() => $store.scriptGroupList || [])
const scriptList = computed(() => $store.scriptList)
const curGroupScripts = computed(() => scriptList.value.filter(item => item.group === props.defaultGroup))
const nextIndex = computed(() => curGroupScripts.value.reduce((acc, cur) => Math.max(acc, Number(cur.index) || 0), 0) + 1)
const formData = reactive({
group: '',
name: '',
description: '',
index: nextIndex.value || 0,
command: ''
})
const isModify = computed(() => Boolean(formData.id))
const rules = {
group: { required: true, message: '选择一个分组' },
name: { required: true, trigger: 'change' },
description: { required: false, trigger: 'change' },
index: { required: false, type: 'number', trigger: 'change' },
command: { required: true, trigger: 'change' }
}
watch(() => props.defaultData, (newVal) => {
if (newVal?.id) {
Object.assign(formData, { ...newVal })
}
}, { immediate: true, deep: true })
watch(() => props.defaultScript, (newVal) => {
if (newVal && !formData.id) {
formData.command = newVal
}
}, { immediate: true })
watch(() => props.defaultGroup, (newVal) => {
if (newVal && !formData.id) {
if (newVal === 'builtin') {
formData.group = 'default'
} else {
formData.group = newVal
}
}
}, { immediate: true })
const handleClose = () => {
formRef.value?.resetFields()
Object.assign(formData, {
id: null,
group: props.defaultGroup,
name: '',
description: '',
index: nextIndex.value,
command: props.defaultScript || ''
})
}
const handleOpen = () => {
if (!formData.id) {
formData.index = nextIndex.value
}
}
const handleSubmit = () => {
formRef.value.validate()
.then(async () => {
const data = { ...formData }
if (isModify.value) {
await $api.updateScript(data.id, data)
} else {
await $api.addScript(data)
}
visible.value = false
await $store.getScriptList()
emit('success')
$message.success('success')
})
}
</script>
@@ -0,0 +1,204 @@
<template>
<el-dialog
v-model="visible"
width="600px"
:top="isMobile() ? '45px' : '15vh'"
:append-to-body="false"
title="脚本分组管理"
:close-on-click-modal="false"
>
<div class="group_container">
<div class="header">
<el-button type="primary" @click="addGroup">添加分组</el-button>
</div>
<el-table v-loading="loading" :data="list">
<el-table-column prop="index" label="序号" />
<el-table-column prop="name" label="分组名称" />
<el-table-column label="关联脚本数量" min-width="115px">
<template #default="{ row }">
<el-popover
v-if="row.scripts.list.length !== 0"
placement="left"
:width="350"
trigger="hover"
>
<template #reference>
<u class="script_count">{{ row.scripts.count }}</u>
</template>
<ul>
<li v-for="item in row.scripts.list" :key="item.id">
<span>{{ item.name }}</span>
-
<span>{{ item.description }}</span>
</li>
</ul>
</el-popover>
<u v-else class="script_count">0</u>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="160px">
<template #default="{ row }">
<template v-if="row.id !== 'builtin'">
<el-button type="primary" @click="handleChange(row)">修改</el-button>
<el-button v-show="row.id !== 'default'" type="danger" @click="deleteGroup(row)">删除</el-button>
</template>
<template v-else>
<span>--</span>
</template>
</template>
</el-table-column>
</el-table>
<el-dialog
v-model="groupFormVisible"
width="600px"
top="150px"
:title="isModify ? '修改分组' : '添加分组'"
:close-on-click-modal="false"
@close="clearFormInfo"
>
<el-form
ref="updateFormRef"
:model="groupForm"
:rules="rules"
label-width="100px"
>
<el-form-item label="分组名称" prop="name">
<el-input v-model="groupForm.name" />
</el-form-item>
<el-form-item label="序号" prop="index">
<el-input v-model.number="groupForm.index" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="groupFormVisible = false">取消</el-button>
<el-button type="primary" @click="updateForm">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</el-dialog>
</template>
<script setup>
import { ref, computed, getCurrentInstance, nextTick } from 'vue'
import { isMobile } from '@/utils'
const props = defineProps({
show: {
required: true,
type: Boolean
}
})
const emit = defineEmits(['update:show', 'group-deleted',])
const visible = computed({
get: () => props.show,
set: (newVal) => emit('update:show', newVal)
})
const { proxy: { $api, $store, $message, $messageBox } } = getCurrentInstance()
const loading = ref(false)
const groupFormVisible = ref(false)
const isModify = ref(false)
const updateFormRef = ref()
const groupForm = ref({
id: null,
name: '',
index: 0
})
const rules = {
name: [{ required: true, message: '请输入分组名称', trigger: 'blur' },],
index: [{ type: 'number', message: '序号必须为数字值' },]
}
const scriptList = computed(() => $store.scriptList)
const groupList = computed(() => $store.scriptGroupList)
const list = computed(() => {
return groupList.value.map(item => {
const scripts = scriptList.value.reduce((prev, next) => {
if (next.group === item.id) {
prev.count++
prev.list.push(next)
}
return prev
}, { count: 0, list: [] })
return { ...item, scripts }
})
})
const addGroup = () => {
groupForm.value.id = null
groupForm.value.index = groupList.value.reduce((max, group) =>
Math.max(max, Number(group.index) || 0), 0) + 1
groupFormVisible.value = true
isModify.value = false
}
const handleChange = (row) => {
Object.assign(groupForm.value, { ...row })
groupFormVisible.value = true
isModify.value = true
}
const updateForm = () => {
updateFormRef.value.validate()
.then(async () => {
const { id, index, name } = groupForm.value
if (isModify.value) {
await $api.updateScriptGroup(id, { index, name })
} else {
await $api.addScriptGroup({ index, name })
}
await $store.getScriptGroupList()
groupFormVisible.value = false
$message.success('success')
})
}
const clearFormInfo = () => {
nextTick(() => updateFormRef.value.resetFields())
}
const deleteGroup = ({ id, name }) => {
$messageBox.confirm(`确认删除分组:${ name } (分组下脚本将移动至默认分组)`, 'Warning', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
await $api.deleteScriptGroup(id)
await $store.getScriptList()
await $store.getScriptGroupList()
emit('group-deleted', id)
$message.success('success')
})
}
</script>
<style lang="scss" scoped>
.group_container {
padding: 0 20px 20px 20px;
.header {
display: flex;
align-items: center;
justify-content: end;
}
}
.script_count {
display: block;
width: 100px;
text-align: center;
font-size: 15px;
color: #87cf63;
cursor: pointer;
}
</style>
+107 -147
View File
@@ -15,12 +15,11 @@
<el-button
v-show="selectScripts.length"
type="danger"
class="batch_remove_btn"
@click="handleBatchRemove"
>
批量删除
</el-button>
<el-button type="primary" class="add_script_btn" @click="addScript">添加脚本</el-button>
<el-button type="primary" @click="addScript">添加脚本</el-button>
<PlusSupportTip>
<el-dropdown trigger="click" :disabled="!isPlusActive">
<el-button type="primary" class="group_action_btn" :disabled="!isPlusActive">
@@ -34,126 +33,98 @@
</template>
</el-dropdown>
</PlusSupportTip>
</div>
<el-table
v-loading="loading"
:data="paginatedFilteredList"
@selection-change="handleSelectionChange"
>
<el-table-column
type="selection"
width="55"
:selectable="(row) => {
return row.index !== '--' && row.index !== '-' && row.index !== undefined && row.index !== null
}"
/>
<el-table-column prop="index" label="序号" width="100px" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="description" label="描述" />
<el-table-column prop="command" label="指令内容" show-overflow-tooltip />
<el-table-column label="操作" fixed="right" width="160px">
<template #default="{ row }">
<template v-if="row.index !== '--'">
<el-button type="primary" @click="handleChange(row)">修改</el-button>
<el-button v-show="row.id !== 'own'" type="danger" @click="handleRemove(row)">删除</el-button>
</template>
<span v-else>--</span>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[20, 50, 100]"
:total="scriptList.length"
layout="total, sizes, prev, pager, next"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
<PlusSupportTip>
<el-button
type="primary"
class="group_action_btn"
:disabled="!isPlusActive"
@click="ScriptGroupVisible = true"
>
分组管理
</el-button>
</PlusSupportTip>
</div>
<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-tabs v-model="activeTab" type="border-card" class="script-tabs">
<el-tab-pane
v-for="group in groupList"
:key="group.id"
:label="group.name"
:name="group.id"
>
<el-form-item label="名称" prop="name">
<el-input
v-model="formData.name"
clearable
placeholder=""
autocomplete="off"
<el-table
v-loading="loading"
:data="getFilteredScriptsByGroup(group.id)"
@selection-change="handleSelectionChange"
>
<el-table-column
type="selection"
width="55"
:selectable="(row) => {
return row.index !== '--' && row.index !== '-' && row.index !== undefined && row.index !== null
}"
/>
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input
v-model="formData.description"
clearable
placeholder=""
autocomplete="off"
<el-table-column prop="index" label="序号" width="100px" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="description" label="描述" />
<el-table-column prop="command" label="指令内容" show-overflow-tooltip />
<el-table-column label="操作" fixed="right" width="160px">
<template #default="{ row }">
<template v-if="row.index !== '--'">
<el-button type="primary" @click="handleChange(row)">修改</el-button>
<el-button v-show="row.id !== 'own'" type="danger" @click="handleRemove(row)">删除</el-button>
</template>
<span v-else>--</span>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[20, 50, 100]"
:total="getFilteredScriptsByGroup(group.id).length"
layout="total, sizes, prev, pager, next"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</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="command" label="内容">
<el-input
v-model="formData.command"
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>
</el-tab-pane>
</el-tabs>
<ScriptEdit
v-model:show="formVisible"
:default-data="currentScript"
:default-group="activeTab"
@success="handleEditSuccess"
/>
<ImportScript
v-model:show="importVisible"
@update-list="() => $store.getScriptList()"
/>
<ScriptGroup
v-model:show="ScriptGroupVisible"
@group-deleted="handleGroupDeleted"
/>
</div>
</template>
<script setup>
import { ref, reactive, computed, nextTick, getCurrentInstance, h } from 'vue'
import { ref, reactive, computed, nextTick, getCurrentInstance, h, watch } from 'vue'
import ImportScript from './components/import-script.vue'
import PlusSupportTip from '@/components/common/PlusSupportTip.vue'
import { ArrowDown, Search } from '@element-plus/icons-vue'
import { exportFile } from '@/utils'
import ScriptGroup from './components/script-group.vue'
import ScriptEdit from './components/script-edit.vue'
const { proxy: { $api, $message, $messageBox, $store, $tools } } = getCurrentInstance()
const loading = ref(false)
const formVisible = ref(false)
let isModify = ref(false)
const selectScripts = ref([])
const handleSelectionChange = (val) => {
selectScripts.value = val
@@ -173,57 +144,21 @@ const handleBatchRemove = () => {
})
}
let formData = reactive({
name: '',
description: '',
index: 0,
command: ''
})
const rules = computed(() => {
return {
name: { required: true, trigger: 'change' },
description: { required: false, trigger: 'change' },
index: { required: false, type: 'number', trigger: 'change' },
command: { required: true, trigger: 'change' }
}
})
const updateFormRef = ref(null)
const currentScript = ref({})
const scriptList = computed(() => $store.scriptList)
const isPlusActive = computed(() => $store.isPlusActive)
let addScript = () => {
formData.id = null
formData.index = scriptList.value.reduce((acc, cur) => Math.max(acc, Number(cur.index) || 0), 0) + 1
isModify.value = false
const groupList = computed(() => $store.scriptGroupList || [])
const addScript = () => {
currentScript.value = {}
formVisible.value = true
}
const handleChange = (row) => {
Object.assign(formData, { ...row })
currentScript.value = { ...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 }) => {
@@ -263,11 +198,14 @@ const filteredScriptList = computed(() => {
)
})
const paginatedFilteredList = computed(() => {
const activeTab = ref(computed(() => groupList.value?.[0]?.id || 'default').value)
const getFilteredScriptsByGroup = (groupId) => {
const groupScripts = filteredScriptList.value.filter(script => script.group === groupId)
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return filteredScriptList.value.slice(start, end)
})
return groupScripts.slice(start, end)
}
const handleSizeChange = (val) => {
pageSize.value = val
@@ -282,24 +220,46 @@ const handleSearch = () => {
currentPage.value = 1
}
const ScriptGroupVisible = ref(false)
const handleGroupDeleted = (deletedGroupId) => {
if (deletedGroupId === activeTab.value) {
nextTick(() => {
activeTab.value = groupList.value?.[0]?.id || 'default'
})
}
}
watch(activeTab, () => {
currentPage.value = 1
})
const handleEditSuccess = () => {
currentScript.value = {}
}
</script>
<style lang="scss" scoped>
.scripts_container {
padding: 20px;
padding: 0 20px 20px 20px;
.header {
padding: 15px;
display: flex;
align-items: center;
justify-content: end;
.add_script_btn {
margin: 0 10px;
.group_action_btn {
margin-left: 10px;
}
.search_input {
width: 300px;
margin-right: auto;
}
}
.script-tabs {
margin-top: 20px;
}
}
.host_count {
+3 -2
View File
@@ -2,9 +2,9 @@
<el-dialog
v-model="visible"
width="600px"
top="45px"
:top="isMobile() ? '45px' : '15vh'"
:append-to-body="false"
title="分组管理"
title="实例分组管理"
:close-on-click-modal="false"
>
<Group />
@@ -14,6 +14,7 @@
<script setup>
import { computed } from 'vue'
import Group from '@/views/group/index.vue'
import { isMobile } from '@/utils'
const props = defineProps({
show: {
@@ -382,7 +382,12 @@ let confHostList = computed(() => {
})
const setDefaultData = () => {
if (!defaultData.value) return
if (!defaultData.value) {
// 添加新实例时,设置index为当前最大index + 1
const maxIndex = Math.max(...hostList.value.map(host => host.index || 0), 0)
hostForm.value.index = maxIndex + 1
return
}
// eslint-disable-next-line no-unused-vars
let { id, ...rest } = defaultData.value
for (let [key,] of Object.entries(hostForm.value)) {
@@ -1,30 +0,0 @@
<template>
<div class="command_history_container">
<ul>
<li
v-for="item in list"
:key="item"
>
{{ item }}
</li>
</ul>
</div>
</template>
<script setup>
import { ref } from 'vue'
defineProps({
list: {
required: true,
type: Array
}
})
</script>
<style lang="scss" scoped>
.command_history_container {
}
</style>
@@ -0,0 +1,101 @@
<template>
<div ref="footerBarContainerRef" class="footer_bar_container">
<div ref="adjustRef" class="adjust" />
<div class="footer_bar_content">
<el-tabs v-model="activeTab" type="border-card">
<el-tab-pane label="文件传输" name="sftp">
<Sftp :host-id="hostId" />
</el-tab-pane>
<el-tab-pane label="脚本库" name="script">
<ScriptInput :host-id="hostId" @exec-command="execCommand" />
</el-tab-pane>
</el-tabs>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick } from 'vue'
import { EventBus } from '@/utils'
import Sftp from './sftp.vue'
import ScriptInput from './script-input.vue'
defineProps({
hostId: {
required: true,
type: String
}
})
const emit = defineEmits(['resize', 'exec-command',])
const footerBarContainerRef = ref(null)
const adjustRef = ref(null)
const activeTab = ref('sftp')
const adjustHeight = async () => {
let startAdjust = false
let timer = null
await nextTick()
try {
let footerBarHeight = localStorage.getItem('footerBarHeight')
if (footerBarHeight) footerBarContainerRef.value.style.height = footerBarHeight
adjustRef.value.addEventListener('mousedown', () => {
startAdjust = true
})
document.addEventListener('mousemove', (e) => {
if (!startAdjust) return
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
footerBarHeight = `calc(100vh - ${ e.pageY }px - 20px)`
footerBarContainerRef.value.style.height = footerBarHeight
emit('resize')
})
})
document.addEventListener('mouseup', () => {
if (!startAdjust) return
startAdjust = false
localStorage.setItem('footerBarHeight', footerBarHeight)
EventBus.$emit('update-footer-bar-height')
})
} catch (error) {
console.warn(error.message)
}
}
const execCommand = (command) => {
emit('exec-command', command)
}
onMounted(() => {
adjustHeight()
EventBus.$on('update-footer-bar-height', () => {
adjustHeight()
})
})
</script>
<style lang="scss" scoped>
.footer_bar_container {
position: relative;
background: #ffffff;
border: 1px solid var(--el-border-color);
min-height: 100px;
.adjust {
user-select: none;
position: absolute;
top: -3px;
width: 100%;
height: 5px;
background: var(--el-color-primary);
opacity: 0.3;
cursor: ns-resize;
}
.footer_bar_content {
width: 100%;
height: 100%;
background-color: var(--el-fill-color-light);
}
}
</style>
@@ -0,0 +1,542 @@
<template>
<div class="script_input_container">
<div v-show="!isPlusActive" class="plus_tips">
<div class="plus_tips_content">
<el-icon class="lock-icon"><Lock /></el-icon>
<div class="plus_label">PLUS</div>
<p>此功能仅限PLUS版使用, <span class="to_active" @click="gotoPlusPage">去激活</span></p>
</div>
</div>
<div class="left_box">
<div class="group_list">
<div
v-for="group in scriptGroupList"
:key="group.id"
:class="['group_item', { active: activeGroup === group.id }]"
@click="handleSelectGroup(group)"
>
{{ group.name }}
</div>
<div
class="group_item group_manage"
@click="handleShowGroupManage"
>
<el-icon><Setting /></el-icon>
分组管理
</div>
</div>
<div class="script_list">
<template v-if="filteredScripts.length">
<div
v-for="script in filteredScripts"
:key="script.id"
class="script_item"
>
<div class="script_info">
<span class="script_name">{{ script.name }}:</span>
<span class="script_command">{{ script.command }}</span>
</div>
<div class="hover_icons">
<el-icon
class="action_icon"
title="执行"
@click="handleExecScript(script)"
>
<VideoPlay />
</el-icon>
<el-icon
v-if="activeGroup !== 'builtin'"
class="action_icon"
title="编辑脚本"
@click="handleEditScript(script)"
>
<Edit />
</el-icon>
<el-icon
class="action_icon"
title="发送到脚本编辑器"
@click="handleSendToEditor(script)"
>
<ArrowRightBold />
</el-icon>
</div>
</div>
<div
v-if="activeGroup !== 'builtin'"
class="script_item add_script_btn"
@click="handleAddScript"
>
<el-icon><Plus /></el-icon>
<span>新建脚本</span>
</div>
</template>
<div v-else class="empty-list">
<el-empty
:image-size="60"
description="暂无脚本"
>
<template #description>
<p>当前分组暂无脚本</p>
</template>
<el-button
type="primary"
@click="handleAddScript"
>
添加脚本
</el-button>
</el-empty>
</div>
</div>
</div>
<div class="right_box">
<div class="editor_title">脚本编辑器</div>
<div class="editor_content">
<el-input
v-model="scriptContent"
type="textarea"
:rows="10"
placeholder="请输入脚本内容..."
/>
<div class="action_btn">
<el-button
v-show="scriptContent"
type="primary"
plain
@click="handleSaveAsScript"
>
保存脚本
</el-button>
<el-button
type="primary"
@click="handleSendToTerminal"
>
发送到终端
</el-button>
</div>
</div>
</div>
</div>
<ScriptEdit
v-model:show="scriptEditVisible"
:default-data="editScriptData"
:default-script="scriptContent"
:default-group="activeGroup"
@success="handleScriptEditSuccess"
/>
<ScriptGroup
v-model:show="scriptGroupVisible"
@group-deleted="handleGroupDeleted"
/>
</template>
<script setup>
import { ref, computed, getCurrentInstance } from 'vue'
import { useRouter } from 'vue-router'
import { VideoPlay, Edit, Setting, Plus, ArrowRightBold, Lock } from '@element-plus/icons-vue'
import ScriptEdit from '@/views/scripts/components/script-edit.vue'
import ScriptGroup from '@/views/scripts/components/script-group.vue'
defineProps({
hostId: {
required: true,
type: String
}
})
const emit = defineEmits(['exec-command',])
const { proxy: { $store, $message } } = getCurrentInstance()
const router = useRouter()
const scriptContent = ref('')
const activeGroup = ref('')
const scriptEditVisible = ref(false)
const editScriptData = ref(null)
const scriptGroupVisible = ref(false)
const isPlusActive = computed(() => $store.isPlusActive)
const scriptGroupList = computed(() => $store.scriptGroupList || [])
const scriptList = computed(() => $store.scriptList || [])
const gotoPlusPage = () => {
router.push('/setting?tabKey=plus')
}
const filteredScripts = computed(() => {
return scriptList.value.filter(script => script.group === activeGroup.value)
})
const handleSelectGroup = (group) => {
if (!isPlusActive.value) return $message.warning('此功能仅限PLUS版使用')
activeGroup.value = group.id
}
const handleExecScript = (script) => {
if (!isPlusActive.value) return $message.warning('此功能仅限PLUS版使用')
emit('exec-command', script.command)
}
const handleEditScript = (script) => {
if (!isPlusActive.value) return $message.warning('此功能仅限PLUS版使用')
editScriptData.value = script
scriptEditVisible.value = true
}
const handleSendToEditor = (script) => {
if (!isPlusActive.value) return $message.warning('此功能仅限PLUS版使用')
scriptContent.value = script.command
}
const handleSendToTerminal = () => {
if (!isPlusActive.value) return $message.warning('此功能仅限PLUS版使用')
if (!scriptContent.value) return
emit('exec-command', scriptContent.value)
scriptContent.value = ''
}
const handleAddScript = () => {
if (!isPlusActive.value) return $message.warning('此功能仅限PLUS版使用')
editScriptData.value = null
scriptEditVisible.value = true
}
const handleScriptEditSuccess = () => {
if (!isPlusActive.value) return $message.warning('此功能仅限PLUS版使用')
editScriptData.value = null
}
const handleShowGroupManage = () => {
if (!isPlusActive.value) return $message.warning('此功能仅限PLUS版使用')
scriptGroupVisible.value = true
}
const handleGroupDeleted = (deletedGroupId) => {
if (!isPlusActive.value) return $message.warning('此功能仅限PLUS版使用')
if (deletedGroupId === activeGroup.value) {
// 如果删除的是当前选中的分组,切换到第一个分组
if (scriptGroupList.value.length) {
activeGroup.value = scriptGroupList.value[0].id
}
}
}
const handleSaveAsScript = () => {
if (!isPlusActive.value) return $message.warning('此功能仅限PLUS版使用')
if (!scriptContent.value) {
return $message.warning('请先输入脚本内容')
}
scriptEditVisible.value = true
}
if (scriptGroupList.value.length) {
activeGroup.value = scriptGroupList.value[0].id
}
</script>
<style lang="scss" scoped>
.script_input_container {
height: 100%;
display: flex;
gap: 10px;
padding: 10px;
min-width: 0;
position: relative;
.plus_tips {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
.plus_tips_content {
text-align: center;
color: #fff;
.lock-icon {
font-size: 32px;
margin-bottom: 12px;
}
.plus_label {
font-size: 20px;
font-weight: bold;
color: #ffd700;
margin-bottom: 8px;
}
p {
font-size: 14px;
margin: 0;
opacity: 0.9;
.to_active {
color: #ffd700;
text-decoration: underline;
cursor: pointer;
}
}
}
}
.left_box {
width: 50%;
min-width: 50%;
max-width: 50%;
flex: 0 0 50%;
display: flex;
flex-direction: column;
border: 1px solid var(--el-border-color);
border-radius: 4px;
.group_list {
padding: 6px 10px;
height: 32px;
box-sizing: border-box;
border-bottom: 1px solid var(--el-border-color);
display: flex;
flex-wrap: nowrap;
gap: 8px;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
&::-webkit-scrollbar {
display: none;
}
.group_item {
padding: 2px 12px;
cursor: pointer;
border-radius: 12px;
margin-bottom: 0;
font-size: 13px;
border: 1px solid var(--el-border-color-light);
flex-shrink: 0;
&:hover {
background-color: var(--el-fill-color-light);
border-color: var(--el-border-color);
}
&.active {
background-color: var(--el-color-primary-light-9);
color: var(--el-color-primary);
border-color: var(--el-color-primary-light-7);
}
&.group_manage {
display: flex;
align-items: center;
gap: 4px;
background-color: var(--el-color-info-light-9);
border-color: var(--el-color-info-light-5);
color: var(--el-text-color-regular);
.el-icon {
font-size: 14px;
}
&:hover {
background-color: var(--el-color-info-light-7);
border-color: var(--el-color-info);
color: var(--el-color-info-dark-2);
}
}
}
}
.script_list {
flex: 1;
overflow-y: auto;
padding: 10px;
padding-bottom: 30px;
.empty-list {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: var(--el-text-color-secondary);
:deep(.el-empty__description) {
margin-bottom: 10px;
p {
margin: 0;
line-height: 1.5;
&.sub-text {
font-size: 12px;
color: var(--el-text-color-placeholder);
}
}
}
}
.script_item {
display: flex;
justify-content: space-between;
align-items: center;
height: 36px;
padding: 0 12px;
border-radius: 4px;
cursor: pointer;
border: 1px solid transparent;
transition: all 0.2s ease;
line-height: 36px;
&:hover {
background-color: var(--el-fill-color-light);
border-color: var(--el-border-color);
.hover_icons {
opacity: 1;
transform: translateX(0);
}
}
.script_info {
flex: 1;
display: flex;
align-items: center;
min-width: 0;
margin-right: 8px;
height: 100%;
padding: 2px 0;
.script_name {
flex-shrink: 0;
font-size: 13px;
color: var(--el-text-color-regular);
margin-right: 4px;
line-height: normal;
&::before {
content: '⌘';
margin-right: 6px;
color: var(--el-text-color-secondary);
font-size: 12px;
}
}
.script_command {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
color: var(--el-text-color-secondary);
line-height: normal;
padding: 2px 0;
}
}
.hover_icons {
display: flex;
gap: 8px;
opacity: 0;
transform: translateX(10px);
transition: all 0.2s ease;
flex-shrink: 0;
height: 100%;
align-items: center;
padding: 2px 0;
.action_icon {
font-size: 16px;
color: var(--el-text-color-secondary);
padding: 4px;
border-radius: 4px;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
&:hover {
color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
transform: scale(1.1);
}
}
}
&.add_script_btn {
justify-content: center;
color: var(--el-text-color-secondary);
border: 1px dashed var(--el-border-color);
margin-top: 10px;
background-color: var(--el-fill-color-blank);
.el-icon {
font-size: 14px;
margin-right: 4px;
}
span {
font-size: 13px;
}
&:hover {
color: var(--el-color-primary);
border-color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
}
}
}
}
}
.right_box {
width: 50%;
min-width: 50%;
max-width: 50%;
flex: 0 0 50%;
display: flex;
flex-direction: column;
border: 1px solid var(--el-border-color);
border-radius: 4px;
position: relative;
.editor_title {
padding: 6px 10px;
height: 32px;
box-sizing: border-box;
font-size: 14px;
font-weight: 500;
color: var(--el-text-color-primary);
border-bottom: 1px solid var(--el-border-color);
display: flex;
align-items: center;
}
.editor_content {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
padding: 10px;
height: calc(100% - 32px);
.el-textarea {
height: 100%;
:deep(.el-textarea__inner) {
height: 100%;
resize: none;
}
}
.action_btn {
position: absolute;
right: 20px;
bottom: 20px;
z-index: 1;
display: flex;
gap: 10px;
}
}
}
}
</style>
+4 -55
View File
@@ -1,6 +1,5 @@
<template>
<div ref="sftpTabContainerRef" class="sftp_tab_container">
<div ref="adjustRef" class="adjust" />
<div class="sftp_tab_container">
<section>
<div class="left box">
<div class="header">
@@ -159,7 +158,7 @@
import { ref, computed, onMounted, onBeforeUnmount, getCurrentInstance } from 'vue'
import socketIo from 'socket.io-client'
import CodeEdit from '@/components/code-edit/index.vue'
import { EventBus, isDir, isFile, sortDirTree, downloadFile, isMobile } from '@/utils'
import { isDir, isFile, sortDirTree, downloadFile, isMobile } from '@/utils'
import dirIcon from '@/assets/image/system/dir.png'
import linkIcon from '@/assets/image/system/link.png'
import fileIcon from '@/assets/image/system/file.png'
@@ -174,8 +173,6 @@ const props = defineProps({
}
})
const emit = defineEmits(['resize',])
const { proxy: { $store, $notification, $message, $messageBox, $serviceURI, $nextTick } } = getCurrentInstance()
const visible = ref(false)
@@ -204,8 +201,6 @@ const curUploadFileName = ref('')
const showDirProgress = ref(false)
const curUploadDirName = ref(0)
const adjustRef = ref(null)
const sftpTabContainerRef = ref(null)
const childDirRef = ref(null)
const uploadFileRef = ref(null)
const uploadDirRef = ref(null)
@@ -221,46 +216,12 @@ const fileList = computed(() => childDir.value.filter(({ name }) => name.include
onMounted(() => {
connectSftp()
adjustHeight()
EventBus.$on('update-sftp-tab-height', () => {
adjustHeight()
})
})
onBeforeUnmount(() => {
if (socket.value) socket.value.close()
})
const adjustHeight = async () => {
let startAdjust = false
let timer = null
await $nextTick()
try {
let sftpHeight = localStorage.getItem('sftpHeight')
if (sftpHeight) sftpTabContainerRef.value.style.height = sftpHeight
adjustRef.value.addEventListener('mousedown', () => {
startAdjust = true
})
document.addEventListener('mousemove', (e) => {
if (!startAdjust) return
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
sftpHeight = `calc(100vh - ${ e.pageY }px - 20px)`
sftpTabContainerRef.value.style.height = sftpHeight
emit('resize')
})
})
document.addEventListener('mouseup', () => {
if (!startAdjust) return
startAdjust = false
localStorage.setItem('sftpHeight', sftpHeight)
EventBus.$emit('update-sftp-tab-height')
})
} catch (error) {
console.warn(error.message)
}
}
const connectSftp = () => {
socket.value = io($serviceURI, {
path: '/sftp',
@@ -653,23 +614,10 @@ defineExpose({
<style lang="scss" scoped>
.sftp_tab_container {
position: relative;
background: #ffffff;
border: 1px solid var(--el-border-color);
.adjust {
user-select: none;
position: absolute;
top: -3px;
width: 100%;
height: 5px;
background: var(--el-color-primary);
opacity: 0.3;
cursor: ns-resize;
}
height: 100%;
section {
height: 100%;
display: flex;
// common
.box {
$header_height: 30px;
.header {
@@ -726,6 +674,7 @@ defineExpose({
user-select: none;
display: flex;
flex-direction: column;
padding-bottom: 30px;
.active {
background: #e9e9e9;
}
@@ -444,8 +444,8 @@ const focusTab = () => {
}, 200)
}
const inputCommand = (command) => {
command = command + (autoExecuteScript.value ? '\n' : '')
const inputCommand = (command, isSyncAllSession = false) => {
command = command + (isSyncAllSession ? '' : (autoExecuteScript.value ? '\n' : ''))
socket.value.emit('input', command)
}
+37 -42
View File
@@ -16,6 +16,7 @@
</template>
</el-dropdown>
<el-dropdown
v-if="!isPlusActive"
trigger="click"
max-height="50vh"
:teleported="false"
@@ -50,9 +51,9 @@
<span class="link_text">功能项<el-icon><arrow-down /></el-icon></span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="showInputCommand = true">
<!-- <el-dropdown-item @click="showInputCommand = true">
<span>长指令输入</span>
</el-dropdown-item>
</el-dropdown-item> -->
<el-dropdown-item @click="handleFullScreen">
<span>启用全屏</span>
</el-dropdown-item>
@@ -72,7 +73,7 @@
<div class="switch_wrap">
<el-tooltip
effect="dark"
content="开启后同步键盘输入到所有会话"
content="同步键盘输入到所有会话"
placement="bottom"
>
<el-switch
@@ -88,16 +89,16 @@
<div class="switch_wrap">
<el-tooltip
effect="dark"
content="SFTP文件传输"
content="包含SFTP文件传输、脚本库等功能"
placement="bottom"
>
<el-switch
v-model="showSftp"
v-model="showFooterBar"
class="swtich"
inline-prompt
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
active-text="SFTP"
inactive-text="SFTP"
active-text="工具栏"
inactive-text="工具栏"
/>
</el-tooltip>
</div>
@@ -148,13 +149,13 @@
</div>
</template>
<div class="tab_content_wrap" :style="{ height: mainHeight + 'px' }">
<!-- @cd-command="cdCommand" -->
<TerminalTab
ref="terminalRefs"
:host-obj="item"
:long-press-ctrl="longPressCtrl"
:long-press-alt="longPressAlt"
@input-command="terminalInput"
@cd-command="cdCommand"
@ping-data="getPingData"
@reset-long-press="resetLongPress"
/>
@@ -164,11 +165,11 @@
:long-press-alt="longPressAlt"
@click-key="handleClickVirtualKeyboard"
/>
<Sftp
v-if="showSftp"
ref="sftpRefs"
<FooterBar
v-if="showFooterBar"
:host-id="item.id"
@resize="resizeTerminal"
@exec-command="handleInputCommand"
/>
</div>
</el-tab-pane>
@@ -197,9 +198,9 @@ import FloatMenu from '@/components/float-menu/index.vue'
import { terminalStatusList, virtualKeyType } from '@/utils/enum'
import TerminalTab from './terminal-tab.vue'
import InfoSide from './info-side.vue'
import Sftp from './sftp.vue'
import HostForm from '../../server/components/host-form.vue'
import TerminalSetting from './terminal-setting.vue'
import FooterBar from './footer-bar.vue'
const { proxy: { $nextTick, $store, $message } } = getCurrentInstance()
@@ -216,12 +217,12 @@ const showInputCommand = ref(false)
const infoSideRef = ref(null)
const pingData = ref({})
const terminalRefs = ref([])
const sftpRefs = ref([])
// const sftpRefs = ref([])
const activeTabIndex = ref(0)
const visible = ref(true)
const showSftp = ref(localStorage.getItem('showSftp') === 'true')
const mainHeight = ref('')
const showFooterBar = ref(localStorage.getItem('showFooterBar') === 'true')
const isSyncAllSession = ref(false)
const mainHeight = ref('')
const hostFormVisible = ref(false)
const updateHostData = ref(null)
const showSetting = ref(false)
@@ -229,6 +230,7 @@ const showMobileInfoSideDialog = ref(false)
const longPressCtrl = ref(false)
const longPressAlt = ref(false)
const isPlusActive = computed(() => $store.isPlusActive)
const terminalTabs = computed(() => props.terminalTabs)
const terminalTabsLen = computed(() => props.terminalTabs.length)
const hostList = computed(() => $store.hostList)
@@ -328,21 +330,22 @@ const terminalInput = (command) => {
return index !== activeTabIndex.value
})
filterTerminalRefs.forEach(hostRef => {
hostRef.inputCommand(command)
hostRef.inputCommand(command, true)
})
}
const cdCommand = (path) => {
// console.log('cdCommand:', path)
if (!showSftp.value) return
if (isSyncAllSession.value) {
sftpRefs.value.forEach(sftpRef => {
sftpRef.openDir(path)
})
} else {
sftpRefs.value[activeTabIndex.value].openDir(path, false)
}
}
// 识别命令动态切换目录功能暂时取消
// const cdCommand = (path) => {
// // console.log('cdCommand:', path)
// if (!showSftp.value) return
// if (isSyncAllSession.value) {
// sftpRefs.value.forEach(sftpRef => {
// sftpRef.openDir(path)
// })
// } else {
// sftpRefs.value[activeTabIndex.value].openDir(path, false)
// }
// }
const getPingData = (data) => {
pingData.value[data.ip] = data
@@ -367,19 +370,15 @@ watch(terminalTabsLen, () => {
deep: false
})
watch(showSftp, () => {
localStorage.setItem('showSftp', showSftp.value)
nextTick(() => {
resizeTerminal()
})
watch(showFooterBar, async () => {
localStorage.setItem('showFooterBar', showFooterBar.value)
await $nextTick()
resizeTerminal()
}, {
immediate: true,
deep: false
})
// const windowBeforeUnload = () => {
// window.onbeforeunload = () => {
// return ''
// }
// }
const removeTab = (index) => {
emit('removeTab', index)
if (index === activeTabIndex.value) {
@@ -542,10 +541,6 @@ const handleInputCommand = async (command) => {
:deep(.terminal_tab_container) {
flex: 1;
}
:deep(.sftp_tab_container) {
height: 300px;
}
}
.full-screen-button {