Files
MBTI_wang/miniprogram/pages/profile/index.js
Ghost 6584417ed8 fix: 审核模式 runtime 同步、邀请码与小端 tabBar/个人中心及管理端用户页
- api: AppConfig runtime 返回 reviewMode;Invite/Settings/AppUser 等调整
- 微信小程序与抖音:app 与 tabBar、enterprise/camera/index/upload/profile 对齐审核与邀请逻辑
- 管理端:Dashboard、Settings、Users、UserDetailDialog 等更新

Made-with: Cursor
2026-03-28 17:46:36 +08:00

367 lines
14 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/profile/index.js - 我的页面
const app = getApp()
const { getTypeOnly } = require('../../utils/resultFormat')
const { request } = require('../../utils/request')
Page({
data: {
hasLogin: false,
userInfo: null,
balance: 0,
testCount: 0,
hasResults: false,
mbtiType: '',
discType: '',
pdpType: '',
aiType: '',
/** 面相记录中的盖洛普前三摘要(/api/test/recent 或本地 aiResult */
gallupPreview: '',
mbtiTime: '',
discTime: '',
pdpTime: '',
aiTime: '',
reviewMode: false,
/** 最近记录的数据库 ID用于跳转时传参 */
mbtiResultId: null,
discResultId: null,
pdpResultId: null,
aiResultId: null,
loginLoading: false,
loginFailed: false,
nicknameDisplay: '',
displayNickname: '',
/** 默认头像(根据昵称):无头像时显示首字+背景色 */
avatarLetter: '登',
avatarBgColor: '#6366f1',
// 推广中心统计与配置(来自 /api/distribution/stats
promoDistributionEnabled: true,
promoCenterTitle: '推广中心',
promoTotalInvite: 0,
promoTotalEarned: '0.00',
promoWithdrawable: '0.00',
/** 是否有企业权限(绑定企业):有则显示「我的简历」 */
hasEnterprise: false
},
onLoad() {
// 仅在 onLoad 执行一次onShow 里的 runLoginThenLoad 会导致重复请求
this.runLoginThenLoad()
},
onShow() {
// 同步审核模式(与 runtime maintenanceMode / reviewMode 一致)
const gd = app.globalData
this.setData({ reviewMode: !!(gd.reviewMode || gd.maintenanceMode) })
// 如果是从其他页面返回,且已经登录,则只刷新数据而不重新执行登录流程
if (this.data.hasLogin) {
this.loadData()
}
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
const audit = !!(gd.reviewMode || gd.maintenanceMode)
this.getTabBar().setData({ selected: 2, reviewMode: audit })
}
},
/** 先确保登录完成(静默登录),再刷新页面数据 */
runLoginThenLoad() {
this.setData({ loginLoading: true, loginFailed: false })
app.ensureLogin().then((ok) => {
this.setData({ loginLoading: false, loginFailed: !ok })
this.loadData()
}).catch(() => {
this.setData({ loginLoading: false, loginFailed: true })
this.loadData()
})
},
/** 生成随机后缀:仅 26 英文字母(大小写)与数字,不含特殊字符,默认 4 位 */
_randomWechatUserSuffix(len) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
let s = ''
for (let i = 0; i < (len || 4); i++) {
s += chars[Math.floor(Math.random() * chars.length)]
}
return s
},
/** 根据昵称生成默认头像的首字与背景色(同一昵称同色) */
_avatarFromNickname(name) {
const str = (name && String(name).trim()) || '登'
const letter = str.charAt(0).toUpperCase() || '登'
const palette = ['#6366f1', '#8b5cf6', '#ec4899', '#f43f5e', '#14b8a6', '#0ea5e9', '#3b82f6', '#eab308']
let hash = 0
for (let i = 0; i < str.length; i++) hash += str.charCodeAt(i)
const bgColor = palette[Math.abs(hash) % palette.length]
return { avatarLetter: letter, avatarBgColor: bgColor }
},
/** 获取或生成当前用户的「微信用户XXXX」后缀同一用户固定仅 26 字母+数字) */
_getOrCreateWechatUserSuffix(userInfo) {
const uid = (userInfo && (userInfo.id || userInfo.userId)) ? String(userInfo.id || userInfo.userId) : '_default'
const storageKey = 'wechat_user_suffix'
const stored = wx.getStorageSync(storageKey)
if (stored && typeof stored === 'object' && stored[uid]) return stored[uid]
const suffix = this._randomWechatUserSuffix(4)
const next = { ...(stored && typeof stored === 'object' ? stored : {}), [uid]: suffix }
wx.setStorageSync(storageKey, next)
return suffix
},
loadData() {
const userInfo = app.globalData.userInfo || wx.getStorageSync('userInfo')
const token = app.globalData.token || wx.getStorageSync('token')
// 1. 先同步渲染用户基础信息(昵称/头像)
const nickname = (userInfo && (userInfo.nickname || userInfo.nickName))
? String(userInfo.nickname || userInfo.nickName).trim() : ''
let nicknameDisplay = nickname
let displayNickname = ''
if (!nickname && userInfo) {
const suffix = this._getOrCreateWechatUserSuffix(userInfo)
displayNickname = '微信用户' + suffix
nicknameDisplay = displayNickname
}
const avatarFromName = userInfo
? this._avatarFromNickname(nicknameDisplay)
: this._avatarFromNickname('点击登录')
const hasEnterprise = !!(userInfo && (userInfo.hasEnterprise === true || (userInfo.enterpriseId && Number(userInfo.enterpriseId) > 0)))
this.setData({
hasLogin: !!token || !!userInfo,
userInfo: userInfo || null,
hasEnterprise,
nicknameDisplay,
displayNickname,
avatarLetter: avatarFromName.avatarLetter,
avatarBgColor: avatarFromName.avatarBgColor
})
// 2. 已登录则从服务端拉取最近记录;失败降级读 localStorage
if (token || userInfo) {
this._loadRecentFromAPI()
this._loadPromoStats()
}
},
/** 从 /api/test/recent 拉取各类型最新记录 */
_loadRecentFromAPI() {
// 固定 scope=all与 appScope 无关。个人/企业 scope 会按 enterpriseId 过滤,若提交时写过
// enterpriseId 而此处仍用 personal会导致库里有记录却显示「未测评」。
request({
url: '/api/test/recent?scope=all',
method: 'GET',
success: (res) => {
// res 是 wx.request 原始响应:{ statusCode, data: { code, data, message } }
const payload = res && res.data
if (!payload || payload.code !== 200 || !payload.data) {
this._loadRecentFromStorage()
return
}
const { records = {}, totalCount = 0 } = payload.data
const r = records
// DISC resultText 后端已含「型」type badge 只显示字母,去掉「型」
const discType = r.disc ? r.disc.resultText.replace(/型$/, '') : ''
const gallupPreview = (r.ai && r.ai.gallupPreview) ? String(r.ai.gallupPreview) : ''
this.setData({
testCount: totalCount,
hasResults: !!(r.mbti || r.disc || r.pdp || r.ai),
mbtiType: r.mbti ? r.mbti.resultText : '',
discType,
pdpType: r.pdp ? r.pdp.resultText : '',
aiType: r.ai ? r.ai.resultText : '',
gallupPreview,
mbtiTime: r.mbti ? r.mbti.testTime : '',
discTime: r.disc ? r.disc.testTime : '',
pdpTime: r.pdp ? r.pdp.testTime : '',
aiTime: r.ai ? r.ai.testTime : '',
mbtiResultId: r.mbti ? r.mbti.id : null,
discResultId: r.disc ? r.disc.id : null,
pdpResultId: r.pdp ? r.pdp.id : null,
aiResultId: r.ai ? r.ai.id : null,
})
},
fail: () => {
this._loadRecentFromStorage()
}
})
},
/** 从 /api/distribution/stats 拉取推广中心统计 */
_loadPromoStats() {
request({
url: '/api/distribution/stats',
method: 'GET',
success: (res) => {
const payload = res && res.data
if (payload && payload.code === 200 && payload.data) {
const d = payload.data
this.setData({
promoDistributionEnabled: d.distributionEnabled !== false,
promoCenterTitle: d.promoCenterTitle || '推广中心',
promoTotalInvite: d.totalInvite || 0,
promoTotalEarned: d.totalEarned || '0.00',
promoWithdrawable: d.walletBalance || '0.00'
})
}
}
})
},
/** 降级:从 localStorage 读最近记录(兼容离线或 API 失败) */
_loadRecentFromStorage() {
const mbtiResult = wx.getStorageSync('mbtiResult')
const discResult = wx.getStorageSync('discResult')
const pdpResult = wx.getStorageSync('pdpResult')
const aiResult = wx.getStorageSync('aiResult')
let gallupPreview = ''
if (aiResult && Array.isArray(aiResult.gallupTop3) && aiResult.gallupTop3.length) {
gallupPreview = aiResult.gallupTop3.slice(0, 3).map(String).join('、')
}
let testCount = 0
if (mbtiResult) testCount++
if (discResult) testCount++
if (pdpResult) testCount++
if (aiResult) testCount++
const _fmt = (ts) => {
if (!ts) return ''
const d = new Date(typeof ts === 'number' && ts < 1e12 ? ts * 1000 : ts)
if (isNaN(d.getTime())) return ''
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
}
this.setData({
testCount,
hasResults: testCount > 0,
mbtiType: mbtiResult ? getTypeOnly(mbtiResult, 'mbti') : '',
discType: discResult ? getTypeOnly(discResult, 'disc') : '',
pdpType: pdpResult ? getTypeOnly(pdpResult, 'pdp') : '',
aiType: aiResult ? (aiResult.mbti || aiResult.mbtiType || aiResult.type || '') : '',
gallupPreview,
mbtiTime: _fmt(mbtiResult && (mbtiResult.createdAt || mbtiResult.timestamp || mbtiResult.testTime)),
discTime: _fmt(discResult && (discResult.createdAt || discResult.timestamp || discResult.testTime)),
pdpTime: _fmt(pdpResult && (pdpResult.createdAt || pdpResult.timestamp || pdpResult.testTime)),
aiTime: _fmt(aiResult && (aiResult.createdAt || aiResult.timestamp || aiResult.testTime)),
mbtiResultId: null,
discResultId: null,
pdpResultId: null,
aiResultId: null,
})
},
/** 点击登录:先静默登录拿到 token */
doLogin() {
this.setData({ loginLoading: true, loginFailed: false })
app.ensureLogin().then((ok) => {
this.setData({ loginLoading: false })
if (ok) {
this.loadData()
} else {
wx.showToast({ title: '登录失败,请检查网络或稍后重试', icon: 'none' })
this.setData({ loginFailed: true })
}
}).catch(() => {
this.setData({ loginLoading: false, loginFailed: true })
wx.showToast({ title: '登录失败', icon: 'none' })
})
},
goToIndex() { wx.switchTab({ url: '/pages/index/index' }) },
goToHistory() {
try { require('../../utils/analytics').track('tap_test_history', {}) } catch (e) {}
wx.navigateTo({ url: '/pages/history/index' })
},
goToUserProfile() { wx.navigateTo({ url: '/pages/user-profile/index' }) },
/** 深度服务统一入口(页内 Tab个人 / 团队与企业) */
goToDeepService() {
try { require('../../utils/analytics').track('tap_deep_service', {}) } catch (e) {}
wx.navigateTo({ url: '/pages/purchase/index' })
},
goToOrders() {
if (!this.data.hasLogin) {
wx.showToast({ title: '请先登录', icon: 'none' })
return
}
try { require('../../utils/analytics').track('tap_my_orders', {}) } catch (e) {}
wx.navigateTo({ url: '/pages/order/index' })
},
goToPurchase() { wx.navigateTo({ url: '/pages/purchase/index' }) },
goToPurchasePersonal() { wx.navigateTo({ url: '/pages/purchase/index?tab=personal' }) },
goToPurchaseEnterprise() { wx.navigateTo({ url: '/pages/purchase/index?tab=enterprise' }) },
goToEnterprise() { wx.navigateTo({ url: '/pages/enterprise/index' }) },
goToPromo() { wx.navigateTo({ url: '/pages/promo/index' }) },
goToMyResume() { wx.navigateTo({ url: '/pages/enterprise/resume-history' }) },
goToSettings() {
wx.showToast({ title: '开发中', icon: 'none' })
},
shareApp() {
// 触发分享
},
viewMBTI() {
const id = this.data.mbtiResultId
if (id) wx.navigateTo({ url: `/pages/result/mbti?id=${id}&type=mbti` })
else wx.navigateTo({ url: '/pages/test/mbti' })
},
viewDISC() {
const id = this.data.discResultId
if (id) wx.navigateTo({ url: `/pages/result/disc?id=${id}&type=disc` })
else wx.navigateTo({ url: '/pages/test/disc' })
},
viewPDP() {
const id = this.data.pdpResultId
if (id) wx.navigateTo({ url: `/pages/result/pdp?id=${id}&type=pdp` })
else wx.navigateTo({ url: '/pages/test/pdp' })
},
/** 盖洛普随面相报告:有记录看报告,否则去拍摄 */
viewGallup() {
const id = this.data.aiResultId
if (id) wx.navigateTo({ url: `/pages/index/result?id=${id}&type=ai` })
else wx.switchTab({ url: '/pages/index/camera' })
},
viewAI() {
const id = this.data.aiResultId
if (id) wx.navigateTo({ url: `/pages/index/result?id=${id}&type=ai` })
else wx.switchTab({ url: '/pages/index/camera' })
},
/** 详细性格测试MBTI / PDP / DISC 选择页 */
goToTestSelect() {
try { require('../../utils/analytics').track('tap_test_select_from_profile', {}) } catch (e) {}
wx.navigateTo({ url: '/pages/test-select/index' })
},
logout() {
wx.showModal({
title: '确认退出',
content: '退出后测试记录仍会保留',
success: (res) => {
if (res.confirm) {
app.logout()
this.setData({ hasLogin: false, userInfo: null, loginFailed: false })
}
}
})
},
onShareAppMessage() {
const { getSharePathByScope } = require('../../utils/share')
const rm = !!app.globalData.reviewMode
return {
title: '神仙团队性格测试 - 发现你的MBTI类型',
path: getSharePathByScope('/pages/index/index')
}
},
onShareTimeline() {
const { buildShareQuery } = require('../../utils/share')
const rm = !!app.globalData.reviewMode
return {
title: '神仙团队性格测试 - 发现你的MBTI类型',
query: buildShareQuery()
}
}
})