front/src/store/modules/tab-bar/index.ts
zzh 3bfe6c6a26
All checks were successful
Build and Deploy Front / deploy (push) Successful in 6m30s
fix: 登录页签残留、表单校验脱节与仓库入口合并
排除登录路由进页签;FormPro 内部 model 优先;侧栏仓库选择与地址弹窗合并并可切换。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-26 12:19:47 +08:00

252 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { RouteLocationNormalized } from 'vue-router';
import { defineStore } from 'pinia';
import {
DEFAULT_ROUTE,
DEFAULT_ROUTE_NAME,
REDIRECT_ROUTE_NAME,
} from '@/router/constants';
import { isString } from '@/utils/is';
import { useRbacUserStore } from '@/store/modules/rbac-user';
import { buildMenuTitleMaps, resolveMenuTitleByPath, resolveMenuTitleFromTree } from '@/utils/menu-display';
import { translateMenuTitleByPath } from '@/utils/menu-i18n';
import { isLoginRouteName } from '@/utils/login-portal';
import i18n from '@/locale';
import type { MenuTreeDto } from '@/api/rbac';
import { TabBarState, TagProps } from './types';
/** keep-alive 同时缓存的页面上限(LRU),避免页签开太多越切越顿 */
export const MAX_KEEP_ALIVE_TABS = 10;
function resolveRouteTabTitle(route: RouteLocationNormalized): string {
const rbacUserStore = useRbacUserStore();
const { pathToTitle } = buildMenuTitleMaps(rbacUserStore.menus as MenuTreeDto[]);
const menuTitle = resolveMenuTitleByPath(route.path, pathToTitle);
if (menuTitle) {
return menuTitle;
}
const metaLocale = route.meta?.locale;
if (typeof metaLocale === 'string' && metaLocale.trim()) {
const translated = String(i18n.global.t(metaLocale.trim()));
if (translated && translated !== metaLocale.trim()) {
return translated;
}
}
const byPath = translateMenuTitleByPath(route.path);
if (byPath) {
return byPath;
}
const metaTitle = route.meta?.title;
if (typeof metaTitle === 'string' && metaTitle.trim()) {
return metaTitle.trim();
}
return String(route.name || route.path || '');
}
const formatTag = (route: RouteLocationNormalized): TagProps => {
const { name, meta, fullPath, query } = route;
return {
title: resolveRouteTabTitle(route),
name: String(name),
fullPath,
query,
ignoreCache: meta.ignoreCache,
};
};
const BAN_LIST = [REDIRECT_ROUTE_NAME, 'H5Login', 'H5AdminLogin', 'siteRoot'];
function shouldSkipTabRoute(route: RouteLocationNormalized): boolean {
const name = route.name != null ? String(route.name) : '';
if (!name) return true;
if (BAN_LIST.includes(name)) return true;
if (isLoginRouteName(name)) return true;
if (route.meta?.noAffix) return true;
if (route.meta?.requiresAuth === false && (route.path === '/login' || route.path === '/admin')) {
return true;
}
return false;
}
const useTabBarStore = defineStore('tabBar', {
state: (): TabBarState => ({
cacheTabList: new Set([DEFAULT_ROUTE_NAME]),
tagList: [DEFAULT_ROUTE],
previewingPath: '',
previewingName: '',
routeRefreshTicks: {},
}),
getters: {
getTabList(): TagProps[] {
return this.tagList;
},
getCacheList(): string[] {
return Array.from(this.cacheTabList);
},
},
actions: {
/** 超出上限时淘汰最久未访问的缓存(保留首页) */
pruneCache(exceptName?: string) {
while (this.cacheTabList.size > MAX_KEEP_ALIVE_TABS) {
let removed = false;
for (const name of this.cacheTabList) {
if (name === DEFAULT_ROUTE_NAME || name === exceptName) {
continue;
}
this.cacheTabList.delete(name);
removed = true;
break;
}
if (!removed) break;
}
},
/** LRU:访问时挪到末尾,并裁剪超量缓存 */
touchCache(name: string) {
if (!isString(name) || name === '') return;
if (this.cacheTabList.has(name)) {
this.cacheTabList.delete(name);
}
this.cacheTabList.add(name);
this.pruneCache(name);
},
updateTabList(route: RouteLocationNormalized) {
if (shouldSkipTabRoute(route)) return;
// 登录页曾被误写入时清掉,避免登录成功后仍挂着「login」页签
this.tagList = this.tagList.filter(
(tag) => !isLoginRouteName(tag.name) && tag.name !== 'H5Login' && tag.name !== 'H5AdminLogin',
);
const next = formatTag(route);
const idx = this.tagList.findIndex(
(tag) =>
tag.fullPath === next.fullPath ||
(!!next.name && tag.name === next.name),
);
// 同路由仅 query 变化(如公告 tab 的 noticeType)时更新已有页签,避免顶栏重复开签
if (idx !== -1) {
const prev = this.tagList[idx];
this.tagList[idx] = {
...prev,
fullPath: next.fullPath,
query: next.query,
title: next.title || prev.title,
ignoreCache: next.ignoreCache,
};
} else {
this.tagList.push(next);
}
if (!route.meta.ignoreCache) {
this.touchCache(String(route.name));
}
},
/** 菜单/标签点击立刻高亮,不等路由 afterEach */
previewNavigate(tag: TagProps) {
if (!tag?.fullPath && !tag?.name) return;
if (
isLoginRouteName(tag.name) ||
tag.name === 'H5Login' ||
tag.name === 'H5AdminLogin'
) {
return;
}
this.previewingPath = tag.fullPath || '';
this.previewingName = tag.name || '';
const exists = this.tagList.some(
(t) =>
(tag.fullPath && t.fullPath === tag.fullPath) ||
(tag.name && t.name === tag.name),
);
if (!exists && tag.name) {
this.tagList.push(tag);
if (!tag.ignoreCache) {
this.touchCache(tag.name);
}
}
},
clearPreview() {
if (!this.previewingPath && !this.previewingName) return;
this.previewingPath = '';
this.previewingName = '';
},
deleteTag(idx: number, tag: TagProps) {
this.tagList.splice(idx, 1);
this.cacheTabList.delete(tag.name);
},
addCache(name: string) {
if (isString(name) && name !== '') {
this.touchCache(name);
}
},
deleteCache(tag: TagProps) {
this.cacheTabList.delete(tag.name);
},
/** 顶栏刷新:仅 bump 一次 key,避免 router.replace 加/去 _r 导致页面挂载两次 */
bumpRouteRefresh(routeName: string) {
const key = String(routeName || '').trim();
if (!key) return;
const prev = this.routeRefreshTicks[key] || 0;
this.routeRefreshTicks[key] = prev + 1;
},
freshTabList(tags: TagProps[]) {
this.tagList = tags;
this.cacheTabList.clear();
this.cacheTabList.add(DEFAULT_ROUTE_NAME);
this.tagList
.filter((el) => !el.ignoreCache)
.map((el) => el.name)
.forEach((x) => {
if (x && x !== DEFAULT_ROUTE_NAME) {
this.cacheTabList.add(x);
}
});
this.pruneCache();
},
resetTabList() {
this.tagList = [DEFAULT_ROUTE];
this.cacheTabList.clear();
this.cacheTabList.add(DEFAULT_ROUTE_NAME);
this.previewingPath = '';
this.previewingName = '';
},
delVisitedRoute(path: string) {
const idx = this.tagList.findIndex(tag => tag.fullPath === path);
if (idx !== -1) {
const tag = this.tagList[idx];
this.deleteTag(idx, tag);
}
},
/** 拖动排序标签:将 fromIndex 位置的标签移动到 toIndex */
reorderTabs(fromIndex: number, toIndex: number) {
if (fromIndex === toIndex || fromIndex < 0 || toIndex < 0) return;
const list = [...this.tagList];
const [item] = list.splice(fromIndex, 1);
list.splice(toIndex, 0, item);
this.freshTabList(list);
},
/** 菜单加载或切换语言后刷新标签标题 */
refreshTagTitles() {
const rbacUserStore = useRbacUserStore();
const menus = rbacUserStore.menus as MenuTreeDto[];
this.tagList = this.tagList.map((tag) => {
const fromTree = resolveMenuTitleFromTree(menus, tag.fullPath, '');
if (fromTree) {
return { ...tag, title: fromTree };
}
const byPath = translateMenuTitleByPath(tag.fullPath);
if (byPath) {
return { ...tag, title: byPath };
}
return tag;
});
},
},
});
export default useTabBarStore;