This commit is contained in:
kk
2026-07-08 18:00:33 +08:00
commit e66b4a3680
312 changed files with 54718 additions and 0 deletions
+132
View File
@@ -0,0 +1,132 @@
import { h } from 'vue';
import store from '@/store';
import website from '@/config/website';
import { isURL } from './router';
import { formatPath } from './avue-router';
/**
* 标签页键控体系
*
* 三档键控语义:
* 1. 默认:以含参数的完整地址为键,地址不同(含参数不同)即独立标签、独立缓存
* 2. meta.tabKey = 'path':以纯路径为键,同路径不同参数复用同一标签,参数原位刷新,
* 页面通过监听 $route.query 感知变化;菜单地址追加保留参数 _single=1 即启用
* 3. meta.isTab = false:不纳入标签体系,视图原样透传,由容器页自行管理
*/
/**
* 单标签模式的保留参数,仅作配置开关,不进入业务 query
*/
const SINGLE_TAB_PARAM = '_single';
/**
* 拆分地址中的参数部分
*/
export function parsePathQuery(fullPath) {
const splitIndex = fullPath.indexOf('?');
if (splitIndex === -1) return { path: fullPath, query: {} };
const query = {};
new URLSearchParams(fullPath.slice(splitIndex + 1)).forEach((value, key) => {
query[key] = value;
});
return { path: fullPath.slice(0, splitIndex), query };
}
/**
* 菜单树归一化:把配置在地址中的参数拆入 query 字段
* 组件解析与路由注册都依赖纯路径,须在 formatPath 之前执行
*/
export function normalizeMenu(menuList = []) {
const props = website.menu;
menuList.forEach(item => {
const rawPath = item[props.path];
if (rawPath && !isURL(rawPath) && rawPath.includes('?')) {
const { path, query } = parsePathQuery(rawPath);
item[props.path] = path;
item[props.query] = { ...query, ...(item[props.query] || {}) };
}
if (item[props.children] && item[props.children].length) {
normalizeMenu(item[props.children]);
}
});
}
/**
* 标签键控元数据落位:识别保留参数并写入 meta
* formatPath 会重建菜单的 meta 对象,须在其之后执行
*/
export function applyTabMeta(menuList = []) {
const props = website.menu;
menuList.forEach(item => {
const query = item[props.query];
if (query && query[SINGLE_TAB_PARAM] !== undefined) {
delete query[SINGLE_TAB_PARAM];
item.meta = item.meta || {};
item.meta.tabKey = 'path';
}
if (item[props.children] && item[props.children].length) {
applyTabMeta(item[props.children]);
}
});
}
/**
* 菜单格式化入口:归一化 → 框架格式化 → 键控元数据落位
*/
export function formatMenu(menuList = []) {
normalizeMenu(menuList);
menuList.forEach(ele => formatPath(ele, true));
applyTabMeta(menuList);
}
/**
* 路由的标签键:决定标签归属与缓存粒度,不纳入标签体系时返回 null
*/
export function tabKeyOf(route) {
const meta = route.meta || {};
if (meta.isTab === false) return null;
return meta.tabKey === 'path' ? route.path : route.fullPath;
}
/**
* 视图代理组件池:以标签键命名代理组件,使 keep-alive 的缓存实例与
* include 白名单都按标签粒度精确控制,同一页面组件开多个标签互不共享实例
*/
const wrapperMap = new Map();
let cleanupWatched = false;
/**
* 标签关闭后回收对应代理组件,避免长期驻留
*/
function watchTagCleanup() {
if (cleanupWatched) return;
cleanupWatched = true;
store.subscribe(mutation => {
if (['DEL_TAG', 'DEL_ALL_TAG', 'DEL_TAG_OTHER'].includes(mutation.type)) {
const alive = new Set(store.getters.tagList.map(tag => tag.fullPath));
wrapperMap.forEach((wrapper, key) => {
if (!alive.has(key)) wrapperMap.delete(key);
});
}
});
}
/**
* 主布局 router-view 的渲染入口
*/
export function tabView(route, Component) {
if (!Component) return Component;
const tabKey = tabKeyOf(route);
if (!tabKey) return Component;
watchTagCleanup();
let wrapper = wrapperMap.get(tabKey);
if (!wrapper) {
wrapper = {
name: tabKey,
render: () => h(Component),
};
wrapperMap.set(tabKey, wrapper);
}
return h(wrapper);
}