Refine workspace toolbar and file distribution UI
This commit is contained in:
@@ -28,7 +28,7 @@ const diagnostics = computed(() => {
|
|||||||
`Version: ${appVersion.value}`,
|
`Version: ${appVersion.value}`,
|
||||||
`First Launch: ${formatTime(productStatusStore.firstLaunchedAt)}`,
|
`First Launch: ${formatTime(productStatusStore.firstLaunchedAt)}`,
|
||||||
`Connections: ${connectionsStore.connections.length}`,
|
`Connections: ${connectionsStore.connections.length}`,
|
||||||
`Transfer Runs: ${transfersStore.runs.length}`,
|
`文件分发记录: ${transfersStore.runs.length}`,
|
||||||
`Activity Logs: ${activityLogStore.entries.length}`,
|
`Activity Logs: ${activityLogStore.entries.length}`,
|
||||||
`User Agent: ${navigator.userAgent}`,
|
`User Agent: ${navigator.userAgent}`,
|
||||||
].join('\n')
|
].join('\n')
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ function formatDuration(durationMs: number) {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="grid min-h-0 flex-1 gap-0 lg:grid-cols-[280px_minmax(0,1fr)]">
|
<div class="grid min-h-0 flex-1 gap-0 lg:grid-cols-[280px_minmax(0,1fr)]">
|
||||||
<aside class="border-b border-slate-800 bg-slate-950/80 lg:border-b-0 lg:border-r">
|
<aside class="flex min-h-0 flex-col border-b border-slate-800 bg-slate-950/80 lg:border-b-0 lg:border-r">
|
||||||
<div class="flex items-center justify-between border-b border-slate-800 px-4 py-3">
|
<div class="flex items-center justify-between border-b border-slate-800 px-4 py-3">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-medium text-slate-100">目标连接</p>
|
<p class="text-sm font-medium text-slate-100">目标连接</p>
|
||||||
@@ -155,7 +155,7 @@ function formatDuration(durationMs: number) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="max-h-[260px] overflow-y-auto lg:max-h-none lg:h-full">
|
<div class="min-h-0 overflow-y-auto max-h-[260px] lg:max-h-none lg:flex-1">
|
||||||
<label
|
<label
|
||||||
v-for="connection in connectionsStore.connections"
|
v-for="connection in connectionsStore.connections"
|
||||||
:key="connection.id"
|
:key="connection.id"
|
||||||
|
|||||||
@@ -791,20 +791,20 @@ const totalUploadProgress = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2 lg:grid-cols-[minmax(0,1fr)_18rem]">
|
<div class="flex flex-wrap items-stretch gap-2">
|
||||||
<div class="relative">
|
<div class="relative min-w-0 flex-1 basis-64">
|
||||||
<Search class="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-500" />
|
<Search class="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-500" />
|
||||||
<input
|
<input
|
||||||
ref="searchInputRef"
|
ref="searchInputRef"
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="搜索文件... (Ctrl/Cmd+F)"
|
placeholder="搜索文件... (Ctrl/Cmd+F)"
|
||||||
class="w-full rounded-lg border border-slate-700 bg-slate-900 py-2 pl-9 pr-9 text-sm text-slate-100 placeholder:text-slate-500 focus:border-cyan-500 focus:outline-none"
|
class="h-10 w-full rounded-lg border border-slate-700 bg-slate-900 py-2 pl-9 pr-9 text-sm text-slate-100 placeholder:text-slate-500 focus:border-cyan-500 focus:outline-none"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="searchQuery"
|
v-if="searchQuery"
|
||||||
type="button"
|
type="button"
|
||||||
class="absolute right-2 top-1/2 inline-flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded text-slate-500 transition-colors hover:bg-slate-800 hover:text-slate-300"
|
class="absolute right-2 top-1/2 inline-flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded text-slate-500 transition-colors hover:bg-slate-800 hover:text-slate-300"
|
||||||
@click="searchQuery = ''"
|
@click="searchQuery = ''"
|
||||||
aria-label="清除搜索"
|
aria-label="清除搜索"
|
||||||
>
|
>
|
||||||
@@ -812,7 +812,7 @@ const totalUploadProgress = computed(() => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2 rounded-lg border border-slate-700 bg-slate-900 px-2 py-1.5">
|
<div class="flex h-10 min-w-0 flex-1 basis-72 items-center gap-2 rounded-lg border border-slate-700 bg-slate-900 px-2">
|
||||||
<input
|
<input
|
||||||
ref="pathInputRef"
|
ref="pathInputRef"
|
||||||
v-model="pathDraft"
|
v-model="pathDraft"
|
||||||
@@ -823,7 +823,7 @@ const totalUploadProgress = computed(() => {
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="inline-flex min-h-[36px] items-center rounded-md bg-slate-800 px-3 text-xs text-slate-200 transition-colors hover:bg-slate-700"
|
class="inline-flex h-8 shrink-0 items-center rounded-md bg-slate-800 px-3 text-xs text-slate-200 transition-colors hover:bg-slate-700"
|
||||||
@click="navigateToTypedPath"
|
@click="navigateToTypedPath"
|
||||||
>
|
>
|
||||||
前往
|
前往
|
||||||
|
|||||||
@@ -88,6 +88,14 @@ const contextMenuY = ref(0)
|
|||||||
const contextWorkspaceId = ref<string | null>(null)
|
const contextWorkspaceId = ref<string | null>(null)
|
||||||
const backupFileInput = ref<HTMLInputElement | null>(null)
|
const backupFileInput = ref<HTMLInputElement | null>(null)
|
||||||
const backupBusy = ref(false)
|
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) {
|
function activateTab(workspaceId: string) {
|
||||||
workspaceStore.activateWorkspace(workspaceId)
|
workspaceStore.activateWorkspace(workspaceId)
|
||||||
@@ -268,13 +276,13 @@ const tabContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="border-b border-slate-700 bg-slate-900">
|
<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-2 sm:px-4">
|
<div class="border-b border-slate-800/80 px-3 py-3 sm:px-4">
|
||||||
<div class="flex flex-wrap items-center gap-2 lg:flex-nowrap lg:gap-3">
|
<div class="flex flex-wrap items-start gap-3 lg:flex-nowrap">
|
||||||
<div class="flex min-w-0 flex-1 items-center gap-2">
|
<div class="flex min-w-0 flex-1 items-start gap-2">
|
||||||
<button
|
<button
|
||||||
type="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 ? '收起会话树' : '打开会话树'"
|
:aria-label="props.sidebarOpen ? '收起会话树' : '打开会话树'"
|
||||||
@click="emit('toggleSidebar')"
|
@click="emit('toggleSidebar')"
|
||||||
>
|
>
|
||||||
@@ -282,120 +290,118 @@ const tabContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||||||
<PanelLeftOpen v-else class="h-4 w-4" />
|
<PanelLeftOpen v-else class="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="min-w-0 flex-1 overflow-x-auto pb-1 lg:pb-0 scrollbar-thin">
|
<div class="min-w-0 flex-1 overflow-x-auto pb-1 scrollbar-thin">
|
||||||
<div class="flex min-w-max items-center gap-2 text-xs text-slate-300">
|
<div class="flex min-w-max items-start gap-2.5">
|
||||||
<button
|
<div :class="commandGroupClass">
|
||||||
type="button"
|
<div class="hidden px-2 text-[11px] font-medium text-slate-500 xl:block">全局</div>
|
||||||
@click="openBatchCommand"
|
<button
|
||||||
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"
|
type="button"
|
||||||
aria-label="批量命令执行"
|
@click="openBatchCommand"
|
||||||
>
|
:class="[commandButtonClass, commandButtonNeutralClass]"
|
||||||
<TerminalSquare class="h-3.5 w-3.5" />
|
aria-label="批量命令执行"
|
||||||
<span>批量命令</span>
|
>
|
||||||
</button>
|
<TerminalSquare class="h-3.5 w-3.5" />
|
||||||
<button
|
<span>批量命令</span>
|
||||||
type="button"
|
</button>
|
||||||
@click="openOperationsHistory"
|
<button
|
||||||
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"
|
type="button"
|
||||||
aria-label="传输历史与操作日志"
|
@click="openOperationsHistory"
|
||||||
>
|
:class="[commandButtonClass, commandButtonNeutralClass]"
|
||||||
<ClipboardList class="h-3.5 w-3.5" />
|
aria-label="传输历史与操作日志"
|
||||||
<span>历史日志</span>
|
>
|
||||||
</button>
|
<ClipboardList class="h-3.5 w-3.5" />
|
||||||
<button
|
<span>历史日志</span>
|
||||||
type="button"
|
</button>
|
||||||
@click="handleExportBackup"
|
<button
|
||||||
:disabled="backupBusy"
|
type="button"
|
||||||
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"
|
@click="handleExportBackup"
|
||||||
aria-label="导出备份"
|
:disabled="backupBusy"
|
||||||
>
|
:class="[commandButtonClass, commandButtonNeutralClass]"
|
||||||
<Download class="h-3.5 w-3.5" />
|
aria-label="导出备份"
|
||||||
<span>导出备份</span>
|
>
|
||||||
</button>
|
<Download class="h-3.5 w-3.5" />
|
||||||
<button
|
<span>导出备份</span>
|
||||||
type="button"
|
</button>
|
||||||
@click="triggerImportBackup"
|
<button
|
||||||
:disabled="backupBusy"
|
type="button"
|
||||||
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"
|
@click="triggerImportBackup"
|
||||||
aria-label="导入备份"
|
:disabled="backupBusy"
|
||||||
>
|
:class="[commandButtonClass, commandButtonNeutralClass]"
|
||||||
<Upload class="h-3.5 w-3.5" />
|
aria-label="导入备份"
|
||||||
<span>导入备份</span>
|
>
|
||||||
</button>
|
<Upload class="h-3.5 w-3.5" />
|
||||||
<button
|
<span>导入备份</span>
|
||||||
type="button"
|
</button>
|
||||||
@click="openTransfers"
|
<button
|
||||||
class="inline-flex min-h-[40px] items-center gap-1.5 rounded-lg border px-3 py-2 transition-colors"
|
type="button"
|
||||||
:class="workspaceStore.transfersModalOpen
|
@click="openTransfers"
|
||||||
? 'border-cyan-500/30 bg-cyan-500/10 text-cyan-200'
|
:class="[commandButtonClass, workspaceStore.transfersModalOpen ? commandButtonActiveClass : commandButtonNeutralClass]"
|
||||||
: 'border-slate-700 bg-slate-800 hover:border-slate-600 hover:text-slate-100'"
|
aria-label="打开文件分发"
|
||||||
aria-label="打开传输页面"
|
>
|
||||||
>
|
<ArrowLeftRight class="h-3.5 w-3.5" />
|
||||||
<ArrowLeftRight class="h-3.5 w-3.5" />
|
<span>文件分发</span>
|
||||||
<span>Transfers</span>
|
</button>
|
||||||
</button>
|
</div>
|
||||||
<button
|
|
||||||
type="button"
|
<div :class="commandGroupClass">
|
||||||
@click="openCreateSession"
|
<div class="hidden px-2 text-[11px] font-medium text-slate-500 xl:block">工作区</div>
|
||||||
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"
|
<button
|
||||||
aria-label="新增会话"
|
type="button"
|
||||||
>
|
@click="openCreateSession"
|
||||||
<Plus class="h-3.5 w-3.5" />
|
:class="[commandButtonClass, commandButtonEmphasisClass]"
|
||||||
<span>新增连接</span>
|
aria-label="新增会话"
|
||||||
</button>
|
>
|
||||||
<button
|
<Plus class="h-3.5 w-3.5" />
|
||||||
type="button"
|
<span>新增连接</span>
|
||||||
@click="duplicateActiveWorkspace"
|
</button>
|
||||||
:disabled="!activeWorkspace"
|
<button
|
||||||
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"
|
type="button"
|
||||||
aria-label="复制当前工作区"
|
@click="duplicateActiveWorkspace"
|
||||||
>
|
:disabled="!activeWorkspace"
|
||||||
<CopyPlus class="h-3.5 w-3.5" />
|
:class="[commandButtonClass, commandButtonNeutralClass]"
|
||||||
<span>复制会话</span>
|
aria-label="复制当前工作区"
|
||||||
</button>
|
>
|
||||||
<button
|
<CopyPlus class="h-3.5 w-3.5" />
|
||||||
type="button"
|
<span>复制会话</span>
|
||||||
@click="toggleTerminal"
|
</button>
|
||||||
:disabled="!activeWorkspace"
|
<button
|
||||||
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"
|
type="button"
|
||||||
:class="activeWorkspace?.terminalVisible
|
@click="toggleTerminal"
|
||||||
? 'border-cyan-500/30 bg-cyan-500/10 text-cyan-200'
|
:disabled="!activeWorkspace"
|
||||||
: 'border-slate-700 bg-slate-800 hover:border-slate-600 hover:text-slate-100'"
|
:class="[commandButtonClass, activeWorkspace?.terminalVisible ? commandButtonActiveClass : commandButtonNeutralClass]"
|
||||||
aria-label="切换终端面板"
|
aria-label="切换终端面板"
|
||||||
>
|
>
|
||||||
<SquareTerminal class="h-3.5 w-3.5" />
|
<SquareTerminal class="h-3.5 w-3.5" />
|
||||||
<span>终端</span>
|
<span>终端</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="toggleSftp"
|
@click="toggleSftp"
|
||||||
:disabled="!activeWorkspace"
|
: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="[commandButtonClass, activeWorkspace?.sftpVisible ? commandButtonActiveClass : commandButtonNeutralClass]"
|
||||||
:class="activeWorkspace?.sftpVisible
|
aria-label="切换 SFTP 面板"
|
||||||
? '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'"
|
<FolderTree class="h-3.5 w-3.5" />
|
||||||
aria-label="切换 SFTP 面板"
|
<span>文件</span>
|
||||||
>
|
</button>
|
||||||
<FolderTree class="h-3.5 w-3.5" />
|
<button
|
||||||
<span>文件</span>
|
type="button"
|
||||||
</button>
|
@click="resetSplit"
|
||||||
<button
|
:disabled="!activeWorkspace"
|
||||||
type="button"
|
:class="[commandButtonClass, commandButtonNeutralClass]"
|
||||||
@click="resetSplit"
|
aria-label="重置分屏比例"
|
||||||
: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"
|
<Columns2 class="h-3.5 w-3.5" />
|
||||||
aria-label="重置分屏比例"
|
<span>重置分屏</span>
|
||||||
>
|
</button>
|
||||||
<Columns2 class="h-3.5 w-3.5" />
|
</div>
|
||||||
<span>重置分屏</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</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
|
<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="通知"
|
title="通知"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
@@ -403,7 +409,7 @@ const tabContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="openAbout"
|
@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="关于与诊断"
|
title="关于与诊断"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
@@ -411,7 +417,7 @@ const tabContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="openSettings"
|
@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="设置中心"
|
title="设置中心"
|
||||||
aria-label="设置中心"
|
aria-label="设置中心"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -420,50 +426,52 @@ const tabContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="openChangePassword"
|
@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="修改密码"
|
title="修改密码"
|
||||||
aria-label="修改密码"
|
aria-label="修改密码"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<ShieldCheck class="h-4 w-4" />
|
<ShieldCheck class="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
<div class="mx-1 hidden h-6 w-px bg-slate-800 sm:block" />
|
||||||
<button
|
<button
|
||||||
@click="handleLogout"
|
@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="退出登录"
|
title="退出登录"
|
||||||
aria-label="退出登录"
|
aria-label="退出登录"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<LogOut class="h-4 w-4" />
|
<LogOut class="h-4 w-4" />
|
||||||
<span class="hidden text-xs sm:inline">退出</span>
|
<span class="hidden sm:inline">退出</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-slate-950/35 px-3 py-2 sm:px-4">
|
<div class="border-t border-slate-900/70 bg-slate-950/55 px-3 pt-2 sm:px-4">
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-end gap-3">
|
||||||
<div class="hidden shrink-0 pt-2 sm:block">
|
<div class="hidden shrink-0 pb-3 sm:block">
|
||||||
<p class="text-[11px] font-medium uppercase tracking-[0.18em] text-slate-500">会话</p>
|
<p class="text-[11px] font-medium text-slate-400">工作区</p>
|
||||||
|
<p class="mt-1 text-xs text-slate-500">{{ workspaceTabCount }} 个打开</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="min-w-0 flex-1">
|
<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
|
<div
|
||||||
v-for="tab in workspaceTabs"
|
v-for="tab in workspaceTabs"
|
||||||
:key="tab.workspaceId"
|
: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
|
:class="tab.active
|
||||||
? 'border-cyan-400/35 bg-slate-900 text-cyan-100 shadow-[0_-1px_0_rgba(34,211,238,0.16)]'
|
? '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)]'
|
||||||
: 'border-slate-700/80 bg-slate-800/65 text-slate-400 hover:bg-slate-800 hover:text-slate-100'"
|
: '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)"
|
@click="activateTab(tab.workspaceId)"
|
||||||
@contextmenu="(e) => openTabContextMenu(tab.workspaceId, e)"
|
@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="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]">{{ tab.title }}</span>
|
<span class="truncate max-w-[220px] text-[13px] font-medium">{{ tab.title }}</span>
|
||||||
<button
|
<button
|
||||||
type="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)"
|
@click="(e) => closeTab(tab.workspaceId, e)"
|
||||||
:aria-label="`关闭会话 ${tab.title}`"
|
:aria-label="`关闭会话 ${tab.title}`"
|
||||||
>
|
>
|
||||||
@@ -471,7 +479,9 @@ const tabContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -379,17 +379,17 @@ function resolveErrorMessage(error: unknown, fallback: string) {
|
|||||||
class="w-full max-w-[1240px] h-[88vh] rounded-2xl border border-slate-800 bg-slate-900 shadow-2xl flex flex-col overflow-hidden"
|
class="w-full max-w-[1240px] h-[88vh] rounded-2xl border border-slate-800 bg-slate-900 shadow-2xl flex flex-col overflow-hidden"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label="Transfers 弹窗"
|
aria-label="文件分发弹窗"
|
||||||
>
|
>
|
||||||
<header class="h-12 px-4 border-b border-slate-800 flex items-center justify-between shrink-0">
|
<header class="h-12 px-4 border-b border-slate-800 flex items-center justify-between shrink-0">
|
||||||
<div class="inline-flex items-center gap-2 text-sm text-slate-200">
|
<div class="inline-flex items-center gap-2 text-sm text-slate-200">
|
||||||
<ArrowLeftRight class="w-4 h-4 text-cyan-300" />
|
<ArrowLeftRight class="w-4 h-4 text-cyan-300" />
|
||||||
<span class="font-medium">Transfers</span>
|
<span class="font-medium">文件分发</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="closeTransfersModal"
|
@click="closeTransfersModal"
|
||||||
class="w-9 h-9 rounded-md border border-slate-700 text-slate-300 hover:bg-slate-800 hover:text-slate-100 transition-colors cursor-pointer"
|
class="w-9 h-9 rounded-md border border-slate-700 text-slate-300 hover:bg-slate-800 hover:text-slate-100 transition-colors cursor-pointer"
|
||||||
aria-label="关闭 Transfers 面板"
|
aria-label="关闭文件分发面板"
|
||||||
>
|
>
|
||||||
<X class="w-4 h-4 mx-auto" />
|
<X class="w-4 h-4 mx-auto" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ onMounted(async () => {
|
|||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<h1 class="font-semibold tracking-tight text-slate-50" :class="props.embedded ? 'text-xl' : 'text-2xl'">Transfers</h1>
|
<h1 class="font-semibold tracking-tight text-slate-50" :class="props.embedded ? 'text-xl' : 'text-2xl'">文件分发</h1>
|
||||||
<p class="text-sm text-slate-400">本机 -> 多台 / 其他机器 -> 多台</p>
|
<p class="text-sm text-slate-400">本机 -> 多台 / 其他机器 -> 多台</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 self-start sm:self-auto">
|
<div class="flex items-center gap-2 self-start sm:self-auto">
|
||||||
|
|||||||
Reference in New Issue
Block a user