120 lines
2.9 KiB
TypeScript
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 }),
|
|
}));
|