188 lines
6.5 KiB
Vue
188 lines
6.5 KiB
Vue
<script setup lang="ts">
|
|
import { computed, ref } from 'vue'
|
|
import { Settings, HelpCircle, Bell, MonitorCog, X, ArrowLeftRight, Plus } from 'lucide-vue-next'
|
|
import { useWorkspaceStore } from '../stores/workspace'
|
|
import { useConnectionsStore } from '../stores/connections'
|
|
import ContextMenu from './ContextMenu.vue'
|
|
import type { ContextMenuItem } from './ContextMenu.vue'
|
|
|
|
const workspaceStore = useWorkspaceStore()
|
|
const connectionsStore = useConnectionsStore()
|
|
|
|
const workspaceTabs = computed(() => {
|
|
return workspaceStore.panelOrder
|
|
.map((connectionId) => {
|
|
const panel = workspaceStore.panels[connectionId]
|
|
if (!panel) return null
|
|
const connection = connectionsStore.connections.find((item) => item.id === panel.connectionId)
|
|
return {
|
|
connectionId: panel.connectionId,
|
|
title: connection?.name || `连接 ${panel.connectionId}`,
|
|
active: workspaceStore.activeConnectionId === panel.connectionId,
|
|
}
|
|
})
|
|
.filter((tab): tab is { connectionId: number; title: string; active: boolean } => Boolean(tab))
|
|
})
|
|
|
|
const showTabContextMenu = ref(false)
|
|
const contextMenuX = ref(0)
|
|
const contextMenuY = ref(0)
|
|
const contextTabConnectionId = ref<number | null>(null)
|
|
|
|
function activateTab(connectionId: number) {
|
|
workspaceStore.openPanel(connectionId)
|
|
}
|
|
|
|
function openTransfers() {
|
|
workspaceStore.toggleTransfersModal()
|
|
}
|
|
|
|
function openCreateSession() {
|
|
workspaceStore.openCreateSessionModal()
|
|
}
|
|
|
|
function closeTab(connectionId: number, event: Event) {
|
|
event.stopPropagation()
|
|
workspaceStore.closePanel(connectionId)
|
|
}
|
|
|
|
function openTabContextMenu(connectionId: number, event: MouseEvent) {
|
|
event.preventDefault()
|
|
contextTabConnectionId.value = connectionId
|
|
contextMenuX.value = event.clientX
|
|
contextMenuY.value = event.clientY
|
|
showTabContextMenu.value = true
|
|
}
|
|
|
|
const tabContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|
const connectionId = contextTabConnectionId.value
|
|
if (!connectionId) return []
|
|
|
|
const tabCount = workspaceTabs.value.length
|
|
const targetIndex = workspaceStore.panelOrder.indexOf(connectionId)
|
|
const hasRightTabs = targetIndex >= 0 && targetIndex < workspaceStore.panelOrder.length - 1
|
|
|
|
return [
|
|
{
|
|
label: '关闭当前',
|
|
action: () => workspaceStore.closePanel(connectionId),
|
|
disabled: tabCount === 0,
|
|
},
|
|
{
|
|
label: '关闭其他',
|
|
action: () => workspaceStore.closeOtherPanels(connectionId),
|
|
disabled: tabCount <= 1,
|
|
},
|
|
{
|
|
label: '关闭右侧',
|
|
action: () => workspaceStore.closePanelsToRight(connectionId),
|
|
disabled: !hasRightTabs,
|
|
},
|
|
{
|
|
label: '关闭全部',
|
|
action: () => workspaceStore.closeAllPanels(),
|
|
disabled: tabCount === 0,
|
|
},
|
|
]
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="h-12 bg-slate-900 border-b border-slate-700 px-4 flex items-center gap-4">
|
|
<div class="flex items-center gap-4">
|
|
<div class="flex items-center gap-2">
|
|
<div class="w-7 h-7 rounded bg-gradient-to-br from-cyan-500 to-blue-600 flex items-center justify-center">
|
|
<MonitorCog class="w-4 h-4 text-white" />
|
|
</div>
|
|
<span class="text-sm font-semibold text-slate-100">SSH Manager</span>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-1 text-xs text-slate-400">
|
|
<button
|
|
@click="openTransfers"
|
|
class="inline-flex items-center gap-1 px-3 py-1.5 rounded transition-colors cursor-pointer"
|
|
:class="workspaceStore.transfersModalOpen
|
|
? 'bg-cyan-500/10 text-cyan-200'
|
|
: 'hover:bg-slate-800 hover:text-slate-200'"
|
|
aria-label="打开传输页面"
|
|
>
|
|
<ArrowLeftRight class="w-3.5 h-3.5" />
|
|
<span>Transfers</span>
|
|
</button>
|
|
<button
|
|
@click="openCreateSession"
|
|
class="inline-flex items-center gap-1 px-3 py-1.5 rounded hover:bg-slate-800 hover:text-slate-200 transition-colors cursor-pointer"
|
|
aria-label="新增会话"
|
|
>
|
|
<Plus class="w-3.5 h-3.5" />
|
|
<span>新增会话</span>
|
|
</button>
|
|
<button class="px-3 py-1.5 rounded hover:bg-slate-800 hover:text-slate-200 transition-colors">
|
|
会话
|
|
</button>
|
|
<button class="px-3 py-1.5 rounded hover:bg-slate-800 hover:text-slate-200 transition-colors">
|
|
工具
|
|
</button>
|
|
<button class="px-3 py-1.5 rounded hover:bg-slate-800 hover:text-slate-200 transition-colors">
|
|
设置
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex-1 min-w-0">
|
|
<div v-if="workspaceTabs.length > 0" class="flex items-center gap-1 overflow-x-auto scrollbar-thin">
|
|
<div
|
|
v-for="tab in workspaceTabs"
|
|
:key="tab.connectionId"
|
|
class="group shrink-0 max-w-[280px] min-h-[32px] flex items-center gap-1 rounded border px-1.5 text-xs transition-colors cursor-pointer"
|
|
:class="tab.active
|
|
? 'border-cyan-500/40 bg-cyan-500/10 text-cyan-200'
|
|
: 'border-slate-700 bg-slate-800 text-slate-300 hover:border-slate-600 hover:text-slate-100'"
|
|
@click="activateTab(tab.connectionId)"
|
|
@contextmenu="(e) => openTabContextMenu(tab.connectionId, e)"
|
|
>
|
|
<span class="w-2 h-2 rounded-full" :class="tab.active ? 'bg-cyan-400' : 'bg-slate-500'" />
|
|
<span class="truncate max-w-[220px]">{{ tab.title }}</span>
|
|
<button
|
|
class="p-0.5 rounded text-slate-500 opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 hover:bg-slate-700 hover:text-slate-200"
|
|
@click="(e) => closeTab(tab.connectionId, e)"
|
|
:aria-label="`关闭会话 ${tab.title}`"
|
|
>
|
|
<X class="w-3 h-3" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div v-else class="text-xs text-slate-500">未打开会话</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2">
|
|
<button
|
|
class="p-2 rounded text-slate-400 hover:bg-slate-800 hover:text-slate-200 transition-colors"
|
|
title="通知"
|
|
>
|
|
<Bell class="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
class="p-2 rounded text-slate-400 hover:bg-slate-800 hover:text-slate-200 transition-colors"
|
|
title="帮助"
|
|
>
|
|
<HelpCircle class="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
class="p-2 rounded text-slate-400 hover:bg-slate-800 hover:text-slate-200 transition-colors"
|
|
title="设置"
|
|
>
|
|
<Settings class="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<ContextMenu
|
|
:show="showTabContextMenu"
|
|
:x="contextMenuX"
|
|
:y="contextMenuY"
|
|
:items="tabContextMenuItems"
|
|
@close="showTabContextMenu = false"
|
|
/>
|
|
</template>
|