133 lines
4.0 KiB
JavaScript
133 lines
4.0 KiB
JavaScript
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 wrapper;
|
|
}
|