309 lines
12 KiB
JavaScript
309 lines
12 KiB
JavaScript
/**
|
||
* 全局兜底分享 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(options) {
|
||
// 名片「发送名片」按钮(open-type="share")触发: dataset 携带名片信息 → 精准转发该名片
|
||
// (覆盖默认 cfg 逻辑; 微信小程序无法在组件内直接定义转发, 只能由页面 onShareAppMessage 提供)
|
||
if (
|
||
options &&
|
||
options.from === 'button' &&
|
||
options.target &&
|
||
options.target.dataset &&
|
||
options.target.dataset.cardId
|
||
) {
|
||
const d = options.target.dataset;
|
||
const name = d.cardName || '我的名片';
|
||
const img = (this.$util && this.$util.img ? this.$util.img(d.cardQr) : d.cardQr) || '';
|
||
return {
|
||
title: name + ' 的电子名片',
|
||
path: '/pages_tool/card/detail?card_id=' + d.cardId,
|
||
imageUrl: img
|
||
};
|
||
}
|
||
// 命中禁用列表的页面:即使 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
|