1. 抽离底部导航归属逻辑为纯函数resolveEntryTab,优化分包页面高亮规则,对齐iOS HIG规范 2. 新增页面Tab通配符映射,简化PAGE_TAB_MAP维护 3. 修复鸿蒙平台webview返回按钮适配问题 4. 新增pages.json校验与导航标题自动注入脚本 5. 优化启动页动画性能,提升合成渲染效率 6. 修复diy-picture组件v-for缺少key的警告 7. 清理鸿蒙平台多余的分包构建配置 8. 更新统计数据与store注释说明
650 lines
25 KiB
JavaScript
650 lines
25 KiB
JavaScript
/**
|
|
* util-tabbar.js
|
|
*
|
|
* TabBar / 子包路径相关工具集 — 从 util.js 拆分出来
|
|
*
|
|
* 包含的函数(全部服务于 redirectTo / diyRedirectTo 的 tabBar 命中判断):
|
|
* - resolvePageUrl : 根据 page_key + params 解析得到实际跳转 URL
|
|
* - isPageInRouteList : 判断当前路由是否命中 page_routes 列表
|
|
* - resolveLinkUrl : 解析 link 对象得到最终 URL(合并 wap_url query 进 page_params)
|
|
* - systemTabBarList : 当前最新的 tabBar.list 配置
|
|
* - adaptSubpackageUrl : 子包路径前缀映射(/pages/order/ → /pages_order/)
|
|
* - _normalizePath : 归一化任意路径(私有,内部用)
|
|
* - isTabBarRootPage : 判断给定路径是否是 TabBar 根页面
|
|
* - isCurrentPageTabBarRoot : 判断**当前**页面是否是 TabBar 根页面(依赖 getCurrentPages/store)
|
|
* - checkTabBarActive : 检查 tabBar 是否激活
|
|
*
|
|
* 设计原则:
|
|
* 1. 不依赖 util.js,避免循环依赖
|
|
* 2. 纯函数为主(isCurrentPageTabBarRoot 除外,它依赖 getCurrentPages 和 store,
|
|
* 业务方在调用时手动注入 store 即可)
|
|
* 3. PAGE_KEY_MAP / NO_TAB_PAGES / PAGE_TAB_MAP / TAB_BAR_KEYS 仍然在 util.js(业务配置)
|
|
* util-tabbar.js 接受外部传入,提高复用性
|
|
*/
|
|
|
|
import { _toParamValueString } from './util-navigate.js';
|
|
|
|
// =============================================================
|
|
// PAGE_KEY → 实际 URL 解析
|
|
// =============================================================
|
|
|
|
/**
|
|
* 根据 page_key + params 解析得到实际跳转 URL
|
|
* @param {string} page_key 页面语义常量, 如 PAGE_GOODS_LIST
|
|
* @param {Object} params 页面参数(JSON 对象)
|
|
* @param {Object} [pageKeyMap={}] page_key → 实际路径的映射表(由调用方注入,通常是 PAGE_KEY_MAP)
|
|
* @returns {string|null} 实际 URL(已拼接参数)或 null(未匹配)
|
|
*/
|
|
export function resolvePageUrl(page_key, params = {}, pageKeyMap = {}) {
|
|
if (!page_key || !pageKeyMap[page_key]) {
|
|
return null;
|
|
}
|
|
let url = pageKeyMap[page_key];
|
|
if (params && typeof params === 'object' && Object.keys(params).length > 0) {
|
|
const parts = [];
|
|
Object.keys(params).forEach(function (key) {
|
|
const value = params[key];
|
|
if (value === null || value === undefined || value === '') return;
|
|
// 对象/数组走 JSON.stringify, 避免 String({}) 退化成 "[object Object]"
|
|
// 消费端 resolveLinkUrl 已用 JSON.parse 还原
|
|
const encodedValue = _toParamValueString(value);
|
|
// H5: 浏览器不自动编码, 需要手动 encodeURIComponent
|
|
// 小程序 / App: uni.navigateTo 会自动编码, 这里不能再 encode, 否则双重编码
|
|
// #ifdef H5
|
|
parts.push(key + '=' + encodeURIComponent(encodedValue));
|
|
// #endif
|
|
// #ifndef H5
|
|
parts.push(key + '=' + encodedValue);
|
|
// #endif
|
|
});
|
|
if (parts.length > 0) {
|
|
url = url + (url.indexOf('?') !== -1 ? '&' : '?') + parts.join('&');
|
|
}
|
|
}
|
|
return url;
|
|
}
|
|
|
|
/**
|
|
* 判断当前路由是否命中 page_routes 列表
|
|
* 支持 3 种写法, 可混合使用:
|
|
* 1) PAGE_KEY_MAP 的 key → 如 "PAGE_INDEX"
|
|
* 2) 实际页面路径 → 如 "/pages/index/index"
|
|
* 3) 通配符路径 → 如 "/pages_tool/*" 或 "PAGE_ORDER_*"
|
|
*
|
|
* @param {string} currentRoute 当前页面路径(会自动补 / 前缀)
|
|
* @param {string[]} routeList 后台配置的 page_routes 数组
|
|
* @param {Object} [pageKeyMap={}] PAGE_KEY_MAP
|
|
* @returns {boolean}
|
|
*/
|
|
export function isPageInRouteList(currentRoute, routeList, pageKeyMap = {}) {
|
|
if (!currentRoute || !routeList || !Array.isArray(routeList) || routeList.length === 0) {
|
|
return false;
|
|
}
|
|
const route = currentRoute.startsWith('/') ? currentRoute : '/' + currentRoute;
|
|
|
|
for (const item of routeList) {
|
|
if (!item || typeof item !== 'string') continue;
|
|
const entry = item.trim();
|
|
if (!entry) continue;
|
|
|
|
// 1) 通配符 /pages_tool/* 或 PAGE_ORDER_*
|
|
if (entry.endsWith('/*')) {
|
|
const prefix = entry.slice(0, -1);
|
|
if (route.startsWith(prefix)) return true;
|
|
continue;
|
|
}
|
|
|
|
if (entry.endsWith('_*') && pageKeyMap) {
|
|
const keyPrefix = entry.slice(0, -1);
|
|
const matchedPaths = [];
|
|
for (const k of Object.keys(pageKeyMap)) {
|
|
if (k.startsWith(keyPrefix) && pageKeyMap[k]) {
|
|
matchedPaths.push(pageKeyMap[k]);
|
|
}
|
|
}
|
|
for (const p of matchedPaths) {
|
|
const pn = p.startsWith('/') ? p : '/' + p;
|
|
const dirPrefix = pn.substring(0, pn.lastIndexOf('/') + 1);
|
|
if (route.startsWith(dirPrefix)) return true;
|
|
if (route === pn) return true;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// 2) PAGE_KEY 名称
|
|
if (pageKeyMap && pageKeyMap[entry]) {
|
|
let mapped = pageKeyMap[entry];
|
|
mapped = mapped.startsWith('/') ? mapped : '/' + mapped;
|
|
if (route === mapped) return true;
|
|
continue;
|
|
}
|
|
|
|
// 3) 实际路径
|
|
if (entry.startsWith('/')) {
|
|
if (route === entry) return true;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// =============================================================
|
|
// resolveLinkUrl
|
|
// =============================================================
|
|
|
|
/**
|
|
* 解析 link 对象得到最终可跳转的 URL
|
|
* 同时把 wap_url 自带的 query 参数与 page_params 合并, 写入 link.page_params;
|
|
* 并把 link.wap_url 改为不含 query 的 path, 避免后续 redirectTo 重复附加
|
|
* 优先级: page_params > wap_url query (新机制优先)
|
|
*
|
|
* @param {Object} link
|
|
* @param {Object} [opts]
|
|
* @param {Object} [opts.pageKeyMap={}] PAGE_KEY_MAP
|
|
* @param {Function} [opts.parsePageParams] page_params 解析函数(默认用 util-navigate 的)
|
|
* @param {Function} [opts.resolvePageUrlFn] page_key → URL 函数(默认用本文件内)
|
|
* @param {Function} [opts.decodeEntity] HTML 实体解码函数
|
|
* @returns {string|null}
|
|
*/
|
|
export function resolveLinkUrl(link, opts = {}) {
|
|
const {
|
|
pageKeyMap = {},
|
|
parsePageParams = null,
|
|
resolvePageUrlFn = null,
|
|
decodeEntity = null,
|
|
} = opts;
|
|
|
|
if (!link) return null;
|
|
|
|
// 0. 把 wap_url 中的 query 合并到 page_params
|
|
if (link.wap_url) {
|
|
const _decode = decodeEntity || ((s) => s);
|
|
const decodedWap = _decode(link.wap_url);
|
|
const queryIdx = decodedWap.indexOf('?');
|
|
if (queryIdx !== -1) {
|
|
const path = decodedWap.substring(0, queryIdx);
|
|
const queryStr = decodedWap.substring(queryIdx + 1);
|
|
const urlParams = {};
|
|
queryStr.split('&').forEach((kv) => {
|
|
if (!kv) return;
|
|
const eqIdx = kv.indexOf('=');
|
|
const k = eqIdx === -1 ? kv : kv.substring(0, eqIdx);
|
|
const v = eqIdx === -1 ? '' : kv.substring(eqIdx + 1);
|
|
if (k) urlParams[k] = v;
|
|
});
|
|
// 解析 page_params (可能是字符串或对象) — 走统一 helper
|
|
const _parse = parsePageParams || ((p) => (p && typeof p === 'object') ? p : {});
|
|
const pageParams = _parse(link.page_params);
|
|
// 合并: page_params 覆盖 urlParams (新机制优先)
|
|
link.page_params = Object.assign({}, urlParams, pageParams);
|
|
// 把 link.wap_url 改为不含 query 的 path
|
|
link.wap_url = path;
|
|
}
|
|
}
|
|
// 1. 优先使用 page_key(新机制)
|
|
if (link.page_key) {
|
|
const _parse = parsePageParams || ((p) => (p && typeof p === 'object') ? p : {});
|
|
const _resolvePageUrl = resolvePageUrlFn || ((key, params) => resolvePageUrl(key, params, pageKeyMap));
|
|
const params = _parse(link.page_params);
|
|
const url = _resolvePageUrl(link.page_key, params);
|
|
if (url) return url;
|
|
}
|
|
// 2. 回退: 旧的 wap_url(已被处理为不含 query 的 path)
|
|
if (link.wap_url) {
|
|
return link.wap_url;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// =============================================================
|
|
// TabBar 列表与适配
|
|
// =============================================================
|
|
|
|
/**
|
|
* 当前最新的 tabBar.list
|
|
* 注意: 该函数需要业务方传入 4 个常量(INDEX/CATEGORY/CONTACT/MEMBER)
|
|
* 通常由 util.js 在 import 后调用 _setSystemTabBarList 注入
|
|
*/
|
|
export function makeSystemTabBarList(indexUrl, categoryUrl, contactUrl, memberUrl) {
|
|
return [indexUrl, categoryUrl, contactUrl, memberUrl];
|
|
}
|
|
|
|
/**
|
|
* 适配子包路径
|
|
* 把老路径 /pages/xxx/ 映射到新路径 /pages_xxx/
|
|
*
|
|
* @param {string} url
|
|
* @returns {string}
|
|
*/
|
|
export function adaptSubpackageUrl(url) {
|
|
if (!url) return url;
|
|
// 定义前缀映射表(key=旧前缀, value=新分包前缀)
|
|
const urlPrefixMap = {
|
|
'/pages/promotion/': '/pages_promotion/',
|
|
'/pages/order/': '/pages_order/',
|
|
'/pages/goods/': '/pages_goods/',
|
|
'/pages/member/': '/pages_tool/member/',
|
|
'/pages/login/': '/pages_tool/login/',
|
|
'/pages/agreement/': '/pages_tool/agreement/',
|
|
'/pages/article/': '/pages_tool/article/',
|
|
'/pages/cases/': '/pages_tool/cases/',
|
|
'/pages/contact/': '/pages_tool/contact/',
|
|
'/pages/files/': '/pages_tool/files/',
|
|
'/pages/form/': '/pages_tool/form/',
|
|
'/pages/help/': '/pages_tool/help/',
|
|
'/pages/notice/': '/pages_tool/notice/',
|
|
'/pages/pay/': '/pages_tool/pay/',
|
|
'/pages/recharge/': '/pages_tool/recharge/',
|
|
'/pages/seal/': '/pages_tool/seal/',
|
|
'/pages/storeclose/': '/pages_tool/storeclose/',
|
|
'/pages/vr/': '/pages_tool/vr/',
|
|
'/pages/weapp/': '/pages_tool/weapp/',
|
|
'/pages/webview/': '/pages_tool/webview/',
|
|
};
|
|
|
|
const prefixes = Object.keys(urlPrefixMap);
|
|
if (prefixes.length === 0) return url;
|
|
|
|
const regex = new RegExp(
|
|
'^(' + prefixes.map((key) =>
|
|
key.replace(/\//g, '\\/').replace(/\*/g, '.*'),
|
|
).join('|') + ')',
|
|
);
|
|
|
|
function replacePrefix(str) {
|
|
return str.replace(regex, (match) => {
|
|
const newPrefix = urlPrefixMap[match];
|
|
return newPrefix || match;
|
|
});
|
|
}
|
|
|
|
// 兜底: 把 3 个及以上的连续 `/` 收敛为 2 个, 只动 path 部分
|
|
const collapsed = url.replace(/([^:])\/{3,}/g, '$1//');
|
|
return replacePrefix(collapsed);
|
|
}
|
|
|
|
// =============================================================
|
|
// TabBar 命中判断(私有辅助)
|
|
// =============================================================
|
|
|
|
/**
|
|
* 归一化任意路径: 确保以 '/' 开头, 去除 query, 适配子包
|
|
* (私有, 仅本模块内部使用)
|
|
*/
|
|
function _normalizePath(path) {
|
|
if (!path) return '';
|
|
const normalized = path.startsWith('/') ? path : '/' + path;
|
|
return adaptSubpackageUrl(normalized).split('?')[0];
|
|
}
|
|
|
|
/**
|
|
* 从归一化路径提取分包名(parts[1])
|
|
* (私有, 仅本模块内部使用)
|
|
*/
|
|
function _getPackageName(normalizedPath) {
|
|
const parts = normalizedPath.split('/');
|
|
return parts[1] || '';
|
|
}
|
|
|
|
/**
|
|
* 解析"当前分包的入口 tab URL"
|
|
*
|
|
* 把 diy-bottom-nav.vue#updateCurrentRoute 里的查找逻辑抽成纯函数,
|
|
* 方便单元测试覆盖所有跳转场景, 同时让组件代码变薄.
|
|
*
|
|
* 查找优先级 (2026-07-16 重构, 对齐 iOS HIG):
|
|
* 1) 当前页面精确命中某个 tab 根页面 → 它就是入口 (tab→tab 跳转)
|
|
* 2) 同包内导航 (上一包 == 当前包) → 用当前包的第一个 tab
|
|
* 例: /pages_goods/list → /pages_goods/detail → CATEGORY 高亮
|
|
* 3) 跨包导航 (上一包 != 当前包) → 历史栈倒序查找最近一次访问的 TabBar 页
|
|
* 例: home → diy → HOME 高亮 (从哪个 tab 出发, 非 tab 页就归属哪个 tab)
|
|
* 4) 跨包但历史栈中无 tab → 退到当前包第一个 tab
|
|
* 5) 深链无历史 → 返回 null (iOS HIG: 无来源时不点亮)
|
|
*
|
|
* 与 checkTabBarActive 的关系:
|
|
* - resolveEntryTab 写入 packageEntryTab[currentPackage]
|
|
* - checkTabBarActive 通过 _findMatchingTabUrl 读取
|
|
* - 两者都不会做"无历史时的同分包兜底", 保证 iOS HIG 行为
|
|
*
|
|
* @param {Object} opts
|
|
* @param {string} opts.currentPageRoute 当前页面路由 (已 _normalizeRoute, 不含 query)
|
|
* @param {Object} opts.tabBarList tabBar 配置对象 ($store.state.tabBarList)
|
|
* @param {string[]} [opts.historyRoutes=[]] 历史栈路由列表 (不含 query, 不含当前页;
|
|
* 已 _normalizeRoute 但未 adaptSubpackageUrl, 函数内部会适配)
|
|
* @param {Set<string>} [opts.noTabPages] NO_TAB_PAGES 集合 (含 login/pay/AI 等全屏流程页)
|
|
* 这些页视为"modal", 不参与包归属判定
|
|
* @returns {string|null} 入口 tab URL (已适配子包路径、不含 query) 或 null
|
|
*/
|
|
export function resolveEntryTab(opts = {}) {
|
|
const {
|
|
currentPageRoute = '',
|
|
tabBarList = null,
|
|
historyRoutes = [],
|
|
noTabPages = null,
|
|
} = opts;
|
|
|
|
if (!currentPageRoute || !tabBarList || !Array.isArray(tabBarList.list)) {
|
|
return null;
|
|
}
|
|
|
|
const tabUrls = tabBarList.list
|
|
.filter((item) => item && item.link && item.link.wap_url)
|
|
.map((item) => adaptSubpackageUrl(item.link.wap_url).split('?')[0]);
|
|
|
|
if (tabUrls.length === 0) return null;
|
|
|
|
const normalizedCurrent = currentPageRoute.split('?')[0];
|
|
const currentPackageName = _getPackageName(normalizedCurrent);
|
|
|
|
// 1) 当前页面精确命中某个 tab → 它就是入口
|
|
const exactMatch = tabUrls.find((url) => url === normalizedCurrent);
|
|
if (exactMatch) return exactMatch;
|
|
|
|
// 工具: 判定一个路由是否"参与归属判定"
|
|
// 跳过 null/空 + NO_TAB_PAGES (login/pay/AI 等 modal 页, 不算中间步骤)
|
|
const isSkippable = (route) => {
|
|
if (!route) return true;
|
|
if (noTabPages && typeof noTabPages.has === 'function') {
|
|
const normalized = adaptSubpackageUrl(route).split('?')[0];
|
|
if (noTabPages.has(normalized)) return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
// 找出"最近的上一页"属于哪个包 (跳过 null/空 + NO_TAB_PAGES)
|
|
let lastPackageName = '';
|
|
let hasPrevPage = false;
|
|
if (Array.isArray(historyRoutes) && historyRoutes.length > 0) {
|
|
for (let i = historyRoutes.length - 1; i >= 0; i--) {
|
|
const route = historyRoutes[i];
|
|
if (isSkippable(route)) continue;
|
|
lastPackageName = _getPackageName(adaptSubpackageUrl(route).split('?')[0]);
|
|
hasPrevPage = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
const isSamePackage = hasPrevPage && lastPackageName === currentPackageName;
|
|
const isCrossPackage = hasPrevPage && lastPackageName !== currentPackageName;
|
|
|
|
// 2) 同包内导航: 用当前包的第一个 tab
|
|
// 业务约定: 包内"主入口" = tabBarList 中该包出现的第一个 tab
|
|
// 例: pages_goods 有 [CATEGORY, CART] 两个 tab, 用户在 goods 频道内任何页面都归 CATEGORY
|
|
// 理由: CATEGORY 是商品频道的入口, 比 CART (特定子场景) 更"通用", 视觉一致
|
|
// 注意: 业务侧 tab 配置顺序很关键, 若有 pages_goods 出现 3+ tab, 主入口必须是业务上正确的那个
|
|
if (isSamePackage) {
|
|
const samePackage = tabUrls.find((url) => _getPackageName(url) === currentPackageName);
|
|
if (samePackage) return samePackage;
|
|
}
|
|
|
|
// 3) 跨包导航: 历史栈倒序查找最近一次访问的 TabBar 页 → 来源 tab
|
|
// 规则: 从哪个 TabBar 出发, 非 tab 页就归属哪个 TabBar 高亮
|
|
// 注意: 跳过 NO_TAB_PAGES, 因为 login/pay 等是 modal, 不是真实的导航步骤
|
|
if (isCrossPackage) {
|
|
for (let i = historyRoutes.length - 1; i >= 0; i--) {
|
|
const route = historyRoutes[i];
|
|
if (isSkippable(route)) continue;
|
|
const normalized = adaptSubpackageUrl(route).split('?')[0];
|
|
const exactTab = tabUrls.find((url) => url === normalized);
|
|
if (exactTab) return exactTab;
|
|
}
|
|
// 4) 跨包但历史栈中无 tab → 退到当前包第一个 tab
|
|
const samePackage = tabUrls.find((url) => _getPackageName(url) === currentPackageName);
|
|
if (samePackage) return samePackage;
|
|
}
|
|
|
|
// 5) 深链无历史 / 跳过 NO_TAB_PAGES 后没找到 → 返回 null
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 给定当前页面路径 + tab 配置, 返回当前页面所属的 tab URL
|
|
* 优先级: 精确匹配 > packageEntryTab
|
|
*
|
|
* 注意 (2026-07-16): 旧版本在 packageEntryTab 没命中时会回退到"同分包第一个 tab",
|
|
* 这导致深链冷启动时 /pages_tool/* 页被错误标成"我的"。
|
|
* 现在改为"找不到就返回 null", 后续 checkTabBarActive 也不会再兜底,
|
|
* 行为对齐 iOS HIG (深链无历史时, TabBar 不点亮)。
|
|
*
|
|
* (私有)
|
|
*/
|
|
function _findMatchingTabUrl(currentPagePath, tabBarList, packageEntryTab) {
|
|
if (!tabBarList || !tabBarList.list || !Array.isArray(tabBarList.list)) return null;
|
|
|
|
const tabUrls = tabBarList.list
|
|
.filter((item) => item.link && item.link.wap_url)
|
|
.map((item) => _normalizePath(item.link.wap_url));
|
|
|
|
const normalizedCurrent = _normalizePath(currentPagePath);
|
|
|
|
// 1) 精确匹配 → 当前页面就是 tab 页面
|
|
const exactMatch = tabUrls.find((url) => url === normalizedCurrent);
|
|
if (exactMatch) return exactMatch;
|
|
|
|
// 2) 通过 packageEntryTab(由 updateCurrentRoute/redirectTo 写入)
|
|
const packageName = _getPackageName(normalizedCurrent);
|
|
if (packageEntryTab && typeof packageEntryTab === 'object' && packageEntryTab[packageName]) {
|
|
const entryTab = packageEntryTab[packageName];
|
|
const normalizedEntry = _normalizePath(entryTab);
|
|
if (tabUrls.includes(normalizedEntry)) return normalizedEntry;
|
|
}
|
|
|
|
// 3) 不再做"同分包兜底" — 找不到就返回 null
|
|
// 深链冷启动场景下无历史可参考, iOS HIG 推荐 TabBar 不点亮
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 通配符路径匹配 — 支持 '/pages_tool/member/*' 形式
|
|
*
|
|
* 规则:
|
|
* - pattern 必须以 '/*' 结尾
|
|
* - '*' 匹配"前缀之后的至少一个字符段"
|
|
* - 例: '/pages_tool/member/*' 匹配 '/pages_tool/member/info' 但不匹配 '/pages_tool/member'
|
|
*
|
|
* @param {string} path 当前路径 (已 _normalizePath)
|
|
* @param {string} pattern 通配符 pattern
|
|
* @returns {boolean}
|
|
*/
|
|
function _matchWildcardPath(path, pattern) {
|
|
if (!path || !pattern) return false;
|
|
if (!pattern.endsWith('/*')) return false;
|
|
const prefix = pattern.slice(0, -2); // 去掉 '/*'
|
|
// 必须有至少一个字符在 '/*' 之后, 即 path 比 prefix 长, 且以 prefix + '/' 开头
|
|
return path.length > prefix.length + 1 && path.startsWith(prefix + '/');
|
|
}
|
|
|
|
/**
|
|
* 在 PAGE_TAB_MAP (精确) + PAGE_TAB_WILDCARDS (通配) 中查找当前页面对应的 tab
|
|
*
|
|
* 优先级: 精确匹配 > 通配符匹配
|
|
* 返回值可以是:
|
|
* - tab URL 字符串 → 命中, 高亮该 tab
|
|
* - null → 显式声明"无 tab", 一定不高亮
|
|
* - undefined → 未声明, 调用方走兜底逻辑 (history-walk / 无 tab)
|
|
*
|
|
* @param {string} currentPath 当前页面路径 (已 _normalizePath)
|
|
* @param {Object} pageTabMap 精确绑定表 (PAGE_TAB_MAP)
|
|
* @param {Array<{pattern: string, tab: string}>} pageTabWildcards 通配符列表 (PAGE_TAB_WILDCARDS)
|
|
* @returns {string|null|undefined}
|
|
*/
|
|
export function lookupPageTabMap(currentPath, pageTabMap, pageTabWildcards) {
|
|
if (!currentPath) return undefined;
|
|
|
|
// 1) 精确匹配
|
|
if (pageTabMap && Object.prototype.hasOwnProperty.call(pageTabMap, currentPath)) {
|
|
return pageTabMap[currentPath];
|
|
}
|
|
|
|
// 2) 通配符匹配
|
|
if (Array.isArray(pageTabWildcards)) {
|
|
for (const entry of pageTabWildcards) {
|
|
if (entry && entry.pattern && _matchWildcardPath(currentPath, entry.pattern)) {
|
|
return entry.tab;
|
|
}
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* 判断给定路径是否是 TabBar 根页面(与 diy_bottom_nav 中任意一项精确相等)
|
|
*
|
|
* @param {string} pagePath 当前页面路径(可以带/不带前导斜杠、可以带 query)
|
|
* @param {Object|null} tabBarList tabBar 配置对象($store.state.tabBarList)
|
|
* @returns {boolean} 如果是 TabBar 根页面返回 true
|
|
*/
|
|
export function isTabBarRootPage(pagePath, tabBarList) {
|
|
if (!pagePath) return false;
|
|
if (!tabBarList || !tabBarList.list || !Array.isArray(tabBarList.list)) return false;
|
|
const normalizedCurrent = _normalizePath(pagePath);
|
|
if (!normalizedCurrent) return false;
|
|
return tabBarList.list.some((item) => {
|
|
if (!item || !item.link || !item.link.wap_url) return false;
|
|
const normalizedTab = _normalizePath(item.link.wap_url);
|
|
return normalizedTab === normalizedCurrent;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 判断**当前**页面是否是 TabBar 根页面
|
|
* 封装了 getCurrentPages() + isTabBarRootPage 的组合, 供业务组件在弹窗/操作前判断
|
|
*
|
|
* 调用方需要注入 getCurrentPages 和 store:
|
|
* isCurrentPageTabBarRoot({
|
|
* getCurrentPages: typeof getCurrentPages === 'function' ? getCurrentPages : null,
|
|
* store,
|
|
* tabBarList: null, // 不传则从 store.state.tabBarList 取
|
|
* })
|
|
*
|
|
* @param {Object} [opts={}]
|
|
* @param {Function} [opts.getCurrentPages]
|
|
* @param {Object} [opts.store]
|
|
* @param {Object} [opts.tabBarList]
|
|
* @returns {boolean}
|
|
*/
|
|
export function isCurrentPageTabBarRoot(opts = {}) {
|
|
try {
|
|
const gcp = opts.getCurrentPages || (typeof getCurrentPages === 'function' ? getCurrentPages : null);
|
|
if (!gcp) return false;
|
|
const pages = gcp();
|
|
if (!pages || !pages.length) return false;
|
|
const currentRoute = '/' + String(pages[pages.length - 1].route || '').replace(/^\//, '');
|
|
if (!currentRoute) return false;
|
|
const tabBarList = opts.tabBarList
|
|
|| (opts.store && opts.store.state ? opts.store.state.tabBarList : null);
|
|
return isTabBarRootPage(currentRoute, tabBarList);
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 检查 tabBar 是否激活
|
|
*
|
|
* @param {string} linkUrl 链接路径 /pages/goods/category
|
|
* @param {string} currentPageRoute 当前页面路径 /pages_goods/category
|
|
* @param {Object} tabBarList tabBar 配置对象, 用于动态判断
|
|
* @param {Object} packageEntryTab 追踪每个分包的入口 Tab { 'pages_goods': '/pages_goods/category' }
|
|
* @param {Object} [currentPageQuery={}] 当前页面 query 参数, 用于读取 from_tab 来源记忆
|
|
* @param {Object} [opts={}]
|
|
* @param {Set} [opts.noTabPages] 独立流程页集合(NO_TAB_PAGES)
|
|
* @param {Object} [opts.pageTabMap] 页面级 Tab 精确绑定表(PAGE_TAB_MAP)
|
|
* @param {Array<{pattern: string, tab: string}>} [opts.pageTabWildcards=[]] 页面级 Tab 通配符表(PAGE_TAB_WILDCARDS)
|
|
* @returns {boolean} 是否激活
|
|
*/
|
|
export function checkTabBarActive(
|
|
linkUrl,
|
|
currentPageRoute,
|
|
tabBarList,
|
|
packageEntryTab,
|
|
currentPageQuery = {},
|
|
opts = {},
|
|
) {
|
|
const { noTabPages = null, pageTabMap = {}, pageTabWildcards = [] } = opts;
|
|
|
|
// __TABBAR_DEBUG__: 开启后打印每一步决策, 用于排查"为什么我的 tab 不亮"
|
|
// 开启方式 (任选一种):
|
|
// - 全局: window.__TABBAR_DEBUG__ = true
|
|
// - 本次: opts.debug = true
|
|
const DEBUG = (typeof globalThis !== 'undefined' && globalThis.__TABBAR_DEBUG__)
|
|
|| opts.debug === true;
|
|
const _log = DEBUG ? (...a) => console.log('[TABBAR]', ...a) : () => {};
|
|
_log('checkTabBarActive called', { linkUrl, currentPageRoute, hasTabBarList: !!tabBarList });
|
|
|
|
// 1) 归一化 linkUrl: 统一前导 '/' + 子包路径适配 + 去除 query
|
|
const normalizedLink = _normalizePath(linkUrl);
|
|
const normalizedCurrent = _normalizePath(currentPageRoute);
|
|
|
|
if (!normalizedLink) {
|
|
_log('step 1: no linkUrl → false');
|
|
return false;
|
|
}
|
|
|
|
// 2) 精确匹配: 当前页面就是 linkUrl 指向的页面
|
|
if (normalizedLink === normalizedCurrent) {
|
|
_log('step 2: exact match → true', { link: normalizedLink });
|
|
return true;
|
|
}
|
|
|
|
// 3) 独立流程页: 不点亮任何 Tab
|
|
if (noTabPages && noTabPages.has && noTabPages.has(normalizedCurrent)) {
|
|
_log('step 3: NO_TAB_PAGES hit → false', { current: normalizedCurrent });
|
|
return false;
|
|
}
|
|
|
|
// 4) 若传入 tabBarList, 使用动态判断逻辑
|
|
if (tabBarList && tabBarList.list && Array.isArray(tabBarList.list)) {
|
|
const tabUrls = tabBarList.list
|
|
.filter((item) => item.link && item.link.wap_url)
|
|
.map((item) => _normalizePath(item.link.wap_url));
|
|
|
|
// 检查当前 linkUrl 是否是合法的 tab URL
|
|
if (!tabUrls.includes(normalizedLink)) {
|
|
_log('step 4: link not in tabUrls → false', { link: normalizedLink });
|
|
return false;
|
|
}
|
|
|
|
// 5) 页面级 Tab 绑定 (精确 + 通配符)
|
|
// 返回值可能是: tab URL | null (显式无 tab) | undefined (未声明)
|
|
const mappedTabUrl = lookupPageTabMap(normalizedCurrent, pageTabMap, pageTabWildcards);
|
|
if (mappedTabUrl !== undefined) {
|
|
_log('step 5: pageTabMap hit', { current: normalizedCurrent, mapped: mappedTabUrl });
|
|
// null = 显式声明"无 tab", 字符串 = 该 tab 高亮
|
|
return mappedTabUrl !== null && normalizedLink === _normalizePath(mappedTabUrl);
|
|
}
|
|
_log('step 5: pageTabMap miss → 进入 step 6/7');
|
|
|
|
// 6) 来源记忆兜底: 从哪个 Tab 跳转过来, 就保持那个 Tab 高亮
|
|
const fromTab = currentPageQuery.from_tab;
|
|
if (fromTab) {
|
|
const normalizedFromTab = _normalizePath(fromTab);
|
|
if (tabUrls.includes(normalizedFromTab)) {
|
|
const hit = normalizedLink === normalizedFromTab;
|
|
_log('step 6: from_tab query', { fromTab, hit });
|
|
return hit;
|
|
}
|
|
}
|
|
_log('step 6: no from_tab');
|
|
|
|
// 7) 分包入口追踪: 通过 updateCurrentRoute/resolveEntryTab 写入的入口 tab
|
|
// 注意: _findMatchingTabUrl 不会做"同分包兜底",找不到就返回 null
|
|
const matchingTabUrl = _findMatchingTabUrl(currentPageRoute, tabBarList, packageEntryTab);
|
|
if (matchingTabUrl) {
|
|
const hit = normalizedLink === matchingTabUrl;
|
|
_log('step 7: packageEntryTab', { matchingTabUrl, hit });
|
|
return hit;
|
|
}
|
|
_log('step 7: packageEntryTab miss → step 8');
|
|
|
|
// 8) 不再做"同分包兜底" — 找不到任何来源 tab 时不高亮
|
|
// 深链冷启动 / 扫码进非 tab 页: TabBar 全不亮, 对齐 iOS HIG
|
|
_log('step 8: no source → false (iOS HIG)');
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|