Files
crmeb-mp/mixins/shareCard.js
Karlcarlo 91f1ee849c feat(分享图): 反查数据源换 /api/mp/public/share-images(含下架应用)—— mama-box/laixin 可配自定义转发图
- APPS_API 换 box_app 专用反查接口(不再被 isPublished=1 过滤卡住)
- 缓存 key bump share_apps_index → v2(旧缓存只有上架应用,残留 1h)
- shareCard.js 首次入库(此前为未跟踪文件);文档同步数据流与缓存说明
2026-09-23 16:59:55 +08:00

235 lines
11 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.
// +----------------------------------------------------------------------
// | 分享卡片:box_app 后台配置的分享图 + H5 postMessage 覆盖
// +----------------------------------------------------------------------
// 背景(详见 crmeb/docs/分享图片获取逻辑.md):
// · web-view 里的 H5 不能自己拉起分享面板,卡片只能由承载 <web-view> 的页面产出;
// · 分享图有两个来源,优先级:
// ① box_app App 表的 shareImageUrl(按当前 H5 的 URL 前缀反查)
// ② H5 postMessage({action:'share', imageUrl})(有值才覆盖 ①)
// · 有两个宿主页都需要这套逻辑:
// pages/annex/web_view/index.vue —— 容器页(太阳码 / 分享落地 / JumpPath 进入)
// pages/index/index.vue —— 首页壳(mama-box 应用宫格用 location.href 换 H5,
// 小程序页面栈不变,所以这里的分享永远发生在首页壳)
// 本 mixin 把「反查 + 缓存 + 朋友圈本地图转换」抽出来共用,避免两边逻辑漂移。
//
// 用法:
// ① import shareCard from '@/mixins/shareCard.js' → mixins: [shareCard]
// ② 页面 onLoad 调一次 this._loadShareApps()(预热全量应用表,分享时可同步命中)
// ③ onShareAppMessage(options) / 分享前:
// const target = (options && options.webViewUrl) || this.shareHref || this.webUrl;
// this.resolveShareCard(target); // 应用切换检测 + 每次按当前地址重解析(同步出图)
// return { ..., imageUrl: this.shareImageUrl || '' };
//
// ⚠️ 首页壳不会随 H5 应用切换重新 onLoad,所以:
// · 必须缓存「全量应用表」,在分享那一刻用 options.webViewUrl 同步匹配;
// · **不能把 shareImageUrl 当长期状态**:每次分享都要重新解析,并检测 target 是否换了应用,
// 否则 B 应用会沿用 A 应用的图(resolveShareCard / syncShareCardTarget 就是干这个的)。
const BOX_API_BASE = 'https://mp.yizhezhe.com';
// 分享图反查表:box_app 专用接口,**含未上架应用**(mama-box 没有上架记录、laixin
// 已下架但仍经底 tab/来信卡进入,它们也要有自定义转发图)。
// 与「宫格展示」用的 /api/mp/apps?isPublished=1 刻意解耦:上架状态只决定宫格显隐,
// 不决定转发图 —— 反查只看 URL 前缀(接口只回 url+shareImageUrl,长前缀优先)。
const APPS_API = BOX_API_BASE + '/api/mp/public/share-images';
const APPS_CACHE_KEY = 'share_apps_index_v2'; // 反查表本地缓存 { t, items }
// (v2:数据源从上架表换成含下架的反查表,bump key 让旧缓存立即失效)
const APP_IMG_CACHE_PREFIX = 'web_view_share_img:'; // 单个应用分享图缓存(按 URL 基){ t, url }
const APPS_CACHE_TTL = 60 * 60 * 1000; // 应用表缓存 1 小时
const APP_IMG_CACHE_TTL = 24 * 60 * 60 * 1000; // 单应用分享图缓存 24 小时(后台改图最迟一天生效)
export default {
data() {
return {
// ---------- 分享(H5 无法自己拉起分享面板,只能由承载 web-view 的页面转发) ----------
shareTitle: '', // H5 postMessage(action:'share') 自定义标题
shareSummary: '', // 分享摘要(部分场景用)
shareHref: '', // H5 指定的分享链接(兜底;深链优先 webViewUrl)
shareImageUrl: '', // 好友转发卡片图(网络 https)
shareTimelineImage: '', // 朋友圈卡片图(必须本地路径,见 _prepareTimelineImage)
shareImageSource: '', // 当前分享图来源:'box'(box_app 反查)| 'h5'(H5 postMessage 覆盖)
shareImageBase: '', // 当前分享图所属的 H5 URL 基(切换应用时用它丢弃上一张)
shareApps: [], // box_app 应用表(url → shareImageUrl),供同步匹配
shareAppsLoaded: false, // 应用表是否已就绪
shareAppsLoading: false,// 应用表请求中(防并发重入)
_shareFetched: false, // 本页是否已做过一次异步兜底反查
};
},
methods: {
/* 当前 H5 URL 去掉 query / hash / 末尾斜杠,作为应用 url 的匹配基 */
_shareUrlBase(url) {
return String(url || '').split('#')[0].split('?')[0].replace(/\/+$/, '');
},
/* 清空分享卡片状态(应用切换时调用,避免张冠李戴) */
_clearShareCard() {
this.shareImageUrl = '';
this.shareTimelineImage = '';
this.shareImageSource = '';
this.shareTitle = '';
this.shareSummary = '';
this.shareHref = '';
},
/**
* 分享前调用:检测"当前要分享的 H5 是否换了一个应用"。
* 首页壳用 location.href 在 H5 内部换应用,小程序页面不重新 onLoad ——
* 若不检测,上一个应用的分享图/标题/href 会一直留着(本次线上问题的根因)。
* @returns {string} 当前 target 的 URL 基
*/
syncShareCardTarget(targetUrl) {
const base = this._shareUrlBase(targetUrl);
if (!base) return '';
if (this.shareImageBase && this.shareImageBase !== base) {
// 换了应用:丢弃上一应用的分享数据
this._clearShareCard();
}
this.shareImageBase = base;
return base;
},
/**
* 分享前的统一解析(两个宿主页的 onShareAppMessage 都调它):
* ① 应用切换检测 → 换应用先清空;
* ② 每次都按"当前 H5 地址"重新反查 box_app 的 shareImageUrl
* (不能因为 shareImageUrl 已有值就跳过 —— 那可能是上一个应用的图);
* ③ 当前图若来自本应用 H5 的 postMessage(source='h5'),保留不覆盖。
*/
resolveShareCard(targetUrl) {
this.syncShareCardTarget(targetUrl);
const fromBox = this._lookupShareImage(targetUrl);
if (fromBox) {
if (this.shareImageSource !== 'h5') this._applyShareImage(fromBox, 'box', targetUrl);
} else if (this.shareImageSource === 'box') {
// 当前应用没有 box_app 配置图,而现有图是上一个应用反查来的 → 丢弃
this.shareImageUrl = '';
this.shareImageSource = '';
this.shareTimelineImage = '';
}
if (!this.shareAppsLoaded) this._loadShareApps();
},
/* 在内存应用表里按 URL 前缀匹配 shareImageUrl(同步,分享那一刻就能出图) */
_matchAppShareImage(targetUrl) {
const base = this._shareUrlBase(targetUrl);
if (!base) return '';
const apps = this.shareApps || [];
for (let i = 0; i < apps.length; i++) {
const app = apps[i];
if (!app || !app.url || !app.shareImageUrl) continue;
const appBase = this._shareUrlBase(app.url);
if (!appBase) continue;
// 如 https://mp.yizhezhe.com/taidong == https://mp.yizhezhe.com/taidong/
if (base === appBase || base.indexOf(appBase + '/') === 0) return app.shareImageUrl;
}
return '';
},
/* 同步查图:优先内存应用表(onLoad 预热,1h 缓存),再退单应用缓存(24h TTL) */
_lookupShareImage(targetUrl) {
const base = this._shareUrlBase(targetUrl);
if (!base) return '';
// 1) 内存应用表:后台改图后最迟 1h 生效
const fromList = this._matchAppShareImage(targetUrl);
if (fromList) return fromList;
// 2) 单应用缓存兜底(应用表请求失败 / 冷启动还没回来时用)
try {
const cached = uni.getStorageSync(APP_IMG_CACHE_PREFIX + base);
// 兼容旧格式(纯 URL 字符串,无 TTL)
const rec = (cached && typeof cached === 'object') ? cached : { t: 0, url: cached };
if (rec && rec.url && String(rec.url).indexOf('http') === 0 &&
(!rec.t || (Date.now() - rec.t) < APP_IMG_CACHE_TTL)) {
return rec.url;
}
} catch (e) {}
return '';
},
/* 拉全量已上架应用(带 1h 本地缓存);cb 可选,拉完(或命中缓存)后回调 */
_loadShareApps(cb) {
// 1) 本地缓存命中:冷启动也能同步出图
try {
const cached = uni.getStorageSync(APPS_CACHE_KEY);
if (cached && cached.items && cached.items.length && cached.t &&
(Date.now() - cached.t) < APPS_CACHE_TTL) {
this.shareApps = cached.items;
this.shareAppsLoaded = true;
if (cb) cb();
return;
}
} catch (e) {}
// 2) 请求中:直接返回,避免并发重复拉(漏掉的 cb 由调用方自身的兜底路径覆盖)
if (this.shareAppsLoading) return;
this.shareAppsLoading = true;
uni.request({
url: APPS_API,
method: 'GET',
success: (res) => {
const items = (res.data && res.data.result && res.data.result.items) || [];
if (items.length) {
this.shareApps = items;
this.shareAppsLoaded = true;
try { uni.setStorageSync(APPS_CACHE_KEY, { t: Date.now(), items: items }); } catch (e) {}
}
if (cb) cb();
},
fail: (err) => {
// 常见原因:mp.yizhezhe.com 未加入小程序后台 request 合法域名白名单
console.warn('[shareCard] 应用表拉取失败(请检查 mp.yizhezhe.com 是否在 request 合法域名):', err);
if (cb) cb();
},
complete: () => { this.shareAppsLoading = false; }
});
},
/* 异步兜底反查(容器页 onLoad 用):同步没命中就拉应用表再匹配一次 */
_fetchShareImage(targetUrl) {
if (!targetUrl) return;
this.syncShareCardTarget(targetUrl); // 建立/校正"当前图属于哪个应用"
if (this.shareImageUrl) return; // H5 postMessage 已给图,不覆盖
if (this._shareFetched) return; // 本页只兜底一次
this._shareFetched = true;
const hit = this._lookupShareImage(targetUrl);
if (hit) { this._applyShareImage(hit, 'box', targetUrl); return; }
this._loadShareApps(() => {
const matched = this._matchAppShareImage(targetUrl);
if (!matched) return;
try { uni.setStorageSync(APP_IMG_CACHE_PREFIX + this._shareUrlBase(targetUrl), { t: Date.now(), url: matched }); } catch (e) {}
this._applyShareImage(matched, 'box', targetUrl);
console.log('[shareCard] 分享图片(兜底): ' + matched);
});
},
/* 统一设置分享图:好友转发用网络图;朋友圈另转本地图(见下)
@param source 'box'(box_app 反查,默认)| 'h5'(H5 postMessage 覆盖)
@param forUrl 该图所属的 H5 地址(用于切换应用时判断是否过期) */
_applyShareImage(url, source, forUrl) {
if (!url) return;
if (forUrl) this.shareImageBase = this._shareUrlBase(forUrl);
if (url === this.shareImageUrl) {
if (source) this.shareImageSource = source;
return; // 同一张图不重复处理(H5 可能重复 postMessage)
}
this.shareImageUrl = url;
this.shareImageSource = source || 'box';
this._prepareTimelineImage(url);
},
/* 朋友圈分享图必须是本地路径/代码包路径(网络图会被微信静默忽略)→ getImageInfo 转本地
注意:getImageInfo 对网络图同样受 downloadFile 合法域名限制,域名未配时朋友圈无自定义图。 */
_prepareTimelineImage(url) {
if (!url) return;
uni.getImageInfo({
src: url,
success: (res) => {
this.shareTimelineImage = res.path || '';
console.log('[shareCard] 朋友圈分享图已转本地:', this.shareTimelineImage);
},
fail: (err) => {
console.warn('[shareCard] 朋友圈分享图转本地失败(网络图/域名问题,回退默认截图):', err);
this.shareTimelineImage = '';
}
});
}
}
};