feat: Implement quick connect layout change
This commit is contained in:
1 parent
b4e8db583d
commit
de1cf2bbc7
4 files changed
+159
-65
No files matched your search
@@ -34,15 +34,20 @@ const routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/quick-connect',
|
path: '/quick-connect',
|
||||||
name: 'QuickConnect',
|
component: () => import('@/views/QuickConnectLayout.vue'),
|
||||||
component: () => import('@/views/QuickConnect.vue'),
|
meta: { requiresAuth: true },
|
||||||
meta: { requiresAuth: true }
|
children: [
|
||||||
},
|
{
|
||||||
{
|
path: '',
|
||||||
path: '/quick-connect-terminal',
|
name: 'QuickConnect',
|
||||||
name: 'QuickConnectTerminal',
|
component: () => import('@/views/QuickConnect.vue')
|
||||||
component: () => import('@/views/QuickConnectTerminal.vue'),
|
},
|
||||||
meta: { requiresAuth: true }
|
{
|
||||||
|
path: 'terminal',
|
||||||
|
name: 'QuickConnectTerminal',
|
||||||
|
component: () => import('@/views/QuickConnectTerminal.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/terminal-new',
|
path: '/terminal-new',
|
||||||
|
|||||||
@@ -1,17 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="quick-connect-page">
|
<div class="quick-connect-page">
|
||||||
<el-container class="quick-connect-container">
|
<el-container class="quick-connect-container">
|
||||||
<!-- 顶部工具栏 -->
|
|
||||||
<el-header class="quick-connect-header">
|
|
||||||
<div class="header-left">
|
|
||||||
<el-button @click="$router.push('/dashboard')" size="small">
|
|
||||||
<el-icon><ArrowLeft /></el-icon>
|
|
||||||
返回
|
|
||||||
</el-button>
|
|
||||||
<span class="page-title">快速连接</span>
|
|
||||||
</div>
|
|
||||||
</el-header>
|
|
||||||
|
|
||||||
<!-- 主内容区域:左侧历史树 + 右侧表单 -->
|
<!-- 主内容区域:左侧历史树 + 右侧表单 -->
|
||||||
<el-container class="main-body">
|
<el-container class="main-body">
|
||||||
<!-- 左侧:连接历史树 -->
|
<!-- 左侧:连接历史树 -->
|
||||||
@@ -526,7 +515,7 @@ const handleConnect = async () => {
|
|||||||
terminalStore.quickConnect(connectionInfo)
|
terminalStore.quickConnect(connectionInfo)
|
||||||
|
|
||||||
connecting.value = false
|
connecting.value = false
|
||||||
router.push('/quick-connect-terminal')
|
router.push('/quick-connect/terminal')
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
connecting.value = false
|
connecting.value = false
|
||||||
@@ -541,7 +530,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.quick-connect-page {
|
.quick-connect-page {
|
||||||
height: 100vh;
|
height: 100%;
|
||||||
background-color: #f0f2f5;
|
background-color: #f0f2f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -551,27 +540,6 @@ onMounted(() => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quick-connect-header {
|
|
||||||
background: white;
|
|
||||||
border-bottom: 1px solid #e4e7ed;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 20px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-title {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 主体布局 */
|
/* 主体布局 */
|
||||||
.main-body {
|
.main-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
<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' }">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 子页面内容 -->
|
||||||
|
<div class="layout-content">
|
||||||
|
<router-view />
|
||||||
|
</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'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const terminalStore = useTerminalStore()
|
||||||
|
|
||||||
|
const isTerminal = computed(() => route.path.includes('/terminal'))
|
||||||
|
|
||||||
|
const tabLabel = computed(() => {
|
||||||
|
const conn = terminalStore.currentConnection
|
||||||
|
if (!conn) return '终端'
|
||||||
|
const name = conn.name || ''
|
||||||
|
const userHost = `${conn.username || ''}@${conn.host || ''}:${conn.port || 22}`
|
||||||
|
return name ? `${name} (${userHost})` : userHost
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.quick-connect-layout {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-header {
|
||||||
|
background: #fafafa;
|
||||||
|
border-bottom: 1px solid #e8e8e8;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 14px;
|
||||||
|
height: 36px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
z-index: 100;
|
||||||
|
gap: 8px;
|
||||||
|
transition: background-color 0.2s, border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-header.dark {
|
||||||
|
background: #252526;
|
||||||
|
border-bottom: 1px solid #3e3e42;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-divider {
|
||||||
|
width: 1px;
|
||||||
|
height: 14px;
|
||||||
|
background: #dcdfe6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-header.dark .header-divider {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Terminal Tab */
|
||||||
|
.terminal-tab {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
background: #1e1e1e;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #3e3e42;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-dot {
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #67c23a;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ccc;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-content {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,18 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="terminal-page">
|
<div class="terminal-page">
|
||||||
<el-container class="terminal-container">
|
<el-container class="terminal-container">
|
||||||
<!-- 顶部工具栏 -->
|
|
||||||
<el-header class="terminal-header" height="36px">
|
|
||||||
<div class="header-left">
|
|
||||||
<el-button @click="goBack" size="small" text style="color: #ccc;">
|
|
||||||
<el-icon><ArrowLeft /></el-icon>
|
|
||||||
返回
|
|
||||||
</el-button>
|
|
||||||
<span class="server-name">{{ serverTitle }}</span>
|
|
||||||
<span class="server-info">{{ connectionDisplay }}</span>
|
|
||||||
</div>
|
|
||||||
</el-header>
|
|
||||||
|
|
||||||
<!-- 主内容区域 -->
|
<!-- 主内容区域 -->
|
||||||
<el-container class="main-content">
|
<el-container class="main-content">
|
||||||
<!-- SFTP文件管理器侧边栏 -->
|
<!-- SFTP文件管理器侧边栏 -->
|
||||||
@@ -531,7 +519,6 @@ const resetConnectedState = () => {
|
|||||||
if (term) {
|
if (term) {
|
||||||
term.reset()
|
term.reset()
|
||||||
term.focus()
|
term.focus()
|
||||||
term.write('\x1b[32mSSH连接已建立,可以开始输入命令\x1b[0m\r\n\r\n')
|
|
||||||
term.write('\x1b[?25h')
|
term.write('\x1b[?25h')
|
||||||
if (typeof term.scrollToTop === 'function') {
|
if (typeof term.scrollToTop === 'function') {
|
||||||
term.scrollToTop()
|
term.scrollToTop()
|
||||||
@@ -568,8 +555,6 @@ watch(() => terminalStore.isConnected, (isConnected, wasConnected) => {
|
|||||||
term.focus()
|
term.focus()
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
term.write('\x1b[32mSSH连接已建立,可以开始输入命令\x1b[0m\r\n\r\n')
|
|
||||||
|
|
||||||
term.write('\x1b[?25h')
|
term.write('\x1b[?25h')
|
||||||
|
|
||||||
if (typeof term.scrollToTop === 'function') {
|
if (typeof term.scrollToTop === 'function') {
|
||||||
@@ -605,17 +590,22 @@ watch(() => terminalStore.connectionError, (error) => {
|
|||||||
|
|
||||||
// 监听窗口大小变化
|
// 监听窗口大小变化
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
if (fitAddon) {
|
if (fitAddon && term) {
|
||||||
fitAddon.fit()
|
fitAddon.fit()
|
||||||
|
|
||||||
const dimensions = fitAddon.proposeDimensions()
|
const dimensions = fitAddon.proposeDimensions()
|
||||||
if (dimensions) {
|
if (dimensions) {
|
||||||
|
// 故意减少 1 行,在底部留出真实的换行空间,避免贴底
|
||||||
|
const adjustedRows = Math.max(1, dimensions.rows - 1)
|
||||||
|
|
||||||
terminalStore.resizeTerminal({
|
terminalStore.resizeTerminal({
|
||||||
rows: dimensions.rows,
|
rows: adjustedRows,
|
||||||
cols: dimensions.cols,
|
cols: dimensions.cols,
|
||||||
height: dimensions.height,
|
height: dimensions.height,
|
||||||
width: dimensions.width
|
width: dimensions.width
|
||||||
})
|
})
|
||||||
|
|
||||||
|
term.resize(dimensions.cols, adjustedRows)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -634,7 +624,6 @@ onMounted(async () => {
|
|||||||
if (terminalStore.isConnected && term) {
|
if (terminalStore.isConnected && term) {
|
||||||
// 已连接:显示欢迎信息
|
// 已连接:显示欢迎信息
|
||||||
term.clear()
|
term.clear()
|
||||||
term.write('\x1b[32mSSH连接已建立,可以开始输入命令\x1b[0m\r\n\r\n')
|
|
||||||
|
|
||||||
// 写入已缓冲的输出数据
|
// 写入已缓冲的输出数据
|
||||||
if (terminalStore.terminalOutput) {
|
if (terminalStore.terminalOutput) {
|
||||||
@@ -676,6 +665,8 @@ onUnmounted(() => {
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: #1e1e1e;
|
background-color: #1e1e1e;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-container {
|
.terminal-container {
|
||||||
@@ -770,18 +761,19 @@ onUnmounted(() => {
|
|||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: width 0.3s ease;
|
transition: width 0.3s ease;
|
||||||
height: 100%;
|
flex: 1;
|
||||||
min-height: 400px;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal {
|
.terminal {
|
||||||
|
flex: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
min-height: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px; /* 轻微底部间隙 */
|
||||||
cursor: text;
|
cursor: text;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -909,6 +901,14 @@ onUnmounted(() => {
|
|||||||
background-color: #1e1e1e !important;
|
background-color: #1e1e1e !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 强制在 xterm-screen 后方追加一段空白,确保滚动到底部时有足够间距 */
|
||||||
|
:deep(.xterm-screen::after) {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
height: 32px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.xterm-helper-textarea) {
|
:deep(.xterm-helper-textarea) {
|
||||||
opacity: 0 !important;
|
opacity: 0 !important;
|
||||||
color: transparent !important;
|
color: transparent !important;
|
||||||
|
|||||||
Reference in new issue
Block a user