mirror of
https://github.com/chaos-zhu/easynode.git
synced 2026-10-06 12:33:59 +08:00
✨ 增强脚本输入功能&优化与修复若干项
This commit is contained in:
@@ -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验证通过)
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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 @@
|
||||
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
|
||||
@@ -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,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
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 @@
|
||||
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
|
||||
@@ -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 }`)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 @@
|
||||
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=
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "3.0.3",
|
||||
"version": "3.0.4",
|
||||
"description": "easynode-server",
|
||||
"bin": "./bin/www",
|
||||
"scripts": {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
[
|
||||
{
|
||||
"version": "v3.0.4",
|
||||
"date": "2025-03-21",
|
||||
"features": [
|
||||
"新增脚本库分组功能",
|
||||
"终端页新增脚本库布局",
|
||||
"优化多行命令编辑器",
|
||||
"优化终端工具栏布局",
|
||||
"优化多项新增序号自动累加",
|
||||
"修复前端连接断开但未释放服务端ssh连接的bug",
|
||||
"修复终端页面同步会话在自动执行脚本开关为true时自动添加回车的bug"
|
||||
]
|
||||
}
|
||||
]
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "3.0.3",
|
||||
"version": "3.0.4",
|
||||
"description": "easynode-web",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -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' })
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -97,6 +97,7 @@ const plusFeatures = [
|
||||
'服务器跳板机功能,支持任意数量服务器的连续跳板',
|
||||
'批量修改实例配置(优化版)',
|
||||
'脚本库批量导出导入',
|
||||
'脚本库分组与终端工具栏支持',
|
||||
'凭据管理支持解密带密码保护的密钥',
|
||||
'通知方式无限制',
|
||||
'本地socket断开自动重连',
|
||||
|
||||
+113
-16
@@ -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 {
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user