build error: Missing semicolon (163:22) 原因:之前修复时误删了 _resolveAppScene 方法的关闭 `},` 导致 _initWebView 紧接 uni.request 的 `});`,缺少 _resolveAppScene 关闭 修复:在 uni.request 关闭后插入 `\t\t},` Node.js --check 验证:语法通过
364 lines
13 KiB
Vue
364 lines
13 KiB
Vue
<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.scene.indexOf('app=') === 0) {
|
||
const identifier = option.scene.slice(4);
|
||
console.log('[web_view] 太阳码 scene 解析: identifier=' + identifier);
|
||
// 异步处理:uni.request 拿 H5 url,赋值给 option.url 走后续逻辑
|
||
this._resolveAppScene(identifier, option);
|
||
return;
|
||
}
|
||
|
||
this._initWebView(option);
|
||
},
|
||
/**
|
||
* 太阳码 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 (_) {}
|
||
}
|
||
},
|
||
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>
|