Merge: 将终端标签页移至左侧边栏
This commit is contained in:
@@ -1,21 +1,37 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { RouterLink, useRoute } from 'vue-router'
|
import { RouterLink, useRoute, useRouter } from 'vue-router'
|
||||||
import { useAuthStore } from '../stores/auth'
|
import { useAuthStore } from '../stores/auth'
|
||||||
import { useConnectionsStore } from '../stores/connections'
|
import { useConnectionsStore } from '../stores/connections'
|
||||||
import { ArrowLeftRight, Server, LogOut, Menu, X } from 'lucide-vue-next'
|
import { useTerminalTabsStore } from '../stores/terminalTabs'
|
||||||
|
import { ArrowLeftRight, Server, LogOut, Menu, X, Terminal } from 'lucide-vue-next'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const connectionsStore = useConnectionsStore()
|
const connectionsStore = useConnectionsStore()
|
||||||
|
const tabsStore = useTerminalTabsStore()
|
||||||
const sidebarOpen = ref(false)
|
const sidebarOpen = ref(false)
|
||||||
|
|
||||||
|
const terminalTabs = computed(() => tabsStore.tabs)
|
||||||
|
|
||||||
connectionsStore.fetchConnections().catch(() => {})
|
connectionsStore.fetchConnections().catch(() => {})
|
||||||
|
|
||||||
function closeSidebar() {
|
function closeSidebar() {
|
||||||
sidebarOpen.value = false
|
sidebarOpen.value = false
|
||||||
}
|
}
|
||||||
</script>
|
|
||||||
|
function handleTabClick(tabId: string) {
|
||||||
|
tabsStore.activate(tabId)
|
||||||
|
router.push('/terminal')
|
||||||
|
closeSidebar()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTabClose(tabId: string, event: Event) {
|
||||||
|
event.stopPropagation()
|
||||||
|
tabsStore.close(tabId)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex h-screen bg-slate-900">
|
<div class="flex h-screen bg-slate-900">
|
||||||
@@ -38,27 +54,53 @@ function closeSidebar() {
|
|||||||
<h1 class="text-lg font-semibold text-slate-100">SSH 传输控制台</h1>
|
<h1 class="text-lg font-semibold text-slate-100">SSH 传输控制台</h1>
|
||||||
<p class="text-sm text-slate-400">{{ authStore.displayName || authStore.username }}</p>
|
<p class="text-sm text-slate-400">{{ authStore.displayName || authStore.username }}</p>
|
||||||
</div>
|
</div>
|
||||||
<nav class="flex-1 p-4 space-y-1 pt-16 lg:pt-4">
|
<nav class="flex-1 p-4 space-y-1 pt-16 lg:pt-4 overflow-y-auto">
|
||||||
<RouterLink
|
<RouterLink
|
||||||
to="/transfers"
|
to="/transfers"
|
||||||
@click="closeSidebar"
|
@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="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 === '/transfers' }"
|
:class="{ 'bg-slate-700 text-cyan-400': route.path === '/transfers' }"
|
||||||
aria-label="传输"
|
aria-label="传输"
|
||||||
>
|
>
|
||||||
<ArrowLeftRight class="w-5 h-5 flex-shrink-0" aria-hidden="true" />
|
<ArrowLeftRight class="w-5 h-5 flex-shrink-0" aria-hidden="true" />
|
||||||
<span>传输</span>
|
<span>传输</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<RouterLink
|
<RouterLink
|
||||||
to="/connections"
|
to="/connections"
|
||||||
@click="closeSidebar"
|
@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="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' }"
|
:class="{ 'bg-slate-700 text-cyan-400': route.path === '/connections' }"
|
||||||
aria-label="连接列表"
|
aria-label="连接列表"
|
||||||
>
|
>
|
||||||
<Server class="w-5 h-5 flex-shrink-0" aria-hidden="true" />
|
<Server class="w-5 h-5 flex-shrink-0" aria-hidden="true" />
|
||||||
<span>连接列表</span>
|
<span>连接列表</span>
|
||||||
</RouterLink>
|
</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>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="p-4 border-t border-slate-700">
|
<div class="p-4 border-t border-slate-700">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted } from 'vue'
|
import { computed, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { useTerminalTabsStore } from '../stores/terminalTabs'
|
import { useTerminalTabsStore } from '../stores/terminalTabs'
|
||||||
import { useConnectionsStore } from '../stores/connections'
|
import { useConnectionsStore } from '../stores/connections'
|
||||||
import TerminalWidget from '../components/TerminalWidget.vue'
|
import TerminalWidget from '../components/TerminalWidget.vue'
|
||||||
import { ArrowLeft, X } from 'lucide-vue-next'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const tabsStore = useTerminalTabsStore()
|
const tabsStore = useTerminalTabsStore()
|
||||||
const connectionsStore = useConnectionsStore()
|
const connectionsStore = useConnectionsStore()
|
||||||
|
|
||||||
@@ -18,69 +15,16 @@ onMounted(() => {
|
|||||||
connectionsStore.fetchConnections()
|
connectionsStore.fetchConnections()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function handleTabClick(tabId: string) {
|
|
||||||
tabsStore.activate(tabId)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleTabClose(tabId: string, event: Event) {
|
|
||||||
event.stopPropagation()
|
|
||||||
tabsStore.close(tabId)
|
|
||||||
}
|
|
||||||
|
|
||||||
function goBack() {
|
|
||||||
router.push('/connections')
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-full flex flex-col">
|
<div class="h-full flex flex-col">
|
||||||
<!-- 顶部导航栏 -->
|
|
||||||
<div class="flex items-center gap-4 px-4 py-3 border-b border-slate-700 bg-slate-800/50">
|
|
||||||
<button
|
|
||||||
@click="goBack"
|
|
||||||
class="p-2 rounded-lg text-slate-400 hover:bg-slate-700 hover:text-slate-100 transition-colors duration-200 cursor-pointer"
|
|
||||||
aria-label="返回"
|
|
||||||
>
|
|
||||||
<ArrowLeft class="w-5 h-5" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
<h2 class="text-lg font-semibold text-slate-100">终端</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 标签栏 -->
|
|
||||||
<div
|
|
||||||
v-if="tabs.length > 0"
|
|
||||||
class="flex items-center gap-2 px-4 py-2 border-b border-slate-700 bg-slate-900/50 overflow-x-auto"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="tab in tabs"
|
|
||||||
:key="tab.id"
|
|
||||||
@click="handleTabClick(tab.id)"
|
|
||||||
class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer transition-colors duration-200 whitespace-nowrap"
|
|
||||||
:class="tab.active ? 'bg-slate-700 text-slate-100' : 'bg-slate-800 text-slate-400 hover:bg-slate-700/50 hover:text-slate-200'"
|
|
||||||
>
|
|
||||||
<span class="text-sm font-medium">{{ tab.title }}</span>
|
|
||||||
<button
|
|
||||||
@click="(e) => handleTabClose(tab.id, e)"
|
|
||||||
class="p-1 rounded hover:bg-slate-600 transition-colors duration-200"
|
|
||||||
aria-label="关闭标签"
|
|
||||||
>
|
|
||||||
<X class="w-3 h-3" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 终端内容区 -->
|
<!-- 终端内容区 -->
|
||||||
<div class="flex-1 min-h-0 p-4">
|
<div class="flex-1 min-h-0 p-4">
|
||||||
<div v-if="tabs.length === 0" class="flex items-center justify-center h-full text-slate-400">
|
<div v-if="tabs.length === 0" class="flex items-center justify-center h-full text-slate-400">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<p class="mb-2">暂无打开的终端</p>
|
<p class="text-lg mb-2">暂无打开的终端</p>
|
||||||
<button
|
<p class="text-sm text-slate-500">从左侧连接列表点击"终端"按钮打开</p>
|
||||||
@click="goBack"
|
|
||||||
class="px-4 py-2 rounded-lg bg-cyan-600 hover:bg-cyan-500 text-white transition-colors duration-200 cursor-pointer"
|
|
||||||
>
|
|
||||||
返回连接列表
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="h-full">
|
<div v-else class="h-full">
|
||||||
|
|||||||
Reference in New Issue
Block a user