feat: CRM 报告与双端结果页、管理端用户/设置与默认企业配置
1、修复了用户详情与人脸/DISC 展示、手机号与资料校验、相机页与测试选择等体验问题。 2、新增了 SystemDefaultEnterprise、discDisplay、管理端设置扩展接口;CrmReport/Test/Analyze 等 API 与路由能力补强。 3、优化了 resultFormat、descriptions、payment、企业上下文;海报与 PDP/DISC 文案;超管 Commerce/Users 与 admin Settings/Users。 Made-with: Cursor
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
// pages/index/camera.js - 拍照页,拍完后上传到服务器再跳转结果页
|
||||
const app = getApp()
|
||||
const { hasPhone, bindPhoneByCode, ensureProfileCompleteAndRedirect } = require('../../utils/phoneAuth.js')
|
||||
const { ensureProfileCompleteAndRedirect } = require('../../utils/phoneAuth.js')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -9,7 +9,6 @@ Page({
|
||||
guideTexts: ['请正对镜头', '请向左转45°', '请向右转45°'],
|
||||
guideText: '请正对镜头',
|
||||
uploading: false,
|
||||
needPhoneAuth: false,
|
||||
aiAnalysisText: '分析',
|
||||
reviewMode: true
|
||||
},
|
||||
@@ -104,30 +103,6 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
// 本页就地授权手机号
|
||||
onGetPhoneNumber(e) {
|
||||
const { code, errMsg } = e.detail || {}
|
||||
if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) {
|
||||
wx.showToast({ title: '需要授权手机号才能继续', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!code && !hasPhone()) {
|
||||
wx.showToast({ title: '获取手机号失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!code && hasPhone()) {
|
||||
// 已有手机号,无需重复请求
|
||||
this.setData({ needPhoneAuth: false })
|
||||
return
|
||||
}
|
||||
bindPhoneByCode(code).then(() => {
|
||||
this.setData({ needPhoneAuth: false })
|
||||
}).catch(() => {
|
||||
// 失败时保持 needPhoneAuth 为 true,等待用户重新授权
|
||||
this.setData({ needPhoneAuth: !hasPhone() })
|
||||
})
|
||||
},
|
||||
|
||||
// 拍照
|
||||
takePhoto() {
|
||||
if (this.data.photos.length >= 3) {
|
||||
@@ -201,11 +176,6 @@ Page({
|
||||
|
||||
// 完成拍照:先上传 3 张图到服务器,拿到 URL 后再跳转结果页
|
||||
completeCapture() {
|
||||
if (!hasPhone()) {
|
||||
wx.showToast({ title: '请先授权手机号', icon: 'none' })
|
||||
this.setData({ needPhoneAuth: true })
|
||||
return
|
||||
}
|
||||
const photos = this.data.photos
|
||||
if (!photos || photos.length === 0) {
|
||||
wx.showToast({ title: '请先拍摄照片', icon: 'none' })
|
||||
|
||||
@@ -67,14 +67,6 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 手机号授权入口:仅在未授权时显示,就地弹出微信系统手机号授权 -->
|
||||
<view class="phone-auth-section" wx:if="{{needPhoneAuth}}">
|
||||
<text class="phone-auth-tip">为保障服务与联系,请先授权手机号。</text>
|
||||
<button class="phone-auth-btn" open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumber">
|
||||
授权手机号后继续拍摄
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
<custom-tab-bar />
|
||||
</view>
|
||||
|
||||
@@ -225,31 +225,6 @@
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.phone-auth-section {
|
||||
padding: 24rpx 24rpx 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.phone-auth-tip {
|
||||
font-size: 26rpx;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.phone-auth-btn {
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
background: linear-gradient(135deg, #7c3aed 0%, #5b21b6 100%);
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.phone-auth-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.capture-button {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
@@ -303,8 +303,8 @@
|
||||
.report-gate-cta-wrap {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
/* 占遮罩层约一半高度,保证内部 flex 垂直居中真正留白上下对称 */
|
||||
height: 48%;
|
||||
/* 遮挡/雾面展示区占 report-gate 整体的 55%,与渐变分层一致 */
|
||||
height: 55%;
|
||||
min-height: 400rpx;
|
||||
max-height: 640rpx;
|
||||
display: flex;
|
||||
@@ -323,14 +323,14 @@
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
/* 约前 50% 区域保持清晰预览,以下渐强变白 */
|
||||
/* 约前 45% 清晰预览;以下 55% 为渐强遮罩展示区 */
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgba(255, 255, 255, 0) 50%,
|
||||
rgba(255, 255, 255, 0.2) 52%,
|
||||
rgba(255, 255, 255, 0.55) 68%,
|
||||
rgba(255, 255, 255, 0.82) 88%,
|
||||
rgba(255, 255, 255, 0) 45%,
|
||||
rgba(255, 255, 255, 0.2) 48%,
|
||||
rgba(255, 255, 255, 0.55) 64%,
|
||||
rgba(255, 255, 255, 0.82) 85%,
|
||||
rgba(255, 255, 255, 0.92) 100%
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
const app = getApp()
|
||||
const payment = require('../../utils/payment')
|
||||
const { getTypeOnly } = require('../../utils/resultFormat')
|
||||
const { isReportProfileComplete } = require('../../utils/phoneAuth.js')
|
||||
|
||||
function toProfileLockedDisc(full) {
|
||||
if (!full) return full
|
||||
return { dominantType: full.dominantType || full.disc || '', locked: true }
|
||||
}
|
||||
|
||||
function toIntPercent(v) {
|
||||
if (v == null) return 0
|
||||
@@ -28,10 +34,10 @@ Page({
|
||||
/** 主+次高权重展示(如 S+I型),与接口摘要一致 */
|
||||
typeSummaryLine: '',
|
||||
typeList: [
|
||||
{ type: 'D', label: 'D型 - 支配型', colorClass: 'fill-d' },
|
||||
{ type: 'I', label: 'I型 - 影响型', colorClass: 'fill-i' },
|
||||
{ type: 'S', label: 'S型 - 稳健型', colorClass: 'fill-s' },
|
||||
{ type: 'C', label: 'C型 - 谨慎型', colorClass: 'fill-c' }
|
||||
{ type: 'D', label: 'D型 - 力量', colorClass: 'fill-d' },
|
||||
{ type: 'I', label: 'I型 - 活跃', colorClass: 'fill-i' },
|
||||
{ type: 'S', label: 'S型 - 和平', colorClass: 'fill-s' },
|
||||
{ type: 'C', label: 'C型 - 完美', colorClass: 'fill-c' }
|
||||
],
|
||||
payInfo: { requiresPayment: false, isPaid: false, amountYuan: 0 },
|
||||
testResultId: null,
|
||||
@@ -46,10 +52,11 @@ Page({
|
||||
this.loadDetail(id)
|
||||
return
|
||||
}
|
||||
const result = wx.getStorageSync('discResult')
|
||||
if (result) {
|
||||
const r = withPercentagesInt(result)
|
||||
this.setData({ result: r, typeSummaryLine: getTypeOnly(result, 'disc') })
|
||||
const raw = wx.getStorageSync('discResult')
|
||||
if (raw) {
|
||||
const gated = isReportProfileComplete() ? raw : toProfileLockedDisc(raw)
|
||||
const r = withPercentagesInt(gated)
|
||||
this.setData({ result: r, typeSummaryLine: getTypeOnly(gated, 'disc') })
|
||||
this.initPayInfoFromRuntime('disc')
|
||||
} else {
|
||||
wx.showToast({ title: '暂无测试结果', icon: 'none' })
|
||||
@@ -95,6 +102,19 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
onShow() {
|
||||
if (this.data.testResultId) return
|
||||
const raw = wx.getStorageSync('discResult')
|
||||
if (!raw) return
|
||||
const gated = isReportProfileComplete() ? raw : toProfileLockedDisc(raw)
|
||||
const r = withPercentagesInt(gated)
|
||||
this.setData({ result: r, typeSummaryLine: getTypeOnly(gated, 'disc') })
|
||||
},
|
||||
|
||||
goCompleteProfile() {
|
||||
wx.navigateTo({ url: '/pages/user-profile/index' })
|
||||
},
|
||||
|
||||
initPayInfoFromRuntime(testType) {
|
||||
app.getRuntimeConfig()
|
||||
.then((cfg) => {
|
||||
|
||||
@@ -25,7 +25,17 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="scores-section" wx:if="{{!payInfo.requiresPayment || payInfo.isPaid}}">
|
||||
<view class="paywall-card" wx:elif="{{result && result.locked}}">
|
||||
<view class="paywall-content">
|
||||
<text class="paywall-fake-title">完整报告需完善资料</text>
|
||||
<text class="paywall-fake-line">请补全头像、昵称并绑定手机号后查看完整 DISC 报告。</text>
|
||||
<view class="paywall-btn" bindtap="goCompleteProfile">
|
||||
<text class="paywall-btn-main">去完善资料</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="scores-section" wx:if="{{result && !result.locked && (!payInfo.requiresPayment || payInfo.isPaid)}}">
|
||||
<text class="section-title">DISC得分详情</text>
|
||||
<view class="score-item" wx:for="{{typeList}}" wx:key="type">
|
||||
<view class="score-header">
|
||||
@@ -39,7 +49,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="analysis-card" wx:if="{{!payInfo.requiresPayment || payInfo.isPaid}}">
|
||||
<view class="analysis-card" wx:if="{{result && !result.locked && (!payInfo.requiresPayment || payInfo.isPaid)}}">
|
||||
<text class="card-title">主要性格特征分析</text>
|
||||
<view class="trait-section">
|
||||
<text class="trait-title">优势</text>
|
||||
@@ -57,7 +67,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="analysis-card" wx:if="{{!payInfo.requiresPayment || payInfo.isPaid}}">
|
||||
<view class="analysis-card" wx:if="{{result && !result.locked && (!payInfo.requiresPayment || payInfo.isPaid)}}">
|
||||
<text class="card-title">职业匹配度分析</text>
|
||||
<view class="career-item" wx:for="{{result.description.careers}}" wx:key="*this">
|
||||
<view class="career-dot"></view>
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
// pages/result/mbti.js - MBTI结果页(支持付费墙 + 历史详情拉取)
|
||||
const app = getApp()
|
||||
const payment = require('../../utils/payment')
|
||||
const { hasPhone, bindPhoneByCode } = require('../../utils/phoneAuth.js')
|
||||
const { hasPhone, bindPhoneByCode, isReportProfileComplete } = require('../../utils/phoneAuth.js')
|
||||
|
||||
function toProfileLockedMbti(full) {
|
||||
if (!full) return full
|
||||
return { mbtiType: full.mbtiType || full.mbti || '', locked: true }
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -35,8 +40,9 @@ Page({
|
||||
return
|
||||
}
|
||||
|
||||
const result = wx.getStorageSync('mbtiResult')
|
||||
if (result) {
|
||||
const raw = wx.getStorageSync('mbtiResult')
|
||||
if (raw) {
|
||||
const result = isReportProfileComplete() ? raw : toProfileLockedMbti(raw)
|
||||
this.applyResult(result)
|
||||
this.initPayInfoFromRuntime('mbti')
|
||||
} else {
|
||||
@@ -47,6 +53,16 @@ Page({
|
||||
|
||||
onShow() {
|
||||
this.setData({ hasPhone: hasPhone() })
|
||||
if (this.data.testResultId) return
|
||||
const raw = wx.getStorageSync('mbtiResult')
|
||||
if (raw) {
|
||||
const result = isReportProfileComplete() ? raw : toProfileLockedMbti(raw)
|
||||
this.applyResult(result)
|
||||
}
|
||||
},
|
||||
|
||||
goCompleteProfile() {
|
||||
wx.navigateTo({ url: '/pages/user-profile/index' })
|
||||
},
|
||||
|
||||
loadDetail(id) {
|
||||
|
||||
@@ -42,7 +42,17 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="dimensions-section" wx:if="{{!payInfo.requiresPayment || payInfo.isPaid}}">
|
||||
<view class="paywall-card" wx:elif="{{result && result.locked}}">
|
||||
<view class="paywall-content">
|
||||
<text class="paywall-fake-title">完整报告需完善资料</text>
|
||||
<text class="paywall-fake-line">请补全头像、昵称并绑定手机号后查看完整性格分析。</text>
|
||||
<button class="paywall-btn" bindtap="goCompleteProfile">
|
||||
<text class="paywall-btn-main">去完善资料</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="dimensions-section" wx:if="{{result && !result.locked && (!payInfo.requiresPayment || payInfo.isPaid)}}">
|
||||
<view class="dimension-item" wx:for="{{dimensions}}" wx:key="key">
|
||||
<view class="dimension-labels">
|
||||
<text class="label-left">{{item.left}}</text>
|
||||
@@ -59,7 +69,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="analysis-card" wx:if="{{!payInfo.requiresPayment || payInfo.isPaid}}">
|
||||
<view class="analysis-card" wx:if="{{result && !result.locked && (!payInfo.requiresPayment || payInfo.isPaid)}}">
|
||||
<text class="card-title">性格特征分析</text>
|
||||
<view class="trait-section">
|
||||
<text class="trait-title">优势</text>
|
||||
@@ -86,7 +96,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="analysis-card" wx:if="{{(!payInfo.requiresPayment || payInfo.isPaid) && mbtiDesc.relationships}}">
|
||||
<view class="analysis-card" wx:if="{{result && !result.locked && (!payInfo.requiresPayment || payInfo.isPaid) && mbtiDesc.relationships}}">
|
||||
<text class="card-title">人际关系分析</text>
|
||||
<text class="relationship-text">{{mbtiDesc.relationships}}</text>
|
||||
</view>
|
||||
|
||||
@@ -2,6 +2,16 @@
|
||||
const app = getApp()
|
||||
const payment = require('../../utils/payment')
|
||||
const { getTypeOnly } = require('../../utils/resultFormat')
|
||||
const { isReportProfileComplete } = require('../../utils/phoneAuth.js')
|
||||
|
||||
function toProfileLockedPdp(full) {
|
||||
if (!full) return full
|
||||
const desc = full.description || {}
|
||||
return {
|
||||
description: { type: desc.type || '', emoji: desc.emoji || '' },
|
||||
locked: true
|
||||
}
|
||||
}
|
||||
|
||||
const PDP_KEYS = ['Tiger', 'Peacock', 'Koala', 'Owl', 'Chameleon']
|
||||
|
||||
@@ -28,7 +38,7 @@ Page({
|
||||
typeList: [
|
||||
{ type: 'Tiger', emoji: '🐅', label: '老虎型', colorClass: 'fill-tiger' },
|
||||
{ type: 'Peacock', emoji: '🦚', label: '孔雀型', colorClass: 'fill-peacock' },
|
||||
{ type: 'Koala', emoji: '🐨', label: '考拉型', colorClass: 'fill-koala' },
|
||||
{ type: 'Koala', emoji: '🐨', label: '无尾熊型', colorClass: 'fill-koala' },
|
||||
{ type: 'Owl', emoji: '🦉', label: '猫头鹰型', colorClass: 'fill-owl' },
|
||||
{ type: 'Chameleon', emoji: '🦎', label: '变色龙型', colorClass: 'fill-chameleon' }
|
||||
],
|
||||
@@ -95,6 +105,21 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
onShow() {
|
||||
if (this.data.testResultId) return
|
||||
const raw = wx.getStorageSync('pdpResult')
|
||||
if (!raw) return
|
||||
const gated = isReportProfileComplete() ? raw : toProfileLockedPdp(raw)
|
||||
this.setData({
|
||||
result: withPercentagesInt(gated),
|
||||
typeSummaryLine: getTypeOnly(gated, 'pdp')
|
||||
})
|
||||
},
|
||||
|
||||
goCompleteProfile() {
|
||||
wx.navigateTo({ url: '/pages/user-profile/index' })
|
||||
},
|
||||
|
||||
initPayInfoFromRuntime(testType) {
|
||||
app.getRuntimeConfig()
|
||||
.then((cfg) => {
|
||||
|
||||
@@ -26,7 +26,17 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="scores-section" wx:if="{{!payInfo.requiresPayment || payInfo.isPaid}}">
|
||||
<view class="paywall-card" wx:elif="{{result && result.locked}}">
|
||||
<view class="paywall-content">
|
||||
<text class="paywall-fake-title">完整报告需完善资料</text>
|
||||
<text class="paywall-fake-line">请补全头像、昵称并绑定手机号后查看完整 PDP 报告。</text>
|
||||
<view class="paywall-btn" bindtap="goCompleteProfile">
|
||||
<text class="paywall-btn-main">去完善资料</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="scores-section" wx:if="{{result && !result.locked && (!payInfo.requiresPayment || payInfo.isPaid)}}">
|
||||
<text class="section-title">PDP得分详情</text>
|
||||
<view class="score-item" wx:for="{{typeList}}" wx:key="type">
|
||||
<view class="score-header">
|
||||
@@ -40,7 +50,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="analysis-card" wx:if="{{!payInfo.requiresPayment || payInfo.isPaid}}">
|
||||
<view class="analysis-card" wx:if="{{result && !result.locked && (!payInfo.requiresPayment || payInfo.isPaid)}}">
|
||||
<text class="card-title">性格特征</text>
|
||||
<view class="trait-section">
|
||||
<text class="trait-title">优势</text>
|
||||
@@ -58,12 +68,12 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="analysis-card" wx:if="{{!payInfo.requiresPayment || payInfo.isPaid}}">
|
||||
<view class="analysis-card" wx:if="{{result && !result.locked && (!payInfo.requiresPayment || payInfo.isPaid)}}">
|
||||
<text class="card-title">团队角色</text>
|
||||
<text class="relationship-text">{{result.description.teamRole}}</text>
|
||||
</view>
|
||||
|
||||
<view class="analysis-card" wx:if="{{!payInfo.requiresPayment || payInfo.isPaid}}">
|
||||
<view class="analysis-card" wx:if="{{result && !result.locked && (!payInfo.requiresPayment || payInfo.isPaid)}}">
|
||||
<text class="card-title">推荐职业</text>
|
||||
<view class="career-item" wx:for="{{result.description.careers}}" wx:key="*this">
|
||||
<view class="career-dot"></view>
|
||||
|
||||
@@ -21,7 +21,8 @@ Page({
|
||||
amountYuan: 0
|
||||
},
|
||||
testResultId: 0,
|
||||
paying: false
|
||||
paying: false,
|
||||
profileGate: false
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
@@ -36,8 +37,19 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
onShow() {
|
||||
const id = this.data.testResultId
|
||||
if (id && this.data.profileGate) {
|
||||
this.loadFromHistory(String(id))
|
||||
}
|
||||
},
|
||||
|
||||
goCompleteProfile() {
|
||||
wx.navigateTo({ url: '/pages/user-profile/index' })
|
||||
},
|
||||
|
||||
loadFromHistory(id) {
|
||||
this.setData({ loading: true, error: '', content: '', sections: [], resumeData: null })
|
||||
this.setData({ loading: true, error: '', content: '', sections: [], resumeData: null, profileGate: false })
|
||||
const apiBase = (app.globalData && app.globalData.apiBase) || wx.getStorageSync('apiBase') || ''
|
||||
const token = (app.globalData && app.globalData.token) || wx.getStorageSync('token') || ''
|
||||
wx.request({
|
||||
@@ -53,30 +65,43 @@ Page({
|
||||
// 兼容:历史数据可能是字符串,或包在 content 里的 JSON 字符串
|
||||
data = this.normalizeStructuredData(data)
|
||||
|
||||
const profileIncomplete = payload.profileIncomplete === true
|
||||
const locked = !!(data && data.locked)
|
||||
const structured = !!data && data._structured === true
|
||||
|
||||
const requiresPayment = !!payload.requiresPayment
|
||||
const isPaid = !!payload.isPaid
|
||||
const amountYuan = payload.paidAmount ? payload.paidAmount / 100 : 0
|
||||
const payWall = requiresPayment && !isPaid
|
||||
const profileGate = !payWall && (profileIncomplete || locked)
|
||||
|
||||
let content = ''
|
||||
let sections = []
|
||||
let resumeData = null
|
||||
|
||||
if (structured) {
|
||||
if (payWall || profileGate) {
|
||||
content = ''
|
||||
sections = []
|
||||
resumeData = null
|
||||
} else if (structured) {
|
||||
const built = this.buildSectionsFromStructured(data)
|
||||
content = built.content
|
||||
sections = built.sections
|
||||
resumeData = data
|
||||
} else {
|
||||
const raw = data.content || ''
|
||||
content = raw
|
||||
sections = this.parseContent(raw)
|
||||
}
|
||||
const requiresPayment = !!payload.requiresPayment
|
||||
const isPaid = !!payload.isPaid
|
||||
const amountYuan = payload.paidAmount ? payload.paidAmount / 100 : 0
|
||||
|
||||
this.setData({
|
||||
loading: false,
|
||||
testResultId: payload.id || parseInt(id) || 0,
|
||||
content: (!requiresPayment || isPaid) ? content : '',
|
||||
sections: (!requiresPayment || isPaid) ? sections : [],
|
||||
resumeData: structured ? data : null,
|
||||
payInfo: { requiresPayment: requiresPayment && !isPaid, isPaid, amountYuan }
|
||||
content,
|
||||
sections,
|
||||
resumeData,
|
||||
profileGate,
|
||||
payInfo: { requiresPayment: payWall, isPaid, amountYuan }
|
||||
})
|
||||
} else {
|
||||
this.setData({ loading: false, error: '加载历史记录失败,请返回重试。' })
|
||||
@@ -140,15 +165,23 @@ Page({
|
||||
// 兼容:API 直接返回结构化对象,或把 JSON 放在 content 里
|
||||
d = this.normalizeStructuredData(d)
|
||||
|
||||
const locked = !!(d && d.locked)
|
||||
const structured = !!d && d._structured === true
|
||||
|
||||
let content = ''
|
||||
let sections = []
|
||||
let resumeData = null
|
||||
let profileGate = false
|
||||
|
||||
if (structured) {
|
||||
if (requiresPayment) {
|
||||
profileGate = false
|
||||
} else if (locked) {
|
||||
profileGate = true
|
||||
} else if (structured) {
|
||||
const built = this.buildSectionsFromStructured(d)
|
||||
content = built.content
|
||||
sections = built.sections
|
||||
resumeData = d
|
||||
} else {
|
||||
const raw = d.content || ''
|
||||
content = raw || '分析已完成,但未返回可展示的内容。'
|
||||
@@ -161,7 +194,8 @@ Page({
|
||||
testResultId: resultId,
|
||||
content: requiresPayment ? '' : content,
|
||||
sections: requiresPayment ? [] : sections,
|
||||
resumeData: structured ? d : null,
|
||||
resumeData,
|
||||
profileGate,
|
||||
payInfo: { requiresPayment, isPaid: !requiresPayment, amountYuan }
|
||||
})
|
||||
} else {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</view>
|
||||
|
||||
<!-- 结果显示 -->
|
||||
<block wx:if="{{!loading || error || content}}">
|
||||
<block wx:if="{{!loading || error || content || profileGate}}">
|
||||
|
||||
<!-- 页面顶部 Hero -->
|
||||
<view class="hero-banner {{error ? 'hero-error' : 'hero-success'}}">
|
||||
@@ -50,8 +50,18 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 资料未完善:与问卷/面相报告一致 -->
|
||||
<view class="card pay-gate-card" wx:if="{{profileGate && (!payInfo.requiresPayment || payInfo.isPaid) && !error}}">
|
||||
<text class="pay-gate-icon">📝</text>
|
||||
<text class="pay-gate-title">完善资料后查看完整报告</text>
|
||||
<text class="pay-gate-desc">请补全头像、昵称并绑定手机号\n即可查看简历综合分析全文</text>
|
||||
<view class="pay-gate-btn" bindtap="goCompleteProfile">
|
||||
<text class="pay-gate-btn-text">去完善资料</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 成功内容:分块卡片展示 -->
|
||||
<block wx:if="{{content && sections.length > 0 && !payInfo.requiresPayment}}">
|
||||
<block wx:if="{{content && sections.length > 0 && !payInfo.requiresPayment && !profileGate}}">
|
||||
<view class="card section-card" wx:for="{{sections}}" wx:key="index">
|
||||
<view class="section-header">
|
||||
<text class="section-icon">{{item.icon}}</text>
|
||||
@@ -71,10 +81,10 @@
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="action-section" wx:if="{{!loading || error}}">
|
||||
<button class="btn btn-primary" open-type="share" wx:if="{{!payInfo.requiresPayment && content}}">
|
||||
<button class="btn btn-primary" open-type="share" wx:if="{{!payInfo.requiresPayment && !profileGate && content}}">
|
||||
<text class="btn-text">分享分析报告</text>
|
||||
</button>
|
||||
<view class="btn btn-outline" bindtap="fetchResumeAnalysis" wx:if="{{!loading && content && !payInfo.requiresPayment}}">
|
||||
<view class="btn btn-outline" bindtap="fetchResumeAnalysis" wx:if="{{!loading && content && !payInfo.requiresPayment && !profileGate}}">
|
||||
<text class="btn-text-outline">重新分析生成</text>
|
||||
</view>
|
||||
<view class="btn btn-outline" bindtap="goHome">
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
</view>
|
||||
<view class="entry-content">
|
||||
<text class="entry-name">PDP 行为偏好测试</text>
|
||||
<text class="entry-brief">老虎 / 孔雀 / 考拉 / 猫头鹰 · 行为风格</text>
|
||||
<text class="entry-brief">老虎 / 孔雀 / 无尾熊 / 猫头鹰 / 变色龙 · 行为风格</text>
|
||||
</view>
|
||||
<text class="entry-arrow">→</text>
|
||||
</view>
|
||||
|
||||
@@ -19,15 +19,16 @@
|
||||
<text class="question-text">{{currentQuestion.question}}</text>
|
||||
<view class="options-container">
|
||||
<view
|
||||
class="option-item {{selectedAnswer === option.value ? 'selected' : ''}}"
|
||||
class="option-item {{answers[currentQuestion.id] === option.value ? 'selected' : ''}}"
|
||||
wx:for="{{currentQuestion.options}}"
|
||||
wx:for-item="option"
|
||||
wx:key="value"
|
||||
wx:for-index="optIdx"
|
||||
wx:key="optIdx"
|
||||
bindtap="selectAnswer"
|
||||
data-value="{{option.value}}"
|
||||
>
|
||||
<view class="radio-button {{selectedAnswer === option.value ? 'checked' : ''}}">
|
||||
<view wx:if="{{selectedAnswer === option.value}}" class="radio-inner"></view>
|
||||
<view class="radio-button {{answers[currentQuestion.id] === option.value ? 'checked' : ''}}">
|
||||
<view wx:if="{{answers[currentQuestion.id] === option.value}}" class="radio-inner"></view>
|
||||
</view>
|
||||
<text class="option-text">{{option.text}}</text>
|
||||
</view>
|
||||
|
||||
@@ -171,7 +171,7 @@ Page({
|
||||
finishTest() {
|
||||
const q = this.data.currentQuestion
|
||||
if (!q) return
|
||||
if (this.data.answers[q.id] == null && this.data.selectedAnswer == null) {
|
||||
if (this.data.answers[q.id] == null) {
|
||||
wx.showToast({ title: '请先选择一项', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -19,15 +19,16 @@
|
||||
<text class="question-text">{{currentQuestion.question}}</text>
|
||||
<view class="options-container">
|
||||
<view
|
||||
class="option-item {{selectedAnswer === option.value ? 'selected' : ''}}"
|
||||
class="option-item {{answers[currentQuestion.id] === option.value ? 'selected' : ''}}"
|
||||
wx:for="{{currentQuestion.options}}"
|
||||
wx:for-item="option"
|
||||
wx:key="value"
|
||||
wx:for-index="optIdx"
|
||||
wx:key="optIdx"
|
||||
bindtap="selectAnswer"
|
||||
data-value="{{option.value}}"
|
||||
>
|
||||
<view class="radio-button {{selectedAnswer === option.value ? 'checked' : ''}}">
|
||||
<view wx:if="{{selectedAnswer === option.value}}" class="radio-inner"></view>
|
||||
<view class="radio-button {{answers[currentQuestion.id] === option.value ? 'checked' : ''}}">
|
||||
<view wx:if="{{answers[currentQuestion.id] === option.value}}" class="radio-inner"></view>
|
||||
</view>
|
||||
<text class="option-text">{{option.text}}</text>
|
||||
</view>
|
||||
|
||||
@@ -19,15 +19,16 @@
|
||||
<text class="question-text">{{currentQuestion.question}}</text>
|
||||
<view class="options-container">
|
||||
<view
|
||||
class="option-item {{selectedAnswer === option.value ? 'selected' : ''}}"
|
||||
class="option-item {{answers[currentQuestion.id] === option.value ? 'selected' : ''}}"
|
||||
wx:for="{{currentQuestion.options}}"
|
||||
wx:for-item="option"
|
||||
wx:key="value"
|
||||
wx:for-index="optIdx"
|
||||
wx:key="optIdx"
|
||||
bindtap="selectAnswer"
|
||||
data-value="{{option.value}}"
|
||||
>
|
||||
<view class="radio-button {{selectedAnswer === option.value ? 'checked' : ''}}">
|
||||
<view wx:if="{{selectedAnswer === option.value}}" class="radio-inner"></view>
|
||||
<view class="radio-button {{answers[currentQuestion.id] === option.value ? 'checked' : ''}}">
|
||||
<view wx:if="{{answers[currentQuestion.id] === option.value}}" class="radio-inner"></view>
|
||||
</view>
|
||||
<text class="option-text">{{option.text}}</text>
|
||||
</view>
|
||||
|
||||
@@ -45,7 +45,7 @@ Page({
|
||||
const gender = (userInfo.gender !== undefined && userInfo.gender !== null) ? Number(userInfo.gender) : 0
|
||||
const genderText = this._genderText(gender)
|
||||
const genderIndex = Math.min(Math.max(0, gender), 2)
|
||||
const phone = userInfo.phone || ''
|
||||
const phone = (userInfo.phone || userInfo.phoneNumber || '').trim()
|
||||
|
||||
const { avatarLetter, avatarBgColor } = this._avatarFromNickname(nickname || '我')
|
||||
|
||||
@@ -123,7 +123,8 @@ Page({
|
||||
return
|
||||
}
|
||||
bindPhoneByCode(code).then((user) => {
|
||||
this.setData({ phone: user.phone || '' })
|
||||
const phone = ((user && (user.phone || user.phoneNumber)) || '').trim()
|
||||
this.setData({ phone })
|
||||
}).catch(() => {})
|
||||
},
|
||||
|
||||
@@ -153,7 +154,28 @@ Page({
|
||||
this.setData({ gender, genderIndex: idx, genderText })
|
||||
},
|
||||
|
||||
/** 保存前校验:头像、昵称、手机号均必填(与页面提示及后端门禁一致) */
|
||||
_validateRequiredForSave() {
|
||||
const avatar = (this.data.avatar || '').trim()
|
||||
const nickname = (this.data.nickname || '').trim()
|
||||
const phone = (this.data.phone || this.data.userInfo?.phone || this.data.userInfo?.phoneNumber || '').trim()
|
||||
const missing = []
|
||||
if (!avatar) missing.push('头像')
|
||||
if (!nickname) missing.push('昵称')
|
||||
if (!phone) missing.push('手机号')
|
||||
if (missing.length === 0) return ''
|
||||
return missing.length === 1
|
||||
? (missing[0] === '头像' ? '请先上传头像' : missing[0] === '昵称' ? '请填写昵称' : '请先授权绑定手机号')
|
||||
: `请完善:${missing.join('、')}`
|
||||
},
|
||||
|
||||
onSave() {
|
||||
const tip = this._validateRequiredForSave()
|
||||
if (tip) {
|
||||
wx.showToast({ title: tip, icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const { nickname, birthday, gender, userInfo } = this.data
|
||||
const profile = {}
|
||||
const origNickname = (userInfo?.nickname || userInfo?.nickName || '').trim()
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
// utils/descriptions.js - 测试结果描述
|
||||
|
||||
// MBTI类型描述
|
||||
const mbtiDescriptions = {
|
||||
ISTJ: { type: "ISTJ", name: "检查员", category: "守护者", description: "务实、负责、可靠的传统主义者", strengths: ["可靠负责", "注重细节", "有条理"], weaknesses: ["可能过于固执", "不喜变化"], careers: ["会计师", "审计师", "项目经理"] },
|
||||
ISFJ: { type: "ISFJ", name: "保护者", category: "守护者", description: "安静、友好、负责任的守护者", strengths: ["忠诚体贴", "观察力强", "耐心"], weaknesses: ["不善拒绝", "过于谦虚"], careers: ["护士", "教师", "行政"] },
|
||||
INFJ: { type: "INFJ", name: "提倡者", category: "理想主义者", description: "寻求意义和联系的理想主义者", strengths: ["洞察力强", "有远见", "坚定"], weaknesses: ["过于理想化", "容易疲惫"], careers: ["心理咨询", "作家", "人力资源"] },
|
||||
INTJ: { type: "INTJ", name: "建筑师", category: "理想主义者", description: "独立、有战略眼光的思考者", strengths: ["战略思维", "独立自信", "意志坚定"], weaknesses: ["可能傲慢", "过于苛刻"], careers: ["战略顾问", "科学家", "架构师"] },
|
||||
ISTP: { type: "ISTP", name: "鉴赏家", category: "探险家", description: "灵活、务实的问题解决者", strengths: ["适应力强", "动手能力", "冷静"], weaknesses: ["可能冷漠", "不善表达"], careers: ["工程师", "技术员", "飞行员"] },
|
||||
ISFP: { type: "ISFP", name: "探险家", category: "探险家", description: "温和、敏感的艺术家", strengths: ["创造力", "同理心", "灵活"], weaknesses: ["过于敏感", "避免冲突"], careers: ["设计师", "艺术家", "摄影师"] },
|
||||
INFP: { type: "INFP", name: "调停者", category: "理想主义者", description: "理想主义、忠诚的调解者", strengths: ["创造力", "同理心", "真诚"], weaknesses: ["过于理想化", "情绪化"], careers: ["作家", "心理咨询", "社工"] },
|
||||
INTP: { type: "INTP", name: "逻辑学家", category: "理想主义者", description: "创新、逻辑的思考者", strengths: ["逻辑思维", "创新能力", "客观"], weaknesses: ["可能孤僻", "忽视情感"], careers: ["程序员", "研究员", "分析师"] },
|
||||
ESTP: { type: "ESTP", name: "企业家", category: "探险家", description: "精力充沛、务实的行动者", strengths: ["果断", "务实", "善于应变"], weaknesses: ["可能冲动", "缺乏耐心"], careers: ["销售", "企业家", "运动员"] },
|
||||
ESFP: { type: "ESFP", name: "表演者", category: "探险家", description: "热情、友好的社交达人", strengths: ["热情友好", "乐观", "灵活"], weaknesses: ["可能肤浅", "容易分心"], careers: ["演员", "销售", "主持人"] },
|
||||
ENFP: { type: "ENFP", name: "竞选者", category: "理想主义者", description: "热情、有创造力的社交者", strengths: ["创造力", "热情", "善于沟通"], weaknesses: ["可能不切实际", "缺乏专注"], careers: ["市场营销", "记者", "顾问"] },
|
||||
ENTP: { type: "ENTP", name: "辩论家", category: "理想主义者", description: "聪明、好奇的思想家", strengths: ["创新能力", "辩论能力", "适应力"], weaknesses: ["可能争辩", "不善执行"], careers: ["律师", "企业家", "咨询"] },
|
||||
ESTJ: { type: "ESTJ", name: "总经理", category: "守护者", description: "务实、果断的组织者", strengths: ["领导力", "组织能力", "务实"], weaknesses: ["可能专制", "不够灵活"], careers: ["管理者", "项目经理", "律师"] },
|
||||
ESFJ: { type: "ESFJ", name: "执政官", category: "守护者", description: "热心、合作的支持者", strengths: ["关心他人", "负责任", "善于合作"], weaknesses: ["过于在意评价", "不善拒绝"], careers: ["教师", "护士", "人力资源"] },
|
||||
ENFJ: { type: "ENFJ", name: "主人公", category: "理想主义者", description: "有魅力、鼓舞人心的领导者", strengths: ["领导力", "同理心", "说服力"], weaknesses: ["过于理想化", "过度付出"], careers: ["培训师", "顾问", "教师"] },
|
||||
ENTJ: { type: "ENTJ", name: "指挥官", category: "理想主义者", description: "大胆、有远见的领导者", strengths: ["领导力", "战略思维", "果断"], weaknesses: ["可能专制", "不耐烦"], careers: ["CEO", "企业家", "律师"] }
|
||||
}
|
||||
|
||||
// DISC类型描述
|
||||
const discDescriptions = {
|
||||
D: { type: "D型", title: "支配型", color: "#EF4444", description: "天生的领导者,注重结果和效率", strengths: ["决断力强", "目标导向", "行动迅速"], weaknesses: ["可能过于强势", "缺乏耐心"], careers: ["企业高管", "创业者", "项目经理"] },
|
||||
I: { type: "I型", title: "影响型", color: "#F59E0B", description: "热情友好,善于社交和表达", strengths: ["善于沟通", "乐观积极", "创意丰富"], weaknesses: ["可能过于乐观", "注意力分散"], careers: ["市场营销", "公关", "培训师"] },
|
||||
S: { type: "S型", title: "稳健型", color: "#10B981", description: "稳重可靠,注重团队和谐", strengths: ["可靠稳定", "团队协作", "耐心倾听"], weaknesses: ["可能抗拒变化", "决策较慢"], careers: ["人力资源", "客户服务", "行政"] },
|
||||
C: { type: "C型", title: "谨慎型", color: "#3B82F6", description: "注重细节和准确性,追求完美", strengths: ["分析能力强", "注重细节", "准确严谨"], weaknesses: ["可能过于完美主义", "决策较慢"], careers: ["数据分析", "工程师", "会计师"] }
|
||||
}
|
||||
|
||||
// PDP类型描述
|
||||
const pdpDescriptions = {
|
||||
Tiger: { type: "老虎型", emoji: "🐅", title: "支配者", color: "#F59E0B", description: "天生的领导者,具有强烈的目标导向和执行力", strengths: ["决断力强", "目标导向", "行动迅速"], weaknesses: ["可能过于强势", "缺乏耐心"], careers: ["企业高管", "创业者", "项目经理"], teamRole: "适合担任领导者角色" },
|
||||
Peacock: { type: "孔雀型", emoji: "🦚", title: "表现者", color: "#8B5CF6", description: "热情友好,善于表达和社交", strengths: ["善于沟通", "乐观积极", "影响力强"], weaknesses: ["可能过于乐观", "注意力分散"], careers: ["市场营销", "公关", "培训师"], teamRole: "适合担任激励者和调解者角色" },
|
||||
Koala: { type: "考拉型", emoji: "🐨", title: "支持者", color: "#10B981", description: "稳重可靠,注重团队和谐", strengths: ["可靠稳定", "团队协作", "善解人意"], weaknesses: ["可能抗拒变化", "决策较慢"], careers: ["人力资源", "客户服务", "教师"], teamRole: "适合担任协调者和支持者角色" },
|
||||
Owl: { type: "猫头鹰型", emoji: "🦉", title: "分析者", color: "#3B82F6", description: "注重细节和准确性,追求完美", strengths: ["分析能力强", "注重细节", "系统思维"], weaknesses: ["可能过于完美主义", "不善社交"], careers: ["数据分析师", "工程师", "研究员"], teamRole: "适合担任专家和质量控制者角色" },
|
||||
Chameleon: { type: "变色龙型", emoji: "🦎", title: "整合者", color: "#06B6D4", description: "适应能力强,能在不同情境中灵活调整", strengths: ["适应力强", "灵活多变", "平衡能力"], weaknesses: ["可能缺乏主见", "身份认同模糊"], careers: ["咨询顾问", "协调员", "自由职业者"], teamRole: "适合担任协调者和多面手角色" }
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mbtiDescriptions,
|
||||
discDescriptions,
|
||||
pdpDescriptions
|
||||
}
|
||||
// utils/descriptions.js - 测试结果描述
|
||||
|
||||
// MBTI类型描述
|
||||
const mbtiDescriptions = {
|
||||
ISTJ: { type: "ISTJ", name: "物流师", category: "守护者", description: "务实、负责、可靠的传统主义者", strengths: ["可靠负责", "注重细节", "有条理"], weaknesses: ["可能过于固执", "不喜变化"], careers: ["会计师", "审计师", "项目经理"] },
|
||||
ISFJ: { type: "ISFJ", name: "守卫者", category: "守护者", description: "安静、友好、负责任的守护者", strengths: ["忠诚体贴", "观察力强", "耐心"], weaknesses: ["不善拒绝", "过于谦虚"], careers: ["护士", "教师", "行政"] },
|
||||
INFJ: { type: "INFJ", name: "提倡者", category: "理想主义者", description: "寻求意义和联系的理想主义者", strengths: ["洞察力强", "有远见", "坚定"], weaknesses: ["过于理想化", "容易疲惫"], careers: ["心理咨询", "作家", "人力资源"] },
|
||||
INTJ: { type: "INTJ", name: "建筑师", category: "理想主义者", description: "独立、有战略眼光的思考者", strengths: ["战略思维", "独立自信", "意志坚定"], weaknesses: ["可能傲慢", "过于苛刻"], careers: ["战略顾问", "科学家", "架构师"] },
|
||||
ISTP: { type: "ISTP", name: "鉴赏家", category: "探险家", description: "灵活、务实的问题解决者", strengths: ["适应力强", "动手能力", "冷静"], weaknesses: ["可能冷漠", "不善表达"], careers: ["工程师", "技术员", "飞行员"] },
|
||||
ISFP: { type: "ISFP", name: "艺术家", category: "探险家", description: "温和、敏感的艺术家", strengths: ["创造力", "同理心", "灵活"], weaknesses: ["过于敏感", "避免冲突"], careers: ["设计师", "艺术家", "摄影师"] },
|
||||
INFP: { type: "INFP", name: "调停者", category: "理想主义者", description: "理想主义、忠诚的调解者", strengths: ["创造力", "同理心", "真诚"], weaknesses: ["过于理想化", "情绪化"], careers: ["作家", "心理咨询", "社工"] },
|
||||
INTP: { type: "INTP", name: "逻辑学家", category: "理想主义者", description: "创新、逻辑的思考者", strengths: ["逻辑思维", "创新能力", "客观"], weaknesses: ["可能孤僻", "忽视情感"], careers: ["程序员", "研究员", "分析师"] },
|
||||
ESTP: { type: "ESTP", name: "动力者", category: "探险家", description: "精力充沛、务实的行动者", strengths: ["果断", "务实", "善于应变"], weaknesses: ["可能冲动", "缺乏耐心"], careers: ["销售", "企业家", "运动员"] },
|
||||
ESFP: { type: "ESFP", name: "表演者", category: "探险家", description: "热情、友好的社交达人", strengths: ["热情友好", "乐观", "灵活"], weaknesses: ["可能肤浅", "容易分心"], careers: ["演员", "销售", "主持人"] },
|
||||
ENFP: { type: "ENFP", name: "竞选者", category: "理想主义者", description: "热情、有创造力的社交者", strengths: ["创造力", "热情", "善于沟通"], weaknesses: ["可能不切实际", "缺乏专注"], careers: ["市场营销", "记者", "顾问"] },
|
||||
ENTP: { type: "ENTP", name: "辩论家", category: "理想主义者", description: "聪明、好奇的思想家", strengths: ["创新能力", "辩论能力", "适应力"], weaknesses: ["可能争辩", "不善执行"], careers: ["律师", "企业家", "咨询"] },
|
||||
ESTJ: { type: "ESTJ", name: "管理者", category: "守护者", description: "务实、果断的组织者", strengths: ["领导力", "组织能力", "务实"], weaknesses: ["可能专制", "不够灵活"], careers: ["管理者", "项目经理", "律师"] },
|
||||
ESFJ: { type: "ESFJ", name: "执政官", category: "守护者", description: "热心、合作的支持者", strengths: ["关心他人", "负责任", "善于合作"], weaknesses: ["过于在意评价", "不善拒绝"], careers: ["教师", "护士", "人力资源"] },
|
||||
ENFJ: { type: "ENFJ", name: "主人公", category: "理想主义者", description: "有魅力、鼓舞人心的领导者", strengths: ["领导力", "同理心", "说服力"], weaknesses: ["过于理想化", "过度付出"], careers: ["培训师", "顾问", "教师"] },
|
||||
ENTJ: { type: "ENTJ", name: "指挥官", category: "理想主义者", description: "大胆、有远见的领导者", strengths: ["领导力", "战略思维", "果断"], weaknesses: ["可能专制", "不耐烦"], careers: ["CEO", "企业家", "律师"] }
|
||||
}
|
||||
|
||||
// DISC类型描述
|
||||
const discDescriptions = {
|
||||
D: { type: "D型", title: "力量", color: "#EF4444", description: "天生的领导者,注重结果和效率", strengths: ["决断力强", "目标导向", "行动迅速"], weaknesses: ["可能过于强势", "缺乏耐心"], careers: ["企业高管", "创业者", "项目经理"] },
|
||||
I: { type: "I型", title: "活跃", color: "#F59E0B", description: "热情友好,善于社交和表达", strengths: ["善于沟通", "乐观积极", "创意丰富"], weaknesses: ["可能过于乐观", "注意力分散"], careers: ["市场营销", "公关", "培训师"] },
|
||||
S: { type: "S型", title: "和平", color: "#10B981", description: "稳重可靠,注重团队和谐", strengths: ["可靠稳定", "团队协作", "耐心倾听"], weaknesses: ["可能抗拒变化", "决策较慢"], careers: ["人力资源", "客户服务", "行政"] },
|
||||
C: { type: "C型", title: "完美", color: "#3B82F6", description: "注重细节和准确性,追求卓越", strengths: ["分析能力强", "注重细节", "准确严谨"], weaknesses: ["可能过于完美主义", "决策较慢"], careers: ["数据分析", "工程师", "会计师"] }
|
||||
}
|
||||
|
||||
// PDP类型描述
|
||||
const pdpDescriptions = {
|
||||
Tiger: { type: "老虎型", emoji: "🐅", title: "支配者", color: "#F59E0B", description: "天生的领导者,具有强烈的目标导向和执行力", strengths: ["决断力强", "目标导向", "行动迅速"], weaknesses: ["可能过于强势", "缺乏耐心"], careers: ["企业高管", "创业者", "项目经理"], teamRole: "适合担任领导者角色" },
|
||||
Peacock: { type: "孔雀型", emoji: "🦚", title: "表现者", color: "#8B5CF6", description: "热情友好,善于表达和社交", strengths: ["善于沟通", "乐观积极", "影响力强"], weaknesses: ["可能过于乐观", "注意力分散"], careers: ["市场营销", "公关", "培训师"], teamRole: "适合担任激励者和调解者角色" },
|
||||
Koala: { type: "无尾熊型", emoji: "🐨", title: "支持者", color: "#10B981", description: "稳重可靠,注重团队和谐", strengths: ["可靠稳定", "团队协作", "善解人意"], weaknesses: ["可能抗拒变化", "决策较慢"], careers: ["人力资源", "客户服务", "教师"], teamRole: "适合担任协调者和支持者角色" },
|
||||
Owl: { type: "猫头鹰型", emoji: "🦉", title: "分析者", color: "#3B82F6", description: "注重细节和准确性,追求完美", strengths: ["分析能力强", "注重细节", "系统思维"], weaknesses: ["可能过于完美主义", "不善社交"], careers: ["数据分析师", "工程师", "研究员"], teamRole: "适合担任专家和质量控制者角色" },
|
||||
Chameleon: { type: "变色龙型", emoji: "🦎", title: "整合者", color: "#06B6D4", description: "适应能力强,能在不同情境中灵活调整", strengths: ["适应力强", "灵活多变", "平衡能力"], weaknesses: ["可能缺乏主见", "身份认同模糊"], careers: ["咨询顾问", "协调员", "自由职业者"], teamRole: "适合担任协调者和多面手角色" }
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mbtiDescriptions,
|
||||
discDescriptions,
|
||||
pdpDescriptions
|
||||
}
|
||||
|
||||
@@ -28,8 +28,9 @@ function getEffectiveEnterpriseId() {
|
||||
/**
|
||||
* 提交给后端的 enterpriseId(analyze、test/submit、支付创建订单、简历分析等)
|
||||
* - 个人版 Tab(appScope=personal):仅传递「本次入口携带的 eid」(enterpriseIdFromScene),
|
||||
* 不因「已绑定企业」或「超管默认企业」带参,避免个人入口却走企业定价/落库企业字段。
|
||||
* 不因「已绑定企业」或 runtime 默认企业带参,避免客户端误把个人页当成强制企业入口。
|
||||
* - 企业版(appScope=enterprise):与 getEffectiveEnterpriseId 一致。
|
||||
* - 未传 enterpriseId 时,后端 Test::submit / Analyze 会按「绑定企业 → 系统默认企业」回落并写入 test_results.enterpriseId。
|
||||
* @returns {number|null}
|
||||
*/
|
||||
function getEnterpriseIdForApiPayload() {
|
||||
|
||||
@@ -130,6 +130,7 @@ function wxPay(options) {
|
||||
// 支付成功后轮询后端订单状态 3~5 次,确保通过微信订单查询接口确认成功
|
||||
pollOrderStatus(orderId, 5, 1000, (ok, order) => {
|
||||
if (ok) {
|
||||
try { reportCrmTestPaymentAfterSuccess(productType, testResultId, enterpriseId || 0) } catch (e) {}
|
||||
try { require('./analytics').reportPayResult(true, { productType: productType || '', orderId, amount }) } catch (e) {}
|
||||
wx.showToast({
|
||||
title: '支付成功',
|
||||
@@ -266,6 +267,35 @@ function pollOrderStatus(orderId, maxAttempts = 5, intervalMs = 1000, done) {
|
||||
tick()
|
||||
}
|
||||
|
||||
/**
|
||||
* 测评类付费(人脸/MBTI/PDP/DISC)支付确认成功后上报存客宝线索;KEY 由后端按企业 cunkebao_keys 解析(对齐深度服务「完成付款」)
|
||||
*/
|
||||
function reportCrmTestPaymentAfterSuccess(productType, testResultId, contextEnterpriseId) {
|
||||
const types = { face: true, mbti: true, disc: true, pdp: true }
|
||||
if (!types[productType]) return
|
||||
const token = wx.getStorageSync('token')
|
||||
if (!token) return
|
||||
wx.request({
|
||||
url: `${paymentApiBase()}/api/crm/report`,
|
||||
method: 'POST',
|
||||
header: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: {
|
||||
testType: productType,
|
||||
testResultId: testResultId || 0,
|
||||
contextEnterpriseId: contextEnterpriseId != null ? Number(contextEnterpriseId) : 0
|
||||
},
|
||||
success(res) {
|
||||
console.log('[CRM] 测评付费线索', res.data)
|
||||
},
|
||||
fail(err) {
|
||||
console.warn('[CRM] 测评付费线索请求失败', err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 购买VIP会员
|
||||
* @param {String} vipType - 'month' | 'quarter' | 'year' | 'lifetime'
|
||||
|
||||
@@ -20,6 +20,19 @@ function isProfileComplete() {
|
||||
return nickname.length > 0 && avatar.length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 与后端 Test::isWechatProfileComplete 一致:昵称、头像、手机号均必填时才解锁完整报告
|
||||
*/
|
||||
function isReportProfileComplete() {
|
||||
const app = getApp()
|
||||
const user = app.globalData.userInfo || wx.getStorageSync('userInfo')
|
||||
if (!user) return false
|
||||
const phone = (user.phone || user.phoneNumber || '').trim()
|
||||
const nickname = (user.nickname || user.nickName || user.username || '').trim()
|
||||
const avatar = (user.avatar || user.avatarUrl || '').trim()
|
||||
return nickname.length > 0 && avatar.length > 0 && phone.length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 若资料未完善则跳转到个人资料页,需登录
|
||||
* @returns {boolean} true=已完善可继续,false=已跳转
|
||||
@@ -112,5 +125,6 @@ module.exports = {
|
||||
hasPhone,
|
||||
bindPhoneByCode,
|
||||
isProfileComplete,
|
||||
isReportProfileComplete,
|
||||
ensureProfileCompleteAndRedirect,
|
||||
}
|
||||
|
||||
@@ -1,268 +1,268 @@
|
||||
/**
|
||||
* 测试结果摘要格式化:类型 + 整数百分比(与管理后台一致)
|
||||
*/
|
||||
|
||||
function toIntPercent(value) {
|
||||
if (value == null) return 0
|
||||
const n = typeof value === 'number' ? value : Number(value)
|
||||
return Number.isFinite(n) ? Math.round(n) : 0
|
||||
}
|
||||
|
||||
const PDP_EN_TO_CN = {
|
||||
Tiger: '老虎型',
|
||||
Peacock: '孔雀型',
|
||||
Koala: '考拉型',
|
||||
Owl: '猫头鹰型',
|
||||
Chameleon: '变色龙型'
|
||||
}
|
||||
|
||||
function pdpOrderedFromScores(scores) {
|
||||
if (!scores || typeof scores !== 'object') return ['', '']
|
||||
const pairs = []
|
||||
for (const k of Object.keys(scores)) {
|
||||
if (!PDP_EN_TO_CN[k]) continue
|
||||
pairs.push([k, Number(scores[k]) || 0])
|
||||
}
|
||||
pairs.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
||||
return [pairs[0] ? pairs[0][0] : '', pairs[1] ? pairs[1][0] : '']
|
||||
}
|
||||
|
||||
function discOrderedFromScores(scores) {
|
||||
if (!scores || typeof scores !== 'object') return ['', '']
|
||||
const allow = { D: 1, I: 1, S: 1, C: 1 }
|
||||
const pairs = []
|
||||
for (const k of Object.keys(scores)) {
|
||||
const u = String(k).trim().toUpperCase().charAt(0)
|
||||
if (!allow[u]) continue
|
||||
pairs.push([u, Number(scores[k]) || 0])
|
||||
}
|
||||
pairs.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
||||
return [pairs[0] ? pairs[0][0] : '', pairs[1] ? pairs[1][0] : '']
|
||||
}
|
||||
|
||||
/** 与 scores 相同维度;DISC 接口常以 percentages 存四维而无 scores */
|
||||
function discOrderedFromPercentages(pct) {
|
||||
return discOrderedFromScores(pct)
|
||||
}
|
||||
|
||||
/** DISC:「S+I型」,仅最后一项带「型」 */
|
||||
function discPrimaryLetter(data) {
|
||||
if (!data || typeof data !== 'object') return ''
|
||||
const dType = data.description && data.description.type
|
||||
if (typeof dType === 'string' && dType) {
|
||||
const noXing = String(dType).trim().replace(/型$/, '')
|
||||
if (noXing.length === 1) {
|
||||
const u = noXing.toUpperCase()
|
||||
if (['D', 'I', 'S', 'C'].includes(u)) return u
|
||||
}
|
||||
}
|
||||
if (data.dominantType) {
|
||||
const u = String(data.dominantType).trim().toUpperCase().charAt(0)
|
||||
if (['D', 'I', 'S', 'C'].includes(u)) return u
|
||||
}
|
||||
if (data.disc) {
|
||||
const noXing = String(data.disc).trim().replace(/型$/, '')
|
||||
if (noXing.length === 1) {
|
||||
const u = noXing.toUpperCase()
|
||||
if (['D', 'I', 'S', 'C'].includes(u)) return u
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function discResolveTwoLetters(data) {
|
||||
let f = discPrimaryLetter(data)
|
||||
let s = ''
|
||||
if (data.secondaryType) {
|
||||
const u = String(data.secondaryType).trim().toUpperCase().charAt(0)
|
||||
if (['D', 'I', 'S', 'C'].includes(u)) s = u
|
||||
}
|
||||
let a0 = ''
|
||||
let b = ''
|
||||
if (data.scores && typeof data.scores === 'object') {
|
||||
const ord = discOrderedFromScores(data.scores)
|
||||
a0 = ord[0] || ''
|
||||
b = ord[1] || ''
|
||||
}
|
||||
if (!a0 && !b && data.percentages && typeof data.percentages === 'object') {
|
||||
const ord = discOrderedFromPercentages(data.percentages)
|
||||
a0 = ord[0] || ''
|
||||
b = ord[1] || ''
|
||||
}
|
||||
if (!f && a0) f = a0
|
||||
if (!s || s === f) {
|
||||
if (b && b !== f) s = b
|
||||
else s = ''
|
||||
}
|
||||
return [f, s]
|
||||
}
|
||||
|
||||
/** 旧接口/库存:「S型 + I型」「S型+I型」→ S+I型 */
|
||||
function discNormalizeLegacyDualType(desc) {
|
||||
if (typeof desc !== 'string' || !desc) return ''
|
||||
const t = desc.replace(/\s+/g, '').replace(/\uFF0B/g, '+')
|
||||
let m = t.match(/^([DISC])型\+([DISC])型$/i)
|
||||
if (m) return m[1].toUpperCase() + '+' + m[2].toUpperCase() + '型'
|
||||
m = t.match(/^([DISC])型$/i)
|
||||
if (m) return m[1].toUpperCase() + '型'
|
||||
return ''
|
||||
}
|
||||
|
||||
function discTopTwoLabel(data) {
|
||||
if (!data || typeof data !== 'object') return ''
|
||||
const [fL, sL] = discResolveTwoLetters(data)
|
||||
if (fL || sL) {
|
||||
if (!fL) return sL ? sL + '型' : ''
|
||||
if (!sL || sL === fL) return fL + '型'
|
||||
return fL + '+' + sL + '型'
|
||||
}
|
||||
return discNormalizeLegacyDualType(data.description && data.description.type)
|
||||
}
|
||||
|
||||
function pdpPrimaryFull(data) {
|
||||
if (!data || typeof data !== 'object') return ''
|
||||
const desc = data.description && data.description.type
|
||||
if (typeof desc === 'string' && desc) return desc.trim()
|
||||
if (data.dominantType) {
|
||||
const key = String(data.dominantType).trim()
|
||||
return PDP_EN_TO_CN[key] || key
|
||||
}
|
||||
if (data.pdp) return String(data.pdp).trim()
|
||||
return ''
|
||||
}
|
||||
|
||||
function pdpResolveTwoFull(data) {
|
||||
let f = pdpPrimaryFull(data)
|
||||
let s = ''
|
||||
if (data.secondaryType) {
|
||||
const k = String(data.secondaryType).trim()
|
||||
s = PDP_EN_TO_CN[k] || k
|
||||
}
|
||||
let aEn = ''
|
||||
let bEn = ''
|
||||
if (data.scores && typeof data.scores === 'object') {
|
||||
const ord = pdpOrderedFromScores(data.scores)
|
||||
aEn = ord[0] || ''
|
||||
bEn = ord[1] || ''
|
||||
}
|
||||
if (!aEn && !bEn && data.percentages && typeof data.percentages === 'object') {
|
||||
const ord = pdpOrderedFromScores(data.percentages)
|
||||
aEn = ord[0] || ''
|
||||
bEn = ord[1] || ''
|
||||
}
|
||||
if (!f && aEn) f = PDP_EN_TO_CN[aEn] || aEn
|
||||
if (!s || s === f) {
|
||||
if (bEn) {
|
||||
const cand = PDP_EN_TO_CN[bEn] || bEn
|
||||
if (cand !== f) s = cand
|
||||
}
|
||||
}
|
||||
return [f, s]
|
||||
}
|
||||
|
||||
/** PDP:「孔雀+老虎型」,仅最后一项带「型」 */
|
||||
function pdpTopTwoLabel(data) {
|
||||
if (!data || typeof data !== 'object') return ''
|
||||
const [ff, sf] = pdpResolveTwoFull(data)
|
||||
if (!ff) return sf || ''
|
||||
if (!sf || sf === ff) return ff
|
||||
const short = String(ff).replace(/型$/, '')
|
||||
return short + '+' + sf
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据测试类型和原始结果,生成带整数百分比的摘要文案
|
||||
* @param {object} data - 单条测试结果(mbtiResult / discResult / pdpResult)
|
||||
* @param {string} testType - 'mbti' | 'disc' | 'pdp'
|
||||
* @returns {string}
|
||||
*/
|
||||
function formatTestSummary(data, testType) {
|
||||
if (!data || typeof data !== 'object') return ''
|
||||
const t = (testType || '').toLowerCase()
|
||||
|
||||
if (t === 'mbti') {
|
||||
const label = data.mbtiType || data.type || data.result || ''
|
||||
const dims = data.dimensionScores
|
||||
if (dims && typeof dims === 'object') {
|
||||
const parts = []
|
||||
const order = ['EI', 'SN', 'TF', 'JP']
|
||||
for (const key of order) {
|
||||
const pct = dims[key] && (dims[key].percentage != null ? dims[key].percentage : dims[key].dominant)
|
||||
if (pct != null) parts.push(toIntPercent(pct) + '%')
|
||||
}
|
||||
if (parts.length) return label + ' (' + parts.join(' ') + ')'
|
||||
}
|
||||
return String(label)
|
||||
}
|
||||
|
||||
if (t === 'disc') {
|
||||
const two = discTopTwoLabel(data)
|
||||
const desc = data.description && data.description.type
|
||||
const label = two || ((typeof desc === 'string' && desc) ? desc : ((data.dominantType ? data.dominantType + '型' : '') || (data.disc || '')))
|
||||
const pct = data.percentages
|
||||
if (pct && typeof pct === 'object') {
|
||||
const d = toIntPercent(pct.D != null ? pct.D : pct.d)
|
||||
const i = toIntPercent(pct.I != null ? pct.I : pct.i)
|
||||
const s = toIntPercent(pct.S != null ? pct.S : pct.s)
|
||||
const c = toIntPercent(pct.C != null ? pct.C : pct.c)
|
||||
return label + ' D:' + d + '% I:' + i + '% S:' + s + '% C:' + c + '%'
|
||||
}
|
||||
return label || ''
|
||||
}
|
||||
|
||||
if (t === 'pdp') {
|
||||
const two = pdpTopTwoLabel(data)
|
||||
const desc = data.description && data.description.type
|
||||
const label = two || ((typeof desc === 'string' && desc) ? desc : (data.dominantType || data.pdp || ''))
|
||||
const pct = data.percentages
|
||||
if (pct && typeof pct === 'object') {
|
||||
const names = { Tiger: '老虎', Peacock: '孔雀', Owl: '猫头鹰', Koala: '考拉', Chameleon: '变色龙' }
|
||||
const parts = []
|
||||
for (const [key, name] of Object.entries(names)) {
|
||||
const v = pct[key] != null ? pct[key] : pct[key.toLowerCase()]
|
||||
if (v != null) parts.push(name + ':' + toIntPercent(v) + '%')
|
||||
}
|
||||
if (parts.length) return label + ' ' + parts.join(' ')
|
||||
}
|
||||
return label || ''
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅返回类型标签(无百分比),用于列表、个人中心等
|
||||
*/
|
||||
function getTypeOnly(data, testType) {
|
||||
if (!data || typeof data !== 'object') return ''
|
||||
const t = (testType || '').toLowerCase()
|
||||
if (t === 'mbti') return String(data.mbtiType ?? data.type ?? data.result ?? '')
|
||||
if (t === 'disc') {
|
||||
const two = discTopTwoLabel(data)
|
||||
if (two) return two
|
||||
const desc = data.description?.type
|
||||
if (typeof desc === 'string' && desc) return desc
|
||||
if (data.dominantType) return String(data.dominantType) + '型'
|
||||
return String(data.disc ?? '')
|
||||
}
|
||||
if (t === 'pdp') {
|
||||
const two = pdpTopTwoLabel(data)
|
||||
if (two) return two
|
||||
const desc = data.description?.type
|
||||
if (typeof desc === 'string' && desc) return desc
|
||||
if (data.dominantType) {
|
||||
const k = String(data.dominantType).trim()
|
||||
return PDP_EN_TO_CN[k] || k
|
||||
}
|
||||
return String(data.pdp ?? '')
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
toIntPercent,
|
||||
formatTestSummary,
|
||||
getTypeOnly,
|
||||
discTopTwoLabel,
|
||||
pdpTopTwoLabel
|
||||
}
|
||||
/**
|
||||
* 测试结果摘要格式化:类型 + 整数百分比(与管理后台一致)
|
||||
*/
|
||||
|
||||
function toIntPercent(value) {
|
||||
if (value == null) return 0
|
||||
const n = typeof value === 'number' ? value : Number(value)
|
||||
return Number.isFinite(n) ? Math.round(n) : 0
|
||||
}
|
||||
|
||||
const PDP_EN_TO_CN = {
|
||||
Tiger: '老虎型',
|
||||
Peacock: '孔雀型',
|
||||
Koala: '无尾熊型',
|
||||
Owl: '猫头鹰型',
|
||||
Chameleon: '变色龙型'
|
||||
}
|
||||
|
||||
function pdpOrderedFromScores(scores) {
|
||||
if (!scores || typeof scores !== 'object') return ['', '']
|
||||
const pairs = []
|
||||
for (const k of Object.keys(scores)) {
|
||||
if (!PDP_EN_TO_CN[k]) continue
|
||||
pairs.push([k, Number(scores[k]) || 0])
|
||||
}
|
||||
pairs.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
||||
return [pairs[0] ? pairs[0][0] : '', pairs[1] ? pairs[1][0] : '']
|
||||
}
|
||||
|
||||
function discOrderedFromScores(scores) {
|
||||
if (!scores || typeof scores !== 'object') return ['', '']
|
||||
const allow = { D: 1, I: 1, S: 1, C: 1 }
|
||||
const pairs = []
|
||||
for (const k of Object.keys(scores)) {
|
||||
const u = String(k).trim().toUpperCase().charAt(0)
|
||||
if (!allow[u]) continue
|
||||
pairs.push([u, Number(scores[k]) || 0])
|
||||
}
|
||||
pairs.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
||||
return [pairs[0] ? pairs[0][0] : '', pairs[1] ? pairs[1][0] : '']
|
||||
}
|
||||
|
||||
/** 与 scores 相同维度;DISC 接口常以 percentages 存四维而无 scores */
|
||||
function discOrderedFromPercentages(pct) {
|
||||
return discOrderedFromScores(pct)
|
||||
}
|
||||
|
||||
/** DISC:「S+I型」,仅最后一项带「型」 */
|
||||
function discPrimaryLetter(data) {
|
||||
if (!data || typeof data !== 'object') return ''
|
||||
const dType = data.description && data.description.type
|
||||
if (typeof dType === 'string' && dType) {
|
||||
const noXing = String(dType).trim().replace(/型$/, '')
|
||||
if (noXing.length === 1) {
|
||||
const u = noXing.toUpperCase()
|
||||
if (['D', 'I', 'S', 'C'].includes(u)) return u
|
||||
}
|
||||
}
|
||||
if (data.dominantType) {
|
||||
const u = String(data.dominantType).trim().toUpperCase().charAt(0)
|
||||
if (['D', 'I', 'S', 'C'].includes(u)) return u
|
||||
}
|
||||
if (data.disc) {
|
||||
const noXing = String(data.disc).trim().replace(/型$/, '')
|
||||
if (noXing.length === 1) {
|
||||
const u = noXing.toUpperCase()
|
||||
if (['D', 'I', 'S', 'C'].includes(u)) return u
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function discResolveTwoLetters(data) {
|
||||
let f = discPrimaryLetter(data)
|
||||
let s = ''
|
||||
if (data.secondaryType) {
|
||||
const u = String(data.secondaryType).trim().toUpperCase().charAt(0)
|
||||
if (['D', 'I', 'S', 'C'].includes(u)) s = u
|
||||
}
|
||||
let a0 = ''
|
||||
let b = ''
|
||||
if (data.scores && typeof data.scores === 'object') {
|
||||
const ord = discOrderedFromScores(data.scores)
|
||||
a0 = ord[0] || ''
|
||||
b = ord[1] || ''
|
||||
}
|
||||
if (!a0 && !b && data.percentages && typeof data.percentages === 'object') {
|
||||
const ord = discOrderedFromPercentages(data.percentages)
|
||||
a0 = ord[0] || ''
|
||||
b = ord[1] || ''
|
||||
}
|
||||
if (!f && a0) f = a0
|
||||
if (!s || s === f) {
|
||||
if (b && b !== f) s = b
|
||||
else s = ''
|
||||
}
|
||||
return [f, s]
|
||||
}
|
||||
|
||||
/** 旧接口/库存:「S型 + I型」「S型+I型」→ S+I型 */
|
||||
function discNormalizeLegacyDualType(desc) {
|
||||
if (typeof desc !== 'string' || !desc) return ''
|
||||
const t = desc.replace(/\s+/g, '').replace(/\uFF0B/g, '+')
|
||||
let m = t.match(/^([DISC])型\+([DISC])型$/i)
|
||||
if (m) return m[1].toUpperCase() + '+' + m[2].toUpperCase() + '型'
|
||||
m = t.match(/^([DISC])型$/i)
|
||||
if (m) return m[1].toUpperCase() + '型'
|
||||
return ''
|
||||
}
|
||||
|
||||
function discTopTwoLabel(data) {
|
||||
if (!data || typeof data !== 'object') return ''
|
||||
const [fL, sL] = discResolveTwoLetters(data)
|
||||
if (fL || sL) {
|
||||
if (!fL) return sL ? sL + '型' : ''
|
||||
if (!sL || sL === fL) return fL + '型'
|
||||
return fL + '+' + sL + '型'
|
||||
}
|
||||
return discNormalizeLegacyDualType(data.description && data.description.type)
|
||||
}
|
||||
|
||||
function pdpPrimaryFull(data) {
|
||||
if (!data || typeof data !== 'object') return ''
|
||||
const desc = data.description && data.description.type
|
||||
if (typeof desc === 'string' && desc) return desc.trim()
|
||||
if (data.dominantType) {
|
||||
const key = String(data.dominantType).trim()
|
||||
return PDP_EN_TO_CN[key] || key
|
||||
}
|
||||
if (data.pdp) return String(data.pdp).trim()
|
||||
return ''
|
||||
}
|
||||
|
||||
function pdpResolveTwoFull(data) {
|
||||
let f = pdpPrimaryFull(data)
|
||||
let s = ''
|
||||
if (data.secondaryType) {
|
||||
const k = String(data.secondaryType).trim()
|
||||
s = PDP_EN_TO_CN[k] || k
|
||||
}
|
||||
let aEn = ''
|
||||
let bEn = ''
|
||||
if (data.scores && typeof data.scores === 'object') {
|
||||
const ord = pdpOrderedFromScores(data.scores)
|
||||
aEn = ord[0] || ''
|
||||
bEn = ord[1] || ''
|
||||
}
|
||||
if (!aEn && !bEn && data.percentages && typeof data.percentages === 'object') {
|
||||
const ord = pdpOrderedFromScores(data.percentages)
|
||||
aEn = ord[0] || ''
|
||||
bEn = ord[1] || ''
|
||||
}
|
||||
if (!f && aEn) f = PDP_EN_TO_CN[aEn] || aEn
|
||||
if (!s || s === f) {
|
||||
if (bEn) {
|
||||
const cand = PDP_EN_TO_CN[bEn] || bEn
|
||||
if (cand !== f) s = cand
|
||||
}
|
||||
}
|
||||
return [f, s]
|
||||
}
|
||||
|
||||
/** PDP:「孔雀+老虎型」,仅最后一项带「型」 */
|
||||
function pdpTopTwoLabel(data) {
|
||||
if (!data || typeof data !== 'object') return ''
|
||||
const [ff, sf] = pdpResolveTwoFull(data)
|
||||
if (!ff) return sf || ''
|
||||
if (!sf || sf === ff) return ff
|
||||
const short = String(ff).replace(/型$/, '')
|
||||
return short + '+' + sf
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据测试类型和原始结果,生成带整数百分比的摘要文案
|
||||
* @param {object} data - 单条测试结果(mbtiResult / discResult / pdpResult)
|
||||
* @param {string} testType - 'mbti' | 'disc' | 'pdp'
|
||||
* @returns {string}
|
||||
*/
|
||||
function formatTestSummary(data, testType) {
|
||||
if (!data || typeof data !== 'object') return ''
|
||||
const t = (testType || '').toLowerCase()
|
||||
|
||||
if (t === 'mbti') {
|
||||
const label = data.mbtiType || data.type || data.result || ''
|
||||
const dims = data.dimensionScores
|
||||
if (dims && typeof dims === 'object') {
|
||||
const parts = []
|
||||
const order = ['EI', 'SN', 'TF', 'JP']
|
||||
for (const key of order) {
|
||||
const pct = dims[key] && (dims[key].percentage != null ? dims[key].percentage : dims[key].dominant)
|
||||
if (pct != null) parts.push(toIntPercent(pct) + '%')
|
||||
}
|
||||
if (parts.length) return label + ' (' + parts.join(' ') + ')'
|
||||
}
|
||||
return String(label)
|
||||
}
|
||||
|
||||
if (t === 'disc') {
|
||||
const two = discTopTwoLabel(data)
|
||||
const desc = data.description && data.description.type
|
||||
const label = two || ((typeof desc === 'string' && desc) ? desc : ((data.dominantType ? data.dominantType + '型' : '') || (data.disc || '')))
|
||||
const pct = data.percentages
|
||||
if (pct && typeof pct === 'object') {
|
||||
const d = toIntPercent(pct.D != null ? pct.D : pct.d)
|
||||
const i = toIntPercent(pct.I != null ? pct.I : pct.i)
|
||||
const s = toIntPercent(pct.S != null ? pct.S : pct.s)
|
||||
const c = toIntPercent(pct.C != null ? pct.C : pct.c)
|
||||
return label + ' D:' + d + '% I:' + i + '% S:' + s + '% C:' + c + '%'
|
||||
}
|
||||
return label || ''
|
||||
}
|
||||
|
||||
if (t === 'pdp') {
|
||||
const two = pdpTopTwoLabel(data)
|
||||
const desc = data.description && data.description.type
|
||||
const label = two || ((typeof desc === 'string' && desc) ? desc : (data.dominantType || data.pdp || ''))
|
||||
const pct = data.percentages
|
||||
if (pct && typeof pct === 'object') {
|
||||
const names = { Tiger: '老虎', Peacock: '孔雀', Owl: '猫头鹰', Koala: '无尾熊', Chameleon: '变色龙' }
|
||||
const parts = []
|
||||
for (const [key, name] of Object.entries(names)) {
|
||||
const v = pct[key] != null ? pct[key] : pct[key.toLowerCase()]
|
||||
if (v != null) parts.push(name + ':' + toIntPercent(v) + '%')
|
||||
}
|
||||
if (parts.length) return label + ' ' + parts.join(' ')
|
||||
}
|
||||
return label || ''
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅返回类型标签(无百分比),用于列表、个人中心等
|
||||
*/
|
||||
function getTypeOnly(data, testType) {
|
||||
if (!data || typeof data !== 'object') return ''
|
||||
const t = (testType || '').toLowerCase()
|
||||
if (t === 'mbti') return String(data.mbtiType ?? data.type ?? data.result ?? '')
|
||||
if (t === 'disc') {
|
||||
const two = discTopTwoLabel(data)
|
||||
if (two) return two
|
||||
const desc = data.description?.type
|
||||
if (typeof desc === 'string' && desc) return desc
|
||||
if (data.dominantType) return String(data.dominantType) + '型'
|
||||
return String(data.disc ?? '')
|
||||
}
|
||||
if (t === 'pdp') {
|
||||
const two = pdpTopTwoLabel(data)
|
||||
if (two) return two
|
||||
const desc = data.description?.type
|
||||
if (typeof desc === 'string' && desc) return desc
|
||||
if (data.dominantType) {
|
||||
const k = String(data.dominantType).trim()
|
||||
return PDP_EN_TO_CN[k] || k
|
||||
}
|
||||
return String(data.pdp ?? '')
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
toIntPercent,
|
||||
formatTestSummary,
|
||||
getTypeOnly,
|
||||
discTopTwoLabel,
|
||||
pdpTopTwoLabel
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user