This commit is contained in:
Alex-larget
2026-03-24 18:45:32 +08:00
parent dcb7961945
commit f3d74ce94a
68 changed files with 2461 additions and 2535 deletions

View File

@@ -3,6 +3,7 @@
* 登录后若仍为默认头像/昵称,在此修改;仅头像与昵称两项
*/
const app = getApp()
const { trackClick } = require('../../utils/trackClick')
Page({
data: {
@@ -14,15 +15,21 @@ Page({
nicknameInputFocus: false,
/** 规则引擎传入avatar | nickname用于高亮对应区块 */
uiFocus: '',
fromNewUser: false,
},
onLoad(options) {
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
const fromNewUser = String(options.from || '').toLowerCase() === 'new_user'
const focus = String(options.focus || '').toLowerCase()
if (focus === 'avatar' || focus === 'nickname') {
this.setData({ uiFocus: focus })
}
this.setData({ fromNewUser })
this.loadFromUser()
if (fromNewUser) {
trackClick('avatar_nickname', 'page_view', '新注册引导页')
}
if (focus === 'nickname') {
setTimeout(() => {
if (typeof wx.requirePrivacyAuthorize === 'function') {
@@ -92,6 +99,7 @@ Page({
async onChooseAvatar(e) {
const tempAvatarUrl = e.detail?.avatarUrl
if (!tempAvatarUrl) return
trackClick('avatar_nickname', 'btn_click', '选择头像')
await this.uploadAndSaveAvatar(tempAvatarUrl)
},
@@ -132,6 +140,9 @@ Page({
}
wx.hideLoading()
wx.showToast({ title: '头像已更新', icon: 'success' })
if (this.data.fromNewUser) {
trackClick('avatar_nickname', 'form_step_done', '头像更新完成')
}
} catch (e) {
wx.hideLoading()
wx.showToast({ title: e.message || '上传失败', icon: 'none' })
@@ -150,6 +161,7 @@ Page({
wx.showToast({ title: '请输入昵称', icon: 'none' })
return
}
trackClick('avatar_nickname', 'btn_click', '完成保存')
this.setData({ saving: true })
try {
await app.request({
@@ -162,6 +174,13 @@ Page({
if (avatar) app.globalData.userInfo.avatar = avatar
wx.setStorageSync('userInfo', app.globalData.userInfo)
}
if (this.data.fromNewUser) {
wx.setStorageSync('new_user_guide_done_at', Date.now())
trackClick('avatar_nickname', 'form_submit', '新注册引导完成', {
hasAvatar: !!avatar,
nicknameLen: nickname.length,
})
}
wx.showToast({ title: '保存成功', icon: 'success' })
setTimeout(() => getApp().goBackOrToHome(), 800)
} catch (e) {
@@ -171,6 +190,7 @@ Page({
},
goToFullProfile() {
trackClick('avatar_nickname', 'nav_click', '编辑完整档案')
wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
},
})

View File

@@ -10,10 +10,12 @@
<view class="content">
<view class="guide-card">
<icon name="handshake" size="64" color="#00CED1" customClass="guide-icon"></icon>
<text class="guide-title">设置对外展示信息</text>
<text class="guide-title">{{fromNewUser ? '欢迎加入,先完成基础信息' : '设置对外展示信息'}}</text>
<text class="guide-badge" wx:if="{{fromNewUser}}">新用户引导</text>
<text class="guide-desc" wx:if="{{uiFocus === 'avatar'}}">请先换一张清晰头像,伙伴更容易认出你。</text>
<text class="guide-desc" wx:elif="{{uiFocus === 'nickname'}}">请改一个真实好记的昵称,方便伙伴称呼你。</text>
<text class="guide-desc" wx:else>头像与昵称会出现在名片与匹配卡片上,方便伙伴认出你。</text>
<text class="guide-desc guide-desc-sub" wx:if="{{fromNewUser}}">完成后可继续编辑完整档案手机号、行业、MBTI 等)。</text>
</view>
<!-- 头像:点击直接弹出微信原生选择器;头像与文字水平对齐 -->

View File

@@ -64,11 +64,25 @@
color: #5EEAD4;
margin-bottom: 12rpx;
}
.guide-badge {
margin-bottom: 10rpx;
padding: 6rpx 16rpx;
font-size: 20rpx;
color: #22d3ee;
border-radius: 999rpx;
border: 1rpx solid rgba(34, 211, 238, 0.4);
background: rgba(34, 211, 238, 0.1);
}
.guide-desc {
font-size: 26rpx;
color: rgba(148, 163, 184, 0.95);
line-height: 1.5;
}
.guide-desc-sub {
margin-top: 8rpx;
color: rgba(148, 163, 184, 0.85);
font-size: 24rpx;
}
.avatar-section {
display: flex;

View File

@@ -54,7 +54,8 @@ Page({
// mp_config.mpUi.chaptersPage
chaptersBookTitle: '一场SOUL的创业实验场',
chaptersBookSubtitle: '来自Soul派对房的真实商业故事'
chaptersBookSubtitle: '来自Soul派对房的真实商业故事',
chaptersNewBadgeText: 'NEW'
},
onLoad() {
@@ -71,13 +72,20 @@ Page({
_applyChaptersMpUi() {
const c = app.globalData.configCache?.mpConfig?.mpUi?.chaptersPage || {}
const h = app.globalData.configCache?.mpConfig?.mpUi?.homePage || {}
const newBadgeText = String(c.newBadgeText || c.sectionNewBadgeText || h.latestSectionTitle || 'NEW').trim() || 'NEW'
this.setData({
chaptersBookTitle: String(c.bookTitle || '一场SOUL的创业实验场').trim() || '一场SOUL的创业实验场',
chaptersBookSubtitle: String(c.bookSubtitle || '来自Soul派对房的真实商业故事').trim() ||
'来自Soul派对房的真实商业故事'
'来自Soul派对房的真实商业故事',
chaptersNewBadgeText: newBadgeText
})
},
_normalizeBadgeText(v) {
return String(v || '').trim().slice(0, 8)
},
async loadFeatureConfig() {
try {
if (app.globalData.features && typeof app.globalData.features.searchEnabled === 'boolean') {
@@ -122,10 +130,14 @@ Page({
let icon = String(p.icon || '').trim()
if (icon && !isSafeImageSrc(icon)) icon = ''
const iconEmoji = icon ? '' : partEmojiForBodyIndex(idx)
const partBadgeText = this._normalizeBadgeText(
p.badgeText || p.badge_text || p.partBadgeText || p.part_badge_text
)
return {
id: p.id,
icon,
iconEmoji,
iconText: partBadgeText,
title: p.title,
subtitle: p.subtitle || '',
chapterCount: p.chapterCount || 0,
@@ -176,6 +188,9 @@ Page({
isFree: r.isFree === true || (r.price !== undefined && r.price === 0),
price: r.price ?? 1,
isNew: r.isNew === true || r.is_new === true,
newBadgeText: this._normalizeBadgeText(
r.newBadgeText || r.new_badge_text || r.sectionBadgeText || r.section_badge_text || r.badgeText || r.badge_text
),
isPremium
})
})

View File

@@ -73,6 +73,7 @@
<view class="part-header" bindtap="togglePart" data-id="{{item.id}}">
<view class="part-left">
<image wx:if="{{item.icon}}" class="part-icon-img" src="{{item.icon}}" mode="aspectFill"/>
<view wx:elif="{{item.iconText}}" class="part-icon part-icon-text">{{item.iconText}}</view>
<view wx:elif="{{item.iconEmoji}}" class="part-icon part-icon-emoji">{{item.iconEmoji}}</view>
<view wx:else class="part-icon">{{item.title[0] || '篇'}}</view>
<view class="part-info">
@@ -101,7 +102,7 @@
<icon wx:else name="lock" size="24" color="rgba(255,255,255,0.3)" customClass="section-lock lock-closed"></icon>
</view>
<text class="section-title {{section.isFree || isVip || (!section.isPremium && hasFullBook) || purchasedSections.indexOf(section.id) > -1 ? '' : 'text-muted'}}">{{section.id}} {{section.title}}</text>
<text wx:if="{{section.isNew}}" class="tag tag-new">NEW</text>
<text wx:if="{{section.isNew || section.newBadgeText}}" class="tag tag-new">{{section.newBadgeText || chaptersNewBadgeText}}</text>
</view>
<view class="section-right">
<text wx:if="{{section.isFree}}" class="tag tag-free">免费</text>

View File

@@ -380,6 +380,13 @@
color: #ffffff;
}
.part-icon-text {
font-size: 22rpx;
font-weight: 700;
letter-spacing: 1rpx;
line-height: 1;
}
.part-subtitle {
font-size: 20rpx;
color: rgba(255, 255, 255, 0.4);

View File

@@ -8,6 +8,7 @@
* 点头像:登录后依次校验本人头像(非默认)、微信号、绑定手机号,再弹「链接「昵称」」;有 ckbLeadToken 走人物获客计划,否则走全局留资
*/
const app = getApp()
const soulBridge = require('../../utils/soulBridge.js')
const { trackClick } = require('../../utils/trackClick')
const { isSafeImageSrc } = require('../../utils/imageUrl.js')
const { resolveAvatarWithMbti } = require('../../utils/mbtiAvatar.js')
@@ -334,45 +335,13 @@ Page({
/** 无人物 token 时:全局留资,便于运营侧主动加好友并协助链接该会员 */
async _doGlobalMemberLeadSubmit(member) {
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) return
const myUserId = app.globalData.userInfo.id
let { phone, wechatId } = await this._resolveLeadPhoneWechat(myUserId)
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
wx.showModal({
title: '补全手机号',
content: '请填写手机号(必填),便于工作人员联系您、协助链接该超级个体。',
confirmText: '去填写',
cancelText: '取消',
success: (res) => { if (res.confirm) wx.navigateTo({ url: '/pages/profile-edit/profile-edit' }) }
})
return
}
wx.showLoading({ title: '提交中...', mask: true })
try {
const res = await app.request({
url: '/api/miniprogram/ckb/lead',
method: 'POST',
data: {
userId: myUserId,
phone: phone || undefined,
wechatId: wechatId || undefined,
name: (app.globalData.userInfo.nickname || '').trim() || undefined,
targetNickname: '',
targetMemberId: member.id || undefined,
targetMemberName: (member.name || '').trim() || undefined,
source: 'member_detail_global',
}
})
wx.hideLoading()
if (res && res.success) {
wx.setStorageSync('lead_last_submit_ts', Date.now())
wx.showToast({ title: res.message || '提交成功,请留意工作人员联系', icon: 'success' })
} else {
wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
}
} catch (e) {
wx.hideLoading()
wx.showToast({ title: (e && e.message) || '提交失败', icon: 'none' })
}
await soulBridge.submitCkbLead(app, {
targetMemberId: String(member.id || ''),
targetMemberName: (member.name || '').trim(),
targetNickname: '',
source: 'member_detail_global',
phoneModalContent: '请填写手机号(必填),便于工作人员联系您、协助链接该超级个体。',
})
},
async _resolveLeadPhoneWechat(myUserId) {
@@ -390,58 +359,16 @@ Page({
return { phone, wechatId }
},
/** 与 read 页 _doMentionAddFriend 一致:targetUserId = Person.token可选带超级个体 userId 写入留资 params */
/** targetUserId = Person.token可选带超级个体 id/name 写入留资 params(与 read 页 @ 同 soulBridge */
async _doCkbLeadSubmit(targetUserId, targetNickname, targetMemberId, targetMemberName) {
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) {
wx.showModal({
title: '提示',
content: '请先登录后再添加好友',
confirmText: '去登录',
cancelText: '取消',
success: (res) => { if (res.confirm) wx.switchTab({ url: '/pages/my/my' }) }
})
return
}
const myUserId = app.globalData.userInfo.id
let { phone, wechatId } = await this._resolveLeadPhoneWechat(myUserId)
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
wx.showModal({
title: '补全手机号',
content: '请填写手机号(必填),便于对方通过获客计划联系您。',
confirmText: '去填写',
cancelText: '取消',
success: (res) => { if (res.confirm) wx.navigateTo({ url: '/pages/profile-edit/profile-edit' }) }
})
return
}
wx.showLoading({ title: '提交中...', mask: true })
try {
const res = await app.request({
url: '/api/miniprogram/ckb/lead',
method: 'POST',
data: {
userId: myUserId,
phone: phone || undefined,
wechatId: wechatId || undefined,
name: (app.globalData.userInfo.nickname || '').trim() || undefined,
targetUserId,
targetNickname: targetNickname || undefined,
targetMemberId: targetMemberId || undefined,
targetMemberName: targetMemberName || undefined,
source: 'member_detail_avatar'
}
})
wx.hideLoading()
if (res && res.success) {
wx.setStorageSync('lead_last_submit_ts', Date.now())
wx.showToast({ title: res.message || '提交成功,对方会尽快联系您', icon: 'success' })
} else {
wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
}
} catch (e) {
wx.hideLoading()
wx.showToast({ title: (e && e.message) || '提交失败', icon: 'none' })
}
await soulBridge.submitCkbLead(app, {
targetUserId,
targetNickname,
targetMemberId: targetMemberId || undefined,
targetMemberName: targetMemberName || undefined,
source: 'member_detail_avatar',
phoneModalContent: '请填写手机号(必填),便于对方通过获客计划联系您。',
})
},
_ensureUnlockedForLink(field) {

View File

@@ -1054,7 +1054,17 @@ Page({
this.loadWalletBalance()
} catch (e) {
wx.hideLoading()
wx.showToast({ title: e.message || '提现失败', icon: 'none' })
const resp = e && e.response
if (resp && (resp.needBind || resp.needBindWechat)) {
wx.showModal({
title: resp.needBindWechat ? '请先绑定微信号' : '需要绑定微信',
content: resp.needBindWechat ? '请到「设置」中绑定微信号后再提现,便于到账核对。' : '请先在设置中绑定微信账号后再提现',
confirmText: '去绑定',
success: (r) => { if (r.confirm) wx.navigateTo({ url: '/pages/settings/settings' }) }
})
return
}
wx.showToast({ title: (e && e.message) || '提现失败', icon: 'none' })
}
}
})

View File

@@ -140,6 +140,15 @@
.save-btn[disabled] { opacity: 0.6; }
.bottom-space { height: 120rpx; }
/* 分享图绘制用 canvas仅用于离屏生成不在页面可见 */
.share-card-canvas {
position: fixed;
left: -9999px;
top: -9999px;
opacity: 0;
pointer-events: none;
}
/* 昵称提示文案 */
.input-tip {
margin-top: 8rpx;
@@ -234,93 +243,7 @@
color: #9CA3AF;
}
/* 昵称隐私弹窗 */
.privacy-mask {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
}
.privacy-modal {
width: 560rpx;
padding: 48rpx 40rpx;
background: #1E293B;
border-radius: 24rpx;
text-align: center;
}
.privacy-modal .privacy-title { font-size: 34rpx; font-weight: 600; color: #fff; margin-bottom: 24rpx; }
.privacy-modal .privacy-desc { font-size: 28rpx; color: #94A3B8; line-height: 1.6; margin-bottom: 40rpx; }
.privacy-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background: #5EEAD4;
color: #0F172A;
font-size: 32rpx;
font-weight: 600;
border-radius: 44rpx;
border: none;
}
/* 隐私授权弹窗 */
.privacy-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
}
.privacy-modal {
width: 580rpx;
background: #1E293B;
border-radius: 24rpx;
padding: 40rpx 32rpx 32rpx;
}
.privacy-modal-title {
font-size: 34rpx;
font-weight: 600;
color: #F8FAFC;
margin-bottom: 24rpx;
text-align: center;
}
.privacy-modal-desc {
font-size: 28rpx;
color: #94A3B8;
line-height: 1.5;
margin-bottom: 32rpx;
}
.privacy-btn {
width: 100%;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
background: #5EEAD4;
color: #0F172A;
font-size: 32rpx;
font-weight: 600;
border-radius: 44rpx;
margin-bottom: 16rpx;
}
.privacy-btn:last-of-type {
margin-bottom: 0;
background: transparent;
color: #94A3B8;
border: 2rpx solid #475569;
}
/* 昵称隐私授权弹窗(解决 errno:104 */
/* 昵称隐私授权弹窗(单一定义,避免样式冲突) */
.privacy-mask {
position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 9999;
display: flex; align-items: center; justify-content: center; padding: 48rpx;

View File

@@ -19,6 +19,7 @@ const { parseScene } = require('../../utils/scene.js')
const contentParser = require('../../utils/contentParser.js')
const { trackClick } = require('../../utils/trackClick')
const { checkAndExecute } = require('../../utils/ruleEngine')
const soulBridge = require('../../utils/soulBridge.js')
const app = getApp()
@@ -729,71 +730,14 @@ Page({
})
},
// 边界:未登录→去登录;无手机/微信号→去资料编辑;重复同一人→本地 key 去重
// 正文 @:统一走 soulBridge登录/手机号校验/提交与错误提示一处维护)
async _doMentionAddFriend(targetUserId, targetNickname) {
const app = getApp()
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) {
wx.showModal({
title: '提示',
content: '请先登录后再添加好友',
confirmText: '去登录',
cancelText: '取消',
success: (res) => {
if (res.confirm) wx.switchTab({ url: '/pages/my/my' })
}
})
return
}
const myUserId = app.globalData.userInfo.id
let phone = (app.globalData.userInfo.phone || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
let wechatId = (app.globalData.userInfo.wechatId || app.globalData.userInfo.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
try {
const profileRes = await app.request({ url: `/api/miniprogram/user/profile?userId=${myUserId}`, silent: true })
if (profileRes?.success && profileRes.data) {
phone = (profileRes.data.phone || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
wechatId = (profileRes.data.wechatId || profileRes.data.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
}
} catch (e) {}
}
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
wx.showModal({
title: '补全手机号',
content: '请填写手机号(必填),便于对方联系您。',
confirmText: '去填写',
cancelText: '取消',
success: (res) => {
if (res.confirm) wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
}
})
return
}
wx.showLoading({ title: '提交中...', mask: true })
try {
const res = await app.request({
url: '/api/miniprogram/ckb/lead',
method: 'POST',
data: {
userId: myUserId,
phone: phone || undefined,
wechatId: wechatId || undefined,
name: (app.globalData.userInfo.nickname || '').trim() || undefined,
targetUserId,
targetNickname: targetNickname || undefined,
source: 'article_mention'
}
})
wx.hideLoading()
if (res && res.success) {
wx.setStorageSync('lead_last_submit_ts', Date.now())
wx.showToast({ title: res.message || '提交成功,对方会尽快联系您', icon: 'success' })
} else {
wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
}
} catch (e) {
wx.hideLoading()
wx.showToast({ title: (e && e.message) || '提交失败', icon: 'none' })
}
await soulBridge.submitCkbLead(getApp(), {
targetUserId,
targetNickname,
source: 'article_mention',
phoneModalContent: '请填写手机号(必填),便于对方联系您。',
})
},
// 分享弹窗
@@ -1245,7 +1189,7 @@ Page({
try {
// 0. 尝试余额支付(若余额足够)
const userId = app.globalData.userInfo?.id
const referralCode = wx.getStorageSync('referral_code') || ''
const referralCode = soulBridge.getReferralCodeForPay(app)
if (userId) {
try {
const balanceRes = await app.request({ url: `/api/miniprogram/balance?userId=${userId}`, silent: true })
@@ -1315,8 +1259,8 @@ Page({
? '《一场Soul的创业实验》全书'
: `章节${sectionId}-${sectionTitle.length > 20 ? sectionTitle.slice(0, 20) + '...' : sectionTitle}`
// 邀请码:谁邀请了我(从落地页 ref 或 storage 带入),会写入订单 referrer_id / referral_code 便于分销与对账
const referralCode = wx.getStorageSync('referral_code') || ''
// 邀请码:与 VIP/钱包一致,优先 storage 落地 ref否则回落本人推荐码便于自购归因
const referralCode = soulBridge.getReferralCodeForPay(app)
const res = await app.request('/api/miniprogram/pay', {
method: 'POST',
data: {

View File

@@ -677,7 +677,7 @@ Page({
wx.navigateTo({ url: '/pages/withdraw-records/withdraw-records' })
},
// 执行提现
// 执行提现success:false 时 app.request 会 reject 并挂 e.response需 catch 里处理 needBindWechat
async doWithdraw(amount) {
wx.showLoading({ title: '提现中...' })
@@ -706,24 +706,23 @@ Page({
// 刷新数据(此时待审核金额会增加,可提现金额会减少)
this.initData()
} else {
if (res.needBind || res.needBindWechat) {
wx.showModal({
title: res.needBindWechat ? '请先绑定微信号' : '需要绑定微信',
content: res.needBindWechat ? '请到「设置」中绑定微信号后再提现,便于到账核对。' : '请先在设置中绑定微信账号后再提现',
confirmText: '去绑定',
success: (modalRes) => {
if (modalRes.confirm) wx.navigateTo({ url: '/pages/settings/settings' })
}
})
} else {
wx.showToast({ title: res.message || res.error || '提现失败', icon: 'none', duration: 3000 })
}
}
} catch (e) {
wx.hideLoading()
console.error('[Referral] 提现失败:', e)
wx.showToast({ title: '提现失败,请重试', icon: 'none' })
const resp = e && e.response
if (resp && (resp.needBind || resp.needBindWechat)) {
wx.showModal({
title: resp.needBindWechat ? '请先绑定微信号' : '需要绑定微信',
content: resp.needBindWechat ? '请到「设置」中绑定微信号后再提现,便于到账核对。' : '请先在设置中绑定微信账号后再提现',
confirmText: '去绑定',
success: (modalRes) => {
if (modalRes.confirm) wx.navigateTo({ url: '/pages/settings/settings' })
}
})
return
}
wx.showToast({ title: (e && e.message) || '提现失败,请重试', icon: 'none' })
}
},