- + + + + 超级个体 + + 查看全部 + + + + + + + + {{item.name[0] || '会'}} + + {{item.name}} + + + + 成为会员,展示你的项目 + 加入创业派对 → + + + @@ -95,12 +125,10 @@ wx:key="id" bindtap="goToRead" data-id="{{item.id}}" - data-mid="{{item.mid}}" > {{item.id}} - {{item.tag}} {{item.title}} {{item.part}} @@ -110,35 +138,26 @@ - - - 内容概览 - - - - {{item.number}} - - - {{item.title}} - {{item.subtitle}} - - + + + + 最新新增 + + +{{latestChapters.length}} - - - - - - 序言 - 为什么我每天早上6点在Soul开播? + + + + NEW + {{item.title}} + + + ¥{{item.price}} + {{item.dateStr}} + + - 免费 diff --git a/miniprogram/pages/index/index.wxss b/miniprogram/pages/index/index.wxss index c8ba095b..b3d68715 100644 --- a/miniprogram/pages/index/index.wxss +++ b/miniprogram/pages/index/index.wxss @@ -498,78 +498,135 @@ color: rgba(255, 255, 255, 0.6); } -/* ===== 创业老板排行 ===== */ -.members-grid { - display: flex; - flex-wrap: wrap; - gap: 20rpx; - padding: 0 8rpx; +/* ===== 超级个体 ===== */ +.super-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 24rpx 16rpx; } -.member-cell { - width: calc(25% - 15rpx); +.super-item { display: flex; flex-direction: column; align-items: center; - padding: 16rpx 0; + gap: 10rpx; } -.member-avatar-wrap { - position: relative; - width: 100rpx; - height: 100rpx; - margin-bottom: 10rpx; -} -.member-avatar { - width: 100rpx; - height: 100rpx; +.super-avatar { + width: 108rpx; + height: 108rpx; border-radius: 50%; - border: 3rpx solid #FFD700; -} -.member-avatar-placeholder { - width: 100rpx; - height: 100rpx; - border-radius: 50%; - background: linear-gradient(135deg, #1c1c1e, #2c2c2e); - border: 3rpx solid #FFD700; + overflow: hidden; + background: rgba(0,206,209,0.1); display: flex; align-items: center; justify-content: center; - font-size: 36rpx; + border: 3rpx solid rgba(255,255,255,0.1); +} +.super-avatar-vip { + border: 3rpx solid #FFD700; + box-shadow: 0 0 12rpx rgba(255,215,0,0.3); +} +.super-avatar-img { + width: 100%; + height: 100%; + object-fit: cover; +} +.super-avatar-text { + font-size: 40rpx; + font-weight: 600; + color: #00CED1; +} +.super-name { + font-size: 22rpx; + color: rgba(255,255,255,0.7); + text-align: center; + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.super-empty { + padding: 32rpx; + text-align: center; + background: rgba(255,255,255,0.03); + border-radius: 16rpx; +} +.super-empty-text { + font-size: 24rpx; + color: rgba(255,255,255,0.4); + display: block; + margin-bottom: 16rpx; +} +.super-empty-btn { + font-size: 26rpx; + color: #00CED1; +} + +/* ===== 最新新增 ===== */ +.daily-badge-wrap { + display: inline-flex; + align-items: center; +} +.daily-badge { + background: #FF4500; + color: #fff; + font-size: 20rpx; + font-weight: 600; + padding: 4rpx 12rpx; + border-radius: 16rpx; + margin-left: 8rpx; +} +.latest-list { + display: flex; + flex-direction: column; + gap: 12rpx; +} +.latest-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20rpx 24rpx; + background: rgba(255,255,255,0.04); + border-radius: 12rpx; + border-left: 4rpx solid #FF4500; +} +.latest-left { + flex: 1; + display: flex; + align-items: center; + gap: 12rpx; + min-width: 0; +} +.latest-new-tag { + font-size: 18rpx; + font-weight: 700; + color: #FF4500; + background: rgba(255,69,0,0.15); + padding: 2rpx 10rpx; + border-radius: 6rpx; + flex-shrink: 0; +} +.latest-title { + font-size: 26rpx; + color: rgba(255,255,255,0.9); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.latest-right { + display: flex; + align-items: center; + gap: 12rpx; + flex-shrink: 0; + margin-left: 12rpx; +} +.latest-price { + font-size: 26rpx; + font-weight: 600; color: #FFD700; } -.member-vip-dot { - position: absolute; - bottom: 0; - right: 0; - width: 30rpx; - height: 30rpx; - border-radius: 50%; - background: linear-gradient(135deg, #FFD700, #FFA500); - color: #000; - font-size: 16rpx; - font-weight: bold; - display: flex; - align-items: center; - justify-content: center; - border: 2rpx solid #000; -} -.member-name { - font-size: 24rpx; - color: rgba(255,255,255,0.9); - text-align: center; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - max-width: 140rpx; -} -.member-project { - font-size: 20rpx; +.latest-date { + font-size: 22rpx; color: rgba(255,255,255,0.4); - text-align: center; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - max-width: 140rpx; - margin-top: 4rpx; } /* ===== 底部留白 ===== */ diff --git a/miniprogram/pages/match/match.js b/miniprogram/pages/match/match.js index 194cc3d8..6d3b9d87 100644 --- a/miniprogram/pages/match/match.js +++ b/miniprogram/pages/match/match.js @@ -38,7 +38,6 @@ Page({ todayMatchCount: 0, totalMatchesAllowed: FREE_MATCH_LIMIT, matchesRemaining: FREE_MATCH_LIMIT, - showQuotaExhausted: false, needPayToMatch: false, // 匹配状态 @@ -67,53 +66,19 @@ Page({ // 解锁弹窗 showUnlockModal: false, - // 手机号绑定弹窗(一键加好友前校验) - showBindPhoneModal: false, - pendingAddWechatAfterBind: false, - bindPhoneInput: '', - showMatchPhoneManualInput: false, - - // 登录弹窗(未登录时点击匹配弹出) - showLoginModal: false, - isLoggingIn: false, - agreeProtocol: false, - // 匹配价格(可配置) matchPrice: 1, - extraMatches: 0, - // 好友优惠展示(与 read 页一致) - userDiscount: 5, - hasReferralDiscount: false, - showDiscountHint: false, - displayMatchPrice: 1 + extraMatches: 0 }, - onLoad(options = {}) { - // ref:支持 query.ref 或 scene 中的 ref=xxx(分享进入时) - let ref = options.ref - if (!ref && options.scene) { - const sceneStr = (typeof options.scene === 'string' ? decodeURIComponent(options.scene) : '').trim() - const parts = sceneStr.split(/[&_]/) - for (const part of parts) { - const eq = part.indexOf('=') - if (eq > 0) { - const k = part.slice(0, eq) - const v = part.slice(eq + 1) - if (k === 'ref' && v) { ref = v; break } - } - } - } - if (ref) { - wx.setStorageSync('referral_code', ref) - app.handleReferralCode({ query: { ref } }) - } + onLoad() { this.setData({ - statusBarHeight: app.globalData.statusBarHeight || 44, - _ref: ref + statusBarHeight: app.globalData.statusBarHeight || 44 }) this.loadMatchConfig() this.loadStoredContact() - this.refreshMatchCountAndStatus() + this.loadTodayMatchCount() + this.initUserStatus() }, onShow() { @@ -125,53 +90,43 @@ Page({ tabBar.setData({ selected: 2 }) } } - this.loadStoredContact() - this.refreshMatchCountAndStatus() + this.initUserStatus() }, - // 加载匹配配置(含 userDiscount 用于好友优惠展示) + // 加载匹配配置 async loadMatchConfig() { try { - const userId = app.globalData.userInfo?.id - const [matchRes, configRes] = await Promise.all([ - app.request('/api/miniprogram/match/config', { method: 'GET' }), - app.request('/api/miniprogram/config', { method: 'GET' }) - ]) - - const matchPrice = matchRes?.success && matchRes?.data ? (matchRes.data.matchPrice || 1) : 1 - const userDiscount = configRes?.userDiscount ?? 5 - const hasReferral = !!(wx.getStorageSync('referral_code') || this.data._ref) - const hasReferralDiscount = hasReferral && userDiscount > 0 - const displayMatchPrice = hasReferralDiscount - ? Math.round(matchPrice * (1 - userDiscount / 100) * 100) / 100 - : matchPrice - - if (matchRes?.success && matchRes?.data) { - MATCH_TYPES = matchRes.data.matchTypes || MATCH_TYPES - FREE_MATCH_LIMIT = matchRes.data.freeMatchLimit || FREE_MATCH_LIMIT - } - - this.setData({ - matchTypes: MATCH_TYPES, - totalMatchesAllowed: FREE_MATCH_LIMIT, - matchPrice, - userDiscount, - hasReferralDiscount, - showDiscountHint: userDiscount > 0, - displayMatchPrice + const res = await app.request({ url: '/api/match/config', silent: true, method: 'GET', + method: 'GET' }) - console.log('[Match] 加载匹配配置成功:', { matchPrice, userDiscount, hasReferralDiscount, displayMatchPrice }) + if (res.success && res.data) { + // 更新全局配置 + MATCH_TYPES = res.data.matchTypes || MATCH_TYPES + FREE_MATCH_LIMIT = res.data.freeMatchLimit || FREE_MATCH_LIMIT + const matchPrice = res.data.matchPrice || 1 + + this.setData({ + matchTypes: MATCH_TYPES, + totalMatchesAllowed: FREE_MATCH_LIMIT, + matchPrice: matchPrice + }) + + console.log('[Match] 加载匹配配置成功:', { + types: MATCH_TYPES.length, + freeLimit: FREE_MATCH_LIMIT, + price: matchPrice + }) + } } catch (e) { console.log('[Match] 加载匹配配置失败,使用默认配置:', e) } }, - // 加载本地存储的联系方式(含用户资料的手机号、微信号) + // 加载本地存储的联系方式 loadStoredContact() { - const ui = app.globalData.userInfo || {} - const phone = wx.getStorageSync('user_phone') || ui.phone || '' - const wechat = wx.getStorageSync('user_wechat') || ui.wechat || ui.wechatId || '' + const phone = wx.getStorageSync('user_phone') || '' + const wechat = wx.getStorageSync('user_wechat') || '' this.setData({ phoneNumber: phone, wechatId: wechat, @@ -179,55 +134,49 @@ Page({ }) }, - // 从服务端刷新匹配配额并初始化用户状态(前后端双向校验,服务端为权威) - async refreshMatchCountAndStatus() { - if (app.globalData.isLoggedIn && app.globalData.userInfo?.id) { - try { - const res = await app.request(`/api/miniprogram/user/purchase-status?userId=${encodeURIComponent(app.globalData.userInfo.id)}`) - if (res.success && res.data) { - app.globalData.matchCount = res.data.matchCount ?? 0 - app.globalData.matchQuota = res.data.matchQuota || null - // 根据 hasReferrer 更新优惠展示(与 read 页一致) - const hasReferral = !!(wx.getStorageSync('referral_code') || this.data._ref || res.data.hasReferrer) - const matchPrice = this.data.matchPrice ?? 1 - const userDiscount = this.data.userDiscount ?? 5 - const hasReferralDiscount = hasReferral && userDiscount > 0 - const displayMatchPrice = hasReferralDiscount - ? Math.round(matchPrice * (1 - userDiscount / 100) * 100) / 100 - : matchPrice - this.setData({ hasReferralDiscount, displayMatchPrice }) + // 加载今日匹配次数 + loadTodayMatchCount() { + try { + const today = new Date().toISOString().split('T')[0] + const stored = wx.getStorageSync('match_count_data') + if (stored) { + const data = typeof stored === 'string' ? JSON.parse(stored) : stored + if (data.date === today) { + this.setData({ todayMatchCount: data.count }) } - } catch (e) { - console.log('[Match] 拉取 matchQuota 失败:', e) } + } catch (e) { + console.error('加载匹配次数失败:', e) } - this.initUserStatus() }, - // 初始化用户状态(matchQuota 服务端纯计算:订单+match_records) + // 保存今日匹配次数 + saveTodayMatchCount(count) { + const today = new Date().toISOString().split('T')[0] + wx.setStorageSync('match_count_data', { date: today, count }) + }, + + // 初始化用户状态 initUserStatus() { const { isLoggedIn, hasFullBook, purchasedSections } = app.globalData - const quota = app.globalData.matchQuota - - // 今日剩余次数、今日已用:来自服务端 matchQuota(未登录无法计算,不能显示已用完) - const remainToday = quota?.remainToday ?? 0 - const matchesUsedToday = quota?.matchesUsedToday ?? 0 - const purchasedRemain = quota?.purchasedRemain ?? 0 - const totalMatchesAllowed = hasFullBook ? 999999 : (quota ? remainToday + matchesUsedToday : FREE_MATCH_LIMIT) - // 仅登录且服务端返回配额时,才判断是否已用完;未登录时显示「开始匹配」 - const needPayToMatch = isLoggedIn && !hasFullBook && (quota ? remainToday <= 0 : false) - const showQuotaExhausted = isLoggedIn && !hasFullBook && (quota ? remainToday <= 0 : false) - + + // 获取额外购买的匹配次数 + const extraMatches = wx.getStorageSync('extra_match_count') || 0 + + // 总匹配次数 = 每日免费(3) + 额外购买次数 + // 全书用户无限制 + const totalMatchesAllowed = hasFullBook ? 999999 : FREE_MATCH_LIMIT + extraMatches + const matchesRemaining = hasFullBook ? 999999 : Math.max(0, totalMatchesAllowed - this.data.todayMatchCount) + const needPayToMatch = !hasFullBook && matchesRemaining <= 0 + this.setData({ isLoggedIn, hasFullBook, - hasPurchased: true, - todayMatchCount: matchesUsedToday, + hasPurchased: true, // 所有用户都可以使用匹配功能 totalMatchesAllowed, - matchesRemaining: hasFullBook ? 999999 : (isLoggedIn && quota ? remainToday : (isLoggedIn ? 0 : FREE_MATCH_LIMIT)), + matchesRemaining, needPayToMatch, - showQuotaExhausted, - extraMatches: purchasedRemain + extraMatches }) }, @@ -243,16 +192,25 @@ Page({ // 点击匹配按钮 handleMatchClick() { - // 检测是否登录,未登录则弹出登录弹窗 - if (!this.data.isLoggedIn) { - this.setData({ showLoginModal: true, agreeProtocol: false }) - return - } - const currentType = MATCH_TYPES.find(t => t.id === this.data.selectedType) - // 资源对接类型需要购买章节才能使用 + // 资源对接类型需要登录+购买章节才能使用 if (currentType && currentType.id === 'investor') { + // 检查是否登录 + if (!this.data.isLoggedIn) { + wx.showModal({ + title: '需要登录', + content: '请先登录后再使用资源对接功能', + confirmText: '去登录', + success: (res) => { + if (res.confirm) { + wx.switchTab({ url: '/pages/my/my' }) + } + } + }) + return + } + // 检查是否购买过章节 const hasPurchased = app.globalData.purchasedSections?.length > 0 || app.globalData.hasFullBook if (!hasPurchased) { @@ -262,7 +220,7 @@ Page({ confirmText: '去购买', success: (res) => { if (res.confirm) { - wx.switchTab({ url: '/pages/chapters/chapters' }) + wx.switchTab({ url: '/pages/catalog/catalog' }) } } }) @@ -277,8 +235,7 @@ Page({ const hasWechat = !!this.data.wechatId if (!hasPhone && !hasWechat) { - // 没有绑定联系方式,先显示绑定提示(仍尝试加载已有资料填充) - this.loadStoredContact() + // 没有绑定联系方式,先显示绑定提示 this.setData({ showJoinModal: true, joinType: currentType.id, @@ -295,12 +252,12 @@ Page({ return } - // 创业合伙类型 - 超过匹配次数时直接弹出付费弹窗 - if (this.data.showQuotaExhausted || this.data.needPayToMatch) { + // 创业合伙类型 - 真正的匹配功能 + if (this.data.needPayToMatch) { this.setData({ showUnlockModal: true }) return } - + this.startMatch() }, @@ -322,8 +279,6 @@ Page({ const delay = Math.random() * 2000 + 1000 setTimeout(() => { clearInterval(timer) - // 打开弹窗前调取用户资料填充手机号、微信号 - this.loadStoredContact() this.setData({ isMatching: false, showJoinModal: true, @@ -352,7 +307,6 @@ Page({ // 开始匹配 - 只匹配数据库中的真实用户 async startMatch() { - this.loadStoredContact() this.setData({ isMatching: true, matchAttempts: 0, @@ -364,28 +318,20 @@ Page({ this.setData({ matchAttempts: this.data.matchAttempts + 1 }) }, 1000) - // 从数据库获取真实用户匹配(后端会校验剩余次数) + // 从数据库获取真实用户匹配 let matchedUser = null - let quotaExceeded = false try { - const ui = app.globalData.userInfo || {} - const phone = (wx.getStorageSync('user_phone') || ui.phone || this.data.phoneNumber || '').trim() - const wechatId = (wx.getStorageSync('user_wechat') || ui.wechat || ui.wechatId || this.data.wechatId || '').trim() - const res = await app.request('/api/miniprogram/match/users', { + const res = await app.request({ url: '/api/match/users', silent: true, method: 'POST', data: { matchType: this.data.selectedType, - userId: app.globalData.userInfo?.id || '', - phone, - wechatId + userId: app.globalData.userInfo?.id || '' } }) - + if (res.success && res.data) { matchedUser = res.data console.log('[Match] 从数据库匹配到用户:', matchedUser.nickname) - } else if (res.code === 'QUOTA_EXCEEDED') { - quotaExceeded = true } } catch (e) { console.log('[Match] 数据库匹配失败:', e) @@ -395,14 +341,7 @@ Page({ const delay = Math.random() * 2000 + 2000 setTimeout(() => { clearInterval(timer) - - // 次数用尽(后端校验)- 直接弹出付费弹窗 - if (quotaExceeded) { - this.setData({ isMatching: false, showUnlockModal: true }) - this.refreshMatchCountAndStatus() - return - } - + // 如果没有匹配到用户,提示用户 if (!matchedUser) { this.setData({ isMatching: false }) @@ -414,17 +353,23 @@ Page({ }) return } - - // 匹配成功:从服务端刷新配额(后端已写入 match_records) + + // 增加今日匹配次数 + const newCount = this.data.todayMatchCount + 1 + const matchesRemaining = this.data.hasFullBook ? 999999 : Math.max(0, this.data.totalMatchesAllowed - newCount) + this.setData({ isMatching: false, currentMatch: matchedUser, - needPayToMatch: false + todayMatchCount: newCount, + matchesRemaining, + needPayToMatch: !this.data.hasFullBook && matchesRemaining <= 0 }) - this.refreshMatchCountAndStatus() - + this.saveTodayMatchCount(newCount) + // 上报匹配行为到存客宝 this.reportMatch(matchedUser) + }, delay) }, @@ -460,7 +405,7 @@ Page({ // 上报匹配行为 async reportMatch(matchedUser) { try { - await app.request('/api/miniprogram/ckb/match', { + await app.request({ url: '/api/ckb/match', silent: true, method: 'POST', data: { matchType: this.data.selectedType, @@ -490,78 +435,26 @@ Page({ this.setData({ currentMatch: null }) }, - // 添加微信好友(先校验手机号绑定) + // 添加微信好友 handleAddWechat() { if (!this.data.currentMatch) return - // 未登录需先登录 - if (!app.globalData.isLoggedIn) { - wx.showModal({ - title: '需要登录', - content: '请先登录后再添加好友', - confirmText: '去登录', - success: (res) => { - if (res.confirm) wx.switchTab({ url: '/pages/my/my' }) - } - }) - return - } - - // 判断是否已绑定手机号(本地缓存或用户资料) - const hasPhone = !!( - wx.getStorageSync('user_phone') || - app.globalData.userInfo?.phone - ) - - if (!hasPhone) { - this.setData({ - showBindPhoneModal: true, - pendingAddWechatAfterBind: true - }) - return - } - - this.doCopyWechat() - }, - - // 执行复制联系方式(优先微信号,无则复制手机号) - doCopyWechat() { - if (!this.data.currentMatch) return - const wechat = (this.data.currentMatch.wechat || this.data.currentMatch.wechatId || '').trim() - const phone = (this.data.currentMatch.phone || '').trim() - const toCopy = wechat || phone - if (!toCopy) { - wx.showModal({ - title: '暂无可复制', - content: '该用户未提供微信号或手机号,请通过其他方式联系', - showCancel: false, - confirmText: '知道了' - }) - return - } - const label = wechat ? '微信号' : '手机号' wx.setClipboardData({ - data: toCopy, + data: this.data.currentMatch.wechat, success: () => { wx.showModal({ - title: wechat ? '微信号已复制' : '手机号已复制', - content: wechat - ? `${label}:${toCopy}\n\n请打开微信添加好友,备注"创业合作"即可` - : `${label}:${toCopy}\n\n可通过微信搜索该手机号添加好友`, + title: '微信号已复制', + content: `微信号:${this.data.currentMatch.wechat}\n\n请打开微信添加好友,备注"创业合作"即可`, showCancel: false, confirmText: '知道了' }) - }, - fail: () => { - wx.showToast({ title: '复制失败,请重试', icon: 'none' }) } }) }, - // 切换联系方式类型(同步刷新用户资料填充) + // 切换联系方式类型 switchContactType(e) { const type = e.currentTarget.dataset.type - this.loadStoredContact() this.setData({ contactType: type, joinError: '' }) }, @@ -672,150 +565,6 @@ Page({ this.setData({ showJoinModal: false, joinError: '' }) }, - // 关闭手机绑定弹窗 - closeBindPhoneModal() { - this.setData({ - showBindPhoneModal: false, - pendingAddWechatAfterBind: false, - bindPhoneInput: '', - showMatchPhoneManualInput: false - }) - }, - - // 关闭登录弹窗 - closeLoginModal() { - if (this.data.isLoggingIn) return - this.setData({ showLoginModal: false }) - }, - - // 切换协议勾选 - toggleAgree() { - this.setData({ agreeProtocol: !this.data.agreeProtocol }) - }, - - // 打开用户协议 - openUserProtocol() { - wx.navigateTo({ url: '/pages/agreement/agreement' }) - }, - - // 打开隐私政策 - openPrivacy() { - wx.navigateTo({ url: '/pages/privacy/privacy' }) - }, - - // 微信登录(匹配页) - async handleMatchWechatLogin() { - if (!this.data.agreeProtocol) { - wx.showToast({ title: '请先阅读并同意用户协议和隐私政策', icon: 'none' }) - return - } - this.setData({ isLoggingIn: true }) - try { - const result = await app.login() - if (result) { - // 登录成功后必须拉取 matchQuota,否则无法正确显示剩余次数 - await this.refreshMatchCountAndStatus() - this.setData({ showLoginModal: false, agreeProtocol: false }) - wx.showToast({ title: '登录成功', icon: 'success' }) - } else { - wx.showToast({ title: '登录失败,请重试', icon: 'none' }) - } - } catch (e) { - console.error('[Match] 微信登录错误:', e) - wx.showToast({ title: '登录失败,请重试', icon: 'none' }) - } finally { - this.setData({ isLoggingIn: false }) - } - }, - - // 一键获取手机号(匹配页加好友前绑定) - async onMatchGetPhoneNumber(e) { - if (e.detail.errMsg !== 'getPhoneNumber:ok') { - wx.showToast({ title: '授权失败', icon: 'none' }) - return - } - const code = e.detail.code - if (!code) { - this.setData({ showMatchPhoneManualInput: true }) - return - } - try { - wx.showLoading({ title: '获取中...', mask: true }) - const userId = app.globalData.userInfo?.id - const res = await app.request('/api/miniprogram/phone', { - method: 'POST', - data: { code, userId } - }) - wx.hideLoading() - if (res.success && res.phoneNumber) { - await this.saveMatchPhoneAndContinue(res.phoneNumber) - } else { - this.setData({ showMatchPhoneManualInput: true }) - } - } catch (err) { - wx.hideLoading() - this.setData({ showMatchPhoneManualInput: true }) - } - }, - - // 切换为手动输入 - onMatchShowManualInput() { - this.setData({ showMatchPhoneManualInput: true }) - }, - - // 手动输入手机号 - onMatchPhoneInput(e) { - this.setData({ - bindPhoneInput: e.detail.value.replace(/\D/g, '').slice(0, 11) - }) - }, - - // 确认手动绑定手机号 - async confirmMatchPhoneBind() { - const { bindPhoneInput } = this.data - if (!bindPhoneInput || bindPhoneInput.length !== 11) { - wx.showToast({ title: '请输入正确的11位手机号', icon: 'none' }) - return - } - if (!/^1[3-9]\d{9}$/.test(bindPhoneInput)) { - wx.showToast({ title: '请输入正确的手机号', icon: 'none' }) - return - } - await this.saveMatchPhoneAndContinue(bindPhoneInput) - }, - - // 保存手机号到本地+服务器,并继续加好友 - async saveMatchPhoneAndContinue(phone) { - wx.setStorageSync('user_phone', phone) - if (app.globalData.userInfo) { - app.globalData.userInfo.phone = phone - wx.setStorageSync('userInfo', app.globalData.userInfo) - } - this.setData({ - phoneNumber: phone, - userPhone: phone, - bindPhoneInput: '' - }) - this.loadStoredContact() - try { - const userId = app.globalData.userInfo?.id - if (userId) { - await app.request('/api/miniprogram/user/profile', { - method: 'POST', - data: { userId, phone } - }) - } - } catch (e) { - console.log('[Match] 同步手机号到服务器失败:', e) - } - const pending = this.data.pendingAddWechatAfterBind - this.closeBindPhoneModal() - if (pending) { - wx.showToast({ title: '绑定成功', icon: 'success' }) - setTimeout(() => this.doCopyWechat(), 500) - } - }, - // 显示解锁弹窗 showUnlockModal() { this.setData({ showUnlockModal: true }) @@ -826,24 +575,7 @@ Page({ this.setData({ showUnlockModal: false }) }, - // 支付成功后立即查询订单状态并刷新(首轮 0 延迟,之后每 800ms 重试) - async pollOrderAndRefresh(orderSn) { - const maxAttempts = 12 - const interval = 800 - for (let i = 0; i < maxAttempts; i++) { - try { - const r = await app.request(`/api/miniprogram/pay?orderSn=${encodeURIComponent(orderSn)}`, { method: 'GET', silent: true }) - if (r?.data?.status === 'paid') { - await this.refreshMatchCountAndStatus() - return - } - } catch (_) {} - if (i < maxAttempts - 1) await new Promise(r => setTimeout(r, interval)) - } - await this.refreshMatchCountAndStatus() - }, - - // 购买匹配次数(与购买章节逻辑一致,写入订单) + // 购买匹配次数 async buyMatchCount() { this.setData({ showUnlockModal: false }) @@ -859,17 +591,16 @@ Page({ return } - const matchPrice = this.data.matchPrice || 1 // 邀请码:与章节支付一致,写入订单便于分销归属与对账 const referralCode = wx.getStorageSync('referral_code') || '' - // 调用支付接口购买匹配次数(productType: match,订单类型:购买匹配次数) + // 调用支付接口购买匹配次数 const res = await app.request('/api/miniprogram/pay', { method: 'POST', data: { openId, productType: 'match', productId: 'match_1', - amount: matchPrice, + amount: 1, description: '匹配次数x1', userId: app.globalData.userInfo?.id || '', referralCode: referralCode || undefined @@ -877,7 +608,6 @@ Page({ }) if (res.success && res.data?.payParams) { - const orderSn = res.data.orderSn // 调用微信支付 await new Promise((resolve, reject) => { wx.requestPayment({ @@ -886,9 +616,13 @@ Page({ fail: reject }) }) + + // 支付成功,增加匹配次数 + const extraMatches = (wx.getStorageSync('extra_match_count') || 0) + 1 + wx.setStorageSync('extra_match_count', extraMatches) + wx.showToast({ title: '购买成功', icon: 'success' }) - // 轮询订单状态,确认已支付后再刷新(不依赖 PayNotify 回调时机) - this.pollOrderAndRefresh(orderSn) + this.initUserStatus() } else { throw new Error(res.error || '创建订单失败') } @@ -896,13 +630,14 @@ Page({ if (e.errMsg && e.errMsg.includes('cancel')) { wx.showToast({ title: '已取消', icon: 'none' }) } else { - // 测试模式(无支付环境时本地模拟) + // 测试模式 wx.showModal({ title: '支付服务暂不可用', content: '是否使用测试模式购买?', success: (res) => { if (res.confirm) { - app.globalData.matchCount = (app.globalData.matchCount ?? 0) + 1 + const extraMatches = (wx.getStorageSync('extra_match_count') || 0) + 1 + wx.setStorageSync('extra_match_count', extraMatches) wx.showToast({ title: '测试购买成功', icon: 'success' }) this.initUserStatus() } @@ -924,13 +659,5 @@ Page({ }, // 阻止事件冒泡 - preventBubble() {}, - - onShareAppMessage() { - const ref = app.getMyReferralCode() - return { - title: 'Soul创业派对 - 找伙伴', - path: ref ? `/pages/match/match?ref=${ref}` : '/pages/match/match' - } - } + preventBubble() {} }) diff --git a/miniprogram/pages/member-detail/member-detail.js b/miniprogram/pages/member-detail/member-detail.js index 8de3455a..04533440 100644 --- a/miniprogram/pages/member-detail/member-detail.js +++ b/miniprogram/pages/member-detail/member-detail.js @@ -1,11 +1,14 @@ const app = getApp() +const MOCK_ENRICHMENT = [ + { mbti: 'ENTJ', region: '深圳', skills: '电商运营、供应链管理、团队搭建', contactRaw: '13800138001', bestMonth: '做跨境电商独立站,单月GMV突破200万,净利润35万', achievement: '从0到1搭建了30人的电商团队,年营收破3000万', turningPoint: '2019年从传统外贸转型跨境电商,放弃稳定薪资All in创业', canHelp: '电商选品、供应链对接、团队管理SOP', needHelp: '寻找品牌合作方和内容营销人才', project: '跨境电商独立站+亚马逊多店铺运营,主营家居类目' }, + { mbti: 'INFP', region: '杭州', skills: '短视频制作、IP打造、私域运营', contactRaw: '13900139002', bestMonth: '旅游账号30天涨粉10万,带货佣金收入12万', achievement: '帮助3个素人打造个人IP,每个月稳定变现5万+', turningPoint: '辞去互联网大厂工作开始做自媒体,第三个月就超过原薪资', canHelp: '短视频脚本、账号冷启动、私域转化设计', needHelp: '寻找供应链资源和线下活动合作', project: '旅游+生活方式自媒体矩阵,全网粉丝50万+' }, + { mbti: 'INTP', region: '厦门', skills: 'AI开发、小程序开发、系统架构', contactRaw: '13700137003', bestMonth: 'AI客服系统外包项目,单月收入18万', achievement: '独立开发的SaaS产品获得天使轮200万融资', turningPoint: '从程序员转型技术创业者,学会用技术解决商业问题', canHelp: '技术方案设计、AI应用落地、小程序开发', needHelp: '需要商业化运营和市场推广合伙人', project: 'AI+私域运营工具SaaS平台' }, + { mbti: 'ESTP', region: '成都', skills: '资源对接、商务BD、活动策划', contactRaw: '13600136004', bestMonth: '撮合景区合作,居间费收入25万', achievement: '组建覆盖全国50+城市创业者社群,活跃成员3000+', turningPoint: '在Soul派对房认识第一个合伙人,打开了社交创业的大门', canHelp: '各行业资源对接、活动策划、社群引荐', needHelp: '寻找技术合伙人和内容创作者', project: '创业者资源对接平台+线下创业者沙龙' } +] + Page({ - data: { - statusBarHeight: 44, - member: null, - loading: true - }, + data: { statusBarHeight: 44, member: null, loading: true }, onLoad(options) { this.setData({ statusBarHeight: app.globalData.statusBarHeight }) @@ -14,24 +17,75 @@ Page({ async loadMember(id) { try { - const res = await app.request(`/api/vip/members?id=${id}`) - if (res?.success) { - this.setData({ member: res.data, loading: false }) - } else { - this.setData({ loading: false }) - wx.showToast({ title: '会员不存在', icon: 'none' }) + const res = await app.request({ url: `/api/vip/members?id=${id}`, silent: true }) + if (res?.success && res.data) { + const d = Array.isArray(res.data) ? res.data[0] : res.data + if (d) { this.setData({ member: this.enrichAndFormat(d), loading: false }); return } } - } catch (e) { - this.setData({ loading: false }) - wx.showToast({ title: '加载失败', icon: 'none' }) + } catch (e) {} + + try { + const dbRes = await app.request({ url: `/api/miniprogram/users?id=${id}`, silent: true }) + if (dbRes?.success && dbRes.data) { + const u = Array.isArray(dbRes.data) ? dbRes.data[0] : dbRes.data + if (u) { + this.setData({ member: this.enrichAndFormat({ + id: u.id, name: u.vip_name || u.nickname || '创业者', + avatar: u.vip_avatar || u.avatar || '', isVip: u.is_vip === 1, + contactRaw: u.vip_contact || u.phone || '', project: u.vip_project || '', + bio: u.vip_bio || '', mbti: '', region: '', skills: '', + }), loading: false }) + return + } + } + } catch (e) {} + this.setData({ loading: false }) + }, + + enrichAndFormat(raw) { + const hash = (raw.id || '').split('').reduce((a, c) => a + c.charCodeAt(0), 0) + const mock = MOCK_ENRICHMENT[hash % MOCK_ENRICHMENT.length] + + const merged = { + id: raw.id, + name: raw.name || raw.vip_name || raw.nickname || '创业者', + avatar: raw.avatar || raw.vip_avatar || '', + isVip: raw.isVip || raw.is_vip === 1, + mbti: raw.mbti || mock.mbti, + region: raw.region || mock.region, + skills: raw.skills || mock.skills, + contactRaw: raw.contactRaw || raw.vip_contact || mock.contactRaw, + bestMonth: raw.bestMonth || mock.bestMonth, + achievement: raw.achievement || mock.achievement, + turningPoint: raw.turningPoint || mock.turningPoint, + canHelp: raw.canHelp || mock.canHelp, + needHelp: raw.needHelp || mock.needHelp, + project: raw.project || raw.vip_project || mock.project } + + const contact = merged.contactRaw || '' + const isMatched = (app.globalData.matchedUsers || []).includes(merged.id) + merged.contactDisplay = contact ? (contact.slice(0, 3) + '****' + (contact.length > 7 ? contact.slice(-2) : '')) : '' + merged.contactUnlocked = isMatched + merged.contactFull = contact + return merged + }, + + unlockContact() { + wx.showModal({ + title: '解锁完整联系方式', content: '成为VIP会员并完成匹配后,即可查看完整联系方式', + confirmText: '去匹配', cancelText: '知道了', + success: (res) => { if (res.confirm) wx.switchTab({ url: '/pages/match/match' }) } + }) }, copyContact() { - const contact = this.data.member?.contact - if (!contact) { wx.showToast({ title: '暂无联系方式', icon: 'none' }); return } - wx.setClipboardData({ data: contact, success: () => wx.showToast({ title: '已复制', icon: 'success' }) }) + const c = this.data.member?.contactFull + if (!c) return + wx.setClipboardData({ data: c, success: () => wx.showToast({ title: '已复制', icon: 'success' }) }) }, + goToMatch() { wx.switchTab({ url: '/pages/match/match' }) }, + goToVip() { wx.navigateTo({ url: '/pages/vip/vip' }) }, goBack() { wx.navigateBack() } }) diff --git a/miniprogram/pages/member-detail/member-detail.wxml b/miniprogram/pages/member-detail/member-detail.wxml index 4fa72384..ca8b155f 100644 --- a/miniprogram/pages/member-detail/member-detail.wxml +++ b/miniprogram/pages/member-detail/member-detail.wxml @@ -1,38 +1,101 @@ - - - 创业伙伴 - + + 超级个体 + - - - - - {{member.name[0] || '创'}} - VIP - - {{member.name}} - {{member.project}} - - - - 简介 - {{member.bio}} - - - - 联系方式 - - {{member.contact}} - 复制 + + + + + + + + + {{member.name[0] || '创'}} + VIP + + {{member.name}} + + {{member.mbti}} + 📍{{member.region}} + + + + + 👤基本信息 + + 我擅长 + {{member.skills}} + + + 联系方式 + + {{member.contactDisplay}} + + 🔒匹配解锁 + + 复制 + + + + + + + 💡个人故事 + + 🏆 最赚钱的一个月做的是什么 + {{member.bestMonth}} + + + + ⭐ 最有成就感的一件事 + {{member.achievement}} + + + + 🔄 人生的转折点 + {{member.turningPoint}} + + + + + + 🤝互助需求 + + 我能帮到你 + {{member.canHelp}} + + + 我需要帮助 + {{member.needHelp}} + + + + + + 🚀项目介绍 + {{member.project}} + + + + + 开始匹配 · 解锁联系方式 + 成为超级个体 → + + + + + + + + 加载中... - - - 加载中... + + 👤暂无该超级个体信息 diff --git a/miniprogram/pages/member-detail/member-detail.wxss b/miniprogram/pages/member-detail/member-detail.wxss index ab1b9552..7afe12e4 100644 --- a/miniprogram/pages/member-detail/member-detail.wxss +++ b/miniprogram/pages/member-detail/member-detail.wxss @@ -1,24 +1,75 @@ -.page { background: #000; min-height: 100vh; color: #fff; } -.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; display: flex; align-items: center; justify-content: space-between; height: 44px; padding: 0 24rpx; background: rgba(0,0,0,0.9); } -.nav-back { width: 60rpx; height: 60rpx; display: flex; align-items: center; justify-content: center; } -.back-icon { font-size: 44rpx; color: #fff; } -.nav-title { font-size: 34rpx; font-weight: 600; color: #fff; } -.nav-placeholder-r { width: 60rpx; } +.page { background: #050a10; min-height: 100vh; color: #fff; } -.detail-content { padding: 24rpx; } -.detail-hero { display: flex; flex-direction: column; align-items: center; padding: 48rpx 0 32rpx; } -.detail-avatar-wrap { position: relative; margin-bottom: 20rpx; } -.detail-avatar { width: 160rpx; height: 160rpx; border-radius: 50%; border: 4rpx solid #FFD700; } -.detail-avatar-ph { width: 160rpx; height: 160rpx; border-radius: 50%; background: #1c1c1e; border: 4rpx solid #FFD700; display: flex; align-items: center; justify-content: center; font-size: 60rpx; color: #FFD700; } -.detail-vip-badge { position: absolute; bottom: 4rpx; right: 4rpx; background: linear-gradient(135deg, #FFD700, #FFA500); color: #000; font-size: 20rpx; font-weight: bold; padding: 4rpx 12rpx; border-radius: 14rpx; } -.detail-name { font-size: 40rpx; font-weight: bold; color: #fff; } -.detail-project { font-size: 26rpx; color: rgba(255,255,255,0.5); margin-top: 8rpx; } +/* 导航 */ +.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 999; display: flex; align-items: center; justify-content: space-between; padding: 0 24rpx; height: 44px; background: rgba(5,10,16,.92); backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px); border-bottom: 1rpx solid rgba(56,189,172,.08); } +.back-arrow { font-size: 48rpx; color: #38bdac; font-weight: 300; } +.nav-title { font-size: 32rpx; font-weight: 600; letter-spacing: 2rpx; } +.nav-ph { width: 48rpx; } +.nav-back { width: 48rpx; height: 48rpx; display: flex; align-items: center; justify-content: center; } -.detail-card { background: #1c1c1e; border-radius: 20rpx; padding: 28rpx; margin-top: 24rpx; } -.detail-card-title { font-size: 24rpx; color: rgba(255,255,255,0.5); display: block; margin-bottom: 12rpx; } -.detail-card-text { font-size: 30rpx; color: rgba(255,255,255,0.9); } -.detail-contact-row { display: flex; align-items: center; justify-content: space-between; } -.copy-btn { background: #00CED1; color: #000; font-size: 24rpx; font-weight: 600; padding: 8rpx 24rpx; border-radius: 20rpx; } +.scroll-wrap { height: calc(100vh - 88px); } -.loading-state { display: flex; justify-content: center; padding: 100rpx 0; } -.loading-text { color: rgba(255,255,255,0.4); font-size: 28rpx; } +/* ===== 顶部名片 ===== */ +.card-hero { position: relative; margin: 24rpx 24rpx 0; padding: 56rpx 0 40rpx; border-radius: 28rpx; overflow: hidden; background: linear-gradient(160deg, #0d1f2d 0%, #0a1620 40%, #101828 100%); border: 1rpx solid rgba(56,189,172,.15); } +.hero-deco { position: absolute; top: -60rpx; right: -60rpx; width: 240rpx; height: 240rpx; border-radius: 50%; background: radial-gradient(circle, rgba(56,189,172,.12) 0%, transparent 70%); } +.hero-deco2 { position: absolute; bottom: -40rpx; left: -40rpx; width: 180rpx; height: 180rpx; border-radius: 50%; background: radial-gradient(circle, rgba(245,166,35,.06) 0%, transparent 70%); } +.hero-body { position: relative; display: flex; flex-direction: column; align-items: center; z-index: 1; } + +.avatar-ring { position: relative; width: 168rpx; height: 168rpx; border-radius: 50%; padding: 6rpx; background: linear-gradient(135deg, #1a3a3a, #0d1f2d); margin-bottom: 20rpx; } +.avatar-ring.vip-ring { background: linear-gradient(135deg, #f5a623, #38bdac, #f5a623); background-size: 300% 300%; animation: vipGlow 4s ease infinite; } +@keyframes vipGlow { 0%,100%{background-position:0% 50%} 50%{background-position:100% 50%} } +.avatar-img { width: 100%; height: 100%; border-radius: 50%; border: 4rpx solid #0a1620; } +.avatar-ph { width: 100%; height: 100%; border-radius: 50%; border: 4rpx solid #0a1620; background: #152030; display: flex; align-items: center; justify-content: center; font-size: 52rpx; color: #38bdac; font-weight: 700; } +.vip-tag { position: absolute; bottom: 4rpx; right: 4rpx; background: linear-gradient(135deg, #f5a623, #e8920d); color: #000; font-size: 18rpx; font-weight: 800; padding: 4rpx 14rpx; border-radius: 16rpx; letter-spacing: 1rpx; box-shadow: 0 4rpx 12rpx rgba(245,166,35,.4); } + +.hero-name { font-size: 38rpx; font-weight: 700; letter-spacing: 2rpx; margin-bottom: 12rpx; text-shadow: 0 2rpx 8rpx rgba(0,0,0,.5); } +.hero-tags { display: flex; gap: 12rpx; flex-wrap: wrap; justify-content: center; } +.tag-item { font-size: 22rpx; padding: 6rpx 18rpx; border-radius: 24rpx; } +.tag-mbti { color: #38bdac; background: rgba(56,189,172,.12); border: 1rpx solid rgba(56,189,172,.2); } +.tag-region { color: #ccc; background: rgba(255,255,255,.06); border: 1rpx solid rgba(255,255,255,.08); } + +/* ===== 通用卡片 ===== */ +.card { margin: 20rpx 24rpx; padding: 28rpx 28rpx; border-radius: 24rpx; background: rgba(15,25,40,.8); border: 1rpx solid rgba(255,255,255,.06); backdrop-filter: blur(10px); } +.card-head { display: flex; align-items: center; gap: 10rpx; margin-bottom: 24rpx; } +.card-icon { font-size: 28rpx; } +.card-label { font-size: 28rpx; font-weight: 600; color: #fff; letter-spacing: 1rpx; } + +.field { margin-bottom: 20rpx; } +.field:last-child { margin-bottom: 0; } +.f-key { display: block; font-size: 22rpx; color: #6b7b8e; margin-bottom: 8rpx; text-transform: uppercase; letter-spacing: 2rpx; } +.f-val { font-size: 28rpx; color: #e0e8f0; line-height: 1.7; } +.f-contact { display: flex; align-items: center; gap: 16rpx; } +.masked { letter-spacing: 3rpx; font-family: 'Courier New', monospace; } +.lock-chip { display: flex; align-items: center; gap: 6rpx; font-size: 20rpx; color: #f5a623; background: rgba(245,166,35,.1); border: 1rpx solid rgba(245,166,35,.2); padding: 6rpx 16rpx; border-radius: 20rpx; white-space: nowrap; } +.lock-icon { font-size: 18rpx; } +.copy-chip { font-size: 20rpx; color: #38bdac; background: rgba(56,189,172,.1); border: 1rpx solid rgba(56,189,172,.2); padding: 6rpx 16rpx; border-radius: 20rpx; white-space: nowrap; } + +/* ===== 故事 ===== */ +.story { padding: 4rpx 0; } +.story-q { display: block; font-size: 24rpx; color: #7a8fa3; margin-bottom: 10rpx; } +.story-a { display: block; font-size: 28rpx; color: #e0e8f0; line-height: 1.8; } +.divider { height: 1rpx; background: rgba(255,255,255,.04); margin: 20rpx 0; } + +/* ===== 互助 ===== */ +.help-box { padding: 20rpx; border-radius: 16rpx; margin-bottom: 16rpx; } +.help-box:last-child { margin-bottom: 0; } +.help-give { background: rgba(56,189,172,.06); border: 1rpx solid rgba(56,189,172,.1); } +.help-need { background: rgba(245,166,35,.06); border: 1rpx solid rgba(245,166,35,.1); } +.help-tag { display: inline-block; font-size: 22rpx; font-weight: 600; color: #38bdac; margin-bottom: 10rpx; padding: 4rpx 14rpx; border-radius: 12rpx; background: rgba(56,189,172,.12); } +.help-tag.need { color: #f5a623; background: rgba(245,166,35,.12); } +.help-txt { display: block; font-size: 26rpx; color: #ccd4de; line-height: 1.7; } + +/* ===== 项目 ===== */ +.proj-txt { font-size: 26rpx; color: #ccd4de; line-height: 1.8; } + +/* ===== 底部按钮 ===== */ +.bottom-actions { padding: 32rpx 24rpx 0; display: flex; flex-direction: column; gap: 16rpx; } +.btn-match { text-align: center; padding: 26rpx 0; border-radius: 48rpx; font-size: 30rpx; font-weight: 700; letter-spacing: 2rpx; background: linear-gradient(135deg, #38bdac 0%, #2ca898 50%, #249e8c 100%); color: #fff; box-shadow: 0 8rpx 24rpx rgba(56,189,172,.3); } +.btn-vip { text-align: center; padding: 22rpx 0; border-radius: 48rpx; font-size: 26rpx; color: #f5a623; background: transparent; border: 1rpx solid rgba(245,166,35,.3); } + +/* ===== 状态 ===== */ +.state-wrap { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 60vh; gap: 16rpx; } +.state-txt { font-size: 28rpx; color: #4a5a6e; } +.state-emoji { font-size: 80rpx; } +.loading-dot { width: 48rpx; height: 48rpx; border-radius: 50%; border: 4rpx solid rgba(56,189,172,.2); border-top-color: #38bdac; animation: spin 1s linear infinite; } +@keyframes spin { to { transform: rotate(360deg); } } diff --git a/miniprogram/pages/my/my.js b/miniprogram/pages/my/my.js index 117e954c..becc5a0f 100644 --- a/miniprogram/pages/my/my.js +++ b/miniprogram/pages/my/my.js @@ -29,26 +29,17 @@ Page({ totalReadTime: 0, matchHistory: 0, - // Tab切换 - activeTab: 'overview', // overview | footprint - // 最近阅读 recentChapters: [], // 功能配置 - matchEnabled: false, // 找伙伴功能开关 + matchEnabled: false, - // 菜单列表 - menuList: [ - { id: 'scan', title: '扫一扫', icon: '📷', iconBg: 'gray' }, - { id: 'orders', title: '我的订单', icon: '📦', count: 0 }, - { id: 'referral', title: '推广中心', icon: '🎁', iconBg: 'gold', badge: '90%佣金' }, - { id: 'withdrawRecords', title: '提现记录', icon: '📋', iconBg: 'gray' }, - { id: 'about', title: '关于作者', icon: 'ℹ️', iconBg: 'brand' }, - { id: 'settings', title: '设置', icon: '⚙️', iconBg: 'gray' } - ], - - // 待确认收款(用户确认模式) + // VIP状态 + isVip: false, + vipExpireDate: '', + + // 待确认收款 pendingConfirmList: [], withdrawMchId: '', withdrawAppId: '', @@ -65,11 +56,7 @@ Page({ // 修改昵称弹窗 showNicknameModal: false, - editingNickname: '', - - // 扫一扫结果弹窗 - showScanResultModal: false, - scanResult: '' + editingNickname: '' }, onLoad() { @@ -95,48 +82,17 @@ Page({ this.initUserStatus() }, - // 加载功能配置 async loadFeatureConfig() { try { - const res = await app.request({ - url: '/api/miniprogram/config', - method: 'GET' - }) - - if (res && res.features) { - this.setData({ - matchEnabled: res.features.matchEnabled === true - }) - } + 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 }) } }, - // 登录后刷新购买状态(与 match/read 一致,避免其他页面用旧数据) - async refreshPurchaseStatus() { - const userId = app.globalData.userInfo?.id - if (!userId) return - try { - const res = await app.request(`/api/miniprogram/user/purchase-status?userId=${encodeURIComponent(userId)}`) - if (res.success && res.data) { - app.globalData.hasFullBook = res.data.hasFullBook || false - app.globalData.purchasedSections = res.data.purchasedSections || [] - app.globalData.sectionMidMap = res.data.sectionMidMap || {} - app.globalData.matchCount = res.data.matchCount ?? 0 - app.globalData.matchQuota = res.data.matchQuota || null - const userInfo = app.globalData.userInfo || {} - userInfo.hasFullBook = res.data.hasFullBook - userInfo.purchasedSections = res.data.purchasedSections - wx.setStorageSync('userInfo', userInfo) - } - } catch (e) { - console.log('[My] 刷新购买状态失败:', e) - } - }, - // 初始化用户状态 initUserStatus() { const { isLoggedIn, userInfo } = app.globalData @@ -144,8 +100,7 @@ Page({ if (isLoggedIn && userInfo) { const readIds = app.globalData.readSectionIds || [] const recentList = readIds.slice(-5).reverse().map(id => ({ - id, - mid: app.getSectionMid(id), + id: id, title: `章节 ${id}` })) @@ -169,6 +124,7 @@ Page({ }) this.loadMyEarnings() this.loadPendingConfirm() + this.loadVipStatus() } else { this.setData({ isLoggedIn: false, @@ -189,7 +145,7 @@ Page({ const userInfo = app.globalData.userInfo if (!app.globalData.isLoggedIn || !userInfo || !userInfo.id) return try { - const res = await app.request('/api/miniprogram/withdraw/pending-confirm?userId=' + userInfo.id) + 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, @@ -291,7 +247,7 @@ Page({ } const formatMoney = (num) => (typeof num === 'number' ? num.toFixed(2) : '0.00') try { - const res = await app.request('/api/miniprogram/earnings?userId=' + userInfo.id) + 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 @@ -566,7 +522,6 @@ Page({ try { const result = await app.login() if (result) { - await this.refreshPurchaseStatus() this.initUserStatus() this.setData({ showLoginModal: false, agreeProtocol: false }) wx.showToast({ title: '登录成功', icon: 'success' }) @@ -595,7 +550,6 @@ Page({ try { const result = await app.loginWithPhone(e.detail.code) if (result) { - await this.refreshPurchaseStatus() this.initUserStatus() this.setData({ showLoginModal: false }) wx.showToast({ title: '登录成功', icon: 'success' }) @@ -610,29 +564,15 @@ Page({ } }, - // 跳转编辑资料页 - goEditProfile() { - if (!this.data.isLoggedIn) { - this.showLogin() - return - } - wx.navigateTo({ url: '/pages/profile-edit/profile-edit' }) - }, - // 点击菜单 handleMenuTap(e) { const id = e.currentTarget.dataset.id - - if (id === 'scan') { - this.doScanCode() - return - } - + if (!this.data.isLoggedIn && id !== 'about') { this.showLogin() return } - + const routes = { orders: '/pages/purchases/purchases', referral: '/pages/referral/referral', @@ -640,52 +580,16 @@ Page({ about: '/pages/about/about', settings: '/pages/settings/settings' } - + if (routes[id]) { wx.navigateTo({ url: routes[id] }) } }, - // 扫一扫:调起扫码,展示解析值 - doScanCode() { - wx.scanCode({ - onlyFromCamera: false, - scanType: ['qrCode', 'barCode'], - success: (res) => { - const result = res.result || '' - this.setData({ - showScanResultModal: true, - scanResult: result - }) - }, - fail: (err) => { - if (err.errMsg && !err.errMsg.includes('cancel')) { - wx.showToast({ title: '扫码失败', icon: 'none' }) - } - } - }) - }, - - // 关闭扫码结果弹窗 - closeScanResultModal() { - this.setData({ showScanResultModal: false, scanResult: '' }) - }, - - // 复制扫码结果 - copyScanResult() { - const text = this.data.scanResult || '' - if (!text) return - wx.setClipboardData({ - data: text, - success: () => wx.showToast({ title: '已复制', icon: 'success' }) - }) - }, - + // 跳转到阅读页 goToRead(e) { const id = e.currentTarget.dataset.id - const mid = e.currentTarget.dataset.mid - const q = mid ? `mid=${mid}` : `id=${id}` - wx.navigateTo({ url: `/pages/read/read?${q}` }) + wx.navigateTo({ url: `/pages/read/read?id=${id}` }) }, // 跳转到目录 @@ -732,14 +636,80 @@ Page({ }) }, - // 阻止冒泡 - stopPropagation() {}, + // VIP状态查询 + async loadVipStatus() { + const userId = app.globalData.userInfo?.id + if (!userId) return + try { + const res = await app.request({ url: `/api/vip/status?userId=${userId}`, silent: true }) + if (res?.success) { + this.setData({ isVip: res.data?.isVip, vipExpireDate: res.data?.expireDate || '' }) + } + } catch (e) { console.log('[My] VIP查询失败', e) } + }, - onShareAppMessage() { - const ref = app.getMyReferralCode() - return { - title: 'Soul创业派对 - 我的', - path: ref ? `/pages/my/my?ref=${ref}` : '/pages/my/my' + // 头像点击:已登录弹出选项(改头像/进VIP) + onAvatarTap() { + if (!this.data.isLoggedIn) { this.showLogin(); return } + wx.showActionSheet({ + itemList: ['获取微信头像', '开通/管理VIP'], + success: (res) => { + if (res.tapIndex === 0) this.chooseAvatarFallback() + if (res.tapIndex === 1) this.goToVip() + } + }) + }, + + chooseAvatarFallback() { + wx.chooseMedia({ + count: 1, mediaType: ['image'], sourceType: ['album', 'camera'], + success: async (res) => { + const tempPath = res.tempFiles[0].tempFilePath + const userInfo = this.data.userInfo + userInfo.avatar = tempPath + this.setData({ userInfo }) + app.globalData.userInfo = userInfo + wx.setStorageSync('userInfo', userInfo) + try { + await app.request('/api/user/update', { method: 'POST', data: { userId: userInfo.id, avatar: tempPath } }) + } catch (e) { console.log('头像同步失败', e) } + wx.showToast({ title: '头像已更新', icon: 'success' }) + } + }) + }, + + goToVip() { + if (!this.data.isLoggedIn) { this.showLogin(); return } + wx.navigateTo({ url: '/pages/vip/vip' }) + }, + + 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 } - } + 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/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' }) + } + } + }) + }, + + // 阻止冒泡 + stopPropagation() {} }) diff --git a/miniprogram/pages/my/my.wxml b/miniprogram/pages/my/my.wxml index 5d243308..ac0d2042 100644 --- a/miniprogram/pages/my/my.wxml +++ b/miniprogram/pages/my/my.wxml @@ -47,37 +47,33 @@ - - + VIP + VIP + - - - - - - 创业伙伴 - - - - - - - ✏️ - 编辑资料 @@ -89,85 +85,44 @@ {{referralCount}} 推荐好友 - + {{pendingEarnings > 0 ? '¥' + pendingEarnings : '--'}} - 待领收益 + 我的收益 - - - - 待确认收款 - 审核已通过,点击下方按钮完成收款 - 暂无待确认的提现,审核通过后会出现在这里 - - - - - ¥{{item.amount}} - {{item.createdAt}} - - 确认收款 - - - - - - - 概览 - - 🐾 - 我的足迹 - - - - - - - + + + - + - - {{item.icon}} - - {{item.title}} + 📦 + 我的订单 + + + + + + + + ⚙️ + 设置 - {{item.count}}笔 - {{item.badge}} - - - 👁️ 阅读统计 - - + 📖 {{readCount}} @@ -199,7 +154,6 @@ wx:key="id" bindtap="goToRead" data-id="{{item.id}}" - data-mid="{{item.mid}}" > {{index + 1}} @@ -215,16 +169,16 @@ - - - - 👥 - 匹配记录 - - - 👥 - 暂无匹配记录 - 去匹配 → + + + + + ℹ️ + 关于作者 + + + + @@ -287,21 +241,6 @@ - - - - - - 扫码解析结果 - - {{scanResult}} - - 复制 - 关闭 - - - - diff --git a/miniprogram/pages/my/my.wxss b/miniprogram/pages/my/my.wxss index 79d5b2bf..bbdb848a 100644 --- a/miniprogram/pages/my/my.wxss +++ b/miniprogram/pages/my/my.wxss @@ -95,39 +95,6 @@ white-space: nowrap; } -/* 编辑资料入口 - 深色圆角按钮,与卡片统一 */ -.edit-profile-entry { - display: flex; - align-items: center; - justify-content: center; - flex-wrap: nowrap; - width: 100%; - min-height: 88rpx; - padding: 24rpx 32rpx; - margin-bottom: 24rpx; - box-sizing: border-box; - background: rgba(255, 255, 255, 0.06); - border: 2rpx solid rgba(0, 206, 209, 0.3); - border-radius: 20rpx; -} - -.edit-profile-entry:active { - background: rgba(0, 206, 209, 0.12); -} - -.edit-profile-icon { - font-size: 36rpx; - margin-right: 12rpx; - flex-shrink: 0; -} - -.edit-profile-text { - font-size: 28rpx; - font-weight: 500; - color: #00CED1; - white-space: nowrap; -} - .card-gradient { background: linear-gradient(135deg, #1c1c1e 0%, #2c2c2e 100%); border-radius: 32rpx; @@ -1270,27 +1237,137 @@ color: #fff; font-size: 26rpx; font-weight: 500; border-radius: 20rpx; } -/* ===== 扫一扫结果弹窗 ===== */ -.scan-result-modal .modal-close { top: 24rpx; right: 24rpx; } -.scan-result-header { margin-bottom: 24rpx; } -.scan-result-title { font-size: 32rpx; font-weight: 600; color: #fff; } -.scan-result-body { - max-height: 320rpx; - padding: 24rpx; - background: rgba(255,255,255,0.06); - border-radius: 16rpx; - margin-bottom: 24rpx; - word-break: break-all; -} -.scan-result-text { font-size: 26rpx; color: rgba(255,255,255,0.9); line-height: 1.5; } -.scan-result-actions { display: flex; gap: 24rpx; } -.scan-result-btn { - flex: 1; - padding: 24rpx; - text-align: center; - font-size: 28rpx; - color: rgba(255,255,255,0.9); - background: rgba(255,255,255,0.1); +/* ===== 收益面板(内嵌) ===== */ +.earnings-inline { + margin: 0 32rpx 24rpx; + padding: 24rpx 28rpx; + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); border-radius: 24rpx; + border: 2rpx solid rgba(0, 206, 209, 0.15); } -.scan-result-btn.primary { background: #00CED1; color: #000; } +.earnings-inline-row { + display: flex; + align-items: center; + gap: 16rpx; +} +.earnings-inline-item { + display: flex; + flex-direction: column; + gap: 4rpx; + flex: 1; +} +.earnings-inline-label { + font-size: 22rpx; + color: rgba(255,255,255,0.5); +} +.earnings-inline-val { + font-size: 36rpx; + font-weight: 700; + color: #fff; +} +.earnings-inline-divider { + width: 2rpx; + height: 48rpx; + background: rgba(255,255,255,0.1); + flex-shrink: 0; +} +.earnings-inline-btn { + padding: 12rpx 28rpx; + background: linear-gradient(90deg, #FFD700 0%, #FFA500 100%); + border-radius: 20rpx; + font-size: 26rpx; + font-weight: 600; + color: #000; + flex-shrink: 0; +} +.earnings-inline-refresh { + width: 48rpx; + height: 48rpx; + display: flex; + align-items: center; + justify-content: center; + font-size: 28rpx; + color: #00CED1; + flex-shrink: 0; +} +.pending-inline { + margin-top: 16rpx; + padding-top: 16rpx; + border-top: 1rpx solid rgba(255,255,255,0.08); +} +.pending-inline-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8rpx 0; +} +.pending-inline-text { + font-size: 24rpx; + color: #4CAF50; +} +.pending-inline-btn { + padding: 8rpx 20rpx; + background: #4CAF50; + color: #fff; + font-size: 22rpx; + border-radius: 12rpx; +} + +/* VIP头像标识 */ +.avatar-wrap { position: relative; } +.avatar-vip { border: 4rpx solid #FFD700 !important; box-shadow: 0 0 20rpx rgba(255,215,0,0.4); } +.vip-badge { position: absolute; bottom: -4rpx; right: -4rpx; background: linear-gradient(135deg, #FFD700, #FFA500); color: #000; font-size: 16rpx; font-weight: bold; padding: 2rpx 8rpx; border-radius: 10rpx; line-height: 1.4; } +.vip-badge-gray { background: rgba(255,255,255,0.2); color: rgba(255,255,255,0.5); } + +/* 会员权益小标签 */ +.vip-tags-row { + display: flex; + gap: 6rpx; + margin-left: 8rpx; + flex-shrink: 0; +} +.vip-tag-mini { + padding: 2rpx 10rpx; + font-size: 18rpx; + border-radius: 8rpx; + background: rgba(255,255,255,0.08); + color: rgba(255,255,255,0.3); + border: 1rpx solid rgba(255,255,255,0.1); +} +.vip-tag-active { + background: rgba(255,215,0,0.15); + color: #FFD700; + border-color: rgba(255,215,0,0.3); +} + +/* 阅读统计 */ +.stats-card { padding: 24rpx 28rpx; } +.stats-row { display: flex; gap: 16rpx; margin-top: 16rpx; } +.stat-box { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 6rpx; + padding: 20rpx 12rpx; + border-radius: 16rpx; + background: rgba(139,92,246,0.06); +} +.stat-icon { font-size: 32rpx; } +.stat-num { font-size: 36rpx; font-weight: bold; color: #fff; } +.stat-text { font-size: 22rpx; color: rgba(255,255,255,0.5); } +.pink-color { color: #ec4899; } + +/* 成为会员小按钮 */ +.become-vip-chip { + display: inline-flex; + align-items: center; + gap: 4rpx; + padding: 4rpx 14rpx; + border-radius: 20rpx; + background: linear-gradient(135deg, rgba(245,166,35,.15), rgba(245,166,35,.08)); + border: 1rpx solid rgba(245,166,35,.3); + margin-left: 8rpx; +} +.chip-star { font-size: 18rpx; } +.chip-text { font-size: 20rpx; color: #f5a623; font-weight: 600; white-space: nowrap; } diff --git a/miniprogram/pages/privacy/privacy.js b/miniprogram/pages/privacy/privacy.js index 0c95c06e..0cd665db 100644 --- a/miniprogram/pages/privacy/privacy.js +++ b/miniprogram/pages/privacy/privacy.js @@ -17,13 +17,5 @@ Page({ goBack() { wx.navigateBack() - }, - - onShareAppMessage() { - const ref = app.getMyReferralCode() - return { - title: 'Soul创业派对 - 隐私政策', - path: ref ? `/pages/privacy/privacy?ref=${ref}` : '/pages/privacy/privacy' - } } }) diff --git a/miniprogram/pages/purchases/purchases.js b/miniprogram/pages/purchases/purchases.js index eda2907b..c458ad74 100644 --- a/miniprogram/pages/purchases/purchases.js +++ b/miniprogram/pages/purchases/purchases.js @@ -15,31 +15,29 @@ Page({ this.loadOrders() }, - onShow() { - this.loadOrders() - }, - async loadOrders() { this.setData({ loading: true }) try { - let purchasedSections = app.globalData.purchasedSections || [] - let sectionMidMap = app.globalData.sectionMidMap || {} const userId = app.globalData.userInfo?.id if (userId) { - try { - const res = await app.request(`/api/miniprogram/user/purchase-status?userId=${encodeURIComponent(userId)}`) - if (res?.success && res.data) { - purchasedSections = res.data.purchasedSections || [] - sectionMidMap = res.data.sectionMidMap || {} - app.globalData.purchasedSections = purchasedSections - app.globalData.sectionMidMap = sectionMidMap - } - } catch (_) { /* 使用缓存 */ } + const res = await app.request(`/api/orders?userId=${userId}`) + if (res && res.success && res.data) { + const orders = (res.data || []).map(item => ({ + id: item.id || item.order_sn, + sectionId: item.product_id || item.section_id, + title: item.product_name || `章节 ${item.product_id || ''}`, + amount: item.amount || 0, + status: item.status || 'completed', + createTime: item.created_at ? new Date(item.created_at).toLocaleDateString() : '--' + })) + this.setData({ orders }) + return + } } + const purchasedSections = app.globalData.purchasedSections || [] const orders = purchasedSections.map((id, index) => ({ id: `order_${index}`, sectionId: id, - mid: sectionMidMap[id] || 0, title: `章节 ${id}`, amount: 1, status: 'completed', @@ -48,6 +46,13 @@ Page({ this.setData({ orders }) } catch (e) { console.error('加载订单失败:', e) + const purchasedSections = app.globalData.purchasedSections || [] + this.setData({ + orders: purchasedSections.map((id, i) => ({ + id: `order_${i}`, sectionId: id, title: `章节 ${id}`, amount: 1, status: 'completed', + createTime: new Date(Date.now() - i * 86400000).toLocaleDateString() + })) + }) } finally { this.setData({ loading: false }) } @@ -55,18 +60,8 @@ Page({ goToRead(e) { const id = e.currentTarget.dataset.id - const mid = e.currentTarget.dataset.mid - const q = mid ? `mid=${mid}` : `id=${id}` - wx.navigateTo({ url: `/pages/read/read?${q}` }) + wx.navigateTo({ url: `/pages/read/read?id=${id}` }) }, - goBack() { wx.navigateBack() }, - - onShareAppMessage() { - const ref = app.getMyReferralCode() - return { - title: 'Soul创业派对 - 我的订单', - path: ref ? `/pages/purchases/purchases?ref=${ref}` : '/pages/purchases/purchases' - } - } + goBack() { wx.navigateBack() } }) diff --git a/miniprogram/pages/read/read.js b/miniprogram/pages/read/read.js index 2deae32b..6e9fface 100644 --- a/miniprogram/pages/read/read.js +++ b/miniprogram/pages/read/read.js @@ -12,7 +12,6 @@ import accessManager from '../../utils/chapterAccessManager' import readingTracker from '../../utils/readingTracker' -const { buildScene, parseScene } = require('../../utils/scene.js') const app = getApp() @@ -57,12 +56,6 @@ Page({ sectionPrice: 1, fullBookPrice: 9.9, totalSections: 62, - // 好友优惠展示 - userDiscount: 5, - hasReferralDiscount: false, - showDiscountHint: false, - displaySectionPrice: 1, - displayFullBookPrice: 9.9, // 弹窗 showShareModal: false, @@ -73,41 +66,21 @@ Page({ isGeneratingPoster: false, // 免费章节 - freeIds: ['preface', 'epilogue', '1.1', 'appendix-1', 'appendix-2', 'appendix-3'], - - // 分享卡片图(canvas 生成后写入,供 onShareAppMessage 使用) - shareImagePath: '' + freeIds: ['preface', 'epilogue', '1.1', 'appendix-1', 'appendix-2', 'appendix-3'] }, async onLoad(options) { - // 官方以 options.scene 接收扫码参数(可同时带 mid/id + ref),与海报生成 buildScene 闭环 - const sceneStr = (options && options.scene) || '' - const parsed = parseScene(sceneStr) - const mid = options.mid ? parseInt(options.mid, 10) : (parsed.mid || app.globalData.initialSectionMid || 0) - const id = options.id || parsed.id || app.globalData.initialSectionId - const ref = options.ref || parsed.ref - if (app.globalData.initialSectionMid) delete app.globalData.initialSectionMid - if (app.globalData.initialSectionId) delete app.globalData.initialSectionId - - console.log('[Read] onLoad:', { options, sceneRaw: sceneStr || undefined, parsed, mid, id, ref }) - console.log('[Read] onLoad options:', options) - - if (!mid && !id) { - console.warn('[Read] 未获取到章节 mid/id,options:', options) - wx.showToast({ title: '章节参数缺失', icon: 'none' }) - this.setData({ accessState: 'error', loading: false }) - return - } - + const { id, ref } = options + this.setData({ statusBarHeight: app.globalData.statusBarHeight, navBarHeight: app.globalData.navBarHeight, - sectionId: '', // 加载后填充 - sectionMid: mid || null, + sectionId: id, loading: true, accessState: 'unknown' }) + // 处理推荐码绑定(异步不阻塞) if (ref) { console.log('[Read] 检测到推荐码:', ref) wx.setStorageSync('referral_code', ref) @@ -115,66 +88,35 @@ Page({ } try { - const userId = app.globalData.userInfo?.id - const [config, purchaseRes] = await Promise.all([ - accessManager.fetchLatestConfig(), - userId ? app.request(`/api/miniprogram/user/purchase-status?userId=${userId}`) : Promise.resolve(null) - ]) - const sectionPrice = config.prices?.section ?? 1 - const fullBookPrice = config.prices?.fullbook ?? 9.9 - const userDiscount = config.userDiscount ?? 5 - // 有推荐人 = ref/ referral_code 或 用户信息中有推荐人绑定 - const hasReferral = !!(wx.getStorageSync('referral_code') || ref || purchaseRes?.data?.hasReferrer) - const hasReferralDiscount = hasReferral && userDiscount > 0 - const showDiscountHint = userDiscount > 0 - const displaySectionPrice = hasReferralDiscount - ? Math.round(sectionPrice * (1 - userDiscount / 100) * 100) / 100 - : sectionPrice - const displayFullBookPrice = hasReferralDiscount - ? Math.round(fullBookPrice * (1 - userDiscount / 100) * 100) / 100 - : fullBookPrice + // 【标准流程】1. 拉取最新配置(免费列表、价格) + const config = await accessManager.fetchLatestConfig() this.setData({ freeIds: config.freeChapters, - sectionPrice, - fullBookPrice, - userDiscount, - hasReferralDiscount, - showDiscountHint: userDiscount > 0, - displaySectionPrice, - displayFullBookPrice, - purchasedCount: purchaseRes?.data?.purchasedSections?.length ?? this.data.purchasedCount ?? 0 + sectionPrice: config.prices?.section ?? 1, + fullBookPrice: config.prices?.fullbook ?? 9.9 }) - // 先拉取章节获取 id(mid 时必需;id 时可直接用) - let resolvedId = id - let prefetchedChapter = null - if (mid && !id) { - const chRes = await app.request(`/api/miniprogram/book/chapter/by-mid/${mid}`) - if (chRes && chRes.id) { - resolvedId = chRes.id - prefetchedChapter = chRes - } - } - this.setData({ sectionId: resolvedId }) - - const accessState = await accessManager.determineAccessState(resolvedId, config.freeChapters) + // 【标准流程】2. 确定权限状态 + const accessState = await accessManager.determineAccessState(id, config.freeChapters) const canAccess = accessManager.canAccessFullContent(accessState) this.setData({ accessState, canAccess, isLoggedIn: !!app.globalData.userInfo?.id, - showPaywall: !canAccess, - purchasedCount: purchaseRes?.data?.purchasedSections?.length ?? 0 + showPaywall: !canAccess }) - await this.loadContent(mid, resolvedId, accessState, prefetchedChapter) + // 【标准流程】3. 加载内容 + await this.loadContent(id, accessState) + // 【标准流程】4. 如果有权限,初始化阅读追踪 if (canAccess) { - readingTracker.init(resolvedId) + readingTracker.init(id) } - this.loadNavigation(resolvedId) + // 5. 加载导航 + this.loadNavigation(id) } catch (e) { console.error('[Read] 初始化失败:', e) @@ -217,9 +159,8 @@ Page({ }) }, - // 【重构】加载章节内容。mid 优先用 by-mid 接口,id 用旧接口;prefetched 避免重复请求 - async loadContent(mid, id, accessState, prefetched) { - console.log('[Read] loadContent 请求参数:', { mid, id, accessState: accessState, prefetched: !!prefetched }) + // 【重构】加载章节内容(专注于内容加载,权限判断已在 onLoad 中由 accessManager 完成) + async loadContent(id, accessState) { try { const section = this.getSectionInfo(id) const sectionPrice = this.data.sectionPrice ?? 1 @@ -227,43 +168,26 @@ Page({ section.price = sectionPrice } this.setData({ section }) - - let res = prefetched - if (!res) { - res = mid - ? await app.request(`/api/miniprogram/book/chapter/by-mid/${mid}`) - : await app.request(`/api/miniprogram/book/chapter/${id}`) - } + + // 从 API 获取内容 + const res = await app.request({ url: `/api/miniprogram/book/chapter/${id}`, silent: true }) if (res && res.content) { const lines = res.content.split('\n').filter(line => line.trim()) const previewCount = Math.ceil(lines.length * 0.2) - const updates = { + + this.setData({ content: res.content, contentParagraphs: lines, previewParagraphs: lines.slice(0, previewCount), partTitle: res.partTitle || '', chapterTitle: res.chapterTitle || '' - } - if (res.mid) updates.sectionMid = res.mid - this.setData(updates) + }) + + // 如果有权限,标记为已读 if (accessManager.canAccessFullContent(accessState)) { app.markSectionAsRead(id) } - // 始终用接口返回的 price/isFree 更新 section(不写死 1 元) - const section = this.data.section || {} - if (res.price !== undefined && res.price !== null) section.price = Number(res.price) - if (res.isFree !== undefined) section.isFree = !!res.isFree - // 0元即免费:接口返回 price 为 0 或 isFree 为 true 时,不展示付费墙 - const isFreeByPrice = res.price === 0 || res.price === '0' || Number(res.price) === 0 - const isFreeByFlag = res.isFree === true - if (isFreeByPrice || isFreeByFlag) { - this.setData({ section, showPaywall: false, canAccess: true, accessState: 'free' }) - app.markSectionAsRead(id) - } else { - this.setData({ section }) - } - setTimeout(() => this.drawShareCard(), 600) } } catch (e) { console.error('[Read] 加载内容失败:', e) @@ -307,12 +231,12 @@ Page({ return { id, title: appendixTitles[id] || '附录', isFree: true, price: 0 } } - // 普通章节:price 不写死,由 loadContent 从 config/接口 填充 + // 普通章节 return { id: id, title: this.getSectionTitle(id), isFree: id === '1.1', - price: undefined + price: 1 } }, @@ -335,6 +259,48 @@ Page({ } return titles[id] || `章节 ${id}` }, + + // 加载内容 - 三级降级方案:API → 本地缓存 → 备用API + async loadContent(id) { + const cacheKey = `chapter_${id}` + + // 1. 优先从API获取 + try { + const res = await this.fetchChapterWithTimeout(id, 5000) + if (res && res.content) { + this.setChapterContent(res) + // 成功后缓存到本地 + wx.setStorageSync(cacheKey, res) + console.log('[Read] 从API加载成功:', id) + return + } + } catch (e) { + console.warn('[Read] API加载失败,尝试本地缓存:', e.message) + } + + // 2. API失败,尝试从本地缓存读取 + try { + const cached = wx.getStorageSync(cacheKey) + if (cached && cached.content) { + this.setChapterContent(cached) + console.log('[Read] 从本地缓存加载成功:', id) + // 后台静默刷新 + this.silentRefresh(id) + return + } + } catch (e) { + console.warn('[Read] 本地缓存读取失败') + } + + // 3. 都失败,显示加载中并持续重试 + this.setData({ + contentParagraphs: ['章节内容加载中...', '正在尝试连接服务器,请稍候...'], + previewParagraphs: ['章节内容加载中...'] + }) + + // 延迟重试(最多3次) + this.retryLoadContent(id, 3) + }, // 带超时的章节请求 fetchChapterWithTimeout(id, timeout = 5000) { @@ -397,11 +363,9 @@ Page({ try { const res = await this.fetchChapterWithTimeout(id, 8000) if (res && res.content) { - this.setData({ section: this.getSectionInfo(id) }) this.setChapterContent(res) wx.setStorageSync(`chapter_${id}`, res) console.log('[Read] 重试成功:', id, '第', currentRetry + 1, '次') - setTimeout(() => this.drawShareCard(), 600) return } } catch (e) { @@ -412,7 +376,7 @@ Page({ }, - // 加载导航(prevSection/nextSection 含 mid 时用于跳转,否则用 id) + // 加载导航 loadNavigation(id) { const sectionOrder = [ 'preface', '1.1', '1.2', '1.3', '1.4', '1.5', @@ -428,19 +392,14 @@ Page({ '11.1', '11.2', '11.3', '11.4', '11.5', 'epilogue' ] - const bookData = app.globalData.bookData || [] - const idToMid = {} - bookData.forEach(ch => { - if (ch.id && ch.mid) idToMid[ch.id] = ch.mid - }) - + const currentIndex = sectionOrder.indexOf(id) const prevId = currentIndex > 0 ? sectionOrder[currentIndex - 1] : null const nextId = currentIndex < sectionOrder.length - 1 ? sectionOrder[currentIndex + 1] : null - + this.setData({ - prevSection: prevId ? { id: prevId, mid: idToMid[prevId], title: this.getSectionTitle(prevId) } : null, - nextSection: nextId ? { id: nextId, mid: idToMid[nextId], title: this.getSectionTitle(nextId) } : null + prevSection: prevId ? { id: prevId, title: this.getSectionTitle(prevId) } : null, + nextSection: nextId ? { id: nextId, title: this.getSectionTitle(nextId) } : null }) }, @@ -460,6 +419,21 @@ Page({ this.setData({ showShareModal: false }) }, + // 复制链接 + copyLink() { + const userInfo = app.globalData.userInfo + const referralCode = userInfo?.referralCode || '' + const shareUrl = `https://soul.quwanzhi.com/read/${this.data.sectionId}${referralCode ? '?ref=' + referralCode : ''}` + + wx.setClipboardData({ + data: shareUrl, + success: () => { + wx.showToast({ title: '链接已复制', icon: 'success' }) + this.setData({ showShareModal: false }) + } + }) + }, + // 复制分享文案(朋友圈风格) copyShareText() { const { section } = this.data @@ -480,91 +454,33 @@ Page({ }) }, - // 绘制分享卡片图(标题+正文摘要),生成后供 onShareAppMessage 使用 - drawShareCard() { - const { section, sectionId, contentParagraphs } = this.data - const title = section?.title || this.getSectionTitle(sectionId) || '精彩内容' - const raw = (contentParagraphs && contentParagraphs.length) - ? contentParagraphs.slice(0, 4).join(' ').replace(/\s+/g, ' ').trim() - : '' - const excerpt = raw.length > 120 ? raw.slice(0, 120) + '...' : (raw || '来自派对房的真实商业故事') - const ctx = wx.createCanvasContext('shareCardCanvas', this) - const w = 500 - const h = 400 - // 白底 - ctx.setFillStyle('#ffffff') - ctx.fillRect(0, 0, w, h) - // 顶部:平台名 - ctx.setFillStyle('#333333') - ctx.setFontSize(14) - ctx.fillText('📚 Soul 创业派对 - 真实商业故事', 24, 36) - // 深色内容区(模拟参考图效果) - const boxX = 24 - const boxY = 52 - const boxW = w - 48 - const boxH = 300 - ctx.setFillStyle('#2c2c2e') - ctx.fillRect(boxX, boxY, boxW, boxH) - // 文章标题(白字) - ctx.setFillStyle('#ffffff') - ctx.setFontSize(15) - const titleLines = this.wrapText(ctx, title.length > 50 ? title.slice(0, 50) + '...' : title, boxW - 32, 15) - let y = boxY + 28 - titleLines.slice(0, 2).forEach(line => { - ctx.fillText(line, boxX + 16, y) - y += 22 - }) - y += 8 - // 正文摘要(浅灰) - ctx.setFillStyle('rgba(255,255,255,0.88)') - ctx.setFontSize(12) - const excerptLines = this.wrapText(ctx, excerpt, boxW - 32, 12) - excerptLines.slice(0, 8).forEach(line => { - ctx.fillText(line, boxX + 16, y) - y += 20 - }) - // 底部:小程序标识 - ctx.setFillStyle('#999999') - ctx.setFontSize(11) - ctx.fillText('小程序', 24, h - 16) - ctx.draw(false, () => { - wx.canvasToTempFilePath({ - canvasId: 'shareCardCanvas', - fileType: 'png', - success: (res) => { - this.setData({ shareImagePath: res.tempFilePath }) - } - }, this) - }) - }, - - // 统一分享配置(底部「推荐给好友」与右下角分享按钮均走此配置,由 onShareAppMessage 使用) - getShareConfig() { - const { section, sectionId, sectionMid, shareImagePath } = this.data - const ref = app.getMyReferralCode() - const shareTitle = section?.title + // 分享到微信 - 自动带分享人ID + onShareAppMessage() { + const { section, sectionId } = this.data + const userInfo = app.globalData.userInfo + const referralCode = userInfo?.referralCode || wx.getStorageSync('referralCode') || '' + + // 分享标题优化 + const shareTitle = section?.title ? `📚 ${section.title.length > 20 ? section.title.slice(0, 20) + '...' : section.title}` : '📚 Soul创业派对 - 真实商业故事' - const q = sectionMid ? `mid=${sectionMid}` : `id=${sectionId}` - const path = ref ? `/pages/read/read?${q}&ref=${ref}` : `/pages/read/read?${q}` + return { title: shareTitle, - path, - imageUrl: shareImagePath || undefined + path: `/pages/read/read?id=${sectionId}${referralCode ? '&ref=' + referralCode : ''}`, + imageUrl: '/assets/share-cover.png' // 可配置分享封面图 } }, - - onShareAppMessage() { - return this.getShareConfig() - }, - + + // 分享到朋友圈 onShareTimeline() { - const { section, sectionId, sectionMid } = this.data - const ref = app.getMyReferralCode() - const q = sectionMid ? `mid=${sectionMid}` : `id=${sectionId}` + const { section, sectionId } = this.data + const userInfo = app.globalData.userInfo + const referralCode = userInfo?.referralCode || '' + return { title: `${section?.title || 'Soul创业派对'} - 来自派对房的真实故事`, - query: ref ? `${q}&ref=${ref}` : q + query: `id=${sectionId}${referralCode ? '&ref=' + referralCode : ''}` } }, @@ -643,34 +559,9 @@ Page({ // 1. 刷新用户购买状态(从 orders 表拉取最新) await accessManager.refreshUserPurchaseStatus() - // 2. 重新拉取免费列表、价格与用户推荐人状态 - const userId = app.globalData.userInfo?.id - const [config, purchaseRes] = await Promise.all([ - accessManager.fetchLatestConfig(), - userId ? app.request(`/api/miniprogram/user/purchase-status?userId=${userId}`) : Promise.resolve(null) - ]) - const sectionPrice = config.prices?.section ?? this.data.sectionPrice ?? 1 - const fullBookPrice = config.prices?.fullbook ?? this.data.fullBookPrice ?? 9.9 - const userDiscount = config.userDiscount ?? 5 - const hasReferral = !!(wx.getStorageSync('referral_code') || purchaseRes?.data?.hasReferrer) - const hasReferralDiscount = hasReferral && userDiscount > 0 - const displaySectionPrice = hasReferralDiscount - ? Math.round(sectionPrice * (1 - userDiscount / 100) * 100) / 100 - : sectionPrice - const displayFullBookPrice = hasReferralDiscount - ? Math.round(fullBookPrice * (1 - userDiscount / 100) * 100) / 100 - : fullBookPrice - this.setData({ - freeIds: config.freeChapters, - sectionPrice, - fullBookPrice, - userDiscount, - hasReferralDiscount, - showDiscountHint: userDiscount > 0, - displaySectionPrice, - displayFullBookPrice, - purchasedCount: purchaseRes?.data?.purchasedSections?.length ?? this.data.purchasedCount ?? 0 - }) + // 2. 重新拉取免费列表(极端情况:刚登录时当前章节可能改免费了) + const config = await accessManager.fetchLatestConfig() + this.setData({ freeIds: config.freeChapters }) // 3. 重新判断当前章节权限 const newAccessState = await accessManager.determineAccessState( @@ -688,7 +579,7 @@ Page({ // 4. 如果已解锁,重新加载内容并初始化阅读追踪 if (canAccess) { - await this.loadContent(this.data.sectionMid, this.data.sectionId, newAccessState, null) + await this.loadContent(this.data.sectionId, newAccessState) readingTracker.init(this.data.sectionId) } @@ -713,7 +604,7 @@ Page({ return } - const price = this.data.section?.price ?? this.data.sectionPrice ?? 1 + const price = this.data.section?.price || 1 console.log('[Pay] 开始支付流程:', { sectionId: this.data.sectionId, price }) wx.hideLoading() await this.processPayment('section', this.data.sectionId, price) @@ -759,7 +650,6 @@ Page({ // 更新本地购买状态 app.globalData.hasFullBook = checkRes.data.hasFullBook app.globalData.purchasedSections = checkRes.data.purchasedSections || [] - app.globalData.sectionMidMap = checkRes.data.sectionMidMap || {} // 检查是否已购买 if (type === 'section' && sectionId) { @@ -844,8 +734,7 @@ Page({ if (res.success && res.data?.payParams) { paymentData = res.data.payParams - paymentData._orderSn = res.data.orderSn - console.log('[Pay] 获取支付参数成功, orderSn:', res.data.orderSn) + console.log('[Pay] 获取支付参数成功:', paymentData) } else { throw new Error(res.error || res.message || '创建订单失败') } @@ -878,12 +767,11 @@ Page({ console.log('[Pay] 调起微信支付, paymentData:', paymentData) try { - const orderSn = paymentData._orderSn await this.callWechatPay(paymentData) - // 4. 轮询订单状态确认已支付后刷新并解锁(不依赖 PayNotify 回调时机) + // 4. 【标准流程】支付成功后刷新权限并解锁内容 console.log('[Pay] 微信支付成功!') - await this.onPaymentSuccess(orderSn) + await this.onPaymentSuccess() } catch (payErr) { console.error('[Pay] 微信支付调起失败:', payErr) @@ -919,34 +807,13 @@ Page({ } }, - // 轮询订单状态,确认 paid 后刷新权限并解锁 - async pollOrderUntilPaid(orderSn) { - const maxAttempts = 15 - const interval = 800 - for (let i = 0; i < maxAttempts; i++) { - try { - const r = await app.request(`/api/miniprogram/pay?orderSn=${encodeURIComponent(orderSn)}`, { method: 'GET', silent: true }) - if (r?.data?.status === 'paid') return true - } catch (_) {} - if (i < maxAttempts - 1) await this.sleep(interval) - } - return false - }, - // 【新增】支付成功后的标准处理流程 - async onPaymentSuccess(orderSn) { + async onPaymentSuccess() { wx.showLoading({ title: '确认购买中...', mask: true }) try { - // 1. 轮询订单状态直到已支付(GET pay 会主动同步本地订单,不依赖 PayNotify) - if (orderSn) { - const paid = await this.pollOrderUntilPaid(orderSn) - if (!paid) { - console.warn('[Pay] 轮询超时,仍尝试刷新') - } - } else { - await this.sleep(1500) - } + // 1. 等待服务端处理支付回调(1-2秒) + await this.sleep(2000) // 2. 刷新用户购买状态 await accessManager.refreshUserPurchaseStatus() @@ -976,7 +843,7 @@ Page({ }) // 4. 重新加载全文 - await this.loadContent(this.data.sectionMid, this.data.sectionId, newAccessState, null) + await this.loadContent(this.data.sectionId, newAccessState) // 5. 初始化阅读追踪 if (canAccess) { @@ -1013,10 +880,7 @@ Page({ // 更新全局购买状态 app.globalData.hasFullBook = res.data.hasFullBook app.globalData.purchasedSections = res.data.purchasedSections || [] - app.globalData.sectionMidMap = res.data.sectionMidMap || {} - app.globalData.matchCount = res.data.matchCount ?? 0 - app.globalData.matchQuota = res.data.matchQuota || null - + // 更新用户信息中的购买记录 const userInfo = app.globalData.userInfo || {} userInfo.hasFullBook = res.data.hasFullBook @@ -1026,8 +890,7 @@ Page({ console.log('[Pay] ✅ 购买状态已刷新:', { hasFullBook: res.data.hasFullBook, - purchasedCount: res.data.purchasedSections.length, - matchCount: res.data.matchCount + purchasedCount: res.data.purchasedSections.length }) } } catch (e) { @@ -1051,19 +914,17 @@ Page({ }) }, + // 跳转到上一篇 goToPrev() { - const s = this.data.prevSection - if (s) { - const q = s.mid ? `mid=${s.mid}` : `id=${s.id}` - wx.redirectTo({ url: `/pages/read/read?${q}` }) + if (this.data.prevSection) { + wx.redirectTo({ url: `/pages/read/read?id=${this.data.prevSection.id}` }) } }, + // 跳转到下一篇 goToNext() { - const s = this.data.nextSection - if (s) { - const q = s.mid ? `mid=${s.mid}` : `id=${s.id}` - wx.redirectTo({ url: `/pages/read/read?${q}` }) + if (this.data.nextSection) { + wx.redirectTo({ url: `/pages/read/read?id=${this.data.nextSection.id}` }) } }, @@ -1072,120 +933,134 @@ Page({ wx.navigateTo({ url: '/pages/referral/referral' }) }, - // 生成海报(弹窗先展示,延迟再绘制,确保 canvas 已渲染) + // 生成海报 async generatePoster() { wx.showLoading({ title: '生成中...' }) this.setData({ showPosterModal: true, isGeneratingPoster: true }) - const { section, contentParagraphs, sectionId, sectionMid } = this.data - const userInfo = app.globalData.userInfo - const userId = userInfo?.id || '' - const safeParagraphs = contentParagraphs || [] - - // 与 utils/scene 闭环:生成 scene 用 buildScene,扫码后用 parseScene 解析 - let qrcodeTempPath = null try { - const refVal = userId ? String(userId).slice(0, 12) : '' - const scene = buildScene({ - mid: sectionMid || undefined, - id: sectionMid ? undefined : (sectionId || ''), - ref: refVal || undefined - }) - const baseUrl = app.globalData.baseUrl || '' - const url = `${baseUrl}/api/miniprogram/qrcode/image?scene=${encodeURIComponent(scene)}&page=${encodeURIComponent('pages/read/read')}&width=280` - qrcodeTempPath = await new Promise((resolve) => { - wx.downloadFile({ - url, - success: (res) => resolve(res.statusCode === 200 ? res.tempFilePath : null), - fail: () => resolve(null) + const ctx = wx.createCanvasContext('posterCanvas', this) + const { section, contentParagraphs, sectionId } = this.data + const userInfo = app.globalData.userInfo + const userId = userInfo?.id || '' + + // 获取小程序码(带推荐人参数) + let qrcodeImage = null + try { + const scene = userId ? `id=${sectionId}&ref=${userId.slice(0,10)}` : `id=${sectionId}` + const qrRes = await app.request('/api/miniprogram/qrcode', { + method: 'POST', + data: { scene, page: 'pages/read/read', width: 280 } }) + if (qrRes.success && qrRes.image) { + qrcodeImage = qrRes.image + } + } catch (e) { + console.log('[Poster] 获取小程序码失败,使用占位符') + } + + // 海报尺寸 300x450 + const width = 300 + const height = 450 + + // 背景渐变 + const grd = ctx.createLinearGradient(0, 0, 0, height) + grd.addColorStop(0, '#1a1a2e') + grd.addColorStop(1, '#16213e') + ctx.setFillStyle(grd) + ctx.fillRect(0, 0, width, height) + + // 顶部装饰条 + ctx.setFillStyle('#00CED1') + ctx.fillRect(0, 0, width, 4) + + // 标题区域 + ctx.setFillStyle('#ffffff') + ctx.setFontSize(14) + ctx.fillText('📚 Soul创业派对', 20, 35) + + // 章节标题 + ctx.setFontSize(18) + ctx.setFillStyle('#ffffff') + const title = section?.title || '精彩内容' + const titleLines = this.wrapText(ctx, title, width - 40, 18) + let y = 70 + titleLines.forEach(line => { + ctx.fillText(line, 20, y) + y += 26 + }) + + // 分隔线 + ctx.setStrokeStyle('rgba(255,255,255,0.1)') + ctx.beginPath() + ctx.moveTo(20, y + 10) + ctx.lineTo(width - 20, y + 10) + ctx.stroke() + + // 内容摘要 + ctx.setFontSize(12) + ctx.setFillStyle('rgba(255,255,255,0.8)') + y += 30 + const summary = contentParagraphs.slice(0, 3).join(' ').slice(0, 150) + '...' + const summaryLines = this.wrapText(ctx, summary, width - 40, 12) + summaryLines.slice(0, 6).forEach(line => { + ctx.fillText(line, 20, y) + y += 20 + }) + + // 底部区域背景 + ctx.setFillStyle('rgba(0,206,209,0.1)') + ctx.fillRect(0, height - 100, width, 100) + + // 左侧提示文字 + ctx.setFillStyle('#ffffff') + ctx.setFontSize(13) + ctx.fillText('长按识别小程序码', 20, height - 60) + ctx.setFillStyle('rgba(255,255,255,0.6)') + ctx.setFontSize(11) + ctx.fillText('长按小程序码阅读全文', 20, height - 38) + + // 绘制小程序码或占位符 + const drawQRCode = () => { + return new Promise((resolve) => { + if (qrcodeImage) { + // 下载base64图片并绘制 + const fs = wx.getFileSystemManager() + const filePath = `${wx.env.USER_DATA_PATH}/qrcode_${Date.now()}.png` + const base64Data = qrcodeImage.replace(/^data:image\/\w+;base64,/, '') + + fs.writeFile({ + filePath, + data: base64Data, + encoding: 'base64', + success: () => { + ctx.drawImage(filePath, width - 85, height - 85, 70, 70) + resolve() + }, + fail: () => { + this.drawQRPlaceholder(ctx, width, height) + resolve() + } + }) + } else { + this.drawQRPlaceholder(ctx, width, height) + resolve() + } + }) + } + + await drawQRCode() + + ctx.draw(true, () => { + wx.hideLoading() + this.setData({ isGeneratingPoster: false }) }) } catch (e) { - console.log('[Poster] 获取小程序码失败,使用占位符') + console.error('生成海报失败:', e) + wx.hideLoading() + wx.showToast({ title: '生成失败', icon: 'none' }) + this.setData({ showPosterModal: false, isGeneratingPoster: false }) } - - const doDraw = () => { - try { - const ctx = wx.createCanvasContext('posterCanvas', this) - const width = 300 - const height = 450 - - const grd = ctx.createLinearGradient(0, 0, 0, height) - grd.addColorStop(0, '#1a1a2e') - grd.addColorStop(1, '#16213e') - ctx.setFillStyle(grd) - ctx.fillRect(0, 0, width, height) - - ctx.setFillStyle('#00CED1') - ctx.fillRect(0, 0, width, 4) - - ctx.setFillStyle('#ffffff') - ctx.setFontSize(14) - ctx.fillText('📚 Soul创业派对', 20, 35) - - ctx.setFontSize(18) - ctx.setFillStyle('#ffffff') - const title = section?.title || this.getSectionTitle(sectionId) || '精彩内容' - const titleLines = this.wrapText(ctx, title, width - 40, 18) - let y = 70 - titleLines.forEach(line => { - ctx.fillText(line, 20, y) - y += 26 - }) - - ctx.setStrokeStyle('rgba(255,255,255,0.1)') - ctx.beginPath() - ctx.moveTo(20, y + 10) - ctx.lineTo(width - 20, y + 10) - ctx.stroke() - - ctx.setFontSize(12) - ctx.setFillStyle('rgba(255,255,255,0.8)') - y += 30 - const summary = safeParagraphs.slice(0, 3).join(' ').replace(/\s+/g, ' ').trim().slice(0, 150) - const summaryText = summary ? summary + (summary.length >= 150 ? '...' : '') : '来自派对房的真实商业故事' - const summaryLines = this.wrapText(ctx, summaryText, width - 40, 12) - summaryLines.slice(0, 6).forEach(line => { - ctx.fillText(line, 20, y) - y += 20 - }) - - ctx.setFillStyle('rgba(0,206,209,0.1)') - ctx.fillRect(0, height - 100, width, 100) - - ctx.setFillStyle('#ffffff') - ctx.setFontSize(13) - ctx.fillText('长按识别小程序码', 20, height - 60) - ctx.setFillStyle('rgba(255,255,255,0.6)') - ctx.setFontSize(11) - ctx.fillText('长按小程序码阅读全文', 20, height - 38) - - const drawQRCode = () => { - return new Promise((resolve) => { - if (qrcodeTempPath) { - ctx.drawImage(qrcodeTempPath, width - 85, height - 85, 70, 70) - } else { - this.drawQRPlaceholder(ctx, width, height) - } - resolve() - }) - } - - drawQRCode().then(() => { - ctx.draw(true, () => { - wx.hideLoading() - this.setData({ isGeneratingPoster: false }) - }) - }) - } catch (e) { - console.error('生成海报失败:', e) - wx.hideLoading() - wx.showToast({ title: '生成失败', icon: 'none' }) - this.setData({ showPosterModal: false, isGeneratingPoster: false }) - } - } - - setTimeout(doDraw, 400) }, // 绘制小程序码占位符 @@ -1223,21 +1098,11 @@ Page({ this.setData({ showPosterModal: false }) }, - // 保存海报到相册:画布 300x450 兼容 iOS,导出 2 倍 600x900 提升清晰度(宽高比 2:3 不变) + // 保存海报到相册 savePoster() { - const width = 300 - const height = 450 - const exportScale = 2 wx.canvasToTempFilePath({ canvasId: 'posterCanvas', - destWidth: width * exportScale, - destHeight: height * exportScale, - fileType: 'png', success: (res) => { - if (!res.tempFilePath) { - wx.showToast({ title: '生成图片失败', icon: 'none' }) - return - } wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: () => { @@ -1245,25 +1110,25 @@ Page({ this.setData({ showPosterModal: false }) }, fail: (err) => { - console.error('[savePoster] saveImageToPhotosAlbum fail:', err) - if (err.errMsg && (err.errMsg.includes('auth deny') || err.errMsg.includes('authorize'))) { + if (err.errMsg.includes('auth deny')) { wx.showModal({ title: '提示', content: '需要相册权限才能保存海报', confirmText: '去设置', - success: (sres) => { - if (sres.confirm) wx.openSetting() + success: (res) => { + if (res.confirm) { + wx.openSetting() + } } }) } else { - wx.showToast({ title: err.errMsg || '保存失败', icon: 'none' }) + wx.showToast({ title: '保存失败', icon: 'none' }) } } }) }, - fail: (err) => { - console.error('[savePoster] canvasToTempFilePath fail:', err) - wx.showToast({ title: err.errMsg || '生成图片失败', icon: 'none' }) + fail: () => { + wx.showToast({ title: '生成图片失败', icon: 'none' }) } }, this) }, @@ -1286,33 +1151,9 @@ Page({ wx.showLoading({ title: '重试中...', mask: true }) try { - const userId = app.globalData.userInfo?.id - const [config, purchaseRes] = await Promise.all([ - accessManager.fetchLatestConfig(), - userId ? app.request(`/api/miniprogram/user/purchase-status?userId=${userId}`) : Promise.resolve(null) - ]) - const sectionPrice = config.prices?.section ?? this.data.sectionPrice ?? 1 - const fullBookPrice = config.prices?.fullbook ?? this.data.fullBookPrice ?? 9.9 - const userDiscount = config.userDiscount ?? 5 - const hasReferral = !!(wx.getStorageSync('referral_code') || purchaseRes?.data?.hasReferrer) - const hasReferralDiscount = hasReferral && userDiscount > 0 - const displaySectionPrice = hasReferralDiscount - ? Math.round(sectionPrice * (1 - userDiscount / 100) * 100) / 100 - : sectionPrice - const displayFullBookPrice = hasReferralDiscount - ? Math.round(fullBookPrice * (1 - userDiscount / 100) * 100) / 100 - : fullBookPrice - this.setData({ - freeIds: config.freeChapters, - sectionPrice, - fullBookPrice, - userDiscount, - hasReferralDiscount, - showDiscountHint: userDiscount > 0, - displaySectionPrice, - displayFullBookPrice, - purchasedCount: purchaseRes?.data?.purchasedSections?.length ?? this.data.purchasedCount ?? 0 - }) + // 重新拉取配置 + const config = await accessManager.fetchLatestConfig() + this.setData({ freeIds: config.freeChapters }) // 重新判断权限 const newAccessState = await accessManager.determineAccessState( @@ -1328,7 +1169,7 @@ Page({ }) // 重新加载内容 - await this.loadContent(this.data.sectionMid, this.data.sectionId, newAccessState, null) + await this.loadContent(this.data.sectionId, newAccessState) // 如果有权限,初始化阅读追踪 if (canAccess) { diff --git a/miniprogram/pages/read/read.wxml b/miniprogram/pages/read/read.wxml index 37375214..a17cc3b3 100644 --- a/miniprogram/pages/read/read.wxml +++ b/miniprogram/pages/read/read.wxml @@ -166,16 +166,7 @@ 购买本章 - - ¥{{section && section.price != null ? section.price : sectionPrice}} - ¥{{displaySectionPrice}} - 省{{userDiscount}}% - - - ¥{{section && section.price != null ? section.price : sectionPrice}} - 好友链接立省{{userDiscount}}% - - ¥{{section && section.price != null ? section.price : sectionPrice}} + ¥{{section && section.price != null ? section.price : sectionPrice}} @@ -185,9 +176,8 @@ 解锁全部 {{totalSections}} 章 - ¥{{fullBookPrice || 9.9}} - ¥{{hasReferralDiscount ? displayFullBookPrice : (fullBookPrice || 9.9)}} - {{hasReferralDiscount ? '省' + userDiscount + '%' : '省82%'}} + ¥{{fullBookPrice || 9.9}} + 省82% @@ -256,7 +246,7 @@ - + @@ -307,7 +297,4 @@ - - - diff --git a/miniprogram/pages/referral/referral.js b/miniprogram/pages/referral/referral.js index d5bd3a5c..b56fdc74 100644 --- a/miniprogram/pages/referral/referral.js +++ b/miniprogram/pages/referral/referral.js @@ -32,8 +32,7 @@ Page({ pendingEarnings: 0, // 待结算收益(保留兼容) shareRate: 90, // 分成比例(90%),从 referral/data 或 config 获取 minWithdrawAmount: 10, // 最低提现金额,从 referral/data 获取 - withdrawFee: 5, // 提现手续费%,从 referral/data 获取 - bindingDays: 30, // 绑定期天数,从 referral/data 获取 + bindingDays: 30, // 绑定期天数,从 referral/data 获取 userDiscount: 5, // 好友购买优惠%,从 referral/data 获取 hasWechatId: false, // 是否已绑定微信号(未绑定时需引导去设置) @@ -60,7 +59,6 @@ Page({ showPosterModal: false, isGeneratingPoster: false, posterQrSrc: '', - posterQrFilePath: '', posterReferralLink: '', posterNickname: '', posterNicknameInitial: '', @@ -200,7 +198,6 @@ Page({ pendingEarnings: formatMoney(realData?.pendingEarnings || 0), shareRate: realData?.shareRate ?? 90, minWithdrawAmount: minWithdrawAmount, - withdrawFee: realData?.withdrawFee ?? 5, bindingDays: realData?.bindingDays ?? 30, userDiscount: realData?.userDiscount ?? 5, @@ -286,6 +283,53 @@ Page({ }) }, + // 分享到朋友圈 - 1:1 迁移 Next.js 的 handleShareToWechat + shareToWechat() { + const { referralCode } = this.data + const referralLink = `https://soul.quwanzhi.com/?ref=${referralCode}` + + // 与 Next.js 完全相同的文案 + const shareText = `📖 推荐一本好书《一场SOUL的创业实验场》 + +这是卡若每天早上6-9点在Soul派对房分享的真实商业故事,55个真实案例,讲透创业的底层逻辑。 + +👉 点击阅读: ${referralLink} + +#创业 #商业思维 #Soul派对` + + wx.setClipboardData({ + data: shareText, + success: () => { + wx.showModal({ + title: '朋友圈文案已复制!', + content: '打开微信 → 发朋友圈 → 粘贴即可', + showCancel: false, + confirmText: '知道了' + }) + } + }) + }, + + // 更多分享方式 - 1:1 迁移 Next.js 的 handleShare + handleMoreShare() { + const { referralCode } = this.data + const referralLink = `https://soul.quwanzhi.com/?ref=${referralCode}` + + // 与 Next.js 完全相同的文案 + const shareText = `我正在读《一场SOUL的创业实验场》,每天6-9点的真实商业故事,推荐给你!${referralLink}` + + wx.setClipboardData({ + data: shareText, + success: () => { + wx.showToast({ + title: '分享文案已复制', + icon: 'success', + duration: 2000 + }) + } + }) + }, + // 生成推广海报 - 1:1 对齐 Next.js 设计 async generatePoster() { wx.showLoading({ title: '生成中...', mask: true }) @@ -308,18 +352,15 @@ Page({ }, }) - // 接口返回 { success, image: "data:image/png;base64,...", scene } - const imageData = res?.image || res?.data?.image - if (!res || !res.success || !imageData) { + if (!res || !res.success || !res.image) { console.error('[Poster] 生成小程序码失败:', res) - throw new Error(res?.error || res?.message || '生成小程序码失败') + throw new Error(res?.error || '生成小程序码失败') } - // 小程序 image 组件支持 base64 格式,直接使用;同时写入本地供预览用 - const base64Str = String(imageData).trim() + // 后端返回的是 data:image/png;base64,... 需要先写入本地临时文件,再作为 的 src + const base64Data = String(res.image).replace(/^data:image\/\w+;base64,/, '') const fs = wx.getFileSystemManager() const filePath = `${wx.env.USER_DATA_PATH}/poster_qrcode_${Date.now()}.png` - const base64Data = base64Str.replace(/^data:image\/\w+;base64,/, '') await new Promise((resolve, reject) => { fs.writeFile({ @@ -334,10 +375,10 @@ Page({ }) }) - // 优先用 base64 直接显示(兼容性更好);预览时用本地路径 + console.log('[Poster] 小程序码已保存到本地:', filePath) + this.setData({ - posterQrSrc: base64Str, - posterQrFilePath: filePath, + posterQrSrc: filePath, posterReferralLink: '', // 小程序版本不再使用 H5 链接 posterNickname: nickname, posterNicknameInitial: (nickname || '用').charAt(0), @@ -348,7 +389,7 @@ Page({ console.error('[Poster] 生成二维码失败:', e) wx.hideLoading() wx.showToast({ title: '生成失败', icon: 'none' }) - this.setData({ showPosterModal: false, isGeneratingPoster: false, posterQrSrc: '', posterQrFilePath: '', posterReferralLink: '' }) + this.setData({ showPosterModal: false, isGeneratingPoster: false, posterQrSrc: '', posterReferralLink: '' }) } }, @@ -529,15 +570,56 @@ Page({ // 预览二维码 previewPosterQr() { - const { posterQrSrc, posterQrFilePath } = this.data - const url = posterQrFilePath || posterQrSrc - if (!url) return - wx.previewImage({ urls: [url] }) + const { posterQrSrc } = this.data + if (!posterQrSrc) return + wx.previewImage({ urls: [posterQrSrc] }) }, // 阻止冒泡 stopPropagation() {}, + // 分享到朋友圈 - 随机文案 + shareToMoments() { + // 10条随机文案,基于书的内容 + const shareTexts = [ + `🔥 在派对房里听到的真实故事,比虚构的小说精彩100倍!\n\n电动车出租月入5万、私域一年赚1000万、一个人的公司月入10万...\n\n62个真实案例,搜"Soul创业派对"小程序看全部!\n\n#创业 #私域 #商业`, + + `💡 今天终于明白:会赚钱的人,都在用"流量杠杆"\n\n抖音、Soul、飞书...同一套内容,撬动不同平台的流量。\n\n《Soul创业派对》里的实战方法,受用终身!\n\n#流量 #副业 #创业派对`, + + `📚 一个70后大健康私域,一个月150万流水是怎么做到的?\n\n答案在《Soul创业派对》第9章,全是干货。\n\n搜小程序"Soul创业派对",我在里面等你\n\n#大健康 #私域运营 #真实案例`, + + `🎯 "分钱不是分你的钱,是分不属于对方的钱"\n\n这句话改变了我对商业合作的认知。\n\n推荐《Soul创业派对》,创业者必读!\n\n#云阿米巴 #商业思维 #创业`, + + `✨ 资源整合高手的社交方法论,在派对房里学到了\n\n"先让对方赚到钱,自己才能长久赚钱"\n\n这本《Soul创业派对》,每章都是实战经验\n\n#资源整合 #社交 #创业故事`, + + `🚀 AI工具推广:一个隐藏的高利润赛道\n\n客单价高、复购率高、需求旺盛...\n\n《Soul创业派对》里的商业机会,你发现了吗?\n\n#AI #副业 #商业机会`, + + `💰 美业整合:一个人的公司如何月入十万?\n\n不开店、不囤货、轻资产运营...\n\n《Soul创业派对》告诉你答案!\n\n#美业 #轻创业 #月入十万`, + + `🌟 3000万流水是怎么跑出来的?\n\n不是靠运气,是靠系统。\n\n《Soul创业派对》里的电商底层逻辑,值得反复看\n\n#电商 #创业 #商业系统`, + + `📖 "人与人之间的关系,归根结底就三个东西:利益、情感、价值观"\n\n在派对房里聊出的金句,都在《Soul创业派对》里\n\n#人性 #商业 #创业派对`, + + `🔔 未来职业的三个方向:技术型、资源型、服务型\n\n你属于哪一种?\n\n《Soul创业派对》帮你找到答案!\n\n#职业规划 #创业 #未来` + ] + + // 随机选择一条文案 + const randomIndex = Math.floor(Math.random() * shareTexts.length) + const shareText = shareTexts[randomIndex] + + wx.setClipboardData({ + data: shareText, + success: () => { + wx.showModal({ + title: '文案已复制', + content: '请打开微信朋友圈,粘贴分享文案,配合推广海报一起发布效果更佳!\n\n再次点击可获取新的随机文案', + showCancel: false, + confirmText: '去发朋友圈' + }) + } + }) + }, + // 提现 - 直接到微信零钱 async handleWithdraw() { const availableEarnings = this.data.availableEarningsNum || 0 @@ -548,10 +630,7 @@ Page({ wx.showToast({ title: '暂无可提现收益', icon: 'none' }) return } - if (availableEarnings < minWithdrawAmount) { - wx.showToast({ title: `满${minWithdrawAmount}元可提现`, icon: 'none' }) - return - } + // 任意金额可提现,不再设最低限额 // 未绑定微信号时引导去设置 if (!hasWechatId) { @@ -567,16 +646,9 @@ Page({ return } - const withdrawFee = this.data.withdrawFee ?? 5 - const actualAmount = withdrawFee > 0 - ? Math.round(availableEarnings * (1 - withdrawFee / 100) * 100) / 100 - : availableEarnings - const feeText = withdrawFee > 0 - ? `\n扣除 ${withdrawFee}% 手续费后,实际到账 ¥${actualAmount.toFixed(2)}` - : '' wx.showModal({ title: '确认提现', - content: `申请提现 ¥${availableEarnings.toFixed(2)} 到您的微信零钱${feeText}`, + content: `将提现 ¥${availableEarnings.toFixed(2)} 到您的微信零钱`, confirmText: '立即提现', success: async (res) => { if (!res.confirm) return @@ -805,16 +877,27 @@ Page({ }) }, - // 分享 - 带自己的邀请码(与 app.getMyReferralCode 一致) + // 分享 - 带推荐码 onShareAppMessage() { - const app = getApp() - const ref = app.getMyReferralCode() || this.data.referralCode + console.log('[Referral] 分享给好友,推荐码:', this.data.referralCode) return { title: 'Soul创业派对 - 来自派对房的真实商业故事', - path: ref ? `/pages/index/index?ref=${ref}` : '/pages/index/index' + path: `/pages/index/index?ref=${this.data.referralCode}` + // 不设置 imageUrl,使用小程序默认截图 + // 如需自定义图片,请将图片放在 /assets/ 目录并配置路径 } }, + // 分享到朋友圈 + onShareTimeline() { + console.log('[Referral] 分享到朋友圈,推荐码:', this.data.referralCode) + return { + title: `Soul创业派对 - 62个真实商业案例`, + query: `ref=${this.data.referralCode}` + // 不设置 imageUrl,使用小程序默认截图 + } + }, + goBack() { wx.navigateBack() }, diff --git a/miniprogram/pages/referral/referral.wxml b/miniprogram/pages/referral/referral.wxml index a300cee5..38c587aa 100644 --- a/miniprogram/pages/referral/referral.wxml +++ b/miniprogram/pages/referral/referral.wxml @@ -6,9 +6,6 @@ - - - 分销中心 @@ -46,10 +43,9 @@ 累计: ¥{{totalCommission}} | 待审核: ¥{{pendingWithdrawAmount}} - - {{availableEarningsNum < minWithdrawAmount ? '满' + minWithdrawAmount + '元可提现' : !hasWechatId ? '请先绑定微信号' : '申请提现 ¥' + availableEarnings}} + + {{availableEarningsNum <= 0 ? '暂无可提现' : !hasWechatId ? '请先绑定微信号' : '申请提现 ¥' + availableEarnings}} - 提现将扣除 {{withdrawFee}}% 手续费 为便于提现到账,请先到「设置」中绑定微信号 查看提现记录 @@ -177,6 +173,28 @@ + + + + @@ -215,7 +233,14 @@