feat: update monitor, terminal, and SFTP interaction flow

This commit is contained in:
liumangmang
2026-04-01 15:22:51 +08:00
parent 832d55c722
commit 9f133bd337
6 changed files with 644 additions and 564 deletions

View File

@@ -7,9 +7,9 @@ import { useSftpTabsStore } from '../stores/sftpTabs'
import { useTerminalTabsStore } from '../stores/terminalTabs'
import TerminalWorkspaceView from '../views/TerminalWorkspaceView.vue'
import { ArrowLeftRight, Server, LogOut, Menu, X, Terminal, FolderOpen } from 'lucide-vue-next'
const route = useRoute()
const router = useRouter()
const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
const connectionsStore = useConnectionsStore()
const sftpTabsStore = useSftpTabsStore()
@@ -20,19 +20,19 @@ const terminalTabs = computed(() => tabsStore.tabs)
const sftpTabs = computed(() => sftpTabsStore.tabs)
const showTerminalWorkspace = computed(() => route.path === '/terminal')
const keepTerminalWorkspaceMounted = computed(() => showTerminalWorkspace.value || terminalTabs.value.length > 0)
connectionsStore.fetchConnections().catch(() => {})
function closeSidebar() {
sidebarOpen.value = false
}
function handleTabClick(tabId: string) {
tabsStore.activate(tabId)
router.push('/terminal')
closeSidebar()
}
connectionsStore.fetchConnections().catch(() => {})
function closeSidebar() {
sidebarOpen.value = false
}
function handleTabClick(tabId: string) {
tabsStore.activate(tabId)
router.push('/terminal')
closeSidebar()
}
function handleTabClose(tabId: string, event: Event) {
event.stopPropagation()
tabsStore.close(tabId)
@@ -67,36 +67,36 @@ function handleSftpTabClose(tabId: string, connectionId: number, event: Event) {
router.push('/connections')
}
</script>
<template>
<div class="flex h-screen bg-slate-900">
<button
@click="sidebarOpen = !sidebarOpen"
class="lg:hidden fixed top-4 left-4 z-30 p-2 rounded-lg bg-slate-800 border border-slate-700 text-slate-300 hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-cyan-500 cursor-pointer"
aria-label="切换侧边栏"
>
<Menu v-if="!sidebarOpen" class="w-6 h-6" aria-hidden="true" />
<X v-else class="w-6 h-6" aria-hidden="true" />
</button>
<aside
:class="[
'w-64 bg-slate-800 border-r border-slate-700 flex flex-col transition-transform duration-200 z-20',
'fixed lg:static inset-y-0 left-0',
sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'
]"
>
<div class="p-4 border-b border-slate-700">
<template>
<div class="flex h-screen bg-slate-900">
<button
@click="sidebarOpen = !sidebarOpen"
class="lg:hidden fixed top-4 left-4 z-30 p-2 rounded-lg bg-slate-800 border border-slate-700 text-slate-300 hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-cyan-500 cursor-pointer"
aria-label="切换侧边栏"
>
<Menu v-if="!sidebarOpen" class="w-6 h-6" aria-hidden="true" />
<X v-else class="w-6 h-6" aria-hidden="true" />
</button>
<aside
:class="[
'w-64 bg-slate-800 border-r border-slate-700 flex flex-col transition-transform duration-200 z-20',
'fixed lg:static inset-y-0 left-0',
sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'
]"
>
<div class="p-4 border-b border-slate-700">
<h1 class="text-lg font-semibold text-slate-100">SSH 传输控制台</h1>
<p class="text-sm text-slate-400">{{ authStore.displayName || authStore.username }}</p>
</div>
<nav class="flex-1 p-4 space-y-1 pt-16 lg:pt-4 overflow-y-auto">
<nav class="flex-1 p-4 space-y-1 pt-16 lg:pt-4 overflow-y-auto">
<RouterLink
to="/connections"
@click="closeSidebar"
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-300 hover:bg-slate-700 hover:text-slate-100 transition-colors duration-200 cursor-pointer min-h-[44px] focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-inset"
:class="{ 'bg-slate-700 text-cyan-400': route.path === '/connections' }"
aria-label="连接列表"
>
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-300 hover:bg-slate-700 hover:text-slate-100 transition-colors duration-200 cursor-pointer min-h-[44px] focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-inset"
:class="{ 'bg-slate-700 text-cyan-400': route.path === '/connections' }"
aria-label="连接列表"
>
<Server class="w-5 h-5 flex-shrink-0" aria-hidden="true" />
<span>连接列表</span>
</RouterLink>
@@ -110,29 +110,29 @@ function handleSftpTabClose(tabId: string, connectionId: number, event: Event) {
<ArrowLeftRight class="w-5 h-5 flex-shrink-0" aria-hidden="true" />
<span>传输</span>
</RouterLink>
<!-- 终端标签区域 -->
<!-- 终端标签区域 -->
<div v-if="terminalTabs.length > 0" class="pt-4 mt-4 border-t border-slate-700">
<div class="flex items-center gap-2 px-3 py-2 text-xs font-semibold text-slate-500 uppercase tracking-wider">
<Terminal class="w-4 h-4" aria-hidden="true" />
<span>终端</span>
</div>
<div class="space-y-1 mt-2">
<button
v-for="tab in terminalTabs"
:key="tab.id"
@click="handleTabClick(tab.id)"
class="w-full flex items-center justify-between gap-2 px-3 py-2.5 rounded-lg text-slate-300 hover:bg-slate-700 hover:text-slate-100 transition-colors duration-200 cursor-pointer min-h-[44px] focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-inset group"
:class="{ 'bg-slate-700 text-cyan-400': tab.active && route.path === '/terminal' }"
>
<span class="truncate text-sm">{{ tab.title }}</span>
<button
@click="(e) => handleTabClose(tab.id, e)"
class="p-1 rounded opacity-0 group-hover:opacity-100 hover:bg-slate-600 transition-all duration-200 flex-shrink-0"
aria-label="关闭标签"
>
<X class="w-3 h-3" aria-hidden="true" />
</button>
<div class="space-y-1 mt-2">
<button
v-for="tab in terminalTabs"
:key="tab.id"
@click="handleTabClick(tab.id)"
class="w-full flex items-center justify-between gap-2 px-3 py-2.5 rounded-lg text-slate-300 hover:bg-slate-700 hover:text-slate-100 transition-colors duration-200 cursor-pointer min-h-[44px] focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-inset group"
:class="{ 'bg-slate-700 text-cyan-400': tab.active && route.path === '/terminal' }"
>
<span class="truncate text-sm">{{ tab.title }}</span>
<button
@click="(e) => handleTabClose(tab.id, e)"
class="p-1 rounded opacity-0 group-hover:opacity-100 hover:bg-slate-600 transition-all duration-200 flex-shrink-0"
aria-label="关闭标签"
>
<X class="w-3 h-3" aria-hidden="true" />
</button>
</button>
</div>
</div>
@@ -169,34 +169,34 @@ function handleSftpTabClose(tabId: string, connectionId: number, event: Event) {
</div>
</div>
</nav>
<div class="p-4 border-t border-slate-700">
<button
@click="authStore.logout(); $router.push('/login')"
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:bg-slate-700 hover:text-slate-100 transition-colors duration-200 cursor-pointer w-full min-h-[44px] focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-inset"
aria-label="退出登录"
>
<LogOut class="w-5 h-5" aria-hidden="true" />
<span>退出登录</span>
</button>
</div>
</aside>
<div
v-if="sidebarOpen"
class="lg:hidden fixed inset-0 bg-black/50 z-10"
aria-hidden="true"
@click="sidebarOpen = false"
/>
<div class="p-4 border-t border-slate-700">
<button
@click="authStore.logout(); $router.push('/login')"
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-slate-400 hover:bg-slate-700 hover:text-slate-100 transition-colors duration-200 cursor-pointer w-full min-h-[44px] focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-inset"
aria-label="退出登录"
>
<LogOut class="w-5 h-5" aria-hidden="true" />
<span>退出登录</span>
</button>
</div>
</aside>
<div
v-if="sidebarOpen"
class="lg:hidden fixed inset-0 bg-black/50 z-10"
aria-hidden="true"
@click="sidebarOpen = false"
/>
<main class="flex-1 overflow-auto min-w-0">
<div v-if="keepTerminalWorkspaceMounted" v-show="showTerminalWorkspace" class="h-full">
<TerminalWorkspaceView :visible="showTerminalWorkspace" />
</div>
<RouterView v-slot="{ Component, route }">
<template v-if="!showTerminalWorkspace">
<keep-alive :max="10" v-if="route.meta.keepAlive">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
<component :is="Component" :key="route.fullPath" v-else />
</template>
<RouterView v-slot="{ Component, route }">
<template v-if="!showTerminalWorkspace">
<keep-alive :max="10" v-if="route.meta.keepAlive">
<component :is="Component" :key="route.params.id" />
</keep-alive>
<component :is="Component" :key="route.fullPath" v-else />
</template>
</RouterView>
</main>
</div>