Files
smart-go/web/stores/tab-store.ts
T
2026-04-23 18:58:13 +08:00

120 lines
2.9 KiB
TypeScript

'use client';
import { create } from 'zustand';
export type AppTab = {
id: string;
title: string;
path: string;
pinned?: boolean;
};
type TabStoreState = {
tabs: AppTab[];
activeId: string;
open: (tab: Omit<AppTab, 'id'> & { id?: string }) => void;
/** 同 path 则仅激活,否则新开 */
openOrActivate: (tab: { path: string; title: string }) => void;
/** 浏览器地址变化时同步当前页签 */
syncFromPath: (path: string) => void;
/** 切换租户:保留固定「概览」并重置页签 */
resetForTenantSwitch: () => void;
close: (id: string) => string | null;
activate: (id: string) => void;
};
let seq = 0;
const overview: AppTab = {
id: 'overview',
title: '概览',
path: '/dashboard',
pinned: true,
};
function titleFromPath(path: string): string {
const parts = path.split('/').filter(Boolean);
const last = parts[parts.length - 1];
return last ? decodeURIComponent(last) : path;
}
export const useTabStore = create<TabStoreState>((set, get) => ({
tabs: [overview],
activeId: 'overview',
open: (tab) => {
const id = tab.id ?? `t-${++seq}`;
set((s) => ({
tabs: [...s.tabs, { ...tab, id }],
activeId: id,
}));
},
openOrActivate: ({ path, title }) => {
const normalized = path.startsWith('/') ? path : `/${path}`;
set((s) => {
const hit = s.tabs.find((t) => t.path === normalized);
if (hit) {
return { activeId: hit.id };
}
const id = `t-${++seq}`;
return {
tabs: [...s.tabs, { id, path: normalized, title: title || titleFromPath(normalized) }],
activeId: id,
};
});
},
syncFromPath: (path) => {
const normalized = path.startsWith('/') ? path : `/${path}`;
if (!normalized.startsWith('/dashboard')) {
return;
}
set((s) => {
const hit = s.tabs.find((t) => t.path === normalized);
if (hit) {
return { activeId: hit.id };
}
const id = `t-${++seq}`;
return {
tabs: [
...s.tabs,
{
id,
path: normalized,
title: titleFromPath(normalized),
},
],
activeId: id,
};
});
},
resetForTenantSwitch: () => {
set({
tabs: [overview],
activeId: 'overview',
});
},
close: (id) => {
const { tabs, activeId } = get();
const t = tabs.find((x) => x.id === id);
if (!t || t.pinned) {
return null;
}
const nextTabs = tabs.filter((x) => x.id !== id);
let nextActive = activeId;
if (activeId === id) {
const idx = tabs.findIndex((x) => x.id === id);
const neighbor = tabs[idx - 1] ?? tabs[idx + 1];
nextActive = neighbor?.id ?? 'overview';
}
set({ tabs: nextTabs, activeId: nextActive });
const activeTab = get().tabs.find((x) => x.id === nextActive);
return activeTab?.path ?? '/dashboard';
},
activate: (id) => set({ activeId: id }),
}));