Files
lucky_shop/common/js/mp-share-default.js

309 lines
12 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.
/**
* 全局兜底分享 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