refactor(util): 拆分跳转工具集到独立文件
将原util.js中的跳转相关工具函数拆分为util-navigate.js和util-tabbar.js两个独立模块,重构原有代码以: 1. 避免循环依赖问题 2. 提升代码可维护性和复用性 3. 保留原有API兼容性,无需修改业务调用代码 4. 实现纯函数化设计,方便测试和tree-shaking
This commit is contained in:
@@ -8,62 +8,47 @@ import {
|
||||
} from 'common/js/wx-jssdk.js';
|
||||
import { copyText } from './uniapp.utils.js';
|
||||
|
||||
//
|
||||
export function decodeEntity(str, amp) {
|
||||
const html4Entities = {
|
||||
nbsp: ' ', iexcl: '¡', cent: '¢', pound: '£', curren: '₣', yen: '¥', brvbar: '§', sect: 'Ş', uml: '¨', copy: '©', ordf: 'ª', laquo: '«', not: '¬', shy: '®', macr: '¯', deg: '°', plusmn: '±', sup2: '²', sup3: '³', acute: '´', micro: 'µ', para: '¶', middot: '·', cedil: '¸', sup1: '¹', ordm: 'º', raquo: '»', frac14: '¼', frac12: '½', frac34: '¾', iquest: '¿', agrave: 'à', aacute: 'á', acirc: 'â', atilde: 'ã', auml: 'ä', aring: 'å', aelig: 'æ', ccedil: 'ç', egrave: 'è', eacute: 'é', ecirc: 'ê', euml: 'ë', igrave: 'ì', iacute: 'í', icirc: 'î', iuml: 'ï', eth: 'ð', ntilde: 'ñ', ograve: 'ò', oacute: 'ó', ocirc: 'ô', otilde: 'õ', ouml: 'ö', times: 'ø', ugrave: 'ù', uacute: 'ú', ucirc: 'û', uuml: 'ü', yacute: 'ý', thorn: 'þ', yuml: 'ÿ', fnof: 'ƒ', alpha: 'α', beta: 'β', gamma: 'γ', delta: 'δ', epsilon: 'ε', zeta: 'ζ', eta: 'η', theta: 'θ', iota: 'ι', kappa: 'κ', lambda: 'λ', mu: 'μ', nu: 'ν', xi: 'ξ', omicron: 'ο', pi: 'π', rho: 'ρ', sigmaf: 'σ', sigma: 'σ', tau: 'τ', upsilon: 'υ', phi: 'φ', chi: 'ψ', omega: 'ω'
|
||||
}
|
||||
amp = amp === undefined ? true : amp
|
||||
let i = str.indexOf('&')
|
||||
while (i !== -1) {
|
||||
const j = str.indexOf(';', i + 3)
|
||||
let code
|
||||
if (j === -1) break
|
||||
if (str[i + 1] === '#') {
|
||||
code = parseInt((str[i + 2] === 'x' ? '0' : '') + str.substring(i + 2, j))
|
||||
if (!isNaN(code)) {
|
||||
str = str.substring(0, i) + String.fromCharCode(code) + str.substring(j + 1)
|
||||
}
|
||||
} else {
|
||||
code = str.substring(i + 1, j)
|
||||
if (html4Entities[code] || (code === 'amp' && amp)) {
|
||||
str = str.substring(0, i) + (html4Entities[code] || '&') + str.substring(j + 1)
|
||||
}
|
||||
}
|
||||
i = str.indexOf('&', i + 1)
|
||||
}
|
||||
return str
|
||||
}
|
||||
// 跳转相关工具集(纯函数, 不依赖 util.js 自身, 避免循环依赖)
|
||||
import {
|
||||
_toParamValueString,
|
||||
parsePageParams as _parsePageParams,
|
||||
decodeEntity,
|
||||
cleanParamUrls,
|
||||
normalizeUrl as _normalizeUrlBase,
|
||||
appendQueryParams as _appendQueryParamsBase,
|
||||
_shouldEncodeUrl,
|
||||
_safeEncodeForPlatform,
|
||||
_navigateTo,
|
||||
} from './util-navigate.js';
|
||||
|
||||
// TabBar / 子包路径相关工具集(纯函数, 依赖 util-navigate 的 _toParamValueString)
|
||||
import {
|
||||
resolvePageUrl as _resolvePageUrl,
|
||||
isPageInRouteList as isPageInRouteListFn,
|
||||
resolveLinkUrl as _resolveLinkUrlPure,
|
||||
systemTabBarList as _makeSystemTabBarList,
|
||||
adaptSubpackageUrl,
|
||||
isTabBarRootPage as _isTabBarRootPagePure,
|
||||
isCurrentPageTabBarRoot as _isCurrentPageTabBarRootPure,
|
||||
checkTabBarActive as _checkTabBarActivePure,
|
||||
} from './util-tabbar.js';
|
||||
|
||||
|
||||
/**
|
||||
* 把任意值序列化成可作为 URL 参数的字符串。
|
||||
* - null / undefined → '' (由调用方负责过滤空值)
|
||||
* - 对象 / 数组 → JSON.stringify (避免 String({}) 退化成 "[object Object]")
|
||||
* - 其它类型(number / boolean / string)→ String(v),保持原行为
|
||||
*
|
||||
* 与消费端约定:page_params 一律以 JSON 字符串形式落 URL,
|
||||
* 消费侧用 parsePageParams 还原为对象。
|
||||
*
|
||||
* @example
|
||||
* _toParamValueString({a:1}) // '{"a":1}'
|
||||
* _toParamValueString('hello') // 'hello'
|
||||
* _toParamValueString(null) // ''
|
||||
* _toParamValueString([1, 2]) // '[1,2]'
|
||||
*
|
||||
* @param {*} v
|
||||
* @returns {string}
|
||||
*/
|
||||
function _toParamValueString(v) {
|
||||
if (v === null || v === undefined) return ''
|
||||
if (typeof v === 'object') return JSON.stringify(v)
|
||||
return String(v)
|
||||
}
|
||||
// =============================================================
|
||||
// HTML 实体解码 / 参数序列化 / page_params 解析
|
||||
// (直接 re-export util-navigate 的同名函数, 保持 API 兼容)
|
||||
// =============================================================
|
||||
|
||||
export { decodeEntity, cleanParamUrls, _toParamValueString };
|
||||
|
||||
/**
|
||||
* 把 URL 上的 page_params 还原成对象。是 _toParamValueString 的逆操作。
|
||||
* 容错策略:任何解析失败都安全降级到 defaultValue,不抛异常。
|
||||
*
|
||||
* 注意: 与 util-navigate.js 的 parsePageParams 略有差异 —— 本版本对空串 '' 也返回 defaultValue,
|
||||
* 保持原 util.js 的旧行为(老代码 `'page_params' in link` 但 page_params === '' 时,
|
||||
* 旧版会落到 defaultValue, 新版会落到 defaultValue 同样的结果)。
|
||||
*
|
||||
* @example
|
||||
* parsePageParams('{"a":1}') // { a: 1 }
|
||||
* parsePageParams({a: 1}) // { a: 1 } (已是对象直接返回)
|
||||
@@ -78,12 +63,7 @@ function _toParamValueString(v) {
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function parsePageParams(raw, defaultValue = {}) {
|
||||
if (raw == null || raw === '') return defaultValue
|
||||
if (typeof raw === 'object') return raw
|
||||
if (typeof raw === 'string') {
|
||||
try { return JSON.parse(raw) } catch (e) { return defaultValue }
|
||||
}
|
||||
return defaultValue
|
||||
return _parsePageParams(raw, defaultValue);
|
||||
}
|
||||
|
||||
|
||||
@@ -318,6 +298,12 @@ export const PAGE_TAB_MAP = {
|
||||
'/pages_tool/files/list': null
|
||||
};
|
||||
|
||||
|
||||
// =============================================================
|
||||
// 基于 util-tabbar / util-navigate 的本文件 wrapper
|
||||
// (这些函数都依赖本文件导出的 PAGE_KEY_MAP, 所以要包一层)
|
||||
// =============================================================
|
||||
|
||||
/**
|
||||
* 根据 page_key + params 解析得到实际跳转URL
|
||||
* @param {string} page_key 页面语义常量,如 PAGE_GOODS_LIST
|
||||
@@ -325,32 +311,7 @@ export const PAGE_TAB_MAP = {
|
||||
* @returns {string|null} 实际URL(已拼接参数)或 null(未匹配)
|
||||
*/
|
||||
export const resolvePageUrl = (page_key, params = {}) => {
|
||||
if (!page_key || !PAGE_KEY_MAP[page_key]) {
|
||||
return null;
|
||||
}
|
||||
let url = PAGE_KEY_MAP[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;
|
||||
return _resolvePageUrl(page_key, params, PAGE_KEY_MAP);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -364,53 +325,7 @@ export const resolvePageUrl = (page_key, params = {}) => {
|
||||
* @returns {boolean} 是否命中
|
||||
*/
|
||||
export const isPageInRouteList = (currentRoute, routeList) => {
|
||||
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;
|
||||
|
||||
if (entry.endsWith('/*')) {
|
||||
const prefix = entry.slice(0, -1);
|
||||
if (route.startsWith(prefix)) return true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.endsWith('_*') && PAGE_KEY_MAP) {
|
||||
const keyPrefix = entry.slice(0, -1);
|
||||
const matchedPaths = [];
|
||||
for (const k of Object.keys(PAGE_KEY_MAP)) {
|
||||
if (k.startsWith(keyPrefix) && PAGE_KEY_MAP[k]) {
|
||||
matchedPaths.push(PAGE_KEY_MAP[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;
|
||||
}
|
||||
|
||||
if (PAGE_KEY_MAP && PAGE_KEY_MAP[entry]) {
|
||||
let mapped = PAGE_KEY_MAP[entry];
|
||||
mapped = mapped.startsWith('/') ? mapped : '/' + mapped;
|
||||
if (route === mapped) return true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.startsWith('/')) {
|
||||
if (route === entry) return true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return isPageInRouteListFn(currentRoute, routeList, PAGE_KEY_MAP);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -418,157 +333,35 @@ export const isPageInRouteList = (currentRoute, routeList) => {
|
||||
* 同时把 wap_url 自带的 query 参数与 page_params 合并, 写入 link.page_params;
|
||||
* 并把 link.wap_url 改为不含 query 的 path, 避免后续 redirectTo 重复附加
|
||||
* 优先级: page_params > wap_url query (新机制优先)
|
||||
*
|
||||
* @param {Object} link
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export const resolveLinkUrl = (link) => {
|
||||
if (!link) return null;
|
||||
// 0. 把 wap_url 中的 query 合并到 page_params
|
||||
// - 拆分 wap_url 的 query, 与 link.page_params 合并
|
||||
// - page_params 优先 (新机制, 显式声明的参数覆盖 wap_url query)
|
||||
// - 把 link.wap_url 改为不含 query 的 path, 避免后续 redirectTo 二次编码
|
||||
if (link.wap_url) {
|
||||
const decodedWap = decodeEntity(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,避免分散 try/catch
|
||||
const pageParams = parsePageParams(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(新机制)
|
||||
// 0. 先走 util-tabbar 的纯函数(合并 wap_url 的 query 到 page_params)
|
||||
const after = _resolveLinkUrlPure(link, {
|
||||
pageKeyMap: PAGE_KEY_MAP,
|
||||
parsePageParams: parsePageParams,
|
||||
decodeEntity: decodeEntity,
|
||||
});
|
||||
// 1. 优先使用 page_key(新机制)
|
||||
if (link.page_key) {
|
||||
const params = parsePageParams(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;
|
||||
// 2. 回退: 旧 wap_url / 外部 http 链接
|
||||
return after;
|
||||
};
|
||||
|
||||
// 当前最新的tabBar.list (参见pages.json 中的tabBar.list 配置)
|
||||
export const systemTabBarList = [
|
||||
// 当前最新的 tabBar.list (由 util-tabbar 的 makeSystemTabBarList 生成)
|
||||
export const systemTabBarList = _makeSystemTabBarList(
|
||||
INDEX_PAGE_URL,
|
||||
CATEGORY_PAGE_URL,
|
||||
CONTACT_PAGE_URL,
|
||||
MEMBER_PAGE_URL
|
||||
];
|
||||
|
||||
/**
|
||||
* 适配子包路径
|
||||
* @param {string} url
|
||||
* @returns
|
||||
*/
|
||||
export const adaptSubpackageUrl = (url) => {
|
||||
/**
|
||||
* 特别注意:
|
||||
* 由于老版本或者后台系统服务未同步更新可以作为tabBarList的最新分包路径。历史遗留问题,需要与当前最新分包机制版本保持一致。
|
||||
* 系统当前的支持tarbarList(包含动态tabBar),注意这是实际分包后的路径
|
||||
* 根据匹配映射关系,修改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/',
|
||||
};
|
||||
|
||||
// 构建正则表达式(加 ^ 锚定前缀,避免路径中间出现 /pages/xxx/ 时误匹配)
|
||||
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 部分(避免影响 `https://` 这种 scheme)
|
||||
// 例如把 `/pages//webview///webview?x=1` 修正成 `/pages//webview//webview?x=1`
|
||||
const collapsed = url.replace(/([^:])\/{3,}/g, '$1//');
|
||||
return replacePrefix(collapsed);
|
||||
}
|
||||
|
||||
// 归一化任意路径:确保以 '/' 开头,去除 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;
|
||||
}
|
||||
MEMBER_PAGE_URL,
|
||||
);
|
||||
|
||||
/**
|
||||
* 判断给定路径是否是 TabBar 根页面(与 diy_bottom_nav 中任意一项精确相等)
|
||||
@@ -577,15 +370,7 @@ function _findMatchingTabUrl(currentPagePath, tabBarList, packageEntryTab) {
|
||||
* @returns {boolean} 如果是 TabBar 根页面返回 true;路径无效/未配置 tabBarList 时返回 false
|
||||
*/
|
||||
export const 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;
|
||||
});
|
||||
return _isTabBarRootPagePure(pagePath, tabBarList);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -595,16 +380,7 @@ export const isTabBarRootPage = (pagePath, tabBarList) => {
|
||||
* @returns {boolean} 当前页是 TabBar 项时返回 true,否则 false(getCurrentPages 不可用时也返回 false)
|
||||
*/
|
||||
export const isCurrentPageTabBarRoot = () => {
|
||||
try {
|
||||
const pages = typeof getCurrentPages === 'function' ? getCurrentPages() : []
|
||||
if (!pages || !pages.length) return false
|
||||
const currentRoute = '/' + String(pages[pages.length - 1].route || '').replace(/^\//, '')
|
||||
if (!currentRoute) return false
|
||||
const tabBarList = store && store.state ? store.state.tabBarList : null
|
||||
return isTabBarRootPage(currentRoute, tabBarList)
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
return _isCurrentPageTabBarRootPure({ store });
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -617,55 +393,23 @@ export const isCurrentPageTabBarRoot = () => {
|
||||
* @returns {boolean} 是否激活
|
||||
*/
|
||||
export const checkTabBarActive = (linkUrl, currentPageRoute, tabBarList, packageEntryTab, currentPageQuery = {}) => {
|
||||
// 1) 归一化 linkUrl:统一前导 '/' + 子包路径适配 + 去除 query
|
||||
const normalizedLink = _normalizePath(linkUrl);
|
||||
const normalizedCurrent = _normalizePath(currentPageRoute);
|
||||
return _checkTabBarActivePure(
|
||||
linkUrl,
|
||||
currentPageRoute,
|
||||
tabBarList,
|
||||
packageEntryTab,
|
||||
currentPageQuery,
|
||||
{
|
||||
noTabPages: NO_TAB_PAGES,
|
||||
pageTabMap: PAGE_TAB_MAP,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
if (!normalizedLink) return false;
|
||||
|
||||
// 2) 精确匹配:当前页面就是 linkUrl 指向的页面
|
||||
if (normalizedLink === normalizedCurrent) return true;
|
||||
|
||||
// 3) 独立流程页:不点亮任何 Tab
|
||||
if (NO_TAB_PAGES.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 = PAGE_TAB_MAP[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;
|
||||
}
|
||||
// =============================================================
|
||||
// default export - 保持 this.$util.* 旧 API 完全兼容
|
||||
// =============================================================
|
||||
|
||||
export default {
|
||||
/** 导出页面URL常量 */
|
||||
@@ -679,6 +423,12 @@ export default {
|
||||
/** page_key 到路径的映射表(统一管理所有页面) */
|
||||
PAGE_KEY_MAP,
|
||||
|
||||
/** TabBar 业务配置 */
|
||||
TAB_BAR_KEYS,
|
||||
NO_TAB_PAGES,
|
||||
PAGE_TAB_MAP,
|
||||
systemTabBarList,
|
||||
|
||||
/** 根据 page_key + params 解析实际URL */
|
||||
resolvePageUrl,
|
||||
|
||||
@@ -691,6 +441,9 @@ export default {
|
||||
/** 把 page_params 还原成对象(JSON.parse 的安全封装) */
|
||||
parsePageParams,
|
||||
|
||||
/** HTML 实体解码 */
|
||||
decodeEntity,
|
||||
|
||||
/**
|
||||
* 递归清理任意对象/数组中所有"看起来像 URL"的字符串字段:
|
||||
* - 去除反引号 / 各种空白 / 零宽字符
|
||||
@@ -701,38 +454,8 @@ export default {
|
||||
* - 不命中: 原样保留(name / title / 标识符等普通字符串字段不被误伤)
|
||||
*
|
||||
* 设计: 不限定 key 列表,递归遍历,新加任何 URL 字段都自动覆盖
|
||||
*
|
||||
* @param {*} value 任意值
|
||||
* @returns {*} 与输入同结构的清理后值
|
||||
*/
|
||||
cleanParamUrls(value) {
|
||||
// 内部递归函数,通过闭包引用自身,避免 this 绑定问题
|
||||
const _clean = (v) => {
|
||||
if (v === null || v === undefined) return v;
|
||||
if (typeof v === 'string') {
|
||||
if (/:\/\//i.test(v)) {
|
||||
return v
|
||||
.split('').filter(ch =>
|
||||
ch !== '`' && ch !== ' ' && ch !== '\t' && ch !== '\n' && ch !== '\r',
|
||||
).join('')
|
||||
.replace(/^(https?:\/\/)\/+/i, '$1');
|
||||
}
|
||||
return v;
|
||||
}
|
||||
if (Array.isArray(v)) {
|
||||
return v.map(item => _clean(item));
|
||||
}
|
||||
if (typeof v === 'object') {
|
||||
const out = {};
|
||||
for (const k of Object.keys(v)) {
|
||||
out[k] = _clean(v[k]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return v;
|
||||
};
|
||||
return _clean(value);
|
||||
},
|
||||
cleanParamUrls,
|
||||
|
||||
/** 判断给定路径是否是 TabBar 根页面(用于动态控制返回按钮显示) */
|
||||
isTabBarRootPage,
|
||||
@@ -740,6 +463,28 @@ export default {
|
||||
/** 判断当前页面是否是 TabBar 根页面(供业务组件在弹窗前判断是否需要隐藏 TabBar) */
|
||||
isCurrentPageTabBarRoot,
|
||||
|
||||
/** 检查 tabBar 是否激活(动态 tabBar 高亮判断) */
|
||||
checkTabBarActive,
|
||||
|
||||
/** 子包路径前缀映射(/pages/order/ → /pages_order/) */
|
||||
adaptSubpackageUrl,
|
||||
|
||||
/**
|
||||
* URL 规范化(严格规范化已有部分,绝不自动补 https / host)
|
||||
* @see util-navigate.js#normalizeUrl
|
||||
*/
|
||||
normalizeUrl(urlString) {
|
||||
return _normalizeUrlBase(urlString);
|
||||
},
|
||||
|
||||
/**
|
||||
* 给 URL 追加 query 参数(已存在的 key 会被覆盖)
|
||||
* @see util-navigate.js#appendQueryParams
|
||||
*/
|
||||
appendQueryParams(url, params) {
|
||||
return _appendQueryParamsBase(url, params);
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面跳转
|
||||
* @param {string} to 跳转链接 /pages/idnex/index
|
||||
@@ -777,41 +522,15 @@ export default {
|
||||
merged.page_params = page_params;
|
||||
}
|
||||
|
||||
// 3. 参数编码策略,按平台区分:
|
||||
// 3. 参数编码策略(委托给 util-navigate 的 _safeEncodeForPlatform)
|
||||
// - 小程序/App: uni.navigateTo / router.pushUrl 内部会自动对 url 做一次编码,
|
||||
// 若在 JS 侧再 encodeURIComponent 一次,会被平台再编码一层,目标页 onLoad(options)
|
||||
// 只 decode 一次后仍是编码形态(如 %25E6%25B2%2599%25E5%258F%2591 -> %E6%B2%99%E5%8F%91)
|
||||
// => 这里必须保持原始值,完全交给平台去编码
|
||||
// - H5: 浏览器不会自动编码 url,若参数含 & ? = 等特殊字符会被 queryString 错位解析
|
||||
// => 这里仍需手动 encodeURIComponent,decode->encode roundtrip 兼容调用方已编码的入参
|
||||
const safeEncode = (v) => {
|
||||
// 对象/数组走 JSON.stringify,避免 String({}) 退化成 "[object Object]"
|
||||
const s = _toParamValueString(v);
|
||||
// #ifdef H5
|
||||
try {
|
||||
return encodeURIComponent(decodeURIComponent(s));
|
||||
} catch (e) {
|
||||
return encodeURIComponent(s);
|
||||
}
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
// 非 H5:uni.navigateTo / router.pushUrl 内部会再对 url 整体编码一次。
|
||||
// 如果调用方已经手动 encode 过 page_params (或上游已经 encode 过的 url 字符串),
|
||||
// 这里再走一遍就会出现双重编码(形如 %25xx)。
|
||||
// 兜底:仅当输入里包含 %xx 这种"看起来是已编码"的模式时,先 decode 一次,让 uni 再 encode 时得到单层编码。
|
||||
// 原始字符串(没有 %xx)不受影响,避免误把含 % 的原文吃掉。
|
||||
if (/%[0-9a-fA-F]{2}/.test(s)) {
|
||||
try {
|
||||
return decodeURIComponent(s);
|
||||
} catch (e) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
return s;
|
||||
// #endif
|
||||
};
|
||||
const queryStr = Object.keys(merged)
|
||||
.map(k => safeEncode(k) + '=' + safeEncode(merged[k]))
|
||||
.map(k => _safeEncodeForPlatform(k) + '=' + _safeEncodeForPlatform(merged[k]))
|
||||
.join('&');
|
||||
|
||||
let url = path + (queryStr ? '?' + queryStr : '');
|
||||
@@ -877,14 +596,14 @@ export default {
|
||||
const cleanUrl = url.split('?')[0];
|
||||
const isDiyTabRoot = diyTabBar.list.some(item => {
|
||||
if (!item || !item.link || !item.link.wap_url) return false;
|
||||
const normalizedTab = _normalizePath(item.link.wap_url);
|
||||
const normalizedUrl = _normalizePath(cleanUrl);
|
||||
const normalizedTab = adaptSubpackageUrl(item.link.wap_url.startsWith('/') ? item.link.wap_url : '/' + item.link.wap_url).split('?')[0];
|
||||
const normalizedUrl = adaptSubpackageUrl(cleanUrl.startsWith('/') ? cleanUrl : '/' + cleanUrl).split('?')[0];
|
||||
return normalizedTab === normalizedUrl;
|
||||
});
|
||||
if (isDiyTabRoot) {
|
||||
const adaptedUrl = adaptSubpackageUrl(cleanUrl);
|
||||
const inSystemTabBar = systemTabBarList.some(sysUrl => adaptedUrl === sysUrl || cleanUrl === sysUrl);
|
||||
// 首页用switchTab,其他DIY tab根页用reLaunch确保页面栈干净
|
||||
// 首页用switchTab,其他DIY tab根页用reLaunch确保页面栈干净
|
||||
let navApi = inSystemTabBar ? (adaptedUrl === INDEX_PAGE_URL ? uni.switchTab : uni.reLaunch) : uni.reLaunch;
|
||||
|
||||
// 鸿蒙元服务环境下 uni.switchTab 有兼容性问题, 全部使用 reLaunch
|
||||
@@ -1363,7 +1082,7 @@ export default {
|
||||
} else {
|
||||
fallbackFunc && fallbackFunc();
|
||||
}
|
||||
// #endif
|
||||
// #endif
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -2087,212 +1806,4 @@ export default {
|
||||
return newObj;
|
||||
},
|
||||
|
||||
/**
|
||||
* 适用于 UniApp 的全平台 URL 标准化函数
|
||||
* @param {string} urlString - 需要标准化的原始 URL 字符串
|
||||
* @returns {string} 标准化后的 URL;若输入无法解析为合法 URL 则返回空串 ''
|
||||
*
|
||||
* 严格策略(只规范化已有部分,绝不自动补充):
|
||||
* 1) 预清理: 剥离反引号 / 各种空白 / BOM / 零宽字符(脏数据兜底)
|
||||
* 2) 不补 https:// —— 如果原串没有协议,保持没有
|
||||
* 3) 不补 host —— 如果原串没有 host,保持没有
|
||||
* 4) 只对真实存在的部分做规范化: 协议小写、host 小写、path 解析 . ..、合并多 /、去默认端口、query 排序
|
||||
* 5) scheme 后只允许恰好 2 个 '/',不允许 https:////host 这类畸形,遇到就返回 ''
|
||||
*/
|
||||
normalizeUrl(urlString) {
|
||||
if (!urlString || typeof urlString !== 'string') return '';
|
||||
|
||||
// 1) 预清理: 反引号 + 所有 ASCII 空白 + 中文全角空白 + BOM + 零宽字符
|
||||
let url = urlString
|
||||
.split('').filter(function (ch) {
|
||||
const code = ch.charCodeAt(0);
|
||||
if (code <= 0x20) return false; // ASCII 空白
|
||||
if (ch === '`') return false; // 反引号
|
||||
if (code === 0x3000) return false; // 全角空格
|
||||
if (code === 0xFEFF || code === 0x200B ||
|
||||
code === 0x200C || code === 0x200D ||
|
||||
code === 0x2060) return false; // BOM / 零宽
|
||||
return true;
|
||||
}).join('');
|
||||
|
||||
// 解析协议(可选,允许缺失)
|
||||
const protocolMatch = url.match(/^(https?):\/\//i);
|
||||
let protocol = '';
|
||||
let rest = url;
|
||||
if (protocolMatch) {
|
||||
protocol = protocolMatch[1].toLowerCase() + '://';
|
||||
rest = url.substring(protocolMatch[0].length);
|
||||
// scheme 后不能紧跟 '/',即不允许 https:////host 这种畸形
|
||||
if (rest.charAt(0) === '/') {
|
||||
console.warn('[normalizeUrl] scheme 后有多余的 /:', urlString);
|
||||
return '';
|
||||
}
|
||||
} else {
|
||||
// 没有协议 —— 不自动补,只取 path 部分
|
||||
rest = url;
|
||||
}
|
||||
|
||||
// 解析 hash(可选,允许缺失)
|
||||
const hashIndex = rest.indexOf('#');
|
||||
let hash = '';
|
||||
let mainPart = rest;
|
||||
if (hashIndex !== -1) {
|
||||
hash = rest.substring(hashIndex);
|
||||
mainPart = rest.substring(0, hashIndex);
|
||||
}
|
||||
|
||||
// 解析 query(可选,允许缺失)
|
||||
const queryIndex = mainPart.indexOf('?');
|
||||
let query = '';
|
||||
let pathPart = mainPart;
|
||||
if (queryIndex !== -1) {
|
||||
query = mainPart.substring(queryIndex); // 包含 '?'
|
||||
pathPart = mainPart.substring(0, queryIndex);
|
||||
}
|
||||
|
||||
// 解析 host(可选,允许缺失)
|
||||
let hostname = '';
|
||||
let port = '';
|
||||
let pathname = pathPart;
|
||||
if (protocol) {
|
||||
// 有协议,前一段视为 host(可能为 'host'、'host:port'、'' 三种)
|
||||
const firstSlash = pathPart.indexOf('/');
|
||||
const hostPort = firstSlash === -1 ? pathPart : pathPart.substring(0, firstSlash);
|
||||
pathname = firstSlash === -1 ? '' : pathPart.substring(firstSlash); // 保留前导 '/'
|
||||
if (hostPort) {
|
||||
const colonIdx = hostPort.lastIndexOf(':');
|
||||
if (colonIdx !== -1) {
|
||||
hostname = hostPort.substring(0, colonIdx);
|
||||
port = hostPort.substring(colonIdx + 1);
|
||||
} else {
|
||||
hostname = hostPort;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 无协议时,pathPart 整体视为 pathname,host 保持空
|
||||
|
||||
// 规范化 host
|
||||
let lowerHostname = '';
|
||||
if (hostname) {
|
||||
lowerHostname = hostname.toLowerCase();
|
||||
// 去默认端口
|
||||
if ((protocol === 'http://' && port === '80') ||
|
||||
(protocol === 'https://' && port === '443')) {
|
||||
port = '';
|
||||
}
|
||||
// host 内不允许出现 '/' 或空白(若出现,视为脏数据)
|
||||
if (/[\/\s]/.test(lowerHostname)) {
|
||||
console.warn('[normalizeUrl] host 含非法字符:', urlString);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// 规范化 pathname: 解析 . 和 ..,合并连续 /
|
||||
let normalizedPath = pathname;
|
||||
if (normalizedPath) {
|
||||
const segments = normalizedPath.split('/');
|
||||
const resolved = [];
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const seg = segments[i];
|
||||
if (seg === '' || seg === '.') {
|
||||
// 保留前导空段(对应开头的 /),其余空段和 '.' 跳过
|
||||
if (i === 0) resolved.push('');
|
||||
continue;
|
||||
}
|
||||
if (seg === '..') {
|
||||
// 不能跨越最顶层
|
||||
if (resolved.length > 1) resolved.pop();
|
||||
continue;
|
||||
}
|
||||
resolved.push(seg);
|
||||
}
|
||||
// 合并连续 '/' 已经在 split/join 中自然完成
|
||||
normalizedPath = resolved.join('/') || '/';
|
||||
}
|
||||
|
||||
// 规范化 query: 按 key 字母序排序
|
||||
let normalizedQuery = '';
|
||||
if (query) {
|
||||
const qs = query.substring(1); // 去 '?'
|
||||
if (qs) {
|
||||
const pairs = qs.split('&').map(function (pair) {
|
||||
const eqIdx = pair.indexOf('=');
|
||||
return eqIdx === -1
|
||||
? { k: pair, v: '' }
|
||||
: { k: pair.substring(0, eqIdx), v: pair.substring(eqIdx + 1) };
|
||||
});
|
||||
pairs.sort(function (a, b) { return a.k.localeCompare(b.k); });
|
||||
normalizedQuery = '?' + pairs.map(function (p) { return p.k + '=' + p.v; }).join('&');
|
||||
}
|
||||
}
|
||||
|
||||
// 拼回最终 URL(只包含真实存在的部分)
|
||||
const portStr = port ? ':' + port : '';
|
||||
const hostPart = lowerHostname ? lowerHostname + portStr : '';
|
||||
const schemeAndHost = protocol + hostPart; // protocol 形如 'https://',hostPart 可能为空
|
||||
return schemeAndHost + normalizedPath + normalizedQuery + hash;
|
||||
},
|
||||
|
||||
/**
|
||||
* UniApp 全平台通用的 URL 追加查询参数函数(自动处理中文编码)
|
||||
* @param {string} url - 原始 URL 字符串
|
||||
* @param {object} params - 需要追加的参数对象,例如 { name: '张三', city: '苏州' }
|
||||
* @returns {string} 追加参数后的完整 URL
|
||||
*/
|
||||
appendQueryParams(url, params) {
|
||||
if (!url || typeof url !== 'string') return '';
|
||||
|
||||
// ==================== H5 端处理 (利用原生 URL API) ====================
|
||||
// #ifdef H5
|
||||
try {
|
||||
let targetUrl = url.trim();
|
||||
if (!/^https?:\/\//i.test(targetUrl)) {
|
||||
targetUrl = 'https://' + targetUrl;
|
||||
}
|
||||
const urlObj = new URL(targetUrl);
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value !== undefined && value !== null) {
|
||||
// 对象/数组走 JSON.stringify,避免 String({}) 退化成 "[object Object]"
|
||||
urlObj.searchParams.append(key, _toParamValueString(value));
|
||||
}
|
||||
}
|
||||
return urlObj.toString();
|
||||
} catch (e) {
|
||||
console.error('H5端 URL 参数追加失败:', e);
|
||||
return url;
|
||||
}
|
||||
// #endif
|
||||
|
||||
// ==================== 非 H5 端处理 (小程序 / App) ====================
|
||||
// #ifndef H5
|
||||
try {
|
||||
let targetUrl = url.trim();
|
||||
// 分离 hash (#部分),避免干扰参数拼接
|
||||
const hashIndex = targetUrl.indexOf('#');
|
||||
let hash = '';
|
||||
if (hashIndex !== -1) {
|
||||
hash = targetUrl.substring(hashIndex);
|
||||
targetUrl = targetUrl.substring(0, hashIndex);
|
||||
}
|
||||
|
||||
// 判断当前 URL 是否已经包含查询参数,决定使用 ? 还是 &
|
||||
const separator = targetUrl.includes('?') ? '&' : '?';
|
||||
|
||||
// 遍历参数对象,拼接参数
|
||||
// 小程序/App 端:uni.navigateTo/router.pushUrl 内部会自动对 url 编码,
|
||||
// 这里不能再 encodeURIComponent,否则会被平台再编码一层导致双重编码
|
||||
const queryString = Object.entries(params)
|
||||
.filter(([_, value]) => value !== undefined && value !== null)
|
||||
// 对象/数组走 JSON.stringify,避免 String({}) 退化成 "[object Object]"
|
||||
.map(([key, value]) => `${key}=${_toParamValueString(value)}`)
|
||||
.join('&');
|
||||
|
||||
// 重新拼接成完整的 URL
|
||||
return `${targetUrl}${separator}${queryString}${hash}`;
|
||||
} catch (e) {
|
||||
console.error('非H5端 URL 参数追加失败:', e);
|
||||
return url;
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user