/** * 阅读进度追踪器 * 记录阅读进度、时长、是否读完,支持断点续读 */ const app = getApp() class ReadingTracker { constructor() { this.activeTracker = null this.reportInterval = null } /** * 初始化阅读追踪 */ init(sectionId) { // 清理旧的追踪器 this.cleanup() this.activeTracker = { sectionId, startTime: Date.now(), lastScrollTime: Date.now(), totalDuration: 0, maxProgress: 0, lastPosition: 0, isCompleted: false, completedAt: null, scrollTimer: null } console.log('[ReadingTracker] 初始化追踪:', sectionId) // 恢复上次阅读位置 this.restoreLastPosition(sectionId) // 开始定期上报(每30秒) this.startProgressReport() // 立即上报一次「打开/点击」,确保内容管理后台的「点击」数据有记录(与 reading_progress 表直接捆绑) setTimeout(() => this.reportProgressToServer(false), 0) } /** * 恢复上次阅读位置(断点续读) */ restoreLastPosition(sectionId) { try { const progressData = wx.getStorageSync('reading_progress') || {} const lastProgress = progressData[sectionId] if (lastProgress && lastProgress.lastPosition > 100) { setTimeout(() => { wx.pageScrollTo({ scrollTop: lastProgress.lastPosition, duration: 300 }) wx.showToast({ title: `继续阅读 (${lastProgress.progress}%)`, icon: 'none', duration: 2000 }) }, 500) } } catch (e) { console.warn('[ReadingTracker] 恢复位置失败:', e) } } /** * 更新阅读进度(由页面滚动事件调用) */ updateProgress(scrollInfo) { if (!this.activeTracker) return const { scrollTop, scrollHeight, clientHeight } = scrollInfo const totalScrollable = scrollHeight - clientHeight if (totalScrollable <= 0) return const progress = Math.min(100, Math.round((scrollTop / totalScrollable) * 100)) // 更新最大进度 if (progress > this.activeTracker.maxProgress) { this.activeTracker.maxProgress = progress this.activeTracker.lastPosition = scrollTop this.saveProgressLocal() console.log('[ReadingTracker] 进度更新:', progress + '%') } // 检查是否读完(≥90%) if (progress >= 90 && !this.activeTracker.isCompleted) { this.checkCompletion() } } /** * 检查是否读完(需要停留3秒) */ async checkCompletion() { if (!this.activeTracker || this.activeTracker.isCompleted) return // 等待3秒,确认用户真的读到底部 await this.sleep(3000) if (this.activeTracker && this.activeTracker.maxProgress >= 90 && !this.activeTracker.isCompleted) { this.activeTracker.isCompleted = true this.activeTracker.completedAt = Date.now() console.log('[ReadingTracker] 阅读完成:', this.activeTracker.sectionId) // 标记已读(app.js 里的已读章节列表) app.markSectionAsRead(this.activeTracker.sectionId) // 立即上报完成状态 await this.reportProgressToServer(true) // 触发埋点 this.trackEvent('chapter_completed', { sectionId: this.activeTracker.sectionId, duration: this.activeTracker.totalDuration }) wx.showToast({ title: '已完成阅读', icon: 'success', duration: 1500 }) } } /** * 保存进度到本地 */ saveProgressLocal() { if (!this.activeTracker) return try { const progressData = wx.getStorageSync('reading_progress') || {} progressData[this.activeTracker.sectionId] = { progress: this.activeTracker.maxProgress, lastPosition: this.activeTracker.lastPosition, lastOpenAt: Date.now() } wx.setStorageSync('reading_progress', progressData) } catch (e) { console.warn('[ReadingTracker] 保存本地进度失败:', e) } } /** * 开始定期上报 */ startProgressReport() { // 每30秒上报一次 this.reportInterval = setInterval(() => { this.reportProgressToServer(false) }, 30000) } /** * 上报进度到服务端 */ async reportProgressToServer(isCompletion = false) { if (!this.activeTracker) return const userId = app.globalData.userInfo?.id if (!userId) return // 计算本次上报的时长 const now = Date.now() const duration = Math.round((now - this.activeTracker.lastScrollTime) / 1000) this.activeTracker.totalDuration += duration this.activeTracker.lastScrollTime = now try { await app.request('/api/miniprogram/user/reading-progress', { method: 'POST', data: { userId, sectionId: this.activeTracker.sectionId, progress: this.activeTracker.maxProgress, duration: this.activeTracker.totalDuration, status: this.activeTracker.isCompleted ? 'completed' : 'reading', completedAt: this.activeTracker.completedAt } }) if (isCompletion) { console.log('[ReadingTracker] 完成状态已上报') } } catch (e) { console.warn('[ReadingTracker] 上报进度失败,下次重试:', e) } } /** * 页面隐藏/卸载时调用(立即上报) */ onPageHide() { if (this.activeTracker) { this.reportProgressToServer(false) } } /** * 清理追踪器 */ cleanup() { if (this.reportInterval) { clearInterval(this.reportInterval) this.reportInterval = null } if (this.activeTracker) { this.reportProgressToServer(false) this.activeTracker = null } } /** * 获取当前章节的阅读进度(用于展示) */ getCurrentProgress() { return this.activeTracker ? this.activeTracker.maxProgress : 0 } /** * 数据埋点(可对接统计平台) */ trackEvent(eventName, eventData) { console.log('[Analytics]', eventName, eventData) // TODO: 接入微信小程序数据助手 / 第三方统计 } /** * 工具:延迟 */ sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)) } } // 导出单例 const readingTracker = new ReadingTracker() export default readingTracker