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:
Ghost
2026-04-01 17:16:20 +08:00
parent 8abc5182af
commit ef19f6860e
63 changed files with 2718 additions and 1190 deletions

View File

@@ -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' })

View File

@@ -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>

View File

@@ -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%;

View File

@@ -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%
);
}

View File

@@ -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) => {

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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) => {

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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()