Skip to content

Commit 660e13f

Browse files
committed
perf: update connect
1 parent ec306f8 commit 660e13f

9 files changed

Lines changed: 472 additions & 108 deletions

File tree

ui/assets/css/main.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,13 @@
125125
--workspace-surface-header: var(--app-surface-header);
126126
--workspace-surface-footer: var(--app-surface-footer);
127127
--workspace-surface-border: var(--app-border-subtle);
128+
--workspace-surface-sub-sidebar: color-mix(in srgb, var(--app-surface-panel) 74%, var(--app-surface-sidebar) 26%);
129+
--workspace-surface-sub-panel: color-mix(in srgb, var(--app-surface-panel-strong) 78%, var(--app-surface-canvas) 22%);
130+
--workspace-surface-sub-header: color-mix(in srgb, var(--app-surface-panel) 80%, var(--app-surface-header) 20%);
131+
--workspace-surface-sub-tab: color-mix(in srgb, var(--app-surface-panel) 72%, var(--app-surface-canvas) 28%);
132+
--workspace-surface-sub-tab-active: color-mix(in srgb, var(--app-surface-canvas) 86%, var(--app-surface-panel-strong) 14%);
133+
--workspace-surface-sub-tree: color-mix(in srgb, var(--app-surface-panel-strong) 68%, var(--app-surface-canvas) 32%);
134+
--workspace-surface-sub-border: color-mix(in srgb, var(--app-border-subtle) 86%, var(--app-surface-panel) 14%);
128135
--ui-bg: var(--app-surface-canvas);
129136
--ui-bg-muted: var(--app-surface-sidebar);
130137
--ui-bg-elevated: var(--app-surface-panel);

ui/components/SideBar/assetTree.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ defineExpose({ refresh, loading });
251251
</template>
252252

253253
<template v-else>
254-
<section class="group flex min-h-0 flex-1 flex-col overflow-hidden border-b border-gray-200 dark:border-white/10">
254+
<section class="group flex min-h-0 flex-1 flex-col overflow-hidden">
255255
<div class="flex h-8 w-full shrink-0 items-center gap-1 px-3 text-xs font-medium text-gray-700 dark:text-gray-300">
256256
<button
257257
type="button"

ui/components/Workspace/sessionPane.vue

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ const { activeTabId, closeSession, toSurfaceTab } = useWorkspaceTabs();
1212
const splitSurfaces = computed(() => props.tab.splitSessions || []);
1313
const hasSplit = computed(() => splitSurfaces.value.length > 0);
1414
15+
function surfaceInstanceKey(tab: WorkspaceSessionTab) {
16+
const payload = tab.payload || {};
17+
const tokenId = String(payload.id || payload.token?.id || "");
18+
const webUrl = String(payload.webUrl || "");
19+
const connectMethod = String(payload.connectMethod?.value || "");
20+
21+
return [tab.id, tokenId, webUrl, connectMethod, tab.status].join(":");
22+
}
23+
1524
function focusSurface() {
1625
surfaceRef.value?.focus?.();
1726
}
@@ -33,7 +42,7 @@ defineExpose({ focus: focusSurface });
3342
<template>
3443
<div class="h-full min-h-0 w-full overflow-hidden" @mousedown="focusSurface">
3544
<div v-if="!hasSplit" class="h-full min-h-0">
36-
<component :is="surfaceComponent" ref="surfaceRef" :tab="tab" />
45+
<component :is="surfaceComponent" :key="surfaceInstanceKey(tab)" ref="surfaceRef" :tab="tab" />
3746
</div>
3847

3948
<div
@@ -42,7 +51,7 @@ defineExpose({ focus: focusSurface });
4251
:class="splitSurfaces.length > 1 ? 'grid-rows-2' : 'grid-cols-2'"
4352
>
4453
<div class="relative min-h-0 min-w-0">
45-
<component :is="surfaceComponent" ref="surfaceRef" :tab="tab" />
54+
<component :is="surfaceComponent" :key="surfaceInstanceKey(tab)" ref="surfaceRef" :tab="tab" />
4655
</div>
4756

4857
<div
@@ -60,6 +69,7 @@ defineExpose({ focus: focusSurface });
6069
</button>
6170
<component
6271
:is="surfaceComponent"
72+
:key="surfaceInstanceKey(splitSurfaceTab(split))"
6373
:tab="splitSurfaceTab(split)"
6474
class="h-full"
6575
/>
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<script setup lang="ts">
2+
export interface WorkspaceSubTab {
3+
id: string
4+
label: string
5+
icon: string
6+
title?: string
7+
dirty?: boolean
8+
}
9+
10+
defineProps<{
11+
tabs: WorkspaceSubTab[]
12+
activeId: string
13+
}>();
14+
15+
defineEmits<{
16+
select: [id: string]
17+
close: [id: string]
18+
}>();
19+
</script>
20+
21+
<template>
22+
<div
23+
v-if="tabs.length"
24+
class="workspace-sub-tab-bar shrink-0 border-b border-[var(--workspace-surface-sub-border)] bg-[var(--workspace-surface-sub-tab)] px-2 py-1.5"
25+
>
26+
<div class="workspace-sub-tab-capsule flex w-fit min-w-0 max-w-full items-center rounded-lg p-px">
27+
<div class="workspace-sub-tab-strip flex w-fit min-w-0 max-w-full items-center gap-0.5 overflow-x-auto">
28+
<button
29+
v-for="(tab, index) in tabs"
30+
:key="tab.id"
31+
type="button"
32+
class="group relative flex h-7 min-w-0 shrink-0 items-center gap-1.5 rounded-lg px-2 text-left transition-all duration-150"
33+
:class="[
34+
activeId === tab.id ? 'max-w-72 px-2.5' : 'max-w-44',
35+
activeId === tab.id
36+
? 'bg-[var(--workspace-surface-sub-tab-active)] text-[var(--app-fg)] shadow-[0_8px_18px_rgba(15,23,42,0.12)] ring-1 ring-[var(--workspace-surface-sub-border)]'
37+
: 'text-[var(--app-muted)] hover:bg-[var(--app-hover-soft)] hover:text-[var(--app-fg)]'
38+
]"
39+
:title="tab.title || tab.label"
40+
@click="$emit('select', tab.id)"
41+
>
42+
<UIcon
43+
:name="tab.icon"
44+
class="size-3.5 shrink-0"
45+
:class="activeId === tab.id ? 'text-primary' : 'text-[var(--app-muted)] group-hover:text-[var(--app-fg)]'"
46+
/>
47+
<span class="min-w-0 truncate font-ui-mono text-[11px] tracking-[0.01em]">{{ tab.label }}</span>
48+
<span v-if="tab.dirty" class="size-1.5 shrink-0 rounded-full bg-primary" title="未保存" />
49+
<span
50+
class="flex size-3.5 shrink-0 items-center justify-center rounded-md opacity-0 transition-opacity hover:bg-[var(--app-hover-strong)] group-hover:opacity-100"
51+
:class="activeId === tab.id ? 'opacity-60' : ''"
52+
@click.stop="$emit('close', tab.id)"
53+
>
54+
<UIcon name="i-lucide-x" class="size-2.5" />
55+
</span>
56+
<span
57+
v-if="activeId !== tab.id && index < tabs.length - 1"
58+
class="pointer-events-none absolute top-1/2 -right-[3px] h-3 -translate-y-1/2 border-r border-[var(--workspace-surface-sub-border)]"
59+
/>
60+
</button>
61+
</div>
62+
</div>
63+
</div>
64+
</template>
65+
66+
<style scoped>
67+
.workspace-sub-tab-capsule {
68+
background-color: color-mix(in srgb, var(--workspace-surface-sub-panel) 72%, var(--workspace-surface-sub-tab) 28%);
69+
border: 1px solid var(--workspace-surface-sub-border);
70+
box-shadow: inset 0 1px 0 color-mix(in srgb, var(--workspace-surface-sub-border) 58%, transparent);
71+
}
72+
73+
.workspace-sub-tab-strip {
74+
scrollbar-width: none;
75+
-ms-overflow-style: none;
76+
}
77+
78+
.workspace-sub-tab-strip::-webkit-scrollbar {
79+
display: none;
80+
}
81+
</style>

ui/components/Workspace/topHeader.vue

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,7 @@ const handleWindowDrag = async (event: MouseEvent) => {
4242
class="header-bg h-10 min-h-10 max-h-10 shrink-0 flex items-center"
4343
:style="{
4444
backgroundColor: 'var(--app-header-bg)',
45-
borderBottom: '1px solid var(--app-border)',
46-
boxShadow: 'inset 0 -1px 0 color-mix(in srgb, var(--app-border) 82%, transparent)'
45+
borderBottom: '1px solid var(--app-border)'
4746
}"
4847
@mousedown="handleWindowDrag"
4948
>

ui/koko/components/SftpIde/index.vue

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,37 @@ const textExtensions = new Set(["txt", "md", "json", "yaml", "yml", "toml", "ini
7070
const languageMap: Record<string, string> = { bash: "shell", c: "c", conf: "ini", cpp: "cpp", css: "css", env: "ini", go: "go", h: "c", hpp: "cpp", html: "html", ini: "ini", java: "java", js: "javascript", json: "json", jsx: "javascript", less: "less", log: "plaintext", md: "markdown", py: "python", rs: "rust", scss: "scss", sh: "shell", sql: "sql", toml: "ini", ts: "typescript", tsx: "typescript", txt: "plaintext", vue: "html", xml: "xml", yaml: "yaml", yml: "yaml", zsh: "shell" };
7171
const editorLanguage = computed(() => languageMap[activeTab.value?.entry.name.split(".").pop()?.toLowerCase() || ""] || "plaintext");
7272
const dirty = (tab: EditorTab) => tab.kind === "text" && tab.content !== tab.savedContent;
73+
const tabIconMap: Record<string, string> = {
74+
css: "i-lucide-palette",
75+
html: "i-lucide-globe",
76+
js: "i-lucide-braces",
77+
json: "i-lucide-braces",
78+
jsx: "i-lucide-braces",
79+
md: "i-lucide-file-text",
80+
py: "i-lucide-file-code",
81+
sh: "i-lucide-terminal",
82+
sql: "i-lucide-database",
83+
ts: "i-lucide-braces",
84+
tsx: "i-lucide-braces",
85+
vue: "i-lucide-component",
86+
yaml: "i-lucide-file-text",
87+
yml: "i-lucide-file-text"
88+
};
89+
90+
function tabIcon(tab: EditorTab) {
91+
if (tab.kind === "image") return "i-lucide-image";
92+
if (tab.kind === "unsupported" || tab.error) return "i-lucide-file-warning";
93+
const ext = tab.entry.name.split(".").pop()?.toLowerCase() || "";
94+
return tabIconMap[ext] || "i-lucide-file-code-2";
95+
}
96+
97+
const subTabs = computed(() => tabs.value.map((tab) => ({
98+
id: tab.path,
99+
label: tab.entry.name,
100+
icon: tabIcon(tab),
101+
title: tab.path,
102+
dirty: dirty(tab)
103+
})));
73104
74105
function joinPath(parent: string, name: string) {
75106
return `${parent.replace(/\/$/, "")}/${name}` || `/${name}`;
@@ -394,8 +425,8 @@ onUnmounted(() => tabs.value.forEach(revokePreview));
394425

395426
<template>
396427
<div class="grid h-full min-h-0 grid-cols-[260px_minmax(0,1fr)] bg-[var(--app-main-bg)] text-[var(--app-fg)]">
397-
<aside class="flex min-h-0 flex-col border-r border-[var(--app-border)] bg-[var(--app-panel-bg)]">
398-
<div class="flex h-10 min-w-0 shrink-0 items-center gap-1 border-b border-[var(--app-border)] px-2">
428+
<aside class="flex min-h-0 flex-col border-r border-[var(--workspace-surface-sub-border)] bg-[var(--workspace-surface-sub-sidebar)]">
429+
<div class="flex h-10 min-w-0 shrink-0 items-center gap-1 border-b border-[var(--workspace-surface-sub-border)] bg-[var(--workspace-surface-sub-header)] px-2">
399430
<button class="min-w-0 flex-1 truncate px-1 text-left font-ui-mono text-[10px]" :class="selectedDirectory === rootPath ? 'text-primary' : 'text-muted'" :title="rootPath" @click="selectedDirectory = rootPath" @contextmenu="openContextMenu({ name: rootPath, size: '', perm: '', mod_time: '', type: '', is_dir: true }, rootPath, $event)">
400431
{{ rootPath || "/" }}
401432
</button>
@@ -405,10 +436,10 @@ onUnmounted(() => tabs.value.forEach(revokePreview));
405436
<UButton icon="i-lucide-search" size="xs" color="neutral" :variant="searchVisible ? 'soft' : 'ghost'" title="搜索" @click="toggleSearch" />
406437
<UButton icon="i-lucide-refresh-cw" size="xs" color="neutral" variant="ghost" title="刷新目录树" @click="refreshTree" />
407438
</div>
408-
<div v-if="searchVisible" class="shrink-0 border-b border-[var(--app-border)] bg-[var(--app-main-bg)] p-2">
439+
<div v-if="searchVisible" class="shrink-0 border-b border-[var(--workspace-surface-sub-border)] bg-[var(--workspace-surface-sub-tree)] p-2">
409440
<UInput v-model="search" icon="i-lucide-search" size="xs" placeholder="筛选文件" class="w-full" />
410441
</div>
411-
<div class="min-h-0 flex-1 overflow-auto bg-[var(--app-panel-bg)] py-1">
442+
<div class="min-h-0 flex-1 overflow-auto bg-[var(--workspace-surface-sub-tree)] py-1">
412443
<template v-for="row in treeRows" :key="row.path">
413444
<button v-if="row.kind === 'entry'" class="flex h-7 w-full items-center gap-1 pr-2 text-left text-xs text-[var(--app-fg)] hover:bg-[var(--app-hover-soft)]" :class="row.entry.is_dir ? (selectedDirectory === row.path ? 'bg-[var(--app-selected-soft)] text-primary' : '') : (activePath === row.path ? 'bg-[var(--app-selected-soft)] text-primary' : '')" :style="{ paddingLeft: `${8 + row.depth * 14}px` }" :title="row.path" @click="openEntry(row.entry, row.path)" @contextmenu="openContextMenu(row.entry, row.path, $event)">
414445
<UIcon v-if="row.entry.is_dir" :name="row.expanded ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'" class="size-3 shrink-0 text-[var(--app-muted)]" />
@@ -419,7 +450,7 @@ onUnmounted(() => tabs.value.forEach(revokePreview));
419450
<div v-else class="py-1 pr-2" :style="{ paddingLeft: `${8 + row.depth * 14}px` }">
420451
<div class="flex items-center gap-1">
421452
<UIcon :name="row.createKind === 'directory' ? 'i-lucide-folder' : 'i-lucide-file-code-2'" class="size-3.5 shrink-0 text-[var(--app-muted)]" />
422-
<input ref="pendingInput" v-model="pendingName" class="h-6 min-w-0 flex-1 rounded border border-primary bg-[var(--app-main-bg)] px-1.5 text-xs text-[var(--app-fg)] outline-none" :placeholder="row.createKind === 'directory' ? '目录名称' : '文件名称'" :disabled="pendingSubmitting" @keydown.enter.prevent="commitCreate" @keydown.esc.prevent="cancelCreate">
453+
<input ref="pendingInput" v-model="pendingName" class="h-6 min-w-0 flex-1 rounded border border-primary bg-[var(--workspace-surface-sub-panel)] px-1.5 text-xs text-[var(--app-fg)] outline-none" :placeholder="row.createKind === 'directory' ? '目录名称' : '文件名称'" :disabled="pendingSubmitting" @keydown.enter.prevent="commitCreate" @keydown.esc.prevent="cancelCreate">
423454
<UButton icon="i-lucide-check" size="xs" color="primary" variant="ghost" :loading="pendingSubmitting" title="确认" @click="commitCreate" />
424455
<UButton icon="i-lucide-x" size="xs" color="neutral" variant="ghost" :disabled="pendingSubmitting" title="取消" @click="cancelCreate" />
425456
</div>
@@ -442,15 +473,14 @@ onUnmounted(() => tabs.value.forEach(revokePreview));
442473
</aside>
443474

444475
<section class="flex min-h-0 min-w-0 flex-col">
445-
<div v-if="tabs.length" class="flex h-9 shrink-0 overflow-x-auto border-b border-[var(--app-border)] bg-[var(--app-panel-bg)]">
446-
<button v-for="tab in tabs" :key="tab.path" class="group flex min-w-28 max-w-52 shrink-0 items-center gap-2 border-r border-[var(--app-border)] px-3 text-xs" :class="activePath === tab.path ? 'bg-[var(--app-main-bg)] text-[var(--app-fg)]' : 'text-[var(--app-muted)] hover:bg-[var(--app-hover-soft)]'" :title="tab.path" @click="activePath = tab.path">
447-
<UIcon name="i-lucide-file-code-2" class="size-3.5 shrink-0" /><span class="min-w-0 flex-1 truncate text-left">{{ tab.entry.name }}</span>
448-
<span v-if="dirty(tab)" class="size-1.5 shrink-0 rounded-full bg-primary" />
449-
<UIcon name="i-lucide-x" class="size-3.5 shrink-0 opacity-0 group-hover:opacity-100" @click.stop="closeTab(tab)" />
450-
</button>
451-
</div>
476+
<WorkspaceSubTabStrip
477+
:tabs="subTabs"
478+
:active-id="activePath"
479+
@select="activePath = $event"
480+
@close="(id) => { const tab = tabs.find((item) => item.path === id); if (tab) closeTab(tab); }"
481+
/>
452482
<template v-if="activeTab">
453-
<header class="flex h-9 shrink-0 items-center justify-between border-b border-[var(--app-border)] bg-[var(--app-panel-bg)] px-3">
483+
<header class="flex h-9 shrink-0 items-center justify-between border-b border-[var(--workspace-surface-sub-border)] bg-[var(--workspace-surface-sub-header)] px-3">
454484
<span class="truncate font-ui-mono text-[10px] text-[var(--app-muted)]">{{ activeTab.path }}</span>
455485
<UButton v-if="activeTab.kind === 'text'" icon="i-lucide-save" size="xs" color="primary" variant="soft" :disabled="!dirty(activeTab)" :loading="activeTab.saving" @click="save()">
456486
保存
@@ -470,7 +500,7 @@ onUnmounted(() => tabs.value.forEach(revokePreview));
470500
<UIcon :name="activeTab.error ? 'i-lucide-circle-alert' : 'i-lucide-file-warning'" class="size-10" /><span>{{ activeTab.error || "该文件暂不支持在线预览或编辑" }}</span>
471501
</div>
472502
</div>
473-
<footer class="flex h-6 shrink-0 items-center justify-between border-t border-[var(--app-border)] bg-[var(--app-panel-bg)] px-3 text-[10px] text-[var(--app-muted)]">
503+
<footer class="flex h-6 shrink-0 items-center justify-between border-t border-[var(--workspace-surface-sub-border)] bg-[var(--workspace-surface-sub-header)] px-3 text-[10px] text-[var(--app-muted)]">
474504
<span>{{ activeTab.path }}</span><span>{{ activeTab.kind === "text" ? `${activeTab.content.length} chars · Ctrl/Cmd+S 保存` : "SFTP" }}</span>
475505
</footer>
476506
</template>

0 commit comments

Comments
 (0)