front/src/store/modules/tab-bar/index.ts
zzh 0791ea968a
All checks were successful
Build and Deploy Front / deploy (push) Successful in 7m38s
fix: 页签缓存名对齐、状态标签去色条,并修复订单详情嵌套字段不显示
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-28 14:15:57 +08:00

258 lines
8.1 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:访问时挪到末尾,并裁剪超量缓存。
* 必须一次性重赋值 Set,禁止先 delete 再 add:
* keep-alive 会监听 include,名字从列表里消失会立刻 prune 掉缓存实例,
* 表现为「切换标签就整页刷新」。
*/
touchCache(name: string) {
if (!isString(name) || name === '') return;
const next = new Set(
[...this.cacheTabList].filter((item) => item !== name),
);
next.add(name);
this.cacheTabList = next;
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;