Files
lucky_shop/common/js/util.js
ZF sun 95194585d2 refactor: 统一替换小程序事件绑定为@tap并优化跳转逻辑
1. 全局替换@click为@tap以适配uni-app事件规范
2. 优化toLogin方法的路由获取逻辑,增加空值判断
3. 重构redirectTo跳转逻辑,统一tab页跳转使用reLaunch确保页面栈正确
2026-06-27 13:46:29 +08:00

1795 lines
53 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
}
// 关于页面路径,使用常量导出
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'
};
/**
* 根据 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' }
* @returns {boolean} 是否激活
*/
export const checkTabBarActive = (linkUrl, currentPageRoute, tabBarList, packageEntryTab) => {
// 1) 归一化 linkUrl:统一前导 '/' + 子包路径适配 + 去除 query
const normalizedLink = _normalizePath(linkUrl);
const normalizedCurrent = _normalizePath(currentPageRoute);
if (!normalizedLink) return false;
// 2) 精确匹配:当前页面就是 linkUrl 指向的页面
if (normalizedLink === normalizedCurrent) return true;
// 3) 若传入 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;
// 找出当前页面所属的 tab
const matchingTabUrl = _findMatchingTabUrl(currentPageRoute, tabBarList, packageEntryTab);
// 当前页面若命中某个 tab,则只有该 tab 高亮
if (matchingTabUrl) return normalizedLink === matchingTabUrl;
// 4) 同分包回退: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) {
//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组件中<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) {
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
}
}