- APPS_API 换 box_app 专用反查接口(不再被 isPublished=1 过滤卡住) - 缓存 key bump share_apps_index → v2(旧缓存只有上架应用,残留 1h) - shareCard.js 首次入库(此前为未跟踪文件);文档同步数据流与缓存说明
235 lines
11 KiB
JavaScript
235 lines
11 KiB
JavaScript
// +----------------------------------------------------------------------
|
||
// | 分享卡片: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 = '';
|
||
}
|
||
});
|
||
}
|
||
}
|
||
};
|