feat: 小程序与管理端迭代(神仙AI、了解自己CRM、AI测试入口、报表与分润等)

Made-with: Cursor
This commit is contained in:
卡若
2026-04-17 19:46:48 +08:00
parent cf835ea585
commit 7108f28280
239 changed files with 21061 additions and 2175 deletions

View File

@@ -2,11 +2,24 @@
const app = getApp()
const payment = require('../../utils/payment')
const { hasPhone, bindPhoneByCode, isReportProfileComplete } = require('../../utils/phoneAuth.js')
function toProfileLockedMbti(full) {
if (!full) return full
return { mbtiType: full.mbtiType || full.mbti || '', locked: true }
}
const {
slicePreviewText,
slicePreviewList,
openTimelineShareHint
} = require('../../utils/resultProfileGate.js')
const {
getMbtiInsight,
getMbtiCategoryTags,
getMbtiFields,
getMbtiGrowth,
decorateCareers
} = require('../../utils/mbtiInsights.js')
const {
computeJourney,
markShared,
markCamera
} = require('../../utils/resultJourneyState.js')
const resultScrollSync = require('../../utils/resultSectionScrollSync.js')
Page({
data: {
@@ -14,12 +27,19 @@ Page({
dimensions: [],
mbtiDesc: {
title: '',
category: '',
description: '',
strengths: [],
weaknesses: [],
careers: [],
relationships: ''
},
mbtiInsight: null,
mbtiTags: [],
mbtiCareerItems: [],
mbtiFields: [],
mbtiGrowth: [],
journey: { step1Unlocked: false, step2Unlocked: false, step3Unlocked: false, activeStep: 1 },
payInfo: {
requiresPayment: false,
isPaid: false,
@@ -30,10 +50,41 @@ Page({
shareToken: '',
hasReloadedAfterPay: false,
hasPhone: false,
fromShare: false
fromShare: false,
/** 未完善头像+昵称+手机且非分享落地时,展示约 30% 预览 */
profileGate: false,
previewDescription: '',
previewStrengths: [],
/** 分类锚点导航(顶部 chip 条) */
sectionNav: [
{ id: 'sec-hero', label: '性格画像', emoji: '🪐' },
{ id: 'sec-insight', label: '深度洞察', emoji: '🧠' },
{ id: 'sec-dim', label: '四维得分', emoji: '📊' },
{ id: 'sec-trait', label: '优势与注意', emoji: '✨' },
{ id: 'sec-career', label: '职业匹配', emoji: '💼' },
{ id: 'sec-rel', label: '人际关系', emoji: '💞' },
{ id: 'sec-cta', label: '深度方案', emoji: '💎' }
],
scrollTarget: '',
activeSection: ''
},
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)
},
onLoad(options) {
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) : ''
@@ -53,8 +104,7 @@ Page({
const raw = wx.getStorageSync('mbtiResult')
if (raw) {
const result = isReportProfileComplete() ? raw : toProfileLockedMbti(raw)
this.applyResult(result)
this.applyResult(raw)
this.initPayInfoFromRuntime('mbti')
} else {
wx.showToast({ title: '暂无测试结果', icon: 'none' })
@@ -64,15 +114,16 @@ Page({
onShow() {
this.setData({ hasPhone: hasPhone() })
this._syncJourney()
if (this.data.testResultId) return
const raw = wx.getStorageSync('mbtiResult')
if (raw) {
const result = isReportProfileComplete() ? raw : toProfileLockedMbti(raw)
this.applyResult(result)
this.applyResult(raw)
}
},
goCompleteProfile() {
try { require('../../utils/analytics').track('tap_complete_profile', { from: 'mbti' }) } catch (e) {}
wx.navigateTo({ url: '/pages/user-profile/index' })
},
@@ -96,6 +147,8 @@ Page({
patch.testResultId = String(payload.id)
}
this.setData(patch)
this._reportPaywallOnce('mbti', payInfo)
this._syncJourney()
},
loadDetail(id) {
@@ -152,26 +205,136 @@ Page({
applyResult(result) {
if (!result) return
const desc = result.description || {}
const dimensions = (result.dimensionScores && !result.locked)
? [
{ key: 'EI', left: '外向(E)', right: '内向(I)', ...result.dimensionScores.EI },
{ key: 'SN', left: '感觉(S)', right: '直觉(N)', ...result.dimensionScores.SN },
{ key: 'TF', left: '思考(T)', right: '情感(F)', ...result.dimensionScores.TF },
{ key: 'JP', left: '判断(J)', right: '知觉(P)', ...result.dimensionScores.JP }
]
const fromShare = !!this.data.fromShare
const profileGate = !fromShare && !isReportProfileComplete()
const dimOk =
result.dimensionScores &&
(!result.locked || profileGate)
const dimExplainMap = {
EI: { left: '外向 E', right: '内向 I', desc: '能量来源:社交场合或独处沉思' },
SN: { left: '感觉 S', right: '直觉 N', desc: '信息处理:关注事实或挖掘联系' },
TF: { left: '思考 T', right: '情感 F', desc: '决策偏好:逻辑推理或价值共情' },
JP: { left: '判断 J', right: '知觉 P', desc: '生活节奏:规划先行或灵活应对' }
}
const dashClass = {
EI: 'dash-fill--ei', SN: 'dash-fill--sn', TF: 'dash-fill--tf', JP: 'dash-fill--jp'
}
const dimensions = dimOk
? ['EI', 'SN', 'TF', 'JP'].map((k) => {
const src = result.dimensionScores[k] || {}
const meta = dimExplainMap[k]
return {
key: k,
left: meta.left,
right: meta.right,
desc: meta.desc,
dashClass: dashClass[k],
dominant: src.dominant || '',
percentage: src.percentage != null ? src.percentage : 0
}
})
: []
const mbtiCode = result.mbtiType || result.mbti || ''
const category = desc.category || ''
this.setData({
result,
dimensions,
profileGate,
previewDescription: slicePreviewText(desc.description || '', 0.3),
previewStrengths: slicePreviewList(desc.strengths || [], 0.3),
mbtiDesc: {
title: desc.name || '',
category,
description: desc.description || '',
strengths: desc.strengths || [],
weaknesses: desc.weaknesses || [],
careers: desc.careers || [],
relationships: desc.relationships || ''
}
},
mbtiInsight: getMbtiInsight(mbtiCode),
mbtiTags: getMbtiCategoryTags(category),
mbtiCareerItems: decorateCareers(desc.careers || []),
mbtiFields: getMbtiFields(mbtiCode),
mbtiGrowth: getMbtiGrowth(mbtiCode)
})
this._syncJourney()
},
onTapDeepService() {
try {
require('../../utils/analytics').track('tap_deep_service_from_mbti', {
mbti: this.data.result && this.data.result.mbtiType
})
} catch (e) {}
wx.navigateTo({ url: '/pages/purchase/index' })
},
onTapPromoCenter() {
try {
require('../../utils/analytics').track('tap_promo_from_mbti', {
mbti: this.data.result && this.data.result.mbtiType
})
} catch (e) {}
wx.navigateTo({ url: '/pages/promo/index' })
},
_syncJourney() {
const j = computeJourney({
profileGate: !!this.data.profileGate,
payRequired: !!(this.data.payInfo && this.data.payInfo.requiresPayment),
isPaid: !!(this.data.payInfo && this.data.payInfo.isPaid)
})
this.setData({ journey: j })
},
onTapReadFull() {
try { require('../../utils/analytics').track('tap_read_full', { type: 'mbti' }) } catch (e) {}
if (this.data.profileGate) {
this.goCompleteProfile()
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: 'mbti' }) } 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: 'mbti' }) } 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: 'mbti', from: 'share' }) } catch (e) {}
wx.switchTab({ url: '/pages/profile/index' })
},
_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) {
@@ -181,13 +344,10 @@ Page({
const reportRequires = cfg.reportRequiresPayment || {}
const requiresPayment = !!(reportRequires && reportRequires[testType])
const amountYuan = Number(pricing[testType]) || (requiresPayment ? 1.98 : 0)
this.setData({
payInfo: {
requiresPayment,
isPaid: false,
amountYuan
}
})
const payInfo = { requiresPayment, isPaid: false, amountYuan }
this.setData({ payInfo })
this._reportPaywallOnce(testType, payInfo)
this._syncJourney()
})
.catch(() => {
this.setData({
@@ -199,6 +359,7 @@ Page({
unlockFullReport() {
const { payInfo, testResultId, hasReloadedAfterPay } = this.data
if (!payInfo.requiresPayment || payInfo.isPaid) return
try { require('../../utils/analytics').track('tap_unlock_full', { type: 'mbti', amountYuan: payInfo.amountYuan }) } catch (e) {}
app.ensureLogin && app.ensureLogin().then((logged) => {
if (!logged) {
wx.showToast({ title: '请先登录', icon: 'none' })
@@ -209,6 +370,7 @@ Page({
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)