mirror of
https://github.com/chaos-zhu/easynode.git
synced 2026-10-06 14:23:19 +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)
|
## [3.0.3](https://github.com/chaos-zhu/easynode/releases) (2024-12-22)
|
||||||
|
|
||||||
* 支持keyboard-interactive服务器验证(serv00验证通过)
|
* 支持keyboard-interactive服务器验证(serv00验证通过)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ module.exports = {
|
|||||||
hostListDBPath: path.join(process.cwd(),'app/db/host.db'),
|
hostListDBPath: path.join(process.cwd(),'app/db/host.db'),
|
||||||
groupConfDBPath: path.join(process.cwd(),'app/db/group.db'),
|
groupConfDBPath: path.join(process.cwd(),'app/db/group.db'),
|
||||||
scriptsDBPath: path.join(process.cwd(),'app/db/scripts.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'),
|
notifyDBPath: path.join(process.cwd(),'app/db/notify.db'),
|
||||||
notifyConfigDBPath: path.join(process.cwd(),'app/db/notify-config.db'),
|
notifyConfigDBPath: path.join(process.cwd(),'app/db/notify-config.db'),
|
||||||
onekeyDBPath: path.join(process.cwd(),'app/db/onekey.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()
|
const scriptsDB = new ScriptsDB().getInstance()
|
||||||
|
|
||||||
let localShell = JSON.parse(JSON.stringify(localShellJson)).map((item) => {
|
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 }) {
|
async function getScriptList({ res }) {
|
||||||
let data = await scriptsDB.findAsync({})
|
let data = await scriptsDB.findAsync({})
|
||||||
data = data.map(item => {
|
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?.sort((a, b) => Number(b.index || 0) - Number(a.index || 0))
|
||||||
data.push(...localShell)
|
data.push(...localShell)
|
||||||
@@ -24,19 +24,19 @@ async function getLocalScriptList({ res }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const addScript = async ({ res, request }) => {
|
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: '参数错误' })
|
if (!name || !command) return res.fail({ data: false, msg: '参数错误' })
|
||||||
index = Number(index) || 0
|
index = Number(index) || 0
|
||||||
let record = { name, description, command, index }
|
let record = { name, description, command, index, group }
|
||||||
await scriptsDB.insertAsync(record)
|
await scriptsDB.insertAsync(record)
|
||||||
res.success({ data: '添加成功' })
|
res.success({ data: '添加成功' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateScriptList = async ({ res, request }) => {
|
const updateScriptList = async ({ res, request }) => {
|
||||||
let { params: { id } } = 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: '参数错误' })
|
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: '修改成功' })
|
res.success({ data: '修改成功' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+16
-1
@@ -1,7 +1,7 @@
|
|||||||
const NodeRSA = require('node-rsa')
|
const NodeRSA = require('node-rsa')
|
||||||
const { randomStr } = require('./utils/tools')
|
const { randomStr } = require('./utils/tools')
|
||||||
const { AESEncryptAsync } = require('./utils/encrypt')
|
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() {
|
async function initKeyDB() {
|
||||||
const keyDB = new KeyDB().getInstance()
|
const keyDB = new KeyDB().getInstance()
|
||||||
@@ -96,9 +96,24 @@ async function initNotifyConfigDB() {
|
|||||||
return notifyConfigDB.insertAsync(defaultData)
|
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 () => {
|
module.exports = async () => {
|
||||||
await initKeyDB()
|
await initKeyDB()
|
||||||
await initNotifyDB()
|
await initNotifyDB()
|
||||||
await initGroupDB()
|
await initGroupDB()
|
||||||
|
await initScriptGroupDB()
|
||||||
await initNotifyConfigDB()
|
await initNotifyConfigDB()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const { login, getpublicKey, updatePwd, getEasynodeVersion, getMFA2Status, getMF
|
|||||||
const { getNotifyConfig, updateNotifyConfig, getNotifyList, updateNotifyList } = require('../controller/notify')
|
const { getNotifyConfig, updateNotifyConfig, getNotifyList, updateNotifyList } = require('../controller/notify')
|
||||||
const { getGroupList, addGroupList, updateGroupList, removeGroup } = require('../controller/group')
|
const { getGroupList, addGroupList, updateGroupList, removeGroup } = require('../controller/group')
|
||||||
const { getScriptList, getLocalScriptList, addScript, updateScriptList, removeScript, batchRemoveScript, importScript } = require('../controller/scripts')
|
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 { getOnekeyRecord, removeOnekeyRecord } = require('../controller/onekey')
|
||||||
const { getLog } = require('../controller/log')
|
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 = [
|
const onekey = [
|
||||||
{
|
{
|
||||||
method: 'get',
|
method: 'get',
|
||||||
@@ -237,4 +261,4 @@ const log = [
|
|||||||
controller: getLog
|
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 {
|
try {
|
||||||
let { authInfo: targetConnectionOptions } = await getConnectionOptions(hostId)
|
let { authInfo: targetConnectionOptions } = await getConnectionOptions(hostId)
|
||||||
let jumpHostResult = connectByJumpHosts && (await connectByJumpHosts(jumpHosts, targetConnectionOptions.host, targetConnectionOptions.port, socket))
|
let jumpHostResult = connectByJumpHosts && (await connectByJumpHosts(jumpHosts, targetConnectionOptions.host, targetConnectionOptions.port, socket))
|
||||||
|
let jumpSshClients = []
|
||||||
if (jumpHostResult) {
|
if (jumpHostResult) {
|
||||||
targetConnectionOptions.sock = jumpHostResult.sock
|
targetConnectionOptions.sock = jumpHostResult.sock
|
||||||
|
jumpSshClients = jumpHostResult.sshClients
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.emit('terminal_print_info', `准备连接目标终端: ${ name } - ${ host }`)
|
socket.emit('terminal_print_info', `准备连接目标终端: ${ name } - ${ host }`)
|
||||||
@@ -74,7 +76,7 @@ async function createTerminal(hostId, socket, targetSSHClient) {
|
|||||||
consola.success('终端连接成功:', host)
|
consola.success('终端连接成功:', host)
|
||||||
socket.emit('connect_terminal_success', `终端连接成功:${ host }`)
|
socket.emit('connect_terminal_success', `终端连接成功:${ host }`)
|
||||||
let stream = await createInteractiveShell(socket, targetSSHClient)
|
let stream = await createInteractiveShell(socket, targetSSHClient)
|
||||||
resolve(stream)
|
resolve({ stream, jumpSshClients })
|
||||||
})
|
})
|
||||||
.on('close', (err) => {
|
.on('close', (err) => {
|
||||||
if (closeNoticeFlag) return closeNoticeFlag = false
|
if (closeNoticeFlag) return closeNoticeFlag = false
|
||||||
@@ -124,6 +126,7 @@ module.exports = (httpServer) => {
|
|||||||
}
|
}
|
||||||
consola.success('terminal websocket 已连接')
|
consola.success('terminal websocket 已连接')
|
||||||
let targetSSHClient = null
|
let targetSSHClient = null
|
||||||
|
let jumpSshClients = []
|
||||||
socket.on('create', async ({ hostId, token }) => {
|
socket.on('create', async ({ hostId, token }) => {
|
||||||
const { code } = await verifyAuthSync(token, requestIP)
|
const { code } = await verifyAuthSync(token, requestIP)
|
||||||
if (code !== 1) {
|
if (code !== 1) {
|
||||||
@@ -132,7 +135,8 @@ module.exports = (httpServer) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
targetSSHClient = new SSHClient()
|
targetSSHClient = new SSHClient()
|
||||||
let stream = null
|
let { stream = null, jumpSshClients: jumpSshClientsFromCreate } = await createTerminal(hostId, socket, targetSSHClient)
|
||||||
|
jumpSshClients = jumpSshClientsFromCreate
|
||||||
function listenerInput(key) {
|
function listenerInput(key) {
|
||||||
if (targetSSHClient._sock.writable === false) return consola.info('终端连接已关闭,禁止输入')
|
if (targetSSHClient._sock.writable === false) return consola.info('终端连接已关闭,禁止输入')
|
||||||
stream && stream.write(key)
|
stream && stream.write(key)
|
||||||
@@ -142,7 +146,6 @@ module.exports = (httpServer) => {
|
|||||||
}
|
}
|
||||||
socket.on('input', listenerInput)
|
socket.on('input', listenerInput)
|
||||||
socket.on('resize', resizeShell)
|
socket.on('resize', resizeShell)
|
||||||
stream = await createTerminal(hostId, socket, targetSSHClient)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('get_ping', async (ip) => {
|
socket.on('get_ping', async (ip) => {
|
||||||
@@ -155,6 +158,10 @@ module.exports = (httpServer) => {
|
|||||||
|
|
||||||
socket.on('disconnect', (reason) => {
|
socket.on('disconnect', (reason) => {
|
||||||
connectionCount--
|
connectionCount--
|
||||||
|
targetSSHClient && targetSSHClient.end()
|
||||||
|
jumpSshClients?.forEach(sshClient => sshClient && sshClient.end())
|
||||||
|
targetSSHClient = null
|
||||||
|
jumpSshClients = null
|
||||||
consola.info(`终端socket连接断开: ${ reason } - 当前连接数: ${ connectionCount }`)
|
consola.info(`终端socket连接断开: ${ reason } - 当前连接数: ${ connectionCount }`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const {
|
|||||||
notifyConfigDBPath,
|
notifyConfigDBPath,
|
||||||
groupConfDBPath,
|
groupConfDBPath,
|
||||||
scriptsDBPath,
|
scriptsDBPath,
|
||||||
|
scriptGroupDBPath,
|
||||||
onekeyDBPath,
|
onekeyDBPath,
|
||||||
logDBPath,
|
logDBPath,
|
||||||
plusDBPath
|
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 {
|
module.exports.OnekeyDB = class OnekeyDB {
|
||||||
constructor() {
|
constructor() {
|
||||||
if (!OnekeyDB.instance) {
|
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 (ip) return ip
|
||||||
}
|
}
|
||||||
if (freeipapi.status === 'fulfilled') {
|
if (freeipapi.status === 'fulfilled') {
|
||||||
let ip = pconline.value?.data?.ipAddress
|
let ip = freeipapi.value?.data?.ipAddress
|
||||||
if (ip) return ip
|
if (ip) return ip
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "3.0.3",
|
"version": "3.0.4",
|
||||||
"description": "easynode-server",
|
"description": "easynode-server",
|
||||||
"bin": "./bin/www",
|
"bin": "./bin/www",
|
||||||
"scripts": {
|
"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",
|
"name": "web",
|
||||||
"version": "3.0.3",
|
"version": "3.0.4",
|
||||||
"description": "easynode-web",
|
"description": "easynode-web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -118,6 +118,18 @@ export default {
|
|||||||
batchRemoveScript(data) {
|
batchRemoveScript(data) {
|
||||||
return axios({ url: '/batch-remove-script', method: 'post', 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() {
|
getOnekeyRecord() {
|
||||||
return axios({ url: '/onekey', method: 'get' })
|
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
|
// custom dark variables
|
||||||
@use "./dark.scss";
|
@use "./dark.scss";
|
||||||
|
|
||||||
|
.el-tabs {
|
||||||
|
--el-tabs-header-height: 32px;
|
||||||
|
}
|
||||||
@@ -97,6 +97,7 @@ const plusFeatures = [
|
|||||||
'服务器跳板机功能,支持任意数量服务器的连续跳板',
|
'服务器跳板机功能,支持任意数量服务器的连续跳板',
|
||||||
'批量修改实例配置(优化版)',
|
'批量修改实例配置(优化版)',
|
||||||
'脚本库批量导出导入',
|
'脚本库批量导出导入',
|
||||||
|
'脚本库分组与终端工具栏支持',
|
||||||
'凭据管理支持解密带密码保护的密钥',
|
'凭据管理支持解密带密码保护的密钥',
|
||||||
'通知方式无限制',
|
'通知方式无限制',
|
||||||
'本地socket断开自动重连',
|
'本地socket断开自动重连',
|
||||||
|
|||||||
+113
-16
@@ -68,20 +68,31 @@
|
|||||||
<p v-if="isNew" class="conspicuous">
|
<p v-if="isNew" class="conspicuous">
|
||||||
新版本可用: {{ latestVersion }} -> <a
|
新版本可用: {{ latestVersion }} -> <a
|
||||||
class="link"
|
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"
|
target="_blank"
|
||||||
>https://github.com/chaos-zhu/easynode/releases</a>
|
>https://github.com/chaos-zhu/easynode</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>
|
|
||||||
</p>
|
</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>
|
<p>
|
||||||
TG更新通知频道:<a class="link" href="https://t.me/easynode_notify" target="_blank">https://t.me/easynode_notify</a>
|
TG更新通知频道:<a class="link" href="https://t.me/easynode_notify" target="_blank">https://t.me/easynode_notify</a>
|
||||||
</p>
|
</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">
|
<div class="about_footer">
|
||||||
<el-button type="info" @click="visible = false">关闭</el-button>
|
<el-button type="info" @click="visible = false">关闭</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -106,7 +117,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, getCurrentInstance, computed, onMounted, onBeforeUnmount } from 'vue'
|
import { ref, getCurrentInstance, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
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 packageJson from '../../package.json'
|
||||||
import MenuList from './menuList.vue'
|
import MenuList from './menuList.vue'
|
||||||
|
|
||||||
@@ -116,6 +127,7 @@ const visible = ref(false)
|
|||||||
const checkVersionErr = ref(false)
|
const checkVersionErr = ref(false)
|
||||||
const currentVersion = ref(`v${ packageJson.version }`)
|
const currentVersion = ref(`v${ packageJson.version }`)
|
||||||
const latestVersion = ref(null)
|
const latestVersion = ref(null)
|
||||||
|
const features = ref([])
|
||||||
const menuCollapse = ref(false)
|
const menuCollapse = ref(false)
|
||||||
const discount = ref(false)
|
const discount = ref(false)
|
||||||
|
|
||||||
@@ -145,25 +157,22 @@ const gotoPlusPage = () => {
|
|||||||
router.push('/setting?tabKey=plus')
|
router.push('/setting?tabKey=plus')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkLatestVersion() {
|
async function checkLatestVersionByGitRelease() {
|
||||||
const timeout = 3000
|
const timeout = 5000
|
||||||
try {
|
try {
|
||||||
const timeoutPromise = new Promise((_, reject) =>
|
const timeoutPromise = new Promise((_, reject) =>
|
||||||
setTimeout(() => reject(new Error('请求超时')), timeout)
|
setTimeout(() => reject(new Error('请求超时')), timeout)
|
||||||
)
|
)
|
||||||
|
|
||||||
const url = `https://api.github.com/repos/chaos-zhu/easynode/releases?ts=${ new Date().getTime() }`
|
const url = `https://api.github.com/repos/chaos-zhu/easynode/releases?ts=${ new Date().getTime() }`
|
||||||
const fetchPromise = fetch(url, {
|
const fetchPromise = fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/vnd.github.v3+json'
|
'Accept': 'application/vnd.github.v3+json'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const response = await Promise.race([fetchPromise, timeoutPromise,])
|
const response = await Promise.race([fetchPromise, timeoutPromise,])
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('版本信息请求失败: ' + response.statusText)
|
throw new Error('版本信息请求失败: ' + response.statusText)
|
||||||
}
|
}
|
||||||
|
|
||||||
const releases = await response.json()
|
const releases = await response.json()
|
||||||
// console.log('releases:', releases)
|
// console.log('releases:', releases)
|
||||||
const filteredReleases = releases.filter(release => !release.tag_name.startsWith('client'))
|
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
|
let timer = null
|
||||||
const checkFirstVisit = () => {
|
const checkFirstVisit = () => {
|
||||||
@@ -237,6 +287,7 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
.new_version {
|
.new_version {
|
||||||
color: red;
|
color: red;
|
||||||
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,6 +350,52 @@ onBeforeUnmount(() => {
|
|||||||
color: #F56C6C;
|
color: #F56C6C;
|
||||||
font-weight: 500;
|
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 {
|
.about_footer {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const useStore = defineStore({
|
|||||||
groupList: [],
|
groupList: [],
|
||||||
sshList: [],
|
sshList: [],
|
||||||
scriptList: [],
|
scriptList: [],
|
||||||
|
scriptGroupList: [],
|
||||||
localScriptList: [],
|
localScriptList: [],
|
||||||
HostStatusSocket: null,
|
HostStatusSocket: null,
|
||||||
user: localStorage.getItem('user') || null,
|
user: localStorage.getItem('user') || null,
|
||||||
@@ -71,6 +72,7 @@ const useStore = defineStore({
|
|||||||
await this.getHostList()
|
await this.getHostList()
|
||||||
await this.getSSHList()
|
await this.getSSHList()
|
||||||
await this.getScriptList()
|
await this.getScriptList()
|
||||||
|
await this.getScriptGroupList()
|
||||||
await this.getPlusInfo()
|
await this.getPlusInfo()
|
||||||
this.wsClientsStatus()
|
this.wsClientsStatus()
|
||||||
},
|
},
|
||||||
@@ -96,6 +98,10 @@ const useStore = defineStore({
|
|||||||
const { data: scriptList } = await $api.getScriptList()
|
const { data: scriptList } = await $api.getScriptList()
|
||||||
this.$patch({ scriptList })
|
this.$patch({ scriptList })
|
||||||
},
|
},
|
||||||
|
async getScriptGroupList() {
|
||||||
|
const { data: scriptGroupList } = await $api.getScriptGroupList()
|
||||||
|
this.$patch({ scriptGroupList })
|
||||||
|
},
|
||||||
async getLocalScriptList() {
|
async getLocalScriptList() {
|
||||||
const { data: localScriptList } = await $api.getLocalScriptList()
|
const { data: localScriptList } = await $api.getLocalScriptList()
|
||||||
this.$patch({ localScriptList })
|
this.$patch({ localScriptList })
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ const handleDecryptKey = async () => {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.credentials_container {
|
.credentials_container {
|
||||||
padding: 20px;
|
padding: 0 20px 20px 20px;
|
||||||
.header {
|
.header {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -120,6 +120,8 @@ const list = computed(() => {
|
|||||||
|
|
||||||
let addGroup = () => {
|
let addGroup = () => {
|
||||||
groupForm.id = null
|
groupForm.id = null
|
||||||
|
const maxIndex = Math.max(...groupList.value.map(item => item.index), 0)
|
||||||
|
groupForm.index = maxIndex + 1
|
||||||
groupFormVisible.value = true
|
groupFormVisible.value = true
|
||||||
isModify.value = false
|
isModify.value = false
|
||||||
}
|
}
|
||||||
@@ -167,10 +169,9 @@ const deleteGroup = ({ id, name }) => {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.group_container {
|
.group_container {
|
||||||
padding: 20px;
|
padding:0 20px 20px 20px;
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
padding: 15px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
|
|||||||
@@ -444,7 +444,7 @@ onActivated(async () => {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.onekey_container {
|
.onekey_container {
|
||||||
padding: 20px;
|
padding: 0 20px 20px 20px;
|
||||||
.header {
|
.header {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
display: flex;
|
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
|
<el-button
|
||||||
v-show="selectScripts.length"
|
v-show="selectScripts.length"
|
||||||
type="danger"
|
type="danger"
|
||||||
class="batch_remove_btn"
|
|
||||||
@click="handleBatchRemove"
|
@click="handleBatchRemove"
|
||||||
>
|
>
|
||||||
批量删除
|
批量删除
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="primary" class="add_script_btn" @click="addScript">添加脚本</el-button>
|
<el-button type="primary" @click="addScript">添加脚本</el-button>
|
||||||
<PlusSupportTip>
|
<PlusSupportTip>
|
||||||
<el-dropdown trigger="click" :disabled="!isPlusActive">
|
<el-dropdown trigger="click" :disabled="!isPlusActive">
|
||||||
<el-button type="primary" class="group_action_btn" :disabled="!isPlusActive">
|
<el-button type="primary" class="group_action_btn" :disabled="!isPlusActive">
|
||||||
@@ -34,126 +33,98 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
</PlusSupportTip>
|
</PlusSupportTip>
|
||||||
</div>
|
<PlusSupportTip>
|
||||||
<el-table
|
<el-button
|
||||||
v-loading="loading"
|
type="primary"
|
||||||
:data="paginatedFilteredList"
|
class="group_action_btn"
|
||||||
@selection-change="handleSelectionChange"
|
:disabled="!isPlusActive"
|
||||||
>
|
@click="ScriptGroupVisible = true"
|
||||||
<el-table-column
|
>
|
||||||
type="selection"
|
分组管理
|
||||||
width="55"
|
</el-button>
|
||||||
:selectable="(row) => {
|
</PlusSupportTip>
|
||||||
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"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-dialog
|
<el-tabs v-model="activeTab" type="border-card" class="script-tabs">
|
||||||
v-model="formVisible"
|
<el-tab-pane
|
||||||
width="600px"
|
v-for="group in groupList"
|
||||||
top="150px"
|
:key="group.id"
|
||||||
:title="isModify ? '修改脚本' : '添加脚本'"
|
:label="group.name"
|
||||||
:close-on-click-modal="false"
|
:name="group.id"
|
||||||
@close="clearFormInfo"
|
|
||||||
>
|
|
||||||
<el-form
|
|
||||||
ref="updateFormRef"
|
|
||||||
:model="formData"
|
|
||||||
:rules="rules"
|
|
||||||
:hide-required-asterisk="true"
|
|
||||||
label-suffix=":"
|
|
||||||
label-width="100px"
|
|
||||||
:show-message="false"
|
|
||||||
>
|
>
|
||||||
<el-form-item label="名称" prop="name">
|
<el-table
|
||||||
<el-input
|
v-loading="loading"
|
||||||
v-model="formData.name"
|
:data="getFilteredScriptsByGroup(group.id)"
|
||||||
clearable
|
@selection-change="handleSelectionChange"
|
||||||
placeholder=""
|
>
|
||||||
autocomplete="off"
|
<el-table-column
|
||||||
|
type="selection"
|
||||||
|
width="55"
|
||||||
|
:selectable="(row) => {
|
||||||
|
return row.index !== '--' && row.index !== '-' && row.index !== undefined && row.index !== null
|
||||||
|
}"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
<el-table-column prop="index" label="序号" width="100px" />
|
||||||
<el-form-item label="描述" prop="description">
|
<el-table-column prop="name" label="名称" />
|
||||||
<el-input
|
<el-table-column prop="description" label="描述" />
|
||||||
v-model="formData.description"
|
<el-table-column prop="command" label="指令内容" show-overflow-tooltip />
|
||||||
clearable
|
<el-table-column label="操作" fixed="right" width="160px">
|
||||||
placeholder=""
|
<template #default="{ row }">
|
||||||
autocomplete="off"
|
<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>
|
</div>
|
||||||
<el-form-item label="序号" prop="index">
|
</el-tab-pane>
|
||||||
<el-input
|
</el-tabs>
|
||||||
v-model.trim.number="formData.index"
|
|
||||||
clearable
|
<ScriptEdit
|
||||||
placeholder=""
|
v-model:show="formVisible"
|
||||||
autocomplete="off"
|
:default-data="currentScript"
|
||||||
/>
|
:default-group="activeTab"
|
||||||
</el-form-item>
|
@success="handleEditSuccess"
|
||||||
<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>
|
|
||||||
|
|
||||||
<ImportScript
|
<ImportScript
|
||||||
v-model:show="importVisible"
|
v-model:show="importVisible"
|
||||||
@update-list="() => $store.getScriptList()"
|
@update-list="() => $store.getScriptList()"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ScriptGroup
|
||||||
|
v-model:show="ScriptGroupVisible"
|
||||||
|
@group-deleted="handleGroupDeleted"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<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 ImportScript from './components/import-script.vue'
|
||||||
import PlusSupportTip from '@/components/common/PlusSupportTip.vue'
|
import PlusSupportTip from '@/components/common/PlusSupportTip.vue'
|
||||||
import { ArrowDown, Search } from '@element-plus/icons-vue'
|
import { ArrowDown, Search } from '@element-plus/icons-vue'
|
||||||
import { exportFile } from '@/utils'
|
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 { proxy: { $api, $message, $messageBox, $store, $tools } } = getCurrentInstance()
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const formVisible = ref(false)
|
const formVisible = ref(false)
|
||||||
let isModify = ref(false)
|
|
||||||
const selectScripts = ref([])
|
const selectScripts = ref([])
|
||||||
const handleSelectionChange = (val) => {
|
const handleSelectionChange = (val) => {
|
||||||
selectScripts.value = val
|
selectScripts.value = val
|
||||||
@@ -173,57 +144,21 @@ const handleBatchRemove = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let formData = reactive({
|
const currentScript = ref({})
|
||||||
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 scriptList = computed(() => $store.scriptList)
|
const scriptList = computed(() => $store.scriptList)
|
||||||
const isPlusActive = computed(() => $store.isPlusActive)
|
const isPlusActive = computed(() => $store.isPlusActive)
|
||||||
|
|
||||||
let addScript = () => {
|
const groupList = computed(() => $store.scriptGroupList || [])
|
||||||
formData.id = null
|
|
||||||
formData.index = scriptList.value.reduce((acc, cur) => Math.max(acc, Number(cur.index) || 0), 0) + 1
|
const addScript = () => {
|
||||||
isModify.value = false
|
currentScript.value = {}
|
||||||
formVisible.value = true
|
formVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleChange = (row) => {
|
const handleChange = (row) => {
|
||||||
Object.assign(formData, { ...row })
|
currentScript.value = { ...row }
|
||||||
formVisible.value = true
|
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 }) => {
|
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 start = (currentPage.value - 1) * pageSize.value
|
||||||
const end = start + pageSize.value
|
const end = start + pageSize.value
|
||||||
return filteredScriptList.value.slice(start, end)
|
return groupScripts.slice(start, end)
|
||||||
})
|
}
|
||||||
|
|
||||||
const handleSizeChange = (val) => {
|
const handleSizeChange = (val) => {
|
||||||
pageSize.value = val
|
pageSize.value = val
|
||||||
@@ -282,24 +220,46 @@ const handleSearch = () => {
|
|||||||
currentPage.value = 1
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.scripts_container {
|
.scripts_container {
|
||||||
padding: 20px;
|
padding: 0 20px 20px 20px;
|
||||||
.header {
|
.header {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
.add_script_btn {
|
.group_action_btn {
|
||||||
margin: 0 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
.search_input {
|
.search_input {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.script-tabs {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.host_count {
|
.host_count {
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="visible"
|
v-model="visible"
|
||||||
width="600px"
|
width="600px"
|
||||||
top="45px"
|
:top="isMobile() ? '45px' : '15vh'"
|
||||||
:append-to-body="false"
|
:append-to-body="false"
|
||||||
title="分组管理"
|
title="实例分组管理"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
>
|
>
|
||||||
<Group />
|
<Group />
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import Group from '@/views/group/index.vue'
|
import Group from '@/views/group/index.vue'
|
||||||
|
import { isMobile } from '@/utils'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
show: {
|
show: {
|
||||||
|
|||||||
@@ -382,7 +382,12 @@ let confHostList = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const setDefaultData = () => {
|
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
|
// eslint-disable-next-line no-unused-vars
|
||||||
let { id, ...rest } = defaultData.value
|
let { id, ...rest } = defaultData.value
|
||||||
for (let [key,] of Object.entries(hostForm.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>
|
<template>
|
||||||
<div ref="sftpTabContainerRef" class="sftp_tab_container">
|
<div class="sftp_tab_container">
|
||||||
<div ref="adjustRef" class="adjust" />
|
|
||||||
<section>
|
<section>
|
||||||
<div class="left box">
|
<div class="left box">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
@@ -159,7 +158,7 @@
|
|||||||
import { ref, computed, onMounted, onBeforeUnmount, getCurrentInstance } from 'vue'
|
import { ref, computed, onMounted, onBeforeUnmount, getCurrentInstance } from 'vue'
|
||||||
import socketIo from 'socket.io-client'
|
import socketIo from 'socket.io-client'
|
||||||
import CodeEdit from '@/components/code-edit/index.vue'
|
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 dirIcon from '@/assets/image/system/dir.png'
|
||||||
import linkIcon from '@/assets/image/system/link.png'
|
import linkIcon from '@/assets/image/system/link.png'
|
||||||
import fileIcon from '@/assets/image/system/file.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 { proxy: { $store, $notification, $message, $messageBox, $serviceURI, $nextTick } } = getCurrentInstance()
|
||||||
|
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
@@ -204,8 +201,6 @@ const curUploadFileName = ref('')
|
|||||||
const showDirProgress = ref(false)
|
const showDirProgress = ref(false)
|
||||||
const curUploadDirName = ref(0)
|
const curUploadDirName = ref(0)
|
||||||
|
|
||||||
const adjustRef = ref(null)
|
|
||||||
const sftpTabContainerRef = ref(null)
|
|
||||||
const childDirRef = ref(null)
|
const childDirRef = ref(null)
|
||||||
const uploadFileRef = ref(null)
|
const uploadFileRef = ref(null)
|
||||||
const uploadDirRef = ref(null)
|
const uploadDirRef = ref(null)
|
||||||
@@ -221,46 +216,12 @@ const fileList = computed(() => childDir.value.filter(({ name }) => name.include
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
connectSftp()
|
connectSftp()
|
||||||
adjustHeight()
|
|
||||||
EventBus.$on('update-sftp-tab-height', () => {
|
|
||||||
adjustHeight()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (socket.value) socket.value.close()
|
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 = () => {
|
const connectSftp = () => {
|
||||||
socket.value = io($serviceURI, {
|
socket.value = io($serviceURI, {
|
||||||
path: '/sftp',
|
path: '/sftp',
|
||||||
@@ -653,23 +614,10 @@ defineExpose({
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.sftp_tab_container {
|
.sftp_tab_container {
|
||||||
position: relative;
|
height: 100%;
|
||||||
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;
|
|
||||||
}
|
|
||||||
section {
|
section {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
// common
|
|
||||||
.box {
|
.box {
|
||||||
$header_height: 30px;
|
$header_height: 30px;
|
||||||
.header {
|
.header {
|
||||||
@@ -726,6 +674,7 @@ defineExpose({
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
padding-bottom: 30px;
|
||||||
.active {
|
.active {
|
||||||
background: #e9e9e9;
|
background: #e9e9e9;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -444,8 +444,8 @@ const focusTab = () => {
|
|||||||
}, 200)
|
}, 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputCommand = (command) => {
|
const inputCommand = (command, isSyncAllSession = false) => {
|
||||||
command = command + (autoExecuteScript.value ? '\n' : '')
|
command = command + (isSyncAllSession ? '' : (autoExecuteScript.value ? '\n' : ''))
|
||||||
socket.value.emit('input', command)
|
socket.value.emit('input', command)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
<el-dropdown
|
<el-dropdown
|
||||||
|
v-if="!isPlusActive"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
max-height="50vh"
|
max-height="50vh"
|
||||||
:teleported="false"
|
:teleported="false"
|
||||||
@@ -50,9 +51,9 @@
|
|||||||
<span class="link_text">功能项<el-icon><arrow-down /></el-icon></span>
|
<span class="link_text">功能项<el-icon><arrow-down /></el-icon></span>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<el-dropdown-menu>
|
<el-dropdown-menu>
|
||||||
<el-dropdown-item @click="showInputCommand = true">
|
<!-- <el-dropdown-item @click="showInputCommand = true">
|
||||||
<span>长指令输入</span>
|
<span>长指令输入</span>
|
||||||
</el-dropdown-item>
|
</el-dropdown-item> -->
|
||||||
<el-dropdown-item @click="handleFullScreen">
|
<el-dropdown-item @click="handleFullScreen">
|
||||||
<span>启用全屏</span>
|
<span>启用全屏</span>
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
@@ -72,7 +73,7 @@
|
|||||||
<div class="switch_wrap">
|
<div class="switch_wrap">
|
||||||
<el-tooltip
|
<el-tooltip
|
||||||
effect="dark"
|
effect="dark"
|
||||||
content="开启后同步键盘输入到所有会话"
|
content="同步键盘输入到所有会话"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
>
|
>
|
||||||
<el-switch
|
<el-switch
|
||||||
@@ -88,16 +89,16 @@
|
|||||||
<div class="switch_wrap">
|
<div class="switch_wrap">
|
||||||
<el-tooltip
|
<el-tooltip
|
||||||
effect="dark"
|
effect="dark"
|
||||||
content="SFTP文件传输"
|
content="包含SFTP文件传输、脚本库等功能"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
>
|
>
|
||||||
<el-switch
|
<el-switch
|
||||||
v-model="showSftp"
|
v-model="showFooterBar"
|
||||||
class="swtich"
|
class="swtich"
|
||||||
inline-prompt
|
inline-prompt
|
||||||
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
|
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
|
||||||
active-text="SFTP"
|
active-text="工具栏"
|
||||||
inactive-text="SFTP"
|
inactive-text="工具栏"
|
||||||
/>
|
/>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -148,13 +149,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="tab_content_wrap" :style="{ height: mainHeight + 'px' }">
|
<div class="tab_content_wrap" :style="{ height: mainHeight + 'px' }">
|
||||||
|
<!-- @cd-command="cdCommand" -->
|
||||||
<TerminalTab
|
<TerminalTab
|
||||||
ref="terminalRefs"
|
ref="terminalRefs"
|
||||||
:host-obj="item"
|
:host-obj="item"
|
||||||
:long-press-ctrl="longPressCtrl"
|
:long-press-ctrl="longPressCtrl"
|
||||||
:long-press-alt="longPressAlt"
|
:long-press-alt="longPressAlt"
|
||||||
@input-command="terminalInput"
|
@input-command="terminalInput"
|
||||||
@cd-command="cdCommand"
|
|
||||||
@ping-data="getPingData"
|
@ping-data="getPingData"
|
||||||
@reset-long-press="resetLongPress"
|
@reset-long-press="resetLongPress"
|
||||||
/>
|
/>
|
||||||
@@ -164,11 +165,11 @@
|
|||||||
:long-press-alt="longPressAlt"
|
:long-press-alt="longPressAlt"
|
||||||
@click-key="handleClickVirtualKeyboard"
|
@click-key="handleClickVirtualKeyboard"
|
||||||
/>
|
/>
|
||||||
<Sftp
|
<FooterBar
|
||||||
v-if="showSftp"
|
v-if="showFooterBar"
|
||||||
ref="sftpRefs"
|
|
||||||
:host-id="item.id"
|
:host-id="item.id"
|
||||||
@resize="resizeTerminal"
|
@resize="resizeTerminal"
|
||||||
|
@exec-command="handleInputCommand"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
@@ -197,9 +198,9 @@ import FloatMenu from '@/components/float-menu/index.vue'
|
|||||||
import { terminalStatusList, virtualKeyType } from '@/utils/enum'
|
import { terminalStatusList, virtualKeyType } from '@/utils/enum'
|
||||||
import TerminalTab from './terminal-tab.vue'
|
import TerminalTab from './terminal-tab.vue'
|
||||||
import InfoSide from './info-side.vue'
|
import InfoSide from './info-side.vue'
|
||||||
import Sftp from './sftp.vue'
|
|
||||||
import HostForm from '../../server/components/host-form.vue'
|
import HostForm from '../../server/components/host-form.vue'
|
||||||
import TerminalSetting from './terminal-setting.vue'
|
import TerminalSetting from './terminal-setting.vue'
|
||||||
|
import FooterBar from './footer-bar.vue'
|
||||||
|
|
||||||
const { proxy: { $nextTick, $store, $message } } = getCurrentInstance()
|
const { proxy: { $nextTick, $store, $message } } = getCurrentInstance()
|
||||||
|
|
||||||
@@ -216,12 +217,12 @@ const showInputCommand = ref(false)
|
|||||||
const infoSideRef = ref(null)
|
const infoSideRef = ref(null)
|
||||||
const pingData = ref({})
|
const pingData = ref({})
|
||||||
const terminalRefs = ref([])
|
const terminalRefs = ref([])
|
||||||
const sftpRefs = ref([])
|
// const sftpRefs = ref([])
|
||||||
const activeTabIndex = ref(0)
|
const activeTabIndex = ref(0)
|
||||||
const visible = ref(true)
|
const visible = ref(true)
|
||||||
const showSftp = ref(localStorage.getItem('showSftp') === 'true')
|
const showFooterBar = ref(localStorage.getItem('showFooterBar') === 'true')
|
||||||
const mainHeight = ref('')
|
|
||||||
const isSyncAllSession = ref(false)
|
const isSyncAllSession = ref(false)
|
||||||
|
const mainHeight = ref('')
|
||||||
const hostFormVisible = ref(false)
|
const hostFormVisible = ref(false)
|
||||||
const updateHostData = ref(null)
|
const updateHostData = ref(null)
|
||||||
const showSetting = ref(false)
|
const showSetting = ref(false)
|
||||||
@@ -229,6 +230,7 @@ const showMobileInfoSideDialog = ref(false)
|
|||||||
const longPressCtrl = ref(false)
|
const longPressCtrl = ref(false)
|
||||||
const longPressAlt = ref(false)
|
const longPressAlt = ref(false)
|
||||||
|
|
||||||
|
const isPlusActive = computed(() => $store.isPlusActive)
|
||||||
const terminalTabs = computed(() => props.terminalTabs)
|
const terminalTabs = computed(() => props.terminalTabs)
|
||||||
const terminalTabsLen = computed(() => props.terminalTabs.length)
|
const terminalTabsLen = computed(() => props.terminalTabs.length)
|
||||||
const hostList = computed(() => $store.hostList)
|
const hostList = computed(() => $store.hostList)
|
||||||
@@ -328,21 +330,22 @@ const terminalInput = (command) => {
|
|||||||
return index !== activeTabIndex.value
|
return index !== activeTabIndex.value
|
||||||
})
|
})
|
||||||
filterTerminalRefs.forEach(hostRef => {
|
filterTerminalRefs.forEach(hostRef => {
|
||||||
hostRef.inputCommand(command)
|
hostRef.inputCommand(command, true)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const cdCommand = (path) => {
|
// 识别命令动态切换目录功能暂时取消
|
||||||
// console.log('cdCommand:', path)
|
// const cdCommand = (path) => {
|
||||||
if (!showSftp.value) return
|
// // console.log('cdCommand:', path)
|
||||||
if (isSyncAllSession.value) {
|
// if (!showSftp.value) return
|
||||||
sftpRefs.value.forEach(sftpRef => {
|
// if (isSyncAllSession.value) {
|
||||||
sftpRef.openDir(path)
|
// sftpRefs.value.forEach(sftpRef => {
|
||||||
})
|
// sftpRef.openDir(path)
|
||||||
} else {
|
// })
|
||||||
sftpRefs.value[activeTabIndex.value].openDir(path, false)
|
// } else {
|
||||||
}
|
// sftpRefs.value[activeTabIndex.value].openDir(path, false)
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
const getPingData = (data) => {
|
const getPingData = (data) => {
|
||||||
pingData.value[data.ip] = data
|
pingData.value[data.ip] = data
|
||||||
@@ -367,19 +370,15 @@ watch(terminalTabsLen, () => {
|
|||||||
deep: false
|
deep: false
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(showSftp, () => {
|
watch(showFooterBar, async () => {
|
||||||
localStorage.setItem('showSftp', showSftp.value)
|
localStorage.setItem('showFooterBar', showFooterBar.value)
|
||||||
nextTick(() => {
|
await $nextTick()
|
||||||
resizeTerminal()
|
resizeTerminal()
|
||||||
})
|
}, {
|
||||||
|
immediate: true,
|
||||||
|
deep: false
|
||||||
})
|
})
|
||||||
|
|
||||||
// const windowBeforeUnload = () => {
|
|
||||||
// window.onbeforeunload = () => {
|
|
||||||
// return ''
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
const removeTab = (index) => {
|
const removeTab = (index) => {
|
||||||
emit('removeTab', index)
|
emit('removeTab', index)
|
||||||
if (index === activeTabIndex.value) {
|
if (index === activeTabIndex.value) {
|
||||||
@@ -542,10 +541,6 @@ const handleInputCommand = async (command) => {
|
|||||||
:deep(.terminal_tab_container) {
|
:deep(.terminal_tab_container) {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.sftp_tab_container) {
|
|
||||||
height: 300px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.full-screen-button {
|
.full-screen-button {
|
||||||
|
|||||||
Reference in New Issue
Block a user