/** * 全局兜底分享 mixin * * 仅在微信小程序编译产物中被加载: * main.js 中的 import + Vue.mixin 调用被 MP-WEIXIN 预处理宏 包住, * 其它平台编译时整个模块不会进入产物,因此文件内部无需再写条件编译。 * * 背景: * - 微信小程序要求每个要支持「转发 / 朋友圈」的页面显式声明 onShareAppMessage / onShareTimeline, * 否则页面右上角"三个点"菜单不会显示"转发"按钮。 * - 本项目之前只有 ~17 个页面声明了分享钩子,剩余页面无法转发。 * - 业务对"全页面都支持转发 + 复制小程序路径"有强诉求。 * * 设计原则: * 1. 不侵入现有页面:所有页面通过 Vue.mixin 自动获得默认行为 * 2. 已有 onShareAppMessage / onShareTimeline 的页面优先级更高(uni-app 会优先用页面自身的钩子) * 3. 默认分享内容由后端 /weapp/api/weapp/share 按当前路由下发, * 拉取失败时退化到站点名 + 当前路径,避免完全无法分享 * 4. 暴露 $mpShare 工具方法(currentPath / copyPath / refresh), * 方便子页面在 onShareAppMessage 中复用 path */ // #ifdef MP-WEIXIN const _safeGetCurrentPage = () => { try { const pages = typeof getCurrentPages === 'function' ? getCurrentPages() : []; return pages.length > 0 ? pages[pages.length - 1] : null; } catch (e) { return null; } }; /** * 关闭分享能力的页面 route 列表(不带前导斜杠,与 mp-weixin page.route 保持一致) * * 为什么需要: * mp-share-default.js 通过 Vue.mixin 全局注入 onShareAppMessage / onShareTimeline, * 但有些页面不适合"分享": * 1) webview 容器页: 转发它等于转发一个 iframe 壳,无意义且会暴露内部 src * 2) 隐私弹窗 / 登录中转页: 状态不稳定,转出去体验差 * 3) 任何业务明确要求"不分享"的页面 * * 命中后行为: * - mixin 内部不再拉取分享配置(节省一次 HTTP 请求) * - mixin 注入的 onShareAppMessage / onShareTimeline / $mpShare_* 工具方法仍然存在, * 但内部立即调用 wx.hideShareMenu() 把转发按钮从右上角菜单中隐藏 * - 这两条防线双保险:即使 mixin 钩子被业务侧转发链路误触发,也不会显示分享弹窗 * * 备选方案对比(都没选,原因列在下方): * - 在 created 里 this.onShareAppMessage = undefined: * Vue mixin 合并策略会把 undefined 视为"未声明",mixin 的钩子仍然生效 → 不可靠 * - mixin 内 return null / 不 return: * 微信会回退到默认 title 转发,按钮仍显示 → 不可靠 * - 使用 wx.hideShareMenu(本方案采用): * 微信官方 API,会立即把"转发"按钮从当前页右上角菜单中隐藏,行为最稳 */ const _DISABLED_PAGES = new Set([ 'pages_tool/webview/webview', // webview 容器,转发无意义 ]); const _isCurrentPageDisabled = () => { const page = _safeGetCurrentPage(); if (!page) return false; const route = (page.route || (page.$page && page.$page.path) || '').replace(/^\//, ''); return _DISABLED_PAGES.has(route); }; /** * 主动隐藏当前页的"转发"按钮 * - 只能在 mp-weixin 环境调用(其它平台 wx 全局不存在) * - 必须在 onLoad 之后调用(需要 Page 实例已注册完成) * - 失败时静默吞掉(权限/版本/非微信环境都可能导致失败) */ const _hideShareMenu = () => { try { // #ifdef MP-WEIXIN if (typeof wx !== 'undefined' && typeof wx.hideShareMenu === 'function') { wx.hideShareMenu({ menus: ['shareAppMessage', 'shareTimeline'] }); } // #endif } catch (e) { // 静默:旧基础库可能不支持 menus 参数,降级到无参版本 try { // #ifdef MP-WEIXIN if (typeof wx !== 'undefined' && typeof wx.hideShareMenu === 'function') { wx.hideShareMenu(); } // #endif } catch (e2) { /* ignore */ } } }; const _getCurrentPath = (withQuery = true) => { const page = _safeGetCurrentPage(); if (!page) return ''; // page.route 在 mp-weixin 上不带前导斜杠、不带 query;page.options 携带 query const route = page.route || (page.$page && page.$page.path) || ''; const options = page.options || {}; if (!withQuery) return route ? '/' + route.replace(/^\//, '') : ''; const query = Object.keys(options) .filter(k => options[k] !== undefined && options[k] !== null && options[k] !== '') .map(k => `${encodeURIComponent(k)}=${encodeURIComponent(options[k])}`) .join('&'); return query ? `/${route}?${query}` : `/${route}`; }; // 模块级缓存:避免重复请求后端 let _shareCache = null; let _shareCacheAt = 0; const SHARE_CACHE_TTL = 60 * 1000; // 60s const _fetchShareFromServer = (path) => { const Http = require('./http.js').default || require('./http.js'); return new Promise((resolve) => { Http.sendRequest({ url: '/weapp/api/weapp/share', data: { path }, success: (res) => { if (res && res.code >= 0 && res.data && res.data.data) { resolve(res.data.data); } else { resolve(null); } }, fail: () => resolve(null) }); }); }; const _buildFallback = (path) => ({ title: '小程序分享', path, imageUrl: '' }); const _getShareConfig = async (path) => { const now = Date.now(); if (_shareCache && (now - _shareCacheAt) < SHARE_CACHE_TTL) { return _shareCache; } const cfg = await _fetchShareFromServer(path); _shareCache = cfg || _buildFallback(path); _shareCacheAt = now; return _shareCache; }; export default { data() { return { __mpShareCache: null, // 当前页面的分享配置 }; }, /** * 在实例创建阶段检查当前页面是否在 _DISABLED_PAGES 列表中。 * 命中后调用 wx.hideShareMenu 把当前页的"转发"按钮从右上角菜单中隐藏。 */ created() { try { if (_isCurrentPageDisabled()) { // 立即隐藏(此时 wx 还没完全就绪,先尝试一次;失败也无所谓,onLoad 会再调一次) _hideShareMenu(); } } catch (e) {} }, onLoad() { try { if (_isCurrentPageDisabled()) { // 命中关闭列表的页面: // 1) 再次调 wx.hideShareMenu(此时 Page 已就绪,稳定生效) // 2) 不再拉取分享配置,避免无谓请求 _hideShareMenu(); return; } const path = _getCurrentPath(true); _getShareConfig(path).then(cfg => { // 后端未返回配置时,注入站点名作为默认标题 if (cfg && !cfg.title) { const siteName = (this.$store && this.$store.state && this.$store.state.siteInfo && this.$store.state.siteInfo.site_name) || ''; cfg.title = siteName || '小程序分享'; } this.__mpShareCache = cfg; }); } catch (e) {} }, onShow() { // 兜底:进入页面后如果尚未就绪,再异步拉一次 if (this.__mpShareCache) return; if (_isCurrentPageDisabled()) return; // 命中关闭列表,不再拉 try { const path = _getCurrentPath(true); _getShareConfig(path).then(cfg => { this.__mpShareCache = cfg; }); } catch (e) {} }, /** * 全局默认 onShareAppMessage: * uni-app 规范:组件自身的钩子优先级 > mixin,因此已显式声明的页面不受影响。 */ onShareAppMessage() { // 命中禁用列表的页面:即使 wx.hideShareMenu 因某些原因没生效, // 这里直接 return undefined / 也不主动拼 cfg,让微信回退到默认行为 // (主防线是 wx.hideShareMenu,这里只是兜底) if (_isCurrentPageDisabled()) { return undefined; } const cfg = this.__mpShareCache; if (cfg && cfg.title) { return { title: cfg.title, path: cfg.path, imageUrl: cfg.imageUrl || '' }; } // 后端无配置时:使用站点名 + 当前路径 return { title: _buildFallback(_getCurrentPath(true)).title, path: _getCurrentPath(true) }; }, /** * 全局默认 onShareTimeline(朋友圈): * 朋友圈只支持 query(不能直接传 path),从 cfg.path 中抽取 ? 之后的 query。 */ onShareTimeline() { if (_isCurrentPageDisabled()) { return undefined; } const cfg = this.__mpShareCache; if (cfg && cfg.title) { const path = cfg.path || _getCurrentPath(true); const query = path.indexOf('?') > 0 ? path.split('?')[1] : path; return { title: cfg.title, query, imageUrl: cfg.imageUrl || '' }; } const path = _getCurrentPath(true); const query = path.indexOf('?') > 0 ? path.split('?')[1] : ''; return { title: _buildFallback(path).title, query }; }, methods: { /** * 获取当前小程序页面路径(含 query) */ $mpShare_currentPath() { return _getCurrentPath(true); }, /** * 刷新当前页分享配置(用于页面参数变更后想重置转发 path 的场景) */ $mpShare_refresh() { _shareCache = null; _shareCacheAt = 0; return _getShareConfig(_getCurrentPath(true)).then(cfg => { this.__mpShareCache = cfg; return cfg; }); }, /** * 复制当前页面小程序路径到剪贴板 * 微信小程序没有"页面 URL"概念,仅可复制形如: * pages/goods/detail?id=123&source_member=xx * 的小程序内部路径,便于分享者在聊天里粘贴。 * 优先复用 util.copyCurrentPath。 */ $mpShare_copyPath() { const path = _getCurrentPath(true); const text = path.replace(/^\//, ''); // 去掉前导斜杠 if (this.$util && typeof this.$util.copyCurrentPath === 'function') { return this.$util.copyCurrentPath({}); } // util.copyCurrentPath 不存在(极端情况:util 模块未加载)时直接调 uni API return new Promise((resolve, reject) => { uni.setClipboardData({ data: text, success: () => resolve(text), fail: (err) => reject(err) }); }); } } }; // #endif