2026-05-21 21:42:23 +08:00
|
|
|
|
import { computed, ref } from 'vue';
|
|
|
|
|
|
import { useDebounceFn } from '@vueuse/core';
|
|
|
|
|
|
import { type WorkbenchColumnId, type WorkbenchModuleKey, useWorkbenchModules } from './use-workbench-modules';
|
|
|
|
|
|
import { buildDefaultLayout } from './workbench-layout-default';
|
|
|
|
|
|
import type { LayoutStorage } from './layout-storage';
|
|
|
|
|
|
import { LocalStorageAdapter } from './layout-storage-local';
|
|
|
|
|
|
import { reconcileLayout } from './workbench-layout-reconcile';
|
2026-05-28 08:20:01 +08:00
|
|
|
|
import { WORKBENCH_LAYOUT_VERSION, type WorkbenchLayout } from './workbench-layout-types';
|
2026-05-21 21:42:23 +08:00
|
|
|
|
|
|
|
|
|
|
export type WorkbenchMode = 'normal' | 'editing';
|
|
|
|
|
|
|
|
|
|
|
|
interface UseWorkbenchLayoutOptions {
|
|
|
|
|
|
userId: string;
|
|
|
|
|
|
storage?: LayoutStorage;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function useWorkbenchLayout(options: UseWorkbenchLayoutOptions) {
|
|
|
|
|
|
const { getAllModules } = useWorkbenchModules();
|
|
|
|
|
|
const storage = options.storage ?? new LocalStorageAdapter();
|
|
|
|
|
|
|
|
|
|
|
|
const layout = ref<WorkbenchLayout>(buildDefaultLayout(getAllModules()));
|
|
|
|
|
|
const mode = ref<WorkbenchMode>('normal');
|
|
|
|
|
|
const dirty = ref(false);
|
|
|
|
|
|
const saving = ref(false);
|
|
|
|
|
|
const error = ref<Error | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
let snapshotBeforeEdit: WorkbenchLayout | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
async function load() {
|
|
|
|
|
|
const fromStorage = await storage.load(options.userId);
|
2026-05-28 08:20:01 +08:00
|
|
|
|
if (fromStorage && fromStorage.version === WORKBENCH_LAYOUT_VERSION) {
|
|
|
|
|
|
layout.value = reconcileLayout(fromStorage, getAllModules());
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 版本不匹配 / 无存储:走新默认布局;旧 settings 迁移过来,避免用户偏好(如 shortcut.menuKeys)被 version bump 清空
|
|
|
|
|
|
const fresh = buildDefaultLayout(getAllModules());
|
|
|
|
|
|
if (fromStorage?.settings) {
|
|
|
|
|
|
fresh.settings = { ...fromStorage.settings };
|
|
|
|
|
|
}
|
|
|
|
|
|
layout.value = fresh;
|
2026-05-21 21:42:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const persist = useDebounceFn(async () => {
|
|
|
|
|
|
saving.value = true;
|
|
|
|
|
|
error.value = null;
|
|
|
|
|
|
try {
|
|
|
|
|
|
await storage.save(options.userId, layout.value);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
error.value = err as Error;
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
saving.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 500);
|
|
|
|
|
|
|
|
|
|
|
|
function markDirty() {
|
|
|
|
|
|
if (mode.value === 'editing') {
|
|
|
|
|
|
dirty.value = true;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 非编辑态写(如折叠)直接落盘
|
|
|
|
|
|
persist();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function enterEditing() {
|
|
|
|
|
|
snapshotBeforeEdit = JSON.parse(JSON.stringify(layout.value));
|
|
|
|
|
|
mode.value = 'editing';
|
|
|
|
|
|
dirty.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function saveEditing() {
|
|
|
|
|
|
saving.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
await storage.save(options.userId, layout.value);
|
|
|
|
|
|
mode.value = 'normal';
|
|
|
|
|
|
dirty.value = false;
|
|
|
|
|
|
snapshotBeforeEdit = null;
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
error.value = err as Error;
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
saving.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function cancelEditing() {
|
|
|
|
|
|
if (snapshotBeforeEdit) {
|
|
|
|
|
|
layout.value = snapshotBeforeEdit;
|
|
|
|
|
|
}
|
|
|
|
|
|
mode.value = 'normal';
|
|
|
|
|
|
dirty.value = false;
|
|
|
|
|
|
snapshotBeforeEdit = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function hideModule(key: WorkbenchModuleKey) {
|
|
|
|
|
|
for (const col of layout.value.columns) {
|
|
|
|
|
|
col.modules = col.modules.filter(k => k !== key);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!layout.value.hidden.includes(key)) layout.value.hidden.push(key);
|
|
|
|
|
|
markDirty();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function showModule(key: WorkbenchModuleKey, columnId: WorkbenchColumnId = 'left') {
|
|
|
|
|
|
layout.value.hidden = layout.value.hidden.filter(k => k !== key);
|
|
|
|
|
|
const target = layout.value.columns.find(c => c.id === columnId);
|
|
|
|
|
|
if (target && !target.modules.includes(key)) target.modules.push(key);
|
|
|
|
|
|
markDirty();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function setColumnModules(columnId: WorkbenchColumnId, modules: WorkbenchModuleKey[]) {
|
|
|
|
|
|
const target = layout.value.columns.find(c => c.id === columnId);
|
|
|
|
|
|
if (target) target.modules = modules;
|
|
|
|
|
|
markDirty();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toggleCollapse(key: WorkbenchModuleKey) {
|
|
|
|
|
|
if (layout.value.collapsed.includes(key)) {
|
|
|
|
|
|
layout.value.collapsed = layout.value.collapsed.filter(k => k !== key);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
layout.value.collapsed.push(key);
|
|
|
|
|
|
}
|
|
|
|
|
|
markDirty();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function updateModuleSettings<K extends keyof WorkbenchLayout['settings']>(
|
|
|
|
|
|
key: K,
|
|
|
|
|
|
value: WorkbenchLayout['settings'][K]
|
|
|
|
|
|
) {
|
|
|
|
|
|
layout.value.settings = { ...layout.value.settings, [key]: value };
|
|
|
|
|
|
markDirty();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function resetToDefault() {
|
|
|
|
|
|
layout.value = buildDefaultLayout(getAllModules());
|
|
|
|
|
|
mode.value = 'normal';
|
|
|
|
|
|
dirty.value = false;
|
|
|
|
|
|
snapshotBeforeEdit = null;
|
|
|
|
|
|
await storage.save(options.userId, layout.value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const isCollapsed = (key: WorkbenchModuleKey) => layout.value.collapsed.includes(key);
|
|
|
|
|
|
|
|
|
|
|
|
const hiddenMetas = computed(() => {
|
|
|
|
|
|
const allMeta = getAllModules();
|
|
|
|
|
|
return layout.value.hidden
|
|
|
|
|
|
.map(k => allMeta.find(m => m.key === k))
|
|
|
|
|
|
.filter((m): m is NonNullable<typeof m> => Boolean(m));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
layout,
|
|
|
|
|
|
mode,
|
|
|
|
|
|
dirty,
|
|
|
|
|
|
saving,
|
|
|
|
|
|
error,
|
|
|
|
|
|
hiddenMetas,
|
|
|
|
|
|
isCollapsed,
|
|
|
|
|
|
load,
|
|
|
|
|
|
enterEditing,
|
|
|
|
|
|
saveEditing,
|
|
|
|
|
|
cancelEditing,
|
|
|
|
|
|
hideModule,
|
|
|
|
|
|
showModule,
|
|
|
|
|
|
setColumnModules,
|
|
|
|
|
|
toggleCollapse,
|
|
|
|
|
|
updateModuleSettings,
|
|
|
|
|
|
resetToDefault
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|