支持多端连接服务器

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')
let data = await readHostList()
data?.sort((a, b) => Number(b.index || 0) - Number(a.index || 0))
data = data.map((item) => {
const { username, port, authType, _id: id } = item
for (const item of data) {
let { username, port, authType, _id: id, credential } = item
if (credential) credential = await AESDecryptSync(credential)
// console.log(credential)
const isConfig = Boolean(username && port && (item[authType]))
return {
...item,
id,
isConfig,
password: '',
privateKey: ''
}
})
Object.assign(item, { id, isConfig, password: '', privateKey: '', credential })
}
res.success({ data })
}

View File

@ -2,7 +2,7 @@ const { Server } = require('socket.io')
const SFTPClient = require('ssh2-sftp-client')
const rawPath = require('path')
const fs = require('fs')
const { readSSHRecord, verifyAuthSync, RSADecryptSync, AESDecryptSync } = require('../utils')
const { readHostList, readSSHRecord, verifyAuthSync, RSADecryptSync, AESDecryptSync } = require('../utils')
const { sftpCacheDir } = require('../config')
const CryptoJS = require('crypto-js')
@ -212,17 +212,27 @@ module.exports = (httpServer) => {
socket.disconnect()
return
}
const sshRecord = await readSSHRecord()
let loginInfo = sshRecord.find(item => item.host === ip)
if(!sshRecord.some(item => item.host === ip)) return socket.emit('create_fail', `未找到【${ ip }】凭证`)
let { type, host, port, username, randomKey } = loginInfo
// 解密放到try里面防止报错【公私钥必须配对, 否则需要重新添加服务器密钥】
randomKey = await AESDecryptSync(randomKey) // 先对称解密key
randomKey = await RSADecryptSync(randomKey) // 再非对称解密key
loginInfo[type] = await AESDecryptSync(loginInfo[type], randomKey) // 对称解密ssh密钥
consola.info('准备连接Sftp', host)
const authInfo = { host, port, username, [type]: loginInfo[type] }
sftpClient.connect(authInfo)
const hostList = await readHostList()
const targetHostInfo = hostList.find(item => item.host === ip) || {}
let { authType, host, port, username } = targetHostInfo
if (!host) return socket.emit('create_fail', `查找【${ ip }】凭证信息失败`)
let authInfo = { host, port, username }
// 解密放到try里面防止报错【commonKey必须配对, 否则需要重新添加服务器密钥】
if (authType === 'credential') {
let credentialId = await AESDecryptSync(targetHostInfo[authType])
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(() => {
consola.success('连接Sftp成功', host)
return sftpClient.list('/')

View File

@ -1,6 +1,6 @@
const { Server } = require('socket.io')
const { Client: SSHClient } = require('ssh2')
const { readSSHRecord, verifyAuthSync, RSADecryptSync, AESDecryptSync } = require('../utils')
const { readHostList, readSSHRecord, verifyAuthSync, RSADecryptSync, AESDecryptSync } = require('../utils')
function createTerminal(socket, sshClient) {
sshClient.shell({ term: 'xterm-color' }, (err, stream) => {
@ -49,19 +49,25 @@ module.exports = (httpServer) => {
socket.disconnect()
return
}
const sshRecord = await readSSHRecord()
let loginInfo = sshRecord.find(item => item.host === ip)
if (!sshRecord.some(item => item.host === ip)) return socket.emit('create_fail', `未找到【${ ip }】凭证`)
// :TODO: 不用tempKey加密了统一使用commonKey加密
let { type, host, port, username, randomKey } = loginInfo
const hostList = await readHostList()
const targetHostInfo = hostList.find(item => item.host === ip) || {}
let { authType, host, port, username } = targetHostInfo
if (!host) return socket.emit('create_fail', `查找【${ ip }】凭证信息失败`)
let authInfo = { host, port, username }
// 统一使用commonKey解密
try {
// 解密放到try里面防止报错【公私钥必须配对, 否则需要重新添加服务器密钥】
randomKey = await AESDecryptSync(randomKey) // 先对称解密key
randomKey = await RSADecryptSync(randomKey) // 再非对称解密key
loginInfo[type] = await AESDecryptSync(loginInfo[type], randomKey) // 对称解密ssh密钥
// 解密放到try里面防止报错【commonKey必须配对, 否则需要重新添加服务器密钥】
if (authType === 'credential') {
let credentialId = await AESDecryptSync(targetHostInfo[authType])
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)
const authInfo = { host, port, username, [type]: loginInfo[type] } // .replace(/\n/g, '')
// console.log(authInfo)
targetHostInfo[targetHostInfo.authType] = await AESDecryptSync(targetHostInfo[targetHostInfo.authType])
consola.log('连接信息', { username, port, authType })
sshClient
.on('ready', () => {
consola.success('已连接到终端:', host)

View File

@ -1,7 +1,25 @@
import { reactive } from 'vue'
import JSRsaEncrypt from 'jsencrypt'
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) =>{
e = e || 16
let str = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678',
@ -97,4 +115,4 @@ export const downloadFile = ({ buffer, name }) => {
export const getSuffix = (name = '') => {
return String(name).split(/\./).pop()
}
}

View File

@ -3,7 +3,12 @@
<AsideBox />
<div class="main_container">
<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>
<component :is="Component" />
</keep-alive>
@ -17,7 +22,7 @@ import { ref, onBeforeMount, getCurrentInstance } from 'vue'
import AsideBox from '@/components/aside-box.vue'
import TopBar from '@/components/top-bar.vue'
const { proxy: { $store } } = getCurrentInstance()
const { proxy: { $store, $route } } = getCurrentInstance()
const loading = ref(true)
const getMainData = async () => {

View File

@ -352,9 +352,11 @@ const handleSave = () => {
.then(async () => {
let tempKey = randomStr(16)
let formData = { ...hostForm }
console.log('formData:', formData)
//
if (formData.password) formData.password = AESEncrypt(formData.password, tempKey)
if (formData.privateKey) formData.privateKey = AESEncrypt(formData.privateKey, tempKey)
if (formData.credential) formData.credential = AESEncrypt(formData.credential, tempKey)
formData.tempKey = RSAEncrypt(tempKey)
if (props.defaultData) {
let { msg } = await $api.updateHost(Object.assign({}, formData, { oldHost: oldHost.value }))

View File

@ -1,5 +1,5 @@
<template>
<div class="info-container" :style="{ width: visible ? `250px` : 0 }">
<div class="info_container" :style="{ width: visible ? `250px` : 0 }">
<header>
<a href="/">
<img src="@/assets/logo-easynode.png" alt="logo">
@ -208,7 +208,7 @@
import { ref, onMounted, onBeforeUnmount, computed, getCurrentInstance } from 'vue'
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({
hostInfo: {
@ -352,14 +352,11 @@ onBeforeUnmount(() => {
</script>
<style lang="scss" scoped>
.info-container {
// min-width: 250px;
// max-width: 250px;
// flex-shrink: 0;
// width: 250px;
.info_container {
flex-shrink: 0;
overflow: scroll;
background-color: #fff; //#E0E2EF;
transition: all 0.3s;
transition: all 0.15s;
header {
display: flex;

View File

@ -1,5 +1,5 @@
<template>
<div class="sftp-container">
<div ref="sftpTabContainerRef" class="sftp_tab_container">
<div ref="adjustRef" class="adjust" />
<section>
<div class="left box">
@ -136,7 +136,7 @@
import { ref, computed, onMounted, onBeforeUnmount, getCurrentInstance } from 'vue'
import socketIo from 'socket.io-client'
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 linkIcon from '@/assets/image/system/link.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 props = defineProps({
token: {
required: true,
type: String
},
host: {
required: true,
type: String
@ -157,7 +153,7 @@ const props = defineProps({
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 originalCode = ref('')
@ -182,21 +178,56 @@ const showFileProgress = ref(false)
const upFileProgress = ref(0)
const curUploadFileName = ref('')
const adjustRef = ref(null)
const sftpTabContainerRef = ref(null)
const childDirRef = ref(null)
const uploadFileRef = ref(null)
const token = computed(() => $store.token)
const curPath = computed(() => paths.value.join('/').replace(/\/{2,}/g, '/'))
const fileList = computed(() => childDir.value.filter(({ name }) => name.includes(filterKey.value)))
onMounted(() => {
connectSftp()
adjustHeight()
EventBus.$on('update-sftp-tab-height', () => {
adjustHeight()
})
})
onBeforeUnmount(() => {
if (socket.value) socket.value.close()
})
const adjustHeight = async () => {
let startAdjust = false
let timer = null
await $nextTick()
try {
let sftpHeight = localStorage.getItem('sftpHeight')
if (sftpHeight) sftpTabContainerRef.value.style.height = sftpHeight
adjustRef.value.addEventListener('mousedown', () => {
startAdjust = true
})
document.addEventListener('mousemove', (e) => {
if (!startAdjust) return
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
sftpHeight = `calc(100vh - ${ e.pageY }px - 20px)`
sftpTabContainerRef.value.style.height = sftpHeight
emit('resize')
})
})
document.addEventListener('mouseup', () => {
if (!startAdjust) return
startAdjust = false
localStorage.setItem('sftpHeight', sftpHeight)
EventBus.$emit('update-sftp-tab-height')
})
} catch (error) {
console.warn(error.message)
}
}
const connectSftp = () => {
socket.value = io($serviceURI, {
path: '/sftp',
@ -206,7 +237,7 @@ const connectSftp = () => {
socket.value.on('connect', () => {
console.log('/sftp socket已连接', socket.value.id)
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) => {
let temp = sortDirTree(tree).filter((item) => isDir(item.type))
temp.unshift({ name: '/', type: 'd' })
@ -496,52 +527,20 @@ const getPath = (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>
<style lang="scss" scoped>
.sftp-container {
.sftp_tab_container {
position: relative;
background: #ffffff;
height: 400px;
.adjust {
user-select: none;
position: absolute;
top: -5px;
left: 50%;
transform: translateX(-25px);
width: 50px;
top: -3px;
width: 100%;
height: 5px;
background: rgb(138, 226, 52);
border-radius: 3px;
background: var(--el-color-primary);
opacity: 0.3;
cursor: ns-resize;
}
section {
@ -554,7 +553,7 @@ const adjustHeight = () => {
user-select: none;
height: $header_height;
padding: 0 5px;
background: #e1e1e2;
background-color: var(--el-fill-color-light);
display: flex;
align-items: center;
font-size: 12px;

View File

@ -1,14 +1,5 @@
<template>
<header>
<!-- 功能 -->
<!-- <el-button type="primary" @click="handleClear">
清空
</el-button>
<el-button type="primary" @click="handlePaste">
粘贴
</el-button> -->
</header>
<div ref="terminalRefs" class="terminal-container" />
<div ref="terminalRefs" class="terminal_tab_container" />
</template>
<script setup>
@ -22,7 +13,7 @@ import { WebLinksAddon } from 'xterm-addon-web-links'
import socketIo from 'socket.io-client'
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({
host: {
@ -110,16 +101,17 @@ const connectIO = () => {
const reConnect = () => {
socket.value.close && socket.value.close()
$messageBox.alert(
'<strong>终端连接断开</strong>',
'Error',
{
dangerouslyUseHTMLString: true,
confirmButtonText: '刷新页面'
}
).then(() => {
location.reload()
})
$message.warn('终端连接断开')
// $messageBox.alert(
// '<strong></strong>',
// 'Error',
// {
// dangerouslyUseHTMLString: true,
// confirmButtonText: ''
// }
// ).then(() => {
// location.reload()
// })
}
const createLocalTerminal = () => {
@ -252,20 +244,17 @@ defineExpose({
</script>
<style lang="scss" scoped>
header {
position: fixed;
z-index: 1;
right: 10px;
top: 50px;
}
.terminal_tab_container {
min-height: 200px;
.terminal-container {
height: 100%;
:deep(.xterm) {
height: 100%;
}
:deep(.xterm-viewport),
:deep(.xterm-screen) {
width: 100% !important;
height: 100% !important;
padding: 0 0 0 10px;
border-radius: var(--el-border-radius-base);
//
&::-webkit-scrollbar {

View File

@ -1,59 +1,52 @@
<template>
<div class="container">
<div class="terminal_wrap">
<InfoSide
ref="infoSideRef"
v-model:show-input-command="showInputCommand"
:host-info="curHost"
:visible="visible"
@connect-sftp="connectSftp"
@click-input-command="clickInputCommand"
/>
<section>
<div class="terminals">
<el-button class="full-screen-button" type="success" @click="handleFullScreen">
{{ isFullScreen ? '退出全屏' : '全屏' }}
</el-button>
<div class="visible" @click="handleVisibleSidebar">
<svg-icon name="icon-jiantou_zuoyouqiehuan" class="svg-icon" />
</div>
<el-tabs
v-model="activeTabIndex"
type="border-card"
addable
tab-position="top"
@tab-remove="removeTab"
@tab-change="tabChange"
<div class="terminals_sftp_wrap">
<!-- <el-button class="full-screen-button" type="success" @click="handleFullScreen">
{{ isFullScreen ? '退出全屏' : '全屏' }}
</el-button> -->
<!-- <div class="visible" @click="handleVisibleSidebar">
<svg-icon name="icon-jiantou_zuoyouqiehuan" class="svg-icon" />
</div> -->
<el-tabs
v-model="activeTabIndex"
type="border-card"
tab-position="top"
@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
v-for="(item, index) in terminalTabs"
:key="index"
:label="item.name"
:name="index"
:closable="true"
>
<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>
<div class="tab_content_wrap" :style="{ height: mainHeight + 'px' }">
<TerminalTab ref="terminalTabRefs" :host="item.host" />
<Sftp :host="item.host" @resize="resizeTerminal" />
</div>
</el-tab-pane>
</el-tabs>
</div>
<InputCommand v-model:show="showInputCommand" @input-command="handleInputCommand" />
</div>
</template>
<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 InfoSide from './info-side.vue'
import SftpFooter from './sftp-footer.vue'
import Sftp from './sftp.vue'
import InputCommand from '@/components/input-command/index.vue'
const { proxy: { $store, $router, $route, $nextTick } } = getCurrentInstance()
const { proxy: { $nextTick } } = getCurrentInstance()
const props = defineProps({
terminalTabs: {
@ -73,20 +66,33 @@ const showInputCommand = ref(false)
const visible = ref(true)
const infoSideRef = ref(null)
const terminalTabRefs = ref([])
let mainHeight = ref('')
const token = computed(() => $store.token)
const terminalTabs = computed(() => props.terminalTabs)
const terminalTabsLen = computed(() => props.terminalTabs.length)
const curHost = computed(() => terminalTabs.value[activeTabIndex.value])
// const closable = computed(() => terminalTabs.length > 1)
watch(terminalTabs, () => {
console.log('add tab:', terminalTabs.value)
let len = terminalTabs.value.length
onMounted(() => {
$nextTick(() => {
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) {
activeTabIndex.value = len - 1
// registryDbClick()
// tabChange(terminalTabs.value[0].key)
tabChange(activeTabIndex.value)
}
}, {
immediate: true,
@ -99,11 +105,6 @@ watch(terminalTabs, () => {
// }
// }
const connectSftp = (flag) => {
showSftp.value = flag
resizeTerminal()
}
const clickInputCommand = () => {
showInputCommand.value = true
}
@ -127,15 +128,9 @@ const removeTab = (index) => {
activeTabIndex.value = 0
}
const tabChange = async (index) => {
await $nextTick()
const curTabTerminal = terminalTabRefs.value[index]
curTabTerminal?.focusTab()
}
const handleFullScreen = () => {
if (isFullScreen.value) document.exitFullscreen()
else document.getElementsByClassName('terminals')[0].requestFullscreen()
else document.getElementsByClassName('tab_content_wrap')[0].requestFullscreen()
isFullScreen.value = !isFullScreen.value
}
@ -167,7 +162,7 @@ const resizeTerminal = () => {
}
const handleInputCommand = async (command) => {
const curTabTerminal = terminalTabRefs.value.find(({ tabKey }) => activeTabIndex.value === tabKey)
const curTabTerminal = terminalTabRefs.value[activeTabIndex.value]
await $nextTick()
curTabTerminal?.focusTab()
curTabTerminal.handleInputCommand(`${ command }\n`)
@ -176,64 +171,77 @@ const handleInputCommand = async (command) => {
</script>
<style lang="scss" scoped>
.container {
.terminal_wrap {
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;
display: flex;
flex-direction: column;
width: calc(100vw - 250px); //
position: relative;
.terminals {
min-height: 150px;
flex: 1;
position: relative;
.tab_content_wrap {
display: flex;
flex-direction: column;
justify-content: space-between;
.full-screen-button {
position: absolute;
right: 10px;
top: 4px;
z-index: 99999;
:deep(.terminal_tab_container) {
flex: 1;
}
:deep(.sftp_tab_container) {
height: 300px;
}
}
.sftp {
border: 1px solid rgb(236, 215, 187);
}
.visible {
.full-screen-button {
position: absolute;
z-index: 999999;
top: 13px;
left: 5px;
cursor: pointer;
transition: all 0.3s;
right: 10px;
top: 4px;
z-index: 99999;
}
}
&:hover {
transform: scale(1.1);
}
.visible {
position: absolute;
z-index: 999999;
top: 13px;
left: 5px;
cursor: pointer;
transition: all 0.3s;
&:hover {
transform: scale(1.1);
}
}
}
</style>
<style lang="scss">
.el-tabs {
border: none;
}
// .el-tabs {
// border: none;
// }
.el-tabs--border-card>.el-tabs__content {
padding: 0;
}
// .el-tabs--border-card>.el-tabs__content {
// padding: 0;
// }
.el-tabs__header {
position: sticky;
top: 0;
z-index: 1;
user-select: none;
}
// .el-tabs__header {
// position: sticky;
// top: 0;
// z-index: 1;
// user-select: none;
// }
// .el-tabs__nav-scroll {
// .el-tabs__nav {
@ -241,12 +249,12 @@ const handleInputCommand = async (command) => {
// }
// }
.el-tabs__new-tab {
position: absolute;
left: 18px;
font-size: 50px;
z-index: 98;
}
// .el-tabs__new-tab {
// position: absolute;
// left: 18px;
// font-size: 50px;
// z-index: 98;
// }
// .el-tabs--border-card {
// height: 100%;
@ -255,14 +263,10 @@ const handleInputCommand = async (command) => {
// flex-direction: column;
// }
.el-tabs__content {
flex: 1;
}
.el-icon.is-icon-close {
font-size: 13px;
position: absolute;
right: 0px;
top: 2px;
}
// .el-icon.is-icon-close {
// font-size: 13px;
// position: absolute;
// right: 0px;
// top: 2px;
// }
</style>

View File

@ -2,14 +2,12 @@
<div class="terminal_container">
<div v-if="showLinkTips" class="terminal_link_tips">
<h2 class="quick_link_text">快速连接</h2>
<el-table
:data="hostList"
:show-header="false"
>
<el-table :data="hostList" :show-header="false">
<el-table-column prop="name" label="name" />
<el-table-column>
<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>
</el-table-column>
<el-table-column v-show="!isAllConfssh">
@ -37,7 +35,7 @@
</el-table>
</div>
<div v-else>
<Terminal :terminal-tabs="terminalTabs" @remove-tab="handleRemoveTab" />
<Terminal ref="terminalRef" :terminal-tabs="terminalTabs" @remove-tab="handleRemoveTab" />
</div>
<HostForm
v-model:show="hostFormVisible"
@ -49,15 +47,18 @@
</template>
<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 HostForm from '../server/components/host-form.vue'
const { proxy: { $store, $message } } = getCurrentInstance()
const { proxy: { $store, $route, $message } } = getCurrentInstance()
let terminalTabs = reactive([])
const hostFormVisible = ref(false)
const updateHostData = ref(null)
const terminalRef = ref(null)
const route = useRoute()
let showLinkTips = computed(() => !Boolean(terminalTabs.length))
@ -90,22 +91,29 @@ const handleUpdateList = async () => {
}
}
onActivated(() => {
console.log()
onActivated(async () => {
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>
<style lang="scss" scoped>
.terminal_container {
height: calc(100vh - 60px - 20px);
overflow: auto;
.terminal_link_tips {
width: 50%;
// margin: 0 auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 20px;
.quick_link_text {
align-self: self-start;
margin: 0 10px;
@ -114,6 +122,7 @@ onActivated(() => {
line-height: 22px;
margin-bottom: 15px;
}
.actios_btns {
display: flex;
justify-content: flex-end;