Merge branch 'yongxu-dev' into devlop

# Conflicts:
#	miniprogram/pages/profile-edit/profile-edit.js
#	miniprogram/pages/profile-edit/profile-edit.wxml
#	miniprogram/pages/settings/settings.js
#	miniprogram/utils/ruleEngine.js
#	soul-admin/src/pages/distribution/DistributionPage.tsx
#	soul-admin/src/pages/users/UsersPage.tsx
#	soul-api/.env.production
#	soul-api/.gitignore
#	soul-api/internal/handler/db_ckb_leads.go
#	soul-api/internal/handler/miniprogram.go
#	soul-api/internal/handler/referral.go
#	开发文档/1、需求/archive/链接人与事-存客宝同步-需求规划.md
#	开发文档/1、需求/archive/链接人与事-实现方案.md
This commit is contained in:
Alex-larget
2026-03-20 14:48:02 +08:00
247 changed files with 8990 additions and 6983 deletions

View File

@@ -1,5 +1,5 @@
/**
* Soul创业派对 - 我的页面
* 卡若创业派对 - 我的页面
* 开发: 卡若
* 技术支持: 存客宝
*/
@@ -60,17 +60,12 @@ Page({
// 登录弹窗
showLoginModal: false,
isLoggingIn: false,
// 用户须主动勾选同意协议(审核要求:不得默认同意)
agreeProtocol: false,
showPrivacyModal: false,
// 修改昵称弹窗
showNicknameModal: false,
editingNickname: '',
// 头像弹窗(含 chooseAvatar 按钮,必须用户点击才可获取微信头像)
showAvatarModal: false,
// 手机/微信号弹窗stitch_soul comprehensive_profile_editor_v1_2
showContactModal: false,
contactPhone: '',
@@ -412,6 +407,8 @@ Page({
wx.hideLoading()
this.setData({ receivingAll: false })
this.loadPendingConfirm()
this.loadMyEarnings()
this.loadWalletBalance()
}
},
@@ -430,9 +427,12 @@ Page({
return
}
const d = res.data
// 我的收益 = 累计佣金;我的余额 = 可提现金额(兼容 snake_case
const totalCommission = d.totalCommission ?? d.total_commission ?? 0
const availableEarnings = d.availableEarnings ?? d.available_earnings ?? 0
this.setData({
earnings: formatMoney(d.totalCommission),
pendingEarnings: formatMoney(d.availableEarnings),
earnings: formatMoney(totalCommission),
pendingEarnings: formatMoney(availableEarnings),
referralCount: d.referralCount ?? this.data.referralCount,
earningsLoading: false,
earningsRefreshing: false
@@ -458,10 +458,9 @@ Page({
wx.showToast({ title: '已刷新', icon: 'success' })
},
// 微信原生获取头像button open-type="chooseAvatar" 回调,真正获取微信头像
// 微信原生获取头像button open-type="chooseAvatar" 回调,点击头像直接唤起选择器
async onChooseAvatar(e) {
const tempAvatarUrl = e.detail?.avatarUrl
this.setData({ showAvatarModal: false })
if (!tempAvatarUrl) return
wx.showLoading({ title: '上传中...', mask: true })
@@ -495,8 +494,11 @@ Page({
})
})
// 2. 获取上传后的完整URL
const avatarUrl = app.globalData.baseUrl + uploadRes.data.url
// 2. 获取上传后的完整URL(显示用);保存时只传路径
let avatarUrl = uploadRes.data?.url || uploadRes.url
if (avatarUrl && !avatarUrl.startsWith('http')) {
avatarUrl = app.globalData.baseUrl + avatarUrl
}
console.log('[My] 头像上传成功:', avatarUrl)
// 3. 更新本地头像
@@ -506,7 +508,7 @@ Page({
app.globalData.userInfo = userInfo
wx.setStorageSync('userInfo', userInfo)
// 4. 同步到服务器数据库
// 4. 同步到服务器数据库(只保存路径,不含域名)
await app.request('/api/miniprogram/user/update', {
method: 'POST',
data: { userId: userInfo.id, avatar: avatarUrl }
@@ -548,12 +550,9 @@ Page({
}
},
// 打开昵称修改弹窗
// 点击昵称跳转资料编辑页type="nickname" 在弹窗内无法触发微信昵称选择器,需在主页面)
editNickname() {
this.setData({
showNicknameModal: true,
editingNickname: this.data.userInfo?.nickname || ''
})
wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
},
// 关闭昵称弹窗
@@ -689,84 +688,23 @@ Page({
console.warn('[My] 检测单页模式失败,回退为正常登录弹窗:', e)
}
try {
this.setData({ showLoginModal: true, agreeProtocol: false })
this.setData({ showLoginModal: true })
} catch (e) {
console.error('[My] showLogin error:', e)
this.setData({ showLoginModal: true })
}
},
// 切换协议勾选(用户主动勾选,非默认同意)
toggleAgree() {
this.setData({ agreeProtocol: !this.data.agreeProtocol })
onLoginModalClose() {
this.setData({ showLoginModal: false, showPrivacyModal: false })
},
// 打开用户协议页(审核要求:点击《用户协议》需有响应)
openUserProtocol() {
wx.navigateTo({ url: '/pages/agreement/agreement' })
onLoginModalPrivacyAgree() {
this.setData({ showPrivacyModal: false })
},
// 打开隐私政策页(审核要求:点击《隐私政策》需有响应)
openPrivacy() {
wx.navigateTo({ url: '/pages/privacy/privacy' })
},
// 关闭登录弹窗
closeLoginModal() {
if (this.data.isLoggingIn) return
onLoginModalSuccess() {
this.initUserStatus()
this.setData({ showLoginModal: false })
},
// 微信登录(须已勾选同意协议,且做好错误处理避免审核报错)
async handleWechatLogin() {
if (!this.data.agreeProtocol) {
wx.showToast({ title: '请先阅读并同意用户协议和隐私政策', icon: 'none' })
return
}
this.setData({ isLoggingIn: true })
try {
const result = await app.login()
if (result) {
this.initUserStatus()
this.setData({ showLoginModal: false, agreeProtocol: false })
wx.showToast({ title: '登录成功', icon: 'success' })
} else {
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
}
} catch (e) {
console.error('[My] 微信登录错误:', e)
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
} finally {
this.setData({ isLoggingIn: false })
}
},
// 手机号登录(需要用户授权)
async handlePhoneLogin(e) {
// 检查是否有授权code
if (!e.detail.code) {
// 用户拒绝授权或获取失败,尝试使用微信登录
console.log('手机号授权失败,尝试微信登录')
return this.handleWechatLogin()
}
this.setData({ isLoggingIn: true })
try {
const result = await app.loginWithPhone(e.detail.code)
if (result) {
this.initUserStatus()
this.setData({ showLoginModal: false })
wx.showToast({ title: '登录成功', icon: 'success' })
} else {
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
}
} catch (e) {
console.error('手机号登录错误:', e)
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
} finally {
this.setData({ isLoggingIn: false })
}
wx.showToast({ title: '登录成功', icon: 'success' })
},
// 点击菜单
@@ -875,62 +813,6 @@ Page({
} catch (e) { console.log('[My] 余额查询失败', e) }
},
// 头像点击:已登录弹出选项(微信头像 / 相册)
onAvatarTap() {
if (!this.data.isLoggedIn) { this.showLogin(); return }
wx.showActionSheet({
itemList: ['获取微信头像', '从相册选择'],
success: (res) => {
if (res.tapIndex === 0) this.setData({ showAvatarModal: true })
if (res.tapIndex === 1) this.chooseAvatarFromAlbum()
}
})
},
closeAvatarModal() {
this.setData({ showAvatarModal: false })
},
// 从相册/相机选择(自定义图片)
chooseAvatarFromAlbum() {
wx.chooseMedia({
count: 1, mediaType: ['image'], sourceType: ['album', 'camera'],
success: async (res) => {
const tempPath = res.tempFiles[0].tempFilePath
wx.showLoading({ title: '上传中...', mask: true })
try {
const uploadRes = await new Promise((resolve, reject) => {
wx.uploadFile({
url: app.globalData.baseUrl + '/api/miniprogram/upload',
filePath: tempPath,
name: 'file',
formData: { folder: 'avatars' },
success: (r) => {
try {
const data = JSON.parse(r.data)
data.success ? resolve(data) : reject(new Error(data.error || '上传失败'))
} catch (e) { reject(new Error('解析失败')) }
},
fail: (e) => reject(e)
})
})
const avatarUrl = app.globalData.baseUrl + uploadRes.data.url
const userInfo = this.data.userInfo
userInfo.avatar = avatarUrl
this.setData({ userInfo })
app.globalData.userInfo = userInfo
wx.setStorageSync('userInfo', userInfo)
await app.request('/api/miniprogram/user/update', { method: 'POST', data: { userId: userInfo.id, avatar: avatarUrl } })
wx.hideLoading()
wx.showToast({ title: '头像已更新', icon: 'success' })
} catch (e) {
wx.hideLoading()
wx.showToast({ title: e.message || '上传失败,请重试', icon: 'none' })
}
}
})
},
goToVip() {
trackClick('my', 'btn_click', '会员中心')
if (!this.data.isLoggedIn) { this.showLogin(); return }
@@ -974,6 +856,7 @@ Page({
wx.hideLoading()
wx.showToast({ title: '提现申请已提交', icon: 'success' })
this.loadMyEarnings()
this.loadWalletBalance()
} catch (e) {
wx.hideLoading()
wx.showToast({ title: e.message || '提现失败', icon: 'none' })
@@ -982,18 +865,19 @@ Page({
})
},
// 提现/找伙伴前检查手机或微信号未填则弹窗stitch_soul
// 提现/找伙伴前检查联系方式:手机号必填(与 profile-edit 规则一致
async ensureContactInfo(callback) {
const userId = app.globalData.userInfo?.id
if (!userId) { callback(); return }
try {
const res = await app.request({ url: `/api/miniprogram/user/profile?userId=${userId}`, silent: true })
const phone = (res?.data?.phone || '').trim()
const wechat = (res?.data?.wechatId || '').trim()
if (phone || wechat) {
const phone = (res?.data?.phone || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
const hasValidPhone = !!phone && /^1[3-9]\d{9}$/.test(phone)
if (hasValidPhone) {
callback()
return
}
const wechat = (res?.data?.wechatId || res?.data?.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
this.setData({
showContactModal: true,
contactPhone: phone || '',
@@ -1015,10 +899,14 @@ Page({
onContactWechatInput(e) { this.setData({ contactWechat: e.detail.value }) },
async saveContactInfo() {
const phone = (this.data.contactPhone || '').trim()
const phone = (this.data.contactPhone || '').trim().replace(/\s/g, '')
const wechat = (this.data.contactWechat || '').trim()
if (!phone && !wechat) {
wx.showToast({ title: '请至少填写手机号或微信号', icon: 'none' })
if (!phone) {
wx.showToast({ title: '请输入手机号(必填)', icon: 'none' })
return
}
if (!/^1[3-9]\d{9}$/.test(phone)) {
wx.showToast({ title: '请输入正确的11位手机号', icon: 'none' })
return
}
this.setData({ contactSaving: true })
@@ -1051,13 +939,13 @@ Page({
onShareAppMessage() {
const ref = app.getMyReferralCode()
return {
title: 'Soul创业派对 - 我的',
title: '卡若创业派对 - 我的',
path: ref ? `/pages/my/my?ref=${ref}` : '/pages/my/my'
}
},
onShareTimeline() {
const ref = app.getMyReferralCode()
return { title: 'Soul创业派对 - 我的', query: ref ? `ref=${ref}` : '' }
return { title: '卡若创业派对 - 我的', query: ref ? `ref=${ref}` : '' }
}
})

View File

@@ -16,20 +16,21 @@
<text wx:else class="guest-avatar-text">{{guestNickname[0] || '游'}}</text>
</view>
<text class="guest-name">{{guestNickname}}</text>
<view class="guest-login-btn" bindtap="showLogin">点击登录</view>
<view class="guest-login-btn" bindtap="showLogin">手机号一键登录</view>
</view>
<!-- 已登录:用户卡片(设计稿布局) -->
<view class="profile-card" wx:else>
<view class="profile-card-inner">
<view class="profile-top-row">
<view class="avatar-wrap" bindtap="onAvatarTap">
<view class="avatar-wrap">
<view class="avatar-inner {{isVip ? 'avatar-vip' : ''}}">
<image wx:if="{{userInfo.avatar}}" class="avatar-img" src="{{userInfo.avatar}}" mode="aspectFill"/>
<text wx:else class="avatar-text">{{userInfo.nickname ? userInfo.nickname[0] : '?'}}</text>
</view>
<view class="vip-badge" wx:if="{{isVip}}">VIP</view>
<view class="vip-badge vip-badge-gray" wx:else>VIP</view>
<button class="avatar-overlay-btn" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar"></button>
</view>
<view class="profile-meta">
<view class="profile-name-row">
@@ -56,7 +57,7 @@
<text class="profile-stat-label">推荐好友</text>
</view>
<view class="profile-stat" wx:if="{{referralEnabled}}" bindtap="goToReferral">
<text class="profile-stat-val">{{earnings === '-' ? '--' : earnings}}</text>
<text class="profile-stat-val">{{pendingEarnings === '-' ? '--' : pendingEarnings}}</text>
<text class="profile-stat-label">我的收益</text>
</view>
<view class="profile-stat" wx:if="{{!auditMode}}" bindtap="handleMenuTap" data-id="wallet">
@@ -87,10 +88,7 @@
</view>
<view class="receive-bottom">
<text class="receive-tip">将依次调起微信收款页完成领取</text>
<view class="receive-link" bindtap="handleMenuTap" data-id="withdrawRecords">
<text>查看提现记录</text>
<icon name="chevron-right" size="24" color="rgba(255,255,255,0.6)" customClass="receive-link-arrow"></icon>
</view>
<text class="receive-link" bindtap="handleMenuTap" data-id="withdrawRecords">查看提现记录 </text>
</view>
</view>
@@ -143,7 +141,7 @@
</view>
<view class="recent-empty" wx:else>
<text class="recent-empty-text">暂无阅读记录</text>
<view class="recent-empty-btn" bindtap="goToChapters">去阅读 <icon name="chevron-right" size="24" color="#00CED1" customClass="recent-empty-arrow"></icon></view>
<view class="recent-empty-btn" bindtap="goToChapters">去阅读 </view>
</view>
</view>
@@ -173,27 +171,16 @@
</view>
</view>
<!-- 登录弹窗 -->
<view class="modal-overlay" wx:if="{{showLoginModal}}" bindtap="closeLoginModal">
<view class="modal-content login-modal-content" catchtap="stopPropagation">
<view class="modal-close" bindtap="closeLoginModal"><icon name="x" size="36" color="#8e8e93"></icon></view>
<view class="login-icon"><icon name="lock" size="80" color="#00CED1"></icon></view>
<text class="login-title">登录 Soul创业派对</text>
<text class="login-desc">登录后可购买章节、解锁更多内容</text>
<button class="btn-wechat {{agreeProtocol ? '' : 'btn-wechat-disabled'}}" bindtap="handleWechatLogin" disabled="{{isLoggingIn || !agreeProtocol}}">
<text class="btn-wechat-icon">微</text>
<text>{{isLoggingIn ? '登录中...' : '微信快捷登录'}}</text>
</button>
<view class="login-modal-cancel" bindtap="closeLoginModal">取消</view>
<view class="login-agree-row" catchtap="toggleAgree">
<view class="agree-checkbox {{agreeProtocol ? 'agree-checked' : ''}}"><icon wx:if="{{agreeProtocol}}" name="check" size="24" color="#34C759"></icon></view>
<text class="agree-text">我已阅读并同意</text>
<text class="agree-link" catchtap="openUserProtocol">《用户协议》</text>
<text class="agree-text">和</text>
<text class="agree-link" catchtap="openPrivacy">《隐私政策》</text>
</view>
</view>
</view>
<!-- 登录弹窗(公用组件) -->
<login-modal
show="{{showLoginModal}}"
desc="登录后可购买章节、解锁更多内容"
showPrivacyModal="{{showPrivacyModal}}"
showCancel="{{true}}"
bind:close="onLoginModalClose"
bind:success="onLoginModalSuccess"
bind:privacyagree="onLoginModalPrivacyAgree"
/>
<!-- 手机/微信号弹窗 -->
<view class="modal-overlay contact-modal-overlay" wx:if="{{showContactModal}}" bindtap="closeContactModal">
@@ -219,17 +206,6 @@
</view>
</view>
<!-- 头像弹窗:必须点击 button 才能获取微信头像(隐私规范) -->
<view class="modal-overlay" wx:if="{{showAvatarModal}}" bindtap="closeAvatarModal">
<view class="modal-content avatar-modal" catchtap="stopPropagation">
<view class="modal-close" bindtap="closeAvatarModal"><icon name="x" size="36" color="#8e8e93"></icon></view>
<text class="avatar-modal-title">获取微信头像</text>
<text class="avatar-modal-desc">点击下方按钮使用你的微信头像</text>
<button class="btn-choose-avatar" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">使用微信头像</button>
<view class="avatar-modal-cancel" bindtap="closeAvatarModal">取消</view>
</view>
</view>
<!-- 修改昵称弹窗 -->
<view class="modal-overlay" wx:if="{{showNicknameModal}}" bindtap="closeNicknameModal">
<view class="modal-content nickname-modal" catchtap="stopPropagation">

View File

@@ -41,7 +41,22 @@
border: 1rpx solid rgba(75,85,99,0.5);
}
.profile-top-row { display: flex; align-items: flex-start; gap: 32rpx; }
.avatar-wrap { position: relative; flex-shrink: 0; }
/* 头像区域view 负责展示button 绝对定位覆盖其上,避免原生样式影响 */
.avatar-wrap {
position: relative;
width: 130rpx; height: 130rpx;
flex-shrink: 0;
}
/* 绝对定位的按钮覆盖在头像上,透明无样式,点击唤起微信选择器(微信头像/相册/拍照) */
.avatar-overlay-btn {
position: absolute;
left: 0; top: 0;
width: 130rpx; height: 130rpx;
padding: 0; margin: 0;
background: transparent; border: none;
display: block;
}
.avatar-overlay-btn::after { border: none; }
.avatar-inner {
width: 130rpx; height: 130rpx; border-radius: 50%; overflow: hidden;
background: #1C2524; border: 5rpx solid #374151;
@@ -208,6 +223,10 @@
.agree-text { color: rgba(255,255,255,0.6); }
.agree-link { color: #4FD1C5; text-decoration: underline; padding: 0 4rpx; }
.btn-wechat-disabled { opacity: 0.6; }
.privacy-wechat-row { margin: 24rpx 0; padding: 24rpx; background: rgba(0,206,209,0.1); border-radius: 16rpx; }
.privacy-wechat-desc { display: block; font-size: 26rpx; color: rgba(255,255,255,0.8); margin-bottom: 16rpx; }
.privacy-agree-btn { width: 100%; padding: 20rpx; background: #07C160; color: #fff; font-size: 28rpx; border-radius: 16rpx; border: none; }
.privacy-agree-btn::after { border: none; }
/* 头像弹窗 */
.avatar-modal .avatar-modal-title { display: block; font-size: 36rpx; font-weight: bold; color: #fff; text-align: center; margin-bottom: 16rpx; }