Files
soul-yongping/miniprogram/utils/readingTracker.js

250 lines
6.4 KiB
JavaScript
Raw Normal View History

/**
* 阅读进度追踪器
* 记录阅读进度时长是否读完支持断点续读
*/
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