Files
crmeb-mp/pages/index/index.vue
Karlcarlo e99b4a475c fix(身份): 宿主页「拿到 openid 才加载 H5」+ 复用 App.vue 并行登录(不重复换 code)
背景(2026-09-20 线上排查):删掉小程序缓存重进时 H5 以访客态加载(孕周 `--`、档案空、
跳应用不带 openid,实测跳授权示例是裸 URL)。服务端证据:该设备只打了 `/api/mp/apps`,
没有任何带 openid 的 profile/pregnancy 调用 —— 宿主那次没注入身份,不是数据丢了。

根因:
1) App.vue 静默授权只处理 token 分支:v2/wechat/silence_auth 对新用户 / 强制绑定手机号返回
   {auth_login:1, key, openid, unionid}(无 token),openid 被丢掉;且 res.data.userInfo 缺失时
   取 .uid 会抛异常把后续 commit 一起带走。
2) 两个宿主页只从 store.openid / 缓存 OPENID / JWT 读身份,读不到就匿名加载(首页壳不等不重试,
   容器页 _finalizeUrl / _fetchOpenidAndLoad 兜底)。

改法:
- App.vue:补「无 token 但有 openid」分支(SET_OPENID + 缓存,unionid 同理);userInfo 判空;
  **并把这次静默登录的 promise 挂到 globalData._silenceAuthPromise**(供宿主页复用,见下)。
- mixins/webviewHost.js:新增
  · _silenceAuthOpenid()(wx.login → v2/wechat/silence_auth,兼容 auth_login/userInfo/JWT 三种
    openid 来源,拿到即落 store + 缓存,带 8s 超时保险丝);
  · _awaitHostLogin(timeout)(等 App.vue 那次登录结束,**不重复换 code**)。
- 两个宿主页统一 fail-closed「拿到 openid 才加载」,并且**第 1 次先 await App.vue 的并行登录**、
  命中缓存就直接加载;之后才自己取。**网络尝试上限 3 → 2,退避 600×n → 固定 500ms**
  (实测:code2session 约 200–300ms,重复换一次就是白花这个钱;最坏等待从 ~3.5s 降到 ~1–2.5s)。
- 首页壳:读不到身份就不设 webUrl(停在小程序自己的加载屏,文案分「取身份中/失败/页面加载失败」三态);
  2 次仍失败 → 「未获取到微信身份」+「重新获取身份」。
- 容器页:三条失败分支(有 token 无 openid 的重试失败、无 token 无 openid、onShow 仍无 openid)
  全部改走 _ensureIdentityThenLoad();删除本页重复实现的 _fetchOpenidAndLoad;新增兜底屏
  (以前 url 为空是白屏)。非可信域(商城自身域名等)不注入身份、照常直接加载。
- utils/util.js:注释里的方法名同步(_fetchOpenidAndLoad → _ensureIdentityThenLoad)。

⚠️ 提交同时包含这两处**此前已存在的未提交 WIP**(同文件、改动交织):pages/annex/web_view/index.vue
   的 mixin 重构(handleMessage→handleHostMessage 等 4 处 handler 改名)与 pages/index/index.vue
   的两行 tabBar 调整。
⚠️ 刻意 fail-closed:CRMEB silence_auth 整体不可用时会停在失败屏而不是降级加载。
⚠️ 未动 manifest.json 版本号(见另一次改动:已统一为 0.3.6 / 36)。
2026-09-20 23:50:31 +08:00

549 lines
26 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
<!-- ============================================================
首页(临时版):兜爸心选 H5 · web-view 壳
------------------------------------------------------------
本页替代了 CRMEB 原装修首页(原文件完整备份在同目录
index.vue.origin-bak,恢复方法见本文件底部注释)。
链路(2026-09 起:mama-box 不再是 box_app 的「应用」,URL 硬编码):
onLoad → loadMamaBox()
→ webUrl = https://mp.yizhezhe.com/mama-box/ + #openid=xxx(buildWebUrl)
→ <web-view> 加载该 H5
(更新首页 = 直接改服务端 box_app/mama-box/ 目录,无需发版即可刷新内容)
两个坑(都来自 app_03 前一次接入的实测结论):
1. 微信 web-view 首次加载会丢弃 URL 的 query string(?a=b),
hash(#a=b)不丢 —— 需要透传给 H5 的参数一律拼在 # 后面。
2. 开发者工具必须勾选「不校验合法域名」才能请求本地 box_app;
线上需要在小程序后台把 box_app 域名同时加进
request 合法域名 和 web-view 业务域名。
============================================================ -->
<view class="mamabox-home">
<web-view v-if="webUrl" :src="webUrl" @message="handleHostMessage" @error="onWebviewError"></web-view>
<!-- 加载中 / 加载失败兜底(web-view 未就绪或加载出错时才渲染)
⚠️ 2026-09-20 起增加「正在获取微信身份」这一态:首页壳**必须拿到 openid 才加载 H5**,
拿不到就重试、并停在这一屏 —— 不再匿名加载(匿名加载会让 H5 进访客态:
孕周 `--`、档案空,用户看到的是「能用但都是空的」,比多等几秒更糟)。 -->
<view v-else class="mamabox-fallback">
<view class="mamabox-emoji">{{ fallbackEmoji }}</view>
<view class="mamabox-tip">
<view class="mamabox-title">{{ fallbackTitle }}</view>
<view class="mamabox-desc">{{ fallbackDesc }}</view>
</view>
<button v-if="loadError || identityFail" class="mamabox-retry" hover-class="mamabox-retry-hover" @tap="retryLoad">{{ loadError ? '重新加载' : '重新获取身份' }}</button>
</view>
<!-- 小程序原生授权面板(H5 postMessage 请求后显示):手机号/昵称/头像 -->
<view v-if="showAuth" class="mb-auth-mask" @tap="closeAuth">
<view class="mb-auth-panel" @tap.stop="noop">
<view class="mb-auth-title">授权完善资料</view>
<view class="mb-auth-sub">用于在「兜爸心选」中展示你的昵称 / 头像 / 手机号</view>
<button class="mb-auth-btn avatar" open-type="chooseAvatar" @chooseavatar="onChooseHostAvatar">
<image v-if="authAvatar" class="mb-auth-ava" :src="authAvatar" mode="aspectFill" />
<text v-else>选择头像</text>
</button>
<input class="mb-auth-input" type="nickname" v-model="authNickname" placeholder="输入昵称" maxlength="16" />
<button class="mb-auth-btn" open-type="getPhoneNumber" @getphonenumber="onHostGetPhone">获取手机号</button>
<view class="mb-auth-actions">
<button class="mb-auth-cancel" @tap="closeAuth">取消</button>
<button class="mb-auth-ok" @tap="submitAuth">保存并返回</button>
</view>
</view>
</view>
</view>
</template>
<script>
import { mapGetters } from 'vuex';
import { getShare } from '@/api/public.js';
import { getTemlIds } from '@/api/api.js';
import { SUBSCRIBE_MESSAGE } from '@/config/cache';
import shareCard from '@/mixins/shareCard.js';
import webviewHost from '@/mixins/webviewHost.js';
const app = getApp();
// mama-box H5 托管地址(硬编码;更新首页 = 改服务端 box_app/mama-box/ 目录)
const MAMA_HOME = 'https://mp.yizhezhe.com/mama-box/';
/* 剥离分享链接里的身份/缓存参数。分享出去的链接必须干净:
① 不能把分享者的 openid/unionid 泄露给接收者;
② 接收者点开后由宿主页(pages/annex/web_view)按自己的身份重新注入。
覆盖 query 的 openid/unionid/t/_v 与 hash 的 openid/unionid/phone/nickname/avatar
(hash 是授权回传通道,见 docs/h5-auth-standard.md)。 */
const SHARE_DROP_KEYS = ['openid', 'unionid', 't', '_v', 'phone', 'nickname', 'avatar'];
function dropKeyValuePairs(str, keys) {
return String(str || '')
.split('&')
.filter(pair => !!pair && keys.indexOf(pair.split('=')[0]) === -1)
.join('&');
}
function stripAuthParams(url) {
let out = String(url || '');
const hashIdx = out.indexOf('#');
let base = hashIdx >= 0 ? out.slice(0, hashIdx) : out;
let hash = hashIdx >= 0 ? out.slice(hashIdx + 1) : '';
const qIdx = base.indexOf('?');
let query = qIdx >= 0 ? base.slice(qIdx + 1) : '';
base = qIdx >= 0 ? base.slice(0, qIdx) : base;
query = dropKeyValuePairs(query, SHARE_DROP_KEYS);
hash = dropKeyValuePairs(hash, SHARE_DROP_KEYS);
// 兜底:hash 是「路由 + ?参数」形态时(如 #/report?openid=x),再剥一层
const hq = hash.indexOf('?');
if (hq >= 0) {
const hqQuery = dropKeyValuePairs(hash.slice(hq + 1), SHARE_DROP_KEYS);
hash = hash.slice(0, hq) + (hqQuery ? '?' + hqQuery : '');
}
return base + (query ? '?' + query : '') + (hash ? '#' + hash : '');
}
export default {
name: 'Index',
mixins: [shareCard, webviewHost],
computed: {
...mapGetters(['isLogin', 'uid', 'cartNum']),
/* 兜底屏文案:三种状态(取身份中 / 取身份失败 / 页面加载失败)分开说,
别让用户对着「正在加载」干等却不知道在等什么。 */
fallbackTitle() {
if (this.loadError) return '页面加载失败';
if (this.identityFail) return '未获取到微信身份';
if (this.identityPending) return '正在获取微信身份';
return '正在加载';
},
fallbackDesc() {
if (this.loadError) return '请检查网络后重试';
if (this.identityFail) return '请检查网络后点下方重试,或退出小程序重新进入';
if (this.identityPending) return '马上就好,请稍候(不会加载空数据的页面)';
return '正在为你打开「兜爸心选」';
},
fallbackEmoji() {
return (this.loadError || this.identityFail) ? '🥲' : '🍼';
},
},
data() {
return {
webUrl: '', // web-view 地址(硬编码 + #openid/#unionid)
requesting: false, // 防重入
_shownOnce: false, // 首次展示标记(避免首次 onShow 重复加载)
_idAttempts: 0, // 「取到 openid 才加载」的自动重试计数(见 fetchIdentityThenLoad)
loadError: false, // web-view 加载失败(显示「重新加载」)
identityPending: false, // 正在获取 openid(此期间**不加载 H5**,见 fetchIdentityThenLoad)
identityFail: false, // 多次仍未拿到 openid → 停在兜底屏,给「重新获取身份」入口
shareInfo: {}, // CRMEB 后台「分享设置」(标题/图,无 App 匹配时兜底)
// 授权面板字段由 mixins/webviewHost.js 提供:
// showAuth / authNickname / authAvatar / _authCode
// 分享字段由 mixins/shareCard.js 提供:
// shareTitle / shareHref / shareImageUrl / shareApps…
};
},
onLoad(options) {
// #ifdef MP
// 开启「转发给好友 / 分享到朋友圈」两项菜单(onShareTimeline 需要它)
uni.showShareMenu({ menus: ['shareAppMessage', 'shareTimeline'] });
// #endif
// #ifdef MP
// 恢复原装修首页的两项预热:订阅消息模板(getTemlIds)+ CRMEB 后台「分享设置」
// (getShare 的标题/图在 box_app 没有匹配应用时兜底,如 mama-box 首页本身)
this.getTemlIds();
getShare().then(res => { this.shareInfo = (res && res.data) || {}; }).catch(() => {});
// #endif
// 预热 box_app 全量应用表(url → shareImageUrl)。
// 本页不会随 H5 应用切换重新 onLoad(应用宫格用 location.href 换页),
// 所以分享图只能在「点转发那一刻」按 options.webViewUrl 同步匹配,
// 依赖这张常驻内存(+1h 本地缓存)的应用表,见 onShareAppMessage。
this._loadShareApps();
this.getOptions(options);
// 商户 / 管理员登录后跳各自工作台 —— 与原首页行为保持一致,
// 不能因为首页换成了 H5 就把这两类人堵在门外。
let identity = this.$store.state.app.identity;
if (identity == 1 && this.isLogin) {
uni.redirectTo({ url: '/pages/merchant/index/goods' });
return;
} else if (identity == 2 && this.isLogin) {
uni.redirectTo({ url: '/pages/admin/work/index' });
return;
}
/* 朋友圈转发落地:onShareTimeline 不能自定义 path(只会重开当前页 + query),
目标 H5 只能经 query 回传。本页不自己加载它(身份注入 / 分享图都在
web_view 容器里),转交容器处理,避免接收者又落回首页。 */
if (options && typeof options.url === 'string' && options.url) {
let target = options.url;
try { target = decodeURIComponent(target); } catch (e) {}
this.loadMamaBox(); // 保留首页作为返回落点
this.$nextTick(() => {
uni.navigateTo({ url: '/pages/annex/web_view/index?url=' + encodeURIComponent(target) });
});
return;
}
// ⚠️ 首页必须主动隐藏原生 tabBar(2026-09-18 定论,推翻下面旧结论):
// 真机上 web-view **盖不住**原生 tabBar —— 旧结论是在 custom:true、
// 原生条根本不渲染的时期测的。H5 自带底部导航 + 原生 tabBar = 双条。
// hideTabBar 是全局状态:商城各 tab 页都挂着 pageFooter,由它按后台
// 「装修→底部导航」配置自管显隐(启用=hideTabBar+渲染 DIY 底栏,
// 禁用=showTabBar 露出原生条兜底),所以这里隐藏不会让商城页变死胡同。
// ⚠️ 但严禁在商城页主动 showTabBar 来「配对」——上一轮翻车根因
// (showTabBar + DIY 底栏同显 = 商品列表双 tabBar,见 onShow 旧警告)。
uni.hideTabBar();
this.loadMamaBox();
},
onShow() {
// 每次回到首页都再压一次 tabBar:hideTabBar 是全局状态,可能被别处
// showTabBar 抬回来(如后台停用 DIY 底部导航时商城页会露出原生条兜底)。
// (原这行的 uni.$emit('tabbar-update') 随 custom-tab-bar 死代码一并移除)
uni.hideTabBar();
// 0) 独立授权页(pages/annex/auth/index)返回:读取 __AUTH_RESULT__,
// 把手机号/昵称/头像写回 H5 URL 的 hash(授权结果只走 hash)。
// ⚠️ 必须在下面「重载 webview」之前处理,否则结果会被 ?t= 重载冲掉。
// ⚠️ 回传落点必须是「发起授权时所在的那个 H5」:应用宫格用
// location.href 换 H5(页面栈不变、webUrl 数据还是 mama-box 的),
// 写死 MAMA_HOME 会把结果送回 mama-box 首页 —— 授权了也等于白授权,
// 目标 H5 永远收不到 phone(2026-09-18 孕检日历授权弹层复现根因之一)。
// shareHref 由 H5 换页/发起授权前的 location 上报实时刷新(askProfile
// 会先 reportLocation 再 navigateTo),是「当前 H5」最可靠的来源。
const authBase = (this.shareHref || this.webUrl || MAMA_HOME).split('#')[0];
const authUrl = this.consumeAuthResult(authBase);
if (authUrl) {
this.loadError = false;
this._shownOnce = true;
this.webUrl = authUrl;
} else if (this._shownOnce && this.webUrl) {
// ⚠️ 从商城/其它 tab 切回首页时:webview 是同一个实例,SPA 会停留在切走前的屏。
// 改为「重载 webview」让 H5 落回首页(H5 默认即是首页)。首次展示不重复刷。
// 同理不能写死 MAMA_HOME —— 用户停在宫格切换的应用里时会被踢回 mama-box;
// 以 shareHref(当前 H5)为基址重载,停留在原应用首页。
this.loadError = false;
const reloadBase = (this.shareHref || MAMA_HOME).split('#')[0];
if (this._hostOpenid()) {
this.webUrl = this.buildWebUrl(reloadBase + '?t=' + Date.now());
} else {
/* ⚠️ 仍无 openid:走同一条「取到才加载」的路(自动重试 3 次 → 兜底屏),
不再匿名重载(原因见 fetchIdentityThenLoad 的注释)。 */
console.log('[auth-flow] 🔄 onShow: 仍无 openid,走「取到才加载」');
this._idAttempts = 0;
this.fetchIdentityThenLoad(reloadBase);
}
} else {
this._shownOnce = true;
}
// 历史(勿照旧回退):曾试过「home hide + 商城页 show」双向 programmatic
// 切换,因商城页 showTabBar 与 DIY 底栏同显 →「商品列表双 tabBar」+跳一下,
// 当时整体回退。2026-09-18 起改为单向:只在首页 hideTabBar,商城页显隐
// 完全交给 pageFooter(后台 DIY 配置驱动),不再有任何 showTabBar 配对逻辑。
// 购物车角标(tabBar 索引 3 = 购物车),沿用原首页逻辑
if (this.cartNum > 0) {
uni.setTabBarBadge({
index: 3,
text: this.cartNum > 99 ? '99+' : this.cartNum + ''
})
} else {
uni.hideTabBarRedDot({
index: 3
})
}
uni.removeStorageSync('form_type_cart')
},
/* 分享(2026-09 修复:接收者必须进入「发送者当时所在的那个 H5」,而不是首页)
------------------------------------------------------------
背景:小程序 web-view 里的 H5 无法自己拉起分享面板,卡片只能由承载
<web-view> 的页面产出。而本页的 web-view 会被 mama-box 的应用宫格用
location.href 换成别的 H5 应用(见 mama-box/js/app.js jumpApp),
此时小程序页面栈仍然只有本页 —— 所以「分享」永远发生在本页。
微信在 web-view 内触发分享时会回调 onShareAppMessage(options),
options.webViewUrl 就是当前 web-view 的真实地址(含 H5 的 SPA 路由),
这是唯一可靠的深链来源;H5 postMessage 来的 href 只作兜底(投递有延迟)。
接收者统一走 pages/annex/web_view 容器:容器会剥掉发送者身份、
按接收者自己的 openid 重新注入,并预取分享图。
分享图(2026-09 补):本页的 web-view 会被应用宫格换成任意 H5 应用,
但页面不会重新 onLoad —— 之前只有容器页会按 URL 反查 box_app 的
App.shareImageUrl,所以从宫格直接进的应用(如数胎动)转发时没有后台配的图。
现在本页 onLoad 预热全量应用表(mixins/shareCard.js),转发时用
options.webViewUrl 同步匹配出图;H5 postMessage 的 imageUrl 有值才覆盖。 */
onShareAppMessage(options) {
const target = (options && options.webViewUrl) || this.shareHref || this.webUrl || MAMA_HOME;
// 按「当前 H5 地址」解析分享图:先做应用切换检测(首页壳不随 H5 换页重新 onLoad),
// 再每次重新反查 box_app —— 不能因为 shareImageUrl 已有值就跳过,
// 那是上一个应用的图(A 应用分享过、切到 B 后仍带 A 的图,就是这里出的错)。
this.resolveShareCard(target);
const info = this.shareInfo || {};
const uid = this.uid || 0;
return {
title: this.shareTitle || info.title || '兜爸心选 · 孕育陪伴',
// spid:恢复原装修首页的分享推广归因(接收者容器页 / App.onShow 读入 globalData.spid)
path: '/pages/annex/web_view/index?url=' + encodeURIComponent(stripAuthParams(target)) + '&spid=' + uid,
imageUrl: this.shareImageUrl || info.img || ''
};
},
onShareTimeline() {
// onShareTimeline 无回调参数,拿不到 webViewUrl,只能用 H5 传来的 href 兜底
const target = this.shareHref || this.webUrl || MAMA_HOME;
const info = this.shareInfo || {};
const uid = this.uid || 0;
return {
title: this.shareTitle || info.title || '兜爸心选 · 孕育陪伴',
query: 'url=' + encodeURIComponent(stripAuthParams(target)) + '&spid=' + uid,
imageUrl: this.shareTimelineImage || info.img || ''
};
},
methods: {
getOptions(options) {
// #ifdef MP
if (options.scene) {
let value = this.$util.getUrlParams(decodeURIComponent(options.scene));
// 记录推广人uid(与原首页一致)
if (value.spid) app.globalData.spid = value.spid;
}
// #endif
if (options.spid) app.globalData.spid = options.spid;
},
/**
* 查询 box_app 拿兜爸心选的托管地址。
* box_app 的 App 表是「产品盒子」的应用注册中心 —— 改后台这条记录的
* url 字段即可给 H5 换地址 / 切版本,小程序不用发版。
*/
loadMamaBox() {
// 2026-09 起:mama-box 不再是 box_app 的「应用」(已从 App 表删除)。
// 它是小程序首页 + H5 入口,URL 在此【硬编码】;
// 更新首页 = 直接改服务端 box_app/mama-box/ 目录,无需发版即可刷新内容。
if (this.requesting) return;
this.requesting = true;
this.loadError = false;
/* ⚠️ 身份兜底(2026-09-20 新增):store.openid / 缓存 OPENID / JWT 三处都读不到时,
**先主动取一次**(wx.login → silence_auth,见 mixin 的 _silenceAuthOpenid),
拿到再拼 hash 加载 —— 别匿名加载。
以前这里直接匿名加载,H5 于是进访客态(孕周 `--`、档案查不到、跳应用不带身份),
而且没人补:根因是新用户/未绑手机号分支的 openid 被 App.vue 丢掉 + 首页壳没有
容器页那样的 openid 兜底(两条都在 2026-09-20 修,现统一叫 _ensureIdentityThenLoad)。
取不到 openid 仍继续加载(保持可用),H5 侧会显示「未获取到微信身份」提示条。 */
this.identityFail = false;
if (this._hostOpenid()) {
this.identityPending = false;
this._idAttempts = 0;
this.webUrl = this.buildWebUrl(MAMA_HOME);
this.requesting = false;
return;
}
/* ⚠️⚠️ 拿不到 openid **就不加载**(2026-09-20 用户定的行为):
宁可让用户在这一屏多等几秒(自动重试 3 次),也不匿名加载 H5 ——
匿名加载会让 H5 进访客态(孕周 `--`、档案查不到),
用户看到「页面能用但全是空」比等一会儿更糟,且事后很难判断是身份问题。 */
this._idAttempts = 0;
this.fetchIdentityThenLoad(MAMA_HOME);
this.requesting = false;
},
/**
* 取到 openid 才加载 H5;自动重试 3 次仍失败 → 停在兜底屏给「重新获取身份」入口。
* ⚠️ 每次尝试前都再读一次 _hostOpenid():App.vue 的 onLaunch 静默授权是并行的,
* 它先落地的话这里直接命中缓存,不必自己再请求一次。
* @param {string} base - 要加载的 H5 基址(首页壳是 MAMA_HOME;onShow 重载时是当前 H5)
*/
fetchIdentityThenLoad(base) {
/* 1) 缓存命中(含 App.vue 并行登录刚落地的)→ 0 等待加载 */
if (this._loadWithCachedIdentity(base)) return;
this.identityPending = true;
this.identityFail = false;
const seq = this._idAttempts; // 0 = 还没取过
this._idAttempts = seq + 1;
const MAX = 2; // ⚠️ 网络尝试上限 2 次(原 3 次,见下方退避说明)
console.log('[auth-flow] 🔑 首页壳取 openid(第 ' + this._idAttempts + '/' + MAX + ' 次)');
/* 2) 第 1 次**不急着自己请求**:先等 App.vue onLaunch 那次静默登录(同一次 exchange)。
它先落地就直接加载 —— 避免两边各换一次 code 白花 200–300ms
(微信 code 一次性;实测 code2session 约 200–300ms)。
之后的重试才自己 wx.login + 换 openid(_silenceAuthOpenid)。 */
const task = (seq === 0) ? this._awaitHostLogin(1500) : this._silenceAuthOpenid();
task.then(() => {
if (this._loadWithCachedIdentity(base)) return;
if (this._idAttempts < MAX) {
/* 退避固定 500ms(原来 600×n,最坏 3 次 + 1.8s 退避 ≈ 3.5s 才出失败屏;
现在 2 次 + 0.5s ≈ 1.5s 量级) */
setTimeout(() => this.fetchIdentityThenLoad(base), 500);
return;
}
this.identityPending = false;
this.identityFail = true;
console.warn('[auth-flow] ⚠️ ' + MAX + ' 次仍未拿到 openid:停在兜底屏等待重试,**不匿名加载**');
});
},
/* 有缓存身份就直接发起加载;返回 true 表示已经发起(调用方别再取) */
_loadWithCachedIdentity(base) {
if (!this._hostOpenid()) return false;
this.identityPending = false;
this.identityFail = false;
this._idAttempts = 0;
this._shownOnce = true;
this.webUrl = this.buildWebUrl(base + (base.indexOf('?') >= 0 ? '&' : '?') + 't=' + Date.now());
console.log('[auth-flow] ✅ 首页壳拿到 openid,开始加载 H5');
return true;
},
/**
* 给 H5 拼 hash 参数(openid 单一身份键 + unionid 可选)。
* ⚠️ 只能拼 # 不能拼 ?:微信 web-view 首次加载丢 query(app_03 实测)。
* 身份解析统一由 mixins/webviewHost.js 的 _hostOpenid / _hostUnionid 提供。
*/
buildWebUrl(url) {
const parts = [];
const openid = this._hostOpenid();
const unionid = this._hostUnionid();
if (openid) parts.push('openid=' + openid);
if (unionid) parts.push('unionid=' + unionid);
if (!parts.length) return url;
return url + (url.indexOf('#') >= 0 ? '&' : '#') + parts.join('&');
},
/* webviewHost hook:本页 web-view 地址字段是 webUrl */
_hostSetWebviewUrl(url) { this.webUrl = url; },
/* webviewHost hook:copyLink 未带 href 时的兜底地址 */
_hostCurrentUrl() { return this.webUrl || MAMA_HOME; },
/* 授权面板提交(base = 当前 H5 地址,同 onShow 的回传落点逻辑 ——
宫格换应用后 MAMA_HOME 不再是用户所在的 H5)→ mixin 的共享授权接口 + hash 回传 */
submitAuth() {
this.submitHostAuth((this.shareHref || this.webUrl || MAMA_HOME).split('#')[0]);
},
/* MP:订阅消息模板预取(原装修首页行为;utils/SubscribeMessage.js 请求时用缓存) */
getTemlIds() {
try { if (uni.getStorageSync(SUBSCRIBE_MESSAGE)) return; } catch (e) {}
getTemlIds().then(res => {
if (res && res.data) {
try { uni.setStorageSync(SUBSCRIBE_MESSAGE, JSON.stringify(res.data)); } catch (e) {}
}
}).catch(() => {});
},
/* web-view 加载失败 → 卸载 web-view 并显示「重新加载」 */
onWebviewError(e) {
console.warn('[home] web-view 加载失败:', e);
this.loadError = true;
this.webUrl = '';
},
retryLoad() {
this.loadError = false;
this.requesting = false;
this._shownOnce = true;
this.identityFail = false;
/* 有身份直接重载;没有就再走一遍「取到才加载」(重置尝试次数,自动重试 3 次) */
if (this._hostOpenid()) {
this.webUrl = this.buildWebUrl(MAMA_HOME + '?t=' + Date.now());
return;
}
this._idAttempts = 0;
this.fetchIdentityThenLoad(MAMA_HOME);
},
/* 授权完善资料(postMessage → 原生面板 → 共享授权接口 → hash 回传)与
pages/annex/web_view 共用 mixins/webviewHost.js:
handleHostMessage / onChooseHostAvatar / onHostGetPhone /
submitHostAuth(MAMA_HOME) / consumeAuthResult(见 onShow)
本页不再各写一份;分享图设置由 mixins/shareCard.js 提供。 */
}
}
</script>
<style lang="scss" scoped>
.mamabox-home {
width: 100%;
height: 100vh;
background-color: #F8F3EF;
}
/* web-view 未就绪时的兜底态(加载中 / 失败) */
.mamabox-fallback {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 80rpx;
box-sizing: border-box;
background-color: #F8F3EF;
}
.mamabox-emoji {
font-size: 96rpx;
line-height: 1;
margin-bottom: 40rpx;
}
.mamabox-tip {
text-align: center;
margin-bottom: 56rpx;
}
.mamabox-title {
font-size: 34rpx;
font-weight: 600;
color: #2B2320;
margin-bottom: 16rpx;
}
.mamabox-desc {
font-size: 26rpx;
color: #6B615B;
line-height: 1.6;
}
.mamabox-retry {
width: 320rpx;
height: 88rpx;
line-height: 88rpx;
border-radius: 44rpx;
background-color: #F4593C;
color: #FFFFFF;
font-size: 30rpx;
text-align: center;
padding: 0;
margin: 0;
border: none;
&::after {
border: none;
}
}
.mamabox-retry-hover {
opacity: .85;
}
/* 授权完善资料 弹层 */
.mb-auth-mask {
position: fixed; inset: 0; z-index: 999;
background: rgba(0, 0, 0, .45);
display: flex; align-items: center; justify-content: center;
}
.mb-auth-panel {
width: 82%; max-width: 340px;
background: #fff; border-radius: 18px;
padding: 24px 20px 18px;
}
.mb-auth-title { font-size: 17px; font-weight: 700; color: #2B2320; text-align: center; }
.mb-auth-sub { font-size: 12px; color: #A79C95; text-align: center; margin: 8px 0 18px; line-height: 1.5; }
.mb-auth-btn {
width: 100%; height: 46px; line-height: 46px; text-align: center;
border-radius: 12px; background: #F8F3EF; color: #2B2320; font-size: 14px; margin-top: 12px;
}
.mb-auth-btn.avatar { display: flex; align-items: center; justify-content: center; gap: 8px; }
.mb-auth-ava { width: 40px; height: 40px; border-radius: 50%; }
.mb-auth-input {
width: 100%; height: 46px; border-radius: 12px; background: #F8F3EF;
font-size: 14px; color: #2B2320; padding: 0 14px; margin-top: 12px;
}
.mb-auth-actions { display: flex; gap: 12px; margin-top: 20px; }
.mb-auth-cancel, .mb-auth-ok { flex: 1; height: 44px; line-height: 44px; text-align: center; border-radius: 12px; font-size: 14px; font-weight: 600; }
.mb-auth-cancel { background: #F1E6E0; color: #6B615B; }
.mb-auth-ok { background: #F4593C; color: #fff; }
</style>
<!-- ================================================================
恢复原 CRMEB 装修首页:
cp pages/index/index.vue.origin-bak pages/index/index.vue
并把 pages.json 里 pages/index/index 的 enablePullDownRefresh
改回 true(本版改成了 false —— web-view 页面下拉刷新无意义)。
================================================================ -->