/** * 卡若创业派对 - 我的页面 * 开发: 卡若 * 技术支持: 存客宝 */ const app = getApp() const { formatStatNum } = require('../../utils/util.js') const { trackClick } = require('../../utils/trackClick') const { cleanSingleLineField } = require('../../utils/contentParser.js') const { navigateMpPath } = require('../../utils/mpNavigate.js') const { isSafeImageSrc } = require('../../utils/imageUrl.js') Page({ data: { // 系统信息 statusBarHeight: 44, navBarHeight: 88, // 用户状态 isLoggedIn: false, userInfo: null, /** 我的页头像展示:微信头像或 MBTI 映射图 */ profileAvatarDisplay: '', // 统计数据 totalSections: 62, readCount: 0, referralCount: 0, earnings: '-', pendingEarnings: '-', earningsLoading: true, earningsRefreshing: false, // 阅读统计 totalReadTime: 0, matchHistory: 0, readCountText: '0', totalReadTimeText: '0', matchHistoryText: '0', orderCountText: '0', giftPayCountText: '0', // 最近阅读 recentChapters: [], // 功能配置 matchEnabled: false, referralEnabled: true, auditMode: false, searchEnabled: true, // VIP状态 isVip: false, vipExpireDate: '', // 待确认收款 pendingConfirmList: [], withdrawMchId: '', withdrawAppId: '', pendingConfirmAmount: '0.00', receivingAll: false, // 未登录假资料(展示用) guestNickname: '游客', guestAvatar: '', // 登录弹窗 showLoginModal: false, showPrivacyModal: false, // 修改昵称弹窗 showNicknameModal: false, editingNickname: '', // 手机/微信号弹窗(stitch_soul comprehensive_profile_editor_v1_2) showContactModal: false, contactPhone: '', contactWechat: '', contactSaving: false, pendingWithdraw: false, // 设置入口:开发版、体验版显示 showSettingsEntry: false, // 我的余额 walletBalanceText: '--', // mp_config.mpUi.myPage(后台可改文案/跳转) mpUiCardLabel: '名片', mpUiVipLabelVip: '会员中心', mpUiVipLabelGuest: '成为会员', mpUiReadStatLabel: '已读章节', mpUiRecentTitle: '最近阅读', }, onLoad() { wx.showShareMenu({ withShareTimeline: true }) const accountInfo = wx.getAccountInfoSync ? wx.getAccountInfoSync() : null const envVersion = accountInfo?.miniProgram?.envVersion || '' const showSettingsEntry = envVersion === 'develop' || envVersion === 'trial' this.setData({ statusBarHeight: app.globalData.statusBarHeight, navBarHeight: app.globalData.navBarHeight, showSettingsEntry }) this.loadFeatureConfig() this.initUserStatus() }, onShow() { this.setData({ auditMode: app.globalData.auditMode || false }) // 设置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() this._applyMyMpUiLabels() }, _getMyPageUi() { const cache = app.globalData.configCache || {} const fromNew = cache?.mpConfig?.mpUi?.myPage if (fromNew && typeof fromNew === 'object') return fromNew const fromLegacy = cache?.configs?.mp_config?.mpUi?.myPage if (fromLegacy && typeof fromLegacy === 'object') return fromLegacy return {} }, _applyMyMpUiLabels() { const my = this._getMyPageUi() this.setData({ mpUiCardLabel: String(my.cardLabel || '名片').trim() || '名片', mpUiVipLabelVip: String(my.vipLabelVip || '会员中心').trim() || '会员中心', mpUiVipLabelGuest: String(my.vipLabelGuest || '成为会员').trim() || '成为会员', mpUiReadStatLabel: String(my.readStatLabel || '已读章节').trim() || '已读章节', mpUiRecentTitle: String(my.recentReadTitle || '最近阅读').trim() || '最近阅读' }) }, async _refreshMyAvatarDisplay(safeUser) { if (!safeUser || !app.globalData.isLoggedIn) return try { if (app.loadMbtiAvatarsMap) await app.loadMbtiAvatarsMap() } catch (_) {} const url = app.resolveAvatarWithMbti ? app.resolveAvatarWithMbti(safeUser.avatar, safeUser.mbti) : '' if (!this.data.isLoggedIn) return this.setData({ profileAvatarDisplay: url || '' }) }, async loadFeatureConfig() { try { const res = await app.getConfig() const features = (res && res.features) || (res && res.data && res.data.features) || {} const matchEnabled = features.matchEnabled === true const referralEnabled = features.referralEnabled !== false const searchEnabled = features.searchEnabled !== false const mp = (res && res.mpConfig) || {} app.globalData.auditMode = !!mp.auditMode await app.getAuditMode() const auditMode = app.globalData.auditMode || false app.globalData.features = { matchEnabled, referralEnabled, searchEnabled } this.setData({ matchEnabled, referralEnabled, searchEnabled, auditMode }) this._applyMyMpUiLabels() } catch (error) { console.log('加载功能配置失败:', error) this.setData({ matchEnabled: false, referralEnabled: true, searchEnabled: true }) this._applyMyMpUiLabels() } }, // 初始化用户状态 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 || '' const safeUser = { ...userInfo } if (!isSafeImageSrc(safeUser.avatar)) safeUser.avatar = '' app.globalData.userInfo = safeUser try { wx.setStorageSync('userInfo', safeUser) } catch (_) {} // 先设基础信息;阅读统计与收益再分别从后端刷新 this.setData({ isLoggedIn: true, userInfo: safeUser, profileAvatarDisplay: '', userIdShort, userWechat, readCount: 0, referralCount: 0, earnings: '-', pendingEarnings: '-', earningsLoading: true, recentChapters: [], totalReadTime: 0, matchHistory: 0, readCountText: '0', totalReadTimeText: '0', matchHistoryText: '0' }) this.loadDashboardStats() this.loadMyEarnings() this.loadPendingConfirm() this.loadVipStatus() this.loadWalletBalance() this._refreshMyAvatarDisplay(safeUser) } else { const guestReadCount = app.getReadCount() const guestRecent = this._mergeRecentChaptersFromLocal([]) this.setData({ isLoggedIn: false, userInfo: null, profileAvatarDisplay: '', userIdShort: '', readCount: guestReadCount, readCountText: formatStatNum(guestReadCount), referralCount: 0, earnings: '-', pendingEarnings: '-', earningsLoading: false, recentChapters: guestRecent, totalReadTime: 0, matchHistory: 0, totalReadTimeText: '0', matchHistoryText: '0' }) } }, /** 本地已打开的章节 id(reading_progress 键 + 历史 readSectionIds),用于与服务端合并展示 */ _localSectionIdsFromStorage() { try { const progressData = wx.getStorageSync('reading_progress') || {} const fromProgress = Object.keys(progressData).filter(Boolean) let fromReadList = [] try { const rs = wx.getStorageSync('readSectionIds') if (Array.isArray(rs)) fromReadList = rs.filter(Boolean) } catch (_) {} return [...new Set([...fromProgress, ...fromReadList])] } catch (_) { return [] } }, /** 接口无最近阅读时,用本地 reading_progress + recent_section_opens + bookData 补全 */ _mergeRecentChaptersFromLocal(apiList) { const normalized = Array.isArray(apiList) ? apiList.map((item) => ({ id: item.id, mid: item.mid, title: cleanSingleLineField(item.title || '') || `章节 ${item.id}` })) : [] if (normalized.length > 0) return normalized try { const progressData = wx.getStorageSync('reading_progress') || {} let opens = wx.getStorageSync('recent_section_opens') if (!Array.isArray(opens)) opens = [] const bookFlat = Array.isArray(app.globalData.bookData) ? app.globalData.bookData : [] const titleOf = (id) => { const row = bookFlat.find((s) => s.id === id) return cleanSingleLineField( row?.sectionTitle || row?.section_title || row?.title || row?.chapterTitle || '' ) || `章节 ${id}` } const midOf = (id) => { const row = bookFlat.find((s) => s.id === id) return row?.mid ?? row?.MID ?? 0 } const latest = new Map() const bump = (sid, ts) => { if (!sid) return const id = String(sid) const t = typeof ts === 'number' && !isNaN(ts) ? ts : 0 const prev = latest.get(id) || 0 if (t >= prev) latest.set(id, t) } Object.keys(progressData).forEach((id) => { const row = progressData[id] bump(id, row?.lastOpenAt || row?.last_open_at || 0) }) opens.forEach((o) => bump(o && o.id, o && o.t)) return [...latest.entries()] .sort((a, b) => b[1] - a[1]) .slice(0, 5) .map(([id]) => ({ id, mid: midOf(id), title: titleOf(id) })) } catch (e) { return [] } }, /** 接口失败或无 data 时,仅用本地 reading_progress / readSectionIds 刷新已读与最近 */ _hydrateReadStatsFromLocal() { const localExtra = this._localSectionIdsFromStorage() const readSectionIds = [...new Set(localExtra.filter(Boolean))] app.globalData.readSectionIds = readSectionIds try { wx.setStorageSync('readSectionIds', readSectionIds) } catch (_) {} const recentChapters = this._mergeRecentChaptersFromLocal([]) const readCount = readSectionIds.length this.setData({ readCount, readCountText: formatStatNum(readCount), recentChapters }) }, 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) { this._hydrateReadStatsFromLocal() return } const apiIds = Array.isArray(res.data.readSectionIds) ? res.data.readSectionIds.filter(Boolean) : [] const localExtra = this._localSectionIdsFromStorage() const prevGlobal = Array.isArray(app.globalData.readSectionIds) ? app.globalData.readSectionIds.filter(Boolean) : [] const readSectionIds = [...new Set([...apiIds, ...prevGlobal, ...localExtra])] app.globalData.readSectionIds = readSectionIds wx.setStorageSync('readSectionIds', readSectionIds) const apiRecent = Array.isArray(res.data.recentChapters) ? res.data.recentChapters.map((item) => ({ id: item.id, mid: item.mid, title: item.title || `章节 ${item.id}` })) : [] const recentChapters = this._mergeRecentChaptersFromLocal(apiRecent) const readCount = readSectionIds.length const totalReadTime = Number(res.data.totalReadMinutes || 0) const matchHistory = Number(res.data.matchHistory || 0) const orderCount = Number(res.data.orderCount || 0) const giftPayCount = Number(res.data.giftPayCount || 0) this.setData({ readCount, totalReadTime, matchHistory, readCountText: formatStatNum(readCount), totalReadTimeText: formatStatNum(totalReadTime), matchHistoryText: formatStatNum(matchHistory), orderCountText: formatStatNum(orderCount), giftPayCountText: formatStatNum(giftPayCount), recentChapters }) } catch (e) { console.log('[My] 拉取阅读统计失败:', e && e.message) this._hydrateReadStatsFromLocal() } }, // 拉取待确认收款列表(用于「确认收款」按钮) 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() { trackClick('my', 'btn_click', '一键收款') 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() this.loadMyEarnings() this.loadWalletBalance() } }, // 专用接口:拉取「我的收益」卡片数据(累计、可提现、推荐人数) 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 // 我的收益 = 累计佣金;我的余额 = 可提现金额(兼容 snake_case) const totalCommission = d.totalCommission ?? d.total_commission ?? 0 const availableEarnings = d.availableEarnings ?? d.available_earnings ?? 0 this.setData({ earnings: formatMoney(totalCommission), pendingEarnings: formatMoney(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' }) }, tapAvatar() { if (!this.data.isLoggedIn) { this.showLogin(); return } wx.navigateTo({ url: '/pages/profile-edit/profile-edit' }) }, async onChooseAvatar(e) { const tempAvatarUrl = e.detail?.avatarUrl 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(显示用);保存时只传路径 let avatarUrl = uploadRes.data?.url || uploadRes.url if (avatarUrl && !avatarUrl.startsWith('http')) { avatarUrl = app.globalData.baseUrl + avatarUrl } console.log('[My] 头像上传成功:', avatarUrl) // 3. 更新本地头像 const userInfo = this.data.userInfo userInfo.avatar = avatarUrl this.setData({ userInfo }) this._refreshMyAvatarDisplay(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() { wx.navigateTo({ url: '/pages/profile-show/profile-show' }) }, // 关闭昵称弹窗 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 }) } catch (e) { console.error('[My] showLogin error:', e) this.setData({ showLoginModal: true }) } }, onLoginModalClose() { this.setData({ showLoginModal: false, showPrivacyModal: false }) }, onLoginModalPrivacyAgree() { this.setData({ showPrivacyModal: false }) }, onLoginModalSuccess() { this.initUserStatus() this.setData({ showLoginModal: false }) wx.showToast({ title: '登录成功', icon: 'success' }) }, // 点击菜单 handleMenuTap(e) { const id = e.currentTarget.dataset.id trackClick('my', 'nav_click', id || '菜单') if (!this.data.isLoggedIn) { this.showLogin() return } const routes = { orders: '/pages/purchases/purchases', giftPay: '/pages/gift-pay/list', referral: '/pages/referral/referral', withdrawRecords: '/pages/withdraw-records/withdraw-records', wallet: '/pages/wallet/wallet', settings: '/pages/settings/settings' } if (routes[id]) { wx.navigateTo({ url: routes[id] }) } }, // 跳转到阅读页(优先传 mid,与分享逻辑一致) goToRead(e) { const id = e.currentTarget.dataset.id trackClick('my', 'card_click', id || '章节') const mid = e.currentTarget.dataset.mid const q = mid ? `mid=${mid}` : `id=${id}` wx.navigateTo({ url: `/pages/read/read?${q}` }) }, // 已读章节:进入阅读记录页(有列表);路径可由 mpUi.myPage.readStatPath 配置 goToReadStat() { trackClick('my', 'nav_click', '已读章节') if (!this.data.isLoggedIn) { this.showLogin() return } const p = String(this._getMyPageUi().readStatPath || '').trim() if (p && navigateMpPath(p)) return navigateMpPath('/pages/reading-records/reading-records?focus=all') }, /** 最近阅读区块标题点击:进入阅读记录(最近维度) */ goToRecentReadHub() { trackClick('my', 'nav_click', '最近阅读区块') if (!this.data.isLoggedIn) { this.showLogin() return } const p = String(this._getMyPageUi().recentReadPath || '').trim() if (p && navigateMpPath(p)) return navigateMpPath('/pages/reading-records/reading-records?focus=recent') }, // 去目录(空状态等) goToChapters() { trackClick('my', 'nav_click', '去目录') wx.switchTab({ url: '/pages/chapters/chapters' }) }, // 跳转到匹配 goToMatch() { trackClick('my', 'nav_click', '匹配伙伴') wx.switchTab({ url: '/pages/match/match' }) }, // 跳转到推广中心(需登录) goToReferral(e) { const focus = e && e.currentTarget && e.currentTarget.dataset ? (e.currentTarget.dataset.focus || '') : '' const action = focus === 'bindings' ? '推荐好友' : focus === 'earnings' ? '我的收益' : '推广中心' trackClick('my', 'nav_click', action) if (!this.data.isLoggedIn) { this.showLogin() return } if (!this.data.referralEnabled) return const url = focus ? `/pages/referral/referral?focus=${focus}` : '/pages/referral/referral' wx.navigateTo({ url }) }, // 退出登录 handleLogout() { wx.showModal({ title: '退出登录', content: '确定要退出登录吗?', success: (res) => { if (res.confirm) { app.logout() this.initUserStatus() wx.showToast({ title: '已退出登录', icon: 'success' }) } } }) }, // 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) } }, async loadWalletBalance() { const userId = app.globalData.userInfo?.id if (!userId) return try { const res = await app.request({ url: `/api/miniprogram/balance?userId=${userId}`, silent: true }) if (res?.success && res.data) { const balance = res.data.balance || 0 this.setData({ walletBalanceText: balance.toFixed(2) }) } } catch (e) { console.log('[My] 余额查询失败', e) } }, goToVip() { trackClick('my', 'btn_click', '会员中心') if (!this.data.isLoggedIn) { this.showLogin(); return } const p = String(this._getMyPageUi().vipPath || '').trim() if (p && navigateMpPath(p)) return wx.navigateTo({ url: '/pages/vip/vip' }) }, // 本人对外名片:默认与「超级个体」同款 member-detail;mpUi.myPage.cardPath 可覆盖(需含完整 query) goToMySuperCard() { trackClick('my', 'btn_click', '名片') if (!this.data.isLoggedIn) { this.showLogin(); return } const uid = this.data.userInfo?.id if (!uid) return const p = String(this._getMyPageUi().cardPath || '').trim() if (p && navigateMpPath(p)) return wx.navigateTo({ url: `/pages/member-detail/member-detail?id=${encodeURIComponent(uid)}` }) }, goToProfileEdit() { trackClick('my', 'nav_click', '资料编辑') if (!this.data.isLoggedIn) { this.showLogin(); return } wx.navigateTo({ url: '/pages/profile-edit/profile-edit' }) }, // 进入个人资料展示页(enhanced_professional_profile),展示页内可再进编辑 goToProfileShow() { trackClick('my', 'btn_click', '编辑') if (!this.data.isLoggedIn) { this.showLogin(); return } wx.navigateTo({ url: '/pages/profile-show/profile-show' }) }, 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() this.loadWalletBalance() } catch (e) { wx.hideLoading() wx.showToast({ title: e.message || '提现失败', icon: 'none' }) } } }) }, // 提现/找伙伴前检查联系方式:手机号必填(与 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 || 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 || '', 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().replace(/\s/g, '') const wechat = (this.data.contactWechat || '').trim() 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 }) 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: '卡若创业派对 - 我的', path: ref ? `/pages/my/my?ref=${ref}` : '/pages/my/my' } }, onShareTimeline() { const ref = app.getMyReferralCode() return { title: '卡若创业派对 - 我的', query: ref ? `ref=${ref}` : '' } } })