Compare commits
6 Commits
f3648d5cef
...
mp-0.3.14-
| Author | SHA1 | Date | |
|---|---|---|---|
| a8cd4ff1a1 | |||
|
|
9b41ba19fb | ||
|
|
8543447df2 | ||
| 53a43ea18c | |||
| 6df7640272 | |||
| 2356f33b47 |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -25,3 +25,10 @@ unpackage
|
||||
project.config.json
|
||||
.hbuilderx
|
||||
/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/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/shareCard.js` | 分享图:按当前 H5 URL 反查 box_app App 表;朋友圈本地图转换 |
|
||||
| `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。
|
||||
7. **恢复/落地统一 `navigateTo` 容器页**,并保留首页壳作为返回落点;不要直接把首页壳的 web-view src 指向第三方应用(首页壳的 `buildWebUrl` 是 **hash** 注入,会破坏自带 hash 路由的应用)。
|
||||
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,都付过线上代价)
|
||||
|
||||
|
||||
18
config/version.js
Normal file
18
config/version.js
Normal file
@@ -0,0 +1,18 @@
|
||||
// +----------------------------------------------------------------------
|
||||
// | 版本号(界面展示兜底值)
|
||||
// +----------------------------------------------------------------------
|
||||
// | 用途:「我的」页底部展示当前版本,方便真机/体验版核对是不是新包。
|
||||
// |
|
||||
// | 取值优先级:
|
||||
// | ① 体验版/正式版 → 读微信真实版本(wx.getAccountInfoSync().miniProgram.version,
|
||||
// | 即上传时填的版本号,线上现为 0.3.13);
|
||||
// | ② 开发版/读不到(开发期为空串)→ 用这里的 VERSION 兜底。
|
||||
// |
|
||||
// | ⚠️ VERSION 必须与 manifest.json 的 versionName 同步:每次发版 bump 时**两处一起改**。
|
||||
// | manifest 决定包版本,但微信上传时填的版本号是独立的,所以线上以 ① 为准,
|
||||
// | 不要把这里当成「已发布版本」的唯一来源。
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
VERSION: '0.3.14'
|
||||
};
|
||||
@@ -2,8 +2,8 @@
|
||||
"name" : "兜爸心选",
|
||||
"appid" : "__UNI__52BA861",
|
||||
"description" : "兜爸心选",
|
||||
"versionName" : "0.3.8",
|
||||
"versionCode" : 38,
|
||||
"versionName" : "0.3.14",
|
||||
"versionCode" : 314,
|
||||
"transformPx" : false,
|
||||
"sassImplementationName" : "node-sass",
|
||||
/* 5+App特有相关 */
|
||||
|
||||
@@ -1088,6 +1088,11 @@
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
}, {
|
||||
"path": "link_view/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "兜爸心选"
|
||||
}
|
||||
}, {
|
||||
"path": "auth/index",
|
||||
"style": {
|
||||
|
||||
218
pages/annex/link_view/index.vue
Normal file
218
pages/annex/link_view/index.vue
Normal file
@@ -0,0 +1,218 @@
|
||||
<!--
|
||||
小程序专用 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 打开;解析不出来显示兜底屏。
|
||||
2026-11-12 起再加两件(v5,box_app/docs/h5-auth-standard.md 第八节):
|
||||
a) 打开前用 wx.login 换身份票据,拼成 <地址>#ticket=<票据>;取票失败或 3 秒超时 → 打开不带票据的地址(H5 照旧公众号授权)。
|
||||
b) 接收 H5 postMessage 的 {action:'share', imageUrl, title},用作转发卡片的图和标题(docs/mp-share-card.md)。
|
||||
|
||||
⚠️ TARGETS 必须和 box_app backend/app/services/link_wxacode_serv.py 的同名表一致
|
||||
(短键、地址前缀、标识规则)。改这张表 = 两端一起改 + 小程序发版;
|
||||
码里只有标识,所以发版后旧码照常可用。
|
||||
⚠️ 不在网址里放明文 openid/unionid:身份只走 hash 里的签名票据,由 H5 自己验签(影子模式期间仍以公众号授权为准)。
|
||||
本页**不引入** webviewHost / shareCard mixin:分享图/标题只认 H5 发来的消息,不反查 box_app。
|
||||
⚠️ 取票接口 https://mp.yizhezhe.com/api/mp/public/link-ticket:该域名已在小程序 request 合法域名里(shareCard 同用)。
|
||||
⚠️ 转发路径只带 t/k,绝不带票据(票据一次性、带身份)。
|
||||
⚠️ 本页路径必须等于 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" @message="onH5Message"></web-view>
|
||||
<view v-else-if="loading" class="lv-loading">正在进入…</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: '兜爸心选 · 好物推荐', page: 's' },
|
||||
f: { base: 'https://fuli.douba.top/a/', re: /^[a-z0-9-]{1,30}$/, title: '兜爸心选 · 福利活动', page: 'a' },
|
||||
}
|
||||
|
||||
/* 微信给小程序的 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 }
|
||||
}
|
||||
|
||||
/* 取身份票据:wx.login → box_app 签票。任何失败都 resolve(''),由调用方退回不带票据的地址。
|
||||
返回体是本仓惯例 {code, message, result:{ticket, exp}},HTTP 恒 200。 */
|
||||
const TICKET_API = 'https://mp.yizhezhe.com/api/mp/public/link-ticket'
|
||||
const TICKET_TIMEOUT = 3000
|
||||
|
||||
function fetchTicket(t, k) {
|
||||
const work = new Promise((resolve) => {
|
||||
uni.login({
|
||||
provider: 'weixin',
|
||||
success(res) {
|
||||
if (!res || !res.code) return resolve('')
|
||||
uni.request({
|
||||
url: TICKET_API,
|
||||
method: 'POST',
|
||||
data: { code: res.code, t, k },
|
||||
timeout: TICKET_TIMEOUT,
|
||||
success(r) {
|
||||
const d = (r && r.data) || {}
|
||||
const tk = d.code === 200 && d.result && d.result.ticket
|
||||
if (!tk) console.warn('[link-view] 取票失败 code=' + d.code + ' ' + (d.message || ''))
|
||||
resolve(typeof tk === 'string' ? tk : '')
|
||||
},
|
||||
fail(e) {
|
||||
console.warn('[link-view] 取票请求失败', e && e.errMsg)
|
||||
resolve('')
|
||||
},
|
||||
})
|
||||
},
|
||||
fail() { resolve('') },
|
||||
})
|
||||
})
|
||||
const timer = new Promise((resolve) => setTimeout(() => resolve(''), TICKET_TIMEOUT))
|
||||
return Promise.race([work, timer])
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
url: '', // 为空 = 兜底屏(web-view 不渲染)
|
||||
t: '', // 短键(分享时回填)
|
||||
k: '', // 标识(分享时回填)
|
||||
loading: false, // 正在取票据(不显示兜底屏)
|
||||
shareImage: '', // H5 上报的转发卡片图(https)
|
||||
shareTitle: '', // H5 上报的转发卡片标题
|
||||
shareK: '', // H5 上报的当前页面标识:用户在 web-view 里跳到别的商品后,转发的是当前商品而不是入口商品
|
||||
}
|
||||
},
|
||||
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.loading = true
|
||||
// web-view 的 src 只设一次:拿到票据(或超时)后再渲染,避免页面加载两遍
|
||||
fetchTicket(hit.t, hit.k).then((ticket) => {
|
||||
this.loading = false
|
||||
this.url = ticket ? hit.url + '#ticket=' + ticket : hit.url
|
||||
console.log('[link-view] scene=' + (opt.scene || '') + ' 短键=' + hit.t +
|
||||
' 标识=' + hit.k + ' → ' + hit.url + (ticket ? '(带票据)' : '(无票据)'))
|
||||
})
|
||||
},
|
||||
onShareAppMessage() {
|
||||
// 转发出去还是这个壳页:好友点开按 t/k 重新拼地址,不带上任何身份参数
|
||||
if (!this.url) return { title: '兜爸心选', path: '/pages/index/index' }
|
||||
const card = {
|
||||
title: this.shareTitle || TARGETS[this.t].title,
|
||||
path: '/pages/annex/link_view/index?t=' + this.t + '&k=' + (this.shareK || this.k),
|
||||
}
|
||||
// 没收到 H5 的图就不传 imageUrl,微信用页面截图
|
||||
if (this.shareImage) card.imageUrl = this.shareImage
|
||||
return card
|
||||
},
|
||||
methods: {
|
||||
/* web-view 的 postMessage 只在转发、后退等时机批量送达,e.detail.data 是数组;按顺序处理,最后一条 share 生效。
|
||||
消息:{action:'share', imageUrl, title, page, slug}
|
||||
- 带 page/slug:page 必须是本目标能打开的页面(l→'s' 商品,f→'a' 活动)且 slug 合法,转发路径就用这个 slug;
|
||||
否则(如链接系统集合页 'c',壳页打不开)清空图和标题,转发退回入口页 + 默认标题,避免「卡片是 A、点开是 B」。
|
||||
- 不带 page/slug(H5 旧版):视为入口页的信息。 */
|
||||
onH5Message(e) {
|
||||
const raw = e && e.detail && e.detail.data
|
||||
const list = Array.isArray(raw) ? raw : (raw ? [raw] : [])
|
||||
const target = TARGETS[this.t]
|
||||
list.forEach((m) => {
|
||||
if (!m || m.action !== 'share' || !target) return
|
||||
let k = ''
|
||||
if (m.page != null || m.slug != null) {
|
||||
const ok = m.page === target.page && typeof m.slug === 'string' && target.re.test(m.slug)
|
||||
if (!ok) {
|
||||
this.shareImage = ''
|
||||
this.shareTitle = ''
|
||||
this.shareK = ''
|
||||
return
|
||||
}
|
||||
k = m.slug
|
||||
}
|
||||
this.shareK = k
|
||||
this.shareImage = typeof m.imageUrl === 'string' && /^https:\/\//.test(m.imageUrl) ? m.imageUrl : ''
|
||||
this.shareTitle = typeof m.title === 'string' && m.title.trim() ? m.title.trim().slice(0, 40) : ''
|
||||
})
|
||||
},
|
||||
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; }
|
||||
.lv-loading { display: flex; align-items: center; justify-content: center; min-height: 70vh; font-size: 26rpx; color: #A79C95; }
|
||||
</style>
|
||||
@@ -21,6 +21,8 @@
|
||||
</view>
|
||||
</template>
|
||||
<image v-else :src="copyrightImage" mode="aspectFill" class="support"></image>
|
||||
<!-- 版本号:体验版/正式版读微信实际版本,开发版回退 config/version.js -->
|
||||
<view class="app-version">版本号 v{{ appVersion }}</view>
|
||||
</view>
|
||||
</template>
|
||||
<view class="mark" v-show="isextension"></view>
|
||||
@@ -45,6 +47,24 @@ import Cache from '@/utils/cache';
|
||||
import Auth from '@/libs/wechat';
|
||||
// #endif
|
||||
import { HTTP_REQUEST_URL } from '@/config/app';
|
||||
import version from '@/config/version.js';
|
||||
|
||||
/* 「我的」页展示的版本号:
|
||||
体验版/正式版直接读微信当前包的版本(上传时填的那个,线上现为 0.3.13);
|
||||
开发版读不到(返回空串)时用 config/version.js 兜底。
|
||||
这样显示的永远是用户手上这个包的真实版本,不会因为 manifest 与上传版本脱节而假报版本。 */
|
||||
function currentAppVersion() {
|
||||
let v = '';
|
||||
// #ifdef MP-WEIXIN
|
||||
try {
|
||||
const info = wx.getAccountInfoSync();
|
||||
v = (info && info.miniProgram && info.miniProgram.version) || '';
|
||||
} catch (e) {}
|
||||
// #endif
|
||||
// 个别开发者工具版本在开发版会回 "0",按「读不到」处理
|
||||
if (!v || v === '0') v = '';
|
||||
return v || version.VERSION;
|
||||
}
|
||||
const app = getApp();
|
||||
import ewcomerPop from '@/components/ewcomerPop/index.vue';
|
||||
import pageFooter from '@/components/pageFooter/index.vue';
|
||||
@@ -191,6 +211,8 @@ export default {
|
||||
},
|
||||
imgHost: HTTP_REQUEST_URL,
|
||||
configData: Cache.get('BASIC_CONFIG'),
|
||||
// 版本号展示:体验版/正式版读微信实际版本,开发版回退 config/version.js
|
||||
appVersion: currentAppVersion(),
|
||||
copyrightImage: HTTP_REQUEST_URL + '/statics/images/product/support.png',
|
||||
giftPic: '',
|
||||
vip_type: 1,
|
||||
@@ -670,5 +692,11 @@ export default {
|
||||
font-size: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
/* 版本号:比版权文案再淡一档,避免抢视觉重心 */
|
||||
.app-version {
|
||||
margin-top: 12rpx;
|
||||
font-size: 20rpx;
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user