feat: unify moba workspace and persist session tree layout
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user