feat: unify moba workspace and persist session tree layout

This commit is contained in:
liumangmang
2026-04-10 11:04:21 +08:00
parent bba36a2e12
commit f606d20000
27 changed files with 1383 additions and 426 deletions

View File

@@ -1,9 +1,94 @@
<script setup lang="ts">
import { Settings, HelpCircle, Bell, MonitorCog } from 'lucide-vue-next'
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 justify-between">
<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">
@@ -13,6 +98,25 @@ import { Settings, HelpCircle, Bell, MonitorCog } from 'lucide-vue-next'
</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>
@@ -25,6 +129,32 @@ import { Settings, HelpCircle, Bell, MonitorCog } from 'lucide-vue-next'
</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"
@@ -46,4 +176,12 @@ import { Settings, HelpCircle, Bell, MonitorCog } from 'lucide-vue-next'
</button>
</div>
</div>
<ContextMenu
:show="showTabContextMenu"
:x="contextMenuX"
:y="contextMenuY"
:items="tabContextMenuItems"
@close="showTabContextMenu = false"
/>
</template>