/** * Soul创业派对 - 阅读页 * 开发: 卡若 * 技术支持: 存客宝 */ const app = getApp() Page({ data: { // 系统信息 statusBarHeight: 44, navBarHeight: 88, // 章节信息 sectionId: '', section: null, partTitle: '', chapterTitle: '', // 内容 content: '', previewContent: '', contentParagraphs: [], previewParagraphs: [], loading: true, // 用户状态 isLoggedIn: false, hasFullBook: false, canAccess: false, purchasedCount: 0, // 阅读进度 readingProgress: 0, showPaywall: false, // 上一篇/下一篇 prevSection: null, nextSection: null, // 价格 sectionPrice: 1, fullBookPrice: 9.9, totalSections: 62, // 弹窗 showShareModal: false, showLoginModal: false, showPosterModal: false, isPaying: false, isGeneratingPoster: false, // 免费章节 freeIds: ['preface', 'epilogue', '1.1', 'appendix-1', 'appendix-2', 'appendix-3'] }, onLoad(options) { const { id, ref } = options this.setData({ statusBarHeight: app.globalData.statusBarHeight, navBarHeight: app.globalData.navBarHeight, sectionId: id }) // 处理推荐码绑定 if (ref) { console.log('[Read] 检测到推荐码:', ref) wx.setStorageSync('referral_code', ref) app.handleReferralCode({ query: { ref } }) } this.initSection(id) }, onPageScroll(e) { // 计算阅读进度 const query = wx.createSelectorQuery() query.select('.page').boundingClientRect() query.exec((res) => { if (res[0]) { const scrollTop = e.scrollTop const pageHeight = res[0].height - this.data.statusBarHeight - 200 const progress = pageHeight > 0 ? Math.min((scrollTop / pageHeight) * 100, 100) : 0 this.setData({ readingProgress: progress }) } }) }, // 初始化章节 async initSection(id) { this.setData({ loading: true }) try { // 模拟获取章节数据 const section = this.getSectionInfo(id) const { isLoggedIn, hasFullBook, purchasedSections } = app.globalData const isFree = this.data.freeIds.includes(id) const isPurchased = hasFullBook || (purchasedSections && purchasedSections.includes(id)) const canAccess = isFree || isPurchased const purchasedCount = purchasedSections?.length || 0 this.setData({ section, isLoggedIn, hasFullBook, canAccess, purchasedCount, showPaywall: !canAccess }) // 加载内容 await this.loadContent(id) // 获取上一篇/下一篇 this.loadNavigation(id) } catch (e) { console.error('初始化章节失败:', e) wx.showToast({ title: '加载失败', icon: 'none' }) } finally { this.setData({ loading: false }) } }, // 获取章节信息 getSectionInfo(id) { // 特殊章节 if (id === 'preface') { return { id: 'preface', title: '为什么我每天早上6点在Soul开播?', isFree: true, price: 0 } } if (id === 'epilogue') { return { id: 'epilogue', title: '这本书的真实目的', isFree: true, price: 0 } } if (id.startsWith('appendix')) { const appendixTitles = { 'appendix-1': 'Soul派对房精选对话', 'appendix-2': '创业者自检清单', 'appendix-3': '本书提到的工具和资源' } return { id, title: appendixTitles[id] || '附录', isFree: true, price: 0 } } // 普通章节 return { id: id, title: this.getSectionTitle(id), isFree: id === '1.1', price: 1 } }, // 获取章节标题 getSectionTitle(id) { const titles = { '1.1': '荷包:电动车出租的被动收入模式', '1.2': '老墨:资源整合高手的社交方法', '1.3': '笑声背后的MBTI', '1.4': '人性的三角结构:利益、情感、价值观', '1.5': '沟通差的问题:为什么你说的别人听不懂', '2.1': '相亲故事:你以为找的是人,实际是在找模式', '2.2': '找工作迷茫者:为什么简历解决不了人生', '2.3': '撸运费险:小钱困住大脑的真实心理', '2.4': '游戏上瘾的年轻人:不是游戏吸引他,是生活没吸引力', '2.5': '健康焦虑(我的糖尿病经历):疾病是人生的第一次清醒', '3.1': '3000万流水如何跑出来(退税模式解析)', '8.1': '流量杠杆:抖音、Soul、飞书', '9.14': '大健康私域:一个月150万的70后' } return titles[id] || `章节 ${id}` }, // 加载内容 - 从真实API读取章节内容 async loadContent(id) { try { // 从API获取真实章节内容 const res = await app.request(`/api/book/chapter/${id}`) if (res && res.content) { const lines = res.content.split('\n').filter(line => line.trim()) const previewCount = Math.ceil(lines.length * 0.2) this.setData({ content: res.content, previewContent: lines.slice(0, previewCount).join('\n'), contentParagraphs: lines, previewParagraphs: lines.slice(0, previewCount), partTitle: res.partTitle || '', chapterTitle: res.chapterTitle || '' }) console.log('[Read] 成功加载章节内容:', id) return } } catch (e) { console.error('[Read] API加载章节失败:', e.message) } // API失败时显示提示,不使用假内容 this.setData({ content: '章节内容加载中,请稍候...', previewContent: '章节内容加载中,请稍候...', contentParagraphs: ['章节内容加载中,请稍候...', '如果长时间无法加载,请检查网络连接后刷新页面。'], previewParagraphs: ['章节内容加载中,请稍候...'] }) // 延迟重试一次 setTimeout(async () => { try { const res = await app.request(`/api/book/chapter/${id}`) if (res && res.content) { const lines = res.content.split('\n').filter(line => line.trim()) const previewCount = Math.ceil(lines.length * 0.2) this.setData({ content: res.content, previewContent: lines.slice(0, previewCount).join('\n'), contentParagraphs: lines, previewParagraphs: lines.slice(0, previewCount), partTitle: res.partTitle || '', chapterTitle: res.chapterTitle || '' }) } } catch (e) { console.error('[Read] 重试加载失败:', e.message) } }, 2000) }, // 加载导航 loadNavigation(id) { const sectionOrder = [ 'preface', '1.1', '1.2', '1.3', '1.4', '1.5', '2.1', '2.2', '2.3', '2.4', '2.5', '3.1', '3.2', '3.3', '3.4', '4.1', '4.2', '4.3', '4.4', '4.5', '5.1', '5.2', '5.3', '5.4', '5.5', '6.1', '6.2', '6.3', '6.4', '7.1', '7.2', '7.3', '7.4', '7.5', '8.1', '8.2', '8.3', '8.4', '8.5', '8.6', '9.1', '9.2', '9.3', '9.4', '9.5', '9.6', '9.7', '9.8', '9.9', '9.10', '9.11', '9.12', '9.13', '9.14', '10.1', '10.2', '10.3', '10.4', '11.1', '11.2', '11.3', '11.4', '11.5', 'epilogue' ] 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, title: this.getSectionTitle(prevId) } : null, nextSection: nextId ? { id: nextId, title: this.getSectionTitle(nextId) } : null }) }, // 返回 goBack() { wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/chapters/chapters' }) }) }, // 分享弹窗 showShare() { this.setData({ showShareModal: true }) }, closeShareModal() { 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 }) } }) }, // 分享到微信 - 自动带分享人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创业派对 - 真实商业故事' return { title: shareTitle, path: `/pages/read/read?id=${sectionId}${referralCode ? '&ref=' + referralCode : ''}`, imageUrl: '/assets/share-cover.png' // 可配置分享封面图 } }, // 分享到朋友圈 onShareTimeline() { const { section, sectionId } = this.data const userInfo = app.globalData.userInfo const referralCode = userInfo?.referralCode || '' return { title: `${section?.title || 'Soul创业派对'} - 来自派对房的真实故事`, query: `id=${sectionId}${referralCode ? '&ref=' + referralCode : ''}` } }, // 显示登录弹窗 showLoginModal() { this.setData({ showLoginModal: true }) }, closeLoginModal() { this.setData({ showLoginModal: false }) }, // 微信登录 async handleWechatLogin() { try { const result = await app.login() if (result) { this.setData({ showLoginModal: false }) this.initSection(this.data.sectionId) wx.showToast({ title: '登录成功', icon: 'success' }) } } catch (e) { wx.showToast({ title: '登录失败', icon: 'none' }) } }, // 手机号登录 async handlePhoneLogin(e) { if (!e.detail.code) { return this.handleWechatLogin() } try { const result = await app.loginWithPhone(e.detail.code) if (result) { this.setData({ showLoginModal: false }) this.initSection(this.data.sectionId) wx.showToast({ title: '登录成功', icon: 'success' }) } } catch (e) { wx.showToast({ title: '登录失败', icon: 'none' }) } }, // 购买章节 - 直接调起支付 async handlePurchaseSection() { if (!this.data.isLoggedIn) { this.setData({ showLoginModal: true }) return } await this.processPayment('section', this.data.sectionId, this.data.section.price) }, // 购买全书 - 直接调起支付 async handlePurchaseFullBook() { if (!this.data.isLoggedIn) { this.setData({ showLoginModal: true }) return } await this.processPayment('fullbook', null, this.data.fullBookPrice) }, // 处理支付 - 调用真实微信支付接口 async processPayment(type, sectionId, amount) { // 检查是否已购买(避免重复购买) if (type === 'section' && sectionId) { const purchasedSections = app.globalData.purchasedSections || [] if (purchasedSections.includes(sectionId)) { wx.showToast({ title: '已购买过此章节', icon: 'none' }) return } } if (type === 'fullbook' && app.globalData.hasFullBook) { wx.showToast({ title: '已购买全书', icon: 'none' }) return } this.setData({ isPaying: true }) try { // 1. 先获取openId (支付必需) let openId = app.globalData.openId || wx.getStorageSync('openId') if (!openId) { console.log('[Pay] 需要先获取openId,尝试静默获取') openId = await app.getOpenId() if (!openId) { // openId获取失败,但已登录用户可以使用用户ID替代 if (app.globalData.isLoggedIn && app.globalData.userInfo?.id) { console.log('[Pay] 使用用户ID作为替代') openId = app.globalData.userInfo.id } else { wx.showModal({ title: '提示', content: '需要登录后才能支付,请先登录', showCancel: false }) this.setData({ showLoginModal: true, isPaying: false }) return } } } console.log('[Pay] 开始创建订单:', { type, sectionId, amount, openId: openId.slice(0, 10) + '...' }) // 2. 调用后端创建预支付订单 let paymentData = null try { const res = await app.request('/api/miniprogram/pay', { method: 'POST', data: { openId, productType: type, productId: sectionId, amount, description: type === 'fullbook' ? '《一场Soul的创业实验》全书' : `章节-${sectionId}`, userId: app.globalData.userInfo?.id || '' } }) console.log('[Pay] 创建订单响应:', res) if (res.success && res.data?.payParams) { paymentData = res.data.payParams console.log('[Pay] 获取支付参数成功') } else { throw new Error(res.error || '创建订单失败') } } catch (apiError) { console.error('[Pay] API创建订单失败:', apiError.message) wx.showToast({ title: '支付服务暂时不可用,请稍后重试', icon: 'none', duration: 3000 }) this.setData({ isPaying: false }) return } // 3. 调用微信支付 console.log('[Pay] 调起微信支付') await this.callWechatPay(paymentData) // 4. 支付成功,更新本地数据 this.mockPaymentSuccess(type, sectionId) wx.showToast({ title: '购买成功', icon: 'success' }) // 5. 刷新页面 this.initSection(this.data.sectionId) } catch (e) { console.error('[Pay] 支付失败:', e) if (e.errMsg && e.errMsg.includes('cancel')) { wx.showToast({ title: '已取消支付', icon: 'none' }) } else { wx.showToast({ title: e.errMsg || '支付失败', icon: 'none' }) } } finally { this.setData({ isPaying: false }) } }, // 模拟支付成功 mockPaymentSuccess(type, sectionId) { if (type === 'fullbook') { app.globalData.hasFullBook = true const userInfo = app.globalData.userInfo || {} userInfo.hasFullBook = true app.globalData.userInfo = userInfo wx.setStorageSync('userInfo', userInfo) } else if (sectionId) { const purchasedSections = app.globalData.purchasedSections || [] if (!purchasedSections.includes(sectionId)) { purchasedSections.push(sectionId) app.globalData.purchasedSections = purchasedSections const userInfo = app.globalData.userInfo || {} userInfo.purchasedSections = purchasedSections app.globalData.userInfo = userInfo wx.setStorageSync('userInfo', userInfo) } } }, // 调用微信支付 callWechatPay(paymentData) { return new Promise((resolve, reject) => { wx.requestPayment({ timeStamp: paymentData.timeStamp, nonceStr: paymentData.nonceStr, package: paymentData.package, signType: paymentData.signType || 'MD5', paySign: paymentData.paySign, success: resolve, fail: reject }) }) }, // 跳转到上一篇 goToPrev() { if (this.data.prevSection) { wx.redirectTo({ url: `/pages/read/read?id=${this.data.prevSection.id}` }) } }, // 跳转到下一篇 goToNext() { if (this.data.nextSection) { wx.redirectTo({ url: `/pages/read/read?id=${this.data.nextSection.id}` }) } }, // 跳转到推广中心 goToReferral() { wx.navigateTo({ url: '/pages/referral/referral' }) }, // 生成海报 async generatePoster() { wx.showLoading({ title: '生成中...' }) this.setData({ showPosterModal: true, isGeneratingPoster: true }) try { const ctx = wx.createCanvasContext('posterCanvas', this) const { section, contentParagraphs } = this.data const userInfo = app.globalData.userInfo const referralCode = userInfo?.referralCode || 'SOUL' // 海报尺寸 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 - 120, width, 120) // 小程序码占位(实际需要获取小程序码图片) ctx.setFillStyle('#ffffff') ctx.beginPath() ctx.arc(width - 55, height - 60, 35, 0, Math.PI * 2) ctx.fill() ctx.setFillStyle('#00CED1') ctx.setFontSize(10) ctx.fillText('扫码阅读', width - 72, height - 58) // 邀请信息 ctx.setFillStyle('#ffffff') ctx.setFontSize(12) ctx.fillText('长按识别 · 阅读全文', 20, height - 70) ctx.setFillStyle('#FFD700') ctx.setFontSize(11) ctx.fillText(`邀请码: ${referralCode}`, 20, height - 50) ctx.setFillStyle('rgba(255,255,255,0.6)') ctx.setFontSize(10) ctx.fillText('好友购买你获90%收益', 20, height - 32) 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 }) } }, // 文字换行处理 wrapText(ctx, text, maxWidth, fontSize) { const lines = [] let line = '' for (let i = 0; i < text.length; i++) { const testLine = line + text[i] const metrics = ctx.measureText(testLine) if (metrics.width > maxWidth && line) { lines.push(line) line = text[i] } else { line = testLine } } if (line) lines.push(line) return lines }, // 关闭海报弹窗 closePosterModal() { this.setData({ showPosterModal: false }) }, // 保存海报到相册 savePoster() { wx.canvasToTempFilePath({ canvasId: 'posterCanvas', success: (res) => { wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: () => { wx.showToast({ title: '已保存到相册', icon: 'success' }) this.setData({ showPosterModal: false }) }, fail: (err) => { if (err.errMsg.includes('auth deny')) { wx.showModal({ title: '提示', content: '需要相册权限才能保存海报', confirmText: '去设置', success: (res) => { if (res.confirm) { wx.openSetting() } } }) } else { wx.showToast({ title: '保存失败', icon: 'none' }) } } }) }, fail: () => { wx.showToast({ title: '生成图片失败', icon: 'none' }) } }, this) }, // 阻止冒泡 stopPropagation() {} })