feat: 重构布局&sftp

This commit is contained in:
chaoszhu
2025-07-07 23:56:42 +08:00
parent 757c48d8b4
commit f7e9654cf0
9 changed files with 362 additions and 150 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
<meta name="viewport"
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0">
<title>EasyNode</title>
<script src="//at.alicdn.com/t/c/font_3309550_vrpmw9jmjf8.js"></script>
<script src="//at.alicdn.com/t/c/font_3309550_ldx6nvwonvh.js"></script>
</head>
<body>
+1 -1
View File
@@ -17,7 +17,7 @@ textarea {
// 滚动条整体部分
&::-webkit-scrollbar {
width: 6px;
width: 0px; // 暂且隐藏滚动条
height: 6px;
}
@@ -9,7 +9,7 @@
>
<div class="group_container">
<div class="header">
<el-button type="primary" @click="addGroup">添加分组</el-button>
<el-button type="primary" size="small" @click="addGroup">添加分组</el-button>
</div>
<el-table v-loading="loading" :data="list">
<el-table-column prop="index" label="序号" />
@@ -39,8 +39,15 @@
<el-table-column label="操作" fixed="right" width="160px">
<template #default="{ row }">
<template v-if="row.id !== 'builtin'">
<el-button type="primary" @click="handleChange(row)">修改</el-button>
<el-button v-show="row.id !== 'default'" type="danger" @click="deleteGroup(row)">删除</el-button>
<el-button type="primary" size="small" @click="handleChange(row)">修改</el-button>
<el-button
v-show="row.id !== 'default'"
type="danger"
size="small"
@click="deleteGroup(row)"
>
删除
</el-button>
</template>
<template v-else>
<span>--</span>
@@ -72,8 +79,8 @@
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="groupFormVisible = false">取消</el-button>
<el-button type="primary" @click="updateForm">确定</el-button>
<el-button size="small" @click="groupFormVisible = false">取消</el-button>
<el-button type="primary" size="small" @click="updateForm">确定</el-button>
</span>
</template>
</el-dialog>
@@ -1,11 +1,11 @@
<template>
<div ref="footerBarContainerRef" class="footer_bar_container">
<div ref="adjustRef" class="adjust" />
<!-- <div ref="adjustRef" class="adjust" /> -->
<div class="footer_bar_content">
<el-tabs v-model="activeTab" type="border-card" class="footer_bar_tabs">
<el-tab-pane label="文件传输" name="sftp">
<!-- <el-tab-pane label="文件传输" name="sftp">
<Sftp :host-id="hostId" />
</el-tab-pane>
</el-tab-pane> -->
<el-tab-pane label="脚本库" name="script">
<ScriptInput :host-id="hostId" @exec-command="execCommand" />
</el-tab-pane>
@@ -18,7 +18,7 @@
</template>
<script setup>
import { ref, onMounted, nextTick } from 'vue'
import { ref, onMounted, nextTick, onBeforeUnmount } from 'vue'
import { EventBus } from '@/utils'
import Sftp from './sftp.vue'
import ScriptInput from './script-input.vue'
@@ -34,35 +34,69 @@ defineProps({
const emit = defineEmits(['resize', 'exec-command',])
const footerBarContainerRef = ref(null)
const adjustRef = ref(null)
const activeTab = ref('sftp')
const activeTab = ref('script')
const adjustHeight = async () => {
let startAdjust = false
let timer = null
await nextTick()
try {
let footerBarHeight = localStorage.getItem('footerBarHeight')
if (footerBarHeight) footerBarContainerRef.value.style.height = footerBarHeight
adjustRef.value.addEventListener('mousedown', () => {
const savedHeight = localStorage.getItem('footerBarHeight')
if (savedHeight) {
footerBarContainerRef.value.style.height = savedHeight
}
let startAdjust = false
let timer = null
// 清理之前的事件监听器
const cleanup = () => {
if (timer) {
clearTimeout(timer)
timer = null
}
}
const handleMouseDown = () => {
startAdjust = true
})
document.addEventListener('mousemove', (e) => {
}
const handleMouseMove = (e) => {
if (!startAdjust) return
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
footerBarHeight = `calc(100vh - ${ e.pageY }px - 20px)`
const footerBarHeight = `calc(100vh - ${ e.pageY }px - 20px)`
footerBarContainerRef.value.style.height = footerBarHeight
emit('resize')
})
})
document.addEventListener('mouseup', () => {
timer = null
}, 0)
}
const handleMouseUp = () => {
if (!startAdjust) return
startAdjust = false
localStorage.setItem('footerBarHeight', footerBarHeight)
const currentHeight = footerBarContainerRef.value.style.height
footerBarContainerRef.value.style.height = `${ currentHeight } !important`
footerBarContainerRef.value.style.maxHeight = `${ currentHeight } !important`
localStorage.setItem('footerBarHeight', currentHeight)
EventBus.$emit('update-footer-bar-height')
})
emit('resize')
cleanup()
}
adjustRef.value.addEventListener('mousedown', handleMouseDown)
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
return () => {
cleanup()
adjustRef.value?.removeEventListener('mousedown', handleMouseDown)
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
}
} catch (error) {
console.warn(error.message)
console.warn('adjustHeight error:', error.message)
return () => {}
}
}
@@ -70,12 +104,23 @@ const execCommand = (command) => {
emit('exec-command', command)
}
onMounted(() => {
adjustHeight()
EventBus.$on('update-footer-bar-height', () => {
adjustHeight()
// 保存清理函数的引用
let cleanupAdjustHeight = null
onMounted(async () => {
cleanupAdjustHeight = await adjustHeight()
EventBus.$on('update-footer-bar-height', async () => {
if (cleanupAdjustHeight) cleanupAdjustHeight()
cleanupAdjustHeight = await adjustHeight()
})
})
onBeforeUnmount(() => {
if (cleanupAdjustHeight) {
cleanupAdjustHeight()
}
})
</script>
<style lang="scss" scoped>
@@ -84,7 +129,7 @@ onMounted(() => {
position: relative;
background: #ffffff;
border: 1px solid var(--el-border-color);
min-height: 100px;
height: 100%;
.adjust {
user-select: none;
position: absolute;
@@ -287,26 +287,9 @@ onBeforeUnmount(() => {
<style lang="scss" scoped>
.info_container {
// border-top: 1px solid var(--el-border-color);
flex-shrink: 0;
// overflow: scroll;
transition: all 0.15s;
height: 100%;
// header {
// display: flex;
// justify-content: space-between;
// align-items: center;
// height: 30px;
// margin: 10px;
// position: relative;
// img {
// cursor: pointer;
// height: 80%;
// }
// }
// 表格中系统标识的title
.item-title {
user-select: none;
white-space: nowrap;
@@ -317,7 +300,7 @@ onBeforeUnmount(() => {
.host-info-ip {
word-break: break-all;
max-width: 100px;
max-width: 92px;
display: inline-block;
vertical-align: top;
margin-right: 10px;
@@ -66,13 +66,14 @@
</template>
<div v-else class="empty-list">
<el-empty
:image-size="60"
:image-size="38"
description="暂无脚本"
>
<template #description>
<p>当前分组暂无脚本</p>
</template>
<el-button
size="small"
type="primary"
@click="handleAddScript"
>
@@ -92,20 +93,24 @@
placeholder="请输入脚本内容..."
/>
<div class="action_btn">
<el-button
v-show="scriptContent"
type="primary"
plain
@click="handleSaveAsScript"
>
保存脚本
</el-button>
<el-button
<el-dropdown
split-button
trigger="click"
type="primary"
size="small"
:disabled="!scriptContent"
@click="handleSendToTerminal"
>
发送到终端
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>发送至当前窗口终端</el-dropdown-item>
<el-dropdown-item>发送至所有窗口终端</el-dropdown-item>
<el-dropdown-item @click="handleSaveAsScript">保存至脚本库</el-dropdown-item>
<el-dropdown-item @click="handleSendClearContent">发送后清空内容: {{ isClearContent ? '是' : '否' }}</el-dropdown-item>
</el-dropdown-menu>
</template>
发送至终端
</el-dropdown>
</div>
</div>
</div>
@@ -149,6 +154,7 @@ const activeGroup = ref('')
const scriptEditVisible = ref(false)
const editScriptData = ref(null)
const scriptGroupVisible = ref(false)
const isClearContent = ref(false)
const isPlusActive = computed(() => $store.isPlusActive)
const scriptGroupList = computed(() => $store.scriptGroupList || [])
@@ -185,9 +191,14 @@ const handleSendToEditor = (script) => {
const handleSendToTerminal = () => {
if (!isPlusActive.value) return $message.warning('此功能仅限PLUS版使用')
if (!scriptContent.value) return
emit('exec-command', scriptContent.value)
scriptContent.value = ''
if (isClearContent.value) {
scriptContent.value = ''
}
}
const handleSendClearContent = () => {
isClearContent.value = !isClearContent.value
}
const handleAddScript = () => {
@@ -271,6 +282,7 @@ if (scriptGroupList.value.length) {
font-size: 13px;
border: 1px solid var(--el-border-color-light);
flex-shrink: 0;
transition: all 0.2s;
&:hover {
background-color: var(--el-fill-color-light);
@@ -287,7 +299,6 @@ if (scriptGroupList.value.length) {
display: flex;
align-items: center;
gap: 4px;
background-color: var(--el-color-info-light-9);
border-color: var(--el-color-info-light-5);
color: var(--el-text-color-regular);
@@ -295,11 +306,6 @@ if (scriptGroupList.value.length) {
font-size: 14px;
}
&:hover {
background-color: var(--el-color-info-light-7);
border-color: var(--el-color-info);
color: var(--el-color-info-dark-2);
}
}
}
}
@@ -317,10 +323,10 @@ if (scriptGroupList.value.length) {
color: var(--el-text-color-secondary);
:deep(.el-empty__description) {
margin-bottom: 10px;
margin-bottom: 0px;
p {
margin: 0;
line-height: 1.5;
line-height: 0.5;
&.sub-text {
font-size: 12px;
@@ -0,0 +1,69 @@
<template>
<div class="sftp_v2_tab_container">
test
</div>
</template>
<script setup>
</script>
<style lang="scss" scoped>
.sftp_v2_tab_container {
height: 100%;
section {
height: 100%;
display: flex;
.box {
$header_height: 30px;
.header {
user-select: none;
height: $header_height;
padding: 0 5px;
background-color: var(--el-fill-color-light);
display: flex;
align-items: center;
font-size: 12px;
.operation {
display: flex;
align-items: center;
// margin-right: 20px;
.img {
margin: 0 5px;
width: 20px;
height: 20px;
img {
width: 100%;
height: 100%;
}
&:hover {
background: #cec4c4;
}
}
}
.filter_input {
width: 200px;
min-width: 200px;
margin: 0 20px 0 10px;
}
.path_input {
width: 450px;
min-width: 450px;
}
.path {
flex: 1;
user-select: all;
cursor: pointer;
&:hover {
color: var(--el-color-primary);
}
}
.up-file-progress-wrap {
min-width: 200px;
max-width: 350px;
}
}
}
}
}
</style>
@@ -695,6 +695,7 @@ defineExpose({
<style lang="scss" scoped>
.terminal_tab_container {
height: 100%;
min-height: 200px;
position: relative;
.terminal_container {
+178 -77
View File
@@ -152,7 +152,7 @@
</div>
</div>
</div>
<el-drawer
<!-- <el-drawer
v-if="isMobileScreen"
v-model="showMobileInfoSideDialog"
:with-header="false"
@@ -173,59 +173,126 @@
:visible="visible"
:ping-data="pingData"
/>
</div>
<div class="terminal_and_sftp_wrap">
<el-tabs
v-model="activeTabIndex"
type="border-card"
tab-position="top"
@tab-remove="removeTab"
@tab-change="tabChange"
</div> -->
<el-tabs
v-model="activeTabIndex"
type="border-card"
tab-position="top"
class="tabs_container"
@tab-remove="removeTab"
@tab-change="tabChange"
>
<el-tab-pane
v-for="(item, index) in terminalTabs"
:key="item.key"
:label="item.name"
:name="index"
:closable="true"
>
<el-tab-pane
v-for="(item, index) in terminalTabs"
:key="item.key"
:label="item.name"
:name="index"
:closable="true"
class="el_tab_pane"
>
<template #label>
<div class="tab_label">
<span
class="tab_status"
:style="{ background: getStatusColor(item.status) }"
/>
<span>{{ item.name }}</span>
</div>
</template>
<div class="tab_content_wrap" :style="{ height: mainHeight + 'px' }">
<!-- @cd-command="cdCommand" -->
<TerminalTab
ref="terminalRefs"
:host-obj="item"
:long-press-ctrl="longPressCtrl"
:long-press-alt="longPressAlt"
@input-command="terminalInput"
@ping-data="getPingData"
@reset-long-press="resetLongPress"
<template #label>
<div class="tab_label">
<span
class="tab_status"
:style="{ background: getStatusColor(item.status) }"
/>
<FloatMenu
<span>{{ item.name }}</span>
</div>
</template>
<div class="tab_content_wrap">
<div class="tab_content_header">
<div class="tab_content_wrap_header_item active">
<el-tooltip
effect="dark"
content="状态"
placement="bottom"
>
<svg-icon name="icon-zhuangtai" class="icon" />
</el-tooltip>
</div>
<div class="tab_content_wrap_header_item">
<el-tooltip
effect="dark"
content="Sftp"
placement="bottom"
>
<svg-icon name="icon-sftp" class="icon" />
</el-tooltip>
</div>
<div class="tab_content_wrap_header_item">
<el-tooltip
effect="dark"
content="同步输入到所有分屏"
placement="bottom"
>
<svg-icon name="icon-lianjie" class="icon" />
</el-tooltip>
</div>
<div class="tab_content_wrap_header_item">
<el-tooltip
effect="dark"
content="上下分屏"
placement="bottom"
>
<svg-icon name="icon-a-06gaodufenping" class="icon" />
</el-tooltip>
</div>
<div class="tab_content_wrap_header_item">
<el-tooltip
effect="dark"
content="左右分屏"
placement="bottom"
>
<svg-icon name="icon-a-05kuandufenping" class="icon" />
</el-tooltip>
</div>
</div>
<div class="tab_content_main">
<div class="tab_content_main_info">
<InfoSide
ref="infoSideRef"
:host-info="curHost"
:visible="visible"
:ping-data="pingData"
/>
</div>
<div class="tab_content_main_terminals">
<!-- @cd-command="cdCommand" -->
<TerminalTab
ref="terminalRefs"
:host-obj="item"
:long-press-ctrl="longPressCtrl"
:long-press-alt="longPressAlt"
@input-command="terminalInput"
@ping-data="getPingData"
@reset-long-press="resetLongPress"
/>
</div>
<div class="tab_content_main_sftp">
<SftpV2
:host-id="item.id"
@resize="resizeTerminal"
@exec-command="handleInputCommand"
/>
</div>
<!-- <FloatMenu
v-if="isMobileScreen"
:long-press-ctrl="longPressCtrl"
:long-press-alt="longPressAlt"
@click-key="handleClickVirtualKeyboard"
/>
/> -->
</div>
<div
:class="['tab_content_footer', { 'show_footer_bar': showFooterBar }]"
>
<FooterBar
v-if="showFooterBar"
:host-id="item.id"
@resize="resizeTerminal"
@exec-command="handleInputCommand"
/>
</div>
</el-tab-pane>
</el-tabs>
</div>
</div>
</el-tab-pane>
</el-tabs>
<InputCommand
v-model:show="showInputCommand"
@@ -263,6 +330,7 @@ import InfoSide from './info-side.vue'
import HostForm from '../../server/components/host-form.vue'
import TerminalSetting from './terminal-setting.vue'
import FooterBar from './footer-bar.vue'
import SftpV2 from './sftp-v2.vue'
const {
proxy: { $nextTick, $store, $message }
@@ -369,15 +437,6 @@ const formatScriptList = computed(() => {
}))
})
onMounted(() => {
handleResizeTerminalSftp()
window.addEventListener('resize', handleResizeTerminalSftp)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResizeTerminalSftp)
})
const getStatusColor = (status) => {
return (
terminalStatusList.find((item) => item.value === status)?.color || 'gray'
@@ -395,13 +454,6 @@ const handleUpdateList = async ({ host }) => {
}
}
const handleResizeTerminalSftp = () => {
$nextTick(() => {
mainHeight.value =
document.querySelector('.terminal_and_sftp_wrap')?.offsetHeight - 45 // 45 is tab-header height+15
})
}
const handleCloseAllTab = () => {
emit('close-all-tab')
}
@@ -546,10 +598,12 @@ watch(
watch(
showFooterBar,
async () => {
localStorage.setItem('showFooterBar', showFooterBar.value)
await $nextTick()
resizeTerminal()
() => {
setTimeout(async () => {
localStorage.setItem('showFooterBar', showFooterBar.value)
await $nextTick()
resizeTerminal()
}, 210)
},
{
immediate: true,
@@ -605,13 +659,10 @@ const handleInputCommand = async (command) => {
<style lang="scss" scoped>
.terminal_wrap {
display: flex;
flex-wrap: wrap;
height: 100%;
// height: 100%;
:deep(.el-tabs__content) {
// width: 100%;
padding: 0 0 5px 0;
padding: 0;
}
:deep(.el-tabs--border-card) {
@@ -694,12 +745,7 @@ const handleInputCommand = async (command) => {
border: var(--el-descriptions-table-border);
}
.terminal_and_sftp_wrap {
height: calc(100% - $terminalTopHeight);
overflow: hidden;
flex: 1;
display: flex;
flex-direction: column;
.tabs_container {
position: relative;
.tab_label {
@@ -719,13 +765,68 @@ const handleInputCommand = async (command) => {
}
.tab_content_wrap {
flex: 1;
height: calc(100vh - 142px);
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: space-between;
:deep(.terminal_tab_container) {
flex: 1;
.tab_content_header {
height: 30px;
min-height: 30px;
display: flex;
align-items: center;
// border-bottom: 1px solid var(--el-border-color);
.tab_content_wrap_header_item {
padding: 0 15px;
&.active {
color: var(--el-color-success);
}
.icon {
font-size: 16px;
cursor: pointer;
outline: none;
}
}
}
.tab_content_main {
flex: 1;
min-height: 300px;
display: flex;
.tab_content_main_info {
width: 250px;
min-width: 250px;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
}
.tab_content_main_terminals {
height: 100%;
flex: 1;
}
.tab_content_main_sftp {
height: 100%;
width: 450px;
min-width: 450px;
overflow-y: auto;
overflow-x: hidden;
}
}
.tab_content_footer {
transition: all 0.2s;
height: 0;
min-height: 0;
&.show_footer_bar {
height: 250px;
min-height: 250px;
overflow-x: hidden;
overflow-y: auto;
}
}
}
.full-screen-button {