✨ 支持cd全路径指令联动SFTP面板
This commit is contained in:
parent
324463d649
commit
2977e61ed5
@ -35,12 +35,12 @@ const pipeStream = (path, writeStream) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function listenInput(sftpClient, socket) {
|
function listenInput(sftpClient, socket) {
|
||||||
socket.on('open_dir', async (path) => {
|
socket.on('open_dir', async (path, tips = true) => {
|
||||||
const exists = await sftpClient.exists(path)
|
const exists = await sftpClient.exists(path)
|
||||||
if(!exists) return socket.emit('not_exists_dir', '目录不存在或当前不可访问')
|
if(!exists) return socket.emit('not_exists_dir', tips ? '目录不存在或当前不可访问' : '')
|
||||||
try {
|
try {
|
||||||
let dirLs = await sftpClient.list(path)
|
let dirLs = await sftpClient.list(path)
|
||||||
socket.emit('dir_ls', dirLs)
|
socket.emit('dir_ls', dirLs, path)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
consola.error('open_dir Error', error.message)
|
consola.error('open_dir Error', error.message)
|
||||||
socket.emit('sftp_error', error.message)
|
socket.emit('sftp_error', error.message)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "2.1.3",
|
"version": "2.1.4",
|
||||||
"description": "easynode-web",
|
"description": "easynode-web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
{{ FileName }} - <span>{{ status }}</span>
|
{{ filename }}
|
||||||
|
<!-- {{ filename }} - <span>{{ status }}</span> -->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<codemirror
|
<codemirror
|
||||||
|
@ -1,19 +1,24 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
// import HostList from '@views/list/index.vue'
|
import Login from '@views/login/index.vue'
|
||||||
// import Login from '@views/login/index.vue'
|
import Container from '@views/index.vue'
|
||||||
// import terminal from '@views/terminal/index.vue'
|
import Server from '@views/server/index.vue'
|
||||||
// import test from '@views/test/index.vue'
|
import Terminal from '@views/terminal/index.vue'
|
||||||
|
import Credentials from '@views/credentials/index.vue'
|
||||||
|
import Group from '@views/group/index.vue'
|
||||||
|
import Onekey from '@views/onekey/index.vue'
|
||||||
|
import Scripts from '@views/scripts/index.vue'
|
||||||
|
import Setting from '@views/setting/index.vue'
|
||||||
|
|
||||||
const Login = () => import('@views/login/index.vue')
|
// const Login = () => import('@views/login/index.vue')
|
||||||
const Container = () => import('@views/index.vue')
|
// const Container = () => import('@views/index.vue')
|
||||||
const Server = () => import('@views/server/index.vue')
|
// const Server = () => import('@views/server/index.vue')
|
||||||
const Terminal = () => import('@views/terminal/index.vue')
|
// const Terminal = () => import('@views/terminal/index.vue')
|
||||||
const Credentials = () => import('@views/credentials/index.vue')
|
// const Credentials = () => import('@views/credentials/index.vue')
|
||||||
const Group = () => import('@views/group/index.vue')
|
// const Group = () => import('@views/group/index.vue')
|
||||||
const Onekey = () => import('@views/onekey/index.vue')
|
// const Onekey = () => import('@views/onekey/index.vue')
|
||||||
const Scripts = () => import('@views/scripts/index.vue')
|
// const Scripts = () => import('@views/scripts/index.vue')
|
||||||
const Setting = () => import('@views/setting/index.vue')
|
// const Setting = () => import('@views/setting/index.vue')
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{ path: '/login', component: Login },
|
{ path: '/login', component: Login },
|
||||||
|
@ -281,12 +281,17 @@ const connectSftp = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const listenSftp = () => {
|
const listenSftp = () => {
|
||||||
socket.value.on('dir_ls', (dirLs) => {
|
socket.value.on('dir_ls', (dirLs, path) => {
|
||||||
childDir.value = sortDirTree(dirLs)
|
childDir.value = sortDirTree(dirLs)
|
||||||
childDirLoading.value = false
|
childDirLoading.value = false
|
||||||
|
// 格式化path为当前目录
|
||||||
|
let formatPath = path.split('/').filter(item => item)
|
||||||
|
formatPath.unshift('/')
|
||||||
|
// console.log('formatPath:', formatPath)
|
||||||
|
paths.value = formatPath
|
||||||
})
|
})
|
||||||
socket.value.on('not_exists_dir', (errMsg) => {
|
socket.value.on('not_exists_dir', (errMsg) => {
|
||||||
$message.error(errMsg)
|
if (errMsg) $message.error(errMsg)
|
||||||
childDirLoading.value = false
|
childDirLoading.value = false
|
||||||
})
|
})
|
||||||
socket.value.on('rm_success', (res) => {
|
socket.value.on('rm_success', (res) => {
|
||||||
@ -517,16 +522,20 @@ const uploadSliceFile = (fileInfo) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const openDir = () => {
|
const openDir = (path = '', tips = true) => {
|
||||||
childDirLoading.value = true
|
childDirLoading.value = true
|
||||||
curTarget.value = null
|
curTarget.value = null
|
||||||
socket.value.emit('open_dir', curPath.value)
|
socket.value.emit('open_dir', path || curPath.value, tips)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPath = (name = '') => {
|
const getPath = (name = '') => {
|
||||||
return curPath.value.length === 1 ? `/${ name }` : `${ curPath.value }/${ name }`
|
return curPath.value.length === 1 ? `/${ name }` : `${ curPath.value }/${ name }`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
openDir
|
||||||
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -22,7 +22,7 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['inputCommand',])
|
const emit = defineEmits(['inputCommand', 'cdCommand',])
|
||||||
|
|
||||||
const socket = ref(null)
|
const socket = ref(null)
|
||||||
const term = ref(null)
|
const term = ref(null)
|
||||||
@ -194,14 +194,76 @@ const onSelectionChange = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const terminalText = ref(null)
|
||||||
|
const enterTimer = ref(null)
|
||||||
|
|
||||||
|
function filterAnsiSequences(str) {
|
||||||
|
// 使用正则表达式移除ANSI转义序列
|
||||||
|
// return str.replace(/\x1b\[[0-9;]*m|\x1b\[?[\d;]*[A-HJKSTfmin]/g, '')
|
||||||
|
// eslint-disable-next-line
|
||||||
|
return str.replace(/\x1b\[[0-9;]*[mGK]|(\x1b\][0-?]*[0-7;]*\x07)|(\x1b[\[\]()#%;][0-9;?]*[0-9A-PRZcf-ntqry=><])/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 Backspace,删除前一个字符
|
||||||
|
function applyBackspace(text) {
|
||||||
|
let result = []
|
||||||
|
for (let i = 0; i < text.length; i++) {
|
||||||
|
if (text[i] === '\b') {
|
||||||
|
if (result.length > 0) {
|
||||||
|
result.pop()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.push(text[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractLastCdPath(text) {
|
||||||
|
const regex = /cd\s+([^\s]+)(?=\s|$)/g
|
||||||
|
let lastMatch
|
||||||
|
let match
|
||||||
|
regex.lastIndex = 0
|
||||||
|
while ((match = regex.exec(text)) !== null) {
|
||||||
|
lastMatch = match
|
||||||
|
}
|
||||||
|
return lastMatch ? lastMatch[1] : null
|
||||||
|
}
|
||||||
|
|
||||||
const onData = () => {
|
const onData = () => {
|
||||||
socket.value.on('output', (str) => {
|
socket.value.on('output', (str) => {
|
||||||
term.value.write(str)
|
term.value.write(str)
|
||||||
|
terminalText.value += str
|
||||||
|
// console.log(terminalText.value)
|
||||||
})
|
})
|
||||||
term.value.onData((key) => {
|
term.value.onData((key) => {
|
||||||
let acsiiCode = key.codePointAt()
|
let acsiiCode = key.codePointAt()
|
||||||
if (acsiiCode === 22) return handlePaste()
|
if (acsiiCode === 22) return handlePaste()
|
||||||
if (acsiiCode === 6) return searchBar.value.show()
|
if (acsiiCode === 6) return searchBar.value.show()
|
||||||
|
enterTimer.value = setTimeout(() => {
|
||||||
|
if (enterTimer.value) clearTimeout(enterTimer.value)
|
||||||
|
if (key === '\r') { // Enter
|
||||||
|
let cleanText = applyBackspace(filterAnsiSequences(terminalText.value))
|
||||||
|
const lines = cleanText.split('\n')
|
||||||
|
// console.log('lines: ', lines)
|
||||||
|
const lastLine = lines[lines.length - 1].trim()
|
||||||
|
// console.log('lastLine: ', lastLine)
|
||||||
|
// 截取最后一个提示符后的内容('$'或'#'后的内容)
|
||||||
|
const commandStartIndex = lastLine.lastIndexOf('#') + 1
|
||||||
|
const commandText = lastLine.substring(commandStartIndex).trim()
|
||||||
|
// console.log('Processed command: ', commandText)
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const cdPath = extractLastCdPath(commandText)
|
||||||
|
|
||||||
|
if (cdPath) {
|
||||||
|
console.log('cd command path:', cdPath)
|
||||||
|
let firstChar = cdPath.charAt(0)
|
||||||
|
if (!['/',].includes(firstChar)) return console.log('err fullpath:', cdPath) // 后端依赖不支持 '~'
|
||||||
|
emit('cdCommand', cdPath)
|
||||||
|
}
|
||||||
|
terminalText.value = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
emit('inputCommand', key)
|
emit('inputCommand', key)
|
||||||
socket.value.emit('input', key)
|
socket.value.emit('input', key)
|
||||||
})
|
})
|
||||||
|
@ -125,8 +125,14 @@
|
|||||||
ref="terminalRefs"
|
ref="terminalRefs"
|
||||||
:host="item.host"
|
:host="item.host"
|
||||||
@input-command="terminalInput"
|
@input-command="terminalInput"
|
||||||
|
@cd-command="cdCommand"
|
||||||
|
/>
|
||||||
|
<Sftp
|
||||||
|
v-if="showSftp"
|
||||||
|
ref="sftpRefs"
|
||||||
|
:host="item.host"
|
||||||
|
@resize="resizeTerminal"
|
||||||
/>
|
/>
|
||||||
<Sftp v-if="showSftp" :host="item.host" @resize="resizeTerminal" />
|
|
||||||
</div>
|
</div>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
@ -165,9 +171,10 @@ const emit = defineEmits(['closed', 'removeTab', 'add-host',])
|
|||||||
const showInputCommand = ref(false)
|
const showInputCommand = ref(false)
|
||||||
const infoSideRef = ref(null)
|
const infoSideRef = ref(null)
|
||||||
const terminalRefs = ref([])
|
const terminalRefs = ref([])
|
||||||
|
const sftpRefs = ref([])
|
||||||
let activeTabIndex = ref(0)
|
let activeTabIndex = ref(0)
|
||||||
let visible = ref(true)
|
let visible = ref(true)
|
||||||
let showSftp = ref(false)
|
let showSftp = ref(localStorage.getItem('showSftp') === 'true')
|
||||||
let mainHeight = ref('')
|
let mainHeight = ref('')
|
||||||
let isSyncAllSession = ref(false)
|
let isSyncAllSession = ref(false)
|
||||||
let hostFormVisible = ref(false)
|
let hostFormVisible = ref(false)
|
||||||
@ -233,6 +240,18 @@ const terminalInput = (command) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cdCommand = (path) => {
|
||||||
|
// console.log('cdCommand:', path)
|
||||||
|
if (!showSftp.value) return
|
||||||
|
if (isSyncAllSession.value) {
|
||||||
|
sftpRefs.value.forEach(sftpRef => {
|
||||||
|
sftpRef.openDir(path)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
sftpRefs.value[activeTabIndex.value].openDir(path, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const tabChange = async (index) => {
|
const tabChange = async (index) => {
|
||||||
await $nextTick()
|
await $nextTick()
|
||||||
const curTerminalRef = terminalRefs.value[index]
|
const curTerminalRef = terminalRefs.value[index]
|
||||||
@ -253,6 +272,7 @@ watch(terminalTabsLen, () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
watch(showSftp, () => {
|
watch(showSftp, () => {
|
||||||
|
localStorage.setItem('showSftp', showSftp.value)
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
resizeTerminal()
|
resizeTerminal()
|
||||||
})
|
})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user