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; }