Compare commits
7 Commits
f3648d5cef
...
feat/app-a
| Author | SHA1 | Date | |
|---|---|---|---|
| a0f710c1a8 | |||
| 53a43ea18c | |||
| 237320c7a1 | |||
| 6df7640272 | |||
| 2356f33b47 | |||
| e9b85e9e61 | |||
| a6340250f2 |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -25,3 +25,10 @@ unpackage
|
|||||||
project.config.json
|
project.config.json
|
||||||
.hbuilderx
|
.hbuilderx
|
||||||
/config/app.js
|
/config/app.js
|
||||||
|
|
||||||
|
# Android 签名证书(私钥,禁止入库;见 manifest.json app-plus.distribute.android.keystore)
|
||||||
|
keystore/
|
||||||
|
|
||||||
|
# 本地备份文件(历史页面/配置快照),不入库
|
||||||
|
*.bak*
|
||||||
|
*.origin-bak
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ CRMEB 商城小程序(uniapp,manifest `name=DoubaShop`),同时是**整
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `pages/index/index.vue` | 首页壳:身份 fail-closed、加载 mama-box、**冷启动恢复 last_app**、分享卡片、tabBar 显隐 |
|
| `pages/index/index.vue` | 首页壳:身份 fail-closed、加载 mama-box、**冷启动恢复 last_app**、分享卡片、tabBar 显隐 |
|
||||||
| `pages/annex/web_view/index.vue` | 容器页:太阳码 scene 解析、身份注入(query)、`_reportLastApp` 上报 |
|
| `pages/annex/web_view/index.vue` | 容器页:太阳码 scene 解析、身份注入(query)、`_reportLastApp` 上报 |
|
||||||
|
| `pages/annex/link_view/index.vue` | **链接型壳页**(2026-11-04):链接系统 / 福利系统的小程序码落地。scene=`l=<slug>` / `f=<slug>` 或转发 `?t=&k=` → 按硬编码的 `TARGETS` 拼 `wenda.douba.top/s/`、`fuli.douba.top/a/` 用 web-view 打开;**不注入身份、不引 mixin**;解析不出来走兜底屏(见 §3.9) |
|
||||||
| `mixins/webviewHost.js` | 宿主公共能力:postMessage 分发(location/share/copyLink/setTitle/requestProfile)、原生授权面板、授权回传、静默登录复用 |
|
| `mixins/webviewHost.js` | 宿主公共能力:postMessage 分发(location/share/copyLink/setTitle/requestProfile)、原生授权面板、授权回传、静默登录复用 |
|
||||||
| `mixins/shareCard.js` | 分享图:按当前 H5 URL 反查 box_app App 表;朋友圈本地图转换 |
|
| `mixins/shareCard.js` | 分享图:按当前 H5 URL 反查 box_app App 表;朋友圈本地图转换 |
|
||||||
| `App.vue` | 冷启动静默登录、`globalData`(`spid` / `appShowAt`)、`onShow`(前台化时间戳) |
|
| `App.vue` | 冷启动静默登录、`globalData`(`spid` / `appShowAt`)、`onShow`(前台化时间戳) |
|
||||||
@@ -46,6 +47,11 @@ CRMEB 商城小程序(uniapp,manifest `name=DoubaShop`),同时是**整
|
|||||||
6. **tabBar 是全局状态**:首页壳必须 `uni.hideTabBar()`(web-view 盖不住原生条,H5 自带底栏会双条);**严禁在商城页主动 `showTabBar` 配对** —— 会与 DIY 底栏同显成双 tabBar。
|
6. **tabBar 是全局状态**:首页壳必须 `uni.hideTabBar()`(web-view 盖不住原生条,H5 自带底栏会双条);**严禁在商城页主动 `showTabBar` 配对** —— 会与 DIY 底栏同显成双 tabBar。
|
||||||
7. **恢复/落地统一 `navigateTo` 容器页**,并保留首页壳作为返回落点;不要直接把首页壳的 web-view src 指向第三方应用(首页壳的 `buildWebUrl` 是 **hash** 注入,会破坏自带 hash 路由的应用)。
|
7. **恢复/落地统一 `navigateTo` 容器页**,并保留首页壳作为返回落点;不要直接把首页壳的 web-view src 指向第三方应用(首页壳的 `buildWebUrl` 是 **hash** 注入,会破坏自带 hash 路由的应用)。
|
||||||
8. **只用 `options.url` 当显式落地判据**:`options.scene` **每次冷启动都有值**(1001 最近使用 / 1007 分享 / 1011 扫码…),拿它判落地会导致恢复永远不执行。
|
8. **只用 `options.url` 当显式落地判据**:`options.scene` **每次冷启动都有值**(1001 最近使用 / 1007 分享 / 1011 扫码…),拿它判落地会导致恢复永远不执行。
|
||||||
|
9. **链接型壳页 `pages/annex/link_view` 的三条同步契约**(2026-11-04,方案见 box_app `docs/lianjie-wxacode-plan.md`):
|
||||||
|
① 页内 `TARGETS`(短键 / 地址前缀 / 标识正则)必须与 box_app `backend/app/services/link_wxacode_serv.py` 的同名表**逐字一致**,改表 = 两端一起改 + 小程序发版;
|
||||||
|
② 页面路径必须等于 box_app 生产 `.env` 的 `LINK_WXACODE_PAGE`(默认 `pages/annex/link_view/index`),否则已生成的码全部扫不进页面;
|
||||||
|
③ 该页**故意不注入 openid/unionid**(两家 H5 走自己的公众号网页授权),所以不要往里加 `webviewHost` / `shareCard`,也不要往 URL 上带身份参数。
|
||||||
|
`wenda.douba.top`、`fuli.douba.top`、`dbh5.ywsz.com.cn` 三个域名必须在微信后台「业务域名」里,**体验版真机同样校验**(漏一个 = 部分活动白屏)。
|
||||||
|
|
||||||
## 4. 踩坑:宿主生命周期(2026-09-22,都付过线上代价)
|
## 4. 踩坑:宿主生命周期(2026-09-22,都付过线上代价)
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
{
|
{
|
||||||
"version" : "1",
|
"version": "1",
|
||||||
"prompt" : "template",
|
"prompt": "template",
|
||||||
"title" : "用户协议与隐私政策",
|
"title": "用户协议与隐私政策",
|
||||||
"message" : "\t请务必审慎阅读、充分理解“用户协议与 隐私政策”各条款,包括但不限于:为了 向你提供即时通讯、内容分享等服务,我 们需要收集你的设备信息、操作日志等个 人信息。你可以在“设置”中查看、变更、删除个人信息并管理你的授权。<br/>
|
"message": "\t请务必审慎阅读、充分理解“用户协议与 隐私政策”各条款,包括但不限于:为了 向你提供即时通讯、内容分享等服务,我 们需要收集你的设备信息、操作日志等个 人信息。你可以在“设置”中查看、变更、删除个人信息并管理你的授权。<br/>你可以阅读 <a href=\"pages/columnGoods/static/yhxy.html\">《用户协议》</a>与 <a href=\"pages/columnGoods/static/yszc.html\">《隐私政策》</a>了解详细信息。如你同意,请点击“我同意”开始接受我们的服务。",
|
||||||
你可以阅读 <a href=\"pages/columnGoods/static/yhxy.html\">《用户协议》</a>与 <a href=\"pages/columnGoods/static/yszc.html\">《隐私政策》</a>了解详细信息。如你同意,请点击“我同意”开始接受我们的服务。",
|
"buttonAccept": "同意并接受",
|
||||||
"buttonAccept" : "同意并接受",
|
"buttonRefuse": "暂不同意",
|
||||||
"buttonRefuse" : "暂不同意",
|
"backToExit": "true",
|
||||||
"second" : {
|
"second": {
|
||||||
"title" : "确认提示",
|
"title": "确认提示",
|
||||||
"message" : "进入应用前,你需先同意<a href=\"pages/columnGoods/static/yhxy.html\">《用户协议》</a>与<a href=\"pages/columnGoods/static/yszc.html\">《隐私政策》</a>,否则将退出应用。",
|
"message": "进入应用前,你需先同意<a href=\"pages/columnGoods/static/yhxy.html\">《用户协议》</a>与<a href=\"pages/columnGoods/static/yszc.html\">《隐私政策》</a>,否则将退出应用。",
|
||||||
"buttonAccept" : "同意并继续",
|
"buttonAccept": "同意并继续",
|
||||||
"buttonRefuse" : "退出应用"
|
"buttonRefuse": "退出应用"
|
||||||
},
|
},
|
||||||
"styles" : {
|
"styles": {
|
||||||
"backgroundColor" : "#fff",
|
"backgroundColor": "#fff",
|
||||||
"borderRadius" : "5px",
|
"borderRadius": "5px",
|
||||||
"title" : {
|
"title": {
|
||||||
"color" : "#000"
|
"color": "#000"
|
||||||
},
|
},
|
||||||
"buttonAccept" : {
|
"buttonAccept": {
|
||||||
"color" : "#fff"
|
"color": "#fff"
|
||||||
},
|
},
|
||||||
"buttonRefuse" : {
|
"buttonRefuse": {
|
||||||
"color" : "#ccc"
|
"color": "#ccc"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
api/user.js
13
api/user.js
@@ -900,4 +900,17 @@ export function channelApplyInfoApi() {
|
|||||||
// 采购商切换身份
|
// 采购商切换身份
|
||||||
export function checkIdentityApi(data) {
|
export function checkIdentityApi(data) {
|
||||||
return request.post(`user/switch_identity`, data);
|
return request.post(`user/switch_identity`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- App 身份(2026-10-26,Android 一期) ----------
|
||||||
|
宿主在短信登录后调用:
|
||||||
|
appIdentity() 取规范 openid(routine 优先 → app → unionid 反查)
|
||||||
|
appBind({ code }) 新用户:uni.login 拿 code → 绑定到当前 uid
|
||||||
|
两个接口都要求登录态(request 自动带 Authori-zation)。 */
|
||||||
|
export function appIdentity() {
|
||||||
|
return request.get('wechat/app_identity');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function appBind(data) {
|
||||||
|
return request.post('wechat/app_bind', data);
|
||||||
}
|
}
|
||||||
30
config/legal.js
Normal file
30
config/legal.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | 合规展示信息(主体 / 备案号)
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | 用途:小米、华为等商店硬性要求「应用内可见 APP 备案号」(截图要体现),
|
||||||
|
// | 而备案号在开发期还不存在 —— 统一收在这一个文件,号下来只改这里。
|
||||||
|
// |
|
||||||
|
// | 为什么不放 config/app.js:那个文件在 .gitignore 里(本地私有配置),
|
||||||
|
// | 放进去等于不进仓库、换个环境就丢;也不写死在页面里:同一个号要出现在
|
||||||
|
// | 设置页与登录页两处,写死两份必然不同步。
|
||||||
|
// |
|
||||||
|
// | 使用位置:
|
||||||
|
// | pages/users/user_set/index.vue 「我的 → 设置」底部的关于区块
|
||||||
|
// | pages/users/login/index.vue 登录页页脚(未登录也能看到)
|
||||||
|
// | ⚠️ 两处展示都包在 #ifdef APP-PLUS 里:小程序 / H5 的界面与行为不受影响。
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
/**
|
||||||
|
* APP 备案号:ICP 备案号 + 「-序号A」后缀,例:京ICP备17065521号-3A
|
||||||
|
* 空字符串 = 不展示该行(备案号下发前保持为空,避免截图里出现占位文案被商店挑刺)
|
||||||
|
* ⚠️ 号码下发后填这里 → 重新出包;不要去改页面。
|
||||||
|
*/
|
||||||
|
APP_BEIAN_NO: '',
|
||||||
|
|
||||||
|
/** 网站/主体 ICP 备案号(已有,官网与小程序在用) */
|
||||||
|
WEBSITE_ICP_NO: '京ICP备17065521号',
|
||||||
|
|
||||||
|
/** 备案主体 = 软著著作权人(北京臻惠网络科技有限公司)—— 三处必须一致,改名要同步软著与备案 */
|
||||||
|
SUBJECT: '北京臻惠网络科技有限公司'
|
||||||
|
};
|
||||||
@@ -8,6 +8,11 @@
|
|||||||
"sassImplementationName" : "node-sass",
|
"sassImplementationName" : "node-sass",
|
||||||
/* 5+App特有相关 */
|
/* 5+App特有相关 */
|
||||||
"app-plus" : {
|
"app-plus" : {
|
||||||
|
/* 隐私政策提示框开关:template = 启动时在 splash 层弹原生隐私框(上架硬性要求)。
|
||||||
|
提示框内容/样式在同目录 androidPrivacy.json,两者必须同时存在且 JSON 合法。 */
|
||||||
|
"privacy" : {
|
||||||
|
"prompt" : "template"
|
||||||
|
},
|
||||||
"usingComponents" : true,
|
"usingComponents" : true,
|
||||||
"nvueCompiler" : "uni-app",
|
"nvueCompiler" : "uni-app",
|
||||||
"nvueStyleCompiler" : "uni-app",
|
"nvueStyleCompiler" : "uni-app",
|
||||||
@@ -19,6 +24,11 @@
|
|||||||
"distribute" : {
|
"distribute" : {
|
||||||
/* android打包配置 */
|
/* android打包配置 */
|
||||||
"android" : {
|
"android" : {
|
||||||
|
"packagename" : "com.yizhezhe.douba",
|
||||||
|
"keystore" : "keystore/douba-shop.keystore",
|
||||||
|
"password" : "douba@2026",
|
||||||
|
"aliasname" : "douba",
|
||||||
|
"schemes" : "douba",
|
||||||
"permissions" : [
|
"permissions" : [
|
||||||
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||||
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||||
@@ -26,24 +36,18 @@
|
|||||||
"<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
|
"<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
|
||||||
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||||
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||||
"<uses-permission android:name=\"android.permission.CALL_PHONE\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||||
"<uses-permission android:name=\"android.permission.CAPTURE_AUDIO_OUTPUT\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||||
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||||
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||||
"<uses-permission android:name=\"android.permission.INTERNET\"/>",
|
"<uses-permission android:name=\"android.permission.INTERNET\"/>",
|
||||||
"<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
|
"<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
|
||||||
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
|
||||||
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
|
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
|
||||||
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||||
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||||
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>",
|
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>"
|
||||||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
|
||||||
],
|
],
|
||||||
"abiFilters" : [ "armeabi-v7a" ],
|
"abiFilters" : [ "armeabi-v7a", "arm64-v8a" ],
|
||||||
"permissionExternalStorage" : {
|
"permissionExternalStorage" : {
|
||||||
"request" : "none",
|
"request" : "none",
|
||||||
"prompt" : "应用保存运行状态等信息,需要获取读写手机存储(系统提示为访问设备上的照片、媒体内容和文件)权限,请允许。"
|
"prompt" : "应用保存运行状态等信息,需要获取读写手机存储(系统提示为访问设备上的照片、媒体内容和文件)权限,请允许。"
|
||||||
@@ -53,8 +57,8 @@
|
|||||||
"prompt" : "为保证您正常、安全地使用,需要获取设备识别码(部分手机提示为获取手机号码)使用权限,请允许"
|
"prompt" : "为保证您正常、安全地使用,需要获取设备识别码(部分手机提示为获取手机号码)使用权限,请允许"
|
||||||
},
|
},
|
||||||
"minSdkVersion" : 21,
|
"minSdkVersion" : 21,
|
||||||
"targetSdkVersion" : 30, // 建议设置为 30 或更高
|
"targetSdkVersion" : 34, // 商店普遍要求 30+,34 为稳妥档(36 需同步验证);API 34 起前台服务/权限更严
|
||||||
"compileSdkVersion" : 30
|
"compileSdkVersion" : 36 // HBuilderX 4.81+ 适配 16KB page size 要求 36
|
||||||
},
|
},
|
||||||
/* ios打包配置 */
|
/* ios打包配置 */
|
||||||
"ios" : {
|
"ios" : {
|
||||||
@@ -82,8 +86,8 @@
|
|||||||
"payment" : {
|
"payment" : {
|
||||||
"weixin" : {
|
"weixin" : {
|
||||||
"__platform__" : [ "ios", "android" ],
|
"__platform__" : [ "ios", "android" ],
|
||||||
"appid" : "wx3e14a895971fbea6",
|
"appid" : "wxbe5081d4835af21b",
|
||||||
"UniversalLinks" : "https://pro.crmeb.net/uni-universallinks/__UNI__7E0054B/"
|
"UniversalLinks" : "https://mp.yizhezhe.com/uni-universallinks/__UNI__52BA861/"
|
||||||
},
|
},
|
||||||
"alipay" : {
|
"alipay" : {
|
||||||
"__platform__" : [ "ios", "android" ]
|
"__platform__" : [ "ios", "android" ]
|
||||||
@@ -91,8 +95,8 @@
|
|||||||
},
|
},
|
||||||
"share" : {
|
"share" : {
|
||||||
"weixin" : {
|
"weixin" : {
|
||||||
"appid" : "wx3e14a895971fbea6",
|
"appid" : "wxbe5081d4835af21b",
|
||||||
"UniversalLinks" : "https://pro.crmeb.net/uni-universallinks/__UNI__7E0054B/"
|
"UniversalLinks" : "https://mp.yizhezhe.com/uni-universallinks/__UNI__52BA861/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"push" : {},
|
"push" : {},
|
||||||
@@ -104,9 +108,9 @@
|
|||||||
},
|
},
|
||||||
"oauth" : {
|
"oauth" : {
|
||||||
"weixin" : {
|
"weixin" : {
|
||||||
"appid" : "wx3e14a895971fbea6",
|
"appid" : "wxbe5081d4835af21b",
|
||||||
"appsecret" : "ee83a6c94edbc5dd7a5b4614316d85a7",
|
"appsecret" : "5dafb0ed5b106eb8f56f70824893f7ec",
|
||||||
"UniversalLinks" : "https://pro.crmeb.net/uni-universallinks/__UNI__7E0054B/"
|
"UniversalLinks" : "https://mp.yizhezhe.com/uni-universallinks/__UNI__52BA861/"
|
||||||
},
|
},
|
||||||
"apple" : {}
|
"apple" : {}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -132,7 +132,78 @@ export default {
|
|||||||
{auth_login:1, openid, unionid}(新用户,无 token)或 {token, ...}(老用户)
|
{auth_login:1, openid, unionid}(新用户,无 token)或 {token, ...}(老用户)
|
||||||
两端都算成功,只要拿到 openid 就落到 store + 缓存。
|
两端都算成功,只要拿到 openid 就落到 store + 缓存。
|
||||||
返回 Promise<{openid, unionid}>;拿不到就 resolve 空串,由调用方决定是否匿名加载。 */
|
返回 Promise<{openid, unionid}>;拿不到就 resolve 空串,由调用方决定是否匿名加载。 */
|
||||||
|
/* ============================================================
|
||||||
|
App(Android 一期)身份链路 —— 2026-10-26
|
||||||
|
------------------------------------------------------------
|
||||||
|
App 没有 wx.login / openid;本地身份来源是「短信登录后的 uid」:
|
||||||
|
① 有 token → GET /api/wechat/app_identity(routine openid 优先)
|
||||||
|
② 没有 openid(全新用户)→ uni.login({provider:'weixin'}) 拿 code
|
||||||
|
→ POST /api/wechat/app_bind 绑定到当前 uid → 再取 app_identity
|
||||||
|
③ 没装微信/拒绝授权 → 返回空,页面给「登录后继续 / 重试」
|
||||||
|
⚠️ 不在启动时弹微信:只有走到这里(且确实没有 openid)才授权。
|
||||||
|
============================================================ */
|
||||||
|
_appIdentityFlow() {
|
||||||
|
const needLogin = { openid: '', unionid: '', needLogin: true };
|
||||||
|
const empty = { openid: '', unionid: '', needLogin: false };
|
||||||
|
let hasToken = false;
|
||||||
|
try { hasToken = !!(store.state.app.token || uni.getStorageSync(LOGIN_STATUS)); } catch (e) { hasToken = !!store.state.app.token; }
|
||||||
|
if (!hasToken) return Promise.resolve(needLogin);
|
||||||
|
|
||||||
|
const save = (openid, unionid) => {
|
||||||
|
if (!openid) return empty;
|
||||||
|
try { store.commit('SET_OPENID', openid); uni.setStorageSync('OPENID', openid); } catch (e) {}
|
||||||
|
if (unionid) {
|
||||||
|
try { store.commit('SET_UNIONID', unionid); uni.setStorageSync('UNIONID', unionid); } catch (e) {}
|
||||||
|
}
|
||||||
|
return { openid: openid, unionid: unionid || '', needLogin: false };
|
||||||
|
};
|
||||||
|
const read = (res) => {
|
||||||
|
const d = (res && res.data) || {};
|
||||||
|
return { openid: d.openid || '', unionid: d.unionid || '', source: d.source || '' };
|
||||||
|
};
|
||||||
|
return import('@/api/user.js').then((api) => {
|
||||||
|
return api.appIdentity().then((res) => {
|
||||||
|
const id = read(res);
|
||||||
|
console.log('[auth-flow] App app_identity: openid=' + (id.openid ? '有' : '无') + ' source=' + (id.source || '?'));
|
||||||
|
if (id.openid) return save(id.openid, id.unionid);
|
||||||
|
return this._weixinLoginCode().then((code) => {
|
||||||
|
if (!code) {
|
||||||
|
console.warn('[auth-flow] App:未拿到微信 code(未安装微信/拒绝授权)');
|
||||||
|
return empty;
|
||||||
|
}
|
||||||
|
return api.appBind({ code: code }).then(() => {
|
||||||
|
return api.appIdentity().then((r2) => {
|
||||||
|
const id2 = read(r2);
|
||||||
|
console.log('[auth-flow] App app_bind 后身份: openid=' + (id2.openid ? '有' : '无'));
|
||||||
|
return save(id2.openid, id2.unionid);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).catch((err) => {
|
||||||
|
console.warn('[auth-flow] App 身份解析失败:', err && (err.message || err));
|
||||||
|
return empty;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
_weixinLoginCode() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
uni.login({
|
||||||
|
provider: 'weixin',
|
||||||
|
success: (r) => resolve((r && r.code) || ''),
|
||||||
|
fail: (e) => { console.warn('[auth-flow] uni.login(weixin) 失败:', e); resolve(''); }
|
||||||
|
});
|
||||||
|
// #endif
|
||||||
|
// #ifndef APP-PLUS
|
||||||
|
resolve('');
|
||||||
|
// #endif
|
||||||
|
});
|
||||||
|
},
|
||||||
_silenceAuthOpenid() {
|
_silenceAuthOpenid() {
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
/* App:走短信登录 + app_identity / app_bind(不碰 wx.login) */
|
||||||
|
return this._appIdentityFlow();
|
||||||
|
// #endif
|
||||||
const empty = { openid: '', unionid: '' };
|
const empty = { openid: '', unionid: '' };
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
/* ⚠️ 保险丝:wx.login / 接口有可能长时间不回调(弱网、域名没配好)。
|
/* ⚠️ 保险丝:wx.login / 接口有可能长时间不回调(弱网、域名没配好)。
|
||||||
@@ -253,11 +324,18 @@ export default {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'requestProfile':
|
case 'requestProfile':
|
||||||
|
/* App:没有小程序授权页,也不该弹页内面板(web-view 是原生组件、会盖住)。
|
||||||
|
直接用当前 openid 查共享资料,把 hash 写回 web-view 即可。 */
|
||||||
|
// #ifdef APP-PLUS
|
||||||
|
this._pushAppProfile(m.href);
|
||||||
|
// #endif
|
||||||
|
// #ifndef APP-PLUS
|
||||||
this.showAuth = true;
|
this.showAuth = true;
|
||||||
/* 面板回传基址:H5 在消息里带 href(发起面板时的当前地址)→ 提交后
|
/* 面板回传基址:H5 在消息里带 href(发起面板时的当前地址)→ 提交后
|
||||||
按它回传。消息送达=面板弹出,href 同一条消息到达,确定性强于
|
按它回传。消息送达=面板弹出,href 同一条消息到达,确定性强于
|
||||||
赌 shareHref 的 postMessage 派发时机(宫格换应用后可能还是旧地址)。 */
|
赌 shareHref 的 postMessage 派发时机(宫格换应用后可能还是旧地址)。 */
|
||||||
if (m.href) this._panelBaseUrl = m.href;
|
if (m.href) this._panelBaseUrl = m.href;
|
||||||
|
// #endif
|
||||||
break;
|
break;
|
||||||
case 'setTitle':
|
case 'setTitle':
|
||||||
if (m.title) {
|
if (m.title) {
|
||||||
@@ -312,6 +390,42 @@ export default {
|
|||||||
'&avatar=' + encodeURIComponent(r.avatar || '');
|
'&avatar=' + encodeURIComponent(r.avatar || '');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/* App:H5 发 requestProfile 时,直接用当前 openid 查共享资料并回传 hash。
|
||||||
|
—— 不弹面板、不调 getPhoneNumber(短信登录已验证手机号);
|
||||||
|
—— 也绕开「App 的 web-view 是原生组件、页内面板会被盖住」的问题。 */
|
||||||
|
_pushAppProfile(href) {
|
||||||
|
const openid = this._hostOpenid();
|
||||||
|
if (!openid) return uni.showToast({ title: '未获得 openid,请先登录', icon: 'none' });
|
||||||
|
const current = (typeof this._hostCurrentUrl === 'function') ? this._hostCurrentUrl() : '';
|
||||||
|
const base = String(href || this.shareHref || current || '').split('#')[0];
|
||||||
|
if (!base) return;
|
||||||
|
uni.showLoading({ title: '获取资料中', mask: true });
|
||||||
|
uni.request({
|
||||||
|
url: SHARED_AUTH_API + '?openid=' + encodeURIComponent(openid),
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 10000,
|
||||||
|
success: (r) => {
|
||||||
|
uni.hideLoading();
|
||||||
|
const d = r.data || {};
|
||||||
|
if (d.code === 200) {
|
||||||
|
const res = d.result || {};
|
||||||
|
this._hostSetWebviewUrl(this.hostAuthUrl(base, {
|
||||||
|
openid: openid,
|
||||||
|
phone: res.phone || '',
|
||||||
|
nickname: res.userName || '',
|
||||||
|
avatar: res.avatar || ''
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: d.message || '资料获取失败', icon: 'none' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
uni.hideLoading();
|
||||||
|
uni.showToast({ title: '资料请求失败,请检查网络', icon: 'none' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 读取独立授权页(pages/annex/auth/index)写下的 __AUTH_RESULT__。
|
* 读取独立授权页(pages/annex/auth/index)写下的 __AUTH_RESULT__。
|
||||||
* 该页成功后 navigateBack 回到宿主页 —— 两个宿主页都必须在 onShow 调用本方法,
|
* 该页成功后 navigateBack 回到宿主页 —— 两个宿主页都必须在 onShow 调用本方法,
|
||||||
|
|||||||
@@ -1088,6 +1088,11 @@
|
|||||||
// #endif
|
// #endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}, {
|
||||||
|
"path": "link_view/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "兜爸心选"
|
||||||
|
}
|
||||||
}, {
|
}, {
|
||||||
"path": "auth/index",
|
"path": "auth/index",
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
137
pages/annex/link_view/index.vue
Normal file
137
pages/annex/link_view/index.vue
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
<!--
|
||||||
|
小程序专用 web-view 壳页(2026-11-04)—— 只给「链接系统 / 福利系统」的小程序码用
|
||||||
|
方案:box_app/docs/lianjie-wxacode-plan.md(v4 硬编码,第四节之一 4.2)
|
||||||
|
|
||||||
|
入口:
|
||||||
|
1) 扫小程序码:options.scene = "l=g8I0yMDYzlCu" / "f=zanxte3x"(微信会 URL 编码 scene)
|
||||||
|
2) 转发卡片: /pages/annex/link_view/index?t=l&k=g8I0yMDYzlCu
|
||||||
|
|
||||||
|
只做三件事:解析 scene 或转发参数 → 拼地址 → web-view 打开;解析不出来显示兜底屏。
|
||||||
|
|
||||||
|
⚠️ TARGETS 必须和 box_app backend/app/services/link_wxacode_serv.py 的同名表一致
|
||||||
|
(短键、地址前缀、标识规则)。改这张表 = 两端一起改 + 小程序发版;
|
||||||
|
码里只有标识,所以发版后旧码照常可用。
|
||||||
|
⚠️ 不注入任何小程序身份(openid/unionid),也不反查分享图/标题:两家都走自己的
|
||||||
|
公众号网页授权(方案第五节),所以本页**不引入** webviewHost / shareCard mixin。
|
||||||
|
⚠️ 本页路径必须等于 box_app 生产 .env 的 LINK_WXACODE_PAGE(默认就是这个路径),
|
||||||
|
改路径要同步改那边,否则已生成的码全部扫不进页面。
|
||||||
|
⚠️ web-view 打开的域名必须在小程序后台「业务域名」里(方案 4.4):
|
||||||
|
wenda.douba.top / fuli.douba.top / dbh5.ywsz.com.cn。
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<view>
|
||||||
|
<web-view v-if="url" :src="url"></web-view>
|
||||||
|
<view v-else class="lv-fallback">
|
||||||
|
<view class="lv-emoji">🔗</view>
|
||||||
|
<view class="lv-title">链接已失效</view>
|
||||||
|
<view class="lv-desc">这个码可能已过期,或者链接已下架</view>
|
||||||
|
<button class="lv-btn" hover-class="lv-btn-hover" @tap="goHome">去首页</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/* 目标表(方案第三节):短键 → 地址前缀 + 标识规则。
|
||||||
|
base 写死在小程序里:换域名或新增系统要发版过审,这一点在方案第八节已知。 */
|
||||||
|
const TARGETS = {
|
||||||
|
l: { base: 'https://wenda.douba.top/s/', re: /^[0-9A-Za-z]{1,30}$/, title: '兜爸心选 · 好物推荐' },
|
||||||
|
f: { base: 'https://fuli.douba.top/a/', re: /^[a-z0-9-]{1,30}$/, title: '兜爸心选 · 福利活动' },
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 微信给小程序的 scene 是 URL 编码的("l%3Dslug"),个别机型/版本还会二次编码。
|
||||||
|
必须 try/catch:裸 '%' 会让 decodeURIComponent 抛错,直接冲掉 onLoad → 白屏。
|
||||||
|
解不开就用原文,后面正则校验会把它挡成兜底屏,绝不会把野值塞进 web-view。 */
|
||||||
|
function decodeScene(raw) {
|
||||||
|
let s = String(raw == null ? '' : raw)
|
||||||
|
for (let i = 0; i < 2 && s.indexOf('%') !== -1; i++) {
|
||||||
|
try {
|
||||||
|
const next = decodeURIComponent(s)
|
||||||
|
if (next === s) break
|
||||||
|
s = next
|
||||||
|
} catch (e) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
/* "l=slug&ch=XX" → { l: 'slug', ch: 'XX' }
|
||||||
|
按第一个 '=' 切,值里再出现 '=' 也不会被截断。 */
|
||||||
|
function parseScene(raw) {
|
||||||
|
const out = {}
|
||||||
|
String(raw || '').split('&').forEach((kv) => {
|
||||||
|
const i = kv.indexOf('=')
|
||||||
|
if (i > 0) out[kv.slice(0, i)] = kv.slice(i + 1)
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 返回 { t, k, url };返回 null 表示走兜底屏。
|
||||||
|
优先级:显式 t/k(转发落地)> scene(扫码)——同一条码两种情况都要能用。 */
|
||||||
|
function resolveTarget(options) {
|
||||||
|
const opt = options || {}
|
||||||
|
let t = opt.t ? String(opt.t) : ''
|
||||||
|
let k = opt.k ? String(opt.k) : ''
|
||||||
|
if (!t && opt.scene) {
|
||||||
|
const sc = parseScene(decodeScene(opt.scene))
|
||||||
|
for (const key in TARGETS) {
|
||||||
|
if (sc[key]) {
|
||||||
|
t = key
|
||||||
|
k = sc[key]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const target = TARGETS[t]
|
||||||
|
if (!target || !k || !target.re.test(k)) return null
|
||||||
|
return { t, k, url: target.base + k }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
url: '', // 为空 = 兜底屏(web-view 不渲染)
|
||||||
|
t: '', // 短键(分享时回填)
|
||||||
|
k: '', // 标识(分享时回填)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad(options) {
|
||||||
|
const opt = options || {}
|
||||||
|
const hit = resolveTarget(opt)
|
||||||
|
if (!hit) {
|
||||||
|
// 真机排查用:一条日志区分「码/参数不对」和「域名没加业务域名」
|
||||||
|
console.warn('[link-view] 标识不合法,显示兜底屏: scene=' + (opt.scene || '') +
|
||||||
|
' t=' + (opt.t || '') + ' k=' + (opt.k || ''))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.t = hit.t
|
||||||
|
this.k = hit.k
|
||||||
|
this.url = hit.url
|
||||||
|
console.log('[link-view] scene=' + (opt.scene || '') + ' 短键=' + hit.t +
|
||||||
|
' 标识=' + hit.k + ' → ' + hit.url)
|
||||||
|
},
|
||||||
|
onShareAppMessage() {
|
||||||
|
// 转发出去还是这个壳页:好友点开按 t/k 重新拼地址,不带上任何身份参数
|
||||||
|
if (!this.url) return { title: '兜爸心选', path: '/pages/index/index' }
|
||||||
|
return {
|
||||||
|
title: TARGETS[this.t].title,
|
||||||
|
path: '/pages/annex/link_view/index?t=' + this.t + '&k=' + this.k,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
goHome() {
|
||||||
|
uni.switchTab({ url: '/pages/index/index' })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 兜底屏沿用宿主页配色(pages/annex/web_view):#2B2320 主文字 / #A79C95 次要 / #F4593C 主按钮 */
|
||||||
|
.lv-fallback { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 70vh; padding: 0 40rpx; }
|
||||||
|
.lv-emoji { font-size: 64rpx; line-height: 1; }
|
||||||
|
.lv-title { margin-top: 24rpx; font-size: 32rpx; font-weight: 700; color: #2B2320; }
|
||||||
|
.lv-desc { margin-top: 12rpx; font-size: 26rpx; color: #A79C95; line-height: 1.6; text-align: center; }
|
||||||
|
.lv-btn { margin-top: 36rpx; padding: 0 48rpx; height: 80rpx; line-height: 80rpx; border-radius: 40rpx; background: #F4593C; color: #fff; font-size: 28rpx; }
|
||||||
|
.lv-btn-hover { opacity: .85; }
|
||||||
|
</style>
|
||||||
@@ -8,10 +8,11 @@
|
|||||||
以前 url 为空时这里是白屏(没有任何占位),所以必须给一块可见的屏,
|
以前 url 为空时这里是白屏(没有任何占位),所以必须给一块可见的屏,
|
||||||
否则「不加载」会变成「白屏 + 用户不知道该干嘛」。 -->
|
否则「不加载」会变成「白屏 + 用户不知道该干嘛」。 -->
|
||||||
<view v-else class="wv-fallback">
|
<view v-else class="wv-fallback">
|
||||||
<view class="wv-emoji">{{ identityFail ? '🥲' : '🍼' }}</view>
|
<view class="wv-emoji">{{ appNeedLogin ? '🔐' : (identityFail ? '🥲' : '🍼') }}</view>
|
||||||
<view class="wv-title">{{ identityFail ? '未获取到微信身份' : '正在获取微信身份' }}</view>
|
<view class="wv-title">{{ appNeedLogin ? '登录后继续' : (identityFail ? '未获取到微信身份' : '正在获取微信身份') }}</view>
|
||||||
<view class="wv-desc">{{ identityFail ? '请检查网络后点下方重试,或退出小程序重新进入' : '马上就好,请稍候(不会加载空数据的页面)' }}</view>
|
<view class="wv-desc">{{ appNeedLogin ? '使用手机号验证码登录;登录后会获取微信身份并自动打开该应用' : (identityFail ? '请检查网络后点下方重试,或退出小程序重新进入' : '马上就好,请稍候(不会加载空数据的页面)') }}</view>
|
||||||
<button v-if="identityFail" class="wv-retry" hover-class="wv-retry-hover" @tap="retryIdentity">重新获取身份</button>
|
<button v-if="appNeedLogin" class="wv-retry" hover-class="wv-retry-hover" @tap="goAppLogin">登录后继续</button>
|
||||||
|
<button v-else-if="identityFail" class="wv-retry" hover-class="wv-retry-hover" @tap="retryIdentity">重新获取身份</button>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 共享原生授权面板(任意 H5 应用 postMessage({action:'requestProfile'}) 后显示) -->
|
<!-- 共享原生授权面板(任意 H5 应用 postMessage({action:'requestProfile'}) 后显示) -->
|
||||||
@@ -156,6 +157,7 @@ export default {
|
|||||||
savedUrl: '',
|
savedUrl: '',
|
||||||
identityPending: false, // 正在获取 openid(此期间**不加载 H5**)
|
identityPending: false, // 正在获取 openid(此期间**不加载 H5**)
|
||||||
identityFail: false, // 多次仍未拿到 openid → 停在兜底屏,给「重新获取身份」入口
|
identityFail: false, // 多次仍未拿到 openid → 停在兜底屏,给「重新获取身份」入口
|
||||||
|
appNeedLogin: false, // App:未登录 → 兜底屏给「登录后继续」
|
||||||
_idAttempts: 0, // 自动重试计数
|
_idAttempts: 0, // 自动重试计数
|
||||||
_pendingTarget: '' // 重试时要加载的目标 URL(重试按钮用)
|
_pendingTarget: '' // 重试时要加载的目标 URL(重试按钮用)
|
||||||
}
|
}
|
||||||
@@ -378,9 +380,23 @@ export default {
|
|||||||
console.log('[auth-flow] 🔑 容器页取 openid(第 ' + this._idAttempts + '/' + MAX + ' 次),此期间不加载 H5');
|
console.log('[auth-flow] 🔑 容器页取 openid(第 ' + this._idAttempts + '/' + MAX + ' 次),此期间不加载 H5');
|
||||||
/* 2) 第 1 次先等 App.vue onLaunch 那次静默登录(同一次 exchange,别重复换 code);
|
/* 2) 第 1 次先等 App.vue onLaunch 那次静默登录(同一次 exchange,别重复换 code);
|
||||||
之后的重试才自己 wx.login + 换 openid。理由与首页壳一致,见 mixins 的 _awaitHostLogin。 */
|
之后的重试才自己 wx.login + 换 openid。理由与首页壳一致,见 mixins 的 _awaitHostLogin。 */
|
||||||
const task = (seq === 0) ? this._awaitHostLogin(1500) : this._silenceAuthOpenid();
|
let task;
|
||||||
task.then(() => {
|
// #ifdef APP-PLUS
|
||||||
|
task = this._silenceAuthOpenid();
|
||||||
|
// #endif
|
||||||
|
// #ifndef APP-PLUS
|
||||||
|
task = (seq === 0) ? this._awaitHostLogin(1500) : this._silenceAuthOpenid();
|
||||||
|
// #endif
|
||||||
|
task.then((r) => {
|
||||||
if (this._loadWithCachedIdentity(t)) return;
|
if (this._loadWithCachedIdentity(t)) return;
|
||||||
|
/* App 未登录:停止自动重试,兜底屏给「登录后继续」 */
|
||||||
|
if (r && r.needLogin) {
|
||||||
|
this.identityPending = false;
|
||||||
|
this.identityFail = false;
|
||||||
|
this.appNeedLogin = true;
|
||||||
|
console.log('[auth-flow] App 未登录(容器页)→ 等待用户登录');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this._idAttempts < MAX) {
|
if (this._idAttempts < MAX) {
|
||||||
setTimeout(() => this._ensureIdentityThenLoad(t), 500); // 退避固定 500ms(原 600×n)
|
setTimeout(() => this._ensureIdentityThenLoad(t), 500); // 退避固定 500ms(原 600×n)
|
||||||
return;
|
return;
|
||||||
@@ -396,6 +412,7 @@ export default {
|
|||||||
if (!cached.openid) return false;
|
if (!cached.openid) return false;
|
||||||
this.identityPending = false;
|
this.identityPending = false;
|
||||||
this.identityFail = false;
|
this.identityFail = false;
|
||||||
|
this.appNeedLogin = false;
|
||||||
this._idAttempts = 0;
|
this._idAttempts = 0;
|
||||||
this._applyIdentity(t, cached.openid, cached.unionid, getUrlParam(t, 'openid'));
|
this._applyIdentity(t, cached.openid, cached.unionid, getUrlParam(t, 'openid'));
|
||||||
return true;
|
return true;
|
||||||
@@ -404,9 +421,21 @@ export default {
|
|||||||
retryIdentity() {
|
retryIdentity() {
|
||||||
this._idAttempts = 0;
|
this._idAttempts = 0;
|
||||||
this.identityFail = false;
|
this.identityFail = false;
|
||||||
|
this.appNeedLogin = false;
|
||||||
this._ensureIdentityThenLoad(this._pendingTarget || this.savedUrl || this.url);
|
this._ensureIdentityThenLoad(this._pendingTarget || this.savedUrl || this.url);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/* App:去登录;登录成功 reLaunch 回容器页(带原目标 URL) */
|
||||||
|
goAppLogin() {
|
||||||
|
const back = this._pendingTarget || this.savedUrl || this.url || '';
|
||||||
|
try {
|
||||||
|
/* 必须走 $Cache.set(loginMobile 读的是 $Cache.get(BACK_URL)) */
|
||||||
|
this.$Cache.set('login_back_url',
|
||||||
|
back ? '/pages/annex/web_view/index?url=' + encodeURIComponent(back) : '/pages/index/index');
|
||||||
|
} catch (e) {}
|
||||||
|
uni.navigateTo({ url: '/pages/users/login/index' });
|
||||||
|
},
|
||||||
|
|
||||||
_applyIdentity(targetUrl, openid, unionid, urlOpenid) {
|
_applyIdentity(targetUrl, openid, unionid, urlOpenid) {
|
||||||
console.log('[auth-flow] _applyIdentity: openid=' + (openid || '(空)') + ', unionid=' + (unionid || '(空)') + ', urlOpenid=' + (urlOpenid || '(无)'));
|
console.log('[auth-flow] _applyIdentity: openid=' + (openid || '(空)') + ', unionid=' + (unionid || '(空)') + ', urlOpenid=' + (urlOpenid || '(无)'));
|
||||||
if (urlOpenid && urlOpenid !== openid) {
|
if (urlOpenid && urlOpenid !== openid) {
|
||||||
|
|||||||
@@ -32,7 +32,8 @@
|
|||||||
<view class="mamabox-title">{{ fallbackTitle }}</view>
|
<view class="mamabox-title">{{ fallbackTitle }}</view>
|
||||||
<view class="mamabox-desc">{{ fallbackDesc }}</view>
|
<view class="mamabox-desc">{{ fallbackDesc }}</view>
|
||||||
</view>
|
</view>
|
||||||
<button v-if="loadError || identityFail" class="mamabox-retry" hover-class="mamabox-retry-hover" @tap="retryLoad">{{ loadError ? '重新加载' : '重新获取身份' }}</button>
|
<button v-if="appNeedLogin" class="mamabox-retry" hover-class="mamabox-retry-hover" @tap="goAppLogin">登录后继续</button>
|
||||||
|
<button v-else-if="loadError || identityFail" class="mamabox-retry" hover-class="mamabox-retry-hover" @tap="retryLoad">{{ loadError ? '重新加载' : '重新获取身份' }}</button>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 小程序原生授权面板(H5 postMessage 请求后显示):手机号/昵称/头像 -->
|
<!-- 小程序原生授权面板(H5 postMessage 请求后显示):手机号/昵称/头像 -->
|
||||||
@@ -122,19 +123,21 @@
|
|||||||
别让用户对着「正在加载」干等却不知道在等什么。 */
|
别让用户对着「正在加载」干等却不知道在等什么。 */
|
||||||
fallbackTitle() {
|
fallbackTitle() {
|
||||||
if (this.loadError) return '页面加载失败';
|
if (this.loadError) return '页面加载失败';
|
||||||
|
if (this.appNeedLogin) return '登录后继续';
|
||||||
if (this.identityFail) return '未获取到微信身份';
|
if (this.identityFail) return '未获取到微信身份';
|
||||||
if (this.identityPending) return '正在获取微信身份';
|
if (this.identityPending) return '正在获取微信身份';
|
||||||
return '正在加载';
|
return '正在加载';
|
||||||
},
|
},
|
||||||
fallbackDesc() {
|
fallbackDesc() {
|
||||||
if (this.loadError) return '请检查网络后重试';
|
if (this.loadError) return '请检查网络后重试';
|
||||||
|
if (this.appNeedLogin) return '使用手机号验证码登录;登录后会获取微信身份并自动进入首页';
|
||||||
if (this.identityFail) return '请检查网络后点下方重试,或退出小程序重新进入';
|
if (this.identityFail) return '请检查网络后点下方重试,或退出小程序重新进入';
|
||||||
if (this.identityPending) return '马上就好,请稍候(不会加载空数据的页面)';
|
if (this.identityPending) return '马上就好,请稍候(不会加载空数据的页面)';
|
||||||
if (this.restoring) return '正在回到你上次使用的应用';
|
if (this.restoring) return '正在回到你上次使用的应用';
|
||||||
return '正在为你打开「兜爸心选」';
|
return '正在为你打开「兜爸心选」';
|
||||||
},
|
},
|
||||||
fallbackEmoji() {
|
fallbackEmoji() {
|
||||||
return (this.loadError || this.identityFail) ? '🥲' : '🍼';
|
return this.appNeedLogin ? '🔐' : ((this.loadError || this.identityFail) ? '🥲' : '🍼');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -150,6 +153,7 @@
|
|||||||
_noRestore: false, // 本次加载 mama-box 是否要带 #norestore=1(禁止 H5 兜底再把用户送回应用)
|
_noRestore: false, // 本次加载 mama-box 是否要带 #norestore=1(禁止 H5 兜底再把用户送回应用)
|
||||||
identityPending: false, // 正在获取 openid(此期间**不加载 H5**,见 fetchIdentityThenLoad)
|
identityPending: false, // 正在获取 openid(此期间**不加载 H5**,见 fetchIdentityThenLoad)
|
||||||
identityFail: false, // 多次仍未拿到 openid → 停在兜底屏,给「重新获取身份」入口
|
identityFail: false, // 多次仍未拿到 openid → 停在兜底屏,给「重新获取身份」入口
|
||||||
|
appNeedLogin: false, // App:没登录(或登录后仍无身份)→ 兜底屏给「登录后继续」
|
||||||
shareInfo: {}, // CRMEB 后台「分享设置」(标题/图,无 App 匹配时兜底)
|
shareInfo: {}, // CRMEB 后台「分享设置」(标题/图,无 App 匹配时兜底)
|
||||||
// 授权面板字段由 mixins/webviewHost.js 提供:
|
// 授权面板字段由 mixins/webviewHost.js 提供:
|
||||||
// showAuth / authNickname / authAvatar / _authCode
|
// showAuth / authNickname / authAvatar / _authCode
|
||||||
@@ -620,9 +624,25 @@
|
|||||||
它先落地就直接加载 —— 避免两边各换一次 code 白花 200–300ms
|
它先落地就直接加载 —— 避免两边各换一次 code 白花 200–300ms
|
||||||
(微信 code 一次性;实测 code2session 约 200–300ms)。
|
(微信 code 一次性;实测 code2session 约 200–300ms)。
|
||||||
之后的重试才自己 wx.login + 换 openid(_silenceAuthOpenid)。 */
|
之后的重试才自己 wx.login + 换 openid(_silenceAuthOpenid)。 */
|
||||||
const task = (seq === 0) ? this._awaitHostLogin(1500) : this._silenceAuthOpenid();
|
let task;
|
||||||
task.then(() => {
|
// #ifdef APP-PLUS
|
||||||
|
/* App:没有并行的 wx.login,直接走「短信登录态 + app_identity/app_bind」 */
|
||||||
|
task = this._silenceAuthOpenid();
|
||||||
|
// #endif
|
||||||
|
// #ifndef APP-PLUS
|
||||||
|
task = (seq === 0) ? this._awaitHostLogin(1500) : this._silenceAuthOpenid();
|
||||||
|
// #endif
|
||||||
|
task.then((r) => {
|
||||||
if (this._loadWithCachedIdentity(base)) return;
|
if (this._loadWithCachedIdentity(base)) return;
|
||||||
|
/* App 未登录:**停止自动重试**,兜底屏给「登录后继续」按钮 ——
|
||||||
|
没有 token 时重试多少次都拿不到身份,继续循环只是让用户干等。 */
|
||||||
|
if (r && r.needLogin) {
|
||||||
|
this.identityPending = false;
|
||||||
|
this.identityFail = false;
|
||||||
|
this.appNeedLogin = true;
|
||||||
|
console.log('[auth-flow] App 未登录 → 等待用户登录(不自动重试)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this._idAttempts < MAX) {
|
if (this._idAttempts < MAX) {
|
||||||
/* 退避固定 500ms(原来 600×n,最坏 3 次 + 1.8s 退避 ≈ 3.5s 才出失败屏;
|
/* 退避固定 500ms(原来 600×n,最坏 3 次 + 1.8s 退避 ≈ 3.5s 才出失败屏;
|
||||||
现在 2 次 + 0.5s ≈ 1.5s 量级) */
|
现在 2 次 + 0.5s ≈ 1.5s 量级) */
|
||||||
@@ -639,6 +659,7 @@
|
|||||||
if (!this._hostOpenid()) return false;
|
if (!this._hostOpenid()) return false;
|
||||||
this.identityPending = false;
|
this.identityPending = false;
|
||||||
this.identityFail = false;
|
this.identityFail = false;
|
||||||
|
this.appNeedLogin = false;
|
||||||
this._idAttempts = 0;
|
this._idAttempts = 0;
|
||||||
this._shownOnce = true;
|
this._shownOnce = true;
|
||||||
// 沿用本次的 _noRestore(见 loadMamaBox 注释):本轮加载是否需要禁止 mama-box 兜底恢复
|
// 沿用本次的 _noRestore(见 loadMamaBox 注释):本轮加载是否需要禁止 mama-box 兜底恢复
|
||||||
@@ -697,6 +718,7 @@
|
|||||||
this.requesting = false;
|
this.requesting = false;
|
||||||
this._shownOnce = true;
|
this._shownOnce = true;
|
||||||
this.identityFail = false;
|
this.identityFail = false;
|
||||||
|
this.appNeedLogin = false;
|
||||||
/* 有身份直接重载;没有就再走一遍「取到才加载」(重置尝试次数,自动重试 3 次)
|
/* 有身份直接重载;没有就再走一遍「取到才加载」(重置尝试次数,自动重试 3 次)
|
||||||
⚠️ 沿用 this._noRestore:重试首页时同样不许首页 H5 兜底恢复(见 loadMamaBox)。 */
|
⚠️ 沿用 this._noRestore:重试首页时同样不许首页 H5 兜底恢复(见 loadMamaBox)。 */
|
||||||
const homeUrl = this._homeBase();
|
const homeUrl = this._homeBase();
|
||||||
@@ -709,6 +731,15 @@
|
|||||||
this.fetchIdentityThenLoad(homeUrl);
|
this.fetchIdentityThenLoad(homeUrl);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/* App:去登录。登录页成功后会 reLaunch 回 login_back_url(这里显式设成本页),
|
||||||
|
回到首页再走一遍「短信登录态 → app_identity → app_bind」。 */
|
||||||
|
goAppLogin() {
|
||||||
|
/* ⚠️ 必须走 $Cache.set:loginMobile 用 $Cache.get(BACK_URL) 读,
|
||||||
|
裸 uni.setStorageSync 会因缺少过期标记被当成「无值」覆盖掉。 */
|
||||||
|
try { this.$Cache.set('login_back_url', '/pages/index/index'); } catch (e) {}
|
||||||
|
uni.navigateTo({ url: '/pages/users/login/index' });
|
||||||
|
},
|
||||||
|
|
||||||
/* 授权完善资料(postMessage → 原生面板 → 共享授权接口 → hash 回传)与
|
/* 授权完善资料(postMessage → 原生面板 → 共享授权接口 → hash 回传)与
|
||||||
pages/annex/web_view 共用 mixins/webviewHost.js:
|
pages/annex/web_view 共用 mixins/webviewHost.js:
|
||||||
handleHostMessage / onChooseHostAvatar / onHostGetPhone /
|
handleHostMessage / onChooseHostAvatar / onHostGetPhone /
|
||||||
|
|||||||
@@ -78,6 +78,10 @@
|
|||||||
<text class="main-color" @click.stop="privacy('user')">《用户协议》</text>
|
<text class="main-color" @click.stop="privacy('user')">《用户协议》</text>
|
||||||
与<text class="main-color" @click.stop="privacy('privacy')">《隐私协议》</text>
|
与<text class="main-color" @click.stop="privacy('privacy')">《隐私协议》</text>
|
||||||
</checkbox-group>
|
</checkbox-group>
|
||||||
|
<!-- #ifdef APP-PLUS -->
|
||||||
|
<!-- 备案号:商店审核要求应用内可见(未登录也能看到);号未下发时该行不渲染,取值见 config/legal.js -->
|
||||||
|
<view class="beian" v-if="beianNo">APP 备案号:{{ beianNo }}</view>
|
||||||
|
<!-- #endif -->
|
||||||
</view>
|
</view>
|
||||||
<Verify v-if="!disabled" @success="success" :captchaType="captchaType" :imgSize="{ width: '330px', height: '155px' }"
|
<Verify v-if="!disabled" @success="success" :captchaType="captchaType" :imgSize="{ width: '330px', height: '155px' }"
|
||||||
ref="verify"></Verify>
|
ref="verify"></Verify>
|
||||||
@@ -116,6 +120,7 @@
|
|||||||
import colors from '@/mixins/color.js';
|
import colors from '@/mixins/color.js';
|
||||||
import Verify from '../components/verify/verify.vue';
|
import Verify from '../components/verify/verify.vue';
|
||||||
import { HTTP_REQUEST_URL, CAPTCHA_TYPE } from '@/config/app';
|
import { HTTP_REQUEST_URL, CAPTCHA_TYPE } from '@/config/app';
|
||||||
|
import legal from '@/config/legal.js';
|
||||||
export default {
|
export default {
|
||||||
name: "Login",
|
name: "Login",
|
||||||
components: {
|
components: {
|
||||||
@@ -147,6 +152,8 @@
|
|||||||
appleShow: false, // 苹果登录版本必须要求ios13以上的
|
appleShow: false, // 苹果登录版本必须要求ios13以上的
|
||||||
keyLock: true,
|
keyLock: true,
|
||||||
copyrightContext:'',
|
copyrightContext:'',
|
||||||
|
// App 备案号(商店要求应用内可见):号未下发时为空 → 该行不渲染
|
||||||
|
beianNo: legal.APP_BEIAN_NO,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -798,6 +805,14 @@
|
|||||||
color: var(--view-theme);
|
color: var(--view-theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* App 备案号:字号比协议再小一档,避免抢协议勾选的视觉重心 */
|
||||||
|
.beian {
|
||||||
|
margin-top: 14rpx;
|
||||||
|
font-size: 20rpx;
|
||||||
|
line-height: 28rpx;
|
||||||
|
color: #b5b5b5;
|
||||||
|
}
|
||||||
|
|
||||||
.trembling {
|
.trembling {
|
||||||
animation: shake 0.6s;
|
animation: shake 0.6s;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { mapGetters, mapMutations } from 'vuex';
|
|||||||
import colors from '@/mixins/color.js';
|
import colors from '@/mixins/color.js';
|
||||||
import tuiModal from '@/components/tui-modal/index.vue';
|
import tuiModal from '@/components/tui-modal/index.vue';
|
||||||
import NavBar from '@/components/NavBar.vue';
|
import NavBar from '@/components/NavBar.vue';
|
||||||
|
import legal from '@/config/legal.js';
|
||||||
export default {
|
export default {
|
||||||
computed: mapGetters(['isLogin']),
|
computed: mapGetters(['isLogin']),
|
||||||
data() {
|
data() {
|
||||||
@@ -15,7 +16,11 @@ export default {
|
|||||||
loginType: 'h5',
|
loginType: 'h5',
|
||||||
showModal: false,
|
showModal: false,
|
||||||
version: '',
|
version: '',
|
||||||
fileSizeString: ''
|
fileSizeString: '',
|
||||||
|
// 关于区块的合规信息(App 内必须可见备案号):统一取 config/legal.js
|
||||||
|
beianNo: legal.APP_BEIAN_NO,
|
||||||
|
icpNo: legal.WEBSITE_ICP_NO,
|
||||||
|
subject: legal.SUBJECT
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mixins: [colors],
|
mixins: [colors],
|
||||||
@@ -149,6 +154,28 @@ export default {
|
|||||||
</view>
|
</view>
|
||||||
</navigator>
|
</navigator>
|
||||||
</view>
|
</view>
|
||||||
|
<!-- #ifdef APP-PLUS -->
|
||||||
|
<!-- 关于:版本 + 备案信息。小米/华为等商店硬性要求应用内可见备案号(截图要体现),
|
||||||
|
取值全部在 config/legal.js;备案号未下发时该行自动隐藏。 -->
|
||||||
|
<view class="list rd-24rpx mx-20">
|
||||||
|
<view class="item acea-row row-between-wrapper">
|
||||||
|
<view>当前版本</view>
|
||||||
|
<view class="grab value">{{ version || '—' }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="item acea-row row-between-wrapper" v-if="beianNo">
|
||||||
|
<view>APP 备案号</view>
|
||||||
|
<view class="grab value">{{ beianNo }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="item acea-row row-between-wrapper">
|
||||||
|
<view>ICP 备案号</view>
|
||||||
|
<view class="grab value">{{ icpNo }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="item acea-row row-between-wrapper">
|
||||||
|
<view>开发者</view>
|
||||||
|
<view class="grab value">{{ subject }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- #endif -->
|
||||||
<!-- #ifdef MP -->
|
<!-- #ifdef MP -->
|
||||||
<view class="list rd-24rpx mx-20">
|
<view class="list rd-24rpx mx-20">
|
||||||
<view class="item acea-row row-between-wrapper" @click="outLogin">
|
<view class="item acea-row row-between-wrapper" @click="outLogin">
|
||||||
@@ -230,6 +257,15 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 右侧纯文本值(版本号 / 备案号 / 主体):备案号可能较长,允许换行 */
|
||||||
|
.value {
|
||||||
|
color: #999;
|
||||||
|
font-size: 26rpx;
|
||||||
|
max-width: 60%;
|
||||||
|
text-align: right;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
/deep/.uni-switch-input {
|
/deep/.uni-switch-input {
|
||||||
width: 84rpx;
|
width: 84rpx;
|
||||||
height: 48rpx;
|
height: 48rpx;
|
||||||
|
|||||||
Reference in New Issue
Block a user