chore: 入库生产基线(2026-10-26 工作树快照)
- 含首页壳/容器页/授权页/分享图/首页配置等此前未入库的在途改动 - 新增未跟踪的 pages/index/mall.vue(第 5 个 tabBar 页,pages.json 已引用) - 排除本地备份文件(*.bak* / *.origin-bak) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
58
AGENTS.md
58
AGENTS.md
@@ -29,7 +29,7 @@ CRMEB 商城小程序(uniapp,manifest `name=DoubaShop`),同时是**整
|
||||
| `mixins/webviewHost.js` | 宿主公共能力:postMessage 分发(location/share/copyLink/setTitle/requestProfile)、原生授权面板、授权回传、静默登录复用 |
|
||||
| `mixins/shareCard.js` | 分享图:按当前 H5 URL 反查 box_app App 表;朋友圈本地图转换 |
|
||||
| `App.vue` | 冷启动静默登录、`globalData`(`spid` / `appShowAt`)、`onShow`(前台化时间戳) |
|
||||
| `pages/annex/auth/index.vue` | 独立授权页(`navigateTo` 进入,成功写 `__AUTH_RESULT__` 后 `navigateBack`) |
|
||||
| `pages/annex/auth/index.vue` | 独立授权页(`navigateTo` 进入,**先手机号 → 再问头像昵称**,结束写 `__AUTH_RESULT__` 后 `navigateBack`,见 §4.4) |
|
||||
| `pages/guide/index.vue` | `pages.json` 第一个页面(冷启动入口,无广告时 `switchTab` 到首页壳) |
|
||||
|
||||
## 3. 硬约定
|
||||
@@ -86,6 +86,62 @@ CRMEB 商城小程序(uniapp,manifest `name=DoubaShop`),同时是**整
|
||||
误清会毁掉刚用上的记录(实测:普通加载与 `#norestore=1` 落点加载都不清)。
|
||||
- ⚠️ 首页壳只在**重载目标是 mama-box** 时清;重载目标是某个应用(`shareHref` 指向它)时不能清。
|
||||
|
||||
### 4.4 独立授权页:只采集手机号,再按库里资料决定是否问复用(2026-09-22 改版)
|
||||
|
||||
页面交互按 `box_app/design/授权流程演示/index.html` 重做,链路与契约(`backUrl` / `__AUTH_RESULT__` 形状 /
|
||||
宿主 `consumeAuthResult`)**都没变**,只改了页内流程:
|
||||
|
||||
```
|
||||
S0 半弹窗问手机号 ─「否」→ 直接 navigateBack(不写结果)
|
||||
└「是,授权手机号」= <button open-type="getPhoneNumber"> 本身 → 拿 code
|
||||
→ POST /api/mp/public/auth/profile?code=… 落库手机号
|
||||
→ GET /api/mp/public/auth/profile?openid=… 查库(判分支、取值展示)
|
||||
├ 库里有昵称/头像 → 问「是否复用」→ 是/否都收尾返回
|
||||
└ 库里没有 → **不追问、不给表单**,直接收尾返回
|
||||
收尾 = 写 __AUTH_RESULT__(手机号 + 库里真实值) → navigateBack
|
||||
```
|
||||
|
||||
五个必须守住的点(前三条照抄演示页 README 的告诫):
|
||||
|
||||
1. **「是,授权手机号」按钮本身必须是 `open-type="getPhoneNumber"`** —— 手机号没有可编程调起的 API,
|
||||
不能「先点按钮 → 关弹窗 → 代码调起」。
|
||||
2. **手机号落库的成功判据是 `code===200 && result.phone` 非空**(后端解密失败会如实返回 400)。不满足就
|
||||
**留在手机号弹窗**报错,绝不写 `__AUTH_RESULT__`、绝不提示成功 —— 假成功会让 H5 闸门拦截 → 用户反复授权
|
||||
反复被拦(2026-09-18 死循环事故)。
|
||||
3. **判「库里是否已有资料」用 `!!(userName || avatar)`,不能用「记录是否存在」**:`GET/POST auth/profile`
|
||||
对任何 openid 都返回 200 并现场建一条空记录,用存在性判断会 100% 命中「老用户」分支。
|
||||
4. **查库判分支必须走 `GET`,不能拿手机号那次 `POST` 的返回凑**(2026-09-22 修):后端的
|
||||
「先取后授权」兜底回填(本地 → 上游用户中心 → `CrmebService.user_info_by_openid` 同步 CRMEB 商城)
|
||||
**只在 `get_user_auth`(GET)里做,`apply_auth`(POST)只 find_one/建空记录**。用户只在商城
|
||||
有昵称/头像时,POST 返回仍是空 → 会误判成新用户,白丢「是否复用」那一屏。
|
||||
⚠️ 顺带明确:这一屏的昵称/头像**不是**当场从微信取的(`getUserProfile` 已废弃,昵称/头像只能由
|
||||
用户手势触发 `input type=nickname` / `chooseAvatar`),展示的永远是**库里已存过的值**。
|
||||
5. **本页只采集手机号**(2026-09-22 产品定案):库里没有昵称/头像时**不追问、不给表单**,
|
||||
直接收尾返回 —— 所以 `chooseAvatar` / `input type=nickname` / `/api/upload` 这条采集链
|
||||
已从本页移除,昵称/头像改由小程序其它流程 / 上游用户中心 / 商城同步维护。
|
||||
⚠️ 由此:`get_user_auth`(GET) 里读 CRMEB 商城用户表回填 `nickname`/`avatar` 的兜底
|
||||
(`CrmebService.user_info_by_openid`,命中标 `fromCRMEB=true`)**必须保留**(2026-09-22 确认)——
|
||||
它是头像目前**唯一稳定来源**(上游用户中心只有 `nickname`、没有 avatar,本页又不再采集)。
|
||||
去掉它 = 头像恒空、「是否复用」那一屏基本不再出现、H5 侧展示一起变空。
|
||||
口径:H5 侧仍只读 box_app(不直连商城);box_app 对 H5 是上游、对商城是下游。
|
||||
|
||||
其它:手机号落库与随后的昵称头像**解耦**(所以「不同步直接返回」不会丢手机号);「复用」是普通 button
|
||||
(不调微信接口),会再 POST 一次把 `authorized` 置位;工具宫格数据来自 `GET /api/mp/apps`
|
||||
(**仅展示、不可点击**,未授权时右上角显示小锁),拉取失败只是宫格留空、不阻塞授权。
|
||||
|
||||
⚠️ **CSS 陷阱(2026-09-22 真机截图返工)**:页面顶部有条 `button` 全局 reset
|
||||
(`.auth-page button { padding:0; border:none; … }`,特异性 (0,1,1),scoped 编译后约 (0,2,1)),
|
||||
它**高于**单类选择器 `.bsec` / `.bmain` / `.cta`((0,1,0))。所以按钮自己的 `padding` / `border`
|
||||
规则必须写成 `.auth-page .bsec { … }` 这样带前缀提权,否则会被 reset 清成 0 ——
|
||||
表现是「否」没有边框只剩文字、所有按钮高度塌成一行字高。同理 `.bwx` 这种只改底色的变体
|
||||
要写成 `.auth-page .bmain.bwx`,否则会被同级的 `.auth-page .bmain` 底色盖掉。
|
||||
|
||||
### 4.5 授权页宫格的内容风险(未决)
|
||||
|
||||
宫格按演示页照搬了「已有 36.5万+ 位妈妈在用」+ 三个内联 SVG 插画头像(`SOCIAL_COUNT` 常量),
|
||||
以及「全部免费 / 换手机不丢」等承诺 —— **演示页 README 明确标注这些是占位数据**,
|
||||
上线前必须核实真实数字并取得产品/合规确认,否则涉嫌虚假宣传。
|
||||
|
||||
## 5. 本仓库状态提醒
|
||||
|
||||
- ⚠️ `mixins/shareCard.js` **未被 git 跟踪**(untracked),但两个宿主页都 import 它 —— 提交时别漏,否则一旦丢工作区这份文件就没了。
|
||||
|
||||
32
App.vue
32
App.vue
@@ -19,6 +19,8 @@
|
||||
isLogin: false,
|
||||
userInfo: {},
|
||||
globalData: false,
|
||||
appShowAt: 0, // 本次前台化时刻(首页壳据此区分热启动恢复 / 切 tab)
|
||||
restoreArmed: false, // 冷启动是否允许首页壳恢复「上次进入的 H5 应用」(见 onLaunch)
|
||||
windowHeight: uni.getWindowInfo().windowHeight + 'px',
|
||||
sysHeight:uni.getWindowInfo().statusBarHeight,
|
||||
},
|
||||
@@ -39,6 +41,18 @@
|
||||
}
|
||||
},
|
||||
onShow(options) {
|
||||
/* 记录本次「小程序回到前台」的时刻(2026-09-22):首页壳 pages/index/index
|
||||
靠它区分两种 onShow ——
|
||||
· 热启动恢复(点胶囊关闭后再进):页面栈与 web-view 都还在,
|
||||
用户就停在离开时那个 H5 应用里 → **绝不能重载 web-view**;
|
||||
· 从其它 tab 切回首页:App.onShow **不会**触发 → 维持原行为(重载回兜爸心选首页)。
|
||||
判据是时间窗:页面 onShow 紧跟 App.onShow 触发(毫秒级),切 tab 不在窗内。 */
|
||||
try { this.globalData.appShowAt = Date.now(); } catch (e) {}
|
||||
/* 诊断日志(过滤关键字 enter-app):
|
||||
· onShow 触发 = 小程序回到前台(热启动恢复或冷启动都会走),
|
||||
首页壳据此判断要不要重载 web-view;切 tab 不会打这条。 */
|
||||
console.log('[enter-app] App.onShow(前台化)appShowAt=' + this.globalData.appShowAt +
|
||||
'(首页壳 onShow 将据此识别热启动)');
|
||||
const queryData = uni.getEnterOptionsSync(); // uni-app版本 3.5.1+ 支持
|
||||
if (queryData.query.spid) {
|
||||
this.$Cache.set('spread', queryData.query.spid);
|
||||
@@ -70,6 +84,24 @@
|
||||
// #endif
|
||||
},
|
||||
onLaunch(option) {
|
||||
/* 冷启动入口判定(2026-09-22):**只有「落在首页/引导页且没有显式落地 url」才允许
|
||||
首页壳恢复上次应用**。否则(扫码进容器页 / 分享落地 / 朋友圈 / 商品页…)用户是冲着
|
||||
某个页面来的;若此时在小程序内点左上角「小房子」回首页,恢复逻辑又会把他送回刚才那个
|
||||
应用 —— 形成「点了小房子却回不了首页」的循环(用户实测:扫码→小房子→被送回应用→
|
||||
多了个返回箭头→再点返回才到家)。
|
||||
⚠️ 只在 onLaunch 置位(热启动/切 tab 不置),首页壳 onLoad 时一次性消费。
|
||||
⚠️ 用 path 判据而不是时间窗:引导页可能停很久(看开屏广告),时间窗会误判。 */
|
||||
try {
|
||||
const _opt = option || {};
|
||||
const _path = _opt.path || '';
|
||||
const _q = _opt.query || {};
|
||||
const _plainHome = (!_path || _path === 'pages/index/index' || _path === 'pages/guide/index') && !_q.url;
|
||||
this.globalData.restoreArmed = _plainHome;
|
||||
console.log('[enter-app] App.onLaunch(冷启动)path=' + _path +
|
||||
' query=' + JSON.stringify(_q) +
|
||||
' scene=' + (_opt.scene || '') +
|
||||
' → restoreArmed=' + _plainHome + '(false=显式落地,首页壳不会恢复上次应用)');
|
||||
} catch (e) {}
|
||||
uni.hideTabBar()
|
||||
//#ifdef APP
|
||||
plus.screen.lockOrientation("portrait-primary");
|
||||
|
||||
@@ -9,34 +9,54 @@
|
||||
|
||||
```
|
||||
[box_app 管理后台]
|
||||
新建/编辑应用 → 上传"分享图片"
|
||||
新建/编辑应用 → 上传"分享图片" + 填"分享标题"
|
||||
→ 存入 App.shareImageUrl = https://mp.yizhezhe.com/upload/picture/xxx.png
|
||||
→ 存入 App.shareTitle(留空 = 用应用名 App.name)
|
||||
|
||||
[box_app API]
|
||||
GET /api/mp/apps?identifier=xxx&isPublished=1 (单个应用,太阳码场景)
|
||||
GET /api/mp/public/share-images (分享图反查表:所有配了图的应用,
|
||||
**不过滤上架状态**,2026-09-22 起)
|
||||
→ 返回 { url, shareImageUrl } 精简结构
|
||||
GET /api/mp/public/share-images (分享卡片反查表:所有有 url 的应用,
|
||||
**不过滤上架状态**,2026-09-22 起;
|
||||
2026-10-13 起每行多回 shareTitle)
|
||||
→ 返回 { url, shareImageUrl, shareTitle } 精简结构
|
||||
(shareImageUrl 为空串 = 没配图;shareTitle 是服务端算好的生效值,
|
||||
后台留空已回退成应用名 —— 客户端不再实现回退链)
|
||||
|
||||
[小程序宿主页(两个,见第三节)]
|
||||
pages/annex/web_view/index.vue 容器页:扫码 / JumpPath / 分享落地
|
||||
pages/index/index.vue 首页壳:兜爸心选应用宫格 location.href 进入的 H5
|
||||
① 获取 shareImageUrl(网络图)→ 内存 + 本地缓存(mixins/shareCard.js)
|
||||
② 用户点 ··· 转发给好友 → 卡片图 = shareImageUrl(网络图,微信支持)
|
||||
① 获取 shareImageUrl(网络图)+ shareTitle → 内存 + 本地缓存(mixins/shareCard.js)
|
||||
② 用户点 ··· 转发给好友 → 卡片图 = shareImageUrl(网络图,微信支持);标题 = shareTitle
|
||||
③ 用户点 ··· 分享到朋友圈 → getImageInfo 转本地路径 → 卡片图 = 本地路径
|
||||
```
|
||||
|
||||
### 分享标题的优先级(2026-10-13)
|
||||
|
||||
| 优先级 | 来源 | 落点 |
|
||||
| --- | --- | --- |
|
||||
| ① | H5 上报(`location`/`share`/`setTitle` 消息的 `title`) | `webviewHost.handleHostMessage` → `shareTitle` + `shareTitleSource='h5'` |
|
||||
| ② | 后台该应用「分享标题」= `App.shareTitle` | `shareCard.resolveShareCard` → `_matchAppShareTitle` |
|
||||
| ③ | 应用名 `App.name`(后台留空时的服务端回退) | 同上(服务端已回退,客户端拿到的就是生效标题) |
|
||||
| ④ | 首页壳 `'兜爸心选 · 孕育陪伴'` / 容器页 `'分享'` | 两个宿主页的 `onShareAppMessage` 兜底 |
|
||||
|
||||
⚠️ 三条纪律(与分享图同款,别拆开改):
|
||||
1. **`shareTitleSource==='h5'` 不被后台标题覆盖**,否则 H5 上报的页级标题会被应用级标题吃掉;
|
||||
2. **换应用要清标题**(`syncShareCardTarget` → `_clearShareCard`),否则 B 应用沿用 A 的标题;
|
||||
3. 反查数据的本地缓存 key 是 `share_apps_index_v3`(v2 的 items 没有 `shareTitle`)——
|
||||
**加字段必须 bump key**,否则老客户端要等 1h TTL 才生效。
|
||||
|
||||
## 二、box_app 侧配置(已实现)
|
||||
|
||||
### 后端(backend/app/models/app.py + services/app_serv.py)
|
||||
- `App` 模型新增字段:`shareImageUrl: str | None`(微信分享卡片图,公网 https URL)
|
||||
- `AppCreate` / `AppUpdate` / `AppOut` 均含 `shareImageUrl`,查询/写入透传(find / find_all / find_one / create / update 共 5 处)
|
||||
- `App` 模型字段:`shareImageUrl: str | None`(微信分享卡片图,公网 https URL)、
|
||||
`shareTitle: str | None`(分享卡片标题,**留空 = 用应用名**,2026-10-13 加)
|
||||
- `AppCreate` / `AppUpdate` / `AppOut` 均含两字段,查询/写入透传(find / find_all / find_one / create / update 共 5 处)
|
||||
- 上传接口:`POST /api/upload`(返回公网 URL)
|
||||
|
||||
### 管理后台前端(frontend/src/pages/ProductManagement.jsx)
|
||||
- 新建/编辑应用表单新增 **"分享图片 (Share Image)"** 上传项
|
||||
- 新建/编辑应用表单:**"分享图片 (Share Image)"** 上传项 + **"分享标题 (Share Title)"** 输入项(maxLength 40)
|
||||
- 支持上传 `/api/upload`、编辑回填、单图(maxCount=1)
|
||||
- extra 提示:建议 5:4 比例,公网可访问
|
||||
- 分享图 extra 提示:建议 5:4 比例,公网可访问;分享标题 extra 提示:留空 = 用应用名称
|
||||
|
||||
## 三、小程序端获取逻辑(两个宿主页共用 `mixins/shareCard.js`)
|
||||
|
||||
@@ -78,10 +98,12 @@
|
||||
|
||||
页面加载时执行,用户点转发时图已就绪。`_shareFetched` 标志保证页面内只做一次异步兜底。
|
||||
|
||||
### ③ H5 postMessage 覆盖(`handleMessage`)
|
||||
### ③ H5 postMessage 覆盖(`webviewHost.handleHostMessage`)
|
||||
|
||||
```
|
||||
H5 bridge.share({ title, imageUrl }) → postMessage → handleMessage 'share'
|
||||
H5 wxAuth.reportLocation({ title, imageUrl }) / bridge.share({ title, imageUrl })
|
||||
→ postMessage → handleHostMessage 'location' / 'share'
|
||||
→ if (data.title) { this.shareTitle = data.title; this.shareTitleSource = 'h5'; } // 有值才覆盖
|
||||
→ if (data.imageUrl) this.shareImageUrl = data.imageUrl // 有值才覆盖
|
||||
→ 为空则保留 box_app 预取的分享图片
|
||||
```
|
||||
@@ -95,11 +117,11 @@ H5 bridge.share({ title, imageUrl }) → postMessage → handleMessage 'share'
|
||||
→ 卡片图为空 → 微信回退默认截图。
|
||||
|
||||
```
|
||||
onLoad → _loadShareApps() // 预热全量应用表:内存 shareApps + 本地缓存 share_apps_index(1h)
|
||||
onLoad → _loadShareApps() // 预热反查表:内存 shareApps + 本地缓存 share_apps_index_v3(1h)
|
||||
点 ··· → onShareAppMessage(options)
|
||||
→ target = options.webViewUrl || this.shareHref || this.webUrl // 当前 web-view 真实地址
|
||||
→ this.resolveShareCard(target) // 见下:先做应用切换检测,再按当前地址反查
|
||||
→ imageUrl = this.shareImageUrl || ''
|
||||
→ this.resolveShareCard(target) // 见下:先做应用切换检测,再按当前地址反查图 + 标题
|
||||
→ imageUrl = this.shareImageUrl || '' ;title = this.shareTitle || info.title || '兜爸心选 · 孕育陪伴'
|
||||
```
|
||||
|
||||
`_lookupShareImage` 是**同步**的:先内存应用表,再单应用缓存。因此首次转发就能出图,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name" : "DoubaShop",
|
||||
"name" : "兜爸心选",
|
||||
"appid" : "__UNI__52BA861",
|
||||
"description" : "兜爸商城",
|
||||
"versionName" : "3.5.1",
|
||||
"versionCode" : 351,
|
||||
"description" : "兜爸心选",
|
||||
"versionName" : "0.3.8",
|
||||
"versionCode" : 38,
|
||||
"transformPx" : false,
|
||||
"sassImplementationName" : "node-sass",
|
||||
/* 5+App特有相关 */
|
||||
|
||||
@@ -31,8 +31,9 @@ const BOX_API_BASE = 'https://mp.yizhezhe.com';
|
||||
// 与「宫格展示」用的 /api/mp/apps?isPublished=1 刻意解耦:上架状态只决定宫格显隐,
|
||||
// 不决定转发图 —— 反查只看 URL 前缀(接口只回 url+shareImageUrl,长前缀优先)。
|
||||
const APPS_API = BOX_API_BASE + '/api/mp/public/share-images';
|
||||
const APPS_CACHE_KEY = 'share_apps_index_v2'; // 反查表本地缓存 { t, items }
|
||||
// (v2:数据源从上架表换成含下架的反查表,bump key 让旧缓存立即失效)
|
||||
const APPS_CACHE_KEY = 'share_apps_index_v3'; // 反查表本地缓存 { t, items }
|
||||
// (v2:数据源从上架表换成含下架的反查表;v3:每行新增 shareTitle)
|
||||
// bump key 让旧缓存立即失效,别等 1h 才生效
|
||||
const APP_IMG_CACHE_PREFIX = 'web_view_share_img:'; // 单个应用分享图缓存(按 URL 基){ t, url }
|
||||
const APPS_CACHE_TTL = 60 * 60 * 1000; // 应用表缓存 1 小时
|
||||
const APP_IMG_CACHE_TTL = 24 * 60 * 60 * 1000; // 单应用分享图缓存 24 小时(后台改图最迟一天生效)
|
||||
@@ -41,17 +42,20 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
// ---------- 分享(H5 无法自己拉起分享面板,只能由承载 web-view 的页面转发) ----------
|
||||
shareTitle: '', // H5 postMessage(action:'share') 自定义标题
|
||||
shareTitle: '', // 分享标题(H5 postMessage 上报的优先,其次 box_app 反查)
|
||||
shareTitleSource: '', // 当前标题来源:'box'(box_app 反查)| 'h5'(H5 postMessage 覆盖)
|
||||
shareSummary: '', // 分享摘要(部分场景用)
|
||||
shareHref: '', // H5 指定的分享链接(兜底;深链优先 webViewUrl)
|
||||
shareImageUrl: '', // 好友转发卡片图(网络 https)
|
||||
shareTimelineImage: '', // 朋友圈卡片图(必须本地路径,见 _prepareTimelineImage)
|
||||
shareImageSource: '', // 当前分享图来源:'box'(box_app 反查)| 'h5'(H5 postMessage 覆盖)
|
||||
shareImageBase: '', // 当前分享图所属的 H5 URL 基(切换应用时用它丢弃上一张)
|
||||
shareApps: [], // box_app 应用表(url → shareImageUrl),供同步匹配
|
||||
shareImageBase: '', // 当前分享数据所属的 H5 URL 基(切换应用时用它丢弃上一应用的图/标题)
|
||||
shareApps: [], // box_app 反查表(url → shareImageUrl / shareTitle),供同步匹配
|
||||
shareAppsLoaded: false, // 应用表是否已就绪
|
||||
shareAppsLoading: false,// 应用表请求中(防并发重入)
|
||||
shareAppsLoading: false,// 反查表请求中(防并发重入)
|
||||
_shareAppsCbs: [], // 请求中的回调队列(请求结束统一回调)
|
||||
_shareFetched: false, // 本页是否已做过一次异步兜底反查
|
||||
_shareTargetUrl: '', // 最近一次要分享的 H5 地址(反查表就绪后按它补解析)
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
@@ -66,6 +70,7 @@ export default {
|
||||
this.shareTimelineImage = '';
|
||||
this.shareImageSource = '';
|
||||
this.shareTitle = '';
|
||||
this.shareTitleSource = '';
|
||||
this.shareSummary = '';
|
||||
this.shareHref = '';
|
||||
},
|
||||
@@ -88,14 +93,18 @@ export default {
|
||||
},
|
||||
|
||||
/**
|
||||
* 分享前的统一解析(两个宿主页的 onShareAppMessage 都调它):
|
||||
* 分享前的统一解析(两个宿主页的 onShareAppMessage / onShareTimeline 都调它):
|
||||
* ① 应用切换检测 → 换应用先清空;
|
||||
* ② 每次都按"当前 H5 地址"重新反查 box_app 的 shareImageUrl
|
||||
* (不能因为 shareImageUrl 已有值就跳过 —— 那可能是上一个应用的图);
|
||||
* ③ 当前图若来自本应用 H5 的 postMessage(source='h5'),保留不覆盖。
|
||||
* ② 每次都按"当前 H5 地址"重新反查 box_app 的分享图 + 分享标题
|
||||
* (不能因为已有值就跳过 —— 那可能是上一个应用的);
|
||||
* ③ H5 自己上报过标题/图(source='h5')的,保留不覆盖。
|
||||
*/
|
||||
resolveShareCard(targetUrl) {
|
||||
// 进入本次解析时的归属基:换应用时 syncShareCardTarget 会清掉标题,
|
||||
// 用它区分「同一应用的标题」与「上一个应用遗留的标题」
|
||||
const baseAtEntry = this.shareImageBase;
|
||||
this.syncShareCardTarget(targetUrl);
|
||||
// ---- 分享图 ----
|
||||
const fromBox = this._lookupShareImage(targetUrl);
|
||||
if (fromBox) {
|
||||
if (this.shareImageSource !== 'h5') this._applyShareImage(fromBox, 'box', targetUrl);
|
||||
@@ -105,23 +114,65 @@ export default {
|
||||
this.shareImageSource = '';
|
||||
this.shareTimelineImage = '';
|
||||
}
|
||||
// ---- 分享标题(box_app 反查,服务端已把「后台分享标题为空」回退成应用名) ----
|
||||
const boxTitle = this._matchAppShareTitle(targetUrl);
|
||||
if (boxTitle) {
|
||||
if (this.shareTitleSource !== 'h5') {
|
||||
this.shareTitle = boxTitle;
|
||||
this.shareTitleSource = 'box';
|
||||
}
|
||||
} else if (this.shareTitleSource === 'box' && this.shareImageBase && this.shareImageBase !== baseAtEntry) {
|
||||
// 换到了反查表里查不到的应用,而现有标题是上一个应用反查来的 → 丢弃。
|
||||
// ⚠️ 同一地址不清:太阳码场景的标题来自 /api/mp/apps(不在反查表里也能有标题)。
|
||||
this.shareTitle = '';
|
||||
this.shareTitleSource = '';
|
||||
}
|
||||
if (!this.shareAppsLoaded) this._loadShareApps();
|
||||
},
|
||||
|
||||
/* 在内存应用表里按 URL 前缀匹配 shareImageUrl(同步,分享那一刻就能出图) */
|
||||
_matchAppShareImage(targetUrl) {
|
||||
const app = this._matchShareApp(targetUrl);
|
||||
return (app && app.shareImageUrl) || '';
|
||||
},
|
||||
|
||||
/* 在内存应用表里按 URL 前缀匹配分享标题(同步;服务端已回退应用名,空串表示没查到) */
|
||||
_matchAppShareTitle(targetUrl) {
|
||||
const app = this._matchShareApp(targetUrl);
|
||||
return (app && app.shareTitle) || '';
|
||||
},
|
||||
|
||||
/**
|
||||
* 用「已经拿到的应用对象」直接设标题(太阳码 scene 路径:/api/mp/apps 一次就带回
|
||||
* shareTitle / name,不必再等反查表)。
|
||||
* 后台没配 shareTitle 时回退应用名称 —— 与反查表服务端同口径。
|
||||
* @param app 应用对象(含 shareTitle / name / url)
|
||||
*/
|
||||
_applyShareTitleFromApp(app) {
|
||||
if (!app) return;
|
||||
const t = app.shareTitle || app.name || '';
|
||||
if (!t) return;
|
||||
this.shareTitle = t;
|
||||
this.shareTitleSource = 'box';
|
||||
},
|
||||
|
||||
/**
|
||||
* 反查表里按 URL 前缀找当前应用(长前缀优先 —— 服务端已按 url 长度倒序下发,
|
||||
* 防「/a 是 /a/b 的前缀」时张冠李戴)。图与标题共用同一条记录,避免两次匹配漂移。
|
||||
*/
|
||||
_matchShareApp(targetUrl) {
|
||||
const base = this._shareUrlBase(targetUrl);
|
||||
if (!base) return '';
|
||||
if (!base) return null;
|
||||
const apps = this.shareApps || [];
|
||||
for (let i = 0; i < apps.length; i++) {
|
||||
const app = apps[i];
|
||||
if (!app || !app.url || !app.shareImageUrl) continue;
|
||||
if (!app || !app.url) continue;
|
||||
const appBase = this._shareUrlBase(app.url);
|
||||
if (!appBase) continue;
|
||||
// 如 https://mp.yizhezhe.com/taidong == https://mp.yizhezhe.com/taidong/
|
||||
if (base === appBase || base.indexOf(appBase + '/') === 0) return app.shareImageUrl;
|
||||
if (base === appBase || base.indexOf(appBase + '/') === 0) return app;
|
||||
}
|
||||
return '';
|
||||
return null;
|
||||
},
|
||||
|
||||
/* 同步查图:优先内存应用表(onLoad 预热,1h 缓存),再退单应用缓存(24h TTL) */
|
||||
@@ -130,7 +181,11 @@ export default {
|
||||
if (!base) return '';
|
||||
// 1) 内存应用表:后台改图后最迟 1h 生效
|
||||
const fromList = this._matchAppShareImage(targetUrl);
|
||||
if (fromList) return fromList;
|
||||
if (fromList) {
|
||||
// 顺手写单应用缓存:冷启动时表还没回来也能立刻出图
|
||||
try { uni.setStorageSync(APP_IMG_CACHE_PREFIX + base, { t: Date.now(), url: fromList }); } catch (e) {}
|
||||
return fromList;
|
||||
}
|
||||
// 2) 单应用缓存兜底(应用表请求失败 / 冷启动还没回来时用)
|
||||
try {
|
||||
const cached = uni.getStorageSync(APP_IMG_CACHE_PREFIX + base);
|
||||
@@ -144,7 +199,7 @@ export default {
|
||||
return '';
|
||||
},
|
||||
|
||||
/* 拉全量已上架应用(带 1h 本地缓存);cb 可选,拉完(或命中缓存)后回调 */
|
||||
/* 拉 box_app 分享卡片反查表(url → 分享图/标题,带 1h 本地缓存);cb 可选,拉完(或命中缓存)后回调 */
|
||||
_loadShareApps(cb) {
|
||||
// 1) 本地缓存命中:冷启动也能同步出图
|
||||
try {
|
||||
@@ -157,9 +212,19 @@ export default {
|
||||
return;
|
||||
}
|
||||
} catch (e) {}
|
||||
// 2) 请求中:直接返回,避免并发重复拉(漏掉的 cb 由调用方自身的兜底路径覆盖)
|
||||
if (this.shareAppsLoading) return;
|
||||
// 2) 请求中:把回调排队,请求结束统一回调(避免并发重复拉,也别丢调用方)
|
||||
if (this.shareAppsLoading) {
|
||||
if (cb) this._shareAppsCbs.push(cb);
|
||||
return;
|
||||
}
|
||||
this.shareAppsLoading = true;
|
||||
if (cb) this._shareAppsCbs.push(cb);
|
||||
const finish = () => {
|
||||
this.shareAppsLoading = false;
|
||||
const cbs = this._shareAppsCbs || [];
|
||||
this._shareAppsCbs = [];
|
||||
cbs.forEach(fn => { try { fn(); } catch (e) {} });
|
||||
};
|
||||
uni.request({
|
||||
url: APPS_API,
|
||||
method: 'GET',
|
||||
@@ -170,35 +235,42 @@ export default {
|
||||
this.shareAppsLoaded = true;
|
||||
try { uni.setStorageSync(APPS_CACHE_KEY, { t: Date.now(), items: items }); } catch (e) {}
|
||||
}
|
||||
if (cb) cb();
|
||||
finish();
|
||||
},
|
||||
fail: (err) => {
|
||||
// 常见原因:mp.yizhezhe.com 未加入小程序后台 request 合法域名白名单
|
||||
console.warn('[shareCard] 应用表拉取失败(请检查 mp.yizhezhe.com 是否在 request 合法域名):', err);
|
||||
if (cb) cb();
|
||||
},
|
||||
complete: () => { this.shareAppsLoading = false; }
|
||||
console.warn('[shareCard] 反查表拉取失败(请检查 mp.yizhezhe.com 是否在 request 合法域名):', err);
|
||||
finish();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/* 异步兜底反查(容器页 onLoad 用):同步没命中就拉应用表再匹配一次 */
|
||||
/* 异步兜底反查(容器页 onLoad 用):同步没命中就拉反查表再匹配一次 */
|
||||
_fetchShareImage(targetUrl) {
|
||||
if (!targetUrl) return;
|
||||
this.syncShareCardTarget(targetUrl); // 建立/校正"当前图属于哪个应用"
|
||||
this._shareTargetUrl = targetUrl; // 拉表回来后按它补解析分享图/标题
|
||||
this.syncShareCardTarget(targetUrl); // 建立/校正"当前数据属于哪个应用"
|
||||
// 反查表还没预热就先拉(标题/图都靠它;拉回来自己会补解析)
|
||||
if (!this.shareAppsLoaded) this._loadShareApps(() => this._syncShareCard(targetUrl));
|
||||
if (this.shareImageUrl) return; // H5 postMessage 已给图,不覆盖
|
||||
if (this._shareFetched) return; // 本页只兜底一次
|
||||
this._shareFetched = true;
|
||||
const hit = this._lookupShareImage(targetUrl);
|
||||
if (hit) { this._applyShareImage(hit, 'box', targetUrl); return; }
|
||||
// 表已就绪但没命中(或上面那次请求刚好失败)→ 再兜底一次,覆盖冷启动时序
|
||||
this._loadShareApps(() => {
|
||||
const matched = this._matchAppShareImage(targetUrl);
|
||||
if (!matched) return;
|
||||
try { uni.setStorageSync(APP_IMG_CACHE_PREFIX + this._shareUrlBase(targetUrl), { t: Date.now(), url: matched }); } catch (e) {}
|
||||
this._applyShareImage(matched, 'box', targetUrl);
|
||||
console.log('[shareCard] 分享图片(兜底): ' + matched);
|
||||
this._syncShareCard(targetUrl);
|
||||
if (this.shareImageUrl) console.log('[shareCard] 分享图片(兜底): ' + this.shareImageUrl);
|
||||
});
|
||||
},
|
||||
|
||||
/* 反查表就绪后补解析(onLoad 阶段表还没回来时用;已就绪则走 resolveShareCard) */
|
||||
_syncShareCard(targetUrl) {
|
||||
const url = targetUrl || this._shareTargetUrl;
|
||||
if (!url) return;
|
||||
this.resolveShareCard(url);
|
||||
},
|
||||
|
||||
/* 统一设置分享图:好友转发用网络图;朋友圈另转本地图(见下)
|
||||
@param source 'box'(box_app 反查,默认)| 'h5'(H5 postMessage 覆盖)
|
||||
@param forUrl 该图所属的 H5 地址(用于切换应用时判断是否过期) */
|
||||
|
||||
@@ -217,18 +217,24 @@ export default {
|
||||
// onShareTimeline(朋友圈)拿不到 webViewUrl 时唯一的同步手段,
|
||||
// 也修掉首页壳换应用后沿用上一个应用分享图的问题。
|
||||
const href = m.href || '';
|
||||
// 诊断日志(过滤关键字 enter-app):这条**不是实时**的 ——
|
||||
// 微信把 web-view 的 postMessage 攒到「后退 / 组件销毁 / 分享 / 复制链接」才派发,
|
||||
// 所以它出现的时间点通常晚于用户实际换应用;宫格换应用后宿主就是靠它才知道当前在哪。
|
||||
console.log('[enter-app] 宿主收到 H5 location 上报(延迟派发)href=' + href);
|
||||
// ⚠️ 顺序:先落 H5 上报的标题,再 resolveShareCard ——
|
||||
// 后者只在标题来源不是 'h5' 时才用 box_app 反查标题覆盖,顺序反了会白上报。
|
||||
if (m.title) { this.shareTitle = m.title; this.shareSummary = m.summary || ''; this.shareTitleSource = 'h5'; }
|
||||
if (href) {
|
||||
this.shareHref = href;
|
||||
if (typeof this.resolveShareCard === 'function') this.resolveShareCard(href);
|
||||
}
|
||||
if (m.title) { this.shareTitle = m.title; this.shareSummary = m.summary || ''; }
|
||||
// H5 给了图就以 H5 为准(仅当前应用;换应用会被 syncShareCardTarget 清掉)
|
||||
if (m.imageUrl) this._applyShareImage(m.imageUrl, 'h5', href || this.shareHref);
|
||||
break;
|
||||
}
|
||||
case 'share':
|
||||
// title / imageUrl / href 各自独立处理,单个字段缺失不影响其它
|
||||
if (m.title) { this.shareTitle = m.title; this.shareSummary = m.summary || ''; }
|
||||
if (m.title) { this.shareTitle = m.title; this.shareSummary = m.summary || ''; this.shareTitleSource = 'h5'; }
|
||||
if (m.href) this.shareHref = m.href;
|
||||
if (m.imageUrl) this._applyShareImage(m.imageUrl, 'h5', m.href);
|
||||
// 小程序无法用代码拉起分享面板,只能引导用户点右上角
|
||||
@@ -256,6 +262,7 @@ export default {
|
||||
case 'setTitle':
|
||||
if (m.title) {
|
||||
this.shareTitle = m.title;
|
||||
this.shareTitleSource = 'h5'; // H5 明确设的标题 → 后续反查不覆盖
|
||||
uni.setNavigationBarTitle({ title: m.title });
|
||||
}
|
||||
break;
|
||||
@@ -318,7 +325,7 @@ export default {
|
||||
uni.removeStorageSync('__AUTH_RESULT__');
|
||||
/* backUrl 优先:H5 发起授权时经 navigateTo query 带上的当前地址
|
||||
(确定性通道,不依赖 postMessage 派发时机);旧版 H5 没有 backUrl
|
||||
时回退到入参基址(shareHref || webUrl || MAMA_HOME)。 */
|
||||
时回退到入参基址(shareHref || webUrl || 生效首页地址)。 */
|
||||
const back = (res.backUrl && String(res.backUrl).split('#')[0]) || '';
|
||||
const base = back || baseUrl;
|
||||
if (!base) return '';
|
||||
@@ -336,7 +343,7 @@ export default {
|
||||
const openid = this._hostOpenid();
|
||||
if (!openid) return uni.showToast({ title: '未获得 openid,请先登录', icon: 'none' });
|
||||
/* 面板回传基址:requestProfile 消息带的 href 优先(确定性),没有才用调用处
|
||||
传入的 baseUrl(shareHref || webUrl || MAMA_HOME)。清掉已用值,
|
||||
传入的 baseUrl(shareHref || webUrl || 生效首页地址)。清掉已用值,
|
||||
避免下次面板在没有新消息 href 时复用旧地址。 */
|
||||
const base = (this._panelBaseUrl && String(this._panelBaseUrl).split('#')[0]) || baseUrl;
|
||||
this._panelBaseUrl = '';
|
||||
|
||||
227
pages.json
227
pages.json
@@ -19,6 +19,19 @@
|
||||
"navigationBarTitleText": "",
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTextStyle": "black",
|
||||
// 首页临时替换为兜爸心选 H5(web-view 壳),下拉刷新无意义先关掉;
|
||||
// 恢复原装修首页(index.vue.origin-bak)时改回 true
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
// 商城首页 = 原装 CRMEB 装修首页恢复成第 5 个 tabBar 页(见 mall.vue 文件头注释);
|
||||
// H5「兜爸心选」tab 的 goMall() switchTab 落点就是它
|
||||
"path": "pages/index/mall",
|
||||
"style": {
|
||||
"navigationBarTitleText": "商城",
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTextStyle": "black",
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
},
|
||||
@@ -68,6 +81,21 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
// ⚠️ 2026-09-20:本次提交审核前**停用** UnionID 桥接调试页的注册(用户要求)——
|
||||
// 它会跟着审核包一起提交,审核员可能点进一个「UnionID 调试」页;
|
||||
// ⚠️ 恢复时注意逗号:它是 pages 数组的**最后一项**,停用时上一项的结尾逗号也一并去掉了
|
||||
// (否则 pages.json 会出现非法尾逗号)。恢复 = 去掉下面 8 行的 // + 给上一项 `}` 补回 `,`。
|
||||
// 原注释(保留备查):UnionID 桥接调试页:保留注册(直达路径/编译模式仍可用),
|
||||
// 但不再作为冷启动默认页 —— 之前排在 pages[0],导致每次重启都进调试页。
|
||||
// 启动流程恢复为 guide(启动页)→ switchTab 首页。
|
||||
// {
|
||||
// "path": "pages/debug_union/index",
|
||||
// "style": {
|
||||
// "navigationBarTitleText": "UnionID 调试",
|
||||
// "navigationBarBackgroundColor": "#fff",
|
||||
// "navigationBarTextStyle": "black"
|
||||
// }
|
||||
// }
|
||||
],
|
||||
// "plugins": {
|
||||
// "live-player-plugin": {
|
||||
@@ -391,52 +419,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "我的推广"
|
||||
}
|
||||
},
|
||||
{
|
||||
"style": {
|
||||
"navigationBarTitleText": "分销海报",
|
||||
"app-plus": {
|
||||
// #ifdef APP-PLUS
|
||||
"titleNView": {
|
||||
"type": "default"
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"style": {
|
||||
"navigationBarTitleText": "佣金记录",
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarBackgroundColor": "#FFFFFF",
|
||||
"app-plus": {
|
||||
// #ifdef APP-PLUS
|
||||
"titleNView": {
|
||||
"type": "default"
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"style": {
|
||||
"navigationBarTitleText": "收款",
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarBackgroundColor": "#F5F5F5",
|
||||
"app-plus": {
|
||||
// #ifdef APP-PLUS
|
||||
"titleNView": {
|
||||
"type": "default"
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
{
|
||||
"path": "user_distribution_info/index",
|
||||
"style": {
|
||||
@@ -452,37 +438,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"style": {
|
||||
"navigationBarTitleText": "提现",
|
||||
// #ifndef APP-PLUS
|
||||
"navigationBarTextStyle": "white",
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
"navigationBarTextStyle": "black",
|
||||
// #endif
|
||||
"navigationStyle": "custom",
|
||||
"app-plus": {
|
||||
// #ifdef APP-PLUS
|
||||
"titleNView": {
|
||||
"type": "default"
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"style": {
|
||||
"navigationBarTitleText": "提现审核",
|
||||
"app-plus": {
|
||||
// #ifdef APP-PLUS
|
||||
"titleNView": {
|
||||
"type": "default"
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
"path": "user_vip/index",
|
||||
"style": {
|
||||
@@ -627,76 +584,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的团队"
|
||||
// #ifdef MP
|
||||
,
|
||||
"navigationBarBackgroundColor": "#FFFFFF",
|
||||
"navigationBarTextStyle": "black"
|
||||
// #endif
|
||||
,
|
||||
"app-plus": {
|
||||
// #ifdef APP-PLUS
|
||||
"titleNView": {
|
||||
"type": "default"
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"style": {
|
||||
"navigationBarTitleText": "分销订单"
|
||||
|
||||
// #ifdef MP
|
||||
,
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarBackgroundColor": "#FFFFFF"
|
||||
// #endif
|
||||
,
|
||||
"app-plus": {
|
||||
// #ifdef APP-PLUS
|
||||
"titleNView": {
|
||||
"type": "default"
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "推广人排行",
|
||||
"app-plus": {
|
||||
// #ifdef APP-PLUS
|
||||
"titleNView": {
|
||||
"type": "default"
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
// #ifdef APP-PLUS
|
||||
"navigationBarTextStyle": "black",
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
"navigationBarTextStyle": "white",
|
||||
// #endif
|
||||
"navigationBarTitleText": "佣金排行",
|
||||
"app-plus": {
|
||||
// #ifdef APP-PLUS
|
||||
"titleNView": {
|
||||
"type": "default"
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
{
|
||||
"path": "user_return_list/index",
|
||||
"style": {
|
||||
@@ -760,18 +651,6 @@
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
}, {
|
||||
"style": {
|
||||
"navigationBarTitleText": "分销员申请",
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},{
|
||||
"style": {
|
||||
"navigationBarTitleText": "",
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "agent/apply",
|
||||
@@ -857,27 +736,6 @@
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"style" :
|
||||
{
|
||||
"navigationBarTitleText" : "",
|
||||
"navigationStyle": "custom",
|
||||
"app-plus": {
|
||||
// #ifdef APP-PLUS
|
||||
"titleNView": {
|
||||
"type": "default"
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"style" :
|
||||
{
|
||||
"navigationBarTitleText" : "",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -2052,6 +1910,17 @@
|
||||
"selectedIconPath": "static/images/1-002.png",
|
||||
"text": "首页"
|
||||
},
|
||||
{
|
||||
// 商城首页 = 原装装修首页恢复页(pages/index/mall.vue)。
|
||||
// 注册进 tabBar 的目的:① switchTab 可达(DIY 底部导航 goRouter 用 switchTab);
|
||||
// ② 后台 DIY 底部导航禁用时,原生 tabBar 兜底显示(顺序=用户要求 首页/商城/分类/购物车/我的)。
|
||||
// ⚠️ 线上实际显示的底栏是后台「装修→底部导航」DIY 配置(/api/navigation → pageFooter 组件),
|
||||
// 商城项要出现在底栏里必须在后台配置里加,改这里不会让底栏多出一项。
|
||||
"pagePath": "pages/index/mall",
|
||||
"iconPath": "static/images/5-001.png",
|
||||
"selectedIconPath": "static/images/5-002.png",
|
||||
"text": "商城"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/goods_cate/goods_cate",
|
||||
"iconPath": "static/images/2-001.png",
|
||||
|
||||
@@ -1,43 +1,142 @@
|
||||
<template>
|
||||
<view class="auth-page">
|
||||
<!-- 顶部说明 -->
|
||||
<view class="auth-head">
|
||||
<text class="auth-title">授权完善资料</text>
|
||||
<text class="auth-sub">用于在应用里展示你的昵称 / 头像 / 手机号</text>
|
||||
<!-- ============ 可滚动内容区 ============ -->
|
||||
<view class="home">
|
||||
<!-- 未授权:价值主张卡(先说清"有什么好处",再要授权) -->
|
||||
<view v-if="!phoneBound" class="pitch">
|
||||
<view class="pbadge">全部免费 · 无需付费</view>
|
||||
<view class="pttl">{{ pitchTitle }}</view>
|
||||
<view class="pttl pttl2 em">授权后全部免费用</view>
|
||||
<view class="psub">胎动、体重、产检、血糖、待产清单……</view>
|
||||
<view class="psub">从孕期到产后,一个地方全记下来</view>
|
||||
<view class="social">
|
||||
<view class="dots">
|
||||
<image v-for="(d, i) in SOCIAL_AVATARS" :key="i" class="dot" :src="d" mode="aspectFit" />
|
||||
</view>
|
||||
<text class="social-txt">已有 {{ SOCIAL_COUNT }} 位妈妈在用</text>
|
||||
</view>
|
||||
<button class="cta" @tap="openPhoneSheet">授权手机号,立即免费体验</button>
|
||||
<view class="ctahint">微信授权 · 10 秒完成 · 仅用于创建你的孕育档案</view>
|
||||
<view class="trust">
|
||||
<text class="trust-i"><text class="trust-tick">✓</text>全部免费</text>
|
||||
<text class="trust-i"><text class="trust-tick">✓</text>无需下载 App</text>
|
||||
<text class="trust-i"><text class="trust-tick">✓</text>换手机不丢</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 授权表单卡片 -->
|
||||
<view class="auth-card">
|
||||
<!-- 头像:圆形选择区,点击选择;图片填满圆并裁剪,文字放圆外 -->
|
||||
<view class="auth-avatar-wrap">
|
||||
<button class="auth-avatar" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
|
||||
<image v-if="authAvatar" class="auth-ava" :src="authAvatar" mode="aspectFill" />
|
||||
<view v-else class="auth-ava-plus">+</view>
|
||||
<!-- 已授权:收起成一条状态,不再占地方 -->
|
||||
<view v-else class="hero">
|
||||
<view class="ava">
|
||||
<image v-if="heroAvatar" class="ava-img" :src="heroAvatar" mode="aspectFill" />
|
||||
<text v-else class="ava-emoji">📱</text>
|
||||
</view>
|
||||
<view class="who">
|
||||
<view class="nm">{{ heroName }}</view>
|
||||
<view class="st">{{ heroSub }}</view>
|
||||
</view>
|
||||
<view class="ok">已授权</view>
|
||||
</view>
|
||||
|
||||
<template v-if="apps.length">
|
||||
<view class="secthead">
|
||||
<text class="secttl">孕期工具</text>
|
||||
<text class="sectmore">共 {{ apps.length }} 个 ›</text>
|
||||
</view>
|
||||
<view v-if="!phoneBound" class="freebar">
|
||||
<text class="freepill">全部免费</text>
|
||||
<text class="freetxt">{{ apps.length }} 款工具当前未解锁,授权手机号后立即可用</text>
|
||||
</view>
|
||||
<!-- 工具宫格:数据来自 box_app 后台在架应用,**仅展示不可点击**;
|
||||
未授权时右上角显示小锁(授权后解锁,仍不响应点击)。 -->
|
||||
<view class="grid" :class="{ locked: !phoneBound }">
|
||||
<view v-for="a in apps" :key="a.id" class="tile">
|
||||
<view class="ic">
|
||||
<image v-if="a.logo" class="tile-logo" :src="a.logo" mode="aspectFit" />
|
||||
<text v-else class="tile-fb">{{ (a.name || '·').slice(0, 1) }}</text>
|
||||
</view>
|
||||
<text class="tn">{{ a.name }}</text>
|
||||
<view class="lock">
|
||||
<image class="lock-ic" :src="LOCK_ICON" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
|
||||
<!-- 常驻底部行动条:用户滚到哪一屏都能点 -->
|
||||
<view v-if="!phoneBound" class="stickycta">
|
||||
<button class="cta cta2" @tap="openPhoneSheet">{{ stickyText }}</button>
|
||||
</view>
|
||||
|
||||
<!-- 遮罩(演示里点遮罩不关闭,必须走两个按钮之一) -->
|
||||
<view class="mask" :class="{ show: !!sheet }" @tap="noop"></view>
|
||||
|
||||
<!-- ① 手机号半弹窗:「是」按钮本身必须是 open-type="getPhoneNumber",
|
||||
手机号没有可编程调起的 API;带右上角「原生」角标的才调微信接口。 -->
|
||||
<view class="sheet" :class="{ show: sheet === 'phone' }">
|
||||
<view class="grab"></view>
|
||||
<view class="sheet-h3">是否授权手机号进行体验?</view>
|
||||
<view class="sheet-p">用于创建你的孕育档案,让胎动、体重、产检记录在换设备后也能找回。</view>
|
||||
<view class="btnrow">
|
||||
<button class="bsec" @tap="denyPhone">否</button>
|
||||
<button class="bmain bwx" open-type="getPhoneNumber" @getphonenumber="onGetPhone">
|
||||
是,授权手机号
|
||||
</button>
|
||||
<text class="auth-ava-label">{{ authAvatar ? '点击更换头像' : '点击选择头像' }}</text>
|
||||
</view>
|
||||
|
||||
<view class="auth-field">
|
||||
<input class="auth-input" type="nickname" v-model="authNickname" placeholder="输入昵称" maxlength="16" placeholder-class="auth-ph" />
|
||||
</view>
|
||||
|
||||
<button class="auth-btn" open-type="getPhoneNumber" @getphonenumber="onGetPhone">{{ authPhone ? '✅ 已授权手机号' : '获取手机号' }}</button>
|
||||
|
||||
<view class="auth-actions">
|
||||
<button class="auth-cancel" @tap="cancel">取消</button>
|
||||
<button class="auth-ok" @tap="submitAuth">保存并返回</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text class="auth-tip">微信授权仅用于在应用内展示你的资料,不会泄露给第三方。</text>
|
||||
<!-- ④-A 老用户(库里已有昵称/头像):是否复用。普通按钮 —— 复用走库里资料,不调微信接口 -->
|
||||
<view class="sheet" :class="{ show: sheet === 'reuse' }">
|
||||
<view class="grab"></view>
|
||||
<view class="sheet-h3">是否复用你的微信昵称和头像?</view>
|
||||
<view class="avarow">
|
||||
<view class="avarow-big">
|
||||
<image v-if="libAvatar" class="avarow-img" :src="libAvatar" mode="aspectFill" />
|
||||
<text v-else class="avarow-emoji">🐣</text>
|
||||
</view>
|
||||
<view class="avarow-txt">
|
||||
<view class="t1">{{ libNickname || '微信用户' }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="btnrow">
|
||||
<button class="bsec" @tap="reuseNo">否,稍后再说</button>
|
||||
<button class="bmain" @tap="reuseYes">是,直接用</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ④-B 库里没有昵称/头像:不再追问、不再给表单,直接收尾返回(2026-09-22 定案) -->
|
||||
<!-- 说明:本页只负责「手机号」这一件事 + 可选复用库里已有资料。
|
||||
昵称/头像不由本页采集(getUserProfile 已废弃,且产品定案不在此追问),
|
||||
它们由小程序其它流程/上游用户中心/商城同步维护,见 AGENTS §4.4。 -->
|
||||
|
||||
<!-- 页内 toast -->
|
||||
<view class="toast" :class="{ show: toast.show }">
|
||||
<text v-if="toast.tick" class="toast-tick">✓</text>
|
||||
<text class="toast-msg">{{ toast.msg }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import store from '@/store';
|
||||
import { HTTP_REQUEST_URL } from '@/config/app';
|
||||
import { LOGIN_STATUS } from '@/config/cache';
|
||||
|
||||
/* 共享授权接口(user_auth_router:全 H5 应用共用,openid 单键,不区分应用) */
|
||||
const AUTH_API = 'https://mp.yizhezhe.com/api/mp/public/auth/profile';
|
||||
/* box_app 公开应用列表:宫格按后台登记的在架应用渲染(样式照 design/授权流程演示) */
|
||||
const APPS_API = 'https://mp.yizhezhe.com/api/mp/apps';
|
||||
|
||||
/* ⚠️ 演示页占位数据:上线前必须核实真实数字,否则涉嫌虚假宣传。
|
||||
三个头像是内联 SVG 示意插画(为保持单文件离线可用),不是真实用户头像。 */
|
||||
const SOCIAL_COUNT = '36.5万+';
|
||||
const SOCIAL_AVATARS = [
|
||||
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCc+PGNpcmNsZSBjeD0nMTInIGN5PScxMicgcj0nMTInIGZpbGw9JyNGN0U0RUEnLz48cGF0aCBkPSdNMi42IDI0Yy43LTQuMyA0LjYtNi45IDkuNC02LjlzOC43IDIuNiA5LjQgNi45eicgZmlsbD0nI0M5N0I5MicvPjxjaXJjbGUgY3g9JzEyJyBjeT0nMTAuNicgcj0nNC4xNScgZmlsbD0nI0YyQzZBQycvPjxlbGxpcHNlIGN4PSc3LjknIGN5PScxMC45JyByeD0nLjk1JyByeT0nMi4zJyBmaWxsPScjM0IyQTI0Jy8+PGVsbGlwc2UgY3g9JzE2LjEnIGN5PScxMC45JyByeD0nLjk1JyByeT0nMi4zJyBmaWxsPScjM0IyQTI0Jy8+PHBhdGggZD0nTTcuOCAxMC42QzcuOCA2LjUgMTYuMiA2LjUgMTYuMiAxMC42Yy0uOS0yLjEtNy41LTIuMS04LjQgMHonIGZpbGw9JyMzQjJBMjQnLz48L3N2Zz4=',
|
||||
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCc+PGNpcmNsZSBjeD0nMTInIGN5PScxMicgcj0nMTInIGZpbGw9JyNFRkVBRkEnLz48cGF0aCBkPSdNMi42IDI0Yy43LTQuMyA0LjYtNi45IDkuNC02LjlzOC43IDIuNiA5LjQgNi45eicgZmlsbD0nIzlCODZDOScvPjxjaXJjbGUgY3g9JzEyJyBjeT0nMTAuNicgcj0nNC4xNScgZmlsbD0nI0Y2RDNCQycvPjxlbGxpcHNlIGN4PSc3LjknIGN5PScxMC45JyByeD0nLjk1JyByeT0nMi4zJyBmaWxsPScjNUEzQjJFJy8+PGVsbGlwc2UgY3g9JzE2LjEnIGN5PScxMC45JyByeD0nLjk1JyByeT0nMi4zJyBmaWxsPScjNUEzQjJFJy8+PHBhdGggZD0nTTcuOCAxMC42QzcuOCA2LjUgMTYuMiA2LjUgMTYuMiAxMC42Yy0uOS0yLjEtNy41LTIuMS04LjQgMHonIGZpbGw9JyM1QTNCMkUnLz48L3N2Zz4=',
|
||||
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCc+PGNpcmNsZSBjeD0nMTInIGN5PScxMicgcj0nMTInIGZpbGw9JyNFNEYyRUMnLz48cGF0aCBkPSdNMi42IDI0Yy43LTQuMyA0LjYtNi45IDkuNC02LjlzOC43IDIuNiA5LjQgNi45eicgZmlsbD0nIzVGQTk4QycvPjxjaXJjbGUgY3g9JzEyJyBjeT0nMTAuNicgcj0nNC4xNScgZmlsbD0nI0VGQzBBMCcvPjxlbGxpcHNlIGN4PSc3LjknIGN5PScxMC45JyByeD0nLjk1JyByeT0nMi4zJyBmaWxsPScjMjQxQzE5Jy8+PGVsbGlwc2UgY3g9JzE2LjEnIGN5PScxMC45JyByeD0nLjk1JyByeT0nMi4zJyBmaWxsPScjMjQxQzE5Jy8+PHBhdGggZD0nTTcuOCAxMC42QzcuOCA2LjUgMTYuMiA2LjUgMTYuMiAxMC42Yy0uOS0yLjEtNy41LTIuMS04LjQgMHonIGZpbGw9JyMyNDFDMTknLz48L3N2Zz4='
|
||||
];
|
||||
/* 宫格右上角小锁(未授权时显示;base64 保证 WXSS/小程序端可直接用) */
|
||||
const LOCK_ICON = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjOEM3QTZGJyBzdHJva2Utd2lkdGg9JzIuMic+PHJlY3QgeD0nNC41JyB5PScxMC41JyB3aWR0aD0nMTUnIGhlaWdodD0nMTAnIHJ4PScyLjUnLz48cGF0aCBkPSdNOCAxMC41VjcuNWE0IDQgMCAwIDEgOCAwdjMnLz48L3N2Zz4=';
|
||||
|
||||
function base64Decode(str) {
|
||||
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
|
||||
var output = '';
|
||||
@@ -90,141 +189,365 @@
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
authNickname: '',
|
||||
authAvatar: '',
|
||||
authPhone: false, // 手机号是否已授权(UI 反馈)
|
||||
_authCode: '',
|
||||
openid: ''
|
||||
openid: '',
|
||||
/* 当前半弹窗:'' | 'phone' | 'reuse' */
|
||||
sheet: '',
|
||||
phoneBound: false, // 手机号是否已落库(= 价值卡收起 / 宫格解锁)
|
||||
phone: '',
|
||||
libNickname: '', // 共享库里已有的昵称(判据的取值来源)
|
||||
libAvatar: '', // 共享库里已有的头像
|
||||
apps: [],
|
||||
busy: false,
|
||||
toast: { show: false, msg: '', tick: false },
|
||||
SOCIAL_COUNT: SOCIAL_COUNT,
|
||||
SOCIAL_AVATARS: SOCIAL_AVATARS,
|
||||
LOCK_ICON: LOCK_ICON
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pitchTitle() {
|
||||
return (this.apps.length ? this.apps.length + ' 款' : '') + '孕育必备工具';
|
||||
},
|
||||
heroName() {
|
||||
return this.libNickname || this.phone || '已授权';
|
||||
},
|
||||
heroSub() {
|
||||
return this.libNickname ? '资料已复用 · 手机号已绑定' : '手机号已绑定 · 未设置昵称';
|
||||
},
|
||||
heroAvatar() {
|
||||
return this.libAvatar;
|
||||
},
|
||||
/* 应用列表拉取失败时不要显示「解锁 0 款工具」 */
|
||||
stickyText() {
|
||||
return this.apps.length ? '立即免费体验 · 解锁 ' + this.apps.length + ' 款工具' : '立即免费体验';
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
this.openid = getOpenid();
|
||||
/* H5 经 navigateTo query 带来的「发起授权的当前地址」(backUrl,已 encode):
|
||||
成功时写进 __AUTH_RESULT__,宿主 onShow 优先按它回传 —— 从哪个 H5 触发的
|
||||
结束后写进 __AUTH_RESULT__,宿主 onShow 优先按它回传 —— 从哪个 H5 触发的
|
||||
授权就回到哪个 H5(宫格 location.href 换应用后,shareHref 可能还是旧应用的)。 */
|
||||
let backUrl = (options && options.backUrl) || '';
|
||||
if (backUrl) {
|
||||
try { backUrl = decodeURIComponent(backUrl); } catch (e) { /* 已是原文 */ }
|
||||
}
|
||||
this._backUrl = backUrl;
|
||||
if (!this.openid) uni.showToast({ title: '未获得 openid,请先登录', icon: 'none' });
|
||||
|
||||
/* 宫格数据:失败不影响授权主流程(旁路能力绝不阻塞) */
|
||||
this.fetchApps();
|
||||
|
||||
if (!this.openid) {
|
||||
this.showToast('未获得 openid,请先登录', false, 1500);
|
||||
setTimeout(() => { uni.navigateBack(); }, 1500);
|
||||
return;
|
||||
}
|
||||
/* 进页即问手机号:用户是从 H5「授权完善资料」进来的,意图已经明确,少一次点击 */
|
||||
this.sheet = 'phone';
|
||||
},
|
||||
onUnload() {
|
||||
if (this._toastTimer) clearTimeout(this._toastTimer);
|
||||
},
|
||||
methods: {
|
||||
onChooseAvatar(e) {
|
||||
const avatarUrl = e.detail.avatarUrl;
|
||||
if (!avatarUrl) return uni.showToast({ title: '未获取到头像', icon: 'none' });
|
||||
/* 上传到 box_app 公开接口(无鉴权):定制授权页设计为匿名可用,
|
||||
不依赖 CRMEB 登录态 —— 用 CRMEB 的 uploadImgs 在退出登录后会 401 弹「请登录」。 */
|
||||
uni.showLoading({ title: '上传头像中…' });
|
||||
uni.uploadFile({
|
||||
url: 'https://mp.yizhezhe.com/api/upload',
|
||||
filePath: avatarUrl,
|
||||
name: 'file',
|
||||
/* ---------- 工具宫格:box_app 在架应用(仅展示,不响应点击) ---------- */
|
||||
fetchApps() {
|
||||
uni.request({
|
||||
url: APPS_API,
|
||||
data: { isPublished: 1, pageSize: 200 },
|
||||
success: (res) => {
|
||||
try {
|
||||
const d = JSON.parse(res.data);
|
||||
if (d.code === 200 && d.result && d.result.url) {
|
||||
this.authAvatar = d.result.url;
|
||||
} else {
|
||||
uni.showToast({ title: '头像上传失败', icon: 'none' });
|
||||
}
|
||||
} catch (err) {
|
||||
uni.showToast({ title: '头像上传失败', icon: 'none' });
|
||||
const d = res.data || {};
|
||||
const items = (d.result && d.result.items) || d.data || [];
|
||||
if (Array.isArray(items)) {
|
||||
this.apps = items.filter((a) => a && a.name);
|
||||
}
|
||||
},
|
||||
fail: () => uni.showToast({ title: '头像上传失败,请检查网络', icon: 'none' }),
|
||||
complete: () => uni.hideLoading()
|
||||
fail: () => {
|
||||
console.warn('[auth] 应用列表拉取失败,宫格留空(不影响授权)');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/* ---------- ① 手机号 ---------- */
|
||||
openPhoneSheet() {
|
||||
if (!this.openid) return this.showToast('未获得 openid,请先登录');
|
||||
this.sheet = 'phone';
|
||||
},
|
||||
denyPhone() {
|
||||
/* 手机号都不授权 = 本次授权放弃:直接回 H5(不写 __AUTH_RESULT__,
|
||||
宿主按现有逻辑重载当前 H5,不会有假成功回流)。 */
|
||||
uni.navigateBack();
|
||||
},
|
||||
onGetPhone(e) {
|
||||
if (e.detail.errMsg === 'getPhoneNumber:ok' && e.detail.code) {
|
||||
this._authCode = e.detail.code;
|
||||
this.authPhone = true;
|
||||
const detail = (e && e.detail) || {};
|
||||
if (detail.errMsg === 'getPhoneNumber:ok' && detail.code) {
|
||||
this.submitPhone(detail.code);
|
||||
} else {
|
||||
uni.showToast({ title: '未授权手机号', icon: 'none' });
|
||||
this.showToast('未授权手机号', false, 1800);
|
||||
}
|
||||
},
|
||||
submitAuth() {
|
||||
if (!this.authNickname && !this.authAvatar && !this._authCode) {
|
||||
return uni.showToast({ title: '请至少授权一项', icon: 'none' });
|
||||
async submitPhone(code) {
|
||||
if (this.busy) return;
|
||||
this.busy = true;
|
||||
uni.showLoading({ title: '授权中…', mask: true });
|
||||
const r = await this.postAuth({ code: code });
|
||||
uni.hideLoading();
|
||||
this.busy = false;
|
||||
|
||||
const phone = (r.res && r.res.phone) || '';
|
||||
/* ⚠️ 成功判据必须核对「手机号真的拿到了」:后端解密失败(如 access_token 被
|
||||
商城后端挤掉)会如实返回 code!=200;旧版后端也可能 200 但 result.phone 为空。
|
||||
两种情况都不能往下走 —— 否则 H5 收到空 phone 的回传,闸门拦截,用户会陷入
|
||||
「授权成功却又要求授权」的死循环(2026-09-18 孕检日历实测)。 */
|
||||
if (!r.ok || !phone) {
|
||||
return this.showToast(r.message || '手机号获取失败,请重试', false, 3000);
|
||||
}
|
||||
const openid = getOpenid() || this.openid;
|
||||
if (!openid) return uni.showToast({ title: '未获得 openid,请先登录', icon: 'none' });
|
||||
/* unionid:用户中心身份键(预留)。带上后 box_app 授权成功自动推送用户中心
|
||||
并回写 openid→unionid 映射;没拿到也不影响授权(后端可映射兜底)。 */
|
||||
const unionid = getUnionid();
|
||||
const api = 'https://mp.yizhezhe.com/api/mp/public/auth/profile?openid=' + encodeURIComponent(openid) +
|
||||
(unionid ? '&unionid=' + encodeURIComponent(unionid) : '') +
|
||||
(this._authCode ? '&code=' + encodeURIComponent(this._authCode) : '') +
|
||||
(this.authNickname ? '&nickname=' + encodeURIComponent(this.authNickname) : '') +
|
||||
(this.authAvatar ? '&avatar=' + encodeURIComponent(this.authAvatar) : '');
|
||||
uni.request({
|
||||
url: api, method: 'POST', timeout: 10000,
|
||||
success: (r) => {
|
||||
const d = r.data || {};
|
||||
const res = d.result || {};
|
||||
// ⚠️ 成功判据必须核对「手机号真的拿到了」:提交了 code 时,
|
||||
// 后端解密失败(如 access_token 被商城后端挤掉)会如实返回
|
||||
// code!=200;旧版后端也可能 200 但 result.phone 为空。
|
||||
// 两种情况都不能写 __AUTH_RESULT__、不能提示成功 —— 否则 H5
|
||||
// 收到空 phone 的回传,闸门拦截,用户会陷入「授权成功却又
|
||||
// 要求授权」的死循环(2026-09-18 孕检日历实测)。
|
||||
const phoneMissing = !!this._authCode && !res.phone;
|
||||
if (d.code === 200 && !phoneMissing) {
|
||||
|
||||
this.phone = phone;
|
||||
this.libNickname = (r.res && r.res.userName) || '';
|
||||
this.libAvatar = (r.res && r.res.avatar) || '';
|
||||
this.phoneBound = true;
|
||||
this.sheet = '';
|
||||
this.showToast('授权成功', true, 1100);
|
||||
|
||||
/* ④ 查共享库(**必须走 GET,不能只看上面 POST 的返回**):
|
||||
本页**不采集**昵称/头像(getUserProfile 已废弃,昵称/头像只能由用户手势触发
|
||||
input type=nickname / chooseAvatar;且产品定案此页不再追问),展示的永远是
|
||||
「库里已经存过的值」—— 可能来自上一次授权、上游用户中心、或 CRMEB 商城用户表同步。
|
||||
而 GET /auth/profile 才带「先取后授权」兜底回填(用户中心 → CRMEB 同步),
|
||||
POST(apply_auth) 不做回填 —— 只在商城有昵称/头像的用户,只有 GET 能带出来。
|
||||
判据用 !!(userName || avatar),**不能**用「记录是否存在」(接口对任何 openid
|
||||
都返回 200 并现场建一条空记录)。 */
|
||||
const startedAt = Date.now();
|
||||
const lib = await this.fetchLibraryProfile();
|
||||
if (lib) {
|
||||
this.libNickname = lib.userName || '';
|
||||
this.libAvatar = lib.avatar || '';
|
||||
}
|
||||
/* 让「授权成功」toast 至少停 1.1s,查库太快也不闪 */
|
||||
const wait = Math.max(0, 1100 - (Date.now() - startedAt));
|
||||
setTimeout(() => {
|
||||
if (this.libNickname || this.libAvatar) {
|
||||
/* 库里有昵称/头像 → 提示是否复用 */
|
||||
this.sheet = 'reuse';
|
||||
} else {
|
||||
/* 库里没有 → **不追问头像昵称**(本页不采集这两项),直接收尾返回 */
|
||||
this.finish('', '', '授权成功');
|
||||
}
|
||||
}, wait);
|
||||
},
|
||||
|
||||
/* ---------- ④-A 复用库里已有资料 ---------- */
|
||||
async reuseYes() {
|
||||
if (this.busy) return;
|
||||
const extra = {};
|
||||
if (this.libNickname) extra.nickname = this.libNickname;
|
||||
if (this.libAvatar) extra.avatar = this.libAvatar;
|
||||
/* 库里值可能来自 CRMEB 商城同步(authorized=false):这里再写一次,
|
||||
把「复用」视为用户主动授权,置位 authorized 并回写用户中心。 */
|
||||
if (extra.nickname || extra.avatar) {
|
||||
this.busy = true;
|
||||
uni.showLoading({ title: '处理中…', mask: true });
|
||||
const r = await this.postAuth(extra);
|
||||
uni.hideLoading();
|
||||
this.busy = false;
|
||||
if (!r.ok) return this.showToast(r.message, false, 3000);
|
||||
this.libNickname = (r.res && r.res.userName) || this.libNickname;
|
||||
this.libAvatar = (r.res && r.res.avatar) || this.libAvatar;
|
||||
}
|
||||
this.finish(this.libNickname, this.libAvatar, '已复用微信资料');
|
||||
},
|
||||
reuseNo() {
|
||||
/* 不同步:库里资料原样保留,照样把真实值回传给 H5,然后直接返回 */
|
||||
this.finish(this.libNickname, this.libAvatar, '已保留手机号,资料未改动');
|
||||
},
|
||||
|
||||
/* ---------- 收尾:写 __AUTH_RESULT__ → 宿主 onShow 回传 H5 hash ---------- */
|
||||
finish(nickname, avatar, msg) {
|
||||
const openid = this.openid || getOpenid();
|
||||
if (!openid) return this.showToast('未获得 openid,请先登录');
|
||||
try {
|
||||
uni.setStorageSync('__AUTH_RESULT__', {
|
||||
openid: openid,
|
||||
phone: res.phone || '',
|
||||
nickname: res.userName || this.authNickname || '',
|
||||
avatar: res.avatar || this.authAvatar || '',
|
||||
phone: this.phone || '',
|
||||
nickname: nickname || '',
|
||||
avatar: avatar || '',
|
||||
backUrl: this._backUrl || ''
|
||||
});
|
||||
uni.showToast({ title: '授权成功', icon: 'success' });
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 600);
|
||||
} else {
|
||||
uni.showToast({ title: (d.message || '授权保存失败,请稍后重试'), icon: 'none', duration: 3000 });
|
||||
} catch (e) {
|
||||
console.warn('[auth] 写 __AUTH_RESULT__ 失败:', e && e.message);
|
||||
}
|
||||
this.sheet = '';
|
||||
this.showToast(msg || '授权成功', true, 1000);
|
||||
setTimeout(() => { uni.navigateBack(); }, 700);
|
||||
},
|
||||
fail: () => uni.showToast({ title: '授权请求失败,请检查网络', icon: 'none' })
|
||||
|
||||
/* ---------- ④ 查共享库(GET)----------
|
||||
唯一带「先取后授权」兜底回填的入口(本地 → 上游用户中心 → CRMEB 商城同步),
|
||||
所以判「库里是否已有昵称/头像」必须走它,不能拿 POST 的返回凑。
|
||||
超时/失败一律 resolve(null)(fail-soft):退回用 POST 带回的值判分支,
|
||||
后续任意一次 GET 仍会把兜底数据补上,不会丢状态。 */
|
||||
fetchLibraryProfile() {
|
||||
return new Promise((resolve) => {
|
||||
const openid = this.openid || getOpenid();
|
||||
if (!openid) return resolve(null);
|
||||
const unionid = getUnionid();
|
||||
let url = AUTH_API + '?openid=' + encodeURIComponent(openid);
|
||||
if (unionid) url += '&unionid=' + encodeURIComponent(unionid);
|
||||
uni.request({
|
||||
url: url,
|
||||
method: 'GET',
|
||||
timeout: 2500,
|
||||
success: (r) => {
|
||||
const d = r.data || {};
|
||||
resolve(d.code === 200 ? (d.result || {}) : null);
|
||||
},
|
||||
fail: () => resolve(null)
|
||||
});
|
||||
});
|
||||
},
|
||||
cancel() { uni.navigateBack(); }
|
||||
|
||||
/* ---------- 共享授权接口 POST(code / nickname / avatar 三选若干) ---------- */
|
||||
postAuth(extra) {
|
||||
return new Promise((resolve) => {
|
||||
const openid = this.openid || getOpenid();
|
||||
if (!openid) return resolve({ ok: false, res: {}, message: '未获得 openid,请先登录' });
|
||||
/* unionid:用户中心身份键(预留)。没拿到也不影响授权(后端可映射兜底)。 */
|
||||
const unionid = getUnionid();
|
||||
let url = AUTH_API + '?openid=' + encodeURIComponent(openid);
|
||||
if (unionid) url += '&unionid=' + encodeURIComponent(unionid);
|
||||
const ex = extra || {};
|
||||
['code', 'nickname', 'avatar'].forEach((k) => {
|
||||
if (ex[k]) url += '&' + k + '=' + encodeURIComponent(ex[k]);
|
||||
});
|
||||
uni.request({
|
||||
url: url,
|
||||
method: 'POST',
|
||||
timeout: 10000,
|
||||
success: (r) => {
|
||||
const d = r.data || {};
|
||||
if (d.code === 200) {
|
||||
resolve({ ok: true, res: d.result || {}, message: d.message || '' });
|
||||
} else {
|
||||
resolve({ ok: false, res: d.result || {}, message: d.message || '授权保存失败,请稍后重试' });
|
||||
}
|
||||
},
|
||||
fail: () => resolve({ ok: false, res: {}, message: '授权请求失败,请检查网络' })
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
showToast(msg, tick, ms) {
|
||||
if (this._toastTimer) clearTimeout(this._toastTimer);
|
||||
this.toast.msg = msg || '';
|
||||
this.toast.tick = !!tick;
|
||||
this.toast.show = true;
|
||||
this._toastTimer = setTimeout(() => { this.toast.show = false; }, ms || 1500);
|
||||
},
|
||||
noop() {}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 清 button 默认边框/padding(避免撑出卡片 + 消除默认灰底边框)。
|
||||
不改 margin —— 统配 margin:0 会压掉各按钮自己的 margin-top,导致元素紧贴。 */
|
||||
⚠️ 两条纪律,踩过:
|
||||
① 这里不设 background —— 否则会盖掉 .cta/.bmain 自己的底色;
|
||||
② `.auth-page button` 的特异性是 (0,1,1)(scoped 编译后约 (0,2,1)),**高于**单类选择器
|
||||
`.bsec`/`.bmain`/`.cta`/(0,1,0)。所以下面所有按钮自己的 padding / border 规则
|
||||
都必须带上 `.auth-page ` 前缀提权,否则会被这条 reset 清成 0 ——
|
||||
表现是「否」没有边框只剩文字、按钮高度塌成一行字高(2026-09-22 真机截图)。 */
|
||||
.auth-page button::after { border: none; }
|
||||
.auth-page button { padding: 0; line-height: normal; box-sizing: border-box; border: none; -webkit-appearance: none; }
|
||||
.auth-page input { box-sizing: border-box; margin: 0; }
|
||||
|
||||
.auth-page { min-height: 100vh; background: #F8F3EF; padding: 24px 20px 40px; display: flex; flex-direction: column; align-items: center; }
|
||||
.auth-head { text-align: center; margin-top: 16px; }
|
||||
.auth-title { font-size: 20px; font-weight: 800; color: #2B2320; display: block; }
|
||||
.auth-sub { font-size: 13px; color: #A79C95; margin-top: 8px; display: block; }
|
||||
.auth-card { width: 100%; max-width: 340px; background: #fff; border-radius: 18px; padding: 28px 20px 20px; margin-top: 28px; box-shadow: 0 6px 24px rgba(0,0,0,.05); display: flex; flex-direction: column; align-items: center; }
|
||||
.auth-page { min-height: 100vh; background: #F5EFE7; color: #4A3B33; font-size: 15px; line-height: 1.6; }
|
||||
|
||||
/* 头部:圆形头像选择区(图片填满圆并裁剪),说明文字放圆外下方 */
|
||||
.auth-avatar-wrap { display: flex; flex-direction: column; align-items: center; }
|
||||
.auth-avatar { width: 92px; height: 92px; border-radius: 50%; background: #F3E9E3; display: flex; align-items: center; justify-content: center; overflow: hidden; border: 1px dashed #D9C4B8; flex: 0 0 auto; }
|
||||
.auth-ava { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }
|
||||
.auth-ava-plus { font-size: 30px; color: #C9B7AC; line-height: 1; }
|
||||
.auth-ava-label { font-size: 11px; color: #A79C95; margin-top: 8px; }
|
||||
/* ---------- 内容区 ---------- */
|
||||
.home { padding: 14px 14px 0; }
|
||||
|
||||
/* 昵称输入 */
|
||||
.auth-field { width: 100%; margin-top: 22px; }
|
||||
.auth-input { width: 100%; height: 48px; border-radius: 12px; background: #F8F3EF; font-size: 14px; color: #2B2320; padding: 0 14px; box-sizing: border-box; }
|
||||
.auth-ph { color: #C9B7AC; }
|
||||
/* ---------- 价值主张卡(未授权时的主转化区) ---------- */
|
||||
.pitch { background-image: linear-gradient(160deg, #FFFFFF 0%, #FFFCF8 46%, #FDF6EF 100%); border: 1px solid #EDE2D7; border-radius: 16px; padding: 17px 15px 15px; box-shadow: 0 6px 18px -8px rgba(122,88,66,.18), 0 1px 3px rgba(122,88,66,.06); text-align: center; }
|
||||
.pbadge { display: inline-block; background-image: linear-gradient(90deg, #F7E4EB, #FBF0E7); color: #A9506C; font-size: 10.5px; font-weight: 700; border-radius: 20px; padding: 4px 11px; }
|
||||
.pttl { font-size: 18.5px; line-height: 1.45; margin-top: 11px; font-weight: 700; color: #4A3B33; }
|
||||
.pttl2 { margin-top: 0; }
|
||||
.pttl.em { color: #A9506C; }
|
||||
.psub { font-size: 11.5px; color: #7D6A5E; line-height: 1.8; }
|
||||
.social { display: flex; align-items: center; justify-content: center; margin-top: 12px; }
|
||||
.dots { display: flex; align-items: center; }
|
||||
.dot { width: 19px; height: 19px; border-radius: 50%; border: 1.5px solid #fff; box-shadow: 0 1px 2px rgba(60,42,36,.12); }
|
||||
.dot + .dot { margin-left: -7px; }
|
||||
.social-txt { font-size: 11px; color: #A9506C; font-weight: 600; margin-left: 7px; }
|
||||
.auth-page .cta { display: block; width: 100%; margin-top: 14px; border-radius: 13px; padding: 14px 0; font-size: 15.5px; font-weight: 700; color: #fff; background-image: linear-gradient(100deg, #B2546D 0%, #CE7089 100%); animation: ctapulse 2.6s ease-in-out infinite; }
|
||||
.auth-page .cta:active { opacity: .88; }
|
||||
@keyframes ctapulse {
|
||||
0%, 100% { box-shadow: 0 8px 18px -9px rgba(169,80,108,.55); }
|
||||
50% { box-shadow: 0 10px 26px -7px rgba(169,80,108,.85); }
|
||||
}
|
||||
.ctahint { font-size: 10.5px; color: #97867C; margin-top: 9px; }
|
||||
.trust { display: flex; justify-content: center; margin-top: 13px; padding-top: 12px; border-top: 1px dashed #EDE2D7; }
|
||||
.trust-i { font-size: 10.5px; color: #7D6A5E; margin: 0 5px; }
|
||||
.trust-tick { color: #12A67F; font-size: 11px; margin-right: 3px; }
|
||||
|
||||
/* 获取手机号按钮 */
|
||||
.auth-btn { width: 100%; height: 48px; border-radius: 12px; background: #F8F3EF; color: #2B2320; font-size: 14px; margin-top: 14px; display: flex; align-items: center; justify-content: center; }
|
||||
/* ---------- 已授权:收起成一条状态 ---------- */
|
||||
.hero { background-image: linear-gradient(160deg, #FFFFFF 0%, #FFFCF8 46%, #FDF6EF 100%); border: 1px solid #EDE2D7; border-radius: 15px; padding: 14px; box-shadow: 0 6px 18px -8px rgba(122,88,66,.18), 0 1px 3px rgba(122,88,66,.06); display: flex; align-items: center; }
|
||||
.hero .ava { width: 44px; height: 44px; border-radius: 50%; background: #F7EFE6; flex: 0 0 44px; display: flex; align-items: center; justify-content: center; overflow: hidden; }
|
||||
.ava-img { width: 100%; height: 100%; border-radius: 50%; }
|
||||
.ava-emoji { font-size: 19px; }
|
||||
.hero .who { flex: 1; min-width: 0; margin-left: 12px; }
|
||||
.hero .nm { font-size: 14.5px; font-weight: 600; }
|
||||
.hero .st { font-size: 11.5px; color: #7D6A5E; margin-top: 2px; }
|
||||
.hero .ok { font-size: 11.5px; color: #12A67F; font-weight: 700; background: #E4F5EF; border-radius: 20px; padding: 5px 11px; }
|
||||
|
||||
.auth-actions { display: flex; gap: 12px; margin-top: 24px; width: 100%; }
|
||||
.auth-cancel, .auth-ok { flex: 1; height: 46px; border-radius: 12px; font-size: 14px; font-weight: 600; display: flex; align-items: center; justify-content: center; }
|
||||
.auth-cancel { background: #F1E6E0; color: #6B615B; }
|
||||
.auth-ok { background: #F4593C; color: #fff; }
|
||||
.auth-tip { font-size: 11px; color: #A79C95; margin-top: 20px; text-align: center; line-height: 1.5; }
|
||||
/* ---------- 工具区 ---------- */
|
||||
.secthead { display: flex; align-items: baseline; justify-content: space-between; margin: 16px 2px 9px; }
|
||||
.secttl { font-size: 12px; color: #7D6A5E; font-weight: 600; }
|
||||
.sectmore { font-size: 11px; color: #97867C; }
|
||||
.freebar { display: flex; align-items: center; margin: 0 2px 11px; padding: 9px 11px; background-image: linear-gradient(120deg, #FDF1F4 0%, #FCF6F1 100%); border: 1px solid #F1DDE3; border-radius: 11px; }
|
||||
.freepill { flex: 0 0 auto; background: #A9506C; color: #fff; font-size: 10px; font-weight: 700; border-radius: 20px; padding: 3px 8px; }
|
||||
.freetxt { font-size: 11px; color: #8E4960; line-height: 1.5; margin-left: 8px; }
|
||||
|
||||
/* 底部留白要够,否则最后一行被常驻行动条压住 */
|
||||
.grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 9px; padding-bottom: 96px; }
|
||||
.tile { background: #fff; border: 1px solid #EDE2D7; border-radius: 13px; padding: 11px 4px 9px; text-align: center; box-shadow: 0 6px 18px -8px rgba(122,88,66,.18), 0 1px 3px rgba(122,88,66,.06); position: relative; }
|
||||
.tile .ic { height: 34px; display: flex; align-items: center; justify-content: center; }
|
||||
.tile-logo { width: 34px; height: 34px; }
|
||||
.tile-fb { font-size: 17px; color: #C4A18C; font-weight: 700; }
|
||||
/* 工具名最长的要能折两行且不挤掉图标 */
|
||||
.tile .tn { font-size: 9.5px; color: #5C4A41; margin-top: 6px; line-height: 1.3; min-height: 25px; }
|
||||
/* 未解锁标记:制造"授权即可打开"的预期(仅展示,不响应点击) */
|
||||
.lock { position: absolute; top: 6px; right: 6px; display: none; line-height: 0; }
|
||||
.grid.locked .lock { display: block; }
|
||||
.lock-ic { width: 12px; height: 12px; opacity: .5; }
|
||||
|
||||
/* ---------- 常驻底部行动条:上缘渐变淡出,避免硬生生切断内容 ---------- */
|
||||
.stickycta { position: fixed; left: 0; right: 0; bottom: 0; padding: 14px 14px calc(14px + env(safe-area-inset-bottom)); z-index: 10; background-image: linear-gradient(180deg, rgba(245,239,231,0) 0%, rgba(245,239,231,.97) 38%); }
|
||||
.auth-page .cta2 { margin-top: 0; font-size: 14.5px; padding: 13px 0; }
|
||||
|
||||
/* ---------- 遮罩 + 底部半弹窗 ---------- */
|
||||
.mask { position: fixed; left: 0; right: 0; top: 0; bottom: 0; background: rgba(66,50,42,.42); opacity: 0; pointer-events: none; transition: opacity .26s; z-index: 20; }
|
||||
.mask.show { opacity: 1; pointer-events: auto; }
|
||||
.sheet { position: fixed; left: 0; right: 0; bottom: 0; background: #fff; border-radius: 20px 20px 0 0; padding: 20px 20px calc(24px + env(safe-area-inset-bottom)); z-index: 30; transform: translateY(102%); transition: transform .3s cubic-bezier(.22,.9,.28,1); box-shadow: 0 -8px 30px -10px rgba(96,70,54,.28); }
|
||||
.sheet.show { transform: translateY(0); }
|
||||
.sheet .grab { width: 34px; height: 4px; border-radius: 4px; background: #EBDCCF; margin: 0 auto 15px; }
|
||||
.sheet-h3 { font-size: 16.5px; font-weight: 600; color: #4A3B33; margin-bottom: 7px; }
|
||||
.sheet-p { font-size: 12.5px; color: #7D6A5E; line-height: 1.75; }
|
||||
.btnrow { display: flex; gap: 10px; margin-top: 18px; }
|
||||
.auth-page .bsec { flex: 1; border: 1px solid #EBDCCF; background: #fff; color: #A9506C; border-radius: 12px; padding: 12px 0; font-size: 14.5px; }
|
||||
.auth-page .bsec:active { background: #FDF8F3; }
|
||||
.auth-page .bmain { flex: 1.55; border-radius: 12px; padding: 12px 0; font-size: 14.5px; font-weight: 600; color: #fff; background: #A9506C; position: relative; }
|
||||
.auth-page .bmain:active { opacity: .88; }
|
||||
.auth-page .bmain.bwx { background: #07C160; }
|
||||
|
||||
/* ---------- ④-A 复用:库里的头像 + 昵称 ---------- */
|
||||
.avarow { display: flex; align-items: center; margin-top: 18px; background: #FDF8F3; border: 1px solid #EBDCCF; border-radius: 12px; padding: 12px; }
|
||||
.avarow-big { width: 52px; height: 52px; border-radius: 50%; flex: 0 0 52px; overflow: hidden; background: #F7EFE6; display: flex; align-items: center; justify-content: center; }
|
||||
.avarow-img { width: 100%; height: 100%; border-radius: 50%; }
|
||||
.avarow-emoji { font-size: 23px; }
|
||||
.avarow-txt { min-width: 0; margin-left: 13px; }
|
||||
.avarow-txt .t1 { font-size: 14px; font-weight: 600; color: #4A3B33; }
|
||||
|
||||
/* ---------- 页内 toast ---------- */
|
||||
.toast { position: fixed; left: 50%; top: 44%; transform: translate(-50%,-50%) scale(.9); background: rgba(68,52,44,.92); color: #fff; border-radius: 12px; padding: 16px 22px; font-size: 14px; z-index: 60; opacity: 0; pointer-events: none; transition: .24s; text-align: center; min-width: 130px; }
|
||||
.toast.show { opacity: 1; transform: translate(-50%,-50%) scale(1); }
|
||||
.toast-tick { display: block; font-size: 26px; margin-bottom: 5px; color: #6EE7B7; }
|
||||
.toast-msg { color: #fff; }
|
||||
</style>
|
||||
|
||||
@@ -161,6 +161,11 @@ export default {
|
||||
}
|
||||
},
|
||||
onLoad(option) {
|
||||
/* 诊断日志(过滤关键字 enter-app):容器页每次被打开都会打这一行 ——
|
||||
它是「真的进入了某个 H5 应用」的最终证据(不管入口是恢复/分享/太阳码/商城宫格)。 */
|
||||
try {
|
||||
console.log('[enter-app] ===== 容器页 onLoad ===== option=' + JSON.stringify(option || {}));
|
||||
} catch (e) {}
|
||||
// 分享落地携带的推广人:与其它页面一致写进 globalData.spid(下单归因用)
|
||||
if (option && option.spid) {
|
||||
try { getApp().globalData.spid = option.spid; } catch (e) {}
|
||||
@@ -251,6 +256,9 @@ export default {
|
||||
this._applyShareImage(app.shareImageUrl, 'box', app.url);
|
||||
console.log('[web_view] 分享图片: ' + app.shareImageUrl);
|
||||
}
|
||||
// 同一响应里就有分享标题(后台「分享标题」,留空 = 应用名称)→ 直接落上,
|
||||
// 不必等反查表回来(太阳码是冷启动首发场景)
|
||||
this._applyShareTitleFromApp(app);
|
||||
let finalUrl = appUrl;
|
||||
if (option._channel) {
|
||||
const separator = finalUrl.includes('?') ? '&' : '?';
|
||||
@@ -446,6 +454,16 @@ export default {
|
||||
// #endif
|
||||
|
||||
this.url = targetUrl;
|
||||
console.log('[enter-app] ✅ 容器页开始加载 H5 应用: ' + targetUrl.split('#')[0]);
|
||||
|
||||
/* 宿主恢复:把「这次真正进入的 H5 应用」记到 box_app(/api/mp/public/last-app)。
|
||||
覆盖商城宫格 / 分享落地 / 朋友圈 / 太阳码 / JumpPath 这些**不经过 mama-box 宫格**
|
||||
的入口;经过 mama-box 宫格的由 H5 自己 sendBeacon 上报
|
||||
(见 mama-box js/app.js → reportLastApp)。冷启动恢复由 mama-box 首屏完成。
|
||||
⚠️ 只上报净地址(stripAuthParams):恢复时身份必须按**当前**用户重新注入。
|
||||
⚠️ 失败静默:这是旁路能力,绝不影响 H5 加载。 */
|
||||
this._reportLastApp(targetUrl);
|
||||
|
||||
try {
|
||||
const res = uni.getWindowInfo();
|
||||
this.windowW = res.windowWidth;
|
||||
@@ -458,6 +476,58 @@ export default {
|
||||
this._fetchShareImage(targetUrl);
|
||||
},
|
||||
|
||||
/* ---------- 宿主恢复:上报「本次进入的 H5 应用」(2026-09-22) ----------
|
||||
box_app /api/mp/public/last-app 是 Redis 里的 per-openid 记录(7 天 TTL),
|
||||
冷启动时由 mama-box 首屏读取并 location.replace 恢复(见 mama-box js/app.js resumeLastApp)。
|
||||
本页负责**非 mama-box 宫格**的入口:商城宫格 / 分享落地 / 朋友圈 / 太阳码 / JumpPath。
|
||||
⚠️ 传净地址:恢复时身份按当前用户重新注入,不能把记录时的 openid 带回去。
|
||||
⚠️ 失败静默:旁路能力,绝不影响 H5 加载。 */
|
||||
_reportLastApp(targetUrl) {
|
||||
try {
|
||||
const openid = getOpenid();
|
||||
if (!openid || !targetUrl) {
|
||||
console.log('[enter-app] ○ 容器页跳过 last-app 上报(openid=' + (openid ? '有' : '空') +
|
||||
', url=' + (targetUrl ? '有' : '空') + ')');
|
||||
return;
|
||||
}
|
||||
const clean = stripAuthParams(targetUrl);
|
||||
if (!clean) return;
|
||||
let api = 'https://mp.yizhezhe.com/api/mp/public/last-app?openid=' + encodeURIComponent(openid) +
|
||||
'&url=' + encodeURIComponent(clean);
|
||||
const id = this._lookupAppIdentifier(clean);
|
||||
if (id) api += '&identifier=' + encodeURIComponent(id);
|
||||
console.log('[enter-app] 容器页上报 last-app: url=' + clean + (id ? ' identifier=' + id : ''));
|
||||
uni.request({
|
||||
url: api,
|
||||
method: 'POST',
|
||||
timeout: 5000,
|
||||
success: (res) => {
|
||||
console.log('[enter-app] 容器页 last-app 上报结果: HTTP ' + ((res && res.statusCode) || '?') +
|
||||
' body=' + JSON.stringify(res && res.data));
|
||||
},
|
||||
fail: (err) => {
|
||||
console.log('[enter-app] ✗ 容器页 last-app 上报失败: ' + JSON.stringify(err));
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('[resume] 上报 last_app 失败(忽略):', e);
|
||||
}
|
||||
},
|
||||
/* 在内存应用表里按 URL 前缀反查 identifier(与 mixins/shareCard.js 的匹配同口径);
|
||||
表还没回来时返回空 —— identifier 是可选参数,服务端会按 URL 兜底校验。 */
|
||||
_lookupAppIdentifier(targetUrl) {
|
||||
const base = this._shareUrlBase(targetUrl);
|
||||
if (!base) return '';
|
||||
const apps = this.shareApps || [];
|
||||
for (let i = 0; i < apps.length; i++) {
|
||||
const a = apps[i];
|
||||
if (!a || !a.url || !a.identifier) continue;
|
||||
const ab = this._shareUrlBase(a.url);
|
||||
if (ab && (base === ab || base.indexOf(ab + '/') === 0)) return a.identifier;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
/* 分享图反查 / 单应用缓存 / 朋友圈本地图转换,统一由 mixins/shareCard.js 提供:
|
||||
_fetchShareImage(targetUrl) —— 本页 onLoad 兜底(按当前 H5 URL 反查 box_app
|
||||
App.shareImageUrl;太阳码场景 _resolveAppScene 已直接设好)
|
||||
@@ -471,9 +541,11 @@ export default {
|
||||
// 含 H5 的 SPA 路由);this.shareHref(H5 postMessage)/ this.url(进入时的地址)
|
||||
// 都可能过期或落后,仅作兜底。
|
||||
const target = (options && options.webViewUrl) || this.shareHref || this.url;
|
||||
// 应用切换检测 + 按当前地址重新反查分享图(H5 postMessage 覆盖优先)
|
||||
// 应用切换检测 + 按当前地址重新反查分享图/标题(H5 postMessage 覆盖优先)
|
||||
this.resolveShareCard(target);
|
||||
return {
|
||||
// 标题来源优先级:H5 postMessage 上报 → box_app 反查(后台「分享标题」,留空 = 应用名称,
|
||||
// 由 resolveShareCard 写进 shareTitle)→ 兜底文案。
|
||||
title: this.shareTitle || '分享',
|
||||
// 分享出去必须剥离 openid/unionid/t 等身份参数:
|
||||
// 1) 避免把分享者的微信匿名标识泄露给接收者;
|
||||
@@ -483,12 +555,15 @@ export default {
|
||||
}
|
||||
},
|
||||
onShareTimeline() {
|
||||
// onShareTimeline 无回调参数,拿不到 webViewUrl,只能用 H5 传来的 href 兜底
|
||||
// onShareTimeline 无回调参数,拿不到 webViewUrl,只能用 H5 传来的 href 兜底。
|
||||
// 同一地址也解析一次:首次分享就走朋友圈时,标题/图不能还是空的。
|
||||
const target = this.shareHref || this.url;
|
||||
this.resolveShareCard(target);
|
||||
return {
|
||||
title: this.shareTitle || '分享',
|
||||
query: 'url=' + encodeURIComponent(stripAuthParams(this.shareHref || this.url)),
|
||||
query: 'url=' + encodeURIComponent(stripAuthParams(target)),
|
||||
// 朋友圈分享图必须是本地路径/代码包路径,网络图会被微信静默忽略;
|
||||
// 网络图已在 _prepareTimelineImage 中通过 getImageInfo 转成本地临时路径。
|
||||
// 网络图已在 _prepareTimelineImage 中通过 getImageInfo 转成临时路径。
|
||||
imageUrl: this.shareTimelineImage || ''
|
||||
}
|
||||
},
|
||||
|
||||
194
pages/debug_union/index.vue
Normal file
194
pages/debug_union/index.vue
Normal file
@@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<view class="debug-page">
|
||||
<view class="card">
|
||||
<view class="title">UnionID 调试 · 获取 wx.login code</view>
|
||||
<view class="desc">
|
||||
点下方按钮执行 uni.login(),拿到 code 后:<br />
|
||||
① 页面直接显示 code(长按可复制)<br />
|
||||
② 同时 console.log 输出,方便开发者工具里复制<br />
|
||||
③ 可选:直接调本地 code2session 接口
|
||||
</view>
|
||||
|
||||
<button class="btn" type="primary" @click="getCode">1. 获取 code</button>
|
||||
<button class="btn" type="warn" @click="copyCode" :disabled="!code">2. 复制 code</button>
|
||||
|
||||
<view v-if="code" class="code-box">
|
||||
<view class="label">code(5 分钟有效,一次性):</view>
|
||||
<view class="code" @longpress="copyCode">{{ code }}</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="label">可选:本地 code2session 接口地址(内网穿透公网地址)</view>
|
||||
<input class="input" v-model="endpoint" placeholder="http://你的公网地址:8090/api/union/code2session" />
|
||||
<button class="btn" type="default" @click="sendToLocal" :disabled="!code">3. 发送到本地接口</button>
|
||||
</view>
|
||||
|
||||
<view v-if="result" class="result">
|
||||
<view class="label">接口返回:</view>
|
||||
<view class="code">{{ result }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
code: '',
|
||||
endpoint: '',
|
||||
result: ''
|
||||
}
|
||||
},
|
||||
onLoad() {},
|
||||
methods: {
|
||||
getCode() {
|
||||
this.code = ''
|
||||
this.result = ''
|
||||
uni.showLoading({
|
||||
title: '获取中'
|
||||
})
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.login({
|
||||
provider: 'weixin',
|
||||
timeout: 10000,
|
||||
success: (res) => {
|
||||
uni.hideLoading()
|
||||
this.code = res.code || ''
|
||||
console.log('[debug-union] MY_CODE=', this.code)
|
||||
uni.showToast({
|
||||
title: '已获取 code',
|
||||
icon: 'success'
|
||||
})
|
||||
},
|
||||
fail: (err) => {
|
||||
uni.hideLoading()
|
||||
console.error('[debug-union] wx.login fail', err)
|
||||
uni.showToast({
|
||||
title: '获取失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '请在微信小程序环境运行',
|
||||
icon: 'none'
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
copyCode() {
|
||||
if (!this.code) return
|
||||
uni.setClipboardData({
|
||||
data: this.code,
|
||||
success: () => {
|
||||
uni.showToast({
|
||||
title: '已复制',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
sendToLocal() {
|
||||
if (!this.code) {
|
||||
uni.showToast({
|
||||
title: '请先获取 code',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!this.endpoint) {
|
||||
uni.showToast({
|
||||
title: '请填接口地址',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
uni.request({
|
||||
url: this.endpoint,
|
||||
method: 'POST',
|
||||
data: {
|
||||
js_code: this.code
|
||||
},
|
||||
header: {
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
success: (res) => {
|
||||
this.result = JSON.stringify(res.data, null, 2)
|
||||
console.log('[debug-union] response=', this.result)
|
||||
},
|
||||
fail: (err) => {
|
||||
this.result = '请求失败: ' + JSON.stringify(err)
|
||||
console.error('[debug-union] request fail', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.debug-page {
|
||||
padding: 24rpx;
|
||||
background: #f7f8fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
}
|
||||
.title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.desc {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
line-height: 1.7;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.btn {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.section {
|
||||
margin-top: 20rpx;
|
||||
padding-top: 20rpx;
|
||||
border-top: 1rpx solid #eee;
|
||||
}
|
||||
.label {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.input {
|
||||
border: 1rpx solid #ddd;
|
||||
border-radius: 8rpx;
|
||||
padding: 16rpx;
|
||||
font-size: 26rpx;
|
||||
margin-bottom: 16rpx;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.code-box {
|
||||
margin-top: 20rpx;
|
||||
background: #f0f5ff;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx;
|
||||
}
|
||||
.code {
|
||||
font-size: 24rpx;
|
||||
word-break: break-all;
|
||||
color: #1a1a1a;
|
||||
user-select: text;
|
||||
}
|
||||
.result {
|
||||
margin-top: 20rpx;
|
||||
background: #fffbe6;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -1,129 +0,0 @@
|
||||
<template>
|
||||
<view class="skeleton">
|
||||
<!-- #ifdef MP || APP -->
|
||||
<view class="blank"></view>
|
||||
<!-- #endif -->
|
||||
<view class="header acea-row row-between-wrapper">
|
||||
<view class="left"></view>
|
||||
<view class="right"></view>
|
||||
</view>
|
||||
<view class="tabNav acea-row row-between-wrapper">
|
||||
<view class="item"></view>
|
||||
<view class="item"></view>
|
||||
<view class="item"></view>
|
||||
<view class="item"></view>
|
||||
</view>
|
||||
<view class="swipers"></view>
|
||||
<view class="nav acea-row row-between-wrapper">
|
||||
<view class="item"></view>
|
||||
<view class="item"></view>
|
||||
<view class="item"></view>
|
||||
<view class="item"></view>
|
||||
<view class="item"></view>
|
||||
</view>
|
||||
<view class="nav acea-row row-between-wrapper">
|
||||
<view class="item"></view>
|
||||
<view class="item"></view>
|
||||
<view class="item"></view>
|
||||
<view class="item"></view>
|
||||
<view class="item"></view>
|
||||
</view>
|
||||
<view class="header acea-row row-between-wrapper">
|
||||
<view class="left"></view>
|
||||
<view class="right"></view>
|
||||
</view>
|
||||
<view class="coupon acea-row row-between-wrapper">
|
||||
<view class="item"></view>
|
||||
<view class="item"></view>
|
||||
</view>
|
||||
<view class="goodList">
|
||||
<view class="item acea-row row-between row-top">
|
||||
<view class="pictrue"></view>
|
||||
<view class="text">
|
||||
<view class="name"></view>
|
||||
<view class="money"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.skeleton{
|
||||
padding: 0 30rpx;
|
||||
.blank{
|
||||
width: 100%;
|
||||
height: 150rpx;
|
||||
background-color: #F5F5F5;
|
||||
}
|
||||
.header{
|
||||
.left{
|
||||
width: 200rpx;
|
||||
height: 50rpx;
|
||||
background-color: #e9e9e9;
|
||||
}
|
||||
.right{
|
||||
width: 460rpx;
|
||||
height: 50rpx;
|
||||
background-color: #e9e9e9;
|
||||
}
|
||||
}
|
||||
.tabNav{
|
||||
margin-top: 20rpx;
|
||||
.item{
|
||||
width: 150rpx;
|
||||
height: 50rpx;
|
||||
background-color: #e9e9e9;
|
||||
}
|
||||
}
|
||||
.swipers{
|
||||
margin-top: 20rpx;
|
||||
width: 100%;
|
||||
height: 400rpx;
|
||||
background-color: #e9e9e9;
|
||||
}
|
||||
.nav{
|
||||
margin: 20rpx 0;
|
||||
.item{
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #e9e9e9;
|
||||
}
|
||||
}
|
||||
.coupon{
|
||||
margin-top: 20rpx;
|
||||
.item{
|
||||
width: 330rpx;
|
||||
height: 120rpx;
|
||||
background-color: #e9e9e9;
|
||||
}
|
||||
}
|
||||
.goodList{
|
||||
margin-top: 20rpx;
|
||||
.item{
|
||||
.pictrue{
|
||||
width: 220rpx;
|
||||
height: 220rpx;
|
||||
background-color: #e9e9e9;
|
||||
}
|
||||
.text{
|
||||
.name{
|
||||
width: 440rpx;
|
||||
height: 76rpx;
|
||||
background-color: #e9e9e9;
|
||||
}
|
||||
.money{
|
||||
width: 100rpx;
|
||||
height: 34rpx;
|
||||
background-color: #e9e9e9;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -5,11 +5,12 @@
|
||||
本页替代了 CRMEB 原装修首页(原文件完整备份在同目录
|
||||
index.vue.origin-bak,恢复方法见本文件底部注释)。
|
||||
|
||||
链路(2026-09 起:mama-box 不再是 box_app 的「应用」,URL 硬编码):
|
||||
onLoad → loadMamaBox()
|
||||
→ webUrl = https://mp.yizhezhe.com/mama-box/ + #openid=xxx(buildWebUrl)
|
||||
链路(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
|
||||
(更新首页 = 直接改服务端 box_app/mama-box/ 目录,无需发版即可刷新内容)
|
||||
(更新首页内容 = 改服务端对应 H5 目录;换首页地址 = 管理后台「首页配置」,均无需发版)
|
||||
|
||||
两个坑(都来自 app_03 前一次接入的实测结论):
|
||||
1. 微信 web-view 首次加载会丢弃 URL 的 query string(?a=b),
|
||||
@@ -68,8 +69,18 @@
|
||||
|
||||
const app = getApp();
|
||||
|
||||
// mama-box H5 托管地址(硬编码;更新首页 = 改服务端 box_app/mama-box/ 目录)
|
||||
// 内置兜底首页地址(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 泄露给接收者;
|
||||
@@ -119,6 +130,7 @@
|
||||
if (this.loadError) return '请检查网络后重试';
|
||||
if (this.identityFail) return '请检查网络后点下方重试,或退出小程序重新进入';
|
||||
if (this.identityPending) return '马上就好,请稍候(不会加载空数据的页面)';
|
||||
if (this.restoring) return '正在回到你上次使用的应用';
|
||||
return '正在为你打开「兜爸心选」';
|
||||
},
|
||||
fallbackEmoji() {
|
||||
@@ -127,11 +139,15 @@
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
webUrl: '', // web-view 地址(硬编码 + #openid/#unionid)
|
||||
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 → 停在兜底屏,给「重新获取身份」入口
|
||||
shareInfo: {}, // CRMEB 后台「分享设置」(标题/图,无 App 匹配时兜底)
|
||||
@@ -142,6 +158,20 @@
|
||||
};
|
||||
},
|
||||
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'] });
|
||||
@@ -152,6 +182,9 @@
|
||||
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 同步匹配,
|
||||
@@ -174,7 +207,9 @@
|
||||
if (options && typeof options.url === 'string' && options.url) {
|
||||
let target = options.url;
|
||||
try { target = decodeURIComponent(target); } catch (e) {}
|
||||
this.loadMamaBox(); // 保留首页作为返回落点
|
||||
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) });
|
||||
});
|
||||
@@ -189,9 +224,44 @@
|
||||
// ⚠️ 但严禁在商城页主动 showTabBar 来「配对」——上一轮翻车根因
|
||||
// (showTabBar + DIY 底栏同显 = 商品列表双 tabBar,见 onShow 旧警告)。
|
||||
uni.hideTabBar();
|
||||
this.loadMamaBox();
|
||||
/* 冷启动恢复(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 死代码一并移除)
|
||||
@@ -205,29 +275,43 @@
|
||||
// 目标 H5 永远收不到 phone(2026-09-18 孕检日历授权弹层复现根因之一)。
|
||||
// shareHref 由 H5 换页/发起授权前的 location 上报实时刷新(askProfile
|
||||
// 会先 reportLocation 再 navigateTo),是「当前 H5」最可靠的来源。
|
||||
const authBase = (this.shareHref || this.webUrl || MAMA_HOME).split('#')[0];
|
||||
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) {
|
||||
} 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 || MAMA_HOME).split('#')[0];
|
||||
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()) {
|
||||
this.webUrl = this.buildWebUrl(reloadBase + '?t=' + Date.now());
|
||||
// ⚠️ 回首页 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
|
||||
@@ -265,7 +349,7 @@
|
||||
现在本页 onLoad 预热全量应用表(mixins/shareCard.js),转发时用
|
||||
options.webViewUrl 同步匹配出图;H5 postMessage 的 imageUrl 有值才覆盖。 */
|
||||
onShareAppMessage(options) {
|
||||
const target = (options && options.webViewUrl) || this.shareHref || this.webUrl || MAMA_HOME;
|
||||
const target = (options && options.webViewUrl) || this.shareHref || this.webUrl || this._homeBase();
|
||||
// 按「当前 H5 地址」解析分享图:先做应用切换检测(首页壳不随 H5 换页重新 onLoad),
|
||||
// 再每次重新反查 box_app —— 不能因为 shareImageUrl 已有值就跳过,
|
||||
// 那是上一个应用的图(A 应用分享过、切到 B 后仍带 A 的图,就是这里出的错)。
|
||||
@@ -273,6 +357,8 @@
|
||||
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,
|
||||
@@ -280,8 +366,10 @@
|
||||
};
|
||||
},
|
||||
onShareTimeline() {
|
||||
// onShareTimeline 无回调参数,拿不到 webViewUrl,只能用 H5 传来的 href 兜底
|
||||
const target = this.shareHref || this.webUrl || MAMA_HOME;
|
||||
// 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 {
|
||||
@@ -302,17 +390,22 @@
|
||||
if (options.spid) app.globalData.spid = options.spid;
|
||||
},
|
||||
/**
|
||||
* 查询 box_app 拿兜爸心选的托管地址。
|
||||
* box_app 的 App 表是「产品盒子」的应用注册中心 —— 改后台这条记录的
|
||||
* url 字段即可给 H5 换地址 / 切版本,小程序不用发版。
|
||||
* 加载首页 H5。
|
||||
* 2026-10-08 起:首页地址不再写死 —— 先等 _homeUrlReady()(box_app 后台配置,
|
||||
* 本地缓存兜底,最多 2s),再按「有 openid 直接拼 hash 加载 / 无 openid 走取到才加载」。
|
||||
*/
|
||||
loadMamaBox() {
|
||||
// 2026-09 起:mama-box 不再是 box_app 的「应用」(已从 App 表删除)。
|
||||
// 它是小程序首页 + H5 入口,URL 在此【硬编码】;
|
||||
// 更新首页 = 直接改服务端 box_app/mama-box/ 目录,无需发版即可刷新内容。
|
||||
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 加载 —— 别匿名加载。
|
||||
@@ -321,10 +414,11 @@
|
||||
容器页那样的 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(MAMA_HOME);
|
||||
this.webUrl = this.buildWebUrl(homeUrl, this._noRestore ? ['norestore=1'] : []);
|
||||
this.requesting = false;
|
||||
return;
|
||||
}
|
||||
@@ -333,14 +427,185 @@
|
||||
匿名加载会让 H5 进访客态(孕周 `--`、档案查不到),
|
||||
用户看到「页面能用但全是空」比等一会儿更糟,且事后很难判断是身份问题。 */
|
||||
this._idAttempts = 0;
|
||||
this.fetchIdentityThenLoad(MAMA_HOME);
|
||||
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 基址(首页壳是 MAMA_HOME;onShow 重载时是当前 H5)
|
||||
* @param {string} base - 要加载的 H5 基址(首页壳是生效首页地址;onShow 重载时是当前 H5)
|
||||
*/
|
||||
fetchIdentityThenLoad(base) {
|
||||
/* 1) 缓存命中(含 App.vue 并行登录刚落地的)→ 0 等待加载 */
|
||||
@@ -376,7 +641,9 @@
|
||||
this.identityFail = false;
|
||||
this._idAttempts = 0;
|
||||
this._shownOnce = true;
|
||||
this.webUrl = this.buildWebUrl(base + (base.indexOf('?') >= 0 ? '&' : '?') + 't=' + Date.now());
|
||||
// 沿用本次的 _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;
|
||||
},
|
||||
@@ -384,26 +651,29 @@
|
||||
* 给 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) {
|
||||
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);
|
||||
if (!parts.length) return url;
|
||||
return url + (url.indexOf('#') >= 0 ? '&' : '#') + parts.join('&');
|
||||
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 || MAMA_HOME; },
|
||||
_hostCurrentUrl() { return this.webUrl || this._homeBase(); },
|
||||
|
||||
/* 授权面板提交(base = 当前 H5 地址,同 onShow 的回传落点逻辑 ——
|
||||
宫格换应用后 MAMA_HOME 不再是用户所在的 H5)→ mixin 的共享授权接口 + hash 回传 */
|
||||
宫格换应用后首页地址不再是用户所在的 H5)→ mixin 的共享授权接口 + hash 回传 */
|
||||
submitAuth() {
|
||||
this.submitHostAuth((this.shareHref || this.webUrl || MAMA_HOME).split('#')[0]);
|
||||
this.submitHostAuth((this.shareHref || this.webUrl || this._homeBase()).split('#')[0]);
|
||||
},
|
||||
|
||||
/* MP:订阅消息模板预取(原装修首页行为;utils/SubscribeMessage.js 请求时用缓存) */
|
||||
@@ -427,19 +697,22 @@
|
||||
this.requesting = false;
|
||||
this._shownOnce = true;
|
||||
this.identityFail = false;
|
||||
/* 有身份直接重载;没有就再走一遍「取到才加载」(重置尝试次数,自动重试 3 次) */
|
||||
/* 有身份直接重载;没有就再走一遍「取到才加载」(重置尝试次数,自动重试 3 次)
|
||||
⚠️ 沿用 this._noRestore:重试首页时同样不许首页 H5 兜底恢复(见 loadMamaBox)。 */
|
||||
const homeUrl = this._homeBase();
|
||||
if (this._hostOpenid()) {
|
||||
this.webUrl = this.buildWebUrl(MAMA_HOME + '?t=' + Date.now());
|
||||
this.webUrl = this.buildWebUrl(homeUrl + '?t=' + Date.now(),
|
||||
this._noRestore ? ['norestore=1'] : []);
|
||||
return;
|
||||
}
|
||||
this._idAttempts = 0;
|
||||
this.fetchIdentityThenLoad(MAMA_HOME);
|
||||
this.fetchIdentityThenLoad(homeUrl);
|
||||
},
|
||||
|
||||
/* 授权完善资料(postMessage → 原生面板 → 共享授权接口 → hash 回传)与
|
||||
pages/annex/web_view 共用 mixins/webviewHost.js:
|
||||
handleHostMessage / onChooseHostAvatar / onHostGetPhone /
|
||||
submitHostAuth(MAMA_HOME) / consumeAuthResult(见 onShow)
|
||||
submitHostAuth(生效首页地址) / consumeAuthResult(见 onShow)
|
||||
本页不再各写一份;分享图设置由 mixins/shareCard.js 提供。 */
|
||||
}
|
||||
}
|
||||
|
||||
875
pages/index/mall.vue
Normal file
875
pages/index/mall.vue
Normal file
@@ -0,0 +1,875 @@
|
||||
<!-- ============================================================
|
||||
商城首页(第 5 个 tabBar 页「商城」)
|
||||
------------------------------------------------------------
|
||||
本页 = 原装 CRMEB 装修首页(pages/index/index.vue.origin-bak)
|
||||
原样恢复成独立路由。起因:pages/index/index 路由已被
|
||||
「兜爸心选」H5 的 web-view 壳占用,H5 底部「兜爸心选」tab
|
||||
(goMall(),见 mama-box 仓库 js/app.js)要跳回老商城首页,
|
||||
只能把它恢复成一个新 tabBar 页(tabBar 上限 5,原 4 项)。
|
||||
|
||||
与原首页的两处差异(其余 1:1):
|
||||
1. 已注册进 pages.json 的 tabBar(第 2 位「商城」)。⚠️ 底栏的真实渲染方是
|
||||
后台「装修→底部导航」DIY 配置(/api/navigation → components/pageFooter),
|
||||
商城项要在底栏出现必须在后台配置里加(链接填 /pages/index/mall,goRouter
|
||||
走 switchTab);这里的 tabBar 注册只保证 switchTab 可达 + DIY 导航禁用时
|
||||
原生 tabBar 兜底可显示(App.vue onLaunch 会 hideTabBar,DIY 启用时
|
||||
pageFooter 也会 hideTabBar,两层兜底见 components/pageFooter/index.vue)。
|
||||
2. 依赖的 4 个组件(appCenter/images/pictureCube/promotionList)
|
||||
在壳替换时被从工作区删了,已按需从 HEAD 恢复,勿再删。
|
||||
|
||||
pages.json:pages 列表 + tabBar 第 2 项均已注册(enablePullDownRefresh
|
||||
沿用原首页 true,与 web-view 壳页的 false 不同,别混)。
|
||||
============================================================ -->
|
||||
<template>
|
||||
<!-- 首页 -->
|
||||
<view v-if="pageShow" class="page"
|
||||
:class="bgTabVal==2?'fullsize noRepeat':bgTabVal==1?'repeat ysize':'noRepeat ysize'"
|
||||
:style="[pageStyle]">
|
||||
<view v-if="!errorNetwork" :style="colorStyle">
|
||||
<!-- 轮播搜索 -->
|
||||
<homeComb v-if="showHomeComb" :dataConfig="homeCombData" @bindSortId="bindSortId":isScrolled="isScrolled"></homeComb>
|
||||
<!-- 顶部搜索框 -->
|
||||
<headerSerch v-if="isHeaderSerch" :dataConfig="headerSerchCombData"></headerSerch>
|
||||
<tabNav v-if="showCateNav" :dataConfig="cateNavData" @bindHeight="bindHeighta"
|
||||
@bindSortId="bindSortId" :isFixed="isFixed && !cateNavData.stickyConfig.tabVal"></tabNav>
|
||||
<view class="index">
|
||||
<!-- 自定义样式 -->
|
||||
<block v-for="(item, index) in styleConfig" :key="index">
|
||||
<!-- <shortVideo v-if="item.name == 'shortVideo'" :dataConfig="item">
|
||||
</shortVideo> -->
|
||||
<userInfor v-if="item.name == 'userInfor'" :dataConfig="item"
|
||||
@changeLogin="changeLogin">
|
||||
</userInfor>
|
||||
<newVip v-if="item.name == 'newVip'" :dataConfig="item"></newVip>
|
||||
<pageTopbar v-if="item.name == 'pageTopbar'" :dataConfig="item"></pageTopbar>
|
||||
<!-- 文章列表 -->
|
||||
<articleList v-if="item.name == 'articleList'" :dataConfig="item">
|
||||
</articleList>
|
||||
<bargain v-if="item.name == 'bargain'" :dataConfig="item" :pullReset="pullReset" @changeBarg="changeBarg"
|
||||
></bargain>
|
||||
<blankPage v-if="item.name == 'blankPage'" :dataConfig="item"></blankPage>
|
||||
<combination v-if="item.name == 'combination'" :dataConfig="item" :pullReset="pullReset">
|
||||
</combination>
|
||||
<!-- 优惠券 -->
|
||||
<coupon v-if="item.name == 'coupon'" :dataConfig="item"
|
||||
@changeLogin="changeLogin"></coupon>
|
||||
<!-- 客户服务 -->
|
||||
<customerService v-if="item.name == 'customerService'" :dataConfig="item">
|
||||
</customerService>
|
||||
<!-- 商品列表 -->
|
||||
<goodList v-if="item.name == 'goodList'" :dataConfig="item"
|
||||
></goodList>
|
||||
<guide v-if="item.name == 'guide'" :dataConfig="item"></guide>
|
||||
<!-- 直播模块 -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<liveBroadcast v-if="item.name == 'liveBroadcast'" :dataConfig="item"></liveBroadcast>
|
||||
<!-- #endif -->
|
||||
<menus v-if="item.name == 'menus'" :dataConfig="item"></menus>
|
||||
<!-- 实时消息 -->
|
||||
<news v-if="item.name == 'news'" :dataConfig="item"></news>
|
||||
<!-- 图片库 -->
|
||||
<pictureCube v-if="item.name == 'pictureCube'" :dataConfig="item"></pictureCube>
|
||||
<!-- 图片组件 -->
|
||||
<images v-if="item.name == 'images'" :dataConfig="item"></images>
|
||||
<!-- 促销列表 -->
|
||||
<promotionList v-if="item.name == 'promotionList'" :dataConfig="item"
|
||||
:productVideoStatus='product_video_status' :showHomeComb="showHomeComb" :isHeaderSerch="isHeaderSerch">
|
||||
</promotionList>
|
||||
<richText v-if="item.name == 'richText'" :dataConfig="item"></richText>
|
||||
<seckill v-if="item.name == 'seckill'" :dataConfig="item" :pullReset="pullReset"></seckill>
|
||||
<!-- 轮播图-->
|
||||
<swiperBg v-if="item.name == 'swiperBg'" :dataConfig="item"></swiperBg>
|
||||
<swipers v-if="item.name == 'swipers'" :dataConfig="item"></swipers>
|
||||
<!-- 顶部选项卡 -->
|
||||
|
||||
<!-- 标题 -->
|
||||
<titles v-if="item.name == 'titles'" :dataConfig="item"></titles>
|
||||
<ranking v-if="item.name == 'ranking'" :dataConfig="item"></ranking>
|
||||
<presale v-if="item.name == 'presale'" :dataConfig="item"></presale>
|
||||
<pointsMall v-if="item.name == 'pointsMall'" :dataConfig="item"></pointsMall>
|
||||
<videos v-if="item.name == 'videos'" :dataConfig="item"></videos>
|
||||
<signIn v-if="item.name == 'signIn'" :dataConfig="item"></signIn>
|
||||
<hotspot v-if="item.name == 'hotspot'" :dataConfig="item"></hotspot>
|
||||
<follow v-if="item.name == 'follow'" :dataConfig="item"></follow>
|
||||
<appCenter v-if="item.name == 'appCenter'" :dataConfig="item"></appCenter>
|
||||
<community v-if="item.name == 'community'" :dataConfig="item"></community>
|
||||
</block>
|
||||
<!-- 分类商品模块 -->
|
||||
<!-- #ifndef APP-PLUS -->
|
||||
<view class="sort-product px-20">
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef APP-PLUS -->
|
||||
<!-- 商品排序 -->
|
||||
<view class="sort-product px-20" :style="{ marginTop: sortMpTop + 'px' }">
|
||||
<!-- #endif -->
|
||||
<view class="rd-24rpx bg--w111-fff p-24 mb-24" v-if="sortList.children && sortList.children.length">
|
||||
<scroll-view scroll-x="true" class="white-nowrap vertical-middle w-full" show-scrollbar="false">
|
||||
<view class="inline-block mr-24" v-for="(item, index) in sortList.children" :key="index">
|
||||
<view class="flex-col flex-center" @tap="changeSort(item, index)">
|
||||
<view class="picture w-90 h-90 rd-50-p111-"
|
||||
:class="{select: curSort == index}">
|
||||
<image :src="item.pic" class="w-full h-full rd-50-p111-"></image>
|
||||
</view>
|
||||
<text class="fs-24 pt-14" :class="{'font-num': curSort == index}">{{item.cate_name}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<waterfallsFlow ref="waterfallsFlow" :wfList="goodList" :showCart="false" @itemTap="goDetail"></waterfallsFlow>
|
||||
<Loading :loaded="loaded" :loading="loading"></Loading>
|
||||
<view v-if="goodList.length == 0 && loaded">
|
||||
<emptyPage title="暂无商品,去看点别的吧~" ></emptyPage>
|
||||
</view>
|
||||
</view>
|
||||
<activityModal :pageIndex="1"></activityModal>
|
||||
<!-- #ifdef H5 -->
|
||||
<view v-if="configData.record_No" class="site-config" @click="goICP">{{ configData.record_No }}</view>
|
||||
<!-- #endif -->
|
||||
<view class="pb-safe h-100" v-if="isFooter"></view>
|
||||
<pageFooter @newDataStatus="newDataStatus"></pageFooter>
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<add-tip></add-tip>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
<!-- #ifdef APP -->
|
||||
<app-update ref="appUpdate" :force="true" :tabbar="false"></app-update>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
<view v-else>
|
||||
<view class="o813d0">
|
||||
<image :src="imgHost + '/statics/images/error-network.gif'"></image>
|
||||
<view class="title">网络连接断开</view>
|
||||
<view class="of0b21">
|
||||
<view class="label">请检查情况:</view>
|
||||
<view class="item">· 在设置中是否已开启网络权限</view>
|
||||
<view class="item">· 当前是否处于弱网环境</view>
|
||||
<view class="item">· 版本是否过低,升级试试吧</view>
|
||||
</view>
|
||||
<view class="btn" @click="reconnect">重新连接</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const app = getApp();
|
||||
import colors from "@/mixins/color";
|
||||
import couponWindow from '@/components/couponWindow/index';
|
||||
import {
|
||||
getCouponV2,
|
||||
getCouponNewUser,
|
||||
copyRight
|
||||
} from '@/api/api.js';
|
||||
import {
|
||||
getShare
|
||||
} from '@/api/public.js';
|
||||
// #ifdef H5
|
||||
import {
|
||||
silenceAuth
|
||||
} from '@/api/public.js';
|
||||
// #endif
|
||||
import Cache from '@/utils/cache';
|
||||
import userInfor from './components/userInfor';
|
||||
import homeComb from './components/homeComb';
|
||||
import newVip from './components/newVip';
|
||||
import pageTopbar from './components/pageTopbar';
|
||||
// import shortVideo from './components/shortVideo';
|
||||
import community from './components/community'
|
||||
import headerSerch from './components/headerSerch';
|
||||
import swipers from './components/swipers';
|
||||
import coupon from './components/coupon';
|
||||
import articleList from './components/articleList';
|
||||
import bargain from './components/bargain';
|
||||
import blankPage from './components/blankPage';
|
||||
import combination from './components/combination';
|
||||
import customerService from './components/customerService';
|
||||
import goodList from './components/goodList';
|
||||
import guide from './components/guide';
|
||||
import liveBroadcast from './components/liveBroadcast';
|
||||
import menus from './components/menus';
|
||||
import news from './components/news';
|
||||
import pictureCube from './components/pictureCube';
|
||||
import images from './components/images';
|
||||
import promotionList from './components/promotionList';
|
||||
import richText from './components/richText';
|
||||
import seckill from './components/seckill';
|
||||
import swiperBg from './components/swiperBg';
|
||||
import tabNav from './components/tabNav';
|
||||
import titles from './components/titles';
|
||||
import ranking from './components/ranking';
|
||||
import presale from './components/presale'
|
||||
import pointsMall from './components/pointsMall';
|
||||
import videos from './components/videos';
|
||||
import signIn from './components/signIn';
|
||||
import hotspot from './components/hotspot';
|
||||
import follow from './components/follow';
|
||||
import appCenter from './components/appCenter';
|
||||
import waterfallsFlow from "@/components/WaterfallsFlow/WaterfallsFlow.vue";
|
||||
import emptyPage from '@/components/emptyPage.vue';
|
||||
import activityModal from "@/components/activityModal/index.vue"
|
||||
// #ifdef MP
|
||||
import addTip from '@/components/weixinTip/index.vue'
|
||||
import {getTemlIds} from '@/api/api.js';
|
||||
import {
|
||||
SUBSCRIBE_MESSAGE,
|
||||
TIPS_KEY
|
||||
} from '@/config/cache';
|
||||
// #endif
|
||||
import {mapGetters,mapMutations} from 'vuex';
|
||||
import {getDiy, getDiyVersion} from '@/api/api.js';
|
||||
import {
|
||||
getCategoryList,
|
||||
getProductslist,
|
||||
getProductHot,
|
||||
diyProductApi
|
||||
} from '@/api/store.js';
|
||||
import { goShopDetail } from '@/libs/order.js';
|
||||
import { toLogin } from '@/libs/login.js';
|
||||
import { HTTP_REQUEST_URL } from '@/config/app';
|
||||
import pageFooter from '@/components/pageFooter/index.vue';
|
||||
import Loading from '@/components/Loading/index.vue';
|
||||
import recommend from '@/components/recommend';
|
||||
import appUpdate from '@/components/update/app-update.vue';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
pageStyle(){
|
||||
return {
|
||||
backgroundColor: this.bgColor,
|
||||
backgroundImage: this.bgPic ? `url(${this.bgPic})` : '',
|
||||
minHeight: this.windowHeight + 'px'
|
||||
}
|
||||
},
|
||||
...mapGetters(['isLogin', 'uid', 'cartNum']),
|
||||
},
|
||||
mixins: [colors],
|
||||
components: {
|
||||
recommend,
|
||||
Loading,
|
||||
pageFooter,
|
||||
couponWindow,
|
||||
homeComb,
|
||||
newVip,
|
||||
pageTopbar,
|
||||
userInfor,
|
||||
community,
|
||||
headerSerch,
|
||||
swipers,
|
||||
coupon,
|
||||
articleList,
|
||||
bargain,
|
||||
blankPage,
|
||||
combination,
|
||||
customerService,
|
||||
goodList,
|
||||
guide,
|
||||
menus,
|
||||
pictureCube,
|
||||
images,
|
||||
news,
|
||||
promotionList,
|
||||
richText,
|
||||
seckill,
|
||||
swiperBg,
|
||||
tabNav,
|
||||
titles,
|
||||
ranking,
|
||||
presale,
|
||||
pointsMall,
|
||||
videos,
|
||||
signIn,
|
||||
hotspot,
|
||||
follow,
|
||||
appCenter,
|
||||
waterfallsFlow,
|
||||
activityModal,
|
||||
emptyPage,
|
||||
// #ifdef APP-PLUS
|
||||
appUpdate,
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
addTip,
|
||||
liveBroadcast,
|
||||
// #endif
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
__debugDiyRender: false,
|
||||
styleConfig: [],
|
||||
loading: false,
|
||||
loadend: false,
|
||||
loadTitle: '下拉加载更多', //提示语
|
||||
page: 1,
|
||||
limit: this.$config.LIMIT,
|
||||
numConfig: 0,
|
||||
code: '',
|
||||
isCouponShow: false,
|
||||
shareInfo: {},
|
||||
sortList: '',
|
||||
sortAll: [],
|
||||
goodPage: 1,
|
||||
goodList: [],
|
||||
sid: 0,
|
||||
curSort: 0,
|
||||
sortMpTop: 0,
|
||||
loaded: false,
|
||||
loading: false,
|
||||
domOffsetTop: 50,
|
||||
// #ifdef APP-PLUS || MP
|
||||
isFixed: true,
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
isFixed: false,
|
||||
// #endif
|
||||
site_config: '',
|
||||
errorNetwork: false, // 是否断网
|
||||
isHeaderSerch: false,
|
||||
showHomeComb: false,
|
||||
showCateNav: false,
|
||||
homeCombData:{},
|
||||
headerSerchCombData:{},
|
||||
cateNavData:{},
|
||||
bgColor: '',
|
||||
bgPic: '',
|
||||
bgTabVal: '',
|
||||
pageShow: true,
|
||||
windowHeight: 0,
|
||||
imgHost: HTTP_REQUEST_URL,
|
||||
isShowAuth: false,
|
||||
isScrolled: false,
|
||||
product_video_status: false,
|
||||
confirm_video_status: false,
|
||||
isFooter: false,
|
||||
pullReset: 0,
|
||||
configData: Cache.get('BASIC_CONFIG') || {},
|
||||
};
|
||||
},
|
||||
onLoad(options) {
|
||||
let that = this;
|
||||
this.__debugDiyRender = uni.getStorageSync('DEBUG_DIY_RENDER') === true
|
||||
that.getOptions(options);
|
||||
this.$nextTick(function() {
|
||||
uni.getSystemInfo({
|
||||
success: function(res) {
|
||||
that.windowHeight = res.windowHeight;
|
||||
}
|
||||
});
|
||||
})
|
||||
const {state, scope} = options;
|
||||
let identity = this.$store.state.app.identity;
|
||||
if(identity == 1 && this.isLogin){
|
||||
uni.redirectTo({
|
||||
url: "/pages/merchant/index/goods"
|
||||
})
|
||||
}else if(identity == 2 && this.isLogin){
|
||||
uni.redirectTo({
|
||||
url:"/pages/admin/work/index"
|
||||
})
|
||||
}else{
|
||||
this.diyData();
|
||||
}
|
||||
|
||||
// #ifdef H5
|
||||
this.setOpenShare();
|
||||
// #endif
|
||||
// #ifdef MP
|
||||
this.getTemlIds();
|
||||
// #endif
|
||||
getShare().then(res => {
|
||||
this.shareInfo = res.data;
|
||||
});
|
||||
},
|
||||
onShow() {
|
||||
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')
|
||||
},
|
||||
methods: {
|
||||
__logDiy(step, payload) {
|
||||
if (!this.__debugDiyRender) return;
|
||||
const safe = payload === undefined ? '' : payload
|
||||
console.log('[DIY][home]', step, safe)
|
||||
},
|
||||
...mapMutations(['SET_AUTOPLAY']),
|
||||
getOptions(options) {
|
||||
let that = this;
|
||||
// #ifdef MP
|
||||
if (options.scene) {
|
||||
let value = that.$util.getUrlParams(decodeURIComponent(options.scene));
|
||||
//记录推广人uid
|
||||
if (value.spid) app.globalData.spid = value.spid;
|
||||
}
|
||||
// #endif
|
||||
if (options.spid) app.globalData.spid = options.spid;
|
||||
},
|
||||
// 重新链接
|
||||
reconnect() {
|
||||
this.diyData();
|
||||
getShare().then(res => {
|
||||
this.shareInfo = res.data;
|
||||
});
|
||||
},
|
||||
goICP() {
|
||||
// #ifdef H5
|
||||
window.open('http://beian.miit.gov.cn/');
|
||||
// #endif
|
||||
// #ifdef MP
|
||||
uni.navigateTo({
|
||||
url: `/pages/annex/web_view/index?url=https://beian.miit.gov.cn/`
|
||||
});
|
||||
// #endif
|
||||
},
|
||||
bindHeighta(data) {
|
||||
// #ifdef APP-PLUS
|
||||
this.sortMpTop = data.top + data.height;
|
||||
// #endif
|
||||
},
|
||||
bindHeight(data) {
|
||||
uni.hideLoading();
|
||||
this.domOffsetTop = data.top;
|
||||
},
|
||||
// 分类点击
|
||||
changeSort(item, index) {
|
||||
if (this.curSort == index) return;
|
||||
this.curSort = index;
|
||||
this.sid = item.id;
|
||||
this.goodList = [];
|
||||
this.goodPage = 1;
|
||||
this.loaded = false;
|
||||
this.getGoodsList();
|
||||
},
|
||||
/**
|
||||
* @param data {
|
||||
classPage: 0 分类id
|
||||
microPage: 0 微页面id
|
||||
type: 1 0 微页面 1 商品分类
|
||||
}*/
|
||||
bindSortId(data) {
|
||||
this.styleConfig = [];
|
||||
this.loaded = false;
|
||||
if (data.type == 1) {
|
||||
this.getProductList(data.classPage);
|
||||
}else{
|
||||
this.sortList = [];
|
||||
this.getMicroPage(data.microPage, true);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 获取DIY
|
||||
* @param {number} id
|
||||
* @param {boolean} type 区分是否是微页面
|
||||
*/
|
||||
getMicroPage(id, type) {
|
||||
let that = this;
|
||||
that.__logDiy('micro_fetch_start', { id })
|
||||
that.styleConfig = [];
|
||||
that.goodList = [];
|
||||
uni.showLoading({
|
||||
title: '加载中...'
|
||||
});
|
||||
getDiy(id).then(res => {
|
||||
uni.hideLoading();
|
||||
let data = res.data;
|
||||
let diyArr = that.objToArr(res.data.value);
|
||||
that.__logDiy('micro_fetch_ok', { title: data?.title, count: diyArr.length, names: diyArr.map(v => v?.name) })
|
||||
diyArr = diyArr.filter(item => item.isHide !== '1');
|
||||
diyArr.forEach((item,index) => {
|
||||
if(['headerSerch','homeComb'].includes(item.name)){
|
||||
diyArr.splice(index, 1);
|
||||
}
|
||||
});
|
||||
this.styleConfig = diyArr;
|
||||
this.$nextTick(() => {
|
||||
this.__logDiy('micro_render_nextTick', { count: this.styleConfig.length, names: this.styleConfig.map(v => v?.name) })
|
||||
})
|
||||
}).catch(err => {
|
||||
return that.$util.Tips({
|
||||
title: err
|
||||
});
|
||||
uni.hideLoading();
|
||||
});
|
||||
},
|
||||
getProductList(data) {
|
||||
let tempObj = '';
|
||||
this.curSort = 0;
|
||||
this.loaded = false;
|
||||
if (this.sortAll.length > 0) {
|
||||
this.sortAll.forEach((el, index) => {
|
||||
if (el.id == data) {
|
||||
this.$set(this, 'sortList', el);
|
||||
this.sid = el.children.length ? el.children[0].id : '';
|
||||
}
|
||||
});
|
||||
this.goodList = [];
|
||||
this.goodPage = 1;
|
||||
this.$nextTick(() => {
|
||||
if (this.sortList != '') this.getGoodsList();
|
||||
});
|
||||
} else {
|
||||
getCategoryList().then(res => {
|
||||
this.sortAll = res.data;
|
||||
res.data.forEach((el, index) => {
|
||||
if (el.id == data) {
|
||||
this.sortList = el;
|
||||
this.sid = el.children.length ? el.children[0].id : '';
|
||||
}
|
||||
});
|
||||
this.goodList = [];
|
||||
this.goodPage = 1;
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (this.sortList != '') this.getGoodsList();
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
// 商品列表
|
||||
getGoodsList() {
|
||||
if (this.loading || this.loaded) return;
|
||||
this.loading = true;
|
||||
getProductslist({
|
||||
sid: this.sid,
|
||||
keyword: '',
|
||||
priceOrder: '',
|
||||
salesOrder: '',
|
||||
news: 0,
|
||||
page: this.goodPage,
|
||||
limit: 10,
|
||||
cid: this.sortList.id
|
||||
}).then(res => {
|
||||
this.loading = false;
|
||||
this.loaded = res.data.length < 10;
|
||||
this.goodPage++;
|
||||
this.goodList = this.goodList.concat(res.data);
|
||||
});
|
||||
},
|
||||
// #ifdef MP
|
||||
getTemlIds() {
|
||||
let messageTmplIds = wx.getStorageSync(SUBSCRIBE_MESSAGE);
|
||||
if (!messageTmplIds) {
|
||||
getTemlIds().then(res => {
|
||||
if (res.data) wx.setStorageSync(SUBSCRIBE_MESSAGE, JSON.stringify(res.data));
|
||||
});
|
||||
}
|
||||
},
|
||||
// #endif
|
||||
// 对象转数组
|
||||
objToArr(data) {
|
||||
let obj = Object.keys(data).sort();
|
||||
let m = obj.map(key => data[key]);
|
||||
return m;
|
||||
},
|
||||
setDiyData(data) {
|
||||
this.__logDiy('home_setDiyData_start', { title: data?.title })
|
||||
this.errorNetwork = false
|
||||
if (data.is_bg_color) {
|
||||
this.bgColor = data.color_picker
|
||||
}
|
||||
if (data.is_bg_pic) {
|
||||
this.bgPic = data.bg_pic
|
||||
this.bgTabVal = data.bg_tab_val
|
||||
}
|
||||
this.pageShow = data.is_show
|
||||
uni.setNavigationBarTitle({
|
||||
title: data.title
|
||||
});
|
||||
let temp = [];
|
||||
let lastArr = this.objToArr(data.value);
|
||||
lastArr.forEach((item, index, arr) => {
|
||||
if (item.name === 'homeComb') {
|
||||
this.showHomeComb = true
|
||||
this.homeCombData = item;
|
||||
}
|
||||
if (item.name == 'headerSerch') {
|
||||
this.isHeaderSerch = true;
|
||||
this.headerSerchCombData = item;
|
||||
}
|
||||
if (item.name == 'tabNav') {
|
||||
this.showCateNav = true;
|
||||
this.cateNavData = item;
|
||||
}
|
||||
|
||||
if (item.isHide !== '1') {
|
||||
temp.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
function sortNumber(a, b) {
|
||||
return a.timestamp - b.timestamp;
|
||||
}
|
||||
temp.sort(sortNumber)
|
||||
this.styleConfig = temp;
|
||||
this.__logDiy('home_setDiyData_done', { count: this.styleConfig.length, names: this.styleConfig.map(v => v?.name) })
|
||||
this.$nextTick(() => {
|
||||
this.__logDiy('home_render_nextTick', { count: this.styleConfig.length, names: this.styleConfig.map(v => v?.name) })
|
||||
})
|
||||
},
|
||||
getDiyData() {
|
||||
this.__logDiy('home_fetch_start', { id: 0 })
|
||||
getDiy(0).then(res => {
|
||||
this.__logDiy('home_fetch_ok', { title: res?.data?.title, keys: Object.keys(res?.data?.value || {}).length })
|
||||
uni.setStorageSync('diyData', JSON.stringify(res.data));
|
||||
this.setDiyData(res.data);
|
||||
}).catch(error => {
|
||||
// #ifdef APP-PLUS
|
||||
if (error.status) {
|
||||
uni.hideLoading()
|
||||
if (this.errorNetwork) {
|
||||
uni.showToast({
|
||||
title: '请开启网络连接',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
this.errorNetwork = true;
|
||||
}
|
||||
// #endif
|
||||
});
|
||||
},
|
||||
diyData() {
|
||||
let diyData = uni.getStorageSync('diyData');
|
||||
if (diyData) {
|
||||
getDiyVersion(0).then(res => {
|
||||
let diyVersion = uni.getStorageSync('diyVersion');
|
||||
if ((res.data.version + '0') === diyVersion) {
|
||||
this.setDiyData(JSON.parse(diyData));
|
||||
} else {
|
||||
uni.setStorageSync('diyVersion', (res.data.version + '0'));
|
||||
this.getDiyData();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.getDiyData();
|
||||
}
|
||||
},
|
||||
changeLogin() {
|
||||
this.getIsLogin();
|
||||
},
|
||||
getIsLogin() {
|
||||
toLogin()
|
||||
},
|
||||
changeBarg(item) {
|
||||
if (!this.isLogin) {
|
||||
this.getIsLogin();
|
||||
} else {
|
||||
uni.navigateTo({
|
||||
url: `/pages/activity/goods_bargain_details/index?id=${item.id}&spid=${this.$store.state.app.uid}`
|
||||
});
|
||||
}
|
||||
},
|
||||
goDetail(item) {
|
||||
goShopDetail(item, this.$store.state.app.uid).catch(res => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/goods_details/index?id=${item.id}`
|
||||
});
|
||||
});
|
||||
},
|
||||
newDataStatus(val) {
|
||||
this.isFooter = val ? true : false;
|
||||
},
|
||||
checked(){
|
||||
console.log('点击了')
|
||||
},
|
||||
// #ifdef H5
|
||||
// 微信分享;
|
||||
setOpenShare: function() {
|
||||
let that = this;
|
||||
let uid = this.uid ? this.uid : 0;
|
||||
if (that.$wechat.isWeixin()) {
|
||||
getShare().then(res => {
|
||||
let data = res.data;
|
||||
let configAppMessage = {
|
||||
desc: data.synopsis,
|
||||
title: data.title,
|
||||
link: location.href + '?spid=' + uid,
|
||||
imgUrl: data.img
|
||||
};
|
||||
that.$wechat.wechatEvevt(['updateAppMessageShareData', 'updateTimelineShareData',
|
||||
'onMenuShareAppMessage', 'onMenuShareTimeline'
|
||||
],
|
||||
configAppMessage);
|
||||
});
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
onReachBottom() {
|
||||
if(this.goodList.length){
|
||||
this.getGoodsList();
|
||||
}
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
this.goodList = [];
|
||||
this.sortList = [];
|
||||
this.diyData();
|
||||
this.pullReset = 1;
|
||||
setTimeout(function () {
|
||||
uni.stopPullDownRefresh();
|
||||
}, 1000);
|
||||
},
|
||||
onPageScroll(e) {
|
||||
// #ifdef H5
|
||||
if (this.isHeaderSerch) {
|
||||
if (e.scrollTop > this.domOffsetTop) {
|
||||
this.isFixed = true;
|
||||
}
|
||||
if (e.scrollTop < this.domOffsetTop) {
|
||||
this.$nextTick(() => {
|
||||
this.isFixed = false;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.isFixed = false
|
||||
}
|
||||
// #endif
|
||||
if (e.scrollTop > 10) {
|
||||
this.isScrolled = true;
|
||||
} else {
|
||||
this.isScrolled = false;
|
||||
}
|
||||
uni.$emit('scroll');
|
||||
uni.$emit('onPageScroll', e.scrollTop);
|
||||
},
|
||||
//#ifdef MP
|
||||
onShareAppMessage() {
|
||||
let uid = this.uid ? this.uid : 0;
|
||||
if(this.shareInfo.img){
|
||||
return {
|
||||
title: this.shareInfo.title,
|
||||
path: '/pages/index/index?spid=' + uid,
|
||||
imageUrl: this.shareInfo.img,
|
||||
desc: this.shareInfo.synopsis
|
||||
};
|
||||
}else{
|
||||
return {
|
||||
title: this.shareInfo.title,
|
||||
path: '/pages/index/index?spid=' + uid
|
||||
// imageUrl: this.shareInfo.img,
|
||||
// desc: this.shareInfo.synopsis
|
||||
};
|
||||
}
|
||||
|
||||
},
|
||||
//分享到朋友圈
|
||||
onShareTimeline() {
|
||||
let uid = this.uid ? this.uid : 0;
|
||||
return {
|
||||
title: this.shareInfo.title,
|
||||
path: '/pages/index/index?spid=' + uid,
|
||||
imageUrl: this.shareInfo.img,
|
||||
desc: this.shareInfo.synopsis
|
||||
};
|
||||
}
|
||||
//#endif
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
// page {
|
||||
// padding-bottom: 50px;
|
||||
// }
|
||||
.pictrue_log_class {
|
||||
background-color: var(--view-theme);
|
||||
}
|
||||
|
||||
.ysize {
|
||||
background-size: 100%;
|
||||
}
|
||||
|
||||
.fullsize {
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.repeat {
|
||||
background-repeat: repeat;
|
||||
}
|
||||
|
||||
.noRepeat {
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.o813d0 {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-top: 40rpx;
|
||||
background: #fff;
|
||||
|
||||
image {
|
||||
width: 414rpx;
|
||||
height: 336rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
position: relative;
|
||||
top: -40rpx;
|
||||
font-size: 32rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.of0b21 {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
|
||||
.label {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.item {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 508rpx;
|
||||
height: 86rpx;
|
||||
margin-top: 100rpx;
|
||||
border: 1px solid #D74432;
|
||||
color: #E93323;
|
||||
font-size: 30rpx;
|
||||
border-radius: 120rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.sort-scroll {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.sort-product {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.site-config {
|
||||
margin: 40rpx 0;
|
||||
font-size: 24rpx;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
|
||||
&.fixed {
|
||||
position: fixed;
|
||||
bottom: 69px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.select{
|
||||
border: 1px solid var(--view-theme);
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"projectname": "crmeb_pro_uniapp",
|
||||
"setting": {
|
||||
"compileHotReLoad": false
|
||||
"compileHotReLoad": false,
|
||||
"urlCheck": false
|
||||
},
|
||||
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html"
|
||||
}
|
||||
Reference in New Issue
Block a user