@@ -70,6 +70,37 @@ const textExtensions = new Set(["txt", "md", "json", "yaml", "yml", "toml", "ini
7070const 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" };
7171const editorLanguage = computed (() => languageMap [activeTab .value ?.entry .name .split (" ." ).pop ()?.toLowerCase () || " " ] || " plaintext" );
7272const 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
74105function 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