Files
crmeb-mp/pages/annex/web_view/index.vue
Karlcarlo 0e9ce6f7d6 fix(web_view): 改用 wxscene 避免 scene 被微信拦截
微信小程序的 navigateTo 会把 `scene` 参数名当作保留字段
(只由扫码填充),导致 `option.scene` 为空。
改用 wxscene,同时在 web_view onLoad 中兼容检查两者。
2026-07-08 18:29:08 +08:00

367 lines
13 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>
<!-- 调试浮层(已隐藏,保留代码以备后续调试) -->
<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) {
// 太阳码 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
if ((option.scene || option.wxscene) && (option.scene || option.wxscene).indexOf('app=') === 0) {
const source = option.scene || option.wxscene;
const identifier = source.slice(4);
console.log('[web_view] 太阳码 scene 解析: identifier=' + identifier);
// 异步处理:uni.request 拿 H5 url,赋值给 option.url 走后续逻辑
this._resolveAppScene(identifier, option);
return;
}
this._initWebView(option);
},
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: {
/**
* 太阳码 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) {
// box_app /api/mp/apps 当前不支持 identifier 过滤,遍历找
const app = items.find(it => it.identifier === identifier) || items[0];
if (app && app.url) {
const appUrl = app.url;
console.log('[web_view] 解析到 H5 url:', appUrl);
// 编码到 option.url,复用标准流程
option.url = encodeURIComponent(appUrl);
} else {
// 解析失败:降级到 CRMEB 首页
console.warn('[web_view] 太阳码 scene 解析失败,降级到 CRMEB 首页');
option.url = encodeURIComponent('https://shop.douba.top');
}
} else {
// 响应异常:降级到 CRMEB 首页
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);
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 (_) {}
}
},
_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>