95 lines
3.1 KiB
TypeScript
95 lines
3.1 KiB
TypeScript
import { create } from 'zustand';
|
|
|
|
export interface Tab { id: string; path: string; pinned: boolean; scrollTop: number; }
|
|
|
|
interface TabState {
|
|
tabs: Tab[];
|
|
activeId: string | null;
|
|
openTab: (path: string) => void;
|
|
closeTab: (id: string) => void;
|
|
setActive: (id: string) => void;
|
|
togglePin: (id: string) => void;
|
|
updateScroll: (id: string, scrollTop: number) => void; // FR-T5:保存滚动位置
|
|
closeAll: () => void; // FR-V6: 切库时清空全部标签页
|
|
renamePath: (oldPath: string, newPath: string) => void; // FR-T7: 重命名/移动后同步标签页路径
|
|
activePath: () => string | null;
|
|
persist: () => void;
|
|
restore: () => void;
|
|
}
|
|
|
|
const SESSION_KEY = 'dsh-session';
|
|
|
|
export const useTabs = create<TabState>((set, get) => ({
|
|
tabs: [],
|
|
activeId: null,
|
|
|
|
openTab: (path) => {
|
|
const existing = get().tabs.find(t => t.path === path);
|
|
if (existing) { set({ activeId: existing.id }); return; }
|
|
const id = 'tab-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6);
|
|
set(s => ({ tabs: [...s.tabs, { id, path, pinned: false, scrollTop: 0 }], activeId: id }));
|
|
get().persist();
|
|
},
|
|
|
|
closeTab: (id) => {
|
|
const { tabs, activeId } = get();
|
|
const idx = tabs.findIndex(t => t.id === id);
|
|
const remaining = tabs.filter(t => t.id !== id);
|
|
let newActive = activeId;
|
|
if (activeId === id) {
|
|
// 切到相邻(优先未固定的相邻)
|
|
const neighbor = remaining[idx] ?? remaining[idx - 1] ?? remaining[remaining.length - 1];
|
|
newActive = neighbor?.id ?? null;
|
|
}
|
|
set({ tabs: remaining, activeId: newActive });
|
|
get().persist();
|
|
},
|
|
|
|
setActive: (id) => { set({ activeId: id }); get().persist(); },
|
|
|
|
// FR-T5:更新标签页滚动位置(不触发 persist,避免频繁写入)
|
|
updateScroll: (id, scrollTop) => {
|
|
set(s => ({ tabs: s.tabs.map(t => t.id === id ? { ...t, scrollTop } : t) }));
|
|
},
|
|
|
|
togglePin: (id) => {
|
|
set(s => {
|
|
const tabs = s.tabs.map(t => t.id === id ? { ...t, pinned: !t.pinned } : t);
|
|
// 固定的排前
|
|
tabs.sort((a, b) => (a.pinned === b.pinned ? 0 : a.pinned ? -1 : 1));
|
|
return { tabs };
|
|
});
|
|
get().persist();
|
|
},
|
|
|
|
closeAll: () => { set({ tabs: [], activeId: null }); get().persist(); },
|
|
|
|
renamePath: (oldPath, newPath) => {
|
|
set(s => ({
|
|
tabs: s.tabs.map(t => {
|
|
if (t.path === oldPath) return { ...t, path: newPath };
|
|
// 文件夹重命名/移动:子文档标签页路径前缀替换
|
|
if (oldPath && t.path.startsWith(oldPath + '/')) return { ...t, path: newPath + t.path.slice(oldPath.length) };
|
|
return t;
|
|
}),
|
|
}));
|
|
get().persist();
|
|
},
|
|
|
|
activePath: () => { const { tabs, activeId } = get(); return tabs.find(t => t.id === activeId)?.path ?? null; },
|
|
|
|
persist: () => {
|
|
const { tabs, activeId } = get();
|
|
try { localStorage.setItem(SESSION_KEY, JSON.stringify({ tabs, activeId })); } catch {}
|
|
},
|
|
|
|
restore: () => {
|
|
try {
|
|
const raw = localStorage.getItem(SESSION_KEY);
|
|
if (!raw) return;
|
|
const saved = JSON.parse(raw);
|
|
set({ tabs: saved.tabs ?? [], activeId: saved.activeId ?? null });
|
|
} catch {}
|
|
},
|
|
}));
|