Refine workspace toolbar and file distribution UI

This commit is contained in:
liumangmang
2026-04-21 00:42:50 +08:00
parent 00ccd33961
commit 05b835eb02
6 changed files with 154 additions and 144 deletions
@@ -28,7 +28,7 @@ const diagnostics = computed(() => {
`Version: ${appVersion.value}`,
`First Launch: ${formatTime(productStatusStore.firstLaunchedAt)}`,
`Connections: ${connectionsStore.connections.length}`,
`Transfer Runs: ${transfersStore.runs.length}`,
`文件分发记录: ${transfersStore.runs.length}`,
`Activity Logs: ${activityLogStore.entries.length}`,
`User Agent: ${navigator.userAgent}`,
].join('\n')
@@ -140,7 +140,7 @@ function formatDuration(durationMs: number) {
</header>
<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>
<p class="text-sm font-medium text-slate-100">目标连接</p>
@@ -155,7 +155,7 @@ function formatDuration(durationMs: number) {
</button>
</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
v-for="connection in connectionsStore.connections"
:key="connection.id"
+6 -6
View File
@@ -791,20 +791,20 @@ const totalUploadProgress = computed(() => {
</div>
</div>
<div class="grid gap-2 lg:grid-cols-[minmax(0,1fr)_18rem]">
<div class="relative">
<div class="flex flex-wrap items-stretch gap-2">
<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" />
<input
ref="searchInputRef"
v-model="searchQuery"
type="text"
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
v-if="searchQuery"
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 = ''"
aria-label="清除搜索"
>
@@ -812,7 +812,7 @@ const totalUploadProgress = computed(() => {
</button>
</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
ref="pathInputRef"
v-model="pathDraft"
@@ -823,7 +823,7 @@ const totalUploadProgress = computed(() => {
/>
<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"
>
前往
+57 -47
View File
@@ -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>
+3 -3
View File
@@ -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"
role="dialog"
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">
<div class="inline-flex items-center gap-2 text-sm text-slate-200">
<ArrowLeftRight class="w-4 h-4 text-cyan-300" />
<span class="font-medium">Transfers</span>
<span class="font-medium">文件分发</span>
</div>
<button
@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"
aria-label="关闭 Transfers 面板"
aria-label="关闭文件分发面板"
>
<X class="w-4 h-4 mx-auto" />
</button>
+1 -1
View File
@@ -255,7 +255,7 @@ onMounted(async () => {
<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="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>
</div>
<div class="flex items-center gap-2 self-start sm:self-auto">