- mixin 新增 _appIdentityFlow:有 token → GET wechat/app_identity; 无 openid → uni.login(weixin) → POST wechat/app_bind → 再取身份 - _silenceAuthOpenid 在 APP-PLUS 下改走该流程(不碰 wx.login) - 首页壳/容器页:未登录时停止自动重试,兜底屏给「登录后继续」; 登录页成功后 reLaunch 回原页(首页 / 容器页带原 url) - App 的 requestProfile 改为直接查共享资料并回传 hash(不弹面板、 不调 getPhoneNumber,规避 web-view 覆盖页内浮层) - api/user.js 新增 appIdentity / appBind Co-authored-by: Cursor <cursoragent@cursor.com>
851 lines
44 KiB
Vue
851 lines
44 KiB
Vue
<template>
|
||
<!-- ============================================================
|
||
首页(临时版):兜爸心选 H5 · web-view 壳
|
||
------------------------------------------------------------
|
||
本页替代了 CRMEB 原装修首页(原文件完整备份在同目录
|
||
index.vue.origin-bak,恢复方法见本文件底部注释)。
|
||
|
||
链路(2026-10-08 起:首页地址后台可定制,不再写死):
|
||
onLoad → _homeUrlReady()(拉 box_app GET /api/mp/public/home/config;
|
||
本地缓存兜底,都没有才用内置默认 MAMA_HOME)
|
||
→ loadMamaBox() → webUrl = 生效首页地址(默认 /mama-box/)+ #openid=xxx(buildWebUrl)
|
||
→ <web-view> 加载该 H5
|
||
(更新首页内容 = 改服务端对应 H5 目录;换首页地址 = 管理后台「首页配置」,均无需发版)
|
||
|
||
两个坑(都来自 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="appNeedLogin" class="mamabox-retry" hover-class="mamabox-retry-hover" @tap="goAppLogin">登录后继续</button>
|
||
<button v-else-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();
|
||
|
||
// 内置兜底首页地址(2026-10-08 起不再是唯一来源):
|
||
// 生效地址 = box_app 后台配置(GET /api/mp/public/home/config)→ 本地缓存 → 这里。
|
||
// 管理后台清空配置 = 回到本值;后端 DEFAULT_HOME_URL 与此保持一致。
|
||
const MAMA_HOME = 'https://mp.yizhezhe.com/mama-box/';
|
||
// 首页地址配置接口(box_app 免登录面;保留名见 docs/api-naming-convention.md 第四节)
|
||
const HOME_CONFIG_API = 'https://mp.yizhezhe.com/api/mp/public/home/config';
|
||
// 生效首页地址的本地缓存(离线/弱网时用上次拿到的地址,不回退默认)
|
||
const HOME_URL_CACHE_KEY = 'MP_HOME_URL_CACHE';
|
||
// 与后端校验同口径:只要 https 且无空白;域名合法性由微信「web-view 业务域名」白名单兜底
|
||
function isValidHomeUrl(u) {
|
||
return typeof u === 'string' && /^https:\/\//i.test(u) && !/\s/.test(u);
|
||
}
|
||
|
||
/* 剥离分享链接里的身份/缓存参数。分享出去的链接必须干净:
|
||
① 不能把分享者的 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.appNeedLogin) return '登录后继续';
|
||
if (this.identityFail) return '未获取到微信身份';
|
||
if (this.identityPending) return '正在获取微信身份';
|
||
return '正在加载';
|
||
},
|
||
fallbackDesc() {
|
||
if (this.loadError) return '请检查网络后重试';
|
||
if (this.appNeedLogin) return '使用手机号验证码登录;登录后会获取微信身份并自动进入首页';
|
||
if (this.identityFail) return '请检查网络后点下方重试,或退出小程序重新进入';
|
||
if (this.identityPending) return '马上就好,请稍候(不会加载空数据的页面)';
|
||
if (this.restoring) return '正在回到你上次使用的应用';
|
||
return '正在为你打开「兜爸心选」';
|
||
},
|
||
fallbackEmoji() {
|
||
return this.appNeedLogin ? '🔐' : ((this.loadError || this.identityFail) ? '🥲' : '🍼');
|
||
},
|
||
},
|
||
data() {
|
||
return {
|
||
webUrl: '', // web-view 地址(首页地址/当前 H5 + #openid/#unionid)
|
||
_homeUrl: '', // 生效首页地址(后台配置;非响应式内部字段,见 _homeUrlReady)
|
||
_homeUrlTask: null, // 「拉首页地址配置」的单飞 Promise(一次冷启动只请求一次)
|
||
requesting: false, // 防重入
|
||
_shownOnce: false, // 首次展示标记(避免首次 onShow 重复加载)
|
||
_idAttempts: 0, // 「取到 openid 才加载」的自动重试计数(见 fetchIdentityThenLoad)
|
||
loadError: false, // web-view 加载失败(显示「重新加载」)
|
||
restoring: false, // 冷启动正在判定/打开「上次进入的 H5 应用」(只影响兜底屏文案)
|
||
_noRestore: false, // 本次加载 mama-box 是否要带 #norestore=1(禁止 H5 兜底再把用户送回应用)
|
||
identityPending: false, // 正在获取 openid(此期间**不加载 H5**,见 fetchIdentityThenLoad)
|
||
identityFail: false, // 多次仍未拿到 openid → 停在兜底屏,给「重新获取身份」入口
|
||
appNeedLogin: false, // App:没登录(或登录后仍无身份)→ 兜底屏给「登录后继续」
|
||
shareInfo: {}, // CRMEB 后台「分享设置」(标题/图,无 App 匹配时兜底)
|
||
// 授权面板字段由 mixins/webviewHost.js 提供:
|
||
// showAuth / authNickname / authAvatar / _authCode
|
||
// 分享字段由 mixins/shareCard.js 提供:
|
||
// shareTitle / shareHref / shareImageUrl / shareApps…
|
||
};
|
||
},
|
||
onLoad(options) {
|
||
/* 诊断日志(过滤关键字 enter-app):首页壳每次「冷启动」都会打这一行;
|
||
如果重进时**看不到它**,说明是热启动(onShow),恢复走的是「保持不动」那条路。 */
|
||
try {
|
||
console.log('[enter-app] ===== 首页壳 onLoad(冷启动)===== options=' + JSON.stringify(options || {}));
|
||
} catch (e) {}
|
||
/* 消费「本次冷启动是否允许恢复」标记(App.onLaunch 置位,见其注释)。
|
||
放在最前面就消费:后面任何提前 return(商户/管理员跳工作台、显式落地)都不该把标记留给下次。 */
|
||
let restoreArmed = false;
|
||
try {
|
||
restoreArmed = !!(app && app.globalData && app.globalData.restoreArmed);
|
||
if (app && app.globalData) app.globalData.restoreArmed = false;
|
||
} catch (e) {}
|
||
console.log('[enter-app] restoreArmed=' + restoreArmed +
|
||
'(true=冷启动落在首页,可恢复;false=显式落地或从应用内回首页,不恢复)');
|
||
// #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
|
||
// 最早发出「首页地址配置」请求(与身份解析并行,通常先于它完成 → 零额外等待)。
|
||
// loadMamaBox / retryLoad / onShow 都通过 _homeUrlReady() / _homeBase() 消费它。
|
||
this._homeUrlReady();
|
||
// 预热 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) {}
|
||
console.log('[enter-app] 显式落地(options.url,分享/朋友圈)→ 直接进容器页: ' + target +
|
||
'(优先级高于「恢复上次应用」)');
|
||
this.loadMamaBox(true); // 保留首页作为返回落点;带 #norestore,避免 H5 兜底把落点换成别的应用
|
||
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();
|
||
/* 冷启动恢复(2026-09-22,档 3):先问 box_app「上次进入的 H5 应用」,
|
||
命中就直接把容器页压上去 —— **不加载 mama-box,因此没有首页闪屏**。
|
||
未命中 / 取数失败 / 无身份 → 回落到原路径(加载 mama-box)。
|
||
⚠️ 只有 options.url(分享/朋友圈落地)算显式落地并已在上面 return;
|
||
options.scene **每次冷启动都有值**(1001 最近使用 / 1007 分享 / 1011 扫码…),
|
||
不能拿它当「显式落地」判据,否则永远不恢复。 */
|
||
/* 只有「冷启动落在首页/引导页且无显式落地」才尝试恢复(restoreArmed 已在 onLoad 开头消费)。
|
||
扫码/分享进容器页、或从应用内点左上角「小房子」回首页时 armed=false → 不恢复,并且加载
|
||
mama-box 时带 #norestore=1(否则它的兜底恢复又会把用户送回刚才那个应用,出现
|
||
「点小房子回不了首页、要多点一次返回」的循环;2026-09-22 用户实测)。 */
|
||
if (!restoreArmed) {
|
||
console.log('[enter-app] 本次不是「冷启动落在首页」(显式落地 / 从应用内回首页)→ 不恢复,' +
|
||
'直接进首页(mama-box 带 #norestore=1)');
|
||
this.loadMamaBox(true);
|
||
return;
|
||
}
|
||
console.log('[enter-app] 非显式落地 → 尝试恢复「上次进入的 H5 应用」…');
|
||
this.restoreOrLoadHome();
|
||
},
|
||
onShow() {
|
||
/* 是否「小程序热启动恢复」(点胶囊关闭后再进 / 切后台再回),而不是从其它 tab 切回首页:
|
||
App.onShow 刚触发过 → 页面栈与 web-view 都还在,用户仍停在离开时的那个 H5 应用里。
|
||
⚠️ 这种情况**必须跳过下面的「重载 webview」**,否则会把用户从 H5 应用踢回兜爸心选首页
|
||
—— 2026-09-22 线上问题根因:热启动不触发 onLoad(恢复逻辑没机会跑),
|
||
却触发了这里的重载;而 shareHref 仍是 mama-box(H5 postMessage 被微信攒着没派发),
|
||
于是重载到首页,mama-box 的兜底又被 sessionStorage 闸门挡住 → 停在首页。
|
||
⚠️ 切 tab 不触发 App.onShow,所以那个场景仍按原行为重载回首页。 */
|
||
let isAppResume = false;
|
||
try {
|
||
const at = (app && app.globalData && app.globalData.appShowAt) || 0;
|
||
isAppResume = at > 0 && (Date.now() - at) < 2000;
|
||
} catch (e) {}
|
||
/* 诊断日志(过滤关键字 enter-app):这一行是「热启动还是切 tab」的唯一判据来源。
|
||
isAppResume=true → 热启动恢复(点胶囊关闭后再进):保持当前 H5 不动,不重载;
|
||
isAppResume=false → 从其它 tab 切回首页:按原行为重载 web-view 回首页。 */
|
||
console.log('[enter-app] 首页壳 onShow: isAppResume=' + isAppResume +
|
||
'(_shownOnce=' + this._shownOnce + ', webUrl=' + (this.webUrl ? '有' : '空') +
|
||
', shareHref=' + (this.shareHref || '空') + ')');
|
||
// 每次回到首页都再压一次 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 || this._homeBase()).split('#')[0];
|
||
const authUrl = this.consumeAuthResult(authBase);
|
||
if (authUrl) {
|
||
this.loadError = false;
|
||
this._shownOnce = true;
|
||
this.webUrl = authUrl;
|
||
} else if (this._shownOnce && this.webUrl && !isAppResume) {
|
||
// ⚠️ 从商城/其它 tab 切回首页时:webview 是同一个实例,SPA 会停留在切走前的屏。
|
||
// 改为「重载 webview」让 H5 落回首页(H5 默认即是首页)。首次展示不重复刷。
|
||
// 同理不能写死 MAMA_HOME —— 用户停在宫格切换的应用里时会被踢回 mama-box;
|
||
// 以 shareHref(当前 H5)为基址重载,停留在原应用首页。
|
||
// ⚠️ 热启动恢复(isAppResume)走不到这里:见 onShow 开头的说明。
|
||
this.loadError = false;
|
||
const reloadBase = (this.shareHref || this._homeBase()).split('#')[0];
|
||
console.log('[enter-app] onShow: 非热启动(切 tab 回首页)→ 重载 web-view,基址=' + reloadBase);
|
||
// 宿主自己把首页重载回首页 H5 时,同步清掉 last_app(人已经回到首页了)。
|
||
// 覆盖「容器页返回箭头回首页」这类不经过 H5 后退的路径 —— H5 侧 clearLastAppOnBackHome()
|
||
// 只认浏览器后退(back_forward/pageshow.persisted)或 referrer,宿主 src 重载不一定带得上 referrer。
|
||
// ⚠️ 2026-10-08 起首页地址可定制:不能再写死判断 '/mama-box',要按生效首页地址比较
|
||
// (否则定制首页后回首页不清 last_app,下次冷启动会被送回旧应用)。
|
||
if (this._isHomeBase(reloadBase)) this._clearLastApp();
|
||
if (this._hostOpenid()) {
|
||
// ⚠️ 回首页 tab 的重载一律带 #norestore=1:这是「用户要回首页」,
|
||
// 不能让 mama-box 的兜底恢复又把他送回上次那个应用(见 onLoad 的入口判定)。
|
||
this.webUrl = this.buildWebUrl(reloadBase + '?t=' + Date.now(), ['norestore=1']);
|
||
} else {
|
||
/* ⚠️ 仍无 openid:走同一条「取到才加载」的路(自动重试 3 次 → 兜底屏),
|
||
不再匿名重载(原因见 fetchIdentityThenLoad 的注释)。 */
|
||
console.log('[auth-flow] 🔄 onShow: 仍无 openid,走「取到才加载」');
|
||
this._idAttempts = 0;
|
||
this._noRestore = true; // 同上:回首页 tab 不许 mama-box 兜底恢复
|
||
this.fetchIdentityThenLoad(reloadBase);
|
||
}
|
||
} else {
|
||
if (isAppResume) {
|
||
console.log('[enter-app] onShow: 热启动恢复 → 保持当前 web-view 不重载(用户仍停在上次的 H5 应用里)');
|
||
}
|
||
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 || this._homeBase();
|
||
// 按「当前 H5 地址」解析分享图:先做应用切换检测(首页壳不随 H5 换页重新 onLoad),
|
||
// 再每次重新反查 box_app —— 不能因为 shareImageUrl 已有值就跳过,
|
||
// 那是上一个应用的图(A 应用分享过、切到 B 后仍带 A 的图,就是这里出的错)。
|
||
this.resolveShareCard(target);
|
||
const info = this.shareInfo || {};
|
||
const uid = this.uid || 0;
|
||
return {
|
||
// 标题来源优先级:H5 postMessage 上报 → box_app 反查(后台「分享标题」,留空 = 应用名称,
|
||
// 由 resolveShareCard 写进 shareTitle)→ CRMEB 后台分享设置 → 内置兜底。
|
||
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 || this._homeBase();
|
||
this.resolveShareCard(target);
|
||
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;
|
||
},
|
||
/**
|
||
* 加载首页 H5。
|
||
* 2026-10-08 起:首页地址不再写死 —— 先等 _homeUrlReady()(box_app 后台配置,
|
||
* 本地缓存兜底,最多 2s),再按「有 openid 直接拼 hash 加载 / 无 openid 走取到才加载」。
|
||
*/
|
||
loadMamaBox(noRestore) {
|
||
/* @param {boolean} noRestore - 加载首页时给 URL 加 #norestore=1,禁止首页 H5 自己恢复
|
||
(它的兜底恢复会把用户从「用户明确要去的页面」再送回上次那个应用)。两种情况要带:
|
||
· 恢复成功后留下的「返回落点」;
|
||
· 本次明确判定「不恢复」(显式落地 / 从应用内回首页)。
|
||
只有「尝试恢复但取数失败」才不带 —— 那时让首页 H5 兜底重试一次。 */
|
||
this._noRestore = !!noRestore;
|
||
console.log('[enter-app] 加载首页(返回落点)noRestore=' + this._noRestore);
|
||
if (this.requesting) return;
|
||
this.requesting = true;
|
||
this.loadError = false;
|
||
this.restoring = 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;
|
||
this._homeUrlReady().then((homeUrl) => {
|
||
if (this._hostOpenid()) {
|
||
this.identityPending = false;
|
||
this._idAttempts = 0;
|
||
this.webUrl = this.buildWebUrl(homeUrl, this._noRestore ? ['norestore=1'] : []);
|
||
this.requesting = false;
|
||
return;
|
||
}
|
||
/* ⚠️⚠️ 拿不到 openid **就不加载**(2026-09-20 用户定的行为):
|
||
宁可让用户在这一屏多等几秒(自动重试 3 次),也不匿名加载 H5 ——
|
||
匿名加载会让 H5 进访客态(孕周 `--`、档案查不到),
|
||
用户看到「页面能用但全是空」比等一会儿更糟,且事后很难判断是身份问题。 */
|
||
this._idAttempts = 0;
|
||
this.fetchIdentityThenLoad(homeUrl);
|
||
this.requesting = false;
|
||
});
|
||
},
|
||
/* ---------- 首页地址:box_app 后台可定制(2026-10-08) ----------
|
||
过去 MAMA_HOME 写死在本文件,换首页要发小程序版。现在冷启动拉
|
||
GET /api/mp/public/home/config 拿生效地址(管理后台「首页配置」页可改,
|
||
homeUrl 清空 = 回内置默认);homeUrlReady 是单飞 Promise,onLoad 最先发出,
|
||
loadMamaBox / retryLoad 消费 —— 与身份解析并行,正常零额外等待。
|
||
可靠性分层(每一层都不能阻塞首页加载,AGENTS.md §5.6):
|
||
① 配置请求失败/超时(2s)→ 用「本地缓存的上次地址」(离线也能进上次的定制首页);
|
||
② 本地也没有 → 内置默认 MAMA_HOME;
|
||
③ 响应 code!==200(⚠️ 这个后端不存在路由时 HTTP 仍是 200,错误码在 body,见 §5.7)
|
||
或 url 非法 → 同②,但**不覆盖**本地缓存(分不清是接口坏了还是后台清空,
|
||
宁可沿用旧值,下次冷启动再试);
|
||
④ 只有拿到合法地址(含后台清空时下发的默认地址)才写缓存。
|
||
⚠️ 生效时机是「下一次冷启动」:已加载的 web-view 不因配置变化主动重载
|
||
(热启动不打扰用户,与 onShow 的 isAppResume 原则一致)。 */
|
||
_homeUrlReady() {
|
||
if (this._homeUrlTask) return this._homeUrlTask;
|
||
// 先用本地缓存占位(非响应式内部字段,只给 JS 逻辑用,与 _shownOnce 同款)
|
||
try {
|
||
const cached = uni.getStorageSync(HOME_URL_CACHE_KEY);
|
||
if (cached && isValidHomeUrl(cached.url)) this._homeUrl = cached.url;
|
||
} catch (e) {}
|
||
this._homeUrlTask = new Promise((resolve) => {
|
||
let done = false;
|
||
const finish = (url) => {
|
||
if (done) return;
|
||
done = true;
|
||
if (url) {
|
||
this._homeUrl = url;
|
||
try { uni.setStorageSync(HOME_URL_CACHE_KEY, { url: url, at: Date.now() }); } catch (e) {}
|
||
}
|
||
resolve(this._homeUrl || MAMA_HOME);
|
||
};
|
||
const timer = setTimeout(() => {
|
||
console.log('[enter-app] ⏱ 首页地址配置超时(2s)→ 按「本地缓存/默认」处理');
|
||
finish(null);
|
||
}, 2000);
|
||
uni.request({
|
||
url: HOME_CONFIG_API,
|
||
method: 'GET',
|
||
timeout: 2000,
|
||
success: (res) => {
|
||
clearTimeout(timer);
|
||
const body = res && res.data;
|
||
const url = body && body.code === 200 && body.result ? body.result.url : null;
|
||
if (isValidHomeUrl(url)) {
|
||
console.log('[enter-app] 首页地址生效: ' + url +
|
||
(url === MAMA_HOME ? '(内置默认)' : '(后台定制)'));
|
||
finish(url);
|
||
} else {
|
||
console.log('[enter-app] 首页地址配置无合法值(body=' + JSON.stringify(body) +
|
||
') → 沿用「本地缓存/默认」');
|
||
finish(null);
|
||
}
|
||
},
|
||
fail: (err) => {
|
||
clearTimeout(timer);
|
||
console.log('[enter-app] ✗ 首页地址配置请求失败 → 按「本地缓存/默认」处理: ' + JSON.stringify(err));
|
||
finish(null);
|
||
}
|
||
});
|
||
});
|
||
return this._homeUrlTask;
|
||
},
|
||
/* 当前生效的首页基址(同步取:后台配置 → 本地缓存 → 内置默认)。
|
||
用于分享兜底 / 授权回传落点 / 兜底屏重试这些「拿不到实时值时」的场景。 */
|
||
_homeBase() {
|
||
return this._homeUrl || MAMA_HOME;
|
||
},
|
||
/* base 是否就是首页 H5(去 hash/query、忽略末尾斜杠后比较)。
|
||
过去写死判断 indexOf('/mama-box');首页可定制后必须按生效地址比较,
|
||
否则定制首页时「宿主重载回首页」不会清 last_app(下次冷启动被送回旧应用)。 */
|
||
_isHomeBase(base) {
|
||
const norm = (u) => String(u || '').split('#')[0].split('?')[0].replace(/\/+$/, '');
|
||
return !!base && norm(base) === norm(this._homeBase());
|
||
},
|
||
/* ---------- 冷启动恢复:直接把容器页指向「上次进入的 H5 应用」(2026-09-22 档 3) ----------
|
||
需求:进入某 H5 应用后退出小程序,再进要恢复它。
|
||
做法:本页自己问 box_app `GET /api/mp/public/last-app?openid=`(Redis,per-openid,
|
||
7 天 TTL);命中就直接 navigateTo 容器页加载目标 —— **不加载 mama-box,无首页闪屏**。
|
||
优先级:显式落地(options.url 分享/朋友圈)> 恢复 > 首页。
|
||
回落:无记录 / 取数失败 / 超时 / 拿不到 openid → 照旧加载 mama-box
|
||
(mama-box 自己的 resumeLastApp 仍是「本页取数失败」时的兜底)。
|
||
⚠️ 身份注入交给容器页(它用 query 注入)。本页的 buildWebUrl 是 **hash** 注入,
|
||
对自带 hash 路由的应用(如 baobao-yingji)会破坏路由,所以恢复路径不自己拼身份。 */
|
||
restoreOrLoadHome() {
|
||
const decide = () => {
|
||
const openid = this._hostOpenid();
|
||
if (!openid) {
|
||
console.log('[enter-app] ✗ 身份仍未就绪(无 openid)→ 不查 last-app,交给原路径加载首页');
|
||
this.loadMamaBox();
|
||
return;
|
||
}
|
||
console.log('[enter-app] 查 last-app: openid=' + openid);
|
||
this._fetchLastApp(openid).then(rec => {
|
||
if (rec && rec.url) {
|
||
this._openRestoredApp(rec.url);
|
||
} else {
|
||
console.log('[enter-app] ○ last-app 无记录/未拿到 → 进兜爸心选首页');
|
||
this.loadMamaBox();
|
||
}
|
||
});
|
||
};
|
||
this.restoring = true;
|
||
if (this._hostOpenid()) {
|
||
console.log('[enter-app] 恢复判定开始:openid 命中缓存,直接查 last-app');
|
||
decide();
|
||
return;
|
||
}
|
||
/* 无缓存身份:先等 App.vue onLaunch 那次静默登录(与 fetchIdentityThenLoad 同款——
|
||
同一次 code exchange,不重复换 code),最多 1.5s;仍拿不到就交回原路径。 */
|
||
console.log('[enter-app] 恢复判定开始:无缓存 openid,先等 App.vue 静默登录(最多 1.5s)…');
|
||
this._awaitHostLogin(1500).then(decide);
|
||
},
|
||
/* 读一次 last_app;任何失败/超时都 resolve(null),绝不阻塞首页 */
|
||
_fetchLastApp(openid) {
|
||
return new Promise(resolve => {
|
||
let done = false;
|
||
const finish = (v) => { if (!done) { done = true; resolve(v); } };
|
||
const api = 'https://mp.yizhezhe.com/api/mp/public/last-app?openid=' + encodeURIComponent(openid);
|
||
const timer = setTimeout(() => {
|
||
console.log('[enter-app] ⏱ last-app 请求超时(1.5s)→ 按无记录处理');
|
||
finish(null);
|
||
}, 1500);
|
||
uni.request({
|
||
url: api,
|
||
method: 'GET',
|
||
timeout: 1500,
|
||
success: (res) => {
|
||
clearTimeout(timer);
|
||
const body = res && res.data;
|
||
const rec = body && body.result;
|
||
console.log('[enter-app] last-app 响应: HTTP ' + ((res && res.statusCode) || '?') +
|
||
' body=' + JSON.stringify(body));
|
||
finish(rec && rec.url ? rec : null);
|
||
},
|
||
fail: (err) => {
|
||
clearTimeout(timer);
|
||
console.log('[enter-app] ✗ last-app 请求失败 → 按无记录处理: ' + JSON.stringify(err));
|
||
finish(null);
|
||
}
|
||
});
|
||
});
|
||
},
|
||
/* 宿主侧兜底清除:小程序自己把首页重载回 mama-box 时同步清 last_app(见 onShow 调用处)。
|
||
与 H5 侧 clearLastAppOnBackHome() 互补:那条认浏览器后退/referrer,这条是宿主确定要回首页。 */
|
||
_clearLastApp() {
|
||
try {
|
||
const openid = this._hostOpenid();
|
||
if (!openid) return;
|
||
console.log('[enter-app] 宿主重载回首页 → 清除 last_app(下次冷启动不再回上次那个应用)');
|
||
uni.request({
|
||
url: 'https://mp.yizhezhe.com/api/mp/public/last-app?openid=' + encodeURIComponent(openid),
|
||
method: 'DELETE',
|
||
timeout: 5000,
|
||
fail: (err) => console.log('[enter-app] ✗ 清除 last_app 失败(忽略): ' + JSON.stringify(err))
|
||
});
|
||
} catch (e) {}
|
||
},
|
||
/* 命中恢复目标:保留首页作为返回落点 + 交容器页加载(与 options.url 分享落地同款)
|
||
⚠️ 落点必须带 #norestore=1:否则 mama-box 会在容器页底下**自己再恢复一次**,
|
||
把返回落点也换成目标应用,back 就回不到首页了。 */
|
||
_openRestoredApp(url) {
|
||
console.log('[enter-app] ✅ 命中 last-app → 直接进容器页加载: ' + url +
|
||
'(不经过 mama-box,无首页闪屏)');
|
||
this.restoring = false;
|
||
this.loadMamaBox(true);
|
||
this.$nextTick(() => {
|
||
uni.navigateTo({ url: '/pages/annex/web_view/index?url=' + encodeURIComponent(url) });
|
||
});
|
||
},
|
||
/**
|
||
* 取到 openid 才加载 H5;自动重试 3 次仍失败 → 停在兜底屏给「重新获取身份」入口。
|
||
* ⚠️ 每次尝试前都再读一次 _hostOpenid():App.vue 的 onLaunch 静默授权是并行的,
|
||
* 它先落地的话这里直接命中缓存,不必自己再请求一次。
|
||
* @param {string} base - 要加载的 H5 基址(首页壳是生效首页地址;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)。 */
|
||
let task;
|
||
// #ifdef APP-PLUS
|
||
/* App:没有并行的 wx.login,直接走「短信登录态 + app_identity/app_bind」 */
|
||
task = this._silenceAuthOpenid();
|
||
// #endif
|
||
// #ifndef APP-PLUS
|
||
task = (seq === 0) ? this._awaitHostLogin(1500) : this._silenceAuthOpenid();
|
||
// #endif
|
||
task.then((r) => {
|
||
if (this._loadWithCachedIdentity(base)) return;
|
||
/* App 未登录:**停止自动重试**,兜底屏给「登录后继续」按钮 ——
|
||
没有 token 时重试多少次都拿不到身份,继续循环只是让用户干等。 */
|
||
if (r && r.needLogin) {
|
||
this.identityPending = false;
|
||
this.identityFail = false;
|
||
this.appNeedLogin = true;
|
||
console.log('[auth-flow] App 未登录 → 等待用户登录(不自动重试)');
|
||
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.appNeedLogin = false;
|
||
this._idAttempts = 0;
|
||
this._shownOnce = true;
|
||
// 沿用本次的 _noRestore(见 loadMamaBox 注释):本轮加载是否需要禁止 mama-box 兜底恢复
|
||
this.webUrl = this.buildWebUrl(base + (base.indexOf('?') >= 0 ? '&' : '?') + 't=' + Date.now(),
|
||
this._noRestore ? ['norestore=1'] : []);
|
||
console.log('[auth-flow] ✅ 首页壳拿到 openid,开始加载 H5');
|
||
return true;
|
||
},
|
||
/**
|
||
* 给 H5 拼 hash 参数(openid 单一身份键 + unionid 可选)。
|
||
* ⚠️ 只能拼 # 不能拼 ?:微信 web-view 首次加载丢 query(app_03 实测)。
|
||
* 身份解析统一由 mixins/webviewHost.js 的 _hostOpenid / _hostUnionid 提供。
|
||
* @param {string} url
|
||
* @param {string[]} [extraHash] 额外 hash 参数(如 ['norestore=1'];仅本页自用)
|
||
*/
|
||
buildWebUrl(url, extraHash) {
|
||
const parts = [];
|
||
const openid = this._hostOpenid();
|
||
const unionid = this._hostUnionid();
|
||
if (openid) parts.push('openid=' + openid);
|
||
if (unionid) parts.push('unionid=' + unionid);
|
||
const all = parts.concat(extraHash || []);
|
||
if (!all.length) return url;
|
||
return url + (url.indexOf('#') >= 0 ? '&' : '#') + all.join('&');
|
||
},
|
||
|
||
/* webviewHost hook:本页 web-view 地址字段是 webUrl */
|
||
_hostSetWebviewUrl(url) { this.webUrl = url; },
|
||
/* webviewHost hook:copyLink 未带 href 时的兜底地址 */
|
||
_hostCurrentUrl() { return this.webUrl || this._homeBase(); },
|
||
|
||
/* 授权面板提交(base = 当前 H5 地址,同 onShow 的回传落点逻辑 ——
|
||
宫格换应用后首页地址不再是用户所在的 H5)→ mixin 的共享授权接口 + hash 回传 */
|
||
submitAuth() {
|
||
this.submitHostAuth((this.shareHref || this.webUrl || this._homeBase()).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;
|
||
this.appNeedLogin = false;
|
||
/* 有身份直接重载;没有就再走一遍「取到才加载」(重置尝试次数,自动重试 3 次)
|
||
⚠️ 沿用 this._noRestore:重试首页时同样不许首页 H5 兜底恢复(见 loadMamaBox)。 */
|
||
const homeUrl = this._homeBase();
|
||
if (this._hostOpenid()) {
|
||
this.webUrl = this.buildWebUrl(homeUrl + '?t=' + Date.now(),
|
||
this._noRestore ? ['norestore=1'] : []);
|
||
return;
|
||
}
|
||
this._idAttempts = 0;
|
||
this.fetchIdentityThenLoad(homeUrl);
|
||
},
|
||
|
||
/* App:去登录。登录页成功后会 reLaunch 回 login_back_url(这里显式设成本页),
|
||
回到首页再走一遍「短信登录态 → app_identity → app_bind」。 */
|
||
goAppLogin() {
|
||
try { uni.setStorageSync('login_back_url', '/pages/index/index'); } catch (e) {}
|
||
uni.navigateTo({ url: '/pages/users/login/index' });
|
||
},
|
||
|
||
/* 授权完善资料(postMessage → 原生面板 → 共享授权接口 → hash 回传)与
|
||
pages/annex/web_view 共用 mixins/webviewHost.js:
|
||
handleHostMessage / onChooseHostAvatar / onHostGetPhone /
|
||
submitHostAuth(生效首页地址) / 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 页面下拉刷新无意义)。
|
||
================================================================ -->
|