feat: implement quick SSH connect more terminals
This commit is contained in:
1 parent
de1cf2bbc7
commit
0d83c9a8b1
3 files changed
+289
-148
No files matched your search
@@ -203,9 +203,10 @@ const HISTORY_KEY = 'webssh_quick_connect_history'
|
||||
const MAX_HISTORY = 20
|
||||
|
||||
const router = useRouter()
|
||||
const terminalStore = useTerminalStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const emit = defineEmits(['connect'])
|
||||
|
||||
const connectForm = ref()
|
||||
const fileInput = ref()
|
||||
const connecting = ref(false)
|
||||
@@ -487,16 +488,6 @@ const handleConnect = async () => {
|
||||
connecting.value = true
|
||||
connectionError.value = ''
|
||||
|
||||
if (!terminalStore.socket) {
|
||||
try {
|
||||
await terminalStore.connectSocket(authStore.token)
|
||||
} catch (error) {
|
||||
connecting.value = false
|
||||
connectionError.value = 'Socket连接失败: ' + error.message
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
saveToHistory()
|
||||
|
||||
const connectionInfo = {
|
||||
@@ -512,14 +503,12 @@ const handleConnect = async () => {
|
||||
connectionInfo.privateKey = form.privateKey
|
||||
}
|
||||
|
||||
terminalStore.quickConnect(connectionInfo)
|
||||
|
||||
emit('connect', connectionInfo)
|
||||
connecting.value = false
|
||||
router.push('/quick-connect/terminal')
|
||||
|
||||
} catch (error) {
|
||||
connecting.value = false
|
||||
ElMessage.error('连接失败,请重试')
|
||||
ElMessage.error('连接准备失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,46 +1,110 @@
|
||||
<template>
|
||||
<div class="quick-connect-layout">
|
||||
<!-- 固定顶部栏 -->
|
||||
<div class="layout-header" :class="{ dark: isTerminal }">
|
||||
<el-button @click="$router.push('/dashboard')" size="small" text :style="{ color: isTerminal ? '#aaa' : '#888' }">
|
||||
<div class="layout-header" :class="{ dark: activeTab && activeTab.type === 'terminal' }">
|
||||
<el-button @click="$router.push('/dashboard')" size="small" text :style="{ color: activeTab && activeTab.type === 'terminal' ? '#aaa' : '#888' }">
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
返回
|
||||
</el-button>
|
||||
<span class="header-divider"></span>
|
||||
<span class="page-title" v-if="!isTerminal">快速连接</span>
|
||||
|
||||
<!-- 终端 Tab -->
|
||||
<div v-if="isTerminal" class="terminal-tab">
|
||||
<span class="tab-dot"></span>
|
||||
<span class="tab-label">{{ tabLabel }}</span>
|
||||
<!-- 动态 Tab 列表 -->
|
||||
<div class="tabs-list">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
class="terminal-tab"
|
||||
:class="{ active: activeTabId === tab.id }"
|
||||
@click="activeTabId = tab.id"
|
||||
>
|
||||
<span class="tab-dot" v-if="tab.type === 'terminal'"></span>
|
||||
<span class="tab-label">{{ getTabLabel(tab) }}</span>
|
||||
<el-icon class="tab-close" @click.stop="closeTab(tab.id)" v-if="tabs.length > 1"><Close /></el-icon>
|
||||
</div>
|
||||
|
||||
<el-tooltip content="新建会话" placement="bottom">
|
||||
<el-button
|
||||
class="new-tab-btn"
|
||||
text
|
||||
circle
|
||||
size="small"
|
||||
@click="createNewTab"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 子页面内容 -->
|
||||
<div class="layout-content">
|
||||
<router-view />
|
||||
<div v-for="tab in tabs" :key="tab.id" v-show="activeTabId === tab.id" class="tab-content-wrapper">
|
||||
<QuickConnect
|
||||
v-if="tab.type === 'form'"
|
||||
@connect="handleConnect(tab.id, $event)"
|
||||
/>
|
||||
<QuickConnectTerminal
|
||||
v-else-if="tab.type === 'terminal'"
|
||||
:tab-id="tab.id"
|
||||
:connection-info="tab.connectionInfo"
|
||||
@close="closeTab(tab.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
import { useTerminalStore } from '@/stores/terminal'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ArrowLeft, Plus, Close } from '@element-plus/icons-vue'
|
||||
import QuickConnect from './QuickConnect.vue'
|
||||
import QuickConnectTerminal from './QuickConnectTerminal.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const terminalStore = useTerminalStore()
|
||||
const router = useRouter()
|
||||
|
||||
const isTerminal = computed(() => route.path.includes('/terminal'))
|
||||
const generateId = () => 'tab_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
|
||||
|
||||
const tabLabel = computed(() => {
|
||||
const conn = terminalStore.currentConnection
|
||||
const tabs = ref([
|
||||
{ id: generateId(), type: 'form', connectionInfo: null }
|
||||
])
|
||||
const activeTabId = ref(tabs.value[0].id)
|
||||
|
||||
const activeTab = computed(() => tabs.value.find(t => t.id === activeTabId.value))
|
||||
|
||||
const getTabLabel = (tab) => {
|
||||
if (tab.type === 'form') return '快速连接'
|
||||
const conn = tab.connectionInfo
|
||||
if (!conn) return '终端'
|
||||
const name = conn.name || ''
|
||||
const userHost = `${conn.username || ''}@${conn.host || ''}:${conn.port || 22}`
|
||||
return name ? `${name} (${userHost})` : userHost
|
||||
})
|
||||
}
|
||||
|
||||
const createNewTab = () => {
|
||||
const newTab = { id: generateId(), type: 'form', connectionInfo: null }
|
||||
tabs.value.push(newTab)
|
||||
activeTabId.value = newTab.id
|
||||
}
|
||||
|
||||
const closeTab = (id) => {
|
||||
const index = tabs.value.findIndex(t => t.id === id)
|
||||
if (index !== -1) {
|
||||
tabs.value.splice(index, 1)
|
||||
if (activeTabId.value === id) {
|
||||
const newActive = tabs.value[index - 1] || tabs.value[index]
|
||||
if (newActive) activeTabId.value = newActive.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleConnect = (tabId, connectionInfo) => {
|
||||
const tab = tabs.value.find(t => t.id === tabId)
|
||||
if (tab) {
|
||||
tab.type = 'terminal'
|
||||
tab.connectionInfo = connectionInfo
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -73,16 +137,25 @@ const tabLabel = computed(() => {
|
||||
width: 1px;
|
||||
height: 14px;
|
||||
background: #dcdfe6;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.layout-header.dark .header-divider {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #606266;
|
||||
/* Tabs List */
|
||||
.tabs-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
.tabs-list::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Terminal Tab */
|
||||
@@ -91,11 +164,25 @@ const tabLabel = computed(() => {
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 12px;
|
||||
background: #1e1e1e;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.terminal-tab.active {
|
||||
background: #1e1e1e;
|
||||
border: 1px solid #3e3e42;
|
||||
}
|
||||
|
||||
.layout-header:not(.dark) .terminal-tab.active {
|
||||
background: #fff;
|
||||
border: 1px solid #dcdfe6;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.tab-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
@@ -110,12 +197,61 @@ const tabLabel = computed(() => {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 300px;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.layout-header:not(.dark) .tab-label {
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.tab-close {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
border-radius: 50%;
|
||||
padding: 2px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.tab-close:hover {
|
||||
background-color: rgba(255,255,255,0.2);
|
||||
color: #fff;
|
||||
}
|
||||
.layout-header:not(.dark) .tab-close:hover {
|
||||
background-color: rgba(0,0,0,0.1);
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.new-tab-btn {
|
||||
color: #aaa;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.new-tab-btn:hover {
|
||||
color: #fff;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.layout-header:not(.dark) .new-tab-btn {
|
||||
color: #888;
|
||||
}
|
||||
.layout-header:not(.dark) .new-tab-btn:hover {
|
||||
color: #333;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.layout-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab-content-wrapper {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -5,7 +5,7 @@
|
||||
<el-container class="main-content">
|
||||
<!-- SFTP文件管理器侧边栏 -->
|
||||
<el-aside
|
||||
v-if="showSftp && terminalStore.isConnected"
|
||||
v-if="showSftp && isConnected"
|
||||
class="sftp-sidebar"
|
||||
:width="sftpPanelWidth + 'px'"
|
||||
>
|
||||
@@ -56,7 +56,7 @@
|
||||
<div
|
||||
class="menu-item"
|
||||
@click="pasteFromContextMenu"
|
||||
:class="{ disabled: !terminalStore.isConnected }"
|
||||
:class="{ disabled: !isConnected }"
|
||||
>
|
||||
<el-icon><DocumentAdd /></el-icon>
|
||||
<span>粘贴</span>
|
||||
@@ -74,7 +74,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 连接提示 -->
|
||||
<div v-if="terminalStore.isConnecting" class="connection-status">
|
||||
<div v-if="isConnecting" class="connection-status">
|
||||
<el-alert
|
||||
title="正在连接服务器..."
|
||||
type="info"
|
||||
@@ -85,16 +85,16 @@
|
||||
</div>
|
||||
|
||||
<!-- 连接状态提示 -->
|
||||
<div v-if="terminalStore.connectionError" class="connection-error">
|
||||
<div v-if="connectionError" class="connection-error">
|
||||
<el-alert
|
||||
:title="terminalStore.connectionError"
|
||||
:title="connectionError"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!terminalStore.isConnected && !terminalStore.isConnecting" class="connection-prompt">
|
||||
<div v-else-if="!isConnected && !isConnecting" class="connection-prompt">
|
||||
<div class="prompt-content">
|
||||
<el-icon size="48" color="#909399"><Monitor /></el-icon>
|
||||
<h3>准备连接</h3>
|
||||
@@ -105,7 +105,7 @@
|
||||
</el-container>
|
||||
|
||||
<!-- 状态栏 -->
|
||||
<el-footer class="terminal-footer" v-if="terminalStore.isConnected">
|
||||
<el-footer class="terminal-footer" v-if="isConnected">
|
||||
<div class="status-bar">
|
||||
<span class="status-item">
|
||||
<el-icon><Connection /></el-icon>
|
||||
@@ -128,14 +128,25 @@ import { Terminal } from 'xterm'
|
||||
import { FitAddon } from 'xterm-addon-fit'
|
||||
import { WebLinksAddon } from 'xterm-addon-web-links'
|
||||
import { ArrowLeft, Connection, Close, Delete, Monitor, Folder, Document, Edit, DocumentCopy, DocumentAdd, Select } from '@element-plus/icons-vue'
|
||||
import { useTerminalStore } from '@/stores/terminal'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useSftpStore } from '@/stores/sftp'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import SftpFileManager from '@/components/SftpFileManager.vue'
|
||||
import { io } from 'socket.io-client'
|
||||
|
||||
const props = defineProps({
|
||||
tabId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
connectionInfo: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const router = useRouter()
|
||||
const terminalStore = useTerminalStore()
|
||||
const authStore = useAuthStore()
|
||||
const sftpStore = useSftpStore()
|
||||
|
||||
@@ -157,9 +168,15 @@ let wheelHandler = null
|
||||
let viewportWheelHandler = null
|
||||
let isComposing = false
|
||||
|
||||
// 连接显示信息(从 terminalStore.currentConnection 获取)
|
||||
// 本地连接状态
|
||||
const isConnected = ref(false)
|
||||
const isConnecting = ref(false)
|
||||
const connectionError = ref(null)
|
||||
let socket = null
|
||||
|
||||
// 连接显示信息
|
||||
const connectionDisplay = computed(() => {
|
||||
const conn = terminalStore.currentConnection
|
||||
const conn = props.connectionInfo
|
||||
if (conn) {
|
||||
return `${conn.username || ''}@${conn.host || ''}:${conn.port || 22}`
|
||||
}
|
||||
@@ -167,14 +184,12 @@ const connectionDisplay = computed(() => {
|
||||
})
|
||||
|
||||
const serverTitle = computed(() => {
|
||||
const conn = terminalStore.currentConnection
|
||||
return conn?.name || '快速连接'
|
||||
return props.connectionInfo?.name || '快速连接'
|
||||
})
|
||||
|
||||
// 返回快速连接页面
|
||||
// 返回/关闭Tab
|
||||
const goBack = () => {
|
||||
terminalStore.disconnect()
|
||||
router.push('/quick-connect')
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// 初始化终端
|
||||
@@ -254,8 +269,8 @@ const initTerminal = () => {
|
||||
e.stopImmediatePropagation()
|
||||
isComposing = false
|
||||
const text = e.data
|
||||
if (text && terminalStore.isConnected) {
|
||||
terminalStore.sendInput(text)
|
||||
if (text && isConnected.value && socket) {
|
||||
socket.emit('ssh-input', text)
|
||||
}
|
||||
Promise.resolve().then(() => { textarea.value = '' })
|
||||
}, true)
|
||||
@@ -279,10 +294,10 @@ const initTerminal = () => {
|
||||
if (isComposing) {
|
||||
return
|
||||
}
|
||||
if (terminalStore.isConnected) {
|
||||
terminalStore.sendInput(data)
|
||||
if (isConnected.value && socket) {
|
||||
socket.emit('ssh-input', data)
|
||||
} else {
|
||||
term.write('\r\n\x1b[31m未连接到服务器,请先连接\x1b[0m\r\n')
|
||||
term.write('\r\n\x1b[31m未连接到服务器\x1b[0m\r\n')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -345,8 +360,8 @@ const initTerminal = () => {
|
||||
term.attachCustomKeyEventHandler((event) => {
|
||||
if (event.code === 'Space' || event.key === ' ') {
|
||||
event.preventDefault()
|
||||
if (terminalStore.isConnected) {
|
||||
terminalStore.sendInput(' ')
|
||||
if (isConnected.value && socket) {
|
||||
socket.emit('ssh-input', ' ')
|
||||
term.scrollToBottom()
|
||||
}
|
||||
return false
|
||||
@@ -359,8 +374,8 @@ const initTerminal = () => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'v') {
|
||||
event.preventDefault()
|
||||
navigator.clipboard.readText().then(text => {
|
||||
if (terminalStore.isConnected) {
|
||||
terminalStore.sendInput(text)
|
||||
if (isConnected.value && socket) {
|
||||
socket.emit('ssh-input', text)
|
||||
}
|
||||
}).catch(err => {
|
||||
console.warn('粘贴失败:', err)
|
||||
@@ -403,7 +418,7 @@ const toggleSftpPanel = () => {
|
||||
|
||||
// 连接SFTP
|
||||
const connectSftp = async () => {
|
||||
const conn = terminalStore.currentConnection
|
||||
const conn = props.connectionInfo
|
||||
if (!conn) {
|
||||
console.error('没有可用的连接信息')
|
||||
return
|
||||
@@ -456,10 +471,10 @@ const copyFromContextMenu = async () => {
|
||||
}
|
||||
|
||||
const pasteFromContextMenu = async () => {
|
||||
if (term && terminalStore.isConnected) {
|
||||
if (term && isConnected.value && socket) {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText()
|
||||
terminalStore.sendInput(text)
|
||||
socket.emit('ssh-input', text)
|
||||
hideContextMenu()
|
||||
} catch (error) {
|
||||
console.warn('粘贴失败:', error)
|
||||
@@ -508,14 +523,13 @@ const copySelected = async () => {
|
||||
const pasteFromClipboard = async () => {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText()
|
||||
if (text) {
|
||||
terminalStore.sendInput(text)
|
||||
if (text && isConnected.value && socket) {
|
||||
socket.emit('ssh-input', text)
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const resetConnectedState = () => {
|
||||
terminalStore.clearOutput()
|
||||
if (term) {
|
||||
term.reset()
|
||||
term.focus()
|
||||
@@ -530,63 +544,76 @@ const resetConnectedState = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 监听终端输出变化
|
||||
watch(() => terminalStore.terminalOutput, (newOutput, oldOutput) => {
|
||||
if (term && terminalStore.isConnected && newOutput !== oldOutput) {
|
||||
const newData = newOutput.slice(oldOutput.length)
|
||||
if (newData) {
|
||||
term.write(newData)
|
||||
if (newData.includes('\x1b[H') && newData.includes('\x1b[2J')) {
|
||||
resetConnectedState()
|
||||
} else {
|
||||
if (terminalStore.scrollOnUserInput) {
|
||||
term.scrollToBottom()
|
||||
}
|
||||
}
|
||||
// 连接到服务器的新逻辑
|
||||
const connectToServer = () => {
|
||||
isConnecting.value = true
|
||||
connectionError.value = null
|
||||
|
||||
socket = io({
|
||||
auth: { token: authStore.token }
|
||||
})
|
||||
|
||||
socket.on('connect', () => {
|
||||
socket.emit('authenticate', authStore.token)
|
||||
})
|
||||
|
||||
socket.on('authenticated', (data) => {
|
||||
if (data.success) {
|
||||
socket.emit('quick-connect', props.connectionInfo)
|
||||
} else {
|
||||
handleError('认证失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 监听服务器连接状态变化
|
||||
watch(() => terminalStore.isConnected, (isConnected, wasConnected) => {
|
||||
if (term) {
|
||||
if (isConnected && !wasConnected) {
|
||||
})
|
||||
|
||||
socket.on('connect_error', (err) => {
|
||||
handleError(err.message || '连接失败')
|
||||
})
|
||||
|
||||
socket.on('ssh-connected', () => {
|
||||
isConnected.value = true
|
||||
isConnecting.value = false
|
||||
if (term) {
|
||||
term.clear()
|
||||
term.focus()
|
||||
|
||||
setTimeout(() => {
|
||||
term.write('\x1b[?25h')
|
||||
|
||||
if (typeof term.scrollToTop === 'function') {
|
||||
term.scrollToTop()
|
||||
}
|
||||
|
||||
const viewport = terminalRef.value?.querySelector('.xterm-viewport')
|
||||
if (viewport) {
|
||||
initialScrollTop = 0
|
||||
viewport.scrollTop = 0
|
||||
|
||||
setTimeout(() => {
|
||||
viewport.scrollTop = 0
|
||||
}, 50)
|
||||
}
|
||||
}, 100)
|
||||
} else if (!isConnected && wasConnected) {
|
||||
term.write('\r\n\x1b[31mSSH连接已断开\x1b[0m\r\n')
|
||||
|
||||
sftpStore.disconnectSftp()
|
||||
showSftp.value = false
|
||||
handleResize()
|
||||
}, 200)
|
||||
}
|
||||
setTimeout(() => { handleResize() }, 150)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
socket.on('ssh-data', (data) => {
|
||||
if (term) {
|
||||
term.write(typeof data === 'string' ? data : String(data))
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('ssh-error', (data) => {
|
||||
handleError(data.error)
|
||||
})
|
||||
|
||||
socket.on('ssh-closed', () => {
|
||||
isConnected.value = false
|
||||
isConnecting.value = false
|
||||
sftpStore.disconnectSftp()
|
||||
showSftp.value = false
|
||||
if (term) {
|
||||
term.write('\r\n\x1b[31mSSH连接已断开\x1b[0m\r\n')
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
isConnected.value = false
|
||||
isConnecting.value = false
|
||||
})
|
||||
}
|
||||
|
||||
// 监听连接错误
|
||||
watch(() => terminalStore.connectionError, (error) => {
|
||||
if (term && error) {
|
||||
term.write(`\r\n\x1b[31m连接错误: ${error}\x1b[0m\r\n`)
|
||||
const handleError = (msg) => {
|
||||
isConnecting.value = false
|
||||
connectionError.value = msg
|
||||
if (term) {
|
||||
term.write(`\r\n\x1b[31m连接错误: ${msg}\x1b[0m\r\n`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 监听窗口大小变化
|
||||
const handleResize = () => {
|
||||
@@ -598,12 +625,14 @@ const handleResize = () => {
|
||||
// 故意减少 1 行,在底部留出真实的换行空间,避免贴底
|
||||
const adjustedRows = Math.max(1, dimensions.rows - 1)
|
||||
|
||||
terminalStore.resizeTerminal({
|
||||
rows: adjustedRows,
|
||||
cols: dimensions.cols,
|
||||
height: dimensions.height,
|
||||
width: dimensions.width
|
||||
})
|
||||
if (isConnected.value && socket) {
|
||||
socket.emit('resize', {
|
||||
rows: adjustedRows,
|
||||
cols: dimensions.cols,
|
||||
height: dimensions.height,
|
||||
width: dimensions.width
|
||||
})
|
||||
}
|
||||
|
||||
term.resize(dimensions.cols, adjustedRows)
|
||||
}
|
||||
@@ -617,31 +646,18 @@ onMounted(async () => {
|
||||
window.addEventListener('resize', handleResize)
|
||||
document.addEventListener('click', onDocumentClick)
|
||||
|
||||
// 快速连接模式:连接已由 QuickConnect.vue 发起
|
||||
// 如果连接已建立且有缓冲输出,立即写入终端
|
||||
await nextTick()
|
||||
|
||||
if (terminalStore.isConnected && term) {
|
||||
// 已连接:显示欢迎信息
|
||||
term.clear()
|
||||
|
||||
// 写入已缓冲的输出数据
|
||||
if (terminalStore.terminalOutput) {
|
||||
term.write(terminalStore.terminalOutput)
|
||||
}
|
||||
|
||||
// 发送 resize 触发服务器重新绘制(关键:让远程 shell 发送完整的提示符)
|
||||
setTimeout(() => {
|
||||
handleResize()
|
||||
term.focus()
|
||||
}, 200)
|
||||
} else if (terminalStore.isConnecting && term) {
|
||||
term.write('\x1b[33m正在连接服务器...\x1b[0m\r\n')
|
||||
if (term) {
|
||||
term.write('\x1b[33m正在初始化连接...\x1b[0m\r\n')
|
||||
}
|
||||
|
||||
connectToServer()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
terminalStore.disconnectSocket()
|
||||
if (socket) {
|
||||
socket.disconnect()
|
||||
socket = null
|
||||
}
|
||||
sftpStore.disconnectSftp()
|
||||
if (term) {
|
||||
term.dispose()
|
||||
|
||||
Reference in new issue
Block a user