feat: 新增页面标签页组件
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user