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

250 lines
6.4 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()
// 立即上报一次「打开/点击」,确保内容管理后台的「点击」数据有记录(与 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