138 lines
5.5 KiB
Vue
138 lines
5.5 KiB
Vue
<!--
|
||
小程序专用 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>
|