diff --git a/.trae-write-probe-662159130 b/.trae-write-probe-662159130 new file mode 100644 index 0000000..78b57df --- /dev/null +++ b/.trae-write-probe-662159130 @@ -0,0 +1 @@ +probe-text \ No newline at end of file diff --git a/common/js/util.js.bak b/common/js/util.js.bak deleted file mode 100644 index c7bdfc0..0000000 --- a/common/js/util.js.bak +++ /dev/null @@ -1,2090 +0,0 @@ -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'; -import { copyText } from './uniapp.utils.js'; - -// 跳转相关工具集(纯函数, 不依赖 util.js 自身, 避免循环依赖) -import { - isHarmonyQuickAppWebview, - _toParamValueString, - parsePageParams as _parsePageParams, - decodeEntity, - cleanParamUrls, - normalizeUrl as _normalizeUrlBase, - appendQueryParams as _appendQueryParamsBase, - extractInternalAppPath as _extractInternalAppPath, - resolveNavMode as _resolveNavMode, - hasStaleUniacidInUrl as _hasStaleUniacidInUrl, - pickCurrentUniacid as _pickCurrentUniacid, - _shouldEncodeUrl, - _safeEncodeForPlatform, - _navigateTo, -} from './util-navigate.js'; - -// TabBar / 子包路径相关工具集(纯函数, 依赖 util-navigate 的 _toParamValueString) -import { - resolvePageUrl as _resolvePageUrl, - isPageInRouteList as isPageInRouteListFn, - resolveLinkUrl as _resolveLinkUrlPure, - makeSystemTabBarList as _makeSystemTabBarList, - adaptSubpackageUrl, - isTabBarRootPage as _isTabBarRootPagePure, - isCurrentPageTabBarRoot as _isCurrentPageTabBarRootPure, - checkTabBarActive as _checkTabBarActivePure, -} from './util-tabbar.js'; - - -// ============================================================= -// HTML 实体解码 / 参数序列化 / page_params 解析 -// (直接 re-export util-navigate 的同名函数, 保持 API 兼容) -// ============================================================= - -export { decodeEntity, cleanParamUrls, _toParamValueString, adaptSubpackageUrl }; - -/** - * 获取系统当前的 uniacid(运行时单点真值)。 - * - * 必须【与 common/js/http.js 发送请求时使用的 uniacid 完全一致】(行 168): - * data.uniacid = uni.getStorageSync('uniacid') || Config.uniacid - * 否则 uniacid 守卫(脏数据判断)和实际网络请求会基于不同的真值, - * 产生不一致行为。 - * - * 真值源: - * 1) uni.getStorageSync('uniacid') — 持久化, 由登录 / 初始化写入 - * 2) Config.uniacid — 编译期兜底(从 .local.config.js / vue.config.js 注入) - * - * 0 视为"无 uniacid": hasStaleUniacidInUrl 收到 0 时直接返回 false (不强行判断)。 - * - * @returns {number} 当前 uniacid; 无法获取时返回 0 - */ -function _getCurrentUniacid() { - let storageVal; - try { - if (typeof uni !== 'undefined' && uni && typeof uni.getStorageSync === 'function') { - storageVal = uni.getStorageSync('uniacid'); - } - } catch (e) {} - let configVal; - try { - if (Config && Config.uniacid) configVal = Config.uniacid; - } catch (e) {} - return _pickCurrentUniacid(storageVal, configVal); -} - -/** - * 把 URL 上的 page_params 还原成对象。是 _toParamValueString 的逆操作。 - * 容错策略:任何解析失败都安全降级到 defaultValue,不抛异常。 - * - * 注意: 与 util-navigate.js 的 parsePageParams 略有差异 —— 本版本对空串 '' 也返回 defaultValue, - * 保持原 util.js 的旧行为(老代码 `'page_params' in link` 但 page_params === '' 时, - * 旧版会落到 defaultValue, 新版会落到 defaultValue 同样的结果)。 - * - * @example - * parsePageParams('{"a":1}') // { a: 1 } - * parsePageParams({a: 1}) // { a: 1 } (已是对象直接返回) - * parsePageParams('[object Object]') // {} (老 bug 残留安全降级) - * parsePageParams('') // {} - * parsePageParams(null) // {} - * parsePageParams(undefined) // {} - * parsePageParams('not json', {x:1}) // { x: 1 } (解析失败走默认值) - * - * @param {*} raw page_params 原始值(可能是 object / string / null / undefined) - * @param {Object} [defaultValue={}] 解析失败时的兜底值 - * @returns {Object} - */ -export function parsePageParams(raw, defaultValue = {}) { - return _parsePageParams(raw, defaultValue); -} - - -/** - * 把任意值(通常是 URL 参数 / query string 解析出的字符串)强转为布尔值。 - * - * 设计动机:URL query 中布尔值的合法写法不止一种—— - * ?show_home=1 -> '1' -> true - * ?show_home=true -> 'true' -> true - * ?show_home=true -> true -> true - * ?show_home=0 -> '0' -> false - * ?show_home=false -> 'false' -> false - * ?show_home= -> '' -> false - * 未传 -> undefined -> false - * - * 与裸 `Boolean(x)` 的差异: - * Boolean('false') === true (任何非空字符串都是 true) - * Boolean('0') === true (同上) - * 本函数对这两个值返回 false,更适合 URL 参数解析场景。 - * - * @param {*} val 任意值 - * @returns {boolean} - */ -export function coerceBool(val) { - if (val === true || val === 1) return true; - if (typeof val === 'string') { - const s = val.trim().toLowerCase(); - return s === '1' || s === 'true'; - } - return false; -} - - -// 关于页面路径,使用常量导出 -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 根路径) -// value 为 null 表示"该页明确不点亮任何 Tab" (e.g. 全屏流程页) -// 未在此映射中的页面, 走 NO_TAB_PAGES 判断 + 下方 PAGE_TAB_WILDCARDS 通配符 + history-walk 同/跨包策略 -export const PAGE_TAB_MAP = { - // 首页 - '/pages/index/index': TAB_BAR_KEYS.HOME, - '/pages/empty/index': TAB_BAR_KEYS.HOME, - - // 分类 — tab 根页面 (子页面用通配符, 见 PAGE_TAB_WILDCARDS) - '/pages_goods/category': TAB_BAR_KEYS.CATEGORY, - '/pages_goods/list': TAB_BAR_KEYS.CATEGORY, - '/pages_goods/detail': TAB_BAR_KEYS.CATEGORY, - - // 入驻企业 — tab 根页面 (子页面用通配符) - '/pages_promotion/merch/merchcategory': TAB_BAR_KEYS.MERCHANT, - '/pages_promotion/merch/detail': TAB_BAR_KEYS.MERCHANT, - - // 购物车 (单页) - '/pages_goods/cart': TAB_BAR_KEYS.CART, - - // 我的 (会员中心 tab 根页面; 子页面用通配符) - '/pages_tool/member/index': 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, - - // 注: /pages_tool/index/diy 不在此映射中 - // 走 history-walk 自动归属: home → diy → HOME; 深链 → diy → 不点亮 -}; - -// 页面级 TabBar 通配符映射 (按子树批量归属, 减少维护成本) -// 匹配规则: pattern 以 '/*' 结尾, '*' 匹配"前缀之后的至少一个字符段" -// 优先级: PAGE_TAB_MAP 精确匹配 > PAGE_TAB_WILDCARDS 通配符 -// 查找函数: util-tabbar.js#lookupPageTabMap -export const PAGE_TAB_WILDCARDS = [ - // 我的频道 — /pages_tool/member/* 下所有子页面 (30+ 页压缩为 1 行) - { pattern: '/pages_tool/member/*', tab: TAB_BAR_KEYS.MEMBER }, - - // 商品工具 — /pages_tool/goods/* 下所有子页面 (search/brand/coupon/...) - { pattern: '/pages_tool/goods/*', tab: TAB_BAR_KEYS.CATEGORY }, -]; - - -// ============================================================= -// 基于 util-tabbar / util-navigate 的本文件 wrapper -// (这些函数都依赖本文件导出的 PAGE_KEY_MAP, 所以要包一层) -// ============================================================= - -/** - * 根据 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 = {}) => { - return _resolvePageUrl(page_key, params, PAGE_KEY_MAP); -}; - -/** - * 判断当前路由是否命中 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) => { - return isPageInRouteListFn(currentRoute, routeList, PAGE_KEY_MAP); -}; - -/** - * 解析 link 对象得到最终可跳转的 URL - * 同时把 wap_url 自带的 query 参数与 page_params 合并, 写入 link.page_params; - * 并把 link.wap_url 改为不含 query 的 path, 避免后续 redirectTo 重复附加 - * 优先级: page_params > wap_url query (新机制优先) - * - * @param {Object} link - * @returns {string|null} - */ -export const resolveLinkUrl = (link) => { - if (!link) return null; - // 0. 先走 util-tabbar 的纯函数(合并 wap_url 的 query 到 page_params) - const after = _resolveLinkUrlPure(link, { - pageKeyMap: PAGE_KEY_MAP, - parsePageParams: parsePageParams, - decodeEntity: decodeEntity, - }); - // 1. 优先使用 page_key(新机制) - if (link.page_key) { - const params = parsePageParams(link.page_params); - const url = resolvePageUrl(link.page_key, params); - if (url) return url; - } - // 2. 回退: 旧 wap_url / 外部 http 链接 - return after; -}; - -// 当前最新的 tabBar.list (由 util-tabbar 的 makeSystemTabBarList 生成) -export const systemTabBarList = _makeSystemTabBarList( - INDEX_PAGE_URL, - CATEGORY_PAGE_URL, - CONTACT_PAGE_URL, - MEMBER_PAGE_URL, -); - -/** - * 判断给定路径是否是 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) => { - return _isTabBarRootPagePure(pagePath, tabBarList); -}; - -/** - * 判断**当前**页面是否是 TabBar 根页面 - * 封装了 getCurrentPages() + isTabBarRootPage 的组合,供业务组件在弹窗/操作前判断 - * 例: 登录弹窗在会员中心页打开时,应当跳过 hideTabBar 的调用 - * @returns {boolean} 当前页是 TabBar 项时返回 true,否则 false(getCurrentPages 不可用时也返回 false) - */ -export const isCurrentPageTabBarRoot = () => { - return _isCurrentPageTabBarRootPure({ store }); -}; - -/** - * 检查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 = {}) => { - return _checkTabBarActivePure( - linkUrl, - currentPageRoute, - tabBarList, - packageEntryTab, - currentPageQuery, - { - noTabPages: NO_TAB_PAGES, - pageTabMap: PAGE_TAB_MAP, - pageTabWildcards: PAGE_TAB_WILDCARDS, - }, - ); -}; - - -// ============================================================= -// default export - 保持 this.$util.* 旧 API 完全兼容 -// ============================================================= - -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, - - /** TabBar 业务配置 */ - TAB_BAR_KEYS, - NO_TAB_PAGES, - PAGE_TAB_MAP, - PAGE_TAB_WILDCARDS, - systemTabBarList, - - isHarmonyQuickAppWebview, - - /** 根据 page_key + params 解析实际URL */ - resolvePageUrl, - - /** 判断当前路由是否命中 page_routes 列表(支持 PAGE_KEY / 实际路径 / 通配符) */ - isPageInRouteList, - - /** 解析 link 对象得到最终可跳转的 URL(优先级: page_key → wap_url) */ - resolveLinkUrl, - - /** 把 page_params 还原成对象(JSON.parse 的安全封装) */ - parsePageParams, - - /** 把任意值强转为布尔值,专治 URL query 参数布尔解析(?show_home=1/'true'/true) */ - coerceBool, - - /** HTML 实体解码 */ - decodeEntity, - - /** - * 递归清理任意对象/数组中所有"看起来像 URL"的字符串字段: - * - 去除反引号 / 各种空白 / 零宽字符 - * - 修复 scheme 后多余斜杠(https:////host → https://host) - * - * "看起来像 URL" 的判定: 含 `://`(大小写不敏感) - * - 命中: 走 cleanUrlString - * - 不命中: 原样保留(name / title / 标识符等普通字符串字段不被误伤) - * - * 设计: 不限定 key 列表,递归遍历,新加任何 URL 字段都自动覆盖 - */ - cleanParamUrls, - - /** 判断给定路径是否是 TabBar 根页面(用于动态控制返回按钮显示) */ - isTabBarRootPage, - - /** 判断当前页面是否是 TabBar 根页面(供业务组件在弹窗前判断是否需要隐藏 TabBar) */ - isCurrentPageTabBarRoot, - - /** 检查 tabBar 是否激活(动态 tabBar 高亮判断) */ - checkTabBarActive, - - /** 子包路径前缀映射(/pages/order/ → /pages_order/) */ - adaptSubpackageUrl, - - /** - * URL 规范化(严格规范化已有部分,绝不自动补 https / host) - * @see util-navigate.js#normalizeUrl - */ - normalizeUrl(urlString) { - return _normalizeUrlBase(urlString); - }, - - /** - * 给 URL 追加 query 参数(已存在的 key 会被覆盖) - * @see util-navigate.js#appendQueryParams - */ - appendQueryParams(url, params) { - return _appendQueryParamsBase(url, params); - }, - - /** - * 页面跳转 - * @param {string} to 跳转链接 /pages/idnex/index - * @param {Object} param 参数 {key : value, ...}, 默认{} - * @param {string} mode 模式 navigateTo、redirectTo、reLaunch、tabbar, 默认navigateTo - */ - redirectTo(to, param = {}, mode = 'navigateTo') { - // 注意: URL 清理只在 diyRedirectTo 入口做(更上游,职责清晰)。 - // redirectTo 接收到的 param 应当是干净的;不重复清理,避免性能浪费 + 误伤边界。 - // - // 0. 智能默认 mode (2026-07-23, 修正 2026-07-23): - // 跳登录入口 LOGIN_PAGE_URL 时, 默认使用 reLaunch 模式 (即使调用方没传 mode). - // 修复场景: - // - 注销 / 退出登录后, token 被清空, cancelstatus.vue 等页面再次进入 - // 会调 redirectTo(LOGIN_PAGE_URL), 历史上走 navigateTo 模式, 旧页面 - // (cancelstatus) 还在栈中, 用户按返回键回到 cancelstatus 后又触发 - // onShow 死循环跳登录页. - // 排除场景 (重要, 避免影响): - // - 带 back 参数的: 表示登录后要回到原页面 (订单详情/退款详情等), - // 必须保留 navigateTo 模式让 back 参数生效. 例如: - // refund_detail.vue: redirectTo(LOGIN_PAGE_URL, { back: '/pages_tool/order/refund_detail?id=xxx' }) - // - 显式传 'navigateTo' / 'redirectTo' / 其他 mode: 保持原意. - // 触发条件: mode 是默认 'navigateTo' AND 没传 back 参数 AND 目标是 LOGIN_PAGE_URL. - if (mode === 'navigateTo' && typeof to === 'string' && !param?.back) { - const cleanPath = to.indexOf('?') === -1 ? to : to.substring(0, to.indexOf('?')); - if (cleanPath === LOGIN_PAGE_URL) { - mode = 'reLaunch'; - console.log('[navigate] redirectTo: 跳 LOGIN_PAGE_URL 自动改为 reLaunch 模式 (没带 back 参数)'); - } - } - - // 1. 拆分 path 与已有 query,避免后面 indexOf('?') 误判 - console.log('[navigate] redirectTo entry: to=', to, 'mode=', mode, 'param=', JSON.stringify(param)); - - 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 这种重复 - // page_params 永远放在 URL 最末:从 param 中抽离后追加到 merged 末尾 - // param 已在函数顶部 0 号步骤做了 URL 清理,这里直接用 - const { page_params, ...restParam } = param; - const merged = Object.assign({}, existingParams, restParam); - if (page_params !== undefined) { - merged.page_params = page_params; - } - - // 3. 参数编码策略(委托给 util-navigate 的 _safeEncodeForPlatform) - // - 微信小程序 + 鸿蒙元服务(mp-weixin/mp-harmony):uni.navigateTo / router.pushUrl 内部会自动对 url 做一次编码, - // JS 侧再做 encodeURIComponent 会被平台再编码一层,目标页 onLoad(options) 拿到的 - // 是已编码字符串(典型表现:商品分类 → 商品列表, - // 搜索栏关键词变成 %E6%B2%99%E5%8F%91 乱码) - // => 这里必须保持原始值,完全交给平台原生去编码(_safeEncodeForPlatform - // 在 mp-weixin / mp-harmony 下不做 encode,只做 decode roundtrip 去除双向 encode) - // - H5 / 华为快应用:浏览器/快应用 Webview 不自动编码 url, - // 参数含 & ? = 等特殊字符会被 queryString 错位解析(微信甚至把 & 当 - // 成 top-level 分隔符丢参数) - // => 这里必须手动 encodeURIComponent 一次(_safeEncodeForPlatform 在这些平台下 - // 做 decode roundtrip + encode 一次) - const queryStr = Object.keys(merged) - .map(k => _safeEncodeForPlatform(k) + '=' + _safeEncodeForPlatform(merged[k])) - .join('&'); - - let url = path + (queryStr ? '?' + queryStr : ''); - console.log(`[redirectTo]0 url= ${url}`, { - to, - param - }); - - // 替换url中的前缀 - url = adaptSubpackageUrl(url); - - console.log('[redirectTo]1 to=', to, 'param=', param, '-> url=', url); - - // 针对tabBar.list中的路径,直接切换tabBar - for (let i = 0; i < systemTabBarList.length; i++) { - const tabBarUrl = systemTabBarList[i]; - if (url.indexOf(tabBarUrl) == 0) { - // 首页使用switchTab(pages.json中注册的tabBar页面),其他tab页面使用reLaunch - // reLaunch关闭所有页面再打开目标页,确保页面栈与浏览器历史一致,避免返回按钮异常 - // 鸿蒙元服务环境下系统 TabBar 点击 uni.switchTab 静默失败, 用 reLaunch 兜底 - if (tabBarUrl == INDEX_PAGE_URL) { - // #ifdef MP-HARMONY - // 鸿蒙元服务环境下系统 TabBar 点击 uni.switchTab 静默失败, 用 reLaunch 兜底 - // 用不携带 query 的干净路径, 避免 HarmonyOS 对带 query 的 tabBar 跳转有兼容性问题 - const cleanUrl = url.split('?')[0]; - console.log('[navigate][HARMONY] tabBar INDEX navigate, url=', cleanUrl); - let navigated = false; - uni.switchTab({ - url: cleanUrl, - success: () => { navigated = true; console.log('[navigate][HARMONY] switchTab success:', cleanUrl); }, - fail: (err) => { - navigated = true; - console.warn('[navigate][HARMONY] switchTab fail, fallback reLaunch:', cleanUrl, err); - uni.reLaunch({ url: cleanUrl }); - } - }); - // 兜底: 500ms 后若仍无回调或未完成, 强制 reLaunch - // (鸿蒙下 switchTab 经常静默失败, success/fail 都不调用) - setTimeout(() => { - if (!navigated) { - console.warn('[navigate][HARMONY] switchTab silent fail, forcing reLaunch:', cleanUrl); - uni.reLaunch({ url: cleanUrl }); - } - }, 500); - // #endif - - // #ifndef MP-HARMONY - uni.switchTab({ url }); - // #endif - } else { - uni.reLaunch({ url }); - } - return; - } - } - - // 动态 diy tabBar:如果目标是 diy_bottom_nav 中的 tab 根页 - // 注意:uni.switchTab 仅对 pages.json 中 tabBar.list 注册的页面有效, - // 其他 DIY 配置页面调用 switchTab 会静默失败(点击无响应)。 - // 因此使用 uni.redirectTo 替换当前页:不进栈 → 无返回按钮 → 对任意页面都有效。 - const diyTabBar = store.state.tabBarList; - if (diyTabBar && diyTabBar.list && Array.isArray(diyTabBar.list)) { - const cleanUrl = url.split('?')[0]; - const isExcludedPage = NO_TAB_PAGES.has(cleanUrl); - if (!isExcludedPage) { - const isDiyTabRoot = diyTabBar.list.some(item => { - if (!item || !item.link || !item.link.wap_url) return false; - const normalizedTab = adaptSubpackageUrl(item.link.wap_url.startsWith('/') ? item.link.wap_url : '/' + item.link.wap_url).split('?')[0]; - const normalizedUrl = adaptSubpackageUrl(cleanUrl.startsWith('/') ? cleanUrl : '/' + cleanUrl).split('?')[0]; - return normalizedTab === normalizedUrl; - }); - if (isDiyTabRoot) { - const adaptedUrl = adaptSubpackageUrl(cleanUrl); - const inSystemTabBar = systemTabBarList.some(sysUrl => adaptedUrl === sysUrl || cleanUrl === sysUrl); - // 首页用switchTab,其他DIY tab根页用reLaunch确保页面栈干净 - let navApi = inSystemTabBar ? (adaptedUrl === INDEX_PAGE_URL ? uni.switchTab : uni.reLaunch) : uni.reLaunch; - - // 鸿蒙元服务环境下 uni.switchTab 有兼容性问题, 全部使用 reLaunch - // #ifdef MP-HARMONY - navApi = uni.reLaunch; - // #endif - - navApi({ - url, - fail: (err) => { - console.error('[navigate] tabBar 跳转失败:', cleanUrl, err); - } - }); - return; - } - } - } - - console.log(`====>mode=${mode}, url=${url}`); - - // 如果url不包含tabBarList中的路径,根据mode判断跳转方式 - switch (mode) { - case 'tabbar': - // 跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面。重置页面栈,仅保留 tabBar 页面 - // 鸿蒙元服务环境下 uni.switchTab 静默失败, 改用 reLaunch - // #ifdef MP-HARMONY - uni.reLaunch({ - url - }); - // #endif - - // #ifndef MP-HARMONY - uni.switchTab({ - url - }); - // #endif - break; - case 'redirectTo': - // 关闭当前页面,跳转到应用内的某个页面。替换栈顶页面,长度不变(上限10层) - uni.redirectTo({ - url - }); - break; - case 'reLaunch': - // 关闭所有页面,打开到应用内的某个页面。清空栈后打开新页面,长度=1 - // H5 平台修复 (2026-07-23): - // uni.reLaunch 在 H5 上是 history.back() + router.replace 的组合, - // 并不能真正删除浏览器历史条目。在某些浏览器 (微信内置浏览器 / - // Android 定制浏览器) 中, 按浏览器返回键仍可回到原页面, 导致 - // 注销/退出登录后用户能看到旧会员信息。 - // location.replace 不产生新历史条目, 直接替换当前条目, - // 真正"清空历史栈"。对快应用 (H5 webview 嵌入) 同样生效。 - // #ifdef H5 - if (typeof window !== 'undefined' && typeof window.location !== 'undefined' - && typeof window.location.replace === 'function') { - window.location.replace(url); - return; - } - // #endif - // 小程序平台修复 (2026-07-23): - // 微信小程序在以下场景会抛 "appLaunch with non-empty page stack" native 错误: - // - 短时间内连发 reLaunch (页面销毁未完成, 新 launch 时栈非空) - // - 异步回调/onLoad 中调 reLaunch (上下文丢失) - // 解决方案: - // 1) 跳转锁: 300ms 内不重复 reLaunch (避免连发) - // 2) setTimeout(0) 包裹: 让 native 层先完成当前页面销毁, 再发新 launch - // #ifdef MP - if (typeof globalThis !== 'undefined' && globalThis.__reLaunchLock) { - console.warn('[redirectTo] reLaunch 锁定中, 忽略本次跳转:', url); - return; - } - globalThis.__reLaunchLock = true; - setTimeout(() => { - uni.reLaunch({ url }); - // 300ms 后释放锁 - setTimeout(() => { globalThis.__reLaunchLock = false; }, 300); - }, 0); - return; - // #endif - // APP / 其他平台: 原生 reLaunch - uni.reLaunch({ - url - }); - break; - case 'navigateTo': - default: - // 保留当前页面,跳转到应用内的某个页面,新增页面到栈顶,长度+1(上限10层) - uni.navigateTo({ - url - }); - } - }, - /** - * 图片路径转换 - * @param {String} img_path 图片地址 - * @param {Object} params 参数,针对商品、相册里面的图片区分大中小,size: big、mid、small - */ - img(img_path, params) { - var path = ""; - if (img_path != undefined && img_path != "") { - if (img_path.split(',').length > 1) { - img_path = img_path.split(',')[0]; - } - if (params && img_path != this.getDefaultImage().goods) { - // 过滤默认图 - let arr = img_path.split("."); - let suffix = arr[arr.length - 1]; - arr.pop(); - arr[arr.length - 1] = arr[arr.length - 1] + "_" + params.size.toUpperCase(); - arr.push(suffix); - // if(img_path.indexOf('attachment') == -1){ - // img_path = arr.join("."); - // } - - } - if (img_path.indexOf("http://") == -1 && img_path.indexOf("https://") == -1) { - path = Config.imgDomain + "/" + img_path; - } else { - // console.log(img_path) - path = img_path; - } - // 修复:去除 URL 中的双斜杠,避免微信 downloadFile 域名匹配失败 - path = path.replace(/([^:]\/)\/+/g, "$1"); - } - - // path += '?t=' + parseInt(new Date().getTime() / 1000); - return path; - }, - /** - * 时间戳转日期格式 - * @param {Object} timeStamp - */ - timeStampTurnTime(timeStamp, type = "") { - if (timeStamp != undefined && timeStamp != "" && timeStamp > 0) { - var date = new Date(); - date.setTime(timeStamp * 1000); - var y = date.getFullYear(); - var m = date.getMonth() + 1; - m = m < 10 ? ('0' + m) : m; - var d = date.getDate(); - d = d < 10 ? ('0' + d) : d; - var h = date.getHours(); - h = h < 10 ? ('0' + h) : h; - var minute = date.getMinutes(); - var second = date.getSeconds(); - minute = minute < 10 ? ('0' + minute) : minute; - second = second < 10 ? ('0' + second) : second; - if (type) { - if (type == 'yearMonthDay') { - return y + Lang.lang('util.dateYear') + m + Lang.lang('util.dateMonth') + d + Lang.lang('util.dateDay'); - } - return y + '-' + m + '-' + d; - } else { - return y + '-' + m + '-' + d + ' ' + h + ':' + minute + ':' + second; - } - } else { - return ""; - } - }, - /** - * 日期格式转时间戳 - * @param {Object} string - */ - timeTurnTimeStamp(string) { - var f = string.split(' ', 2); - var d = (f[0] ? f[0] : '').split('-', 3); - var t = (f[1] ? f[1] : '').split(':', 3); - return (new Date( - parseInt(d[0], 10) || null, - (parseInt(d[1], 10) || 1) - 1, - parseInt(d[2], 10) || null, - parseInt(t[0], 10) || null, - parseInt(t[1], 10) || null, - parseInt(t[2], 10) || null - )).getTime() / 1000; - }, - /** - * 倒计时 - * @param {Object} seconds 秒 - */ - countDown(seconds) { - let [day, hour, minute, second] = [0, 0, 0, 0] - if (seconds > 0) { - day = Math.floor(seconds / (60 * 60 * 24)) - hour = Math.floor(seconds / (60 * 60)) - (day * 24) - minute = Math.floor(seconds / 60) - (day * 24 * 60) - (hour * 60) - second = Math.floor(seconds) - (day * 24 * 60 * 60) - (hour * 60 * 60) - (minute * 60) - } - if (day < 10) { - day = '0' + day - } - if (hour < 10) { - hour = '0' + hour - } - if (minute < 10) { - minute = '0' + minute - } - if (second < 10) { - second = '0' + second - } - return { - d: day, - h: hour, - i: minute, - s: second - }; - }, - /** - * 数值去重 - * @param {Array} arr 数组 - * @param {string} field 字段 - */ - unique(arr, field) { - const res = new Map(); - return arr.filter((a) => !res.has(a[field]) && res.set(a[field], 1)); - }, - /** - * 判断值是否在数组中 - * @param {Object} elem - * @param {Object} arr - */ - inArray: function (elem, arr) { - return arr == null ? -1 : arr.indexOf(elem); - }, - /** - * 获取某天日期 - * @param {Object} day - */ - getDay: function (day) { - var today = new Date(); - var targetday_milliseconds = today.getTime() + 1000 * 60 * 60 * 24 * day; - today.setTime(targetday_milliseconds); - - const doHandleMonth = function (month) { - var m = month; - if (month.toString().length == 1) { - m = "0" + month; - } - return m - } - - var tYear = today.getFullYear(); - var tMonth = today.getMonth(); - var tDate = today.getDate(); - var tWeek = today.getDay(); - var time = parseInt(today.getTime() / 1000); - tMonth = doHandleMonth(tMonth + 1); - tDate = doHandleMonth(tDate); - - const week = [ - Lang.lang('util.weekSunday'), - Lang.lang('util.weekMonday'), - Lang.lang('util.weekTuesday'), - Lang.lang('util.weekWednesday'), - Lang.lang('util.weekThursday'), - Lang.lang('util.weekFriday'), - Lang.lang('util.weekSaturday') - ] - return { - 't': time, - 'y': tYear, - 'm': tMonth, - 'd': tDate, - 'w': week[tWeek] - }; - }, - /** - * 图片选择加上传 - * @param num - * @param params - * @param callback - * @param url - */ - upload: function (num, params, callback, url) { - const { appType, appTypeName } = Platform.getPlatformInfo(); - var data = { - token: store.state.token, - app_type: appType, - app_type_name: appTypeName - } - data = Object.assign(data, params); - - var imgs_num = num; - var _self = this; - - uni.chooseImage({ - count: imgs_num, - sizeType: ['compressed'], //可以指定是原图还是压缩图,默认二者都有 - sourceType: ['album', 'camera'], //从相册或者拍照 - success: async function (res) { - const tempFilePaths = res.tempFilePaths; - var _data = data; - var imgs = []; - uni.showLoading({ - title: Lang.lang('util.uploading') - }) - for (var i = 0; i < tempFilePaths.length; i++) { - var path = await _self.upload_file_server(tempFilePaths[i], _data, params.path, - url); - imgs.push(path); - if (imgs.length == tempFilePaths.length) { - uni.hideLoading() - uni.showToast({ - title: Lang.lang('util.uploadSuccess'), - icon: 'none' - }) - typeof callback == 'function' && callback(imgs); - } - } - }, - fail: err => { - uni.hideLoading() - uni.showToast({ - title: Lang.lang('util.uploadFailed'), - icon: 'none' - }) - } - }); - }, - //上传 - upload_file_server(tempFilePath, data, path, url = "", callback) { - if (url) { - var uploadUrl = Config.baseUrl + url - } else { - var uploadUrl = Config.baseUrl + '/api/upload/' + path - } - return new Promise((resolve, reject) => { - uni.uploadFile({ - url: uploadUrl, - filePath: tempFilePath, - name: 'file', - fileType: data.fileType || 'image', - formData: data, - success: function (res) { - var path_str = JSON.parse(res.data); - if (path_str.code >= 0) { - resolve(path_str.data.pic_path); - typeof callback == 'function' && callback(path_str.data.pic_path); - } else { - reject("error"); - } - } - }); - - }); - - }, - /** - * 复制 - * @param {Object} value - * @param {Object} callback - */ - copy(value, callback) { - // #ifdef H5 - var oInput = document.createElement('input'); //创建一个隐藏input(重要!) - oInput.value = value; //赋值 - oInput.setAttribute("readonly", "readonly"); - document.body.appendChild(oInput); - oInput.select(); // 选择对象 - document.execCommand("Copy"); // 执行浏览器复制命令 - oInput.className = 'oInput'; - oInput.style.display = 'none'; - uni.hideKeyboard(); - this.showToast({ - title: Lang.lang('util.copySuccess') - }); - - typeof callback == 'function' && callback(); - // #endif - - // #ifdef MP || APP-PLUS - uni.setClipboardData({ - data: value, - success: () => { - typeof callback == 'function' && callback(); - } - }); - // #endif - }, - /** - * 是否是微信浏览器 - * @deprecated 请使用 Platform.isWeiXin() - */ - isWeiXin() { - return Platform.isWeiXin(); - }, - /** - * 判断是否在微信小程序的web-view中打开H5 - * @returns {boolean} 是否在微信小程序的web-view中 - * - * 鸿蒙元服务(JSVM)没有 navigator 全局,改用 typeof 守卫兜底,让它直接返回 false - */ - isWeChatMiniProgram() { - if (typeof navigator === 'undefined' || !navigator.userAgent) return false; - const userAgent = navigator.userAgent.toLowerCase(); - - // 微信小程序的web-view User-Agent会包含以下特征之一 - const miniProgramIndicators = [ - 'miniprogram', // 普通微信小程序 - 'wxwork', // 企业微信小程序 - 'micromessenger/[0-9]+\.[0-9]+\.[0-9]+ nettype/wifi wxwebviewtype/1' - ]; - - return miniProgramIndicators.some(indicator => { - if (typeof indicator === 'string' && indicator.includes('/')) { - // 正则表达式匹配 - const regex = new RegExp(indicator, 'i'); - return regex.test(userAgent); - } - return userAgent.includes(indicator); - }); - }, - /** - * 判断是否在微信浏览器中打开H5(非小程序的web-view) - * @returns {boolean} 是否在微信浏览器中打开的H5 - */ - isWeChatBrowser() { - return isWeiXin() && !isWeChatMiniProgram(); - }, - /** - * 显示消息提示框 - * @param {Object} params 参数 - */ - showToast(params = {}) { - params.title = params.title || ""; - params.icon = params.icon || "none"; - // params.position = params.position || 'bottom'; - params.duration = params.duration || 1500; - uni.showToast(params); - if (params.success) params.success(); - }, - /** - * 检测苹果X以上的手机 - */ - isIPhoneX() { - let res = this.getDeviceInfo(); - if (res?.model?.search('iPhone X') != -1) { - return true; - } - return false; - }, - //判断安卓还是iOS - isAndroid() { - let platform = this.getDeviceInfo()?.platform; - if (platform == 'ios') { - return false; - } else if (platform == 'android') { - return true; - } - }, - /** - * 获取设备信息(包含降级处理) - */ - getDeviceInfo() { - let info = {}; - try { info = uni.getDeviceInfo(); } catch (e) { - try { info = uni.getSystemInfoSync(); } catch (e) {} - } - return info; - }, - /** - * 获取窗口信息(包含降级处理) - */ - getWindowInfo() { - let info; - try {info = uni.getWindowInfo();} catch (e) { - try {info = uni.getSystemInfoSync();} catch (e) {} - } - return info; - }, - /** - * 深度拷贝对象 - * @param {Object} obj - */ - deepClone(obj) { - const isObject = function (obj) { - return typeof obj == 'object'; - } - - if (!isObject(obj)) { - throw new Error(Lang.lang('util.deepCloneNotObject')) - } - //判断传进来的是对象还是数组 - let isArray = Array.isArray(obj) - let cloneObj = isArray ? [] : {} - //通过for...in来拷贝 - for (let key in obj) { - cloneObj[key] = isObject(obj[key]) ? this.deepClone(obj[key]) : obj[key] - } - return cloneObj - }, - - - - /** - * 打开微信企业客服 - * @param {Function} fallbackFunc 降级处理函数 - */ - openWxWorkServiceChat(fallbackFunc) { - // #ifdef MP-WEIXIN - let wxworkConfig = store.state?.wxworkConfig - if (wxworkConfig?.enable && wxworkConfig?.contact_url) { - // 直接使用活码链接跳转 - wx.navigateToMiniProgram({ - appId: 'wxeb490c6f9b154ef9', // 是企业微信官方小程序的AppID(固定值, 由腾讯企业微信团队维护, 不需要修改,用于展示"添加企业微信联系人"的官方页面) - path: `pages/contacts/externalContactDetail?url=${encodeURIComponent(wxworkConfig?.contact_url)}`, - success: () => { - }, - fail: (err) => { - } - }); - } else { - fallbackFunc && fallbackFunc(); - } - // #endif - }, - - /** - * 自定义模板的跳转链接 - * @param {Object} link - */ - diyRedirectTo(link) { - console.log(`-----> diyRedirectTo`, JSON.stringify(link)); - //if (link == null || Object.keys(link).length == 1) return; - - // 0. 兜底清理 link 里所有"看起来像 URL"的字符串字段 - // 递归遍历,任意 key / 任意嵌套层级都被命中 - // (去除反引号 / 各种空白 / 零宽字符,修复 scheme 后多余斜杠) - // 这是最后一道防线,防止后台脏数据(如 wap_url 字段被粘贴时带 markdown 反引号) - // 把脏 URL 一直传到 web-view,导致微信 web-view 解析失败 - link = this.cleanParamUrls(link) || {}; - - if (link.wap_url) { - link.wap_url = decodeEntity(link.wap_url); - } - - // 修复: wap_url 自带的 uniacid query 必须与系统当前 uniacid 一致 - // 原因: uniacid 是本平台租户 ID,会随租户迁移 / 平台调整而变化。 - // 装修数据里的 wap_url?uniacid=2482 一旦与系统当前 uniacid(比如 2483)不一致, - // 说明这份数据可能是历史遗留或别的租户的(脏数据), - // 不应该被自动改写成内部 SPA(否则会跳到错租户的内页 / 拿错数据)。 - // 应当保守地走外链(整页 / web-view), 让原 URL(及其 uniacid)如实生效。 - // ★ 必须在 resolveLinkUrl 之前判断 —— 否则 URL 的 query 被抽走就分不清了。 - // ★ open_mode=internal 时不覆盖 —— 用户显式强制内部, 表示"我知道我在做什么"。 - if (_hasStaleUniacidInUrl(link.wap_url, _getCurrentUniacid()) - && _resolveNavMode(link) !== 'internal') { - link.open_mode = 'online'; - } - - // 优先用 page_key 解析实际URL(新机制,向后兼容) - const resolvedUrl = resolveLinkUrl(link); - if (link.page_key && resolvedUrl) { - link.wap_url = resolvedUrl; - } - - // 修复: 内部页面被后台存成【绝对 URL】(如 https://host/hwappx/common/pages_tool/form/formdata) - // 时,下方 `indexOf('http') != -1` 会把它误判为"外部链接": - // - H5 走 window.location.href → 整页刷新,点返回时整个 App 重启、启动屏重播、状态/滚动丢失 - // - MP 走 web-view → 本应原生跳转的内部页被塞进 web-view - // 这里把"指向本 App 内部页面"的绝对 URL 还原为相对路径,使其改走下方内部页面分支(SPA 跳转)。 - // - // 由 open_mode 统一控制(顶层字段 或 page_params): - // - 'auto'(默认): 双闸门 —— host 与本 App 域名【完全一致】且路径带 /pages 段才还原。 - // 不同子域/不同域名一律当外链,彻底避免误伤 Shopify、Facebook 等外站。 - // - 'internal': 强制内部 —— 跳过 host 校验, 只要路径带 /pages 段就还原为 SPA 跳转。 - // 用途: 数据里链接域名与当前部署域名不同(如 xcx3 vs xcx6)但确认是同一 App 内部页。 - // - 'online': 强制线上 —— 不还原, 保持整页刷新 / web-view。 - // 用途: 某些自域名页面是非 SPA / 遗留页,必须整页或 web-view 打开。 - if (link.wap_url && /^https?:\/\//i.test(link.wap_url)) { - const navMode = _resolveNavMode(link); // 'internal' | 'online' | 'auto' - if (navMode !== 'online') { - let internalPath; - if (navMode === 'internal') { - // 强制内部: 传 true 跳过 host 校验 - internalPath = _extractInternalAppPath(link.wap_url, true); - } else { - // auto: host 完全一致才还原 - const appHosts = []; - // #ifdef H5 - try { if (typeof location !== 'undefined' && location.hostname) appHosts.push(location.hostname); } catch (e) { } - // #endif - try { if (Config.h5Domain) appHosts.push(Config.h5Domain); } catch (e) { } - try { if (Config.baseUrl) appHosts.push(Config.baseUrl); } catch (e) { } - internalPath = _extractInternalAppPath(link.wap_url, appHosts); - } - if (internalPath) { - link.wap_url = internalPath; - } - } - } - - // 外部链接 - if (link.wap_url && link.wap_url.indexOf('http') != -1) { - // 公共参数 + 透传 page_params(resolveLinkUrl 已把 wap_url 中的 query 合并到 page_params) - const pageParams = (link.page_params && typeof link.page_params === 'object') ? link.page_params : {}; - // page_params 放在最后,确保其在最终 URL 中位于 title/enable_set_title 之后 - const params = Object.assign( - {}, - { - title: link?.title ?? '', - enable_set_title: link?.enable_set_title ? 1 : 0 - }, - pageParams - ); - 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 - console.log(`外部链接,跳转到webview`, url); - this.redirectTo(PAGE_KEY_MAP.PAGE_WEBVIEW, { - ...link, - src: url - }); - // #endif - - } else if (link.appid) { - // 跳转其他小程序 - // page_params 转 query string,拼到 path 后面(resolveLinkUrl 已把 wap_url 中的 query 合并到 page_params) - let path = link.page || ''; - const pageParams = (link.page_params && typeof link.page_params === 'object') ? link.page_params : {}; - const paramKeys = Object.keys(pageParams); - if (paramKeys.length > 0) { - const qs = paramKeys - .filter(k => pageParams[k] !== undefined && pageParams[k] !== null && pageParams[k] !== '') - .map(k => `${k}=${pageParams[k]}`) - .join('&'); - if (qs) { - path += (path.indexOf('?') !== -1 ? '&' : '?') + qs; - } - } - uni.navigateToMiniProgram({ - appId: link.appid, - path - }) - - } else if (link.name == 'MOBILE' && !link.wap_url) { - // 拨打电话 - uni.makePhoneCall({ - phoneNumber: link.mobile, - success: (res) => { - }, - fail: (res) => { - } - }); - - } else if (link.name == 'MEMBER_CONTACT') { - // 客服 - let servicerConfig = store.state.servicerConfig - let config = { - type: 'none' - }; - - // #ifdef H5 - config = servicerConfig.h5; - // #endif - - // #ifdef MP-WEIXIN - config = servicerConfig.weapp; - // #endif - - switch (config.type) { - case 'wxwork': - // 企业微信客服 - - // #ifdef H5 - window.location.href = config.wxwork_url; - // #endif - - // #ifdef MP-WEIXIN - this.openWxWorkServiceChat(() => { - wx.openCustomerServiceChat({ - extInfo: { - url: config.wxwork_url - }, - corpId: config.corpid, - showMessageCard: true, - sendMessageTitle: 'this.sendMessageTitle', - sendMessagePath: 'this.sendMessagePath', - sendMessageImg: 'this.sendMessageImg' - }); - }); - - // #endif - break; - case 'third': - // 第三方客服 - window.location.href = config.third_url; - break; - case 'niushop': - // Niushop客服 - this.redirectTo('/pages_tool/chat/room', { ...link }); - break; - case 'weapp': - // 微信小程序,由于需要手动点击按钮触发,所以要跳转到中间页 - this.redirectTo(link.wap_url, { ...link }); - break; - default: - // 拨打客服电话 - let siteInfo = store.state.siteInfo; - if (siteInfo && siteInfo.site_tel) { - uni.makePhoneCall({ - phoneNumber: siteInfo.site_tel - }); - } else { - this.showToast({ - title: Lang.lang('customerService.noServiceOffline'), - }) - } - } - - } else if (link.wap_url) { // 微页面/内部页面 - // resolveLinkUrl 已把 wap_url 中的 query 合并到 link.page_params - // 这里过滤掉 link 元数据字段, 避免 wap_url/name/parent/title 等被错误附加到目标 URL - // title/enable_set_title/page_params 仍需透传到目标 URL,page_params 放最后 - const { wap_url, name, parent, type, appid, page, mobile, image, ...navParams } = link; - this.redirectTo(link.wap_url, { - ...navParams, - ...(link.title != null && { title: link.title }), - ...(link.enable_set_title != null && { enable_set_title: link.enable_set_title ? 1 : 0 }), - ...(link.page_params || {}) - }); - } - }, - /** - * 获取默认图 - */ - getDefaultImage() { - let defaultImg = store.state.defaultImg; - defaultImg.goods = this.img(defaultImg.goods); - defaultImg.head = this.img(defaultImg.head); - defaultImg.store = this.img(defaultImg.store); - defaultImg.article = this.img(defaultImg.article); - defaultImg.kefu = this.img(defaultImg.kefu); - defaultImg.phone = this.img(defaultImg.phone); - return defaultImg; - }, - /** - * 判断手机是否为iphoneX系列 - */ - uniappIsIPhoneX() { - let isIphoneX = false; - let systemInfo = this.getDeviceInfo(); - // #ifdef MP - if (systemInfo.model.search('iPhone X') != -1 || systemInfo.model.search('iPhone 11') != -1 || systemInfo.model.search('iPhone 12') != -1 || systemInfo.model.search('iPhone 13') != -1) { - isIphoneX = true; - } - // #endif - - // #ifdef H5 - var u = navigator.userAgent; - var isIOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); //ios终端 - if (isIOS) { - if (systemInfo.screenWidth == 375 && systemInfo.screenHeight == 812 && systemInfo.pixelRatio == 3) { - isIphoneX = true; - } else if (systemInfo.screenWidth == 414 && systemInfo.screenHeight == 896 && systemInfo.pixelRatio == 3) { - isIphoneX = true; - } else if (systemInfo.screenWidth == 414 && systemInfo.screenHeight == 896 && systemInfo.pixelRatio == 2) { - isIphoneX = true; - } - } - // #endif - return isIphoneX; - }, - /** - * 判断手机是否为iphone11系列 - */ - uniappIsIPhone11() { - let isIphone11 = false; - let systemInfo = this?.getDeviceInfo(); - // #ifdef MP - if (systemInfo.model.search('iPhone 11') != -1) { - isIphone11 = true; - } - // #endif - return isIphone11; - }, - // #ifdef H5 - //判断该浏览器是否为safaria浏览器 - isSafari() { - 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); - }) - } - } - }) - }, - /** - * 复制当前页面的小程序路径(含 query)到剪贴板(仅微信小程序) - * 小程序本身没有"页面 URL"概念,仅可复制形如 pages/goods/detail?id=123 的内部路径, - * 便于分享者在聊天里粘贴给对方识别。 - * 内部复用 common/js/uniapp.utils.js 导出的 copyText(回调形式),为保持 Promise 一致性包裹一层。 - * @param {Object} options { copySuccess: '', copyFailed: '' } - * @returns {Promise} 返回实际复制的文本 - */ - // #ifdef MP-WEIXIN - copyCurrentPath(options = {}) { - const { copySuccess = '', copyFailed = '' } = options; - const route = this.getCurrentRoute(); - // 去掉前导斜杠,保持与转发 path 兼容 - const text = (route && route.path ? route.path : '').replace(/^\//, ''); - return new Promise((resolve, reject) => { - copyText(text, { copySuccess, copyFailed }, (success, err) => { - if (success) resolve(text); - else reject(err || new Error('复制失败')); - }); - }); - }, - // #endif - //获取当前路由 - 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; - }, - - // #ifdef H5 - /** - * H5 → 原生(快应用 webview)通信:发送一条结构化 postMessage 给宿主 - * 与 stub-app-projects/mp-quickapp 中 onMessage 收信协议一一对应: - * 原生识别 payload.type(payload.action / payload.cmd 兼容)值: - * 'reload' / 'pullRefresh' / 'refresh' → 触发 webview.reload() - * @param {string} cmd 指令名,如 'reload' / 'share' / 'close' - * @param {Object} [extra] 附加字段,会合并进消息体(不可覆盖 type/source/t) - * @returns {boolean} 是否成功投递(非快应用环境会直接返回 false,但不抛错) - */ - notifyNativeHost(cmd, extra) { - try { - if (!cmd || typeof cmd !== 'string') return false; - if (!this.isQuickApp()) return false; - var reserved = { type: cmd, source: 'h5', t: Date.now() }; - var data = reserved; - if (extra && typeof extra === 'object') { - for (var k in extra) { - if (Object.prototype.hasOwnProperty.call(extra, k) && !(k in reserved)) { - data[k] = extra[k]; - } - } - } - var payload = JSON.stringify(data); - // 主通道:window.parent.postMessage - try { - if (window.parent && window.parent !== window && typeof window.parent.postMessage === 'function') { - window.parent.postMessage(payload, '*'); - } - } catch (parentErr) { - console.error('[notifyNativeHost] parent postMessage failed:', parentErr); - } - // 兜底通道:在同窗口内派发 message 事件,部分宿主通过该路径收信 - try { - window.dispatchEvent(new MessageEvent('message', { data: payload })); - } catch (evtErr) { - console.error('[notifyNativeHost] dispatchEvent failed:', evtErr); - } - return true; - } catch (e) { - console.error('[notifyNativeHost] crashed:', e); - return false; - } - }, - // #endif - - // #ifdef H5 - /** - * 通知快应用宿主重新加载当前 webview(对应下拉刷新等场景) - * 对应原生侧 hello.ux 的 onMessage 处理 'reload' 指令 - * @returns {boolean} - */ - requestQuickAppReload() { - return this.notifyNativeHost('reload', { source: 'pullRefresh' }); - }, - // #endif - - /** - * 蛇形命名转驼峰命名 - * @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; - }, - - /** - * rpx 单位按端适配转换(uni-app 标准约定) - * - * 后端 formConfig / diyview 等接口返回的数值字段(label_font_size / input_height / - * row_min_height 等)一律以 rpx 传递。前端处理: - * - 微信小程序 / App / 鸿蒙元服务:原生支持 rpx,直接返回 `${val}rpx` - * - H5(WebView):浏览器不认 rpx,通过 uni.upx2px 转 px 返回 `${uni.upx2px(val)}px` - * - * 主要用于 JS 动态生成的 CSS 值(computed 里的 inline style、CSS 变量赋值)。 - * Vue 的 :style binding 对普通 CSS 属性会自动转 rpx→px,但对 CSS 自定义属性 - * (以 -- 开头的变量)不会做单位转换,因此必须经此方法处理。 - * - * @param {number|string} val 后端返回的 rpx 数值 - * 可以是数字(如 60)、字符串数字('60')、带 px 后缀的字符串('30px',原样返回) - * 或带 rpx 后缀的字符串('60rpx',先剥后缀再处理) - * @returns {string} 带单位的 CSS 值(H5 返回 'Npx',其它端返回 'Nrpx'); - * val 为空/undefined 时返回 '' - */ - rpx(val) { - if (val === undefined || val === null || val === '') return ''; - // 已带 px 后缀的,原样返回 - if (typeof val === 'string' && /px$/i.test(val.trim())) return val.trim(); - // 已带 rpx 后缀的,剥掉(避免 '60rpx' + 'rpx' → '60rpxrpx' 的双重后缀 bug) - if (typeof val === 'string' && /rpx$/i.test(val.trim())) { - val = parseFloat(val); - } - // #ifdef H5 - if (typeof uni !== 'undefined' && typeof uni.upx2px === 'function') { - return uni.upx2px(Number(val) || 0) + 'px'; - } - return (Number(val) || 0) / 2 + 'px'; - // #endif - // #ifndef H5 - return val + 'rpx'; - // #endif - }, - - /** - * 计算最长 label 宽度(rpx 单位) - * - * 与后端 backend/src/app/component/view/form/js/design.js calculateMaxLabelWidth - * 算法对齐:取所有 title 中最长字符数(必填 +1),按 fontSize(rpx)近似换算宽度。 - * 后端在 H5 中按 1rpx = 0.5px 计算宽度,再加 12 边距。 - * - * @param {Array} formData 表单数据数组(item.value.title 存在即参与计算) - * @param {number|string} labelFontSize 标签字号(rpx) - * @returns {number} 最长 label 宽度,单位 rpx - */ - calculateMaxLabelWidth(formData, labelFontSize) { - if (!formData || formData.length === 0) return 0; - const fontSize = parseInt(labelFontSize) || 28; - let maxChars = 0; - formData.forEach(item => { - if (!item.value || !item.value.title) return; - let len = (item.value.title || '').length; - if (item.value.required) len += 1; - if (len > maxChars) maxChars = len; - }); - // chars * (fontSize/2) * 1.1 + 12,单位 px → 转 rpx(×2) - const widthPx = Math.max(40, maxChars * (fontSize / 2) * 1.1 + 12); - return Math.round(widthPx * 2); - }, - -} \ No newline at end of file