feat: 双端小程序分享路径与测试入口上下文统一

1、修复了结果页、测试页与 test-select 的跳转上下文丢失问题。

2、调整 api/Test 与小程序 app/share 逻辑,补全第三方/企业场景参数透传。

3、优化 DISC/MBTI/PDP/SBTI 结果页样式与按钮交互,一致化微信与抖音端。

Made-with: Cursor
This commit is contained in:
Ghost
2026-04-14 16:40:20 +08:00
parent 5330014a07
commit c946584a7d
42 changed files with 942 additions and 254 deletions

View File

@@ -108,8 +108,6 @@ Page({
onLoad(options) {
this._payInfoSetByDetail = false
const id = options && options.id
const type = options && options.type
// 加载文案配置(分析中提示、报告标题等)
const tc = app.globalData.textConfig
@@ -135,55 +133,101 @@ Page({
}).catch(() => {})
}
// 从历史记录进入根据ID从后端读取数据库中的结果
if (id && type === 'ai') {
const token = app.globalData.token || wx.getStorageSync('token')
const apiBase = app.globalData.apiBase
if (!token || !apiBase) {
wx.showToast({ title: '未登录,无法读取历史记录', icon: 'none' })
const idStr = options && options.id != null && options.id !== '' ? String(options.id) : ''
const st = options && options.st ? String(options.st).trim() : ''
const rawType = options && options.type ? String(options.type).toLowerCase() : ''
// 人脸记录在库中可能是 face 或 ai仅 id 时默认识别为人脸报告(兼容旧分享)
const faceType = rawType || (idStr ? 'ai' : '')
// 带记录 id只读库展示禁止落入 startAnalysis 造成「二次分析」
if (idStr && (faceType === 'ai' || faceType === 'face')) {
this.setData({ testResultId: idStr })
this.loadFaceRecordById(idStr, st, faceType)
return
}
if (idStr) {
wx.showToast({ title: '链接参数无效', icon: 'none' })
setTimeout(() => wx.navigateBack(), 1500)
return
}
// 无 id正常从拍照流程进入调用 /api/analyze
this.startAnalysis()
},
/**
* 加载人脸报告:优先免登录 share-detail好友分享失败且已登录再 detail
*/
loadFaceRecordById(id, st, typeParam) {
const apiBase = app.globalData?.apiBase || ''
if (!apiBase) {
wx.showToast({ title: '配置异常', icon: 'none' })
return
}
const token = app.globalData.token || wx.getStorageSync('token') || ''
const applyPayload = (payload) => {
wx.hideLoading()
const apiData = payload.data || payload
this.initPayInfoFromRuntime(!!payload.requiresPayment, !!payload.isPaid, payload)
this.processResult(apiData)
}
const loadDetailAuthed = () => {
if (!token) {
wx.hideLoading()
wx.showToast({ title: '未登录,无法查看该记录', icon: 'none' })
setTimeout(() => wx.navigateBack(), 1500)
return
}
wx.showLoading({ title: '加载历史记录...' })
wx.showLoading({ title: '加载中...' })
wx.request({
url: `${apiBase}/api/test/detail`,
method: 'GET',
header: {
'Authorization': `Bearer ${token}`
},
header: { Authorization: `Bearer ${token}` },
data: { id },
success: (res) => {
wx.hideLoading()
if (res.statusCode === 200 && res.data && res.data.data) {
const payload = res.data.data
const apiData = payload.data || payload
// 历史详情场景下记录当前测试记录ID
this.setData({ testResultId: id })
// 先确定付费状态(设置 _payInfoSetByDetail 标记),再渲染结果
// 避免 processResult 内部异步拉全局配置覆盖掉数据库级别的付费判定
this.initPayInfoFromRuntime(
!!payload.requiresPayment,
!!payload.isPaid,
payload
)
this.processResult(apiData)
if (res.statusCode === 200 && res.data && res.data.code === 200 && res.data.data) {
applyPayload(res.data.data)
} else {
wx.hideLoading()
wx.showToast({ title: res.data?.message || '加载失败', icon: 'none' })
setTimeout(() => wx.navigateBack(), 1500)
}
},
fail: () => {
wx.hideLoading()
wx.showToast({ title: '网络错误,加载失败', icon: 'none' })
wx.showToast({ title: '网络错误', icon: 'none' })
setTimeout(() => wx.navigateBack(), 1500)
}
})
return
}
// 正常从拍照流程进入:调用 /api/analyze
this.startAnalysis()
wx.showLoading({ title: '加载中...' })
const data = {
id: String(id),
type: typeParam === 'face' ? 'face' : 'ai'
}
if (st) data.st = st
wx.request({
url: `${apiBase}/api/test/share-detail`,
method: 'GET',
data,
success: (res) => {
if (res.statusCode === 200 && res.data && res.data.code === 200 && res.data.data) {
applyPayload(res.data.data)
return
}
wx.hideLoading()
loadDetailAuthed()
},
fail: () => {
wx.hideLoading()
loadDetailAuthed()
}
})
},
onShow() {
@@ -631,7 +675,14 @@ Page({
onShareAppMessage() {
const r = this.data.result
const t = this.data.aiAnalysisText || '智能分析'
const { getSharePathByScope } = require('../../utils/share')
const { getResultSharePath, getSharePathByScope } = require('../../utils/share')
const tid = this.data.testResultId
if (tid) {
return {
title: `${t}我是${r?.mbti} ${r?.pdpEmoji}${r?.pdp}型,来测测你的!`,
path: getResultSharePath('/pages/index/result', { id: tid, type: 'ai' })
}
}
return {
title: `${t}我是${r?.mbti} ${r?.pdpEmoji}${r?.pdp}型,来测测你的!`,
path: getSharePathByScope('/pages/index/index')
@@ -641,7 +692,14 @@ Page({
onShareTimeline() {
const r = this.data.result
const t = this.data.aiAnalysisText || '智能分析'
const { buildShareQuery } = require('../../utils/share')
const { getResultShareTimelineQuery, buildShareQuery } = require('../../utils/share')
const tid = this.data.testResultId
if (tid) {
return {
title: `${t}我是${r?.mbti} ${r?.pdpEmoji}${r?.pdp}型,来测测你的!`,
query: getResultShareTimelineQuery({ id: tid, type: 'ai' })
}
}
return {
title: `${t}我是${r?.mbti} ${r?.pdpEmoji}${r?.pdp}型,来测测你的!`,
query: buildShareQuery()

View File

@@ -34,12 +34,24 @@ Page({
},
onLoad() {
this.loadStats()
this.loadBindings(true)
// 分享直达时 silentLogin 可能未完成,先 ensureLogin 再拉推广数据,避免 401 空白
app.ensureLogin()
.then((ok) => {
if (!ok) {
wx.showToast({ title: '请先登录后查看推广中心', icon: 'none' })
return
}
this.loadStats()
this.loadBindings(true)
})
.catch(() => {
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
})
},
onShow() {
this.loadStats()
const token = app.globalData.token || wx.getStorageSync('token')
if (token) this.loadStats()
},
/** 加载推广统计数据 */

View File

@@ -1,3 +1,4 @@
const app = getApp()
const { getApiBase } = require('../../utils/request')
const { getEffectiveEnterpriseId } = require('../../utils/enterpriseContext.js')
@@ -9,7 +10,18 @@ Page({
},
onLoad() {
this.loadPoster()
app.ensureLogin()
.then((ok) => {
if (!ok) {
this.setData({ loading: false, loadingText: '请先登录后生成海报' })
wx.showToast({ title: '请先登录', icon: 'none' })
return
}
this.loadPoster()
})
.catch(() => {
this.setData({ loading: false, loadingText: '登录失败,请重试' })
})
},
/** 从后端接口下载完整合成海报 */

View File

@@ -43,18 +43,24 @@ Page({
testResultId: null,
shareToken: '',
hasReloadedAfterPay: false,
fromShare: false
},
onLoad(options) {
const id = options && options.id
const type = options && options.type
const fromShareFs =
options && (String(options.fs) === '1' || options.from === 'share')
const id = options && options.id != null && options.id !== '' ? String(options.id) : ''
const st = options && options.st ? String(options.st).trim() : ''
const type = options && options.type ? String(options.type).toLowerCase() : ''
if (id && type === 'disc') {
this.setData({ testResultId: id })
if (options.st) {
this.loadShareDetail(id, options.st)
} else {
this.loadDetail(id)
}
this.setData({ testResultId: id, fromShare: !!fromShareFs })
this.loadShareDetail(id, st, 'disc')
return
}
if (id && st) {
this.setData({ testResultId: id, fromShare: true })
this.loadShareDetail(id, st, '')
return
}
const raw = wx.getStorageSync('discResult')
@@ -86,7 +92,11 @@ Page({
isPaid,
amountYuan: needPaymentToUnlock ? amountYuan : 0
}
this.setData({ payInfo })
const patch = { payInfo }
if (payload.id != null && payload.id !== '') {
patch.testResultId = String(payload.id)
}
this.setData(patch)
},
loadDetail(id) {
@@ -111,14 +121,17 @@ Page({
})
},
loadShareDetail(id, st) {
loadShareDetail(id, st, testType) {
const apiBase = app.globalData?.apiBase || ''
if (!apiBase) { wx.showToast({ title: '配置异常', icon: 'none' }); return }
const data = { id: String(id) }
if (st) data.st = st
if (testType) data.type = testType
wx.showLoading({ title: '加载中...' })
wx.request({
url: `${apiBase}/api/test/share-detail`,
method: 'GET',
data: { id, st },
data,
success: (res) => {
if (res.statusCode === 200 && res.data && res.data.code === 200) {
this.applyDetailPayload(res.data.data || {})
@@ -185,6 +198,10 @@ Page({
wx.navigateTo({ url: '/pages/test/disc' })
},
goWantTest() {
wx.navigateTo({ url: '/pages/test/disc' })
},
goHome() {
const scope = (getApp().globalData && getApp().globalData.appScope) || 'personal'
if (scope === 'enterprise') {
@@ -201,8 +218,7 @@ Page({
title: `我的DISC类型是${result?.dominantType}型(${result?.description?.title}),来测测你的吧!`,
path: getResultSharePath('/pages/result/disc', {
id: this.data.testResultId,
type: 'disc',
shareToken: this.data.shareToken
type: 'disc'
})
}
},
@@ -214,8 +230,7 @@ Page({
title: `我的DISC类型是${result?.dominantType}型(${result?.description?.title}),来测测你的吧!`,
query: getResultShareTimelineQuery({
id: this.data.testResultId,
type: 'disc',
shareToken: this.data.shareToken
type: 'disc'
})
}
}

View File

@@ -1,7 +1,7 @@
<!--pages/result/disc.wxml - DISC结果按旧版模板重构-->
<view class="result-page">
<scroll-view class="content-scroll" scroll-y>
<view class="content-container">
<scroll-view class="content-scroll {{fromShare ? 'content-scroll--with-fixed-footer' : ''}}" scroll-y>
<view class="content-container {{fromShare ? 'content-container--with-fixed-footer' : ''}}">
<view class="type-card">
<view class="type-header">
<text class="type-label">您的DISC性格类型</text>
@@ -25,7 +25,7 @@
</view>
</view>
<view class="paywall-card" wx:elif="{{result && result.locked}}">
<view class="paywall-card" wx:elif="{{result && result.locked && !fromShare}}">
<view class="paywall-content">
<text class="paywall-fake-title">完整报告需完善资料</text>
<text class="paywall-fake-line">请补全头像、昵称并绑定手机号后查看完整 DISC 报告。</text>
@@ -88,4 +88,7 @@
</view> -->
</view>
</scroll-view>
<view class="result-share-footer" wx:if="{{fromShare}}">
<button class="result-share-footer-btn" bindtap="goWantTest">我也要测试</button>
</view>
</view>

View File

@@ -15,6 +15,14 @@
padding-bottom: 64rpx;
}
.content-container--with-fixed-footer {
padding-bottom: calc(64rpx + 120rpx + env(safe-area-inset-bottom));
}
.content-scroll--with-fixed-footer {
height: 100vh;
}
.type-card,
.analysis-card {
background-color: #fff;
@@ -195,3 +203,35 @@
.paywall-btn { position: absolute; left: 5%; right: 5%; width: 90%; bottom: 132rpx; z-index: 3; padding: 20rpx 0; border-radius: 999rpx; background: linear-gradient(135deg, #3B82F6 0%, #60a5fa 100%); box-shadow: 0 8rpx 24rpx rgba(59,130,246,0.35); display: flex; flex-direction: row; align-items: baseline; justify-content: center; gap: 12rpx; }
.paywall-btn-main { font-size: 30rpx; color: #fff; font-weight: 600; }
.paywall-btn-price { font-size: 24rpx; color: #ffe5f0; }
.result-share-footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 200;
box-sizing: border-box;
width: 100%;
padding: 20rpx 32rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: #f5f5f5;
box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
}
.result-share-footer-btn {
width: 100% !important;
box-sizing: border-box;
display: block;
margin: 0;
padding: 0;
min-height: 96rpx;
line-height: 96rpx;
border-radius: 999rpx;
font-size: 30rpx;
font-weight: 600;
color: #fff;
background: linear-gradient(135deg, #2563eb 0%, #60a5fa 100%);
border: none;
}
.result-share-footer-btn::after {
border: none;
}

View File

@@ -30,19 +30,24 @@ Page({
shareToken: '',
hasReloadedAfterPay: false,
hasPhone: false,
fromShare: false
},
onLoad(options) {
const id = options && options.id
const type = options && options.type
const fromShareFs =
options && (String(options.fs) === '1' || options.from === 'share')
const id = options && options.id != null && options.id !== '' ? String(options.id) : ''
const st = options && options.st ? String(options.st).trim() : ''
const type = options && options.type ? String(options.type).toLowerCase() : ''
if (id && type === 'mbti') {
this.setData({ testResultId: id })
if (options.st) {
this.loadShareDetail(id, options.st)
} else {
this.loadDetail(id)
}
this.setData({ testResultId: id, fromShare: !!fromShareFs })
this.loadShareDetail(id, st, 'mbti')
return
}
if (id && st) {
this.setData({ testResultId: id, fromShare: true })
this.loadShareDetail(id, st, '')
return
}
@@ -83,10 +88,14 @@ Page({
isPaid,
amountYuan: needPaymentToUnlock ? amountYuan : 0
}
this.setData({
const patch = {
payInfo,
shareToken: payload.shareToken || ''
})
}
if (payload.id != null && payload.id !== '') {
patch.testResultId = String(payload.id)
}
this.setData(patch)
},
loadDetail(id) {
@@ -114,17 +123,20 @@ Page({
})
},
loadShareDetail(id, st) {
loadShareDetail(id, st, testType) {
const apiBase = app.globalData?.apiBase || ''
if (!apiBase) {
wx.showToast({ title: '配置异常', icon: 'none' })
return
}
const data = { id: String(id) }
if (st) data.st = st
if (testType) data.type = testType
wx.showLoading({ title: '加载中...' })
wx.request({
url: `${apiBase}/api/test/share-detail`,
method: 'GET',
data: { id, st },
data,
success: (res) => {
if (res.statusCode === 200 && res.data && res.data.code === 200) {
this.applyDetailPayload(res.data.data || {})
@@ -243,6 +255,10 @@ Page({
wx.navigateTo({ url: '/pages/test/mbti' })
},
goWantTest() {
wx.navigateTo({ url: '/pages/test/mbti' })
},
goHome() {
const scope = (getApp().globalData && getApp().globalData.appScope) || 'personal'
if (scope === 'enterprise') {
@@ -259,8 +275,7 @@ Page({
title: `我的MBTI类型是${result?.mbtiType}${result?.description?.name}),来测测你的吧!`,
path: getResultSharePath('/pages/result/mbti', {
id: this.data.testResultId,
type: 'mbti',
shareToken: this.data.shareToken
type: 'mbti'
}),
imageUrl: '/images/share-mbti.png'
}
@@ -273,8 +288,7 @@ Page({
title: `我的MBTI类型是${result?.mbtiType}${result?.description?.name}),来测测你的吧!`,
query: getResultShareTimelineQuery({
id: this.data.testResultId,
type: 'mbti',
shareToken: this.data.shareToken
type: 'mbti'
})
}
}

View File

@@ -1,7 +1,7 @@
<!--pages/result/mbti.wxml - MBTI结果页面支持付费墙-->
<view class="result-page">
<scroll-view class="content-scroll" scroll-y>
<view class="content-container">
<scroll-view class="content-scroll {{fromShare ? 'content-scroll--with-fixed-footer' : ''}}" scroll-y>
<view class="content-container {{fromShare ? 'content-container--with-fixed-footer' : ''}}">
<view class="type-card">
<view class="type-header">
<text class="type-label">您的MBTI性格类型</text>
@@ -42,7 +42,7 @@
</view>
</view>
<view class="paywall-card" wx:elif="{{result && result.locked}}">
<view class="paywall-card" wx:elif="{{result && result.locked && !fromShare}}">
<view class="paywall-content">
<text class="paywall-fake-title">完整报告需完善资料</text>
<text class="paywall-fake-line">请补全头像、昵称并绑定手机号后查看完整性格分析。</text>
@@ -114,4 +114,7 @@
</view> -->
</view>
</scroll-view>
<view class="result-share-footer" wx:if="{{fromShare}}">
<button class="result-share-footer-btn" bindtap="goWantTest">我也要测试</button>
</view>
</view>

View File

@@ -15,6 +15,14 @@
padding-bottom: 64rpx;
}
.content-container--with-fixed-footer {
padding-bottom: calc(64rpx + 120rpx + env(safe-area-inset-bottom));
}
.content-scroll--with-fixed-footer {
height: 100vh;
}
.type-card,
.analysis-card {
background-color: #fff;
@@ -247,3 +255,35 @@
}
.paywall-btn-main { font-size: 30rpx; color: #fff; font-weight: 600; }
.paywall-btn-price { font-size: 24rpx; color: #ffe5f0; }
.result-share-footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 200;
box-sizing: border-box;
width: 100%;
padding: 20rpx 32rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: #f5f5f5;
box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
}
.result-share-footer-btn {
width: 100% !important;
box-sizing: border-box;
display: block;
margin: 0;
padding: 0;
min-height: 96rpx;
line-height: 96rpx;
border-radius: 999rpx;
font-size: 30rpx;
font-weight: 600;
color: #fff;
background: linear-gradient(135deg, #e63946 0%, #ff6b9d 100%);
border: none;
}
.result-share-footer-btn::after {
border: none;
}

View File

@@ -46,18 +46,24 @@ Page({
testResultId: null,
shareToken: '',
hasReloadedAfterPay: false,
fromShare: false
},
onLoad(options) {
const id = options && options.id
const type = options && options.type
const fromShareFs =
options && (String(options.fs) === '1' || options.from === 'share')
const id = options && options.id != null && options.id !== '' ? String(options.id) : ''
const st = options && options.st ? String(options.st).trim() : ''
const type = options && options.type ? String(options.type).toLowerCase() : ''
if (id && type === 'pdp') {
this.setData({ testResultId: id })
if (options.st) {
this.loadShareDetail(id, options.st)
} else {
this.loadDetail(id)
}
this.setData({ testResultId: id, fromShare: !!fromShareFs })
this.loadShareDetail(id, st, 'pdp')
return
}
if (id && st) {
this.setData({ testResultId: id, fromShare: true })
this.loadShareDetail(id, st, '')
return
}
const result = wx.getStorageSync('pdpResult')
@@ -89,7 +95,11 @@ Page({
isPaid,
amountYuan: needPaymentToUnlock ? amountYuan : 0
}
this.setData({ payInfo })
const patch = { payInfo }
if (payload.id != null && payload.id !== '') {
patch.testResultId = String(payload.id)
}
this.setData(patch)
},
loadDetail(id) {
@@ -114,14 +124,17 @@ Page({
})
},
loadShareDetail(id, st) {
loadShareDetail(id, st, testType) {
const apiBase = app.globalData?.apiBase || ''
if (!apiBase) { wx.showToast({ title: '配置异常', icon: 'none' }); return }
const data = { id: String(id) }
if (st) data.st = st
if (testType) data.type = testType
wx.showLoading({ title: '加载中...' })
wx.request({
url: `${apiBase}/api/test/share-detail`,
method: 'GET',
data: { id, st },
data,
success: (res) => {
if (res.statusCode === 200 && res.data && res.data.code === 200) {
this.applyDetailPayload(res.data.data || {})
@@ -190,6 +203,10 @@ Page({
wx.navigateTo({ url: '/pages/test/pdp' })
},
goWantTest() {
wx.navigateTo({ url: '/pages/test/pdp' })
},
goHome() {
const scope = (getApp().globalData && getApp().globalData.appScope) || 'personal'
if (scope === 'enterprise') {
@@ -206,8 +223,7 @@ Page({
title: `我的PDP类型是${line},来测测你的吧!`,
path: getResultSharePath('/pages/result/pdp', {
id: this.data.testResultId,
type: 'pdp',
shareToken: this.data.shareToken
type: 'pdp'
})
}
},
@@ -219,8 +235,7 @@ Page({
title: `我的PDP类型是${line},来测测你的吧!`,
query: getResultShareTimelineQuery({
id: this.data.testResultId,
type: 'pdp',
shareToken: this.data.shareToken
type: 'pdp'
})
}
}

View File

@@ -1,7 +1,7 @@
<!--pages/result/pdp.wxml - PDP结果按旧版模板重构-->
<view class="result-page">
<scroll-view class="content-scroll" scroll-y>
<view class="content-container">
<scroll-view class="content-scroll {{fromShare ? 'content-scroll--with-fixed-footer' : ''}}" scroll-y>
<view class="content-container {{fromShare ? 'content-container--with-fixed-footer' : ''}}">
<view class="type-card">
<view class="type-header">
<text class="type-label">PDP性格类型</text>
@@ -26,7 +26,7 @@
</view>
</view>
<view class="paywall-card" wx:elif="{{result && result.locked}}">
<view class="paywall-card" wx:elif="{{result && result.locked && !fromShare}}">
<view class="paywall-content">
<text class="paywall-fake-title">完整报告需完善资料</text>
<text class="paywall-fake-line">请补全头像、昵称并绑定手机号后查看完整 PDP 报告。</text>
@@ -94,4 +94,7 @@
</view> -->
</view>
</scroll-view>
<view class="result-share-footer" wx:if="{{fromShare}}">
<button class="result-share-footer-btn" bindtap="goWantTest">我也要测试</button>
</view>
</view>

View File

@@ -15,6 +15,14 @@
padding-bottom: 64rpx;
}
.content-container--with-fixed-footer {
padding-bottom: calc(64rpx + 120rpx + env(safe-area-inset-bottom));
}
.content-scroll--with-fixed-footer {
height: 100vh;
}
.type-card,
.analysis-card {
background-color: #fff;
@@ -209,3 +217,35 @@
.paywall-btn { position: absolute; left: 5%; right: 5%; width: 90%; bottom: 132rpx; z-index: 3; padding: 20rpx 0; border-radius: 999rpx; background: linear-gradient(135deg, #F59E0B 0%, #fbbf24 100%); box-shadow: 0 8rpx 24rpx rgba(245,158,11,0.35); display: flex; flex-direction: row; align-items: baseline; justify-content: center; gap: 12rpx; }
.paywall-btn-main { font-size: 30rpx; color: #fff; font-weight: 600; }
.paywall-btn-price { font-size: 24rpx; color: #ffe5f0; }
.result-share-footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 200;
box-sizing: border-box;
width: 100%;
padding: 20rpx 32rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: #f5f5f5;
box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
}
.result-share-footer-btn {
width: 100% !important;
box-sizing: border-box;
display: block;
margin: 0;
padding: 0;
min-height: 96rpx;
line-height: 96rpx;
border-radius: 999rpx;
font-size: 30rpx;
font-weight: 600;
color: #fff;
background: linear-gradient(135deg, #d97706 0%, #fbbf24 100%);
border: none;
}
.result-share-footer-btn::after {
border: none;
}

View File

@@ -29,12 +29,29 @@ Page({
const fileUrl = options.fileUrl ? decodeURIComponent(options.fileUrl) : ''
this.setData({ fileUrl })
// 从历史记录进入:直接读已存的数据,不重新生成
if (options.id && options.type === 'resume') {
this.loadFromHistory(options.id)
} else {
this.fetchResumeAnalysis()
const run = () => {
if (options.id && options.type === 'resume') {
this.loadFromHistory(options.id)
} else {
this.fetchResumeAnalysis()
}
}
// 分享直达:需 token 调 detail / analyze等静默登录完成再请求避免白屏
app.ensureLogin()
.then((ok) => {
if (!ok) {
this.setData({
loading: false,
error: '请先登录后查看报告;若已登录请下拉重试或返回重进。'
})
return
}
run()
})
.catch(() => {
this.setData({ loading: false, error: '登录失败,请返回重试。' })
})
},
onShow() {

View File

@@ -54,20 +54,28 @@ Page({
testResultId: null,
shareToken: '',
hasReloadedAfterPay: false,
hasPhone: false
hasPhone: false,
/** 分享落地path 带 fs=1 或旧版仅 id+st用于隐藏「去完善资料」、展示底部「我也要测试」 */
fromShare: false
},
onLoad(options) {
const id = options && options.id
const type = options && options.type
const fromShareFs =
options && (String(options.fs) === '1' || options.from === 'share')
const id = options && options.id != null && options.id !== '' ? String(options.id) : ''
const st = options && options.st ? String(options.st).trim() : ''
const type = options && options.type ? String(options.type).toLowerCase() : ''
// id + type免登录拉取/api/test/share-detail
if (id && type === 'sbti') {
this.setData({ testResultId: id })
if (options.st) {
this.loadShareDetail(id, options.st)
} else {
this.loadDetail(id)
}
this.setData({ testResultId: id, fromShare: !!fromShareFs })
this.loadShareDetail(id, st, 'sbti')
return
}
// 旧版分享仅 id + st
if (id && st) {
this.setData({ testResultId: id, fromShare: true })
this.loadShareDetail(id, st, '')
return
}
@@ -108,10 +116,14 @@ Page({
isPaid,
amountYuan: needPaymentToUnlock ? amountYuan : 0
}
this.setData({
const patch = {
payInfo,
shareToken: payload.shareToken || ''
})
}
if (payload.id != null && payload.id !== '') {
patch.testResultId = String(payload.id)
}
this.setData(patch)
},
loadDetail(id) {
@@ -139,17 +151,20 @@ Page({
})
},
loadShareDetail(id, st) {
loadShareDetail(id, st, testType) {
const apiBase = app.globalData?.apiBase || ''
if (!apiBase) {
wx.showToast({ title: '配置异常', icon: 'none' })
return
}
const data = { id: String(id) }
if (st) data.st = st
if (testType) data.type = testType
wx.showLoading({ title: '加载中...' })
wx.request({
url: `${apiBase}/api/test/share-detail`,
method: 'GET',
data: { id, st },
data,
success: (res) => {
if (res.statusCode === 200 && res.data && res.data.code === 200) {
this.applyDetailPayload(res.data.data || {})
@@ -262,6 +277,11 @@ Page({
wx.navigateTo({ url: '/pages/test/sbti' })
},
/** 分享落地页:引导好友自己做测试 */
goWantTest() {
wx.navigateTo({ url: '/pages/test/sbti' })
},
goHome() {
const scope = (getApp().globalData && getApp().globalData.appScope) || 'personal'
if (scope === 'enterprise') {
@@ -281,8 +301,7 @@ Page({
title: `我的 SBTI 类型是 ${code}${label}),来测测你的吧!`,
path: getResultSharePath('/pages/result/sbti', {
id: this.data.testResultId,
type: 'sbti',
shareToken: this.data.shareToken
type: 'sbti'
}),
imageUrl: img
}
@@ -297,8 +316,7 @@ Page({
title: `我的 SBTI 类型是 ${code}${label}),来测测你的吧!`,
query: getResultShareTimelineQuery({
id: this.data.testResultId,
type: 'sbti',
shareToken: this.data.shareToken
type: 'sbti'
})
}
}

View File

@@ -1,7 +1,7 @@
<!--pages/result/sbti.wxml-->
<view class="result-page">
<scroll-view class="content-scroll" scroll-y>
<view class="content-container">
<scroll-view class="content-scroll {{fromShare ? 'content-scroll--with-fixed-footer' : ''}}" scroll-y>
<view class="content-container {{fromShare ? 'content-container--with-fixed-footer' : ''}}">
<!-- 主类型卡片:有图且图加载成功时隐藏顶部三行(与图内文案重复);无图/失败/加载中仍显示 -->
<view class="card main-type-card">
@@ -72,8 +72,8 @@
</view>
</view>
<!-- 强制完善资料卡片 -->
<view class="card paywall-card" wx:elif="{{result && result.locked}}">
<!-- 强制完善资料卡片(分享落地 fromShare 不展示,避免引导访客完善自己的资料) -->
<view class="card paywall-card" wx:elif="{{result && result.locked && !fromShare}}">
<view class="paywall-content">
<text class="paywall-fake-title">完整报告需完善资料</text>
<text class="paywall-fake-line">请补全头像、昵称并绑定手机号后查看完整解读。</text>
@@ -107,4 +107,7 @@
</view>
</view>
</scroll-view>
<view class="result-share-footer" wx:if="{{fromShare}}">
<button class="result-share-footer-btn" bindtap="goWantTest">我也要测试</button>
</view>
</view>

View File

@@ -15,6 +15,15 @@
padding-bottom: 64rpx;
}
/* 底部固定「我也要测试」时,为滚动内容预留空间(按钮约 96rpx + 安全区) */
.content-container--with-fixed-footer {
padding-bottom: calc(64rpx + 120rpx + env(safe-area-inset-bottom));
}
.content-scroll--with-fixed-footer {
height: 100vh;
}
.card {
background-color: #fff;
border-radius: 24rpx;
@@ -268,3 +277,40 @@
font-weight: 400;
line-height: 1.65;
}
/* 分享落地:底部固定条 */
.result-share-footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 200;
box-sizing: border-box;
width: 100%;
padding: 20rpx 32rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: #f2f7f3;
box-shadow: 0 -8rpx 24rpx rgba(45, 61, 53, 0.08);
}
/* 与 SBTI 答题页主按钮pages/test/sbti主题绿一致 */
.result-share-footer-btn {
width: 100% !important;
box-sizing: border-box;
display: block;
margin: 0;
padding: 0;
min-height: 96rpx;
line-height: 96rpx;
border-radius: 999rpx;
font-size: 30rpx;
font-weight: 600;
color: #fff;
background: linear-gradient(135deg, #5a7268 0%, #6f8a7e 100%);
border: none;
box-shadow: 0 8rpx 24rpx rgba(90, 114, 104, 0.28);
}
.result-share-footer-btn::after {
border: none;
}

View File

@@ -7,7 +7,9 @@ Page({
permMbti: true,
permSbti: true,
permPdp: true,
permDisc: true
permDisc: true,
/** 四类入口均被企业权限关闭时提示,避免误以为白屏 */
allTestsDisabled: false
},
onLoad(options) {
@@ -17,6 +19,8 @@ Page({
try {
wx.showShareMenu({ withShareTicket: true, menus: ['shareAppMessage', 'shareTimeline'] })
} catch (e) {}
// 分享直达时尽早完成静默登录,避免用户点进子页时仍无 token
app.ensureLogin().catch(() => {})
this._syncPerms()
},
@@ -26,12 +30,18 @@ Page({
_syncPerms() {
const p = app.globalData.enterprisePermissions
const permFace = !p || p.face !== false
const permMbti = !p || p.mbti !== false
const permSbti = !p || p.sbti !== false
const permPdp = !p || p.pdp !== false
const permDisc = !p || p.disc !== false
this.setData({
permFace: !p || p.face !== false,
permMbti: !p || p.mbti !== false,
permSbti: !p || p.sbti !== false,
permPdp: !p || p.pdp !== false,
permDisc: !p || p.disc !== false
permFace,
permMbti,
permSbti,
permPdp,
permDisc,
allTestsDisabled: p && !permMbti && !permSbti && !permPdp && !permDisc
})
},

View File

@@ -48,4 +48,8 @@
</view>
<text class="entry-arrow">→</text>
</view>
<view class="card perm-disabled-hint" wx:if="{{allTestsDisabled}}">
<text class="perm-disabled-text">当前环境下暂未开放问卷测试,请联系管理员或从首页进入。</text>
</view>
</view>

View File

@@ -35,6 +35,17 @@
line-height: 1.6;
}
.perm-disabled-hint {
text-align: center;
padding: 32rpx;
}
.perm-disabled-text {
font-size: 28rpx;
color: #888;
line-height: 1.6;
}
/* 入口卡片(与 result 页 next-btn 同色系) */
.entry-card {
display: flex;

View File

@@ -28,8 +28,17 @@ Page({
try {
require('../../utils/thirdPartyContext.js').ingestThirdPartyOnPageLoad(options || {}, app)
} catch (e) {}
loadQuestions('disc', {})
app.ensureLogin()
.then((ok) => {
if (!ok) {
this.setData({ loading: false })
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
return Promise.reject(new Error('login'))
}
return loadQuestions('disc', {})
})
.then((questions) => {
if (!questions) return
if (!questions.length) {
wx.showToast({ title: '暂无题目', icon: 'none' })
this.setData({ loading: false })
@@ -53,6 +62,7 @@ Page({
this.startTimer()
})
.catch((err) => {
if (err && err.message === 'login') return
this.setData({ loading: false })
wx.showToast({ title: (err && err.message) || '加载失败', icon: 'none' })
})
@@ -189,10 +199,17 @@ Page({
wx.setStorageSync('discResult', resultData)
try { require('../../utils/analytics').track('test_complete', { type: 'disc', result: dominantType, duration: resultData.testDuration }) } catch (e) {}
if (app && typeof app.saveTestResult === 'function') {
app.saveTestResult('disc', resultData)
app.saveTestResult('disc', resultData).then((extra) => {
const rid = extra && extra.id
if (rid) {
wx.redirectTo({ url: `/pages/result/disc?id=${rid}&type=disc` })
} else {
wx.redirectTo({ url: '/pages/result/disc' })
}
})
} else {
wx.redirectTo({ url: '/pages/result/disc' })
}
wx.redirectTo({ url: '/pages/result/disc' })
},
onShareAppMessage() {

View File

@@ -30,8 +30,18 @@ Page({
try {
require('../../utils/thirdPartyContext.js').ingestThirdPartyOnPageLoad(options || {}, app)
} catch (e) {}
loadQuestions('mbti', {})
// 分享直达本页时 silentLogin 可能尚未完成,须先 ensureLogin 再拉题,否则 401 → 白屏
app.ensureLogin()
.then((ok) => {
if (!ok) {
this.setData({ loading: false })
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
return Promise.reject(new Error('login'))
}
return loadQuestions('mbti', {})
})
.then((questions) => {
if (!questions) return
const total = questions.length
if (!total) {
wx.showToast({ title: '暂无题目', icon: 'none' })
@@ -58,6 +68,7 @@ Page({
this.startTimer()
})
.catch((err) => {
if (err && err.message === 'login') return
this.setData({ loading: false })
wx.showToast({ title: (err && err.message) || '加载失败', icon: 'none' })
})
@@ -229,11 +240,15 @@ Page({
timestamp: new Date().toISOString()
}
wx.setStorageSync('mbtiResult', resultData)
app.saveTestResult('mbti', resultData)
try { require('../../utils/analytics').track('test_complete', { type: 'mbti', result: result.mbtiType, confidence: result.confidence, duration: resultData.testDuration }) } catch (e) {}
wx.redirectTo({
url: '/pages/result/mbti'
app.saveTestResult('mbti', resultData).then((extra) => {
const rid = extra && extra.id
if (rid) {
wx.redirectTo({ url: `/pages/result/mbti?id=${rid}&type=mbti` })
} else {
wx.redirectTo({ url: '/pages/result/mbti' })
}
})
},

View File

@@ -28,8 +28,17 @@ Page({
try {
require('../../utils/thirdPartyContext.js').ingestThirdPartyOnPageLoad(options || {}, app)
} catch (e) {}
loadQuestions('pdp', {})
app.ensureLogin()
.then((ok) => {
if (!ok) {
this.setData({ loading: false })
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
return Promise.reject(new Error('login'))
}
return loadQuestions('pdp', {})
})
.then((questions) => {
if (!questions) return
if (!questions.length) {
wx.showToast({ title: '暂无题目', icon: 'none' })
this.setData({ loading: false })
@@ -53,6 +62,7 @@ Page({
this.startTimer()
})
.catch((err) => {
if (err && err.message === 'login') return
this.setData({ loading: false })
wx.showToast({ title: (err && err.message) || '加载失败', icon: 'none' })
})
@@ -192,10 +202,17 @@ Page({
wx.setStorageSync('pdpResult', resultData)
try { require('../../utils/analytics').track('test_complete', { type: 'pdp', result: dominantType, duration: resultData.testDuration }) } catch (e) {}
if (app && typeof app.saveTestResult === 'function') {
app.saveTestResult('pdp', resultData)
app.saveTestResult('pdp', resultData).then((extra) => {
const rid = extra && extra.id
if (rid) {
wx.redirectTo({ url: `/pages/result/pdp?id=${rid}&type=pdp` })
} else {
wx.redirectTo({ url: '/pages/result/pdp' })
}
})
} else {
wx.redirectTo({ url: '/pages/result/pdp' })
}
wx.redirectTo({ url: '/pages/result/pdp' })
},
onShareAppMessage() {

View File

@@ -27,9 +27,18 @@ Page({
try {
require('../../utils/thirdPartyContext.js').ingestThirdPartyOnPageLoad(options || {}, app)
} catch (e) {}
fetchQuestionBank('sbti', {})
app.ensureLogin()
.then((ok) => {
if (!ok) {
this.setData({ loading: false })
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
return Promise.reject(new Error('login'))
}
return fetchQuestionBank('sbti', {})
})
.then((all) => {
if (!all || !all.length) {
if (!all) return
if (!all.length) {
wx.showToast({ title: '暂无题目', icon: 'none' })
this.setData({ loading: false })
return
@@ -62,6 +71,7 @@ Page({
this.startTimer()
})
.catch((err) => {
if (err && err.message === 'login') return
this.setData({ loading: false })
wx.showToast({ title: (err && err.message) || '加载失败', icon: 'none' })
})
@@ -236,7 +246,6 @@ Page({
timestamp: new Date().toISOString()
}
wx.setStorageSync('sbtiResult', resultData)
app.saveTestResult('sbti', resultData)
try {
require('../../utils/analytics').track('test_complete', {
type: 'sbti',
@@ -245,8 +254,13 @@ Page({
})
} catch (e) {}
wx.redirectTo({
url: '/pages/result/sbti'
app.saveTestResult('sbti', resultData).then((extra) => {
const rid = extra && extra.id
if (rid) {
wx.redirectTo({ url: `/pages/result/sbti?id=${rid}&type=sbti` })
} else {
wx.redirectTo({ url: '/pages/result/sbti' })
}
})
},