✨ 支持多端连接服务器
This commit is contained in:
parent
655e9bc8af
commit
6a13c961c3
@ -4,17 +4,13 @@ async function getHostList({ res }) {
|
|||||||
// console.log('get-host-list')
|
// console.log('get-host-list')
|
||||||
let data = await readHostList()
|
let data = await readHostList()
|
||||||
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 = data.map((item) => {
|
for (const item of data) {
|
||||||
const { username, port, authType, _id: id } = item
|
let { username, port, authType, _id: id, credential } = item
|
||||||
|
if (credential) credential = await AESDecryptSync(credential)
|
||||||
|
// console.log(credential)
|
||||||
const isConfig = Boolean(username && port && (item[authType]))
|
const isConfig = Boolean(username && port && (item[authType]))
|
||||||
return {
|
Object.assign(item, { id, isConfig, password: '', privateKey: '', credential })
|
||||||
...item,
|
}
|
||||||
id,
|
|
||||||
isConfig,
|
|
||||||
password: '',
|
|
||||||
privateKey: ''
|
|
||||||
}
|
|
||||||
})
|
|
||||||
res.success({ data })
|
res.success({ data })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ const { Server } = require('socket.io')
|
|||||||
const SFTPClient = require('ssh2-sftp-client')
|
const SFTPClient = require('ssh2-sftp-client')
|
||||||
const rawPath = require('path')
|
const rawPath = require('path')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const { readSSHRecord, verifyAuthSync, RSADecryptSync, AESDecryptSync } = require('../utils')
|
const { readHostList, readSSHRecord, verifyAuthSync, RSADecryptSync, AESDecryptSync } = require('../utils')
|
||||||
const { sftpCacheDir } = require('../config')
|
const { sftpCacheDir } = require('../config')
|
||||||
const CryptoJS = require('crypto-js')
|
const CryptoJS = require('crypto-js')
|
||||||
|
|
||||||
@ -212,17 +212,27 @@ module.exports = (httpServer) => {
|
|||||||
socket.disconnect()
|
socket.disconnect()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const sshRecord = await readSSHRecord()
|
|
||||||
let loginInfo = sshRecord.find(item => item.host === ip)
|
const hostList = await readHostList()
|
||||||
if(!sshRecord.some(item => item.host === ip)) return socket.emit('create_fail', `未找到【${ ip }】凭证`)
|
const targetHostInfo = hostList.find(item => item.host === ip) || {}
|
||||||
let { type, host, port, username, randomKey } = loginInfo
|
let { authType, host, port, username } = targetHostInfo
|
||||||
// 解密放到try里面,防止报错【公私钥必须配对, 否则需要重新添加服务器密钥】
|
if (!host) return socket.emit('create_fail', `查找【${ ip }】凭证信息失败`)
|
||||||
randomKey = await AESDecryptSync(randomKey) // 先对称解密key
|
let authInfo = { host, port, username }
|
||||||
randomKey = await RSADecryptSync(randomKey) // 再非对称解密key
|
|
||||||
loginInfo[type] = await AESDecryptSync(loginInfo[type], randomKey) // 对称解密ssh密钥
|
// 解密放到try里面,防止报错【commonKey必须配对, 否则需要重新添加服务器密钥】
|
||||||
consola.info('准备连接Sftp:', host)
|
if (authType === 'credential') {
|
||||||
const authInfo = { host, port, username, [type]: loginInfo[type] }
|
let credentialId = await AESDecryptSync(targetHostInfo[authType])
|
||||||
sftpClient.connect(authInfo)
|
const sshRecordList = await readSSHRecord()
|
||||||
|
const sshRecord = sshRecordList.find(item => item._id === credentialId)
|
||||||
|
authInfo.authType = sshRecord.authType
|
||||||
|
authInfo[authInfo.authType] = await AESDecryptSync(sshRecord[authInfo.authType])
|
||||||
|
}
|
||||||
|
consola.info('准备连接Sftp面板:', host)
|
||||||
|
targetHostInfo[targetHostInfo.authType] = await AESDecryptSync(targetHostInfo[targetHostInfo.authType])
|
||||||
|
|
||||||
|
consola.log('连接信息', { username, port, authType })
|
||||||
|
sftpClient
|
||||||
|
.connect(authInfo)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
consola.success('连接Sftp成功:', host)
|
consola.success('连接Sftp成功:', host)
|
||||||
return sftpClient.list('/')
|
return sftpClient.list('/')
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
const { Server } = require('socket.io')
|
const { Server } = require('socket.io')
|
||||||
const { Client: SSHClient } = require('ssh2')
|
const { Client: SSHClient } = require('ssh2')
|
||||||
const { readSSHRecord, verifyAuthSync, RSADecryptSync, AESDecryptSync } = require('../utils')
|
const { readHostList, readSSHRecord, verifyAuthSync, RSADecryptSync, AESDecryptSync } = require('../utils')
|
||||||
|
|
||||||
function createTerminal(socket, sshClient) {
|
function createTerminal(socket, sshClient) {
|
||||||
sshClient.shell({ term: 'xterm-color' }, (err, stream) => {
|
sshClient.shell({ term: 'xterm-color' }, (err, stream) => {
|
||||||
@ -49,19 +49,25 @@ module.exports = (httpServer) => {
|
|||||||
socket.disconnect()
|
socket.disconnect()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const sshRecord = await readSSHRecord()
|
const hostList = await readHostList()
|
||||||
let loginInfo = sshRecord.find(item => item.host === ip)
|
const targetHostInfo = hostList.find(item => item.host === ip) || {}
|
||||||
if (!sshRecord.some(item => item.host === ip)) return socket.emit('create_fail', `未找到【${ ip }】凭证`)
|
let { authType, host, port, username } = targetHostInfo
|
||||||
// :TODO: 不用tempKey加密了,统一使用commonKey加密
|
if (!host) return socket.emit('create_fail', `查找【${ ip }】凭证信息失败`)
|
||||||
let { type, host, port, username, randomKey } = loginInfo
|
let authInfo = { host, port, username }
|
||||||
|
// 统一使用commonKey解密
|
||||||
try {
|
try {
|
||||||
// 解密放到try里面,防止报错【公私钥必须配对, 否则需要重新添加服务器密钥】
|
// 解密放到try里面,防止报错【commonKey必须配对, 否则需要重新添加服务器密钥】
|
||||||
randomKey = await AESDecryptSync(randomKey) // 先对称解密key
|
if (authType === 'credential') {
|
||||||
randomKey = await RSADecryptSync(randomKey) // 再非对称解密key
|
let credentialId = await AESDecryptSync(targetHostInfo[authType])
|
||||||
loginInfo[type] = await AESDecryptSync(loginInfo[type], randomKey) // 对称解密ssh密钥
|
const sshRecordList = await readSSHRecord()
|
||||||
|
const sshRecord = sshRecordList.find(item => item._id === credentialId)
|
||||||
|
authInfo.authType = sshRecord.authType
|
||||||
|
authInfo[authInfo.authType] = await AESDecryptSync(sshRecord[authInfo.authType])
|
||||||
|
}
|
||||||
consola.info('准备连接终端:', host)
|
consola.info('准备连接终端:', host)
|
||||||
const authInfo = { host, port, username, [type]: loginInfo[type] } // .replace(/\n/g, '')
|
targetHostInfo[targetHostInfo.authType] = await AESDecryptSync(targetHostInfo[targetHostInfo.authType])
|
||||||
// console.log(authInfo)
|
|
||||||
|
consola.log('连接信息', { username, port, authType })
|
||||||
sshClient
|
sshClient
|
||||||
.on('ready', () => {
|
.on('ready', () => {
|
||||||
consola.success('已连接到终端:', host)
|
consola.success('已连接到终端:', host)
|
||||||
|
@ -1,7 +1,25 @@
|
|||||||
|
|
||||||
|
import { reactive } from 'vue'
|
||||||
import JSRsaEncrypt from 'jsencrypt'
|
import JSRsaEncrypt from 'jsencrypt'
|
||||||
import CryptoJS from 'crypto-js'
|
import CryptoJS from 'crypto-js'
|
||||||
|
|
||||||
|
export const EventBus = reactive({})
|
||||||
|
|
||||||
|
// 在组件中触发事件
|
||||||
|
EventBus.$emit = (event, data) => {
|
||||||
|
if (EventBus[event]) {
|
||||||
|
EventBus[event].forEach(callback => callback(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在组件中监听事件
|
||||||
|
EventBus.$on = (event, callback) => {
|
||||||
|
if (!EventBus[event]) {
|
||||||
|
EventBus[event] = []
|
||||||
|
}
|
||||||
|
EventBus[event].push(callback)
|
||||||
|
}
|
||||||
|
|
||||||
export const randomStr = (e) =>{
|
export const randomStr = (e) =>{
|
||||||
e = e || 16
|
e = e || 16
|
||||||
let str = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678',
|
let str = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678',
|
||||||
@ -97,4 +115,4 @@ export const downloadFile = ({ buffer, name }) => {
|
|||||||
|
|
||||||
export const getSuffix = (name = '') => {
|
export const getSuffix = (name = '') => {
|
||||||
return String(name).split(/\./).pop()
|
return String(name).split(/\./).pop()
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,12 @@
|
|||||||
<AsideBox />
|
<AsideBox />
|
||||||
<div class="main_container">
|
<div class="main_container">
|
||||||
<TopBar />
|
<TopBar />
|
||||||
<router-view v-slot="{ Component }" v-loading="loading" class="router_box">
|
<router-view
|
||||||
|
v-slot="{ Component }"
|
||||||
|
:key="$route.fullPath"
|
||||||
|
v-loading="loading"
|
||||||
|
class="router_box"
|
||||||
|
>
|
||||||
<keep-alive>
|
<keep-alive>
|
||||||
<component :is="Component" />
|
<component :is="Component" />
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
@ -17,7 +22,7 @@ import { ref, onBeforeMount, getCurrentInstance } from 'vue'
|
|||||||
import AsideBox from '@/components/aside-box.vue'
|
import AsideBox from '@/components/aside-box.vue'
|
||||||
import TopBar from '@/components/top-bar.vue'
|
import TopBar from '@/components/top-bar.vue'
|
||||||
|
|
||||||
const { proxy: { $store } } = getCurrentInstance()
|
const { proxy: { $store, $route } } = getCurrentInstance()
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
|
|
||||||
const getMainData = async () => {
|
const getMainData = async () => {
|
||||||
|
@ -352,9 +352,11 @@ const handleSave = () => {
|
|||||||
.then(async () => {
|
.then(async () => {
|
||||||
let tempKey = randomStr(16)
|
let tempKey = randomStr(16)
|
||||||
let formData = { ...hostForm }
|
let formData = { ...hostForm }
|
||||||
|
console.log('formData:', formData)
|
||||||
// 加密传输
|
// 加密传输
|
||||||
if (formData.password) formData.password = AESEncrypt(formData.password, tempKey)
|
if (formData.password) formData.password = AESEncrypt(formData.password, tempKey)
|
||||||
if (formData.privateKey) formData.privateKey = AESEncrypt(formData.privateKey, tempKey)
|
if (formData.privateKey) formData.privateKey = AESEncrypt(formData.privateKey, tempKey)
|
||||||
|
if (formData.credential) formData.credential = AESEncrypt(formData.credential, tempKey)
|
||||||
formData.tempKey = RSAEncrypt(tempKey)
|
formData.tempKey = RSAEncrypt(tempKey)
|
||||||
if (props.defaultData) {
|
if (props.defaultData) {
|
||||||
let { msg } = await $api.updateHost(Object.assign({}, formData, { oldHost: oldHost.value }))
|
let { msg } = await $api.updateHost(Object.assign({}, formData, { oldHost: oldHost.value }))
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="info-container" :style="{ width: visible ? `250px` : 0 }">
|
<div class="info_container" :style="{ width: visible ? `250px` : 0 }">
|
||||||
<header>
|
<header>
|
||||||
<a href="/">
|
<a href="/">
|
||||||
<img src="@/assets/logo-easynode.png" alt="logo">
|
<img src="@/assets/logo-easynode.png" alt="logo">
|
||||||
@ -208,7 +208,7 @@
|
|||||||
import { ref, onMounted, onBeforeUnmount, computed, getCurrentInstance } from 'vue'
|
import { ref, onMounted, onBeforeUnmount, computed, getCurrentInstance } from 'vue'
|
||||||
import socketIo from 'socket.io-client'
|
import socketIo from 'socket.io-client'
|
||||||
|
|
||||||
const { proxy: { $router, $store, $serviceURI, $message, $notification, $tools } } = getCurrentInstance()
|
const { proxy: { $store, $serviceURI, $message, $notification, $tools } } = getCurrentInstance()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
hostInfo: {
|
hostInfo: {
|
||||||
@ -352,14 +352,11 @@ onBeforeUnmount(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.info-container {
|
.info_container {
|
||||||
// min-width: 250px;
|
flex-shrink: 0;
|
||||||
// max-width: 250px;
|
|
||||||
// flex-shrink: 0;
|
|
||||||
// width: 250px;
|
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
background-color: #fff; //#E0E2EF;
|
background-color: #fff; //#E0E2EF;
|
||||||
transition: all 0.3s;
|
transition: all 0.15s;
|
||||||
|
|
||||||
header {
|
header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="sftp-container">
|
<div ref="sftpTabContainerRef" class="sftp_tab_container">
|
||||||
<div ref="adjustRef" class="adjust" />
|
<div ref="adjustRef" class="adjust" />
|
||||||
<section>
|
<section>
|
||||||
<div class="left box">
|
<div class="left box">
|
||||||
@ -136,7 +136,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 { isDir, isFile, sortDirTree, downloadFile } from '@/utils'
|
import { EventBus, isDir, isFile, sortDirTree, downloadFile } 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'
|
||||||
@ -145,10 +145,6 @@ import unknowIcon from '@/assets/image/system/unknow.png'
|
|||||||
const { io } = socketIo
|
const { io } = socketIo
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
token: {
|
|
||||||
required: true,
|
|
||||||
type: String
|
|
||||||
},
|
|
||||||
host: {
|
host: {
|
||||||
required: true,
|
required: true,
|
||||||
type: String
|
type: String
|
||||||
@ -157,7 +153,7 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['resize',])
|
const emit = defineEmits(['resize',])
|
||||||
|
|
||||||
const { proxy: { $notification, $message, $messageBox, $serviceURI, $nextTick } } = getCurrentInstance()
|
const { proxy: { $store, $notification, $message, $messageBox, $serviceURI, $nextTick } } = getCurrentInstance()
|
||||||
|
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
const originalCode = ref('')
|
const originalCode = ref('')
|
||||||
@ -182,21 +178,56 @@ const showFileProgress = ref(false)
|
|||||||
const upFileProgress = ref(0)
|
const upFileProgress = ref(0)
|
||||||
const curUploadFileName = ref('')
|
const curUploadFileName = ref('')
|
||||||
const adjustRef = ref(null)
|
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 token = computed(() => $store.token)
|
||||||
const curPath = computed(() => paths.value.join('/').replace(/\/{2,}/g, '/'))
|
const curPath = computed(() => paths.value.join('/').replace(/\/{2,}/g, '/'))
|
||||||
const fileList = computed(() => childDir.value.filter(({ name }) => name.includes(filterKey.value)))
|
const fileList = computed(() => childDir.value.filter(({ name }) => name.includes(filterKey.value)))
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
connectSftp()
|
connectSftp()
|
||||||
adjustHeight()
|
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',
|
||||||
@ -206,7 +237,7 @@ const connectSftp = () => {
|
|||||||
socket.value.on('connect', () => {
|
socket.value.on('connect', () => {
|
||||||
console.log('/sftp socket已连接:', socket.value.id)
|
console.log('/sftp socket已连接:', socket.value.id)
|
||||||
listenSftp()
|
listenSftp()
|
||||||
socket.value.emit('create', { host: props.host, token: props.token })
|
socket.value.emit('create', { host: props.host, token: token.value })
|
||||||
socket.value.on('root_ls', (tree) => {
|
socket.value.on('root_ls', (tree) => {
|
||||||
let temp = sortDirTree(tree).filter((item) => isDir(item.type))
|
let temp = sortDirTree(tree).filter((item) => isDir(item.type))
|
||||||
temp.unshift({ name: '/', type: 'd' })
|
temp.unshift({ name: '/', type: 'd' })
|
||||||
@ -496,52 +527,20 @@ const getPath = (name = '') => {
|
|||||||
return curPath.value.length === 1 ? `/${ name }` : `${ curPath.value }/${ name }`
|
return curPath.value.length === 1 ? `/${ name }` : `${ curPath.value }/${ name }`
|
||||||
}
|
}
|
||||||
|
|
||||||
const adjustHeight = () => {
|
|
||||||
let startAdjust = false
|
|
||||||
let timer = null
|
|
||||||
$nextTick(() => {
|
|
||||||
let sftpHeight = localStorage.getItem('sftpHeight')
|
|
||||||
if (sftpHeight) document.querySelector('.sftp-container').style.height = sftpHeight
|
|
||||||
else document.querySelector('.sftp-container').style.height = '33vh'
|
|
||||||
|
|
||||||
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)`
|
|
||||||
document.querySelector('.sftp-container').style.height = sftpHeight
|
|
||||||
emit('resize')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
document.addEventListener('mouseup', (e) => {
|
|
||||||
if (!startAdjust) return
|
|
||||||
startAdjust = false
|
|
||||||
sftpHeight = `calc(100vh - ${ e.pageY }px)`
|
|
||||||
localStorage.setItem('sftpHeight', sftpHeight)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.sftp-container {
|
.sftp_tab_container {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
height: 400px;
|
|
||||||
.adjust {
|
.adjust {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -5px;
|
top: -3px;
|
||||||
left: 50%;
|
width: 100%;
|
||||||
transform: translateX(-25px);
|
|
||||||
width: 50px;
|
|
||||||
height: 5px;
|
height: 5px;
|
||||||
background: rgb(138, 226, 52);
|
background: var(--el-color-primary);
|
||||||
border-radius: 3px;
|
opacity: 0.3;
|
||||||
cursor: ns-resize;
|
cursor: ns-resize;
|
||||||
}
|
}
|
||||||
section {
|
section {
|
||||||
@ -554,7 +553,7 @@ const adjustHeight = () => {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
height: $header_height;
|
height: $header_height;
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
background: #e1e1e2;
|
background-color: var(--el-fill-color-light);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
@ -1,14 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<header>
|
<div ref="terminalRefs" class="terminal_tab_container" />
|
||||||
<!-- 功能 -->
|
|
||||||
<!-- <el-button type="primary" @click="handleClear">
|
|
||||||
清空
|
|
||||||
</el-button>
|
|
||||||
<el-button type="primary" @click="handlePaste">
|
|
||||||
粘贴
|
|
||||||
</el-button> -->
|
|
||||||
</header>
|
|
||||||
<div ref="terminalRefs" class="terminal-container" />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@ -22,7 +13,7 @@ import { WebLinksAddon } from 'xterm-addon-web-links'
|
|||||||
import socketIo from 'socket.io-client'
|
import socketIo from 'socket.io-client'
|
||||||
|
|
||||||
const { io } = socketIo
|
const { io } = socketIo
|
||||||
const { proxy: { $api, $store, $serviceURI, $notification, $router, $messageBox } } = getCurrentInstance()
|
const { proxy: { $api, $store, $serviceURI, $notification, $router, $message, $messageBox } } = getCurrentInstance()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
host: {
|
host: {
|
||||||
@ -110,16 +101,17 @@ const connectIO = () => {
|
|||||||
|
|
||||||
const reConnect = () => {
|
const reConnect = () => {
|
||||||
socket.value.close && socket.value.close()
|
socket.value.close && socket.value.close()
|
||||||
$messageBox.alert(
|
$message.warn('终端连接断开')
|
||||||
'<strong>终端连接断开</strong>',
|
// $messageBox.alert(
|
||||||
'Error',
|
// '<strong>终端连接断开</strong>',
|
||||||
{
|
// 'Error',
|
||||||
dangerouslyUseHTMLString: true,
|
// {
|
||||||
confirmButtonText: '刷新页面'
|
// dangerouslyUseHTMLString: true,
|
||||||
}
|
// confirmButtonText: '刷新页面'
|
||||||
).then(() => {
|
// }
|
||||||
location.reload()
|
// ).then(() => {
|
||||||
})
|
// location.reload()
|
||||||
|
// })
|
||||||
}
|
}
|
||||||
|
|
||||||
const createLocalTerminal = () => {
|
const createLocalTerminal = () => {
|
||||||
@ -252,20 +244,17 @@ defineExpose({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
header {
|
.terminal_tab_container {
|
||||||
position: fixed;
|
min-height: 200px;
|
||||||
z-index: 1;
|
|
||||||
right: 10px;
|
|
||||||
top: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-container {
|
:deep(.xterm) {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.xterm-viewport),
|
:deep(.xterm-viewport),
|
||||||
:deep(.xterm-screen) {
|
:deep(.xterm-screen) {
|
||||||
width: 100% !important;
|
padding: 0 0 0 10px;
|
||||||
height: 100% !important;
|
border-radius: var(--el-border-radius-base);
|
||||||
|
|
||||||
// 滚动条整体部分
|
// 滚动条整体部分
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
|
@ -1,59 +1,52 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="terminal_wrap">
|
||||||
<InfoSide
|
<InfoSide
|
||||||
ref="infoSideRef"
|
ref="infoSideRef"
|
||||||
v-model:show-input-command="showInputCommand"
|
v-model:show-input-command="showInputCommand"
|
||||||
:host-info="curHost"
|
:host-info="curHost"
|
||||||
:visible="visible"
|
:visible="visible"
|
||||||
@connect-sftp="connectSftp"
|
|
||||||
@click-input-command="clickInputCommand"
|
@click-input-command="clickInputCommand"
|
||||||
/>
|
/>
|
||||||
<section>
|
<div class="terminals_sftp_wrap">
|
||||||
<div class="terminals">
|
<!-- <el-button class="full-screen-button" type="success" @click="handleFullScreen">
|
||||||
<el-button class="full-screen-button" type="success" @click="handleFullScreen">
|
{{ isFullScreen ? '退出全屏' : '全屏' }}
|
||||||
{{ isFullScreen ? '退出全屏' : '全屏' }}
|
</el-button> -->
|
||||||
</el-button>
|
<!-- <div class="visible" @click="handleVisibleSidebar">
|
||||||
<div class="visible" @click="handleVisibleSidebar">
|
<svg-icon name="icon-jiantou_zuoyouqiehuan" class="svg-icon" />
|
||||||
<svg-icon name="icon-jiantou_zuoyouqiehuan" class="svg-icon" />
|
</div> -->
|
||||||
</div>
|
<el-tabs
|
||||||
<el-tabs
|
v-model="activeTabIndex"
|
||||||
v-model="activeTabIndex"
|
type="border-card"
|
||||||
type="border-card"
|
tab-position="top"
|
||||||
addable
|
@tab-remove="removeTab"
|
||||||
tab-position="top"
|
@tab-change="tabChange"
|
||||||
@tab-remove="removeTab"
|
>
|
||||||
@tab-change="tabChange"
|
<el-tab-pane
|
||||||
|
v-for="(item, index) in terminalTabs"
|
||||||
|
:key="index"
|
||||||
|
:label="item.name"
|
||||||
|
:name="index"
|
||||||
|
:closable="true"
|
||||||
>
|
>
|
||||||
<el-tab-pane
|
<div class="tab_content_wrap" :style="{ height: mainHeight + 'px' }">
|
||||||
v-for="(item, index) in terminalTabs"
|
<TerminalTab ref="terminalTabRefs" :host="item.host" />
|
||||||
:key="index"
|
<Sftp :host="item.host" @resize="resizeTerminal" />
|
||||||
:label="item.name"
|
</div>
|
||||||
:name="index"
|
</el-tab-pane>
|
||||||
:closable="true"
|
</el-tabs>
|
||||||
>
|
</div>
|
||||||
<TerminalTab
|
|
||||||
ref="terminalTabRefs"
|
|
||||||
:host="item.host"
|
|
||||||
/>
|
|
||||||
</el-tab-pane>
|
|
||||||
</el-tabs>
|
|
||||||
</div>
|
|
||||||
<div v-if="showSftp" class="sftp">
|
|
||||||
<SftpFooter :token="token" :host="host" @resize="resizeTerminal" />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<InputCommand v-model:show="showInputCommand" @input-command="handleInputCommand" />
|
<InputCommand v-model:show="showInputCommand" @input-command="handleInputCommand" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, defineEmits, computed, onBeforeMount,defineProps, getCurrentInstance, watch } from 'vue'
|
import { ref, defineEmits, computed, defineProps, getCurrentInstance, watch, onMounted } from 'vue'
|
||||||
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 SftpFooter from './sftp-footer.vue'
|
import Sftp from './sftp.vue'
|
||||||
import InputCommand from '@/components/input-command/index.vue'
|
import InputCommand from '@/components/input-command/index.vue'
|
||||||
|
|
||||||
const { proxy: { $store, $router, $route, $nextTick } } = getCurrentInstance()
|
const { proxy: { $nextTick } } = getCurrentInstance()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
terminalTabs: {
|
terminalTabs: {
|
||||||
@ -73,20 +66,33 @@ const showInputCommand = ref(false)
|
|||||||
const visible = ref(true)
|
const visible = ref(true)
|
||||||
const infoSideRef = ref(null)
|
const infoSideRef = ref(null)
|
||||||
const terminalTabRefs = ref([])
|
const terminalTabRefs = ref([])
|
||||||
|
let mainHeight = ref('')
|
||||||
|
|
||||||
const token = computed(() => $store.token)
|
|
||||||
const terminalTabs = computed(() => props.terminalTabs)
|
const terminalTabs = computed(() => props.terminalTabs)
|
||||||
|
const terminalTabsLen = computed(() => props.terminalTabs.length)
|
||||||
const curHost = computed(() => terminalTabs.value[activeTabIndex.value])
|
const curHost = computed(() => terminalTabs.value[activeTabIndex.value])
|
||||||
|
|
||||||
// const closable = computed(() => terminalTabs.length > 1)
|
// const closable = computed(() => terminalTabs.length > 1)
|
||||||
|
|
||||||
watch(terminalTabs, () => {
|
onMounted(() => {
|
||||||
console.log('add tab:', terminalTabs.value)
|
$nextTick(() => {
|
||||||
let len = terminalTabs.value.length
|
mainHeight.value = document.querySelector('.terminals_sftp_wrap').offsetHeight - 45 // 45 is tab-header height+10
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const tabChange = async (index) => {
|
||||||
|
await $nextTick()
|
||||||
|
const curTabTerminal = terminalTabRefs.value[index]
|
||||||
|
curTabTerminal?.focusTab()
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(terminalTabsLen, () => {
|
||||||
|
let len = terminalTabsLen.value
|
||||||
|
console.log('add tab:', len)
|
||||||
if (len > 0) {
|
if (len > 0) {
|
||||||
activeTabIndex.value = len - 1
|
activeTabIndex.value = len - 1
|
||||||
// registryDbClick()
|
// registryDbClick()
|
||||||
// tabChange(terminalTabs.value[0].key)
|
tabChange(activeTabIndex.value)
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
@ -99,11 +105,6 @@ watch(terminalTabs, () => {
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
const connectSftp = (flag) => {
|
|
||||||
showSftp.value = flag
|
|
||||||
resizeTerminal()
|
|
||||||
}
|
|
||||||
|
|
||||||
const clickInputCommand = () => {
|
const clickInputCommand = () => {
|
||||||
showInputCommand.value = true
|
showInputCommand.value = true
|
||||||
}
|
}
|
||||||
@ -127,15 +128,9 @@ const removeTab = (index) => {
|
|||||||
activeTabIndex.value = 0
|
activeTabIndex.value = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabChange = async (index) => {
|
|
||||||
await $nextTick()
|
|
||||||
const curTabTerminal = terminalTabRefs.value[index]
|
|
||||||
curTabTerminal?.focusTab()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFullScreen = () => {
|
const handleFullScreen = () => {
|
||||||
if (isFullScreen.value) document.exitFullscreen()
|
if (isFullScreen.value) document.exitFullscreen()
|
||||||
else document.getElementsByClassName('terminals')[0].requestFullscreen()
|
else document.getElementsByClassName('tab_content_wrap')[0].requestFullscreen()
|
||||||
isFullScreen.value = !isFullScreen.value
|
isFullScreen.value = !isFullScreen.value
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,7 +162,7 @@ const resizeTerminal = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleInputCommand = async (command) => {
|
const handleInputCommand = async (command) => {
|
||||||
const curTabTerminal = terminalTabRefs.value.find(({ tabKey }) => activeTabIndex.value === tabKey)
|
const curTabTerminal = terminalTabRefs.value[activeTabIndex.value]
|
||||||
await $nextTick()
|
await $nextTick()
|
||||||
curTabTerminal?.focusTab()
|
curTabTerminal?.focusTab()
|
||||||
curTabTerminal.handleInputCommand(`${ command }\n`)
|
curTabTerminal.handleInputCommand(`${ command }\n`)
|
||||||
@ -176,64 +171,77 @@ const handleInputCommand = async (command) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.container {
|
.terminal_wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100vh;
|
height: 100%;
|
||||||
|
|
||||||
section {
|
:deep(.el-tabs__content) {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
padding: 5px;
|
||||||
|
padding-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminals_sftp_wrap {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: calc(100vw - 250px); // 减去左边栏
|
position: relative;
|
||||||
|
|
||||||
.terminals {
|
.tab_content_wrap {
|
||||||
min-height: 150px;
|
display: flex;
|
||||||
flex: 1;
|
flex-direction: column;
|
||||||
position: relative;
|
justify-content: space-between;
|
||||||
|
|
||||||
.full-screen-button {
|
:deep(.terminal_tab_container) {
|
||||||
position: absolute;
|
flex: 1;
|
||||||
right: 10px;
|
}
|
||||||
top: 4px;
|
|
||||||
z-index: 99999;
|
:deep(.sftp_tab_container) {
|
||||||
|
height: 300px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sftp {
|
.full-screen-button {
|
||||||
border: 1px solid rgb(236, 215, 187);
|
|
||||||
}
|
|
||||||
|
|
||||||
.visible {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 999999;
|
right: 10px;
|
||||||
top: 13px;
|
top: 4px;
|
||||||
left: 5px;
|
z-index: 99999;
|
||||||
cursor: pointer;
|
}
|
||||||
transition: all 0.3s;
|
}
|
||||||
|
|
||||||
&:hover {
|
.visible {
|
||||||
transform: scale(1.1);
|
position: absolute;
|
||||||
}
|
z-index: 999999;
|
||||||
|
top: 13px;
|
||||||
|
left: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.el-tabs {
|
// .el-tabs {
|
||||||
border: none;
|
// border: none;
|
||||||
}
|
// }
|
||||||
|
|
||||||
.el-tabs--border-card>.el-tabs__content {
|
// .el-tabs--border-card>.el-tabs__content {
|
||||||
padding: 0;
|
// padding: 0;
|
||||||
}
|
// }
|
||||||
|
|
||||||
.el-tabs__header {
|
// .el-tabs__header {
|
||||||
position: sticky;
|
// position: sticky;
|
||||||
top: 0;
|
// top: 0;
|
||||||
z-index: 1;
|
// z-index: 1;
|
||||||
user-select: none;
|
// user-select: none;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// .el-tabs__nav-scroll {
|
// .el-tabs__nav-scroll {
|
||||||
// .el-tabs__nav {
|
// .el-tabs__nav {
|
||||||
@ -241,12 +249,12 @@ const handleInputCommand = async (command) => {
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
.el-tabs__new-tab {
|
// .el-tabs__new-tab {
|
||||||
position: absolute;
|
// position: absolute;
|
||||||
left: 18px;
|
// left: 18px;
|
||||||
font-size: 50px;
|
// font-size: 50px;
|
||||||
z-index: 98;
|
// z-index: 98;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// .el-tabs--border-card {
|
// .el-tabs--border-card {
|
||||||
// height: 100%;
|
// height: 100%;
|
||||||
@ -255,14 +263,10 @@ const handleInputCommand = async (command) => {
|
|||||||
// flex-direction: column;
|
// flex-direction: column;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
.el-tabs__content {
|
// .el-icon.is-icon-close {
|
||||||
flex: 1;
|
// font-size: 13px;
|
||||||
}
|
// position: absolute;
|
||||||
|
// right: 0px;
|
||||||
.el-icon.is-icon-close {
|
// top: 2px;
|
||||||
font-size: 13px;
|
// }
|
||||||
position: absolute;
|
|
||||||
right: 0px;
|
|
||||||
top: 2px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
@ -2,14 +2,12 @@
|
|||||||
<div class="terminal_container">
|
<div class="terminal_container">
|
||||||
<div v-if="showLinkTips" class="terminal_link_tips">
|
<div v-if="showLinkTips" class="terminal_link_tips">
|
||||||
<h2 class="quick_link_text">快速连接</h2>
|
<h2 class="quick_link_text">快速连接</h2>
|
||||||
<el-table
|
<el-table :data="hostList" :show-header="false">
|
||||||
:data="hostList"
|
|
||||||
:show-header="false"
|
|
||||||
>
|
|
||||||
<el-table-column prop="name" label="name" />
|
<el-table-column prop="name" label="name" />
|
||||||
<el-table-column>
|
<el-table-column>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span>{{ row.username ? `ssh ${row.username}@` : '' }}{{ row.host }}{{ row.port ? ` -p ${row.port}` : '' }}</span>
|
<span>{{ row.username ? `ssh ${row.username}@` : '' }}{{ row.host }}{{ row.port ? ` -p ${row.port}` : ''
|
||||||
|
}}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column v-show="!isAllConfssh">
|
<el-table-column v-show="!isAllConfssh">
|
||||||
@ -37,7 +35,7 @@
|
|||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<Terminal :terminal-tabs="terminalTabs" @remove-tab="handleRemoveTab" />
|
<Terminal ref="terminalRef" :terminal-tabs="terminalTabs" @remove-tab="handleRemoveTab" />
|
||||||
</div>
|
</div>
|
||||||
<HostForm
|
<HostForm
|
||||||
v-model:show="hostFormVisible"
|
v-model:show="hostFormVisible"
|
||||||
@ -49,15 +47,18 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onActivated, getCurrentInstance, reactive } from 'vue'
|
import { ref, computed, onActivated, getCurrentInstance, reactive, nextTick, watch } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
import Terminal from './components/terminal.vue'
|
import Terminal from './components/terminal.vue'
|
||||||
import HostForm from '../server/components/host-form.vue'
|
import HostForm from '../server/components/host-form.vue'
|
||||||
|
|
||||||
const { proxy: { $store, $message } } = getCurrentInstance()
|
const { proxy: { $store, $route, $message } } = getCurrentInstance()
|
||||||
|
|
||||||
let terminalTabs = reactive([])
|
let terminalTabs = reactive([])
|
||||||
const hostFormVisible = ref(false)
|
const hostFormVisible = ref(false)
|
||||||
const updateHostData = ref(null)
|
const updateHostData = ref(null)
|
||||||
|
const terminalRef = ref(null)
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
let showLinkTips = computed(() => !Boolean(terminalTabs.length))
|
let showLinkTips = computed(() => !Boolean(terminalTabs.length))
|
||||||
|
|
||||||
@ -90,22 +91,29 @@ const handleUpdateList = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onActivated(() => {
|
onActivated(async () => {
|
||||||
console.log()
|
await nextTick()
|
||||||
|
const { host } = route.query
|
||||||
|
if (!host) return
|
||||||
|
let targetHost = hostList.value.find(item => item.host === host)
|
||||||
|
if (!targetHost) return
|
||||||
|
terminalTabs.push(targetHost)
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.terminal_container {
|
.terminal_container {
|
||||||
|
height: calc(100vh - 60px - 20px);
|
||||||
|
overflow: auto;
|
||||||
.terminal_link_tips {
|
.terminal_link_tips {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
// margin: 0 auto;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
|
||||||
.quick_link_text {
|
.quick_link_text {
|
||||||
align-self: self-start;
|
align-self: self-start;
|
||||||
margin: 0 10px;
|
margin: 0 10px;
|
||||||
@ -114,6 +122,7 @@ onActivated(() => {
|
|||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actios_btns {
|
.actios_btns {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user