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

381 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>
<!-- 调试浮层(debugMsg 有值时显示,点击关闭) -->
<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) {
console.log('========== [DEBUG 太阳码] onLoad 触发 ==========');
console.log('[DEBUG 太阳码] option 完整:', JSON.stringify(option));
console.log('[DEBUG 太阳码] option.scene:', option.scene);
console.log('[DEBUG 太阳码] option.url:', option.url);
// 显示在页面
// 太阳码 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
console.log('[DEBUG 太阳码] 执行 scene 检查...');
const sceneRaw = decodeURIComponent(option.scene || option.wxscene || '');
if (sceneRaw.indexOf('app=') === 0) {
console.log('[DEBUG 太阳码] ✅ scene 匹配成功! 原始:', option.scene, '解码:', sceneRaw);
// 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 不会再次触发
// 检查是否有保存的 URL 和 token,重新处理身份
if (this.savedUrl && store.state.app.token && !this.url) {
console.log('[web_view] onShow: 登录返回,重新处理 URL:', this.savedUrl.substring(0, 80));
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) {
const app = items.find(it => it.identifier === identifier) || items[0];
if (app && app.url) {
const appUrl = app.url;
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('[DEBUG 太阳码] 降级到 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);
console.log('[DEBUG 太阳码] 检查可信域...');
console.log('[DEBUG 太阳码] targetUrl:', targetUrl);
console.log('[DEBUG 太阳码] HTTP_REQUEST_URL:', HTTP_REQUEST_URL);
const isTrustedDomain = targetUrl.indexOf(HTTP_REQUEST_URL) === 0 ||
(TRUSTED_DOMAINS || []).some(d => targetUrl.indexOf(d) === 0);
console.log('[DEBUG 太阳码] 可信域:', isTrustedDomain);
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);
if (openid && urlOpenid && urlOpenid !== openid) this.debugMsg += '→ 身份不一致,将替换';
if (!openid) {
// 获取不到身份信息,用 setTimeout 重试
console.warn('[web_view] 首次获取 openid 为空,延迟重试', info);
setTimeout(() => {
try {
const retry = getCurrentUser();
console.log('[web_view] 重试后 openid=' + (retry.openid || '(空)'));
if (retry.openid) {
this._applyIdentity(targetUrl, retry.openid, retry.unionid, urlOpenid);
} else {
this.url = targetUrl;
}
} catch (e) {
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);
// 异常时直接加载原始 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('[DEBUG 太阳码] _finalizeUrl 设置 this.url');
console.log('[DEBUG 太阳码] 最终 URL:', targetUrl.substring(0, 100));
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>