Files
crmeb-mp/pages/annex/web_view/index.vue

688 lines
30 KiB
Vue
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.
<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>
<!-- 共享原生授权面板(任意 H5 应用 postMessage({action:'requestProfile'}) 后显示) -->
<view v-if="showAuth" class="mb-auth-mask" @tap="closeAuth">
<view class="mb-auth-panel" @tap.stop="noop">
<view class="mb-auth-title">授权完善资料</view>
<view class="mb-auth-sub">用于在应用里展示你的昵称 / 头像 / 手机号</view>
<button class="mb-auth-btn avatar" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
<image v-if="authAvatar" class="mb-auth-ava" :src="authAvatar" mode="aspectFill" />
<text v-else>选择头像</text>
</button>
<input class="mb-auth-input" type="nickname" v-model="authNickname" placeholder="输入昵称" maxlength="16" />
<button class="mb-auth-btn" open-type="getPhoneNumber" @getphonenumber="onGetPhone">获取手机号</button>
<view class="mb-auth-actions">
<button class="mb-auth-cancel" @tap="closeAuth">取消</button>
<button class="mb-auth-ok" @tap="submitAuth">保存并返回</button>
</view>
</view>
</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,
showAuth: false, // 授权面板开关(H5 postMessage 后显示)
authNickname: '',
authAvatar: '',
_authCode: '' // getPhoneNumber 的 code(提交时交 box_app 解密手机号)
}
},
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 重新注入
// 0) 授权页返回:读取 __AUTH_RESULT__,把授权结果写入当前 H5 URL 的 hash,让 H5 重载读取
try {
const authRes = uni.getStorageSync('__AUTH_RESULT__');
if (authRes && authRes.openid && this.url) {
uni.removeStorageSync('__AUTH_RESULT__');
console.log('[auth-flow] 🎯 授权页返回,刷新身份 hash → openid=' + (authRes.openid || '').substring(0, 8) + '...');
const base = this.url.split('#')[0];
const joiner = base.indexOf('?') !== -1 ? '&' : '?';
this.url = base + joiner + 't=' + Date.now() +
'#openid=' + encodeURIComponent(authRes.openid) +
'&phone=' + encodeURIComponent(authRes.phone || '') +
'&nickname=' + encodeURIComponent(authRes.nickname || '') +
'&avatar=' + encodeURIComponent(authRes.avatar || '');
return;
}
} catch (e) {
console.warn('[auth-flow] 读取授权结果失败:', e);
}
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 (_) {}
// 强制缓存刷新:微信 webview 对相同 URL 会复用缓存。
// 每次进入都追加唯一 _v 参数(不受身份分支影响),保证加载最新 H5。
try {
const base = targetUrl.split('#')[0];
const joiner = base.indexOf('?') !== -1 ? '&' : '?';
targetUrl = base + joiner + '_v=' + Date.now() + (targetUrl.indexOf('#') !== -1 ? '#' + targetUrl.split('#')[1] : '');
console.log('[web_view] 已加缓存版本参数 _v →', targetUrl.substring(0, 120));
} catch (e) {
console.warn('[web_view] 加缓存版本参数失败:', e);
}
// #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 'requestProfile':
// 任意 H5 应用请求授权 → 弹共享原生授权面板(手机号/昵称/头像)
this.showAuth = true;
break
case 'setTitle':
if (data.title) {
this.shareTitle = data.title;
uni.setNavigationBarTitle({ title: data.title })
}
break
}
},
/* ---------- 共享授权面板(任意 H5 应用 requestProfile → 弹面板 → box_app /auth/profile) ---------- */
onChooseAvatar(e) {
const avatarUrl = e.detail.avatarUrl;
if (!avatarUrl) return uni.showToast({ title: '未获取到头像', icon: 'none' });
// 上传到 box_app 公开接口:授权链路不依赖 CRMEB 登录态(退出登录后 uploadImgs 会 401 弹「请登录」)
uni.uploadFile({
url: 'https://mp.yizhezhe.com/api/upload',
filePath: avatarUrl,
name: 'file',
success: (res) => {
try {
const d = JSON.parse(res.data);
if (d.code === 200 && d.result && d.result.url) this.authAvatar = d.result.url;
else uni.showToast({ title: '头像上传失败', icon: 'none' });
} catch (e) { uni.showToast({ title: '头像上传失败', icon: 'none' }); }
},
fail: () => uni.showToast({ title: '头像上传失败,请检查网络', icon: 'none' })
});
},
onGetPhone(e) {
if (e.detail.errMsg === 'getPhoneNumber:ok' && e.detail.code) { this._authCode = e.detail.code; }
else { uni.showToast({ title: '未授权手机号', icon: 'none' }); }
},
submitAuth() {
if (!this.authNickname && !this.authAvatar && !this._authCode) {
return uni.showToast({ title: '请至少授权一项', icon: 'none' });
}
const openid = getOpenid();
if (!openid) return uni.showToast({ title: '未获得 openid,请先登录', icon: 'none' });
const api = 'https://mp.yizhezhe.com/api/mp/public/auth/profile?openid=' + encodeURIComponent(openid) +
(this._authCode ? '&code=' + encodeURIComponent(this._authCode) : '') +
(this.authNickname ? '&nickname=' + encodeURIComponent(this.authNickname) : '') +
(this.authAvatar ? '&avatar=' + encodeURIComponent(this.authAvatar) : '');
uni.request({ url: api, method: 'POST', timeout: 10000,
success: (r) => {
const d = r.data || {};
if (d.code === 200) {
const res = d.result || {};
this.showAuth = false; this._authCode = '';
// 更新 web-view src(保留 query + 追加 hash 结果)→ H5 重载读取
const base = this.url.split('#')[0];
this.url = base + (base.indexOf('?') >= 0 ? '&' : '?') + 't=' + Date.now() +
'#openid=' + encodeURIComponent(openid) +
'&phone=' + encodeURIComponent(res.phone || '') +
'&nickname=' + encodeURIComponent(res.userName || this.authNickname || '') +
'&avatar=' + encodeURIComponent(res.avatar || this.authAvatar || '');
} else {
uni.showToast({ title: d.message || '授权保存失败', icon: 'none' });
}
},
fail: () => uni.showToast({ title: '授权请求失败,请检查网络', icon: 'none' })
});
},
closeAuth() { this.showAuth = false; },
noop() {},
}
</script>
<style scoped>
.mb-auth-mask { position: fixed; inset: 0; z-index: 9999; background: rgba(0,0,0,.45); display: flex; align-items: center; justify-content: center; }
.mb-auth-panel { width: 82%; max-width: 340px; background: #fff; border-radius: 18px; padding: 24px 20px 18px; }
.mb-auth-title { font-size: 17px; font-weight: 700; color: #2B2320; text-align: center; }
.mb-auth-sub { font-size: 12px; color: #A79C95; text-align: center; margin: 8px 0 18px; line-height: 1.5; }
.mb-auth-btn { width: 100%; height: 46px; line-height: 46px; text-align: center; border-radius: 12px; background: #F8F3EF; color: #2B2320; font-size: 14px; margin-top: 12px; }
.mb-auth-btn.avatar { display: flex; align-items: center; justify-content: center; gap: 8px; }
.mb-auth-ava { width: 40px; height: 40px; border-radius: 50%; }
.mb-auth-input { width: 100%; height: 46px; border-radius: 12px; background: #F8F3EF; font-size: 14px; color: #2B2320; padding: 0 14px; margin-top: 12px; }
.mb-auth-actions { display: flex; gap: 12px; margin-top: 20px; }
.mb-auth-cancel, .mb-auth-ok { flex: 1; height: 44px; line-height: 44px; text-align: center; border-radius: 12px; font-size: 14px; font-weight: 600; }
.mb-auth-cancel { background: #F1E6E0; color: #6B615B; }
.mb-auth-ok { background: #F4593C; color: #fff; }
</style>