Files
lucky_shop/common/js/util.js
ZF sun 679c53c501 refactor(util): 统一 URL 参数序列化与解析逻辑
1. 新增_toParamValueString处理复杂参数序列化
2. 新增parsePageParams作为page_params统一解析工具
3. 重构多处参数处理逻辑,避免重复代码和对象转字符串异常
4. 统一处理对象/数组参数的JSON序列化,避免出现[object Object]
2026-07-10 13:56:37 +08:00

2118 lines
66 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Config from './config.js'
import store from '@/store/index.js'
import Http from './http.js'
import Platform from './platform.js'
import Lang from './lang.js'
import {
Weixin
} from 'common/js/wx-jssdk.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
}
/**
* 把任意值序列化成可作为 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)
}
/**
* 把 URL 上的 page_params 还原成对象。是 _toParamValueString 的逆操作。
* 容错策略:任何解析失败都安全降级到 defaultValue,不抛异常。
*
* @example
* parsePageParams('{"a":1}') // { a: 1 }
* parsePageParams({a: 1}) // { a: 1 } (已是对象直接返回)
* parsePageParams('[object Object]') // {} (老 bug 残留安全降级)
* parsePageParams('') // {}
* parsePageParams(null) // {}
* parsePageParams(undefined) // {}
* parsePageParams('not json', {x:1}) // { x: 1 } (解析失败走默认值)
*
* @param {*} raw page_params 原始值(可能是 object / string / null / undefined)
* @param {Object} [defaultValue={}] 解析失败时的兜底值
* @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
}
// 关于页面路径,使用常量导出
export const INDEX_PAGE_URL = '/pages/index/index';
export const CATEGORY_PAGE_URL = '/pages_goods/category';
export const CONTACT_PAGE_URL = '/pages_tool/contact/contact';
export const MEMBER_PAGE_URL = '/pages_tool/member/index';
export const LOGIN_PAGE_URL = '/pages_tool/login/login';
export const AI_CHAT_PAGE_URL = '/pages_tool/ai-chat/index';
// page_key 到实际路径的映射表(统一管理所有页面的语义常量 → 实际路径)
// 前端组织页面(重命名路径、分包调整)时只需要修改此映射表
export const PAGE_KEY_MAP = {
// === 主页 & 商品 ===
PAGE_INDEX: '/pages/index/index',
PAGE_GOODS_LIST: '/pages_goods/list',
PAGE_GOODS_CATEGORY: '/pages_goods/category',
PAGE_GOODS_DETAIL: '/pages_goods/detail',
PAGE_GOODS_CART: '/pages_goods/cart',
PAGE_GOODS_COUPON: '/pages_tool/goods/coupon',
PAGE_GOODS_BRAND: '/pages_tool/goods/brand',
// === 订单 ===
PAGE_ORDER_LIST: '/pages/order/list',
PAGE_ORDER_DETAIL: '/pages/order/detail',
PAGE_ORDER_CREATE: '/pages/order/create',
PAGE_ORDER_REFUND: '/pages/order/refund',
PAGE_ORDER_ACTIVIST: '/pages/order/activist',
// === 会员中心 ===
PAGE_MEMBER_INDEX: '/pages_tool/member/index',
PAGE_MEMBER_INFO: '/pages_tool/member/info',
PAGE_MEMBER_ACCOUNT: '/pages_tool/member/account',
PAGE_MEMBER_COLLECTION: '/pages_tool/member/collection',
PAGE_MEMBER_FOOTPRINT: '/pages_tool/member/footprint',
PAGE_MEMBER_COUPON: '/pages_tool/member/coupon',
PAGE_MEMBER_BALANCE: '/pages_tool/member/balance_detail',
PAGE_MEMBER_ADDRESS: '/pages_tool/member/address',
PAGE_MEMBER_LEVEL: '/pages_tool/member/level',
PAGE_MEMBER_SIGNIN: '/pages_tool/member/signin',
PAGE_MEMBER_CARD: '/pages_tool/member/card',
PAGE_MEMBER_CONTACT: '/pages_tool/member/contact',
// === 公告/文章/帮助 ===
PAGE_NOTICE_LIST: '/pages_tool/notice/list',
PAGE_ARTICLE_LIST: '/pages_tool/article/list',
PAGE_ARTICLE_DETAIL: '/pages_tool/article/detail',
PAGE_HELP_LIST: '/pages_tool/help/list',
// === 积分商城 ===
PAGE_POINT_LIST: '/pages_promotion/point/list',
PAGE_POINT_ORDER_LIST: '/pages_promotion/point/order_list',
// === 分销 ===
PAGE_FENXIAO_INDEX: '/pages_promotion/fenxiao/index',
PAGE_FENXIAO_WITHDRAW: '/pages_promotion/fenxiao/withdraw_list',
PAGE_FENXIAO_ORDER: '/pages_promotion/fenxiao/order',
PAGE_FENXIAO_TEAM: '/pages_promotion/fenxiao/team',
PAGE_FENXIAO_PROMOTE: '/pages_promotion/fenxiao/promote_code',
// === 商户 ===
PAGE_MERCH_CATEGORY: '/pages_promotion/merch/merchcategory',
// === 案例展示 ===
PAGE_CASES_INDEX: '/pages_tool/cases/index',
// === 电子名片 ===
PAGE_CONTACT_CARD: '/pages/contact/contact',
// === VR ===
PAGE_VR_INDEX: '/pages_tool/vr/index',
// === 介质材料/结构选型 ===
PAGE_SEAL_MEDIUM: '/pages_tool/seal/medium/search',
PAGE_SEAL_STRUCTURE: '/pages_tool/seal/structure',
// === 充值/核销 ===
PAGE_RECHARGE_LIST: '/pages_tool/recharge/list',
PAGE_VERIFICATION_INDEX: '/pages_tool/verification/index',
// === 登录/联系 ===
PAGE_LOGIN: '/pages_tool/login/login',
PAGE_REGISTER: '/pages_tool/login/register',
PAGE_CONTACT: '/pages_tool/contact/contact',
// === 文件/微页面 ===
PAGE_FILES_LIST: '/pages_tool/files/list',
PAGE_FILES_DETAIL: '/pages_tool/files/detail',
PAGE_DIY_VIEW: '/pages_tool/index/diy',
// === 其他 ===
PAGE_AI_CHAT: '/pages_tool/ai-chat/index',
PAGE_WEBVIEW: '/pages_tool/webview/webview'
};
// ================== TabBar 高亮映射配置 ==================
// TabBar 标识(使用各 Tab 根页面的 wap_url)
export const TAB_BAR_KEYS = {
HOME: '/pages/index/index',
CATEGORY: '/pages_goods/category',
MERCHANT: '/pages_promotion/merch/merchcategory',
CART: '/pages_goods/cart',
MEMBER: '/pages_tool/member/index'
};
// 明确不归属任何 TabBar 的独立页面:进入这些页面时 TabBar 全不亮
export const NO_TAB_PAGES = new Set([
// 登录/注册/找回密码
'/pages_tool/login/login',
'/pages_tool/login/register',
'/pages_tool/login/find',
// 支付/收银流程
'/pages_tool/pay/index',
'/pages_tool/pay/wx_pay',
'/pages_tool/pay/cashier',
'/pages_tool/pay/result',
// 订单独立流程页(详情/退款/物流/评价)
'/pages_order/payment',
'/pages_order/detail',
'/pages_order/detail_local_delivery',
'/pages_order/detail_pickup',
'/pages_order/detail_point',
'/pages_order/refund',
'/pages_order/refund_detail',
'/pages_order/refund_goods_select',
'/pages_order/refund_type_select',
'/pages_order/refund_batch',
'/pages_order/logistics',
'/pages_order/activist',
'/pages_order/order/evaluate',
'/pages_order/detail_virtual',
// 表单
'/pages_tool/form/form',
'/pages_tool/form/formdata',
// 外部/特殊页面
'/pages_tool/webview/webview',
'/pages_tool/ai-chat/index',
'/pages_tool/vr/index',
'/pages_tool/cases/index',
'/pages_tool/contact/contact',
'/pages_tool/weapp/order_shipping',
'/pages_tool/file-preview/file-preview',
// 帮助/公告/文章详情
'/pages_tool/help/detail',
'/pages_tool/notice/detail',
'/pages_tool/article/detail',
// 介质材料
'/pages_tool/seal/structure',
'/pages_tool/seal/medium/detail'
]);
// 页面级 TabBar 精确归属映射(key: 页面路径, value: TabBar 根路径)
// 未在此映射中的页面,优先走 NO_TAB_PAGES 判断,其次走同分包回退逻辑
export const PAGE_TAB_MAP = {
// 首页
'/pages/index/index': TAB_BAR_KEYS.HOME,
'/pages/empty/index': TAB_BAR_KEYS.HOME,
// 分类
'/pages_goods/category': TAB_BAR_KEYS.CATEGORY,
'/pages_goods/list': TAB_BAR_KEYS.CATEGORY,
'/pages_goods/detail': TAB_BAR_KEYS.CATEGORY,
'/pages_tool/goods/search': TAB_BAR_KEYS.CATEGORY,
'/pages_tool/goods/brand': TAB_BAR_KEYS.CATEGORY,
'/pages_tool/goods/coupon': TAB_BAR_KEYS.CATEGORY,
'/pages_tool/goods/coupon_receive': TAB_BAR_KEYS.CATEGORY,
'/pages_tool/goods/evaluate': TAB_BAR_KEYS.CATEGORY,
'/pages_tool/goods/not_exist': TAB_BAR_KEYS.CATEGORY,
// 入驻企业
'/pages_promotion/merch/merchcategory': TAB_BAR_KEYS.MERCHANT,
'/pages_promotion/merch/detail': TAB_BAR_KEYS.MERCHANT,
// 购物车
'/pages_goods/cart': TAB_BAR_KEYS.CART,
// 我的(会员中心及子页面)
'/pages_tool/member/index': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/info': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/info_edit': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/address': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/address_edit': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/account': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/account_edit': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/balance': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/balance_detail': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/point': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/point_detail': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/coupon': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/level': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/level_growth_rules': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/card': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/card_buy': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/card_log': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/card_agreement': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/invite_friends': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/collection': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/footprint': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/contact': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/modify_face': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/pay_password': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/apply_withdrawal': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/withdrawal': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/withdrawal_detail': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/cancellation': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/cancelstatus': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/cancelsuccess': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/cancelrefuse': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/assets': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/signin': TAB_BAR_KEYS.MEMBER,
'/pages_tool/member/bind_mobile': TAB_BAR_KEYS.MEMBER,
// 我的频道下的其他功能页
'/pages_order/list': TAB_BAR_KEYS.MEMBER,
'/pages_tool/recharge/list': TAB_BAR_KEYS.MEMBER,
'/pages_tool/recharge/order_list': TAB_BAR_KEYS.MEMBER,
// 帮助/公告/文章列表属于首页频道
'/pages_tool/help/list': TAB_BAR_KEYS.HOME,
'/pages_tool/notice/list': TAB_BAR_KEYS.HOME,
'/pages_tool/article/list': TAB_BAR_KEYS.HOME,
// 文件列表(视业务可归首页,暂不归任何 Tab)
'/pages_tool/files/list': null
};
/**
* 根据 page_key + params 解析得到实际跳转URL
* @param {string} page_key 页面语义常量,如 PAGE_GOODS_LIST
* @param {Object} params 页面参数(JSON对象)
* @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;
};
/**
* 判断当前路由是否命中 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 数组
* @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;
};
/**
* 解析 link 对象得到最终可跳转的 URL
* 同时把 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(新机制)
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;
};
// 当前最新的tabBar.list (参见pages.json 中的tabBar.list 配置)
export const systemTabBarList = [
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;
});
}
return replacePrefix(url);
}
// 归一化任意路径:确保以 '/' 开头,去除 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;路径无效/未配置 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;
});
};
/**
* 检查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 来源记忆
* @returns {boolean} 是否激活
*/
export const checkTabBarActive = (linkUrl, currentPageRoute, tabBarList, packageEntryTab, currentPageQuery = {}) => {
// 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 (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;
}
export default {
/** 导出页面URL常量 */
MEMBER_PAGE_URL,
CATEGORY_PAGE_URL,
CONTACT_PAGE_URL,
INDEX_PAGE_URL,
LOGIN_PAGE_URL,
AI_CHAT_PAGE_URL,
/** page_key 到路径的映射表(统一管理所有页面) */
PAGE_KEY_MAP,
/** 根据 page_key + params 解析实际URL */
resolvePageUrl,
/** 判断当前路由是否命中 page_routes 列表(支持 PAGE_KEY / 实际路径 / 通配符) */
isPageInRouteList,
/** 解析 link 对象得到最终可跳转的 URL(优先级: page_key → wap_url) */
resolveLinkUrl,
/** 把 page_params 还原成对象(JSON.parse 的安全封装) */
parsePageParams,
/** 判断给定路径是否是 TabBar 根页面(用于动态控制返回按钮显示) */
isTabBarRootPage,
/**
* 页面跳转
* @param {string} to 跳转链接 /pages/idnex/index
* @param {Object} param 参数 {key : value, ...}, 默认{}
* @param {string} mode 模式 navigateTo、redirectTo、reLaunch、tabbar, 默认navigateTo
*/
redirectTo(to, param = {}, mode = 'navigateTo') {
// 1. 拆分 path 与已有 query,避免后面 indexOf('?') 误判
// #ifdef MP-HARMONY
console.log('[navigate][HARMONY] redirectTo entry: to=', to, 'mode=', mode, 'param=', param);
// #endif
const queryIdx = to.indexOf('?');
let path = queryIdx === -1 ? to : to.substring(0, queryIdx);
const existingParams = {};
if (queryIdx !== -1) {
to.substring(queryIdx + 1).split('&').forEach(kv => {
if (!kv) return;
// 用 indexOf('=') 取首个等号,兼容 value 里带 '=' 的场景
const eqIdx = kv.indexOf('=');
const k = eqIdx === -1 ? kv : kv.substring(0, eqIdx);
const v = eqIdx === -1 ? '' : kv.substring(eqIdx + 1);
if (k) existingParams[k] = v;
});
}
// 2. 合并 param:同 key 时新值覆盖旧值,杜绝 ?name=A&name=B 这种重复
const merged = Object.assign({}, existingParams, param || {});
// 3. 参数编码策略,按平台区分:
// - 小程序/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]))
.join('&');
let url = path + (queryStr ? '?' + queryStr : '');
// 替换url中的前缀
url = adaptSubpackageUrl(url);
// 针对tabBar.list中的路径,直接切换tabBar
for (let i = 0; i < systemTabBarList.length; i++) {
const tabBarUrl = systemTabBarList[i];
if (url.indexOf(tabBarUrl) == 0) {
// 首页使用switchTab(pages.json中注册的tabBar页面),其他tab页面使用reLaunch
// reLaunch关闭所有页面再打开目标页,确保页面栈与浏览器历史一致,避免返回按钮异常
// 鸿蒙元服务环境下系统 TabBar 点击 uni.switchTab 静默失败, 用 reLaunch 兜底
if (tabBarUrl == INDEX_PAGE_URL) {
// #ifdef MP-HARMONY
// 鸿蒙元服务环境下系统 TabBar 点击 uni.switchTab 静默失败, 用 reLaunch 兜底
// 用不携带 query 的干净路径, 避免 HarmonyOS 对带 query 的 tabBar 跳转有兼容性问题
const cleanUrl = url.split('?')[0];
console.log('[navigate][HARMONY] tabBar INDEX navigate, url=', cleanUrl);
let navigated = false;
uni.switchTab({
url: cleanUrl,
success: () => { navigated = true; console.log('[navigate][HARMONY] switchTab success:', cleanUrl); },
fail: (err) => {
navigated = true;
console.warn('[navigate][HARMONY] switchTab fail, fallback reLaunch:', cleanUrl, err);
uni.reLaunch({ url: cleanUrl });
}
});
// 兜底: 500ms 后若仍无回调或未完成, 强制 reLaunch
// (鸿蒙下 switchTab 经常静默失败, success/fail 都不调用)
setTimeout(() => {
if (!navigated) {
console.warn('[navigate][HARMONY] switchTab silent fail, forcing reLaunch:', cleanUrl);
uni.reLaunch({ url: cleanUrl });
}
}, 500);
// #endif
// #ifndef MP-HARMONY
uni.switchTab({ url });
// #endif
} else {
uni.reLaunch({ url });
}
return;
}
}
// 动态 diy tabBar:如果目标是 diy_bottom_nav 中的 tab 根页
// 注意:uni.switchTab 仅对 pages.json 中 tabBar.list 注册的页面有效,
// 其他 DIY 配置页面调用 switchTab 会静默失败(点击无响应)。
// 因此使用 uni.redirectTo 替换当前页:不进栈 → 无返回按钮 → 对任意页面都有效。
const diyTabBar = store.state.tabBarList;
if (diyTabBar && diyTabBar.list && Array.isArray(diyTabBar.list)) {
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);
return normalizedTab === normalizedUrl;
});
if (isDiyTabRoot) {
const adaptedUrl = adaptSubpackageUrl(cleanUrl);
const inSystemTabBar = systemTabBarList.some(sysUrl => adaptedUrl === sysUrl || cleanUrl === sysUrl);
// 首页用switchTab,其他DIY tab根页用reLaunch确保页面栈干净
let navApi = inSystemTabBar ? (adaptedUrl === INDEX_PAGE_URL ? uni.switchTab : uni.reLaunch) : uni.reLaunch;
// 鸿蒙元服务环境下 uni.switchTab 有兼容性问题, 全部使用 reLaunch
// #ifdef MP-HARMONY
navApi = uni.reLaunch;
// #endif
navApi({
url,
fail: (err) => {
console.error('[navigate] tabBar 跳转失败:', cleanUrl, err);
}
});
return;
}
}
// 如果url不包含tabBarList中的路径,根据mode判断跳转方式
switch (mode) {
case 'tabbar':
// 跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面。重置页面栈,仅保留 tabBar 页面
// 鸿蒙元服务环境下 uni.switchTab 静默失败, 改用 reLaunch
// #ifdef MP-HARMONY
uni.reLaunch({
url
});
// #endif
// #ifndef MP-HARMONY
uni.switchTab({
url
});
// #endif
break;
case 'redirectTo':
// 关闭当前页面,跳转到应用内的某个页面。替换栈顶页面,长度不变(上限10层)
uni.redirectTo({
url
});
break;
case 'reLaunch':
// 关闭所有页面,打开到应用内的某个页面。清空栈后打开新页面,长度=1
uni.reLaunch({
url
});
break;
default:
// 保留当前页面,跳转到应用内的某个页面,新增页面到栈顶,长度+1(上限10层)
uni.navigateTo({
url
});
}
},
/**
* 图片路径转换
* @param {String} img_path 图片地址
* @param {Object} params 参数,针对商品、相册里面的图片区分大中小,size: big、mid、small
*/
img(img_path, params) {
var path = "";
if (img_path != undefined && img_path != "") {
if (img_path.split(',').length > 1) {
img_path = img_path.split(',')[0];
}
if (params && img_path != this.getDefaultImage().goods) {
// 过滤默认图
let arr = img_path.split(".");
let suffix = arr[arr.length - 1];
arr.pop();
arr[arr.length - 1] = arr[arr.length - 1] + "_" + params.size.toUpperCase();
arr.push(suffix);
// if(img_path.indexOf('attachment') == -1){
// img_path = arr.join(".");
// }
}
if (img_path.indexOf("http://") == -1 && img_path.indexOf("https://") == -1) {
path = Config.imgDomain + "/" + img_path;
} else {
// console.log(img_path)
path = img_path;
}
// 修复:去除 URL 中的双斜杠,避免微信 downloadFile 域名匹配失败
path = path.replace(/([^:]\/)\/+/g, "$1");
}
// path += '?t=' + parseInt(new Date().getTime() / 1000);
return path;
},
/**
* 时间戳转日期格式
* @param {Object} timeStamp
*/
timeStampTurnTime(timeStamp, type = "") {
if (timeStamp != undefined && timeStamp != "" && timeStamp > 0) {
var date = new Date();
date.setTime(timeStamp * 1000);
var y = date.getFullYear();
var m = date.getMonth() + 1;
m = m < 10 ? ('0' + m) : m;
var d = date.getDate();
d = d < 10 ? ('0' + d) : d;
var h = date.getHours();
h = h < 10 ? ('0' + h) : h;
var minute = date.getMinutes();
var second = date.getSeconds();
minute = minute < 10 ? ('0' + minute) : minute;
second = second < 10 ? ('0' + second) : second;
if (type) {
if (type == 'yearMonthDay') {
return y + Lang.lang('util.dateYear') + m + Lang.lang('util.dateMonth') + d + Lang.lang('util.dateDay');
}
return y + '-' + m + '-' + d;
} else {
return y + '-' + m + '-' + d + ' ' + h + ':' + minute + ':' + second;
}
} else {
return "";
}
},
/**
* 日期格式转时间戳
* @param {Object} string
*/
timeTurnTimeStamp(string) {
var f = string.split(' ', 2);
var d = (f[0] ? f[0] : '').split('-', 3);
var t = (f[1] ? f[1] : '').split(':', 3);
return (new Date(
parseInt(d[0], 10) || null,
(parseInt(d[1], 10) || 1) - 1,
parseInt(d[2], 10) || null,
parseInt(t[0], 10) || null,
parseInt(t[1], 10) || null,
parseInt(t[2], 10) || null
)).getTime() / 1000;
},
/**
* 倒计时
* @param {Object} seconds 秒
*/
countDown(seconds) {
let [day, hour, minute, second] = [0, 0, 0, 0]
if (seconds > 0) {
day = Math.floor(seconds / (60 * 60 * 24))
hour = Math.floor(seconds / (60 * 60)) - (day * 24)
minute = Math.floor(seconds / 60) - (day * 24 * 60) - (hour * 60)
second = Math.floor(seconds) - (day * 24 * 60 * 60) - (hour * 60 * 60) - (minute * 60)
}
if (day < 10) {
day = '0' + day
}
if (hour < 10) {
hour = '0' + hour
}
if (minute < 10) {
minute = '0' + minute
}
if (second < 10) {
second = '0' + second
}
return {
d: day,
h: hour,
i: minute,
s: second
};
},
/**
* 数值去重
* @param {Array} arr 数组
* @param {string} field 字段
*/
unique(arr, field) {
const res = new Map();
return arr.filter((a) => !res.has(a[field]) && res.set(a[field], 1));
},
/**
* 判断值是否在数组中
* @param {Object} elem
* @param {Object} arr
*/
inArray: function (elem, arr) {
return arr == null ? -1 : arr.indexOf(elem);
},
/**
* 获取某天日期
* @param {Object} day
*/
getDay: function (day) {
var today = new Date();
var targetday_milliseconds = today.getTime() + 1000 * 60 * 60 * 24 * day;
today.setTime(targetday_milliseconds);
const doHandleMonth = function (month) {
var m = month;
if (month.toString().length == 1) {
m = "0" + month;
}
return m
}
var tYear = today.getFullYear();
var tMonth = today.getMonth();
var tDate = today.getDate();
var tWeek = today.getDay();
var time = parseInt(today.getTime() / 1000);
tMonth = doHandleMonth(tMonth + 1);
tDate = doHandleMonth(tDate);
const week = [
Lang.lang('util.weekSunday'),
Lang.lang('util.weekMonday'),
Lang.lang('util.weekTuesday'),
Lang.lang('util.weekWednesday'),
Lang.lang('util.weekThursday'),
Lang.lang('util.weekFriday'),
Lang.lang('util.weekSaturday')
]
return {
't': time,
'y': tYear,
'm': tMonth,
'd': tDate,
'w': week[tWeek]
};
},
/**
* 图片选择加上传
* @param num
* @param params
* @param callback
* @param url
*/
upload: function (num, params, callback, url) {
const { appType, appTypeName } = Platform.getPlatformInfo();
var data = {
token: store.state.token,
app_type: appType,
app_type_name: appTypeName
}
data = Object.assign(data, params);
var imgs_num = num;
var _self = this;
uni.chooseImage({
count: imgs_num,
sizeType: ['compressed'], //可以指定是原图还是压缩图,默认二者都有
sourceType: ['album', 'camera'], //从相册或者拍照
success: async function (res) {
const tempFilePaths = res.tempFilePaths;
var _data = data;
var imgs = [];
uni.showLoading({
title: Lang.lang('util.uploading')
})
for (var i = 0; i < tempFilePaths.length; i++) {
var path = await _self.upload_file_server(tempFilePaths[i], _data, params.path,
url);
imgs.push(path);
if (imgs.length == tempFilePaths.length) {
uni.hideLoading()
uni.showToast({
title: Lang.lang('util.uploadSuccess'),
icon: 'none'
})
typeof callback == 'function' && callback(imgs);
}
}
},
fail: err => {
uni.hideLoading()
uni.showToast({
title: Lang.lang('util.uploadFailed'),
icon: 'none'
})
}
});
},
//上传
upload_file_server(tempFilePath, data, path, url = "", callback) {
if (url) {
var uploadUrl = Config.baseUrl + url
} else {
var uploadUrl = Config.baseUrl + '/api/upload/' + path
}
return new Promise((resolve, reject) => {
uni.uploadFile({
url: uploadUrl,
filePath: tempFilePath,
name: 'file',
fileType: data.fileType || 'image',
formData: data,
success: function (res) {
var path_str = JSON.parse(res.data);
if (path_str.code >= 0) {
resolve(path_str.data.pic_path);
typeof callback == 'function' && callback(path_str.data.pic_path);
} else {
reject("error");
}
}
});
});
},
/**
* 复制
* @param {Object} value
* @param {Object} callback
*/
copy(value, callback) {
// #ifdef H5
var oInput = document.createElement('input'); //创建一个隐藏input(重要!)
oInput.value = value; //赋值
oInput.setAttribute("readonly", "readonly");
document.body.appendChild(oInput);
oInput.select(); // 选择对象
document.execCommand("Copy"); // 执行浏览器复制命令
oInput.className = 'oInput';
oInput.style.display = 'none';
uni.hideKeyboard();
this.showToast({
title: Lang.lang('util.copySuccess')
});
typeof callback == 'function' && callback();
// #endif
// #ifdef MP || APP-PLUS
uni.setClipboardData({
data: value,
success: () => {
typeof callback == 'function' && callback();
}
});
// #endif
},
/**
* 是否是微信浏览器
* @deprecated 请使用 Platform.isWeiXin()
*/
isWeiXin() {
return Platform.isWeiXin();
},
/**
* 判断是否在微信小程序的web-view中打开H5
* @returns {boolean} 是否在微信小程序的web-view中
*
* 鸿蒙元服务(JSVM)没有 navigator 全局,改用 typeof 守卫兜底,让它直接返回 false
*/
isWeChatMiniProgram() {
if (typeof navigator === 'undefined' || !navigator.userAgent) return false;
const userAgent = navigator.userAgent.toLowerCase();
// 微信小程序的web-view User-Agent会包含以下特征之一
const miniProgramIndicators = [
'miniprogram', // 普通微信小程序
'wxwork', // 企业微信小程序
'micromessenger/[0-9]+\.[0-9]+\.[0-9]+ nettype/wifi wxwebviewtype/1'
];
return miniProgramIndicators.some(indicator => {
if (typeof indicator === 'string' && indicator.includes('/')) {
// 正则表达式匹配
const regex = new RegExp(indicator, 'i');
return regex.test(userAgent);
}
return userAgent.includes(indicator);
});
},
/**
* 判断是否在微信浏览器中打开H5(非小程序的web-view)
* @returns {boolean} 是否在微信浏览器中打开的H5
*/
isWeChatBrowser() {
return isWeiXin() && !isWeChatMiniProgram();
},
/**
* 显示消息提示框
* @param {Object} params 参数
*/
showToast(params = {}) {
params.title = params.title || "";
params.icon = params.icon || "none";
// params.position = params.position || 'bottom';
params.duration = params.duration || 1500;
uni.showToast(params);
if (params.success) params.success();
},
/**
* 检测苹果X以上的手机
*/
isIPhoneX() {
let res = this.getDeviceInfo();
if (res.model.search('iPhone X') != -1) {
return true;
}
return false;
},
//判断安卓还是iOS
isAndroid() {
let platform = this.getDeviceInfo().platform;
if (platform == 'ios') {
return false;
} else if (platform == 'android') {
return true;
}
},
/**
* 获取设备信息(包含降级处理)
*/
getDeviceInfo() {
try {
return uni.getDeviceInfo();
} catch (e) {
return uni.getSystemInfoSync();
}
},
/**
* 获取窗口信息(包含降级处理)
*/
getWindowInfo() {
try {
return uni.getWindowInfo();
} catch (e) {
return uni.getSystemInfoSync();
}
},
/**
* 深度拷贝对象
* @param {Object} obj
*/
deepClone(obj) {
const isObject = function (obj) {
return typeof obj == 'object';
}
if (!isObject(obj)) {
throw new Error(Lang.lang('util.deepCloneNotObject'))
}
//判断传进来的是对象还是数组
let isArray = Array.isArray(obj)
let cloneObj = isArray ? [] : {}
//通过for...in来拷贝
for (let key in obj) {
cloneObj[key] = isObject(obj[key]) ? this.deepClone(obj[key]) : obj[key]
}
return cloneObj
},
/**
* 打开微信企业客服
* @param {Function} fallbackFunc 降级处理函数
*/
openWxWorkServiceChat(fallbackFunc) {
// #ifdef MP-WEIXIN
let wxworkConfig = store.state?.wxworkConfig
if (wxworkConfig?.enable && wxworkConfig?.contact_url) {
// 直接使用活码链接跳转
wx.navigateToMiniProgram({
appId: 'wxeb490c6f9b154ef9', // 是企业微信官方小程序的AppID(固定值, 由腾讯企业微信团队维护, 不需要修改,用于展示"添加企业微信联系人"的官方页面)
path: `pages/contacts/externalContactDetail?url=${encodeURIComponent(wxworkConfig?.contact_url)}`,
success: () => {
},
fail: (err) => {
}
});
} else {
fallbackFunc && fallbackFunc();
}
// #endif
},
/**
* 自定义模板的跳转链接
* @param {Object} link
*/
diyRedirectTo(link) {
console.log(`-----> diyRedirectTo`, JSON.stringify(link));
//if (link == null || Object.keys(link).length == 1) return;
if (link.wap_url) {
link.wap_url = decodeEntity(link.wap_url);
}
// 优先用 page_key 解析实际URL(新机制,向后兼容)
const resolvedUrl = resolveLinkUrl(link);
if (link.page_key && resolvedUrl) {
link.wap_url = resolvedUrl;
}
// 外部链接
if (link.wap_url && link.wap_url.indexOf('http') != -1) {
// 公共参数
const params = {
title: link?.title ?? '',
enable_set_title: link?.enable_set_title ? 1 : 0
}
let url = this.normalizeUrl(decodeEntity(link.wap_url));
url = this.appendQueryParams(url, params);
// #ifdef H5
// console.log(`url ---->`, url, link)
window.location.href = url;
// #endif
// #ifdef MP
this.redirectTo('/pages_tool/webview/webview', {
...link,
src: url
});
// #endif
} else if (link.appid) {
// 跳转其他小程序
uni.navigateToMiniProgram({
appId: link.appid,
path: link.page
})
} else if (link.name == 'MOBILE' && !link.wap_url) {
// 拨打电话
uni.makePhoneCall({
phoneNumber: link.mobile,
success: (res) => {
},
fail: (res) => {
}
});
} else if (link.name == 'MEMBER_CONTACT') {
// 客服
let servicerConfig = store.state.servicerConfig
let config = {
type: 'none'
};
// #ifdef H5
config = servicerConfig.h5;
// #endif
// #ifdef MP-WEIXIN
config = servicerConfig.weapp;
// #endif
switch (config.type) {
case 'wxwork':
// 企业微信客服
// #ifdef H5
window.location.href = config.wxwork_url;
// #endif
// #ifdef MP-WEIXIN
this.openWxWorkServiceChat(() => {
wx.openCustomerServiceChat({
extInfo: {
url: config.wxwork_url
},
corpId: config.corpid,
showMessageCard: true,
sendMessageTitle: 'this.sendMessageTitle',
sendMessagePath: 'this.sendMessagePath',
sendMessageImg: 'this.sendMessageImg'
});
});
// #endif
break;
case 'third':
// 第三方客服
window.location.href = config.third_url;
break;
case 'niushop':
// Niushop客服
this.redirectTo('/pages_tool/chat/room', {...link});
break;
case 'weapp':
// 微信小程序,由于需要手动点击按钮触发,所以要跳转到中间页
this.redirectTo(link.wap_url, {...link});
break;
default:
// 拨打客服电话
let siteInfo = store.state.siteInfo;
if (siteInfo && siteInfo.site_tel) {
uni.makePhoneCall({
phoneNumber: siteInfo.site_tel
});
} else {
this.showToast({
title: Lang.lang('customerService.noServiceOffline'),
})
}
}
} else if (link.wap_url) { // 微页面/内部页面
// resolveLinkUrl 已把 wap_url 中的 query 合并到 link.page_params
// 这里过滤掉 link 元数据字段, 避免 wap_url/name/parent/title 等被错误附加到目标 URL
const { wap_url, name, parent, title, enable_set_title, type, appid, page, mobile, image, ...navParams } = link;
this.redirectTo(link.wap_url, { ...navParams, ...(link.page_params || {}) });
}
},
/**
* 获取默认图
*/
getDefaultImage() {
let defaultImg = store.state.defaultImg;
defaultImg.goods = this.img(defaultImg.goods);
defaultImg.head = this.img(defaultImg.head);
defaultImg.store = this.img(defaultImg.store);
defaultImg.article = this.img(defaultImg.article);
defaultImg.kefu = this.img(defaultImg.kefu);
defaultImg.phone = this.img(defaultImg.phone);
return defaultImg;
},
/**
* 判断手机是否为iphoneX系列
*/
uniappIsIPhoneX() {
let isIphoneX = false;
let systemInfo = this.getDeviceInfo();
// #ifdef MP
if (systemInfo.model.search('iPhone X') != -1 || systemInfo.model.search('iPhone 11') != -1 || systemInfo.model.search('iPhone 12') != -1 || systemInfo.model.search('iPhone 13') != -1) {
isIphoneX = true;
}
// #endif
// #ifdef H5
var u = navigator.userAgent;
var isIOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); //ios终端
if (isIOS) {
if (systemInfo.screenWidth == 375 && systemInfo.screenHeight == 812 && systemInfo.pixelRatio == 3) {
isIphoneX = true;
} else if (systemInfo.screenWidth == 414 && systemInfo.screenHeight == 896 && systemInfo.pixelRatio == 3) {
isIphoneX = true;
} else if (systemInfo.screenWidth == 414 && systemInfo.screenHeight == 896 && systemInfo.pixelRatio == 2) {
isIphoneX = true;
}
}
// #endif
return isIphoneX;
},
/**
* 判断手机是否为iphone11系列
*/
uniappIsIPhone11() {
let isIphone11 = false;
let systemInfo = this.getDeviceInfo();
// #ifdef MP
if (systemInfo.model.search('iPhone 11') != -1) {
isIphone11 = true;
}
// #endif
return isIphone11;
},
// #ifdef H5
//判断该浏览器是否为safaria浏览器
isSafari() {
let res = this.getDeviceInfo();
var ua = navigator.userAgent.toLowerCase();
if (ua.indexOf('applewebkit') > -1 && ua.indexOf('mobile') > -1 && ua.indexOf('safari') > -1 &&
ua.indexOf('linux') === -1 && ua.indexOf('android') === -1 && ua.indexOf('chrome') === -1 &&
ua.indexOf('ios') === -1 && ua.indexOf('browser') === -1) {
return true;
} else {
return false;
}
},
// #endif
numberFixed(e, f) {
if (!f) {
f = 0;
}
return Number(e).toFixed(f);
},
/**
* 获取url参数
*/
getUrlCode(callback) {
var url = location.search;
var theRequest = new Object();
if (url.indexOf('?') != -1) {
var str = url.substr(1);
var strs = str.split('&');
for (var i = 0; i < strs.length; i++) {
theRequest[strs[i].split('=')[0]] = strs[i].split('=')[1];
}
}
typeof callback == 'function' && callback(theRequest);
},
/**
* 获取当前页面路由
*/
getCurrRoute() {
let routes = getCurrentPages(); // 获取当前打开过的页面路由数组
return routes.length ? routes[routes.length - 1].route : '';
},
goBack(backUrl = INDEX_PAGE_URL) {
if (getCurrentPages().length == 1) {
this.redirectTo(backUrl);
} else {
uni.navigateBack();
}
},
/**
* 隐藏“返回首页/小房子”按钮
* 这个函数,用到页面show, onshow 的生命周期时
*/
hideHomeButton() {
// #ifdef MP-WEIXIN
wx.hideHomeButton();
// #endif
},
/**
*
* @param val 转化时间字符串 (转化时分秒)
* @returns {string}
*/
getTimeStr(val) {
var h = parseInt(val / 3600).toString();
var m = parseInt((val % 3600) / 60).toString();
if (m.length == 1) {
m = '0' + m;
}
if (h.length == 1) {
h = '0' + h;
}
return h + ':' + m;
},
/**
* 获取定位信息
*/
getLocation(param = {}) {
/*uni.getLocation({
type: param.type ?? 'gcj02',
success: res => {
store.commit('setLocation', res);
typeof param.success == 'function' && param.success(res);
},
fail: res => {
typeof param.fail == 'function' && param.fail(res);
},
complete: res => {
typeof param.complete == 'function' && param.complete(res);
}
});*/
},
// 计算两个经纬度之间的距离
getDistance(lat1, lng1, lat2, lng2) {
var radLat1 = lat1 * Math.PI / 180.0;
var radLat2 = lat2 * Math.PI / 180.0;
var a = radLat1 - radLat2;
var b = lng1 * Math.PI / 180.0 - lng2 * Math.PI / 180.0;
var s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) +
Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2)));
s = s * 6378.137; // EARTH_RADIUS;
s = Math.round(s * 10000) / 10000;
return s;
},
//记录分享人
onSourceMember(source_member) {
Http.sendRequest({
url: '/api/Member/alterShareRelation',
data: {
share_member: source_member,
},
success: res => {
if (res.code >= 0) {
uni.removeStorage({
key: 'source_member',
success: res => {
}
})
}
}
})
},
/**
* 微信订阅消息
*/
subscribeMessage(keywords) {
let url = ""
// #ifdef MP-WEIXIN
url = '/weapp/api/weapp/messagetmplids';
// #endif
// #ifdef MP-ALIPAY
url = '/aliapp/api/aliapp/messagetmplids';
// #endif
Http.sendRequest({
url,
data: {
keywords: keywords
},
success: res => {
if (res.data.length) {
// #ifdef MP-WEIXIN
uni.requestSubscribeMessage({
tmplIds: res.data,
success: (res) => {
},
fail: (res) => {
}
})
// #endif
// #ifdef MP-ALIPAY
my.requestSubscribeMessage({
entityIds: res.data,
success: res => {
},
fail: res => {
}
});
// #endif
}
}
})
},
/**
* 获取小程序分享内容数据
*/
getMpShare(path) {
//如果没有特别指定 则获取当前页面的路由
if (!path) {
let route = this.getCurrentRoute();
path = route.path;
if (path == '/pages_tool/member/index') {
return new Promise((resolve, reject) => {
resolve({})
});
}
}
return new Promise((resolve, reject) => {
Http.sendRequest({
url: "/weapp/api/weapp/share",
data: {
path: path
},
success: res => {
if (res.code >= 0) {
let shareConfig = res.data.data;
if (shareConfig) {
//分享给好友
let appMessageData = {
title: shareConfig.title,
path: shareConfig.path,
imageUrl: shareConfig.imageUrl,
success: res => {
},
fail: res => {
}
}
//分享到朋友圈
let query = '';
if (shareConfig.path.indexOf('?') > 0) {
query = shareConfig.path.split('?')[1];
}
let timeLineData = {
title: shareConfig.title,
query: shareConfig.path,
imageUrl: shareConfig.imageUrl,
}
resolve({
appMessage: appMessageData,
timeLine: timeLineData,
})
} else {
reject(res.data);
}
} else {
reject(res.data)
}
}
});
})
},
/**
* 设置公众号分享
* @param shareData
* @param callback
*/
setPublicShare(shareData, callback) {
if (!this.isWeiXin()) return;
Http.sendRequest({
url: '/wechat/api/wechat/jssdkconfig',
data: {
url: uni.getSystemInfoSync().platform == 'ios' ? uni.getStorageSync('initUrl') : window.location
.href
},
success: res => {
if (res.code == 0) {
var wxJS = new Weixin();
wxJS.init(res.data);
wxJS.weixin.showOptionMenu();
wxJS.setShareData({
title: shareData.title ?? '',
desc: shareData.desc ?? '',
link: shareData.link ?? location.href,
imgUrl: shareData.imgUrl ? this.img(shareData.imgUrl) : ''
}, (res) => {
typeof callback == 'function' && callback(res);
})
}
}
})
},
//获取当前路由
getCurrentRoute() {
let currentRoutes = getCurrentPages(); // 获取当前打开过的页面路由数组
let currentRoute = currentRoutes[currentRoutes.length - 1].route //获取当前页面路由
let currentParam = currentRoutes[currentRoutes.length - 1].options; //获取路由参数
// 拼接参数
let param = [];
for (let key in currentParam) {
param.push(key + '=' + currentParam[key])
}
let currentPath = '/' + currentRoute;
let currentQuery = param.join('&');
if (currentQuery) currentPath += '?' + currentQuery;
return {
path: currentPath,
query: currentQuery,
}
},
//获取分享路由
getCurrentShareRoute(member_id) {
let route = this.getCurrentRoute();
//去掉原来的分享人数据
route.path = route.path.replace(/[?|&]source_member=\d+/, '');
if (member_id) {
//路径的处理
if (route.path.indexOf('?') > 0) {
route.path += '&';
} else {
route.path += '?';
}
route.path += 'source_member=' + member_id;
//参数的处理
if (route.query) {
route.query += '&';
}
route.query += 'source_member=' + member_id;
}
return route;
},
/**
* 对象转 style 字符串
* - 自动将 camelCase 转 kebab-case(fontSize → font-size),CSS 自定义属性(以 -- 开头)保持原样
* - 自动过滤值为 undefined / null / '' 的项,避免输出脏数据
* - 末尾补一个 ';',方便与其他 style 拼接
* - 非对象入参一律返回 ''
* @param {Object} obj
* @return {String}
*/
objToStyle(obj) {
if (!obj || typeof obj !== 'object') return '';
const s = [];
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
const k = keys[i];
const v = obj[k];
if (v === undefined || v === null || v === '') continue;
// 以 -- 开头的 CSS 自定义属性保持原样;其余 camelCase 转 kebab-case
const key = k.indexOf('--') === 0 ? k : k.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
s.push(key + ':' + v);
}
return s.length ? s.join(';') + ';' : '';
},
/**
* 颜色减值
* @param {Object} c1
* @param {Object} c2
* @param {Object} ratio
*/
colourBlend(c1, c2, ratio) {
ratio = Math.max(Math.min(Number(ratio), 1), 0)
let r1 = parseInt(c1.substring(1, 3), 16)
let g1 = parseInt(c1.substring(3, 5), 16)
let b1 = parseInt(c1.substring(5, 7), 16)
let r2 = parseInt(c2.substring(1, 3), 16)
let g2 = parseInt(c2.substring(3, 5), 16)
let b2 = parseInt(c2.substring(5, 7), 16)
let r = Math.round(r1 * (1 - ratio) + r2 * ratio)
let g = Math.round(g1 * (1 - ratio) + g2 * ratio)
let b = Math.round(b1 * (1 - ratio) + b2 * ratio)
r = ('0' + (r || 0).toString(16)).slice(-2)
g = ('0' + (g || 0).toString(16)).slice(-2)
b = ('0' + (b || 0).toString(16)).slice(-2)
return '#' + r + g + b
},
/**
* 生成贝塞尔曲线轨迹
* @param {Object} points
* @param {Object} times
*/
bezier(points, times) {
var bezier_points = [];
var points_D = [];
var points_E = [];
const DIST_AB = Math.sqrt(Math.pow(points[1]['x'] - points[0]['x'], 2) + Math.pow(points[1]['y'] - points[0][
'y'
], 2));
// 邻控制BC点间距
const DIST_BC = Math.sqrt(Math.pow(points[2]['x'] - points[1]['x'], 2) + Math.pow(points[2]['y'] - points[1][
'y'
], 2));
// D每次在AB方向上移动的距离
if (points[0]['x'] > points[2]['x']) {
var EACH_MOVE_AD = -(DIST_AB / times);
// E每次在BC方向上移动的距离
var EACH_MOVE_BE = -(DIST_BC / times);
} else {
var EACH_MOVE_AD = +(DIST_AB / times);
// E每次在BC方向上移动的距离
var EACH_MOVE_BE = +(DIST_BC / times);
}
// 点AB的正切
const TAN_AB = (points[1]['y'] - points[0]['y']) / (points[1]['x'] - points[0]['x']);
// 点BC的正切
const TAN_BC = (points[2]['y'] - points[1]['y']) / (points[2]['x'] - points[1]['x']);
// 点AB的弧度值
const RADIUS_AB = Math.atan(TAN_AB);
// 点BC的弧度值
const RADIUS_BC = Math.atan(TAN_BC);
// 每次执行
for (var i = 1; i <= times; i++) {
// AD的距离
var dist_AD = EACH_MOVE_AD * i;
// BE的距离
var dist_BE = EACH_MOVE_BE * i;
// D点的坐标
var point_D = {};
point_D['x'] = dist_AD * Math.cos(RADIUS_AB) + points[0]['x'];
point_D['y'] = dist_AD * Math.sin(RADIUS_AB) + points[0]['y'];
points_D.push(point_D);
// E点的坐标
var point_E = {};
point_E['x'] = dist_BE * Math.cos(RADIUS_BC) + points[1]['x'];
point_E['y'] = dist_BE * Math.sin(RADIUS_BC) + points[1]['y'];
points_E.push(point_E);
// 此时线段DE的正切值
var tan_DE = (point_E['y'] - point_D['y']) / (point_E['x'] - point_D['x']);
// tan_DE的弧度值
var radius_DE = Math.atan(tan_DE);
// 地市DE的间距
var dist_DE = Math.sqrt(Math.pow((point_E['x'] - point_D['x']), 2) + Math.pow((point_E['y'] - point_D['y']),
2));
// 此时DF的距离
var dist_DF = (dist_AD / DIST_AB) * dist_DE;
// 此时DF点的坐标
var point_F = {};
point_F['x'] = dist_DF * Math.cos(radius_DE) + point_D['x'];
point_F['y'] = dist_DF * Math.sin(radius_DE) + point_D['y'];
bezier_points.push(point_F);
}
return {
'bezier_points': bezier_points
};
},
/**
* 验证手机号
* @param {string} mobile 被验证的mobile
* @return {object} 验证后的结果
**/
verifyMobile(mobile) {
var parse = /^\d{11}$/.test(mobile);
return parse;
},
/**
* 判断是否为快应用环境
* @returns {boolean} 是否为快应用
*/
isQuickApp() {
// #ifndef QUICKAPP-WEBVIEW
let _isQuickApp = false;
try {
const ua = navigator?.userAgent?.toLowerCase();
_isQuickApp = ua.indexOf('quickapp') !== -1;
if (!_isQuickApp) {
// 特殊说明由于网站是嵌入到快应用的Webview组件中<web>,UA中不会包含quickapp字符串,
// 但是会包含uniacid参数,所以这里通过uniacid参数来判断是否为快应用环境
_isQuickApp = ua.indexOf('uniacid=') !== -1;
}
} catch (e) { }
if (!_isQuickApp) {
const systemInfo = this.getDeviceInfo();
_isQuickApp = systemInfo?.platform === 'quickapp' || process?.env?.UNI_PLATFORM === 'quickapp';
}
return _isQuickApp;
// #endif
return true;
},
/**
* 判断是否为华为快应用环境
* @returns {boolean} 是否为华为快应用
*/
isHuaweiQuickApp() {
if (!this.isQuickApp()) return false;
// #ifndef QUICKAPP-WEBVIEW-HUAWEI
const systemInfo = this.getDeviceInfo();
return systemInfo.brand === 'HUAWEI' || systemInfo.manufacturer === 'HUAWEI' ||
(typeof qh !== 'undefined' && qh.platform === 'huawei');
// #endif
return true;
},
/**
* 蛇形命名转驼峰命名
* @param {string} str 蛇形命名字符串
* @returns {string} 驼峰命名字符串
*/
snakeToCamel(str) {
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
},
/**
* 蛇形命名转驼峰命名(递归处理对象中的所有属性名)
* @param {Object} obj 包含蛇形命名字符串属性的对象
* @returns {Object} 驼峰命名字符串属性的对象
*/
snakeToCamelForObj(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
if (Array.isArray(obj)) return obj.map(this.snakeToCamelForObj.bind(this));
const newObj = {};
for (const key in obj) {
if (Object.hasOwnProperty.call(obj, key)) {
const newKey = this.snakeToCamel(key);
newObj[newKey] = this.snakeToCamelForObj(obj[key]);
}
}
return newObj;
},
/**
* 适用于 UniApp 的全平台 URL 标准化函数
* @param {string} urlString - 需要标准化的原始 URL 字符串
* @returns {string} 标准化后的 URL
*/
normalizeUrl(urlString) {
if (!urlString || typeof urlString !== 'string') return '';
let url = urlString.trim();
if (!/^https?:\/\//i.test(url)) {
url = 'https://' + url;
}
// ==================== H5 端处理 (利用原生 URL API) ====================
// #ifdef H5
try {
const urlObj = new URL(url);
urlObj.protocol = urlObj.protocol.toLowerCase();
urlObj.hostname = urlObj.hostname.toLowerCase();
if ((urlObj.protocol === 'http:' && urlObj.port === '80') ||
(urlObj.protocol === 'https:' && urlObj.port === '443')) {
urlObj.port = '';
}
const tempUrl = new URL(urlObj.pathname, urlObj.origin);
urlObj.pathname = tempUrl.pathname;
if (urlObj.search) {
const searchParams = new URLSearchParams(urlObj.search);
const sortedParams = new URLSearchParams();
Array.from(searchParams.keys()).sort().forEach(key => {
sortedParams.append(key, searchParams.get(key));
});
urlObj.search = sortedParams.toString();
}
if (urlObj.hash === '#') urlObj.hash = '';
return urlObj.toString();
} catch (e) {
console.error('H5端 URL 解析失败:', e);
return url;
}
// #endif
// ==================== 非 H5 端处理 (小程序 / App) ====================
// #ifndef H5
try {
const protocolMatch = url.match(/^(https?):\/\//i);
const protocol = protocolMatch ? protocolMatch[1].toLowerCase() + '://' : 'https://';
const withoutProtocol = url.replace(/^https?:\/\//i, '');
const hashIndex = withoutProtocol.indexOf('#');
let hash = '';
let mainPart = withoutProtocol;
if (hashIndex !== -1) {
hash = withoutProtocol.substring(hashIndex);
mainPart = withoutProtocol.substring(0, hashIndex);
}
const pathQuerySplit = mainPart.split('/');
const hostPort = pathQuerySplit.shift();
let pathname = '/' + pathQuerySplit.join('/');
const [hostname, port] = hostPort.split(':');
const lowerHostname = hostname.toLowerCase();
let finalPort = port;
if ((protocol === 'http://' && port === '80') || (protocol === 'https://' && port === '443')) {
finalPort = '';
}
let pathParts = pathname.split('/').filter(p => p !== '' && p !== '.');
const resolvedPath = [];
for (const part of pathParts) {
if (part === '..') {
resolvedPath.pop();
} else {
resolvedPath.push(part);
}
}
pathname = '/' + resolvedPath.join('/');
const queryIndex = pathname.indexOf('?');
let query = '';
if (queryIndex !== -1) {
const queryString = pathname.substring(queryIndex + 1);
pathname = pathname.substring(0, queryIndex);
const params = queryString.split('&').map(pair => {
const [k, v = ''] = pair.split('=');
return { k, v };
});
params.sort((a, b) => a.k.localeCompare(b.k));
query = '?' + params.map(p => `${p.k}=${p.v}`).join('&');
}
const portStr = finalPort ? `:${finalPort}` : '';
return `${protocol}//${lowerHostname}${portStr}${pathname}${query}${hash}`;
} catch (e) {
console.error('非H5端 URL 解析失败:', e);
return url;
}
// #endif
},
/**
* 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
}
}