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

407 lines
16 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';
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 不会再次触发
// 1) 首次登录返回(savedUrl 有值,url 未设置)→ 用保存的 URL 重新处理身份
// 2) H5 内 requireAuth 跳登录页返回 → url 已有值,但 token/身份可能刚更新 → 强制刷新注入新身份
if (this.savedUrl && store.state.app.token && !this.url) {
console.log('[auth-flow] 🔄 onShow: 首次登录返回,重新处理 URL:', this.savedUrl.substring(0, 80));
const targetUrl = this.savedUrl;
const { openid, unionid } = getCurrentUser();
const urlOpenid = getUrlParam(targetUrl, 'openid');
console.log('[auth-flow] onShow 身份: openid=' + (openid || '(空)') + ', unionid=' + (unionid || '(空)'));
if (openid) {
this._applyIdentity(targetUrl, openid, unionid, urlOpenid);
} else {
console.warn('[auth-flow] ⚠️ onShow: 仍无 openid,加载原始URL');
this._finalizeUrl(targetUrl);
}
} else if (this.url && store.state.app.token) {
// H5 授权登录返回:重新注入最新身份(openid 可能刚获取到)
const { openid, unionid } = getCurrentUser();
const currentUrl = this.url;
const urlOpenid = getUrlParam(currentUrl, 'openid');
console.log('[auth-flow] 🔄 onShow: H5 授权返回检测 → openid=' + (openid || '(空)') + ', urlOpenid=' + (urlOpenid || '(无)'));
if (openid && urlOpenid !== openid) {
console.log('[auth-flow] ✅ 授权返回,刷新身份 openid=', openid, ', unionid=', unionid || '(空)');
this._applyIdentity(currentUrl, openid, unionid, urlOpenid);
} else {
console.log('[auth-flow] onShow: 身份一致或仍无 openid,不刷新');
}
}
},
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;
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 || '(空)');
if (token) {
let { openid, unionid } = getCurrentUser();
const urlOpenid = getUrlParam(targetUrl, 'openid');
console.log('[auth-flow] 静默身份: openid=' + (openid || '(空)') + ', unionid=' + (unionid || '(空)'));
console.log('[auth-flow] URL 已有 openid:', urlOpenid || '(无)');
// 收集调试信息
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('[auth-flow] ⚠️ 有 token 但 openid 为空,500ms 后重试', info);
setTimeout(() => {
try {
const retry = getCurrentUser();
console.log('[auth-flow] 重试后 openid=' + (retry.openid || '(空)') + ', unionid=' + (retry.unionid || '(空)'));
if (retry.openid) {
console.log('[auth-flow] ✅ 重试成功,注入身份并跳转');
this._applyIdentity(targetUrl, retry.openid, retry.unionid, urlOpenid);
} else {
console.warn('[auth-flow] ⚠️ 重试仍无 openid,匿名加载 H5(unionid 无法携带)');
this.url = targetUrl;
}
} catch (e) {
console.error('[auth-flow] 重试异常,匿名加载:', e);
this.url = targetUrl;
}
}, 500);
return;
}
console.log('[auth-flow] ✅ 有 openid,注入身份跳转(unionid ' + (unionid ? '已携带' : '为空,可接受') + ')');
this._applyIdentity(targetUrl, openid, unionid, urlOpenid);
return; // _applyIdentity 内部已调用 _finalizeUrl,避免163重复覆盖
} else {
// 无 token:不拦截,匿名直达 H5(openid 为空)
// 用户需要手机号/头像/昵称时,由 H5 调 bridge.requireAuth() 再授权
console.log('[auth-flow] 🔓 无 token,匿名直达 H5(不弹授权,按需授权由 H5 requireAuth 触发)');
this._finalizeUrl(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 (_) {}
}
},
_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.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>