Files
MBTI_wang/miniprogram/pages/result/sbti.js
Ghost a411bf7b21 高考版优化
审核模式优化
2026-04-29 18:01:26 +08:00

531 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// pages/result/sbti.js — SBTI 结果页
const app = getApp()
const payment = require('../../utils/payment')
const {
hasPhone,
bindPhoneByCode,
needsResultProfileGate,
navigateToCompleteProfileAfterPhoneIfNeeded
} = require('../../utils/phoneAuth.js')
const unlockGate = require('../../utils/unlockGate.js')
const inviteCodeGate = require('../../utils/inviteCodeGate.js')
const { shouldHideInviteCodeEntry } = require('../../utils/miniprogramAuditGate.js')
const {
slicePreviewText,
openTimelineShareHint
} = require('../../utils/resultProfileGate.js')
const { decorateSbtiDims, groupSbtiDims, splitSbtiDesc } = require('../../utils/sbtiDisplay.js')
const { TYPE_IMAGES } = require('../../utils/sbtiEngine.js')
const {
computeJourney,
markShared,
markCamera
} = require('../../utils/resultJourneyState.js')
const resultScrollSync = require('../../utils/resultSectionScrollSync.js')
/** 根据结果类型代码取展示图(与 sbtiData.TYPE_IMAGES 一致) */
/** 旧版结果仅有 badge / bestNormal补全 matchPercent、hitDimCount */
function normalizeSbtiResultForDisplay(result) {
if (!result || typeof result !== 'object') return result
const out = { ...result }
const bn = out.bestNormal
if (out.matchPercent == null && bn != null && typeof bn.similarity === 'number') {
out.matchPercent = bn.similarity
}
if (out.hitDimCount == null && bn != null && typeof bn.exact === 'number') {
out.hitDimCount = bn.exact
}
if (out.special && out.sbtiType === 'DRUNK') {
if (out.matchPercent == null) out.matchPercent = 100
if (out.hitDimCount == null) out.hitDimCount = 15
}
return out
}
function resolveSbtiTypeImageUrl(result) {
if (!result || typeof result !== 'object') return ''
const code = result.sbtiType || (result.finalType && result.finalType.code) || ''
if (!code || !TYPE_IMAGES) return ''
const url = TYPE_IMAGES[code]
return typeof url === 'string' ? url : ''
}
Page({
data: {
result: null,
typeImageUrl: '',
/** 为 true 时展示顶部文字备用(无图地址或图片加载失败) */
typeImageLoadFailed: false,
/** 类型图是否已成功加载(有 URL 且 bindload 触发后为 true用于隐藏与图重复的顶部三行字 */
typeImageLoaded: false,
dimExplainList: [],
payInfo: {
requiresPayment: false,
isPaid: false,
amountYuan: 0
},
testResultId: null,
shareToken: '',
hasReloadedAfterPay: false,
hasPhone: false,
/** 分享落地path 带 fs=1 或旧版仅 id+st用于隐藏「登录解锁」区、展示底部「我也要测试」 */
fromShare: false,
profileGate: false,
previewSbtiIntro: '',
previewSbtiDesc: '',
previewSbtiDescParts: [],
descParts: [],
dimGroups: [],
openGroup: '',
journey: { step1Unlocked: false, step2Unlocked: false, step3Unlocked: false, activeStep: 1 },
sectionNav: [
{ id: 'sec-hero', label: 'SBTI 画像', emoji: '🎭' },
{ id: 'sec-desc', label: '人格描述', emoji: '📝' },
{ id: 'sec-dim', label: '十五维度', emoji: '📊' },
{ id: 'sec-cta', label: '深度方案', emoji: '💎' }
],
scrollTarget: '',
activeSection: '',
showInviteCodeDialog: false,
hideInviteCodeEntry: false
},
onTapSectionNav(e) {
const id = e && e.detail && e.detail.id
if (!id) return
this.setData({ scrollTarget: '', activeSection: id }, () => {
this.setData({ scrollTarget: id })
})
},
onSectionScroll(e) {
resultScrollSync.onScroll(this, e)
},
_syncJourney() {
this.setData({
journey: computeJourney({
profileGate: !!this.data.profileGate,
payRequired: !!(this.data.payInfo && this.data.payInfo.requiresPayment),
isPaid: !!(this.data.payInfo && this.data.payInfo.isPaid)
})
})
},
onLoad(options) {
this._vscope = options && options.vscope ? String(options.vscope) : ''
try {
wx.showShareMenu({ withShareTicket: true, menus: ['shareAppMessage', 'shareTimeline'] })
} catch (e) {}
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, fromShare: !!fromShareFs })
this.loadShareDetail(id, st, 'sbti')
return
}
// 旧版分享仅 id + st
if (id && st) {
this.setData({ testResultId: id, fromShare: true })
this.loadShareDetail(id, st, '')
return
}
const raw = wx.getStorageSync('sbtiResult')
if (raw) {
this.applyResult(raw)
this.initPayInfoFromRuntime('sbti')
} else {
wx.showToast({ title: '暂无测试结果', icon: 'none' })
setTimeout(() => wx.navigateBack(), 1500)
}
},
onShow() {
const gd = app.globalData || {}
this.setData({
hasPhone: hasPhone(),
hideInviteCodeEntry: shouldHideInviteCodeEntry(gd)
})
if (this.data.testResultId && this.data.result) {
this.applyResult(this.data.result)
} else if (!this.data.testResultId) {
const raw = wx.getStorageSync('sbtiResult')
if (raw) this.applyResult(raw)
}
this._syncJourney()
},
goCompleteProfile() {
try { require('../../utils/analytics').track('tap_complete_profile', { from: 'sbti' }) } catch (e) {}
wx.navigateTo({ url: '/pages/user-profile/index' })
},
applyDetailPayload(payload) {
const data = payload.data || payload
const isPaid = !!payload.isPaid
const paidAmount = payload.paidAmount != null ? Number(payload.paidAmount) : 0
const amountYuan = payload.amountYuan != null ? Number(payload.amountYuan) : (paidAmount > 0 ? paidAmount / 100 : 0)
const needPaymentToUnlock = payload.needPaymentToUnlock === true || (!!payload.requiresPayment && !isPaid && paidAmount > 0)
this.applyResult(data)
const payInfo = {
requiresPayment: needPaymentToUnlock,
isPaid,
amountYuan: needPaymentToUnlock ? amountYuan : 0
}
const patch = {
payInfo,
shareToken: payload.shareToken || ''
}
if (payload.id != null && payload.id !== '') {
patch.testResultId = String(payload.id)
}
this.setData(patch)
this._reportPaywallOnce('sbti', payInfo)
},
loadDetail(id) {
const apiBase = app.globalData?.apiBase || ''
const token = app.globalData?.token || wx.getStorageSync('token') || ''
if (!apiBase) {
wx.showToast({ title: '配置异常', icon: 'none' })
return
}
wx.showLoading({ title: '加载中...' })
wx.request({
url: `${apiBase}/api/test/detail`,
method: 'GET',
header: token ? { Authorization: `Bearer ${token}` } : {},
data: {
id,
...(this._vscope === 'gaokao' || app.globalData.appScope === 'gaokao'
? { scope: 'gaokao' }
: {})
},
success: (res) => {
if (res.statusCode === 200 && res.data && res.data.code === 200) {
this.applyDetailPayload(res.data.data || {})
} else {
wx.showToast({ title: res.data?.message || '加载失败', icon: 'none' })
}
},
fail: () => wx.showToast({ title: '网络错误', icon: 'none' }),
complete: () => wx.hideLoading()
})
},
loadShareDetail(id, st, testType) {
const apiBase = app.globalData?.apiBase || ''
if (!apiBase) {
wx.showToast({ title: '配置异常', icon: 'none' })
return
}
const data = {
id: String(id),
...(this._vscope === 'gaokao' || app.globalData.appScope === 'gaokao'
? { scope: 'gaokao' }
: {})
}
if (st) data.st = st
if (testType) data.type = testType
wx.showLoading({ title: '加载中...' })
wx.request({
url: `${apiBase}/api/test/share-detail`,
method: 'GET',
data,
success: (res) => {
if (res.statusCode === 200 && res.data && res.data.code === 200) {
this.applyDetailPayload(res.data.data || {})
} else {
wx.showToast({ title: res.data?.message || '分享链接无效或已失效', icon: 'none' })
}
},
fail: () => wx.showToast({ title: '网络错误', icon: 'none' }),
complete: () => wx.hideLoading()
})
},
applyResult(result) {
if (!result) return
const normalized = normalizeSbtiResultForDisplay(result)
const fromShare = !!this.data.fromShare
const profileGate = needsResultProfileGate(fromShare)
const src = Array.isArray(normalized.dimExplainList) ? normalized.dimExplainList : []
const allowDims = profileGate || !normalized.locked
let dimExplainList = []
if (src.length && allowDims) {
dimExplainList = profileGate
? src.slice(0, Math.max(1, Math.ceil(src.length * 0.3)))
: src
}
dimExplainList = decorateSbtiDims(dimExplainList)
const dimGroups = groupSbtiDims(dimExplainList)
const descParts = profileGate ? [] : splitSbtiDesc(normalized.desc || '', 12)
const previewSbtiDescParts = profileGate ? splitSbtiDesc(slicePreviewText(normalized.desc || '', 0.3), 4) : []
const typeImageUrl = resolveSbtiTypeImageUrl(normalized)
this.setData({
result: normalized,
profileGate,
previewSbtiIntro: slicePreviewText(normalized.intro || '', 0.3),
previewSbtiDesc: slicePreviewText(normalized.desc || '', 0.3),
previewSbtiDescParts,
descParts,
dimExplainList,
dimGroups,
openGroup: dimGroups[0] ? dimGroups[0].key : '',
typeImageUrl,
typeImageLoadFailed: false,
typeImageLoaded: false
})
this._syncJourney()
},
toggleGroup(e) {
const key = e.currentTarget.dataset.key
if (!key) return
this.setData({ openGroup: this.data.openGroup === key ? '' : key })
},
onTapDeepService() {
try { require('../../utils/analytics').track('tap_deep_service_from_sbti', {}) } catch (e) {}
wx.navigateTo({ url: '/pages/purchase/index' })
},
onTapPromoCenter() {
try { require('../../utils/analytics').track('tap_promo_from_sbti', {}) } catch (e) {}
wx.navigateTo({ url: '/pages/promo/index' })
},
onPhoneLoginForResultGate(e) {
const { code, errMsg } = e.detail || {}
if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) {
wx.showToast({ title: '需要授权手机号才能查看完整报告', icon: 'none' })
return
}
if (!code) {
wx.showToast({ title: '获取手机号失败', icon: 'none' })
return
}
bindPhoneByCode(code)
.then(() => {
this.setData({ hasPhone: hasPhone() })
const r = this.data.result || wx.getStorageSync('sbtiResult')
if (r) this.applyResult(r)
navigateToCompleteProfileAfterPhoneIfNeeded()
})
.catch(() => {})
},
onTapReadFull() {
try { require('../../utils/analytics').track('tap_read_full', { type: 'sbti' }) } catch (e) {}
if (this.data.profileGate) {
unlockGate.scrollToUnlockAnchor(this, { scrollIntoView: true })
wx.showToast({
title: this.data.hasPhone ? '请先完善头像与昵称' : '请在上滑区域内完成手机号授权',
icon: 'none'
})
return
}
if (this.data.payInfo.requiresPayment && !this.data.payInfo.isPaid) {
this.unlockFullReport()
return
}
wx.showToast({ title: '当前已是完整报告', icon: 'none' })
},
onTapShareMoment() {
try { require('../../utils/analytics').track('tap_share_moment', { type: 'sbti' }) } catch (e) {}
if (!this.data.journey.step1Unlocked) {
wx.showToast({ title: '请先解锁全文', icon: 'none' })
this.onTapReadFull()
return
}
markShared()
this._syncJourney()
openTimelineShareHint()
},
onTapFaceCamera() {
try { require('../../utils/analytics').track('tap_face_camera', { from: 'sbti' }) } catch (e) {}
if (!this.data.journey.step2Unlocked) {
wx.showToast({ title: '请先分享朋友圈', icon: 'none' })
return
}
markCamera()
this._syncJourney()
wx.switchTab({ url: '/pages/index/camera' })
},
goReadFullFromShare() {
try { require('../../utils/analytics').track('tap_read_full', { type: 'sbti', from: 'share' }) } catch (e) {}
wx.switchTab({ url: '/pages/profile/index' })
},
onTypeImageLoad() {
this.setData({ typeImageLoadFailed: false, typeImageLoaded: true })
},
onTypeImageError() {
this.setData({ typeImageLoadFailed: true, typeImageLoaded: false })
},
_reportPaywallOnce(testType, payInfo) {
if (!payInfo || !payInfo.requiresPayment || payInfo.isPaid) return
if (this._paywallReported) return
this._paywallReported = true
try {
require('../../utils/analytics').track('paywall_view', { type: testType, amountYuan: payInfo.amountYuan })
} catch (e) {}
},
initPayInfoFromRuntime(testType) {
app.getRuntimeConfig()
.then((cfg) => {
const pricing = cfg.pricing || {}
const reportRequires = cfg.reportRequiresPayment || {}
const requiresPayment = !!(reportRequires && reportRequires[testType])
const amountYuan = Number(pricing[testType]) || (requiresPayment ? 1.98 : 0)
const payInfo = { requiresPayment, isPaid: false, amountYuan }
this.setData({ payInfo })
this._reportPaywallOnce(testType, payInfo)
this._syncJourney()
})
.catch(() => {
this.setData({
payInfo: { requiresPayment: false, isPaid: false, amountYuan: 0 }
})
})
},
unlockFullReport() {
const { payInfo, testResultId, hasReloadedAfterPay } = this.data
if (!payInfo.requiresPayment || payInfo.isPaid) return
try { require('../../utils/analytics').track('tap_unlock_full', { type: 'sbti', amountYuan: payInfo.amountYuan }) } catch (e) {}
app.ensureLogin && app.ensureLogin().then((logged) => {
if (!logged) {
wx.showToast({ title: '请先登录', icon: 'none' })
return
}
unlockGate.ensureUnlockPrerequisitesBeforePay(this, { scrollIntoView: true }).then((ok) => {
if (!ok) return
inviteCodeGate.ensureInviteCodeGate(this).then((go) => {
if (!go) return
payment.purchaseSbtiTest({
testResultId: testResultId || undefined,
success: () => {
wx.showToast({ title: '已解锁完整报告', icon: 'success' })
this.setData({ 'payInfo.isPaid': true })
this._syncJourney()
if (testResultId && !hasReloadedAfterPay) {
this.setData({ hasReloadedAfterPay: true })
setTimeout(() => this.loadDetail(testResultId), 500)
}
},
fail: () => {}
})
})
})
})
},
openInviteCodeFill() {
app.ensureLogin().then((ok) => {
if (!ok) {
wx.showToast({ title: '请先登录', icon: 'none' })
return
}
inviteCodeGate.openInviteCodeDialog(this)
})
},
onInviteCodeSkip() {
inviteCodeGate.finishInviteCodeGate(this, true)
},
onInviteCodeSuccess() {
inviteCodeGate.finishInviteCodeGate(this, true)
},
onGetPhoneNumberForSbtiPay(e) {
const { code, errMsg } = e.detail || {}
if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) {
if (!hasPhone()) {
wx.showToast({ title: '需要授权手机号才能继续', icon: 'none' })
return
}
this.unlockFullReport()
return
}
if (!code) {
if (hasPhone()) {
this.unlockFullReport()
} else {
wx.showToast({ title: '获取手机号失败', icon: 'none' })
}
return
}
bindPhoneByCode(code)
.then(() => {
this.setData({ hasPhone: hasPhone() })
this.unlockFullReport()
})
.catch(() => {})
},
retakeTest() {
if (!this.data.testResultId) {
wx.removeStorageSync('sbtiResult')
}
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') {
wx.navigateTo({ url: '/pages/enterprise/index' })
} else {
wx.switchTab({ url: '/pages/index/index' })
}
},
onShareAppMessage() {
const result = this.data.result
const { getResultSharePath } = require('../../utils/share')
const label = result?.sbtiCn || result?.finalType?.cn || 'SBTI'
const code = result?.sbtiType || result?.finalType?.code || ''
const img = this.data.typeImageUrl || '/images/share-mbti.png'
return {
title: `我的 SBTI 类型是 ${code}${label}),来测测你的吧!`,
path: getResultSharePath('/pages/result/sbti', {
id: this.data.testResultId,
type: 'sbti'
}),
imageUrl: img
}
},
onShareTimeline() {
const result = this.data.result
const { getResultShareTimelineQuery } = require('../../utils/share')
const label = result?.sbtiCn || result?.finalType?.cn || 'SBTI'
const code = result?.sbtiType || result?.finalType?.code || ''
return {
title: `我的 SBTI 类型是 ${code}${label}),来测测你的吧!`,
query: getResultShareTimelineQuery({
id: this.data.testResultId,
type: 'sbti'
})
}
}
})