|
|
|
@@ -88,6 +88,14 @@ const contextMenuY = ref(0)
|
|
|
|
|
const contextWorkspaceId = ref<string | null>(null)
|
|
|
|
|
const backupFileInput = ref<HTMLInputElement | null>(null)
|
|
|
|
|
const backupBusy = ref(false)
|
|
|
|
|
const workspaceTabCount = computed(() => workspaceTabs.value.length)
|
|
|
|
|
|
|
|
|
|
const commandGroupClass = 'flex min-w-max items-center gap-1 rounded-2xl border border-slate-800/80 bg-slate-900/80 px-1.5 py-1 shadow-[inset_0_1px_0_rgba(148,163,184,0.04)]'
|
|
|
|
|
const commandButtonClass = 'inline-flex h-10 shrink-0 items-center gap-2 rounded-xl px-3 text-[13px] font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-400/60 disabled:cursor-not-allowed disabled:opacity-45'
|
|
|
|
|
const commandButtonNeutralClass = 'text-slate-300 hover:bg-slate-800/85 hover:text-slate-50'
|
|
|
|
|
const commandButtonEmphasisClass = 'border border-slate-700/80 bg-slate-800/90 text-slate-100 shadow-[inset_0_1px_0_rgba(148,163,184,0.05)] hover:border-slate-600/80 hover:bg-slate-800'
|
|
|
|
|
const commandButtonActiveClass = 'border border-cyan-500/30 bg-cyan-500/12 text-cyan-100 shadow-[inset_0_1px_0_rgba(103,232,249,0.12)] hover:border-cyan-400/40 hover:bg-cyan-500/16'
|
|
|
|
|
const utilityButtonClass = 'inline-flex h-10 min-w-[40px] items-center justify-center rounded-xl text-slate-400 transition-colors duration-200 hover:bg-slate-800/90 hover:text-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-400/60'
|
|
|
|
|
|
|
|
|
|
function activateTab(workspaceId: string) {
|
|
|
|
|
workspaceStore.activateWorkspace(workspaceId)
|
|
|
|
@@ -268,13 +276,13 @@ const tabContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<div class="border-b border-slate-700 bg-slate-900">
|
|
|
|
|
<div class="border-b border-slate-800/80 px-3 py-2 sm:px-4">
|
|
|
|
|
<div class="flex flex-wrap items-center gap-2 lg:flex-nowrap lg:gap-3">
|
|
|
|
|
<div class="flex min-w-0 flex-1 items-center gap-2">
|
|
|
|
|
<div class="border-b border-slate-800 bg-[radial-gradient(circle_at_top_left,_rgba(34,211,238,0.08),_transparent_30%),linear-gradient(180deg,_rgba(15,23,42,0.98),_rgba(2,6,23,0.98))]">
|
|
|
|
|
<div class="border-b border-slate-800/80 px-3 py-3 sm:px-4">
|
|
|
|
|
<div class="flex flex-wrap items-start gap-3 lg:flex-nowrap">
|
|
|
|
|
<div class="flex min-w-0 flex-1 items-start gap-2">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="inline-flex min-h-[40px] min-w-[40px] shrink-0 items-center justify-center rounded-lg border border-slate-700 bg-slate-800 text-slate-300 transition-colors hover:border-slate-600 hover:text-slate-100 lg:hidden"
|
|
|
|
|
class="inline-flex h-11 min-w-[44px] shrink-0 items-center justify-center rounded-xl border border-slate-800 bg-slate-900/85 text-slate-300 transition-colors duration-200 hover:border-slate-700 hover:bg-slate-800/90 hover:text-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-400/60 lg:hidden"
|
|
|
|
|
:aria-label="props.sidebarOpen ? '收起会话树' : '打开会话树'"
|
|
|
|
|
@click="emit('toggleSidebar')"
|
|
|
|
|
>
|
|
|
|
@@ -282,12 +290,14 @@ const tabContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|
|
|
|
<PanelLeftOpen v-else class="h-4 w-4" />
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<div class="min-w-0 flex-1 overflow-x-auto pb-1 lg:pb-0 scrollbar-thin">
|
|
|
|
|
<div class="flex min-w-max items-center gap-2 text-xs text-slate-300">
|
|
|
|
|
<div class="min-w-0 flex-1 overflow-x-auto pb-1 scrollbar-thin">
|
|
|
|
|
<div class="flex min-w-max items-start gap-2.5">
|
|
|
|
|
<div :class="commandGroupClass">
|
|
|
|
|
<div class="hidden px-2 text-[11px] font-medium text-slate-500 xl:block">全局</div>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
@click="openBatchCommand"
|
|
|
|
|
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg border border-slate-700 bg-slate-800 px-3 py-2 transition-colors hover:border-slate-600 hover:text-slate-100"
|
|
|
|
|
:class="[commandButtonClass, commandButtonNeutralClass]"
|
|
|
|
|
aria-label="批量命令执行"
|
|
|
|
|
>
|
|
|
|
|
<TerminalSquare class="h-3.5 w-3.5" />
|
|
|
|
@@ -296,7 +306,7 @@ const tabContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
@click="openOperationsHistory"
|
|
|
|
|
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg border border-slate-700 bg-slate-800 px-3 py-2 transition-colors hover:border-slate-600 hover:text-slate-100"
|
|
|
|
|
:class="[commandButtonClass, commandButtonNeutralClass]"
|
|
|
|
|
aria-label="传输历史与操作日志"
|
|
|
|
|
>
|
|
|
|
|
<ClipboardList class="h-3.5 w-3.5" />
|
|
|
|
@@ -306,7 +316,7 @@ const tabContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|
|
|
|
type="button"
|
|
|
|
|
@click="handleExportBackup"
|
|
|
|
|
:disabled="backupBusy"
|
|
|
|
|
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg border border-slate-700 bg-slate-800 px-3 py-2 transition-colors hover:border-slate-600 hover:text-slate-100 disabled:cursor-not-allowed disabled:opacity-50"
|
|
|
|
|
:class="[commandButtonClass, commandButtonNeutralClass]"
|
|
|
|
|
aria-label="导出备份"
|
|
|
|
|
>
|
|
|
|
|
<Download class="h-3.5 w-3.5" />
|
|
|
|
@@ -316,7 +326,7 @@ const tabContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|
|
|
|
type="button"
|
|
|
|
|
@click="triggerImportBackup"
|
|
|
|
|
:disabled="backupBusy"
|
|
|
|
|
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg border border-slate-700 bg-slate-800 px-3 py-2 transition-colors hover:border-slate-600 hover:text-slate-100 disabled:cursor-not-allowed disabled:opacity-50"
|
|
|
|
|
:class="[commandButtonClass, commandButtonNeutralClass]"
|
|
|
|
|
aria-label="导入备份"
|
|
|
|
|
>
|
|
|
|
|
<Upload class="h-3.5 w-3.5" />
|
|
|
|
@@ -325,19 +335,20 @@ const tabContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
@click="openTransfers"
|
|
|
|
|
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg border px-3 py-2 transition-colors"
|
|
|
|
|
:class="workspaceStore.transfersModalOpen
|
|
|
|
|
? 'border-cyan-500/30 bg-cyan-500/10 text-cyan-200'
|
|
|
|
|
: 'border-slate-700 bg-slate-800 hover:border-slate-600 hover:text-slate-100'"
|
|
|
|
|
aria-label="打开传输页面"
|
|
|
|
|
:class="[commandButtonClass, workspaceStore.transfersModalOpen ? commandButtonActiveClass : commandButtonNeutralClass]"
|
|
|
|
|
aria-label="打开文件分发"
|
|
|
|
|
>
|
|
|
|
|
<ArrowLeftRight class="h-3.5 w-3.5" />
|
|
|
|
|
<span>Transfers</span>
|
|
|
|
|
<span>文件分发</span>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div :class="commandGroupClass">
|
|
|
|
|
<div class="hidden px-2 text-[11px] font-medium text-slate-500 xl:block">工作区</div>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
@click="openCreateSession"
|
|
|
|
|
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg border border-slate-700 bg-slate-800 px-3 py-2 transition-colors hover:border-slate-600 hover:text-slate-100"
|
|
|
|
|
:class="[commandButtonClass, commandButtonEmphasisClass]"
|
|
|
|
|
aria-label="新增会话"
|
|
|
|
|
>
|
|
|
|
|
<Plus class="h-3.5 w-3.5" />
|
|
|
|
@@ -347,7 +358,7 @@ const tabContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|
|
|
|
type="button"
|
|
|
|
|
@click="duplicateActiveWorkspace"
|
|
|
|
|
:disabled="!activeWorkspace"
|
|
|
|
|
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg border border-slate-700 bg-slate-800 px-3 py-2 transition-colors hover:border-slate-600 hover:text-slate-100 disabled:cursor-not-allowed disabled:opacity-50"
|
|
|
|
|
:class="[commandButtonClass, commandButtonNeutralClass]"
|
|
|
|
|
aria-label="复制当前工作区"
|
|
|
|
|
>
|
|
|
|
|
<CopyPlus class="h-3.5 w-3.5" />
|
|
|
|
@@ -357,10 +368,7 @@ const tabContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|
|
|
|
type="button"
|
|
|
|
|
@click="toggleTerminal"
|
|
|
|
|
:disabled="!activeWorkspace"
|
|
|
|
|
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg border px-3 py-2 transition-colors disabled:cursor-not-allowed disabled:opacity-50"
|
|
|
|
|
:class="activeWorkspace?.terminalVisible
|
|
|
|
|
? 'border-cyan-500/30 bg-cyan-500/10 text-cyan-200'
|
|
|
|
|
: 'border-slate-700 bg-slate-800 hover:border-slate-600 hover:text-slate-100'"
|
|
|
|
|
:class="[commandButtonClass, activeWorkspace?.terminalVisible ? commandButtonActiveClass : commandButtonNeutralClass]"
|
|
|
|
|
aria-label="切换终端面板"
|
|
|
|
|
>
|
|
|
|
|
<SquareTerminal class="h-3.5 w-3.5" />
|
|
|
|
@@ -370,10 +378,7 @@ const tabContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|
|
|
|
type="button"
|
|
|
|
|
@click="toggleSftp"
|
|
|
|
|
:disabled="!activeWorkspace"
|
|
|
|
|
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg border px-3 py-2 transition-colors disabled:cursor-not-allowed disabled:opacity-50"
|
|
|
|
|
:class="activeWorkspace?.sftpVisible
|
|
|
|
|
? 'border-cyan-500/30 bg-cyan-500/10 text-cyan-200'
|
|
|
|
|
: 'border-slate-700 bg-slate-800 hover:border-slate-600 hover:text-slate-100'"
|
|
|
|
|
:class="[commandButtonClass, activeWorkspace?.sftpVisible ? commandButtonActiveClass : commandButtonNeutralClass]"
|
|
|
|
|
aria-label="切换 SFTP 面板"
|
|
|
|
|
>
|
|
|
|
|
<FolderTree class="h-3.5 w-3.5" />
|
|
|
|
@@ -383,7 +388,7 @@ const tabContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|
|
|
|
type="button"
|
|
|
|
|
@click="resetSplit"
|
|
|
|
|
:disabled="!activeWorkspace"
|
|
|
|
|
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg border border-slate-700 bg-slate-800 px-3 py-2 transition-colors hover:border-slate-600 hover:text-slate-100 disabled:cursor-not-allowed disabled:opacity-50"
|
|
|
|
|
:class="[commandButtonClass, commandButtonNeutralClass]"
|
|
|
|
|
aria-label="重置分屏比例"
|
|
|
|
|
>
|
|
|
|
|
<Columns2 class="h-3.5 w-3.5" />
|
|
|
|
@@ -392,10 +397,11 @@ const tabContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="ml-auto flex shrink-0 items-center gap-1 sm:gap-2">
|
|
|
|
|
<div class="ml-auto flex shrink-0 items-center gap-1 rounded-2xl border border-slate-800/80 bg-slate-950/70 px-1.5 py-1">
|
|
|
|
|
<button
|
|
|
|
|
class="inline-flex min-h-[40px] min-w-[40px] items-center justify-center rounded-lg text-slate-400 transition-colors hover:bg-slate-800 hover:text-slate-200"
|
|
|
|
|
:class="utilityButtonClass"
|
|
|
|
|
title="通知"
|
|
|
|
|
type="button"
|
|
|
|
|
>
|
|
|
|
@@ -403,7 +409,7 @@ const tabContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
@click="openAbout"
|
|
|
|
|
class="hidden min-h-[40px] min-w-[40px] items-center justify-center rounded-lg text-slate-400 transition-colors hover:bg-slate-800 hover:text-slate-200 sm:inline-flex"
|
|
|
|
|
:class="[utilityButtonClass, 'hidden sm:inline-flex']"
|
|
|
|
|
title="关于与诊断"
|
|
|
|
|
type="button"
|
|
|
|
|
>
|
|
|
|
@@ -411,7 +417,7 @@ const tabContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
@click="openSettings"
|
|
|
|
|
class="inline-flex min-h-[40px] min-w-[40px] items-center justify-center rounded-lg text-slate-400 transition-colors hover:bg-slate-800 hover:text-slate-200"
|
|
|
|
|
:class="utilityButtonClass"
|
|
|
|
|
title="设置中心"
|
|
|
|
|
aria-label="设置中心"
|
|
|
|
|
type="button"
|
|
|
|
@@ -420,50 +426,52 @@ const tabContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
@click="openChangePassword"
|
|
|
|
|
class="inline-flex min-h-[40px] min-w-[40px] items-center justify-center rounded-lg text-slate-400 transition-colors hover:bg-slate-800 hover:text-slate-200"
|
|
|
|
|
:class="utilityButtonClass"
|
|
|
|
|
title="修改密码"
|
|
|
|
|
aria-label="修改密码"
|
|
|
|
|
type="button"
|
|
|
|
|
>
|
|
|
|
|
<ShieldCheck class="h-4 w-4" />
|
|
|
|
|
</button>
|
|
|
|
|
<div class="mx-1 hidden h-6 w-px bg-slate-800 sm:block" />
|
|
|
|
|
<button
|
|
|
|
|
@click="handleLogout"
|
|
|
|
|
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg px-2.5 py-2 text-slate-400 transition-colors hover:bg-slate-800 hover:text-slate-200"
|
|
|
|
|
class="inline-flex h-10 items-center gap-1.5 rounded-xl px-3 text-[13px] font-medium text-slate-300 transition-colors duration-200 hover:bg-slate-800/90 hover:text-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-400/60"
|
|
|
|
|
title="退出登录"
|
|
|
|
|
aria-label="退出登录"
|
|
|
|
|
type="button"
|
|
|
|
|
>
|
|
|
|
|
<LogOut class="h-4 w-4" />
|
|
|
|
|
<span class="hidden text-xs sm:inline">退出</span>
|
|
|
|
|
<span class="hidden sm:inline">退出</span>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="bg-slate-950/35 px-3 py-2 sm:px-4">
|
|
|
|
|
<div class="flex items-start gap-3">
|
|
|
|
|
<div class="hidden shrink-0 pt-2 sm:block">
|
|
|
|
|
<p class="text-[11px] font-medium uppercase tracking-[0.18em] text-slate-500">会话</p>
|
|
|
|
|
<div class="border-t border-slate-900/70 bg-slate-950/55 px-3 pt-2 sm:px-4">
|
|
|
|
|
<div class="flex items-end gap-3">
|
|
|
|
|
<div class="hidden shrink-0 pb-3 sm:block">
|
|
|
|
|
<p class="text-[11px] font-medium text-slate-400">工作区</p>
|
|
|
|
|
<p class="mt-1 text-xs text-slate-500">{{ workspaceTabCount }} 个打开</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="min-w-0 flex-1">
|
|
|
|
|
<div v-if="workspaceTabs.length > 0" class="flex items-center gap-1 overflow-x-auto pb-1 scrollbar-thin">
|
|
|
|
|
<div v-if="workspaceTabs.length > 0" class="flex items-end gap-2 overflow-x-auto scrollbar-thin">
|
|
|
|
|
<div
|
|
|
|
|
v-for="tab in workspaceTabs"
|
|
|
|
|
:key="tab.workspaceId"
|
|
|
|
|
class="group -mb-px flex min-h-[38px] max-w-[280px] shrink-0 items-center gap-2 rounded-t-lg border border-b-0 px-3 text-xs transition-colors cursor-pointer"
|
|
|
|
|
class="group relative flex max-w-[300px] shrink-0 cursor-pointer items-center gap-2 rounded-t-2xl border px-3.5 transition-all duration-200"
|
|
|
|
|
:class="tab.active
|
|
|
|
|
? 'border-cyan-400/35 bg-slate-900 text-cyan-100 shadow-[0_-1px_0_rgba(34,211,238,0.16)]'
|
|
|
|
|
: 'border-slate-700/80 bg-slate-800/65 text-slate-400 hover:bg-slate-800 hover:text-slate-100'"
|
|
|
|
|
? 'z-10 h-11 border-slate-700 border-b-slate-950 bg-slate-950 text-slate-50 shadow-[0_-10px_30px_rgba(15,23,42,0.3),inset_0_1px_0_rgba(148,163,184,0.08)]'
|
|
|
|
|
: 'mt-2 h-9 border-transparent bg-slate-900/35 text-slate-500 hover:mt-1 hover:h-10 hover:bg-slate-900/60 hover:text-slate-200'"
|
|
|
|
|
@click="activateTab(tab.workspaceId)"
|
|
|
|
|
@contextmenu="(e) => openTabContextMenu(tab.workspaceId, e)"
|
|
|
|
|
>
|
|
|
|
|
<span class="h-2 w-2 rounded-full" :class="tab.active ? 'bg-cyan-400' : 'bg-slate-500'" />
|
|
|
|
|
<span class="truncate max-w-[220px]">{{ tab.title }}</span>
|
|
|
|
|
<span class="h-2 w-2 rounded-full" :class="tab.active ? 'bg-cyan-400 shadow-[0_0_0_4px_rgba(34,211,238,0.12)]' : 'bg-slate-600'" />
|
|
|
|
|
<span class="truncate max-w-[220px] text-[13px] font-medium">{{ tab.title }}</span>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="rounded p-0.5 text-slate-500 opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100 hover:bg-slate-700 hover:text-slate-200"
|
|
|
|
|
class="rounded-lg p-1 text-slate-500 opacity-0 transition-all duration-200 group-hover:opacity-100 group-focus-within:opacity-100 hover:bg-slate-800 hover:text-slate-200 focus-visible:opacity-100 focus-visible:outline-none"
|
|
|
|
|
@click="(e) => closeTab(tab.workspaceId, e)"
|
|
|
|
|
:aria-label="`关闭会话 ${tab.title}`"
|
|
|
|
|
>
|
|
|
|
@@ -471,7 +479,9 @@ const tabContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-else class="py-2 text-xs text-slate-500">未打开工作区。点击左侧连接可创建新实例。</div>
|
|
|
|
|
<div v-else class="flex h-11 items-center rounded-t-2xl border border-dashed border-slate-800 bg-slate-950/45 px-4 text-sm text-slate-500">
|
|
|
|
|
未打开工作区。点击左侧连接可创建新实例。
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|