diff --git a/docs/分享图片获取逻辑.md b/docs/分享图片获取逻辑.md index 41a63dd..0314383 100644 --- a/docs/分享图片获取逻辑.md +++ b/docs/分享图片获取逻辑.md @@ -13,11 +13,15 @@ → 存入 App.shareImageUrl = https://mp.yizhezhe.com/upload/picture/xxx.png [box_app API] - GET /api/mp/apps?identifier=xxx&isPublished=1 - → 返回 { identifier, url, shareImageUrl, ... } + GET /api/mp/apps?identifier=xxx&isPublished=1 (单个应用,太阳码场景) + GET /api/mp/public/share-images (分享图反查表:所有配了图的应用, + **不过滤上架状态**,2026-09-22 起) + → 返回 { url, shareImageUrl } 精简结构 -[小程序 web_view(pages/annex/web_view/index.vue)] - ① 扫码/点击应用 → 获取 shareImageUrl → 本地缓存 +[小程序宿主页(两个,见第三节)] + pages/annex/web_view/index.vue 容器页:扫码 / JumpPath / 分享落地 + pages/index/index.vue 首页壳:兜爸心选应用宫格 location.href 进入的 H5 + ① 获取 shareImageUrl(网络图)→ 内存 + 本地缓存(mixins/shareCard.js) ② 用户点 ··· 转发给好友 → 卡片图 = shareImageUrl(网络图,微信支持) ③ 用户点 ··· 分享到朋友圈 → getImageInfo 转本地路径 → 卡片图 = 本地路径 ``` @@ -34,15 +38,23 @@ - 支持上传 `/api/upload`、编辑回填、单图(maxCount=1) - extra 提示:建议 5:4 比例,公网可访问 -## 三、小程序端获取逻辑(web_view/index.vue) +## 三、小程序端获取逻辑(两个宿主页共用 `mixins/shareCard.js`) -### 三种获取路径(按优先级) +> 宿主页有两个,**分享卡片由谁产出取决于用户是从哪进来的**: +> - `pages/annex/web_view/index.vue` —— 容器页:太阳码扫码 / 分享卡片落地 / JumpPath / 客服 / 登录跳转; +> - `pages/index/index.vue` —— 首页壳:兜爸心选首页的应用宫格用 `location.href` 换 H5(见 `mama-box/js/app.js` +> `jumpApp`),**小程序页面栈不变**,所以从这里进的应用(数胎动、孕检日历…)分享永远发生在首页壳。 +> +> 反查 / 缓存 / 朋友圈本地图转换统一抽到 `mixins/shareCard.js`,两页行为一致。 + +### 四种获取路径(按优先级) | 优先级 | 场景 | 方法 | 说明 | |--------|------|------|------| | ① 首选 | 太阳码扫码 | `_resolveAppScene` | 调 `mp/apps?identifier=xxx` 时**一次请求同时拿** H5 url + shareImageUrl,identifier 严格匹配 | -| ② 兜底 | 点击应用(JumpPath 直传 URL) | `_fetchShareImage` | 按 URL 前缀反查 `mp/apps` 列表,命中写入本地缓存 | +| ② 兜底 | 容器页点击应用(JumpPath 直传 URL) | `_fetchShareImage` | 按 URL 前缀反查 `mp/apps` 列表,命中写入本地缓存 | | ③ 覆盖 | H5 postMessage 自定义 | `handleMessage` 'share' | H5 `bridge.share({imageUrl})` 传了图才覆盖,为空保留 box_app 预取值 | +| ④ 同步 | 首页壳(应用宫格进入任意 H5) | `_loadShareApps` + `resolveShareCard` | 首页壳 onLoad 预热**全量应用表**(内存 + 1h 本地缓存),`onShareAppMessage(options)` 里用 `options.webViewUrl` 同步匹配出图(首次转发就有图);**每次分享都重解析并检测应用切换**,避免沿用上一个应用的图 | ### ① 太阳码扫码(`_resolveAppScene`) @@ -58,13 +70,13 @@ ``` 点应用 → JumpPath 传完整 H5 URL(如 https://mp.yizhezhe.com/app_03/) -→ 1. 查本地缓存 web_view_share_img:{originPath} → 命中直接复用 -→ 2. 未命中 → GET /api/mp/apps?isPublished=1&pageSize=100(拉全量已上架应用) -→ 3. URL 前缀匹配:originPath === appBase || originPath.startsWith(appBase + '/') -→ 4. 命中 → this._applyShareImage(shareImageUrl) + 写入缓存 +→ 1. 先同步查:内存应用表(若已预热)→ 单应用缓存 web_view_share_img:{urlBase}(24h TTL) +→ 2. 未命中 → GET /api/mp/public/share-images(拉分享图反查表,**含未上架应用**,1h 本地缓存) +→ 3. URL 前缀匹配:urlBase === appBase || urlBase.startsWith(appBase + '/') +→ 4. 命中 → this._applyShareImage(shareImageUrl) + 写入单应用缓存 ``` -页面加载时执行,用户点转发时图已就绪。`_shareFetched` 标志保证页面内只请求一次。 +页面加载时执行,用户点转发时图已就绪。`_shareFetched` 标志保证页面内只做一次异步兜底。 ### ③ H5 postMessage 覆盖(`handleMessage`) @@ -74,30 +86,119 @@ H5 bridge.share({ title, imageUrl }) → postMessage → handleMessage 'share' → 为空则保留 box_app 预取的分享图片 ``` -## 四、好友转发 vs 朋友圈(微信平台差异) +### ④ 首页壳同步匹配(`pages/index/index.vue`,2026-09 补) + +> 症状:从兜爸心选首页的应用宫格进入的 H5(如数胎动),点右上角 ··· 转发**没有**后台配置的分享图。 + +原因:宫格用 `location.href` 换 H5,小程序页面栈仍是首页壳 `pages/index/index`;而反查逻辑此前 +只写在容器页,首页壳的 `shareImageUrl` 只能靠 H5 postMessage,数胎动这类 H5 没有 `bridge.share` +→ 卡片图为空 → 微信回退默认截图。 + +``` +onLoad → _loadShareApps() // 预热全量应用表:内存 shareApps + 本地缓存 share_apps_index(1h) +点 ··· → onShareAppMessage(options) +→ target = options.webViewUrl || this.shareHref || this.webUrl // 当前 web-view 真实地址 +→ this.resolveShareCard(target) // 见下:先做应用切换检测,再按当前地址反查 +→ imageUrl = this.shareImageUrl || '' +``` + +`_lookupShareImage` 是**同步**的:先内存应用表,再单应用缓存。因此首次转发就能出图, +不依赖 postMessage 的投递时机。后台改分享图后,最迟 1 小时(列表缓存 TTL)生效。 +朋友圈图仍走 `_prepareTimelineImage`(`getImageInfo` 转本地路径)。 + +> ⚠️ **跨应用残留(2026-09 修)**:症状是「A 应用转发能出图,切到 B 应用转发还是 A 的图」。 +> 根因是首页壳**不随 H5 换页重新 onLoad**(宫格用 `location.href`),而当时分享图取到后就一直 +> 留在 `shareImageUrl`,并且解析被 `if (!this.shareImageUrl)` 短路掉了 —— 于是上一个应用的图钉住不换。 +> +> 修法(`resolveShareCard` / `syncShareCardTarget`,两个宿主页共用): +> 1. `shareImageBase` 记录当前图属于哪个 H5 应用;分享前发现 `target` 换了应用 → **清空** +> `shareImageUrl` / `shareTimelineImage` / `shareTitle` / `shareHref`; +> 2. **每次都按当前 `target` 重新反查**,不再因为已有图就跳过; +> 3. 当前图若来自本应用 H5 的 postMessage(`shareImageSource === 'h5'`)才保留优先, +> 否则 box_app 配置图覆盖; +> 4. 当前应用没有配置图 → 宁可留空回退微信默认截图,也不沿用上一个应用的图。 +> +> 局限与正解:`onShareTimeline` 拿不到 `webViewUrl`,宿主无法自己判断当前应用。 +> 正解是 **H5 主动上报当前位置**:换页/路由切换时发 +> `wx.miniProgram.postMessage({ data: { action: 'location', href: location.href } })`, +> 宿主 `webviewHost.handleHostMessage` 收到后更新 `shareHref` 并调 `resolveShareCard(href)`, +> 朋友圈分享即拿到正确的落地页与分享图(共享库内置 `wxAuth.reportLocation()` / `wxAuth.watchLocation()`)。 +> H5 侧完整接入说明见 `box_app/docs/h5-auth-standard.md` 第四节。 +> 若 H5 未接入该上报,朋友圈分享只能以最后一次好友转发解析出的图为准。 + +## 四、分享路径(深链)—— 2026-09 修复 + +> 症状:在 web-view 的 H5 里点「分享给微信好友」,接收者点开卡片却落到**小程序首页**, +> 而不是发送者当时所在的那个 H5 应用/页面。 + +### 根因 + +1. web-view 里的 H5 **无法用代码拉起分享面板**,卡片只能由承载 `` 的**宿主小程序页**产出。 +2. H5 应用是从 mama-box(首页 web-view)的应用宫格用 `location.href` 换页进入的 + (`mama-box/js/app.js` → `jumpApp`),小程序页面栈**仍是首页** `pages/index/index`。 +3. `app_03` 的 `bridge.share()` 曾在 postMessage 后调 `wx.miniProgram.navigateBack({delta:1})` + 试图触发投递 —— 这会把 web-view 页弹掉(栈里只有 tabBar 根页时还会失败), + 转发随即被**别的页面**接管;首页回调又写死 `path: '/pages/index/index'`, + 于是接收者永远进首页。 +4. `onShareAppMessage` 忽略了微信回调参数 `options.webViewUrl`(当前 web-view 真实地址, + 含 H5 的 SPA 路由),H5 的 postMessage `href` 又因投递延迟不可靠 → 深链丢失。 + +### 修复 + +| 端 | 改动 | +|----|------| +| 小程序首页 `pages/index/index.vue` | `onShareAppMessage(options)` 用 `options.webViewUrl`(兜底 `shareHref` / `webUrl`)拼 `path: '/pages/annex/web_view/index?url=...'`;`onMessage` 支持 `action:'share'/'setTitle'`;补 `onShareTimeline` + `uni.showShareMenu`;`onLoad` 支持 `?url=` 落地(朋友圈转发经此转交容器) | +| 小程序容器 `pages/annex/web_view/index.vue` | `onShareAppMessage(options)` 同样优先 `options.webViewUrl`;`onShareTimeline` 兜底 `shareHref \|\| url` | +| 两页 `stripAuthParams` | 按 key 过滤(不再用会吃掉 `#hash` 的正则),剥 query 的 `openid/unionid/t/_v` 与 hash 的 `openid/unionid/phone/nickname/avatar` | +| H5 `app_03/frontend/src/app/services/bridge.ts` | **删除 `navigateBack`**;`share()` 只发 postMessage 并返回是否在小程序内,页内提示用户点右上角「···」 | + +> **接收者身份**:分享链接一律剥掉发送者身份,接收者经 `pages/annex/web_view` 容器 +> 按自己的 openid 重新注入(容器 `_applyIdentity`,query 传参)。 + +## 五、好友转发 vs 朋友圈(微信平台差异) | 场景 | 图片要求 | 实现 | |------|---------|------| | **转发给好友** `onShareAppMessage` | 支持 https 网络图 | `imageUrl: this.shareImageUrl`(box_app 网络 URL 直接用) | | **分享到朋友圈** `onShareTimeline` | 仅本地路径/代码包路径,网络图静默忽略 | `_prepareTimelineImage` 用 `uni.getImageInfo` 转本地临时路径 `shareTimelineImage` | -## 五、关键前置条件(易踩坑) +> ⚠️ `onShareTimeline` **没有回调参数**,拿不到 `webViewUrl`,只能兜底 `shareHref || url`; +> 它也不能自定义 `path`(只重开当前页 + query),所以首页用 `?url=` 把目标转交 web_view 容器。 + +## 六、关键前置条件(易踩坑) 1. **box_app 已配置分享图**:管理后台 → 应用 → 上传"分享图片" 2. **图片必须公网 https 可访问**(微信服务器要抓取) 3. **mp.yizhezhe.com 必须在微信后台合法域名白名单**: - - `request` 合法域名(拉取 mp/apps 接口) + - `request` 合法域名(拉取 mp/apps 接口;首页壳预热应用表也用它) - `downloadFile` 合法域名(朋友圈转本地图 getImageInfo) - ⚠️ 未配置时:好友图可能正常(网络图),但**朋友圈图转换失败回退默认截图**,`_fetchShareImage` fail 分支会提示 +4. **分享图有缓存,改图不是立刻生效**:反查表缓存 `share_apps_index_v2` 1h,单应用分享图缓存 `web_view_share_img:*` 24h。 + 真机验证新图时可先清缓存(开发者工具 → 清除数据缓存 / 重新编译)或等 TTL 过期。 -## 六、相关代码位置 +> **反查表与上架状态解耦(2026-09-22)**:旧数据源 `/api/mp/apps?isPublished=1` 只回上架应用, +> 导致 mama-box(未建上架记录)和 laixin(下架、经 mama-box 底 tab/来信卡的硬编码兜底 URL 进入) +> 拿不到自定义转发图。现改用专用接口 `GET /api/mp/public/share-images`(box_app `mp_router.py`): +> 返回**所有配置了分享图**的应用(含下架),只含 `url + shareImageUrl` 两字段,按 url 长度倒序 +> (长前缀优先命中,防撞车)。**给「不上宫格但要可进入」的应用配图 = 后台建/编辑记录、传分享图、 +> 保持下架**;宫格展示仍由 `/api/mp/apps?isPublished=1` 决定,互不影响。缓存 key 随之 bump 为 +> `share_apps_index_v2`(旧缓存里只有上架应用,不 bump 会残留 1h)。 + +## 七、相关代码位置 | 位置 | 文件 | |------|------| | box_app 模型字段 | `box_app/backend/app/models/app.py` | | box_app 服务透传 | `box_app/backend/app/services/app_serv.py` | | box_app 表单上传 | `box_app/frontend/src/pages/ProductManagement.jsx` | +| **分享图公共逻辑(两个宿主页共用)** | `crmeb/mixins/shareCard.js` → `resolveShareCard` / `syncShareCardTarget` / `_loadShareApps` / `_lookupShareImage` / `_fetchShareImage` / `_applyShareImage` / `_prepareTimelineImage` | +| **宿主页其它公共能力**(消息分发 / 授权面板 / 授权回传 / openid+unionid) | `crmeb/mixins/webviewHost.js` → `handleHostMessage` / `consumeAuthResult` / `submitHostAuth` | | 小程序扫码取图 | `crmeb/pages/annex/web_view/index.vue` → `_resolveAppScene` | -| 小程序兜底反查 | 同上 → `_fetchShareImage` / `_applyShareImage` / `_prepareTimelineImage` | -| 小程序分享回调 | 同上 → `onShareAppMessage` / `onShareTimeline` / `handleMessage` | -| H5 传图 | `app_03/frontend/src/app/services/bridge.ts` → `share({imageUrl})` | +| 小程序容器页(onLoad 兜底反查 / 转发回调) | 同上 → `_fetchShareImage`(mixin)/ `onShareAppMessage` / `onShareTimeline` / `@message="handleHostMessage"` | +| 小程序首页壳(应用宫格进入的 H5) | `crmeb/pages/index/index.vue` → `onLoad` 预热应用表 + `onShareAppMessage` 同步匹配(带 `spid`)/ `onShareTimeline` / `@message="handleHostMessage"` | +| 首页分享兜底(mama-box 等无 App 记录时) | 首页壳 `getShare()` → CRMEB 后台「分享设置」的 `title` / `img` | +| 分享链接净化 | 两页的 `stripAuthParams` / `dropKeyValuePairs` | +| H5 传图 / 触发分享 | `app_03/frontend/src/app/services/bridge.ts` → `share({title,href,imageUrl})` | + +> 授权/消息这一侧的完整链路(两条:独立授权页 与 宿主页内嵌面板)见 +> `box_app/docs/h5-miniprogram-auth.md` 第 6 节。 diff --git a/mixins/shareCard.js b/mixins/shareCard.js new file mode 100644 index 0000000..1326ff7 --- /dev/null +++ b/mixins/shareCard.js @@ -0,0 +1,234 @@ +// +---------------------------------------------------------------------- +// | 分享卡片:box_app 后台配置的分享图 + H5 postMessage 覆盖 +// +---------------------------------------------------------------------- +// 背景(详见 crmeb/docs/分享图片获取逻辑.md): +// · web-view 里的 H5 不能自己拉起分享面板,卡片只能由承载 的页面产出; +// · 分享图有两个来源,优先级: +// ① 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 = ''; + } + }); + } + } +};