支持多端连接服务器

This commit is contained in:
chaoszhu 2024-07-22 18:14:05 +08:00
parent 655e9bc8af
commit 6a13c961c3
11 changed files with 278 additions and 243 deletions

View File

@ -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 })
} }

View File

@ -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('/')

View File

@ -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)

View File

@ -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()
} }

View File

@ -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 () => {

View File

@ -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 }))

View File

@ -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;

View File

@ -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;

View File

@ -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 {

View File

@ -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>

View File

@ -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;