fix(web_view): 修复 H5 分享消息投递,废弃无效 uni.share()

- handleMessage share 分支:存储 H5 传来的分享数据,Toast 引导用户点击右上角原生分享
- onShareAppMessage / onShareTimeline:使用 H5 传来的 title/imageUrl/href
- 新增 shareSummary/shareImageUrl/shareHref data 属性
This commit is contained in:
2026-07-02 19:54:53 +08:00
parent 797724c01d
commit 69da6597d9

View File

@@ -1,30 +1,309 @@
<template>
<web-view class="web-view" :webview-styles="webviewStyles" :src="url" :style="{width: windowW + 'px', height: windowH + 'px'}"></web-view>
</template>
<script>
export default {
data() {
return {
windowH: 0,
windowW: 0,
webviewStyles: {
progress: {
color: 'transparent'
}
},
url: ''
}
},
onLoad(option) {
this.url = decodeURIComponent(option.url);
try {
const res = uni.getWindowInfo();
this.windowW = res.windowWidth;
this.windowH = res.windowHeight;
} catch (e) {
// error
}
}
}
</script>
<template>
<view>
<!-- 调试浮层(已隐藏,保留代码以备后续调试) -->
<cover-view v-if="false" class="debug-overlay" @click="debugMsg=''">
<cover-view class="debug-text">{{ debugMsg }}</cover-view>
</cover-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';
import { toLogin } from '@/libs/login';
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() {
const token = store.state.app.token;
if (!token) return { openid: '', unionid: '' };
let openid = getOpenid();
let unionid = store.state.app.unionid || '';
if (!unionid) { try { unionid = uni.getStorageSync('UNIONID'); } catch (e) {} }
if (!unionid) {
try {
let parts = token.split('.');
if (parts.length === 3) {
let payload = parts[1].replace(/-/g, '+').replace(/_/g, '/');
while (payload.length % 4 !== 0) payload += '=';
unionid = JSON.parse(base64Decode(payload)).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: '',
shareHref: '',
debugMsg: '',
savedUrl: ''
}
},
onLoad(option) {
try {
let targetUrl = decodeURIComponent(option.url);
console.log('[web_view] 原始URL:', targetUrl);
this.debugMsg = '[web_view] 加载中...\nurl=' + targetUrl.substring(0, 100);
const isTrustedDomain = targetUrl.indexOf(HTTP_REQUEST_URL) === 0 ||
(TRUSTED_DOMAINS || []).some(d => targetUrl.indexOf(d) === 0);
if (isTrustedDomain) {
const token = store.state.app.token;
if (token) {
let { openid, unionid } = getCurrentUser();
const urlOpenid = getUrlParam(targetUrl, 'openid');
// 收集调试信息
let info = '[debug] token=' + (token ? '有' : '无') +
', myOpenid=' + (openid || '(空)') +
', urlOpenid=' + (urlOpenid || '(空)') +
', url=' + targetUrl.substring(0, 80);
this.debugMsg = info;
if (openid && urlOpenid && urlOpenid !== openid) this.debugMsg += '→ 身份不一致,将替换';
if (!openid) {
// 获取不到身份信息,用 setTimeout 重试
console.warn('[web_view] 首次获取 openid 为空,延迟重试', info);
this.debugMsg = info + '\n→ 等待重试...';
setTimeout(() => {
try {
const retry = getCurrentUser();
console.log('[web_view] 重试后 openid=' + (retry.openid || '(空)'));
this.debugMsg = info + '\n→ 重试后 openid=' + (retry.openid || '(空)');
if (retry.openid) {
this._applyIdentity(targetUrl, retry.openid, retry.unionid, urlOpenid);
} else {
this.debugMsg = info + '\n→ 重试仍失败,直接加载原始URL';
this.url = targetUrl;
}
} catch (e) {
this.debugMsg = '重试异常: ' + e.message;
this.url = targetUrl;
}
}, 500);
return;
}
this._applyIdentity(targetUrl, openid, unionid, urlOpenid);
return; // _applyIdentity 内部已调用 _finalizeUrl,避免163重复覆盖
} else {
console.log('[web_view] 无 token,跳转登录');
this.savedUrl = targetUrl;
toLogin();
return;
}
} else {
console.log('[web_view] 非可信域,不做身份替换');
}
this._finalizeUrl(targetUrl);
} catch (e) {
console.error('[web_view] onLoad 异常:', e);
this.debugMsg = 'onLoad异常: ' + e.message + '\n' + e.stack;
// 异常时直接加载原始 URL
try {
this.url = decodeURIComponent(option.url);
} catch (_) {}
}
},
onShow() {
// 登录返回后:页面仍在栈中,onLoad 不会再次触发
// 检查是否有保存的 URL 和 token,重新处理身份
if (this.savedUrl && store.state.app.token && !this.url) {
console.log('[web_view] onShow: 登录返回,重新处理 URL:', this.savedUrl.substring(0, 80));
this.debugMsg = '[web_view] 登录返回...\n' + this.savedUrl.substring(0, 100);
const targetUrl = this.savedUrl;
const { openid, unionid } = getCurrentUser();
const urlOpenid = getUrlParam(targetUrl, 'openid');
if (openid) {
this._applyIdentity(targetUrl, openid, unionid, urlOpenid);
} else {
console.warn('[web_view] onShow: 仍无 openid,加载原始URL');
this._finalizeUrl(targetUrl);
}
}
},
methods: {
_applyIdentity(targetUrl, openid, unionid, 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('[web_view] 替换为当前用户身份');
} 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('[web_view] 补充身份参数');
} else {
console.log('[web_view] 身份一致,保留原 URL');
}
this.debugMsg += ' [最终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
// 自动关闭调试浮层(3秒后)
setTimeout(() => {
if (this.debugMsg) this.debugMsg = '';
}, 3000);
this.url = targetUrl;
try {
const res = uni.getWindowInfo();
this.windowW = res.windowWidth;
this.windowH = res.windowHeight;
} catch (_) {}
},
},
onShareAppMessage() {
return {
title: this.shareTitle || '分享',
path: '/pages/annex/web_view/index?url=' + encodeURIComponent(this.shareHref || this.url),
imageUrl: this.shareImageUrl || ''
}
},
onShareTimeline() {
return {
title: this.shareTitle || '分享',
query: 'url=' + encodeURIComponent(this.shareHref || this.url),
imageUrl: this.shareImageUrl || ''
}
},
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 使用
if (data.title) {
this.shareTitle = data.title;
this.shareSummary = data.summary || '';
this.shareImageUrl = data.imageUrl || '';
this.shareHref = data.href || this.url;
}
// 小程序中无法用代码触发分享面板,引导用户点击右上角 ··· 按钮
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>
<style>
.debug-overlay {
position: fixed; top: 0; left: 0; right: 0; height: 45%;
background: rgba(0,0,0,0.8); z-index: 9999;
padding: 20px; box-sizing: border-box;
overflow-y: auto; border-bottom: 2px solid #0f0;
}
.debug-text {
color: #0f0; font-size: 13px;
white-space: pre-wrap; word-break: break-all; line-height: 1.6;
}
</style>