Skip to content

Commit b8cfb16

Browse files
committed
feat(workspace): 命名工作区(保存/打开一组根)
新增 useNamedWorkspaces(KV 存储)+ WorkspaceManager 弹窗:把当前主根+额外挂载根 存为命名条目,一键切换;打开时设主根并恢复额外根。命令面板「命名工作区」入口。
1 parent e5c4dcb commit b8cfb16

6 files changed

Lines changed: 145 additions & 3 deletions

File tree

src/App.vue

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,8 @@
331331
<!-- 文件夹内全局搜索 -->
332332
<SearchPanel v-if="showSearch && rootDir" :root-dir="rootDir" :extra-roots="extraRoots" :scope="searchScope" @open="openSearchResult" @replaced="reloadAffectedFiles" @close="showSearch = false"/>
333333

334+
<WorkspaceManager v-if="showWorkspaces" :root-dir="rootDir" :extra-roots="extraRoots" @open="openWorkspace" @close="showWorkspaces = false"/>
335+
334336
<!-- 快速打开文件 -->
335337
<QuickOpen v-if="showQuickOpen && rootDir"
336338
:root-dir="rootDir"
@@ -591,6 +593,7 @@ import {useDebug} from './composables/useDebug'
591593
import AiAssistant from './components/AiAssistant.vue'
592594
import InlineGenerate from './components/InlineGenerate.vue'
593595
import SearchPanel from './components/SearchPanel.vue'
596+
import WorkspaceManager from './components/WorkspaceManager.vue'
594597
import {useTheme, type AppTheme} from './composables/useTheme'
595598
import Modal from './ui/Modal.vue'
596599
import Button from './ui/Button.vue'
@@ -754,7 +757,15 @@ const rootDir = ref<string | null>(null)
754757
const {copyPermalink, openPermalink, openRepoOnWeb} = useGitPermalink(rootDir, currentFilePath, cursorInfo)
755758
756759
// 多根工作区:额外挂载的文件夹(Git/搜索仍走主根 rootDir)
757-
const {extraRoots, addWorkspaceFolder, removeWorkspaceFolder, resetExtraRoots} = useWorkspaceRoots(rootDir)
760+
const {extraRoots, addWorkspaceFolder, removeWorkspaceFolder, resetExtraRoots, setExtraRoots} = useWorkspaceRoots(rootDir)
761+
762+
// 命名工作区:保存/打开一组根
763+
const showWorkspaces = ref(false)
764+
const openWorkspace = (ws: {rootDir: string; extraRoots: string[]}) => {
765+
showWorkspaces.value = false
766+
openFolderPath(ws.rootDir) // 设主根并清空额外根
767+
setExtraRoots(ws.extraRoots) // 再恢复该工作区的额外根
768+
}
758769
const sidebarVisible = ref(kvGet('sidebar-visible') === 'true')
759770
// 专注模式:隐藏顶部工具栏/运行输入/侧栏/状态栏,沉浸编辑
760771
const zenMode = ref(false)
@@ -2231,7 +2242,7 @@ const isOverlayOpen = () =>
22312242
|| showHistory.value || showViewer.value || showRunPrompt.value
22322243
|| showQuickOpen.value || showGenerate.value || showSearch.value
22332244
|| showCommandPalette.value || showDiff.value || showGoToLine.value || showOutline.value || showSnippets.value
2234-
|| applyPreview.value != null || clipboardDiff.value != null
2245+
|| applyPreview.value != null || clipboardDiff.value != null || showWorkspaces.value
22352246
22362247
// 全局快捷键(绑定可在设置中自定义)
22372248
const {matchAction: matchShortcut, reload: reloadShortcuts, getBinding, formatCombo} = useShortcuts()
@@ -2291,6 +2302,7 @@ const paletteCommands = computed<PaletteCommand[]>(() => [
22912302
{id: 'formatOnSave', label: formatOnSave.value ? t('command.formatOnSaveOff') : t('command.formatOnSaveOn'), icon: Save, run: () => toggleFormatOnSave()},
22922303
{id: 'open', label: t('command.open'), icon: FolderOpen, hint: hintOf('open'), run: () => handleOpenFileClick()},
22932304
{id: 'openFolder', label: t('command.openFolder'), icon: FolderOpen, run: () => openFolder()},
2305+
{id: 'workspaces', label: t('command.workspaces'), icon: FolderOpen, run: () => { showWorkspaces.value = true }},
22942306
{id: 'save', label: t('command.save'), icon: Save, hint: hintOf('save'), run: () => handleSave()},
22952307
{id: 'saveAs', label: t('command.saveAs'), icon: Save, hint: hintOf('saveAs'), run: () => saveFileAs()},
22962308
{id: 'newTab', label: t('command.newTab'), icon: Plus, hint: hintOf('newTab'), run: () => handleNewTab()},
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<template>
2+
<div class="fixed inset-0 z-50 flex items-start justify-center pt-24" @click="emit('close')">
3+
<div class="w-[520px] max-w-[90vw] bg-white dark:bg-gray-800 dark:text-gray-100 rounded-lg shadow-2xl border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col max-h-[60vh]"
4+
@click.stop>
5+
<div class="flex items-center gap-2 px-4 py-2.5 border-b border-gray-200 dark:border-gray-700 text-sm font-medium text-gray-700 dark:text-gray-200">
6+
<Layers class="w-4 h-4 text-brand-500"/>
7+
<span>{{ t('workspaces.title') }}</span>
8+
</div>
9+
10+
<!-- 保存当前 -->
11+
<div class="flex items-center gap-2 px-3 py-2.5 border-b border-gray-200 dark:border-gray-700">
12+
<input v-model="name"
13+
class="flex-1 text-sm border border-gray-300 dark:border-gray-600 dark:bg-gray-900 rounded px-2 py-1.5 focus:outline-none focus:border-blue-500"
14+
:placeholder="t('workspaces.namePlaceholder')"
15+
@keydown.enter="saveCurrent"/>
16+
<button class="text-xs px-3 py-1.5 rounded bg-brand-600 text-white hover:bg-brand-700 disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer whitespace-nowrap"
17+
:disabled="!rootDir || !name.trim()"
18+
@click="saveCurrent">
19+
{{ t('workspaces.saveCurrent') }}
20+
</button>
21+
</div>
22+
23+
<!-- 列表 -->
24+
<div class="flex-1 overflow-y-auto py-1">
25+
<div v-if="!workspaces.length" class="px-4 py-8 text-center text-sm text-gray-400">{{ t('workspaces.empty') }}</div>
26+
<div v-for="w in workspaces" :key="w.name"
27+
class="group flex items-center gap-2 px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700">
28+
<button class="flex-1 min-w-0 text-left cursor-pointer" @click="emit('open', w)">
29+
<div class="text-sm font-medium text-gray-800 dark:text-gray-100 truncate">{{ w.name }}</div>
30+
<div class="text-xs text-gray-400 truncate">
31+
{{ folderName(w.rootDir) }}<span v-if="w.extraRoots.length"> · +{{ w.extraRoots.length }}</span>
32+
</div>
33+
</button>
34+
<button class="p-1 rounded text-gray-400 hover:text-red-500 opacity-0 group-hover:opacity-100 cursor-pointer"
35+
:title="t('workspaces.delete')"
36+
@click.stop="remove(w.name)">
37+
<Trash2 class="w-3.5 h-3.5"/>
38+
</button>
39+
</div>
40+
</div>
41+
</div>
42+
</div>
43+
</template>
44+
45+
<script setup lang="ts">
46+
import {ref} from 'vue'
47+
import {useI18n} from 'vue-i18n'
48+
import {Layers, Trash2} from 'lucide-vue-next'
49+
import {useNamedWorkspaces, type NamedWorkspace} from '../composables/useNamedWorkspaces'
50+
51+
const {t} = useI18n()
52+
const props = defineProps<{ rootDir: string | null; extraRoots: string[] }>()
53+
const emit = defineEmits<{ open: [ws: NamedWorkspace]; close: [] }>()
54+
55+
const {workspaces, save, remove} = useNamedWorkspaces()
56+
const name = ref('')
57+
58+
const folderName = (p: string) => p.split(/[\\/]/).filter(Boolean).pop() || p
59+
60+
const saveCurrent = () => {
61+
if (!props.rootDir || !name.value.trim()) {
62+
return
63+
}
64+
save(name.value, props.rootDir, props.extraRoots)
65+
name.value = ''
66+
}
67+
</script>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// 命名工作区:把一组根(主根 + 额外挂载根)存为命名条目,便于切换。
2+
import {ref} from 'vue'
3+
import {kvGetJSON, kvSetJSON} from './useKvStore'
4+
5+
export interface NamedWorkspace {
6+
name: string
7+
rootDir: string
8+
extraRoots: string[]
9+
}
10+
11+
const KEY = 'named-workspaces'
12+
13+
export function useNamedWorkspaces() {
14+
const workspaces = ref<NamedWorkspace[]>(kvGetJSON<NamedWorkspace[]>(KEY, []))
15+
16+
const persist = () => kvSetJSON(KEY, workspaces.value)
17+
18+
// 保存/覆盖同名条目
19+
const save = (name: string, rootDir: string, extraRoots: string[]) => {
20+
const n = name.trim()
21+
if (!n || !rootDir) {
22+
return
23+
}
24+
const entry: NamedWorkspace = {name: n, rootDir, extraRoots: [...extraRoots]}
25+
const idx = workspaces.value.findIndex(w => w.name === n)
26+
if (idx >= 0) {
27+
workspaces.value = workspaces.value.map((w, i) => (i === idx ? entry : w))
28+
}
29+
else {
30+
workspaces.value = [...workspaces.value, entry]
31+
}
32+
persist()
33+
}
34+
35+
const remove = (name: string) => {
36+
workspaces.value = workspaces.value.filter(w => w.name !== name)
37+
persist()
38+
}
39+
40+
return {workspaces, save, remove}
41+
}

src/composables/useWorkspaceRoots.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,11 @@ export function useWorkspaceRoots(rootDir: Ref<string | null>) {
2929
persist()
3030
}
3131

32-
return {extraRoots, addWorkspaceFolder, removeWorkspaceFolder, resetExtraRoots}
32+
// 整体设置额外根(打开命名工作区时用)
33+
const setExtraRoots = (paths: string[]) => {
34+
extraRoots.value = [...paths]
35+
persist()
36+
}
37+
38+
return {extraRoots, addWorkspaceFolder, removeWorkspaceFolder, resetExtraRoots, setExtraRoots}
3339
}

src/i18n/locales/en.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,7 @@
671671
"formatOnSaveOff": "Disable format on save",
672672
"open": "Open file",
673673
"openFolder": "Open folder",
674+
"workspaces": "Named workspaces",
674675
"save": "Save file",
675676
"saveAs": "Save as",
676677
"newTab": "New tab",
@@ -822,6 +823,13 @@
822823
"noOutput": "No output",
823824
"noOutputHint": "Try running some code"
824825
},
826+
"workspaces": {
827+
"title": "Named workspaces",
828+
"namePlaceholder": "Name this workspace…",
829+
"saveCurrent": "Save current",
830+
"empty": "No saved workspaces yet",
831+
"delete": "Delete"
832+
},
825833
"sidebar": {
826834
"explorer": "Explorer",
827835
"openFolder": "Open folder",

src/i18n/locales/zh-CN.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,7 @@
671671
"formatOnSaveOff": "关闭保存时格式化",
672672
"open": "打开文件",
673673
"openFolder": "打开文件夹",
674+
"workspaces": "命名工作区",
674675
"save": "保存文件",
675676
"saveAs": "另存为",
676677
"newTab": "新建标签",
@@ -822,6 +823,13 @@
822823
"noOutput": "没有输出",
823824
"noOutputHint": "可以尝试运行一些代码"
824825
},
826+
"workspaces": {
827+
"title": "命名工作区",
828+
"namePlaceholder": "为当前工作区命名…",
829+
"saveCurrent": "保存当前",
830+
"empty": "还没有保存的工作区",
831+
"delete": "删除"
832+
},
825833
"sidebar": {
826834
"explorer": "资源管理器",
827835
"openFolder": "打开文件夹",

0 commit comments

Comments
 (0)