Files
soul-yongping/miniprogram/utils/readingTracker.js
卡若 e5e6ffd7b1 miniprogram: 用永平版本替换(含超级个体、会员详情、提现等)
- 来源: 一场soul的创业实验-永平/soul/miniprogram
- 新增: addresses/agreement/privacy/withdraw-records 等页面
- 新增: components/icon, utils/chapterAccessManager, readingTracker
- 删除: 上传脚本、部署说明等冗余文件
- 同步永平最新结构和功能

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 14:35:58 +08:00

247 lines
6.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

/**
* 阅读进度追踪器
* 记录阅读进度、时长、是否读完,支持断点续读
*/
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()
}
/**
* 恢复上次阅读位置(断点续读)
*/
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