168 lines
4.9 KiB
TypeScript
168 lines
4.9 KiB
TypeScript
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';
|
||
import { WORKBENCH_LAYOUT_VERSION, type WorkbenchLayout } from './workbench-layout-types';
|
||
|
||
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);
|
||
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;
|
||
}
|
||
|
||
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
|
||
};
|
||
}
|