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

440 lines
20 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>
<!-- ============================================================
首页(临时版):兜爸心选 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 未就绪或加载出错时才渲染) -->
<view v-else class="mamabox-fallback">
<view class="mamabox-emoji">{{ loadError ? '🥲' : '🍼' }}</view>
<view class="mamabox-tip">
<view class="mamabox-title">{{ loadError ? '页面加载失败' : '正在加载' }}</view>
<view class="mamabox-desc">{{ loadError ? '请检查网络后重试' : '正在为你打开「兜爸心选」' }}</view>
</view>
<button v-if="loadError" class="mamabox-retry" hover-class="mamabox-retry-hover" @tap="retryLoad">重新加载</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'])
},
data() {
return {
webUrl: '', // web-view 地址(硬编码 + #openid/#unionid)
requesting: false, // 防重入
_shownOnce: false, // 首次展示标记(避免首次 onShow 重复加载)
loadError: false, // web-view 加载失败(显示「重新加载」)
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;
}
// ✅ 实测结论(微信开发者工具):首页的 <web-view> 是原生全屏组件,
// 会**盖住**原生 tabBar,所以首页只显示 H5 内部导航(无双条);
// 切到商城原生页(分类/购物车/我的)时 tabBar 正常显示,点「首页」tab 可回 H5。
// ⇒ 无需 uni.hideTabBar()(隐藏了反而让商城 tab 根页变死胡同),保持现状即可。
this.loadMamaBox();
},
onShow() {
// 通知自定义 tabBar 按当前路由刷新(首页=web-view,不显示 tabBar)
uni.$emit('tabbar-update');
// 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];
this.webUrl = this.buildWebUrl(reloadBase + '?t=' + Date.now());
} else {
this._shownOnce = true;
}
// ⚠️ 实测:home/商城 之间 programmatic 切换 tabBar(hide/show) 会引发
// 「进入商城跳一下 + 商品列表双 tabBar」两个副作用。已回退该方案。
// 首页的底部导航由 web-view 自身决定(多数版本会盖住原生 tabBar,
// 首页只显示 H5 内部导航),不再手动 hideTabBar。
// 购物车角标(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;
this.webUrl = this.buildWebUrl(MAMA_HOME);
this.requesting = false;
},
/**
* 给 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.webUrl = this.buildWebUrl(MAMA_HOME + '?t=' + Date.now());
},
/* 授权完善资料(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 页面下拉刷新无意义)。
================================================================ -->