- pages.json 移除 14 个提现/分销页面注册(user_cash/user_spread/spreadData/promoter/distributor),文件保留可恢复 - template4.vue 隐藏'立即提现'区块(用户中心可见入口,分销功能已关闭) - record.vue 抽奖记录移除提现收款跳转(改 toast 提示) - web_view 分享图片:太阳码/点击应用时从 box_app 预取 shareImageUrl,H5 postMessage 传图优先 - web_view _fetchOpenidAndLoad 兼容老用户响应(openid 从 userInfo/JWT 兜底解析)并写回缓存 - App.vue 移除 expired=[object Promise] 无意义日志
559 lines
23 KiB
Vue
559 lines
23 KiB
Vue
<template>
|
||
<view>
|
||
<web-view v-if="url" class="web-view" :webview-styles="webviewStyles" :src="url"
|
||
:style="{width: windowW + 'px', height: windowH + 'px'}" @message="handleMessage"></web-view>
|
||
</view>
|
||
</template>
|
||
<script>
|
||
import store from '@/store';
|
||
import { HTTP_REQUEST_URL, TRUSTED_DOMAINS } from '@/config/app';
|
||
import { LOGIN_STATUS } from '@/config/cache';
|
||
|
||
function base64Decode(str) {
|
||
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
|
||
var output = '';
|
||
var i = 0;
|
||
str = String(str).replace(/[^A-Za-z0-9+/=]/g, '');
|
||
while (i < str.length) {
|
||
var b1 = chars.indexOf(str.charAt(i++));
|
||
var b2 = chars.indexOf(str.charAt(i++));
|
||
var b3 = chars.indexOf(str.charAt(i++));
|
||
var b4 = chars.indexOf(str.charAt(i++));
|
||
output += String.fromCharCode((b1 << 2) | (b2 >> 4));
|
||
if (b3 !== 64) output += String.fromCharCode(((b2 & 15) << 4) | (b3 >> 2));
|
||
if (b4 !== 64) output += String.fromCharCode(((b3 & 3) << 6) | b4);
|
||
}
|
||
return output;
|
||
}
|
||
|
||
function getOpenid() {
|
||
let oid = store.state.app.openid;
|
||
if (oid) return oid;
|
||
try { oid = uni.getStorageSync('OPENID'); } catch (e) {}
|
||
if (oid) return oid;
|
||
try {
|
||
const token = store.state.app.token || uni.getStorageSync(LOGIN_STATUS);
|
||
if (token) {
|
||
let parts = token.split('.');
|
||
if (parts.length === 3) {
|
||
let payload = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
||
while (payload.length % 4 !== 0) payload += '=';
|
||
const data = JSON.parse(base64Decode(payload));
|
||
oid = data.openid || '';
|
||
if (oid) {
|
||
store.commit('SET_OPENID', oid);
|
||
return oid;
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.warn('[web_view] JWT decode openid failed:', e.message);
|
||
}
|
||
return '';
|
||
}
|
||
|
||
function getCurrentUser() {
|
||
// 即使 token 失效/退出登录,也从 store + 缓存中读取 openid/unionid。
|
||
// 微信匿名标识同小程序内永久稳定,退出后仍需向可信 H5 注入身份。
|
||
const token = store.state.app.token || '';
|
||
let openid = getOpenid();
|
||
let unionid = store.state.app.unionid || '';
|
||
if (!unionid) { try { unionid = uni.getStorageSync('UNIONID'); } catch (e) {} }
|
||
if ((!openid || !unionid) && token) {
|
||
try {
|
||
let parts = token.split('.');
|
||
if (parts.length === 3) {
|
||
let payload = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
||
while (payload.length % 4 !== 0) payload += '=';
|
||
const data = JSON.parse(base64Decode(payload));
|
||
if (!openid) openid = data.openid || '';
|
||
if (!unionid) unionid = data.unionid || '';
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
return { openid, unionid };
|
||
}
|
||
|
||
function stripAuthParams(url) {
|
||
return url
|
||
.replace(/([?&])openid=[^&]+(&|$)/g, (_, pre, post) => pre === '?' ? '?' : (post || ''))
|
||
.replace(/([?&])unionid=[^&]+(&|$)/g, (_, pre, post) => pre === '?' ? '?' : (post || ''))
|
||
.replace(/([?&])t=[^&]+(&|$)/g, (_, pre, post) => pre === '?' ? '?' : (post || ''))
|
||
.replace(/\?&$/, '').replace(/\?$/, '').replace(/&$/, '');
|
||
}
|
||
|
||
function getUrlParam(url, name) {
|
||
try {
|
||
const match = url.match(new RegExp('[?&]' + name + '=([^&]*)'));
|
||
return match ? decodeURIComponent(match[1]) : '';
|
||
} catch (e) { return ''; }
|
||
}
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
windowH: 0, windowW: 0,
|
||
webviewStyles: { progress: { color: 'transparent' } },
|
||
url: '',
|
||
shareTitle: '',
|
||
shareSummary: '',
|
||
shareImageUrl: '',
|
||
shareTimelineImage: '',
|
||
shareHref: '',
|
||
savedUrl: '',
|
||
_shareFetched: false
|
||
}
|
||
},
|
||
onLoad(option) {
|
||
// 太阳码 scene 解析:scene="app=<identifier>" 时调 box_app 查 url
|
||
// 例如 box_app /api/wxacode?app=pregnancy → scene="app=pregnancy"
|
||
// CRMEB 收到后查 box_app /api/mp/apps?isPublished=1 找 url,跳对应 H5
|
||
const sceneRaw = decodeURIComponent(option.scene || option.wxscene || '');
|
||
if (sceneRaw.indexOf('app=') === 0) {
|
||
// scene 格式: "app=pregnancy" 或 "app=pregnancy&ch=YFMM"
|
||
const source = sceneRaw;
|
||
const identifier = source.slice(4).split('&')[0];
|
||
// 提取渠道码
|
||
let channel = '';
|
||
const chMatch = source.match(/&ch=([A-Z]+)/);
|
||
if (chMatch) channel = chMatch[1];
|
||
console.log('[web_view] 太阳码 scene 解析: identifier=' + identifier + (channel ? ' ch=' + channel : ''));
|
||
// 把 channel 存到 option 中传给 _resolveAppScene
|
||
option._channel = channel;
|
||
// 异步处理:uni.request 拿 H5 url,赋值给 option.url 走后续逻辑
|
||
this._resolveAppScene(identifier, option);
|
||
return;
|
||
}
|
||
|
||
this._initWebView(option);
|
||
},
|
||
onShow() {
|
||
// 登录返回后:页面仍在栈中,onLoad 不会再次触发
|
||
// 1) 首次登录返回(savedUrl 有值,url 未设置)→ 用保存的 URL 重新处理身份
|
||
// 2) H5 内 requireAuth 跳登录页返回 → url 已有值,但 token/身份可能刚更新 → 强制刷新注入新身份
|
||
// 3) 退出登录后再次进入 web_view(url 已存在)→ 用缓存的 openid 重新注入
|
||
const { openid, unionid } = getCurrentUser();
|
||
if (this.savedUrl && !this.url) {
|
||
console.log('[auth-flow] 🔄 onShow: savedUrl 重新处理 → openid=' + (openid || '(空)'));
|
||
const targetUrl = this.savedUrl;
|
||
const urlOpenid = getUrlParam(targetUrl, 'openid');
|
||
if (openid) {
|
||
this._applyIdentity(targetUrl, openid, unionid, urlOpenid);
|
||
} else {
|
||
console.warn('[auth-flow] ⚠️ onShow: 仍无 openid,加载原始URL');
|
||
this._finalizeUrl(targetUrl);
|
||
}
|
||
} else if (this.url && openid) {
|
||
// H5 授权登录返回 / 退出后回到 web_view:重新注入最新身份
|
||
const currentUrl = this.url;
|
||
const urlOpenid = getUrlParam(currentUrl, 'openid');
|
||
console.log('[auth-flow] 🔄 onShow: 身份变更检测 → openid=' + openid + ', urlOpenid=' + (urlOpenid || '(无)'));
|
||
if (urlOpenid !== openid) {
|
||
console.log('[auth-flow] ✅ 授权返回,刷新身份 openid=', openid, ', unionid=', unionid || '(空)');
|
||
this._applyIdentity(currentUrl, openid, unionid, urlOpenid);
|
||
} else {
|
||
console.log('[auth-flow] onShow: 身份一致,不刷新');
|
||
}
|
||
}
|
||
},
|
||
methods: {
|
||
/**
|
||
* 太阳码 scene 解析:调 box_app /api/mp/apps 找 url,找到后注入到 option.url
|
||
* 然后调用 _initWebView(option) 走标准 web_view 加载流程
|
||
*/
|
||
_resolveAppScene(identifier, option) {
|
||
const appToken = store.state.app.token || '';
|
||
uni.request({
|
||
url: 'https://mp.yizhezhe.com/api/mp/apps?identifier=' + encodeURIComponent(identifier) + '&isPublished=1&pageSize=1',
|
||
method: 'GET',
|
||
header: appToken ? { 'Authorization': 'Bearer ' + appToken } : {},
|
||
success: (res) => {
|
||
console.log('[web_view] 太阳码 scene 解析响应:', JSON.stringify(res.statusCode));
|
||
const items = res.data && res.data.result && res.data.result.items;
|
||
if (res.statusCode === 200 && items && items.length > 0) {
|
||
// identifier 严格匹配,避免误用其他应用的 URL / 分享图(张冠李戴)
|
||
const app = items.find(it => it.identifier === identifier);
|
||
if (app && app.url) {
|
||
const appUrl = app.url;
|
||
// 预取分享图片(转发给好友时用作分享卡片图)
|
||
if (app.shareImageUrl) {
|
||
this._applyShareImage(app.shareImageUrl);
|
||
console.log('[web_view] 分享图片: ' + app.shareImageUrl);
|
||
}
|
||
let finalUrl = appUrl;
|
||
if (option._channel) {
|
||
const separator = finalUrl.includes('?') ? '&' : '?';
|
||
finalUrl += separator + 'ch=' + option._channel;
|
||
}
|
||
console.log('[web_view] 解析到 H5 url:', appUrl, option._channel ? ' ch=' + option._channel : '');
|
||
option.url = encodeURIComponent(finalUrl);
|
||
} else {
|
||
console.warn('[web_view] 太阳码 scene 解析: 未找到对应 app,降级到 CRMEB 首页');
|
||
option.url = encodeURIComponent('https://shop.douba.top');
|
||
}
|
||
} else {
|
||
console.warn('[web_view] 太阳码 scene 解析响应异常,降级到 CRMEB 首页');
|
||
option.url = encodeURIComponent('https://shop.douba.top');
|
||
}
|
||
this._initWebView(option);
|
||
},
|
||
fail: (err) => {
|
||
console.error('[web_view] 太阳码 scene 网络请求失败:', err);
|
||
option.url = encodeURIComponent('https://shop.douba.top');
|
||
this._initWebView(option);
|
||
}
|
||
});
|
||
},
|
||
|
||
_initWebView(option) {
|
||
try {
|
||
let targetUrl = decodeURIComponent(option.url);
|
||
console.log('[web_view] 原始URL:', targetUrl);
|
||
|
||
const isTrustedDomain = targetUrl.indexOf(HTTP_REQUEST_URL) === 0 ||
|
||
(TRUSTED_DOMAINS || []).some(d => targetUrl.indexOf(d) === 0);
|
||
if (isTrustedDomain) {
|
||
const token = store.state.app.token;
|
||
console.log('[auth-flow] ===== 进入 H5 跳转流程 =====');
|
||
console.log('[auth-flow] 入口: 太阳码/点击应用');
|
||
console.log('[auth-flow] token:', token ? '有' : '无');
|
||
console.log('[auth-flow] store openid:', store.state.app.openid || '(空)', ', unionid:', store.state.app.unionid || '(空)');
|
||
const { openid, unionid } = getCurrentUser();
|
||
if (openid) {
|
||
const urlOpenid = getUrlParam(targetUrl, 'openid');
|
||
console.log('[auth-flow] 静默身份: openid=' + (openid || '(空)') + ', unionid=' + (unionid || '(空)'));
|
||
console.log('[auth-flow] URL 已有 openid:', urlOpenid || '(无)');
|
||
console.log('[auth-flow] ✅ 有 openid,注入身份跳转(unionid ' + (unionid ? '已携带' : '为空,可接受') + ')');
|
||
this._applyIdentity(targetUrl, openid, unionid, urlOpenid);
|
||
return; // _applyIdentity 内部已调用 _finalizeUrl
|
||
}
|
||
if (token) {
|
||
// 有 token 但 openid 仍空(极少见:JWT 不含 openid 字段):500ms 重试一次
|
||
console.warn('[auth-flow] ⚠️ 有 token 但 openid 为空,500ms 后重试');
|
||
setTimeout(() => {
|
||
try {
|
||
const retry = getCurrentUser();
|
||
if (retry.openid) {
|
||
console.log('[auth-flow] ✅ 重试成功,注入身份并跳转');
|
||
this._applyIdentity(targetUrl, retry.openid, retry.unionid, getUrlParam(targetUrl, 'openid'));
|
||
} else {
|
||
console.warn('[auth-flow] ⚠️ 重试仍无 openid,匿名加载 H5');
|
||
this._finalizeUrl(targetUrl);
|
||
}
|
||
} catch (e) {
|
||
console.error('[auth-flow] 重试异常,匿名加载:', e);
|
||
this._finalizeUrl(targetUrl);
|
||
}
|
||
}, 500);
|
||
return;
|
||
}
|
||
// 无 token + 无 openid(新用户 / 退出后 / 静默授权失败 / 冷启动竞态兜底超时):
|
||
// 主动用 wx.login 拿 code 调 CRMEB silenceAuthNoLogin(v2/wechat/silence_auth)
|
||
// 拿到 openid 后注入 URL,让 H5 端完成 openid-login 建档
|
||
// 该端点不查 store_user_mobile,新用户也能拿到 openid
|
||
console.log('[auth-flow] 🔑 无 openid,调 silenceAuthNoLogin 拿 openid');
|
||
this._fetchOpenidAndLoad(targetUrl);
|
||
return;
|
||
} else {
|
||
console.log('[web_view] 非可信域,不做身份替换');
|
||
}
|
||
|
||
this._finalizeUrl(targetUrl);
|
||
} catch (e) {
|
||
console.error('[web_view] onLoad 异常:', e);
|
||
// 异常时直接加载原始 URL
|
||
try {
|
||
this.url = decodeURIComponent(option.url);
|
||
} catch (_) {}
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 主动 wx.login() 拿 code,调 CRMEB /v2/wechat/silence_auth 拿 openid
|
||
* 然后注入 URL 并加载 H5(即使后端返回 auth_login=1 无 token,openid 已够 H5 端走 openid-login 建档)
|
||
* @param {string} targetUrl - 原始 H5 URL
|
||
*/
|
||
_fetchOpenidAndLoad(targetUrl) {
|
||
uni.login({
|
||
provider: 'weixin',
|
||
success: (loginRes) => {
|
||
const code = (loginRes && loginRes.code) || '';
|
||
if (!code) {
|
||
console.warn('[auth-flow] ⚠️ wx.login 返回空 code,匿名加载 H5');
|
||
this._finalizeUrl(targetUrl);
|
||
return;
|
||
}
|
||
console.log('[auth-flow] wx.login 拿到 code(前 8 位: ' + code.substring(0, 8) + '...),调 silenceAuthNoLogin');
|
||
// 调 api/public.js 已有的 silenceAuth(MP 端 → v2/wechat/silence_auth)
|
||
// 动态 import 避免在 web_view 页面外被 import 失败
|
||
const apiPath = '@/api/public.js';
|
||
// #ifdef MP
|
||
import('@/api/public.js').then(api => {
|
||
api.silenceAuth({ code: code }).then(res => {
|
||
const data = res.data || {};
|
||
// 兼容两种响应格式:
|
||
// 1) 新用户 notLogin=true: {auth_login:1, key, openid, unionid}(顶层 openid)
|
||
// 2) 老用户: {token, userInfo:{...}, expires_time, params:{openid}}(openid 在 JWT/userInfo 里)
|
||
let openid = data.openid || '';
|
||
let unionid = data.unionid || '';
|
||
if (!openid) {
|
||
// 从 userInfo 里取
|
||
const ui = data.userInfo || {};
|
||
openid = ui.openid || '';
|
||
unionid = ui.unionid || '';
|
||
}
|
||
if (!openid && data.token) {
|
||
// 从 JWT payload 里解 openid(兜底)
|
||
try {
|
||
const parts = String(data.token).split('.');
|
||
if (parts.length === 3) {
|
||
let payload = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
||
while (payload.length % 4 !== 0) payload += '=';
|
||
const p = JSON.parse(base64Decode(payload));
|
||
openid = p.openid || '';
|
||
unionid = p.unionid || '';
|
||
}
|
||
} catch (e) {
|
||
console.warn('[auth-flow] JWT 解码 openid 失败:', e.message);
|
||
}
|
||
}
|
||
// 拿到 openid 后同步到 store/缓存(避免后续重复请求)
|
||
if (openid) {
|
||
try {
|
||
store.commit('SET_OPENID', openid);
|
||
uni.setStorageSync('OPENID', openid);
|
||
} catch (e) {}
|
||
if (unionid) {
|
||
try {
|
||
store.commit('SET_UNIONID', unionid);
|
||
uni.setStorageSync('UNIONID', unionid);
|
||
} catch (e) {}
|
||
}
|
||
}
|
||
console.log('[auth-flow] silenceAuthNoLogin 响应: status=' + res.status + ', has openid=' + !!openid + ', auth_login=' + data.auth_login);
|
||
if (openid) {
|
||
console.log('[auth-flow] ✅ 拿到 openid=' + openid.substring(0, 8) + '...,注入 URL');
|
||
this._applyIdentity(targetUrl, openid, unionid, getUrlParam(targetUrl, 'openid'));
|
||
} else {
|
||
console.warn('[auth-flow] ⚠️ silenceAuthNoLogin 未返回 openid,匿名加载 H5');
|
||
this._finalizeUrl(targetUrl);
|
||
}
|
||
}).catch(err => {
|
||
console.warn('[auth-flow] ⚠️ silenceAuthNoLogin 网络失败,匿名加载 H5:', err);
|
||
this._finalizeUrl(targetUrl);
|
||
});
|
||
}).catch(err => {
|
||
console.warn('[auth-flow] ⚠️ import api 失败,匿名加载 H5:', err);
|
||
this._finalizeUrl(targetUrl);
|
||
});
|
||
// #endif
|
||
},
|
||
fail: (err) => {
|
||
console.warn('[auth-flow] ⚠️ wx.login 失败,匿名加载 H5:', err);
|
||
this._finalizeUrl(targetUrl);
|
||
}
|
||
});
|
||
},
|
||
|
||
_applyIdentity(targetUrl, openid, unionid, urlOpenid) {
|
||
console.log('[auth-flow] _applyIdentity: openid=' + (openid || '(空)') + ', unionid=' + (unionid || '(空)') + ', urlOpenid=' + (urlOpenid || '(无)'));
|
||
if (urlOpenid && urlOpenid !== openid) {
|
||
targetUrl = stripAuthParams(targetUrl);
|
||
const joiner = targetUrl.indexOf('?') !== -1 ? '&' : '?';
|
||
const t = Math.floor(Date.now() / 1000);
|
||
targetUrl = targetUrl + joiner + 'openid=' + encodeURIComponent(openid);
|
||
if (unionid) targetUrl += '&unionid=' + encodeURIComponent(unionid);
|
||
targetUrl += '&t=' + t;
|
||
console.log('[auth-flow] 🔁 替换为当前用户身份');
|
||
} else if (!urlOpenid) {
|
||
targetUrl = stripAuthParams(targetUrl);
|
||
const joiner = targetUrl.indexOf('?') !== -1 ? '&' : '?';
|
||
const t = Math.floor(Date.now() / 1000);
|
||
targetUrl = targetUrl + joiner + 'openid=' + encodeURIComponent(openid);
|
||
if (unionid) targetUrl += '&unionid=' + encodeURIComponent(unionid);
|
||
targetUrl += '&t=' + t;
|
||
console.log('[auth-flow] ➕ 补充身份参数');
|
||
} else {
|
||
console.log('[auth-flow] 身份一致,保留原 URL');
|
||
}
|
||
console.log('[auth-flow] 最终注入 URL:', targetUrl.substring(0, 120));
|
||
this._finalizeUrl(targetUrl);
|
||
},
|
||
_finalizeUrl(targetUrl) {
|
||
console.log('[web_view] 最终URL:', targetUrl);
|
||
try {
|
||
const qIdx = targetUrl.indexOf('?');
|
||
if (qIdx !== -1) console.log('[web_view] params:', targetUrl.slice(qIdx + 1));
|
||
} catch (_) {}
|
||
|
||
// #ifdef MP
|
||
uni.showShareMenu({ menus: ['shareAppMessage', 'shareTimeline'] });
|
||
// #endif
|
||
|
||
this.url = targetUrl;
|
||
try {
|
||
const res = uni.getWindowInfo();
|
||
this.windowW = res.windowWidth;
|
||
this.windowH = res.windowHeight;
|
||
} catch (_) {}
|
||
|
||
// 预取分享图片:太阳码场景 _resolveAppScene 已设置 shareImageUrl
|
||
// 点击应用场景(JumpPath 无 identifier)或 H5 未传 imageUrl 时兜底拉取
|
||
// 优先保留 H5 postMessage 传来的分享数据,未设置时才用 box_app 的
|
||
this._fetchShareImage(targetUrl);
|
||
},
|
||
|
||
/**
|
||
* 根据当前 H5 URL 反查 box_app 应用,获取分享图片(shareImageUrl)
|
||
* URL 形如 https://mp.yizhezhe.com/app_03/ 或 https://mp.yizhezhe.com/test_app/?ch=YFMM
|
||
* 通过 mp/apps 列表匹配 url 前缀,找到对应应用后取 shareImageUrl
|
||
* 命中本地缓存直接复用(进入页面后立刻分享也能带图),避免每次重复反查
|
||
* @param {string} targetUrl
|
||
*/
|
||
_fetchShareImage(targetUrl) {
|
||
if (!targetUrl) return;
|
||
// H5 已通过 postMessage 设置分享图片,不覆盖
|
||
if (this.shareImageUrl) return;
|
||
// 页面内只请求一次,避免重复拉取
|
||
if (this._shareFetched) return;
|
||
this._shareFetched = true;
|
||
try {
|
||
const originPath = targetUrl.split('?')[0].replace(/\/$/, '');
|
||
// 本地缓存:同一应用 URL 二次进入直接复用分享图,降低首帧分享无图概率
|
||
const cacheKey = 'web_view_share_img:' + originPath;
|
||
let cached = '';
|
||
try { cached = uni.getStorageSync(cacheKey); } catch (e) {}
|
||
if (cached) {
|
||
this._applyShareImage(cached);
|
||
console.log('[web_view] 分享图片(缓存命中): ' + cached);
|
||
return;
|
||
}
|
||
uni.request({
|
||
url: 'https://mp.yizhezhe.com/api/mp/apps?isPublished=1&pageSize=100',
|
||
method: 'GET',
|
||
success: (res) => {
|
||
try {
|
||
const items = res.data && res.data.result && res.data.result.items;
|
||
if (!items || !items.length) return;
|
||
const matched = items.find(it => {
|
||
if (!it.url) return false;
|
||
const appBase = it.url.split('?')[0].replace(/\/$/, '');
|
||
// 当前 URL 与应用的 url 前缀匹配(如 https://mp.yizhezhe.com/app_03 == https://mp.yizhezhe.com/app_03/)
|
||
return originPath === appBase || originPath.indexOf(appBase + '/') === 0;
|
||
});
|
||
if (matched && matched.shareImageUrl) {
|
||
try { uni.setStorageSync(cacheKey, matched.shareImageUrl); } catch (e) {}
|
||
this._applyShareImage(matched.shareImageUrl);
|
||
console.log('[web_view] 分享图片(兜底): ' + matched.shareImageUrl);
|
||
}
|
||
} catch (e) {
|
||
console.warn('[web_view] 分享图片解析失败:', e);
|
||
}
|
||
},
|
||
fail: (err) => {
|
||
// 常见原因:mp.yizhezhe.com 未加入小程序后台 request/downloadFile 合法域名白名单
|
||
console.warn('[web_view] 分享图片请求失败(请检查 mp.yizhezhe.com 是否已加入合法域名白名单):', err);
|
||
}
|
||
});
|
||
} catch (e) {
|
||
console.warn('[web_view] _fetchShareImage 异常:', e);
|
||
}
|
||
},
|
||
/**
|
||
* 统一设置分享图:同时准备好友转发图(shareImageUrl)与朋友圈本地图(shareTimelineImage)
|
||
* @param {string} url 网络图片地址(box_app 返回或 H5 postMessage 传入)
|
||
*/
|
||
_applyShareImage(url) {
|
||
if (!url) return;
|
||
if (url === this.shareImageUrl) return; // 同一张图不重复处理(H5 可能重复 postMessage)
|
||
this.shareImageUrl = url;
|
||
this._prepareTimelineImage(url);
|
||
},
|
||
/**
|
||
* 朋友圈分享图必须是本地路径/代码包路径(网络图会被微信静默忽略),
|
||
* 这里用 getImageInfo 把网络图转成本地临时路径;失败则留空,回退微信默认截图。
|
||
* 注意:getImageInfo 对网络图同样受 downloadFile 合法域名限制,域名未配置时朋友圈无自定义图。
|
||
* @param {string} url 网络图片地址
|
||
*/
|
||
_prepareTimelineImage(url) {
|
||
if (!url) return;
|
||
uni.getImageInfo({
|
||
src: url,
|
||
success: (res) => {
|
||
this.shareTimelineImage = res.path || '';
|
||
console.log('[web_view] 朋友圈分享图已转本地:', this.shareTimelineImage);
|
||
},
|
||
fail: (err) => {
|
||
console.warn('[web_view] 朋友圈分享图转本地失败(网络图/域名问题,回退默认截图):', err);
|
||
this.shareTimelineImage = '';
|
||
}
|
||
});
|
||
},
|
||
},
|
||
onShareAppMessage() {
|
||
return {
|
||
title: this.shareTitle || '分享',
|
||
// 分享出去必须剥离 openid/unionid/t 身份参数:
|
||
// 1) 避免把分享者的微信匿名标识泄露给接收者;
|
||
// 2) 接收者打开后由 web_view 按自己的身份重新注入(onShow/onLoad 有兜底替换)。
|
||
path: '/pages/annex/web_view/index?url=' + encodeURIComponent(stripAuthParams(this.shareHref || this.url)),
|
||
imageUrl: this.shareImageUrl || ''
|
||
}
|
||
},
|
||
onShareTimeline() {
|
||
return {
|
||
title: this.shareTitle || '分享',
|
||
query: 'url=' + encodeURIComponent(stripAuthParams(this.shareHref || this.url)),
|
||
// 朋友圈分享图必须是本地路径/代码包路径,网络图会被微信静默忽略;
|
||
// 网络图已在 _prepareTimelineImage 中通过 getImageInfo 转成本地临时路径。
|
||
imageUrl: this.shareTimelineImage || ''
|
||
}
|
||
},
|
||
handleMessage(e) {
|
||
const msgs = e.detail && e.detail.data ? e.detail.data : [];
|
||
const data = msgs[msgs.length - 1] || null;
|
||
if (!data) return
|
||
|
||
switch (data.action) {
|
||
case 'share':
|
||
// 存储分享数据,供 onShareAppMessage / onShareTimeline 使用
|
||
// title/imageUrl/href 各自独立处理,避免单个字段缺失导致整包分享数据被丢弃
|
||
if (data.title) {
|
||
this.shareTitle = data.title;
|
||
this.shareSummary = data.summary || '';
|
||
}
|
||
// H5 传了 imageUrl 才覆盖(否则保留 box_app 预取的分享图片)
|
||
if (data.imageUrl) {
|
||
this._applyShareImage(data.imageUrl);
|
||
}
|
||
if (data.href) {
|
||
this.shareHref = data.href;
|
||
}
|
||
// 小程序中无法用代码触发分享面板,引导用户点击右上角 ··· 按钮
|
||
uni.showToast({ title: '点击右上角 ··· 分享给好友', icon: 'none', duration: 2500 });
|
||
break
|
||
|
||
case 'copyLink':
|
||
const link = data.href || this.url
|
||
uni.setClipboardData({
|
||
data: link,
|
||
success() { uni.showToast({ title: '链接已复制', icon: 'success' }) },
|
||
fail(err) {
|
||
console.warn('[web_view] 复制失败:', err)
|
||
uni.showToast({ title: '复制失败', icon: 'none' })
|
||
}
|
||
})
|
||
break
|
||
|
||
case 'setTitle':
|
||
if (data.title) {
|
||
this.shareTitle = data.title;
|
||
uni.setNavigationBarTitle({ title: data.title })
|
||
}
|
||
break
|
||
}
|
||
},
|
||
|
||
}
|
||
</script>
|