/** * 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 配置, 返回当前页面所属的 tab URL * 优先级: 精确匹配 > packageEntryTab > 分包名回退匹配 * (私有) */ 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) 回退: 同分包内的第一个 tab(基于分包名匹配) return tabUrls.find((url) => _getPackageName(url) === packageName) || null; } /** * 判断给定路径是否是 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) * @returns {boolean} 是否激活 */ export function checkTabBarActive( linkUrl, currentPageRoute, tabBarList, packageEntryTab, currentPageQuery = {}, opts = {}, ) { const { noTabPages = null, pageTabMap = {} } = opts; // 1) 归一化 linkUrl: 统一前导 '/' + 子包路径适配 + 去除 query const normalizedLink = _normalizePath(linkUrl); const normalizedCurrent = _normalizePath(currentPageRoute); if (!normalizedLink) return false; // 2) 精确匹配: 当前页面就是 linkUrl 指向的页面 if (normalizedLink === normalizedCurrent) return true; // 3) 独立流程页: 不点亮任何 Tab if (noTabPages && noTabPages.has && noTabPages.has(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)) return false; // 5) 页面级 Tab 精确绑定 const mappedTabUrl = pageTabMap[normalizedCurrent]; if (mappedTabUrl) { return normalizedLink === _normalizePath(mappedTabUrl); } if (mappedTabUrl === null) return false; // 6) 来源记忆兜底: 从哪个 Tab 跳转过来, 就保持那个 Tab 高亮 const fromTab = currentPageQuery.from_tab; if (fromTab) { const normalizedFromTab = _normalizePath(fromTab); if (tabUrls.includes(normalizedFromTab)) { return normalizedLink === normalizedFromTab; } } // 7) 分包入口追踪: 通过 updateCurrentRoute 写入的入口 tab const matchingTabUrl = _findMatchingTabUrl(currentPageRoute, tabBarList, packageEntryTab); if (matchingTabUrl) return normalizedLink === matchingTabUrl; // 8) 同分包回退: linkUrl 和当前页面在同一分包 → 高亮 const linkPackage = _getPackageName(normalizedLink); const currentPackage = _getPackageName(normalizedCurrent); return linkPackage && currentPackage && linkPackage === currentPackage; } return false; }