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 } // 关于页面路径,使用常量导出 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; parts.push(key + '=' + encodeURIComponent(value)); }); 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 * 优先级: page_key → wap_url → null * @param {Object} link * @returns {string|null} */ export const resolveLinkUrl = (link) => { if (!link) return null; // 优先使用 page_key(新机制) if (link.page_key) { let params = link.page_params; if (typeof params === 'string' && params) { try { params = JSON.parse(params); } catch (e) { params = {}; } } const url = resolvePageUrl(link.page_key, params || {}); if (url) return url; } // 回退:旧的 wap_url if (link.wap_url) { return decodeEntity(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, /** 判断给定路径是否是 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('?') 误判 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. 统一编码。decode -> encode 双向 roundtrip 兼具两种语义: // - 调用方传原始值(/pages_tool/index/diy?name=XXX) -> encode 后 %2F%3F... // - 调用方已传编码值(%2F%3F...) -> decode 还原 -> encode 还原回去,不会被二次编码 // - 任何不能 decode 的非法序列(如裸 %) 走 catch 路径,直接 encode const safeEncode = (v) => { const s = v == null ? '' : String(v); try { return encodeURIComponent(decodeURIComponent(s)); } catch (e) { return encodeURIComponent(s); } }; 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关闭所有页面再打开目标页,确保页面栈与浏览器历史一致,避免返回按钮异常 if (tabBarUrl == INDEX_PAGE_URL) { uni.switchTab({ url }); } 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确保页面栈干净 const navApi = inSystemTabBar ? (adaptedUrl === INDEX_PAGE_URL ? uni.switchTab : uni.reLaunch) : uni.reLaunch; navApi({ url, fail: (err) => { console.error('[navigate] tabBar 跳转失败:', cleanUrl, err); } }); return; } } // 如果url不包含tabBarList中的路径,根据mode判断跳转方式 switch (mode) { case 'tabbar': // 跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面。重置页面栈,仅保留 tabBar 页面 uni.switchTab({ url }); 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中 */ isWeChatMiniProgram() { 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`, 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) { // 微页面/内部页面 this.redirectTo(link.wap_url, {...link}); } }, /** * 获取默认图 */ 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组件中,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) { urlObj.searchParams.append(key, String(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('?') ? '&' : '?'; // 遍历参数对象,手动进行 encodeURIComponent 编码并拼接 const queryString = Object.entries(params) .filter(([_, value]) => value !== undefined && value !== null) .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`) .join('&'); // 重新拼接成完整的 URL return `${targetUrl}${separator}${queryString}${hash}`; } catch (e) { console.error('非H5端 URL 参数追加失败:', e); return url; } // #endif } }