feat(mp-weixin): 新增全局兜底分享mixin与路径复制能力

为微信小程序端添加全局分享兜底支持,为所有未显式声明分享钩子的页面提供默认转发和朋友圈分享能力,同时新增小程序路径复制工具方法。
This commit is contained in:
2026-07-11 15:19:26 +08:00
parent 397589c4e6
commit a2066285cd
3 changed files with 234 additions and 0 deletions

View File

@@ -0,0 +1,201 @@
/**
* 全局兜底分享 mixin
*
* 仅在微信小程序编译产物中被加载:
* main.js 中的 import + Vue.mixin 调用被 // #ifdef 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;
}
};
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, // 当前页面的分享配置
};
},
onLoad() {
try {
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) {
try {
const path = _getCurrentPath(true);
_getShareConfig(path).then(cfg => {
this.__mpShareCache = cfg;
});
} catch (e) {}
}
},
/**
* 全局默认 onShareAppMessage:
* uni-app 规范:组件自身的钩子优先级 > mixin,因此已显式声明的页面不受影响。
*/
onShareAppMessage() {
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() {
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

View File

@@ -6,6 +6,7 @@ import Lang from './lang.js'
import {
Weixin
} from 'common/js/wx-jssdk.js';
import { copyText } from './uniapp.utils.js';
//
export function decodeEntity(str, amp) {
@@ -1735,6 +1736,28 @@ export default {
}
})
},
/**
* 复制当前页面的小程序路径(含 query)到剪贴板(仅微信小程序)
* 小程序本身没有"页面 URL"概念,仅可复制形如 pages/goods/detail?id=123 的内部路径,
* 便于分享者在聊天里粘贴给对方识别。
* 内部复用 common/js/uniapp.utils.js 导出的 copyText(回调形式),为保持 Promise 一致性包裹一层。
* @param {Object} options { copySuccess: '', copyFailed: '' }
* @returns {Promise<String>} 返回实际复制的文本
*/
// #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(); // 获取当前打开过的页面路由数组

10
main.js
View File

@@ -41,6 +41,16 @@ Vue.prototype.$eventBus = EventBus;
Vue.mixin(globalConfig);
// #ifdef MP-WEIXIN
// 全局兜底分享 mixin:
// 为所有未显式声明 onShareAppMessage / onShareTimeline 的页面提供默认实现,
// 保证"全页面都支持转发 / 朋友圈 / 复制路径"。
// - 已显式声明的页面优先级更高(uni-app 规范),不受影响
// - 后端 /weapp/api/weapp/share 提供标题 / 封面,失败时退化到站点名 + 当前路径
import MpShareDefault from './common/js/mp-share-default.js';
Vue.mixin(MpShareDefault);
// #endif
// 远端 i18n 翻译热更新:复用项目原有"componentRefresh"机制(同 diy.js 下拉刷新)
// lang.js 在翻译覆写更新时 commit('setComponentRefresh'),
// golbalConfig.js 已把 componentRefresh 注册为全局 computed,