Files
soul-yongping/miniprogram/pages/my/my.js
卡若 708547d0dd feat: 数据概览简化 + 用户管理增加余额/提现列
- 数据概览:去掉代付统计独立卡片,总收入中以小标签显示代付金额
- 数据概览:移除余额统计区块(余额改在用户管理中展示)
- 数据概览:恢复转化率卡片(唯一付费用户/总用户)
- 用户管理:用户列表新增「余额/提现」列,显示钱包余额和已提现金额
- 后端:DBUsersList 增加 user_balances 查询,返回 walletBalance 字段
- 后端:User model 添加 WalletBalance 非数据库字段
- 包含之前的小程序埋点和管理后台点击统计面板

Made-with: Cursor
2026-03-15 15:57:09 +08:00

1033 lines
32 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Soul创业派对 - 我的页面
* 开发: 卡若
* 技术支持: 存客宝
*/
const app = getApp()
const { trackClick } = require('../../utils/trackClick')
const { checkAndExecute } = require('../../utils/ruleEngine')
Page({
data: {
// 系统信息
statusBarHeight: 44,
navBarHeight: 88,
// 用户状态
isLoggedIn: false,
userInfo: null,
// 统计数据
totalSections: 62,
readCount: 0,
referralCount: 0,
earnings: '-',
pendingEarnings: '-',
earningsLoading: true,
earningsRefreshing: false,
// 阅读统计
totalReadTime: 0,
matchHistory: 0,
// 最近阅读
recentChapters: [],
// 功能配置
matchEnabled: false,
// VIP状态
isVip: false,
vipExpireDate: '',
// 待确认收款
pendingConfirmList: [],
withdrawMchId: '',
withdrawAppId: '',
pendingConfirmAmount: '0.00',
receivingAll: false,
// 未登录假资料(展示用)
guestNickname: '游客',
guestAvatar: '',
// 登录弹窗
showLoginModal: false,
isLoggingIn: false,
// 用户须主动勾选同意协议(审核要求:不得默认同意)
agreeProtocol: false,
// 修改昵称弹窗
showNicknameModal: false,
editingNickname: '',
// 头像弹窗(含 chooseAvatar 按钮,必须用户点击才可获取微信头像)
showAvatarModal: false,
// 手机/微信号弹窗stitch_soul comprehensive_profile_editor_v1_2
showContactModal: false,
contactPhone: '',
contactWechat: '',
contactSaving: false,
pendingWithdraw: false,
// 我的余额wallet 页入口展示)
walletBalance: 0,
},
onLoad() {
wx.showShareMenu({ withShareTimeline: true })
this.setData({
statusBarHeight: app.globalData.statusBarHeight,
navBarHeight: app.globalData.navBarHeight
})
this.loadFeatureConfig()
this.initUserStatus()
// 规则引擎:登录后检查(填头像等)
checkAndExecute('after_login', this)
},
onShow() {
// 设置TabBar选中状态根据 matchEnabled 动态设置)
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
const tabBar = this.getTabBar()
if (tabBar.updateSelected) {
tabBar.updateSelected()
} else {
const selected = tabBar.data.matchEnabled ? 3 : 2
tabBar.setData({ selected })
}
}
this.initUserStatus()
},
async loadFeatureConfig() {
try {
const res = await app.request('/api/miniprogram/config')
const features = (res && res.features) || (res && res.data && res.data.features) || {}
this.setData({ matchEnabled: features.matchEnabled === true })
} catch (error) {
console.log('加载功能配置失败:', error)
this.setData({ matchEnabled: false })
}
},
// 初始化用户状态
initUserStatus() {
const { isLoggedIn, userInfo } = app.globalData
if (isLoggedIn && userInfo) {
const userId = userInfo.id || ''
const userIdShort = userId.length > 20 ? userId.slice(0, 10) + '...' + userId.slice(-6) : userId
const userWechat = wx.getStorageSync('user_wechat') || userInfo.wechat || ''
// 先设基础信息;阅读统计与收益再分别从后端刷新
this.setData({
isLoggedIn: true,
userInfo,
userIdShort,
userWechat,
readCount: 0,
referralCount: userInfo.referralCount || 0,
earnings: '-',
pendingEarnings: '-',
earningsLoading: true,
recentChapters: [],
totalReadTime: 0,
matchHistory: 0
})
this.loadDashboardStats()
this.loadMyEarnings()
this.loadPendingConfirm()
this.loadVipStatus()
this.loadWalletBalance()
} else {
this.setData({
isLoggedIn: false,
userInfo: null,
userIdShort: '',
readCount: app.getReadCount(),
referralCount: 0,
earnings: '-',
pendingEarnings: '-',
earningsLoading: false,
recentChapters: [],
totalReadTime: 0,
matchHistory: 0
})
}
},
async loadDashboardStats() {
const userId = app.globalData.userInfo?.id
if (!userId) return
try {
const res = await app.request({
url: `/api/miniprogram/user/dashboard-stats?userId=${encodeURIComponent(userId)}`,
silent: true
})
if (!res?.success || !res.data) return
const readSectionIds = Array.isArray(res.data.readSectionIds) ? res.data.readSectionIds : []
app.globalData.readSectionIds = readSectionIds
wx.setStorageSync('readSectionIds', readSectionIds)
const recentChapters = Array.isArray(res.data.recentChapters)
? res.data.recentChapters.map((item) => ({
id: item.id,
mid: item.mid || app.getSectionMid(item.id),
title: item.title || `章节 ${item.id}`
}))
: []
this.setData({
readCount: Number(res.data.readCount || 0),
totalReadTime: Number(res.data.totalReadMinutes || 0),
matchHistory: Number(res.data.matchHistory || 0),
recentChapters
})
} catch (e) {
console.log('[My] 拉取阅读统计失败:', e && e.message)
}
},
// 拉取待确认收款列表(用于「确认收款」按钮)
async loadPendingConfirm() {
const userInfo = app.globalData.userInfo
if (!app.globalData.isLoggedIn || !userInfo || !userInfo.id) return
try {
const res = await app.request({ url: '/api/miniprogram/withdraw/pending-confirm?userId=' + userInfo.id, silent: true })
if (res && res.success && res.data) {
const list = (res.data.list || []).map(item => ({
id: item.id,
amount: (item.amount || 0).toFixed(2),
package: item.package,
createdAt: (item.createdAt ?? item.created_at) ? this.formatDateMy(item.createdAt ?? item.created_at) : '--'
}))
const total = list.reduce((sum, it) => sum + (parseFloat(it.amount) || 0), 0)
this.setData({
pendingConfirmList: list,
withdrawMchId: res.data.mchId ?? res.data.mch_id ?? '',
withdrawAppId: res.data.appId ?? res.data.app_id ?? '',
pendingConfirmAmount: total.toFixed(2)
})
} else {
this.setData({ pendingConfirmList: [], withdrawMchId: '', withdrawAppId: '', pendingConfirmAmount: '0.00' })
}
} catch (e) {
this.setData({ pendingConfirmList: [], pendingConfirmAmount: '0.00' })
}
},
formatDateMy(dateStr) {
if (!dateStr) return '--'
const d = new Date(dateStr)
const m = (d.getMonth() + 1).toString().padStart(2, '0')
const day = d.getDate().toString().padStart(2, '0')
return `${m}-${day}`
},
// 确认收款:有 package 时调起微信收款页,成功后记录;无 package 时仅调用后端记录「已确认收款」
async confirmReceive(e) {
const index = e.currentTarget.dataset.index
const id = e.currentTarget.dataset.id
const list = this.data.pendingConfirmList || []
let item = (typeof index === 'number' || (index !== undefined && index !== '')) ? list[index] : null
if (!item && id) item = list.find(x => x.id === id) || null
if (!item) {
wx.showToast({ title: '请稍后刷新再试', icon: 'none' })
return
}
const mchId = this.data.withdrawMchId
const appId = this.data.withdrawAppId
const hasPackage = item.package && mchId && appId && wx.canIUse('requestMerchantTransfer')
const recordConfirmReceived = async () => {
const userInfo = app.globalData.userInfo
if (userInfo && userInfo.id) {
try {
await app.request({
url: '/api/miniprogram/withdraw/confirm-received',
method: 'POST',
data: { withdrawalId: item.id, userId: userInfo.id }
})
} catch (e) { /* 仅记录,不影响前端展示 */ }
}
const newList = list.filter(x => x.id !== item.id)
this.setData({ pendingConfirmList: newList })
this.loadPendingConfirm()
}
if (hasPackage) {
wx.showLoading({ title: '调起收款...', mask: true })
wx.requestMerchantTransfer({
mchId,
appId,
package: item.package,
success: async () => {
wx.hideLoading()
wx.showToast({ title: '收款成功', icon: 'success' })
await recordConfirmReceived()
},
fail: (err) => {
wx.hideLoading()
const msg = (err.errMsg || '').includes('cancel') ? '已取消' : (err.errMsg || '收款失败')
wx.showToast({ title: msg, icon: 'none' })
},
complete: () => { wx.hideLoading() }
})
return
}
// 无 package 时仅记录「确认已收款」(当前直接打款无 package用户点按钮即记录
wx.showLoading({ title: '提交中...', mask: true })
try {
await recordConfirmReceived()
wx.hideLoading()
wx.showToast({ title: '已记录确认收款', icon: 'success' })
} catch (e) {
wx.hideLoading()
wx.showToast({ title: (e && e.message) || '操作失败', icon: 'none' })
}
},
// 一键收款:逐条调起微信收款页(有上一页则返回,无则回首页)
async handleOneClickReceive() {
if (!this.data.isLoggedIn) { this.showLogin(); return }
if (this.data.receivingAll) return
const list = this.data.pendingConfirmList || []
if (list.length === 0) {
wx.showToast({ title: '暂无待收款', icon: 'none' })
return
}
if (!wx.canIUse('requestMerchantTransfer')) {
wx.showToast({ title: '当前微信版本过低,请更新后重试', icon: 'none' })
return
}
const mchIdDefault = this.data.withdrawMchId || ''
const appIdDefault = this.data.withdrawAppId || ''
this.setData({ receivingAll: true })
try {
for (let i = 0; i < list.length; i++) {
const item = list[i]
wx.showLoading({ title: `收款中 ${i + 1}/${list.length}`, mask: true })
// 兜底:每次收款前取最新 confirm-info避免 package 不完整或过期
let mchId = mchIdDefault
let appId = appIdDefault
let pkg = item.package
try {
const infoRes = await app.request({
url: '/api/miniprogram/withdraw/confirm-info?id=' + encodeURIComponent(item.id),
silent: true
})
if (infoRes && infoRes.success && infoRes.data) {
mchId = infoRes.data.mchId || mchId
appId = infoRes.data.appId || appId
pkg = infoRes.data.package || pkg
}
} catch (e) { /* confirm-info 失败不阻断,使用列表字段兜底 */ }
if (!pkg) {
wx.hideLoading()
wx.showModal({
title: '提示',
content: '当前订单无法调起收款页,请稍后在「提现记录」中点击“领取零钱”。',
confirmText: '去查看',
cancelText: '知道了',
success: (r) => {
if (r.confirm) wx.navigateTo({ url: '/pages/withdraw-records/withdraw-records' })
}
})
break
}
// requestMerchantTransfer失败/取消会走 fail
await new Promise((resolve, reject) => {
wx.requestMerchantTransfer({
mchId,
appId: appId || wx.getAccountInfoSync().miniProgram.appId,
package: pkg,
success: resolve,
fail: reject
})
})
// 收款页调起成功后记录确认(后端负责状态流转)
const userInfo = app.globalData.userInfo
if (userInfo && userInfo.id) {
try {
await app.request({
url: '/api/miniprogram/withdraw/confirm-received',
method: 'POST',
data: { withdrawalId: item.id, userId: userInfo.id }
})
} catch (e) { /* 仅记录,不影响前端 */ }
}
}
} catch (err) {
const msg = (err && err.errMsg && String(err.errMsg).includes('cancel')) ? '已取消收款' : '收款失败,请重试'
wx.showToast({ title: msg, icon: 'none' })
} finally {
wx.hideLoading()
this.setData({ receivingAll: false })
this.loadPendingConfirm()
}
},
// 专用接口:拉取「我的收益」卡片数据(累计、可提现、推荐人数)
async loadMyEarnings() {
const userInfo = app.globalData.userInfo
if (!app.globalData.isLoggedIn || !userInfo || !userInfo.id) {
this.setData({ earningsLoading: false })
return
}
const formatMoney = (num) => (typeof num === 'number' ? num.toFixed(2) : '0.00')
try {
const res = await app.request({ url: '/api/miniprogram/earnings?userId=' + userInfo.id, silent: true })
if (!res || !res.success || !res.data) {
this.setData({ earningsLoading: false, earnings: '0.00', pendingEarnings: '0.00' })
return
}
const d = res.data
this.setData({
earnings: formatMoney(d.totalCommission),
pendingEarnings: formatMoney(d.availableEarnings),
referralCount: d.referralCount ?? this.data.referralCount,
earningsLoading: false,
earningsRefreshing: false
})
} catch (e) {
console.log('[My] 拉取我的收益失败:', e && e.message)
this.setData({
earningsLoading: false,
earningsRefreshing: false,
earnings: '0.00',
pendingEarnings: '0.00'
})
}
},
// 点击刷新图标:刷新我的收益
async refreshEarnings() {
if (!this.data.isLoggedIn) return
if (this.data.earningsRefreshing) return
this.setData({ earningsRefreshing: true })
wx.showToast({ title: '刷新中...', icon: 'loading', duration: 2000 })
await this.loadMyEarnings()
wx.showToast({ title: '已刷新', icon: 'success' })
},
// 微信原生获取头像button open-type="chooseAvatar" 回调,真正获取微信头像)
async onChooseAvatar(e) {
const tempAvatarUrl = e.detail?.avatarUrl
this.setData({ showAvatarModal: false })
if (!tempAvatarUrl) return
wx.showLoading({ title: '上传中...', mask: true })
try {
// 1. 先上传图片到服务器
console.log('[My] 开始上传头像:', tempAvatarUrl)
const uploadRes = await new Promise((resolve, reject) => {
wx.uploadFile({
url: app.globalData.baseUrl + '/api/miniprogram/upload',
filePath: tempAvatarUrl,
name: 'file',
formData: {
folder: 'avatars'
},
success: (res) => {
try {
const data = JSON.parse(res.data)
if (data.success) {
resolve(data)
} else {
reject(new Error(data.error || '上传失败'))
}
} catch (err) {
reject(new Error('解析响应失败'))
}
},
fail: (err) => {
reject(err)
}
})
})
// 2. 获取上传后的完整URL
const avatarUrl = app.globalData.baseUrl + uploadRes.data.url
console.log('[My] 头像上传成功:', avatarUrl)
// 3. 更新本地头像
const userInfo = this.data.userInfo
userInfo.avatar = avatarUrl
this.setData({ userInfo })
app.globalData.userInfo = userInfo
wx.setStorageSync('userInfo', userInfo)
// 4. 同步到服务器数据库
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()
console.error('[My] 上传头像失败:', e)
wx.showToast({
title: e.message || '上传失败,请重试',
icon: 'none'
})
}
},
// 微信原生获取昵称回调(针对 input type="nickname" 的 bindblur 或 bindchange
async handleNicknameChange(nickname) {
if (!nickname || nickname === this.data.userInfo?.nickname) return
try {
const userInfo = this.data.userInfo
userInfo.nickname = nickname
this.setData({ userInfo })
app.globalData.userInfo = userInfo
wx.setStorageSync('userInfo', userInfo)
// 同步到服务器
await app.request('/api/miniprogram/user/update', {
method: 'POST',
data: { userId: userInfo.id, nickname }
})
wx.showToast({ title: '昵称已更新', icon: 'success' })
} catch (e) {
console.error('[My] 同步昵称失败:', e)
}
},
// 打开昵称修改弹窗
editNickname() {
this.setData({
showNicknameModal: true,
editingNickname: this.data.userInfo?.nickname || ''
})
},
// 关闭昵称弹窗
closeNicknameModal() {
this.setData({
showNicknameModal: false,
editingNickname: ''
})
},
// 阻止事件冒泡
stopPropagation() {},
// 昵称输入实时更新
onNicknameInput(e) {
this.setData({
editingNickname: e.detail.value
})
},
// 昵称变化(微信自动填充时触发)
onNicknameChange(e) {
const nickname = e.detail.value
console.log('[My] 昵称已自动填充:', nickname)
this.setData({
editingNickname: nickname
})
// 自动填充时也尝试直接同步
this.handleNicknameChange(nickname)
},
// 确认修改昵称
async confirmNickname() {
const newNickname = this.data.editingNickname.trim()
if (!newNickname) {
wx.showToast({ title: '昵称不能为空', icon: 'none' })
return
}
if (newNickname.length < 1 || newNickname.length > 20) {
wx.showToast({ title: '昵称1-20个字符', icon: 'none' })
return
}
// 关闭弹窗
this.closeNicknameModal()
// 显示加载
wx.showLoading({ title: '更新中...', mask: true })
try {
// 1. 同步到服务器
const res = await app.request('/api/miniprogram/user/update', {
method: 'POST',
data: {
userId: this.data.userInfo.id,
nickname: newNickname
}
})
if (res && res.success) {
// 2. 更新本地状态
const userInfo = this.data.userInfo
userInfo.nickname = newNickname
this.setData({ userInfo })
// 3. 更新全局和缓存
app.globalData.userInfo = userInfo
wx.setStorageSync('userInfo', userInfo)
wx.hideLoading()
wx.showToast({ title: '昵称已修改', icon: 'success' })
} else {
throw new Error(res?.message || '更新失败')
}
} catch (e) {
wx.hideLoading()
console.error('[My] 修改昵称失败:', e)
wx.showToast({ title: '修改失败,请重试', icon: 'none' })
}
},
// 复制联系方式优先复制微信号其次复制用户ID
copyUserId() {
const userWechat = (this.data.userWechat || '').trim()
if (userWechat) {
wx.setClipboardData({
data: userWechat,
success: () => {
wx.showToast({ title: '微信号已复制', icon: 'success' })
}
})
return
}
const userId = this.data.userInfo?.id || ''
if (!userId) {
wx.showToast({ title: '暂无ID', icon: 'none' })
return
}
wx.setClipboardData({
data: userId,
success: () => {
wx.showToast({ title: 'ID已复制', icon: 'success' })
}
})
},
// 切换Tab
switchTab(e) {
const tab = e.currentTarget.dataset.tab
this.setData({ activeTab: tab })
},
// 显示登录弹窗(每次打开时协议未勾选,符合审核要求)
showLogin() {
trackClick('my', 'btn_click', '登录')
// 朋友圈等单页模式下,不直接弹登录,用官方推荐的方式引导用户「前往小程序」
try {
const sys = wx.getSystemInfoSync()
const isSinglePage = (sys && sys.mode === 'singlePage') || getApp().globalData.isSinglePageMode
if (isSinglePage) {
wx.showModal({
title: '请前往完整小程序',
content: '当前为朋友圈单页,仅支持部分体验。想登录并管理账户,请点击底部「前往小程序」后再操作。',
showCancel: false,
confirmText: '我知道了',
})
return
}
} catch (e) {
console.warn('[My] 检测单页模式失败,回退为正常登录弹窗:', e)
}
try {
this.setData({ showLoginModal: true, agreeProtocol: false })
} catch (e) {
console.error('[My] showLogin error:', e)
this.setData({ showLoginModal: true })
}
},
// 切换协议勾选(用户主动勾选,非默认同意)
toggleAgree() {
this.setData({ agreeProtocol: !this.data.agreeProtocol })
},
// 打开用户协议页(审核要求:点击《用户协议》需有响应)
openUserProtocol() {
wx.navigateTo({ url: '/pages/agreement/agreement' })
},
// 打开隐私政策页(审核要求:点击《隐私政策》需有响应)
openPrivacy() {
wx.navigateTo({ url: '/pages/privacy/privacy' })
},
// 关闭登录弹窗
closeLoginModal() {
if (this.data.isLoggingIn) return
this.setData({ showLoginModal: false })
},
// 微信登录(须已勾选同意协议,且做好错误处理避免审核报错)
async handleWechatLogin() {
trackClick('my', 'btn_click', '微信登录')
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 })
}
},
// 点击菜单
handleMenuTap(e) {
trackClick('my', 'btn_click', e.currentTarget.dataset.id || '菜单')
const id = e.currentTarget.dataset.id
if (!this.data.isLoggedIn && id !== 'about') {
this.showLogin()
return
}
const routes = {
wallet: '/pages/wallet/wallet',
orders: '/pages/purchases/purchases',
referral: '/pages/referral/referral',
withdrawRecords: '/pages/withdraw-records/withdraw-records',
about: '/pages/about/about',
settings: '/pages/settings/settings'
}
if (routes[id]) {
wx.navigateTo({ url: routes[id] })
}
},
// 跳转到阅读页(优先传 mid与分享逻辑一致
goToRead(e) {
const id = e.currentTarget.dataset.id
const mid = e.currentTarget.dataset.mid || app.getSectionMid(id)
const q = mid ? `mid=${mid}` : `id=${id}`
wx.navigateTo({ url: `/pages/read/read?${q}` })
},
// 跳转到目录
goToChapters() {
trackClick('my', 'nav_click', '目录')
wx.switchTab({ url: '/pages/chapters/chapters' })
},
// 跳转到关于页
goToAbout() {
wx.navigateTo({ url: '/pages/about/about' })
},
// 跳转到匹配
goToMatch() {
trackClick('my', 'nav_click', '匹配')
wx.switchTab({ url: '/pages/match/match' })
},
// 跳转到推广中心(需登录)
goToReferral() {
trackClick('my', 'nav_click', '推广')
if (!this.data.isLoggedIn) {
this.showLogin()
return
}
wx.navigateTo({ url: '/pages/referral/referral' })
},
// 跳转到找伙伴
goToMatch() {
trackClick('my', 'nav_click', '匹配')
wx.switchTab({ url: '/pages/match/match' })
},
// 退出登录
handleLogout() {
wx.showModal({
title: '退出登录',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
app.logout()
this.initUserStatus()
wx.showToast({ title: '已退出登录', icon: 'success' })
}
}
})
},
async loadWalletBalance() {
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) return
const userId = app.globalData.userInfo.id
try {
const res = await app.request({ url: `/api/miniprogram/balance?userId=${userId}`, silent: true })
if (res && res.data) {
this.setData({ walletBalance: (res.data.balance || 0).toFixed(2) })
}
} catch (e) {}
},
// VIP状态查询注意hasFullBook=9.9 买断,不等同 VIP
async loadVipStatus() {
const userId = app.globalData.userInfo?.id
if (!userId) return
try {
const res = await app.request({ url: `/api/miniprogram/vip/status?userId=${userId}`, silent: true })
if (res?.success) {
const isVip = !!res.data?.isVip
app.globalData.isVip = isVip
app.globalData.vipExpireDate = res.data?.expireDate || ''
this.setData({
isVip,
vipExpireDate: res.data?.expireDate || this.data.vipExpireDate || ''
})
// 同步到 storage便于其他页面复用注意hasFullBook=买断isVip=会员)
const userInfo = app.globalData.userInfo || {}
userInfo.isVip = isVip
userInfo.vipExpireDate = res.data?.expireDate || ''
wx.setStorageSync('userInfo', userInfo)
}
} catch (e) { console.log('[My] VIP查询失败', 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', 'nav_click', 'VIP')
if (!this.data.isLoggedIn) { this.showLogin(); return }
wx.navigateTo({ url: '/pages/vip/vip' })
},
// 进入个人资料编辑页stitch_soul
goToProfileEdit() {
trackClick('my', 'nav_click', '设置')
if (!this.data.isLoggedIn) { this.showLogin(); return }
wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
},
async handleWithdraw() {
if (!this.data.isLoggedIn) { this.showLogin(); return }
const amount = parseFloat(this.data.pendingEarnings)
if (isNaN(amount) || amount <= 0) {
wx.showToast({ title: '暂无可提现金额', icon: 'none' })
return
}
await this.ensureContactInfo(() => this.doWithdraw(amount))
},
async doWithdraw(amount) {
wx.showModal({
title: '申请提现',
content: `确认提现 ¥${amount.toFixed(2)} `,
success: async (res) => {
if (!res.confirm) return
wx.showLoading({ title: '提交中...', mask: true })
try {
const userId = app.globalData.userInfo?.id
await app.request({ url: '/api/miniprogram/withdraw', method: 'POST', data: { userId, amount } })
wx.hideLoading()
wx.showToast({ title: '提现申请已提交', icon: 'success' })
this.loadMyEarnings()
} catch (e) {
wx.hideLoading()
wx.showToast({ title: e.message || '提现失败', icon: 'none' })
}
}
})
},
// 提现/找伙伴前检查手机或微信号未填则弹窗stitch_soul
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) {
callback()
return
}
this.setData({
showContactModal: true,
contactPhone: phone || '',
contactWechat: wechat || '',
pendingWithdraw: true,
})
this._contactCallback = callback
} catch (e) {
callback()
}
},
closeContactModal() {
this.setData({ showContactModal: false, pendingWithdraw: false })
this._contactCallback = null
},
onContactPhoneInput(e) { this.setData({ contactPhone: e.detail.value }) },
onContactWechatInput(e) { this.setData({ contactWechat: e.detail.value }) },
async saveContactInfo() {
const phone = (this.data.contactPhone || '').trim()
const wechat = (this.data.contactWechat || '').trim()
if (!phone && !wechat) {
wx.showToast({ title: '请至少填写手机号或微信号', icon: 'none' })
return
}
this.setData({ contactSaving: true })
try {
await app.request({
url: '/api/miniprogram/user/profile',
method: 'POST',
data: {
userId: app.globalData.userInfo?.id,
phone: phone || undefined,
wechatId: wechat || undefined,
},
})
if (phone) wx.setStorageSync('user_phone', phone)
if (wechat) wx.setStorageSync('user_wechat', wechat)
this.closeContactModal()
wx.showToast({ title: '已保存', icon: 'success' })
const cb = this._contactCallback
this._contactCallback = null
if (cb) cb()
} catch (e) {
wx.showToast({ title: e.message || '保存失败', icon: 'none' })
}
this.setData({ contactSaving: false })
},
// 阻止冒泡
stopPropagation() {},
onShareAppMessage() {
const ref = app.getMyReferralCode()
return {
title: 'Soul创业派对 - 我的',
path: ref ? `/pages/my/my?ref=${ref}` : '/pages/my/my'
}
},
onShareTimeline() {
const ref = app.getMyReferralCode()
return { title: 'Soul创业派对 - 我的', query: ref ? `ref=${ref}` : '' }
}
})