feat: 新增页面标签页组件

This commit is contained in:
ZhuRui
2026-07-27 16:29:12 +08:00
parent e238328919
commit 826a541a60
8 changed files with 529 additions and 47 deletions
+120
View File
@@ -0,0 +1,120 @@
import { reactive, computed, toRefs } from 'vue';
import router from '@/router';
/**
* 单个页面标签的数据结构(标签 = 已经访问过的页面)
*
* path 唯一标识,用于路由激活态判断
* name 路由组件 name,用于 <keep-alive :include="cachedViews">
* title 展示在 tab 上的文字
*/
export interface TabView {
path: string;
name: string;
title: string;
}
interface TabsState {
/** 已打开的页面标签列表,按访问顺序累加 */
tabs: TabView[];
/** 需要被 keep-alive 缓存的组件 name 列表(来自每个 tab 的 name */
cachedViews: string[];
}
const state = reactive<TabsState>({
tabs: [],
cachedViews: [],
});
/**
* 全局唯一 store 的 composable 入口
* 参考其他 store (menuStore / permissionStore) 的写法,保持代码风格统一
*/
export function useTabsStore() {
/**
* 往 tabs 列表加入一个 tab。
* - 已存在则忽略(根据 path 去重)
* - 同步把 name 加入 cachedViews,确保 <keep-alive> 能缓存该组件
*
* 通常由 router.afterEach 主动调用,所以 nav 来源 = 路由跳转。
*/
const addTab = (view: TabView): void => {
if (!view || !view.path || !view.name) return;
if (!state.tabs.some((t) => t.path === view.path)) {
state.tabs.push(view);
}
if (!state.cachedViews.includes(view.name)) {
state.cachedViews.push(view.name);
}
};
/**
* 主动(程序化地)关闭一个 tab
* - 只剩一个标签时不允许关闭(至少保留一个)
* - 关闭当前激活的 tab 时,自动跳到相邻 tab
* - 同步从 cachedViews 里移除(关闭后下次再访问会重新挂载)
*/
const removeTab = (path: string): void => {
if (state.tabs.length <= 1) return;
const idx = state.tabs.findIndex((t) => t.path === path);
if (idx === -1) return;
const target = state.tabs[idx];
const isActive = router.currentRoute.value.path === path;
const next = state.tabs[idx + 1] || state.tabs[idx - 1];
state.tabs.splice(idx, 1);
const cachedIdx = state.cachedViews.indexOf(target.name);
if (cachedIdx !== -1) state.cachedViews.splice(cachedIdx, 1);
if (isActive && next) {
router.push(next.path);
}
};
/** 关闭除 path 之外的所有 tab */
const removeOtherTabs = (path: string): void => {
state.tabs = state.tabs.filter((t) => t.path === path);
syncCachedViews();
};
/** 关闭所有 tab,保留第一个并跳转过去 */
const removeAllTabs = (): void => {
state.tabs = state.tabs.slice(0, 1);
syncCachedViews();
const first = state.tabs[0];
if (first) router.push(first.path);
};
/** 更新某个 tab 的字段(比如标题改名),按 path 定位 */
const updateTab = (path: string, partial: Partial<TabView>): void => {
const target = state.tabs.find((t) => t.path === path);
if (target) Object.assign(target, partial);
};
/** 全部清空——退出登录时调用 */
const clearTabs = (): void => {
state.tabs = [];
state.cachedViews = [];
};
/** 把 cachedViews 同步成「tabs 中仍存在的 name」 */
function syncCachedViews() {
state.cachedViews = state.cachedViews.filter((name) => state.tabs.some((t) => t.name === name));
}
return {
...toRefs(state),
tabs: computed(() => state.tabs),
cachedViews: computed(() => state.cachedViews),
addTab,
removeTab,
removeOtherTabs,
removeAllTabs,
updateTab,
clearTabs,
};
}