299 lines
8.7 KiB
JavaScript
299 lines
8.7 KiB
JavaScript
|
|
/**
|
|||
|
|
* 学习监控工具类
|
|||
|
|
* 实现屏幕截图和上传功能
|
|||
|
|
*/
|
|||
|
|
import request from './request.js'
|
|||
|
|
|
|||
|
|
class Monitor {
|
|||
|
|
constructor() {
|
|||
|
|
this.screenshotTimer = null
|
|||
|
|
this.uploadQueue = [] // 上传队列
|
|||
|
|
this.isUploading = false
|
|||
|
|
this.interval = 30000 // 默认30秒
|
|||
|
|
this.currentCourseId = null
|
|||
|
|
this.isEnabled = false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 启动监控
|
|||
|
|
* @param {Number} courseId 课程ID(可选)
|
|||
|
|
* @param {Number} interval 截图间隔(毫秒,默认30秒)
|
|||
|
|
*/
|
|||
|
|
start(courseId = null, interval = 30000) {
|
|||
|
|
if (this.isEnabled) {
|
|||
|
|
console.log('监控已启动')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.currentCourseId = courseId
|
|||
|
|
this.interval = interval
|
|||
|
|
this.isEnabled = true
|
|||
|
|
|
|||
|
|
console.log('启动学习监控,间隔:', interval / 1000, '秒')
|
|||
|
|
|
|||
|
|
// 立即执行一次
|
|||
|
|
this.captureAndUpload()
|
|||
|
|
|
|||
|
|
// 定时执行
|
|||
|
|
this.screenshotTimer = setInterval(() => {
|
|||
|
|
if (this.isEnabled) {
|
|||
|
|
this.captureAndUpload()
|
|||
|
|
}
|
|||
|
|
}, this.interval)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 停止监控
|
|||
|
|
*/
|
|||
|
|
stop() {
|
|||
|
|
this.isEnabled = false
|
|||
|
|
if (this.screenshotTimer) {
|
|||
|
|
clearInterval(this.screenshotTimer)
|
|||
|
|
this.screenshotTimer = null
|
|||
|
|
}
|
|||
|
|
console.log('学习监控已停止')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 设置当前课程ID
|
|||
|
|
*/
|
|||
|
|
setCourseId(courseId) {
|
|||
|
|
this.currentCourseId = courseId
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 截图并上传
|
|||
|
|
*/
|
|||
|
|
async captureAndUpload() {
|
|||
|
|
try {
|
|||
|
|
// 检查网络状态
|
|||
|
|
const networkType = await this.getNetworkType()
|
|||
|
|
if (networkType === 'none') {
|
|||
|
|
console.log('网络未连接,跳过截图上传')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 截图
|
|||
|
|
const screenshotPath = await this.captureScreenshot()
|
|||
|
|
if (!screenshotPath) {
|
|||
|
|
console.log('截图失败')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 添加到上传队列
|
|||
|
|
this.uploadQueue.push({
|
|||
|
|
filePath: screenshotPath,
|
|||
|
|
courseId: this.currentCourseId,
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 处理上传队列
|
|||
|
|
this.processUploadQueue()
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('截图上传失败:', error)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 截图
|
|||
|
|
*/
|
|||
|
|
captureScreenshot() {
|
|||
|
|
return new Promise((resolve, reject) => {
|
|||
|
|
// #ifdef APP-PLUS
|
|||
|
|
// App环境:使用plus API
|
|||
|
|
this.captureScreenshotApp(resolve, reject)
|
|||
|
|
// #endif
|
|||
|
|
|
|||
|
|
// #ifdef H5
|
|||
|
|
// H5环境:使用html2canvas或简化方案
|
|||
|
|
this.captureScreenshotH5(resolve, reject)
|
|||
|
|
// #endif
|
|||
|
|
|
|||
|
|
// #ifndef H5 || APP-PLUS
|
|||
|
|
// 其他平台:暂不支持或使用降级方案
|
|||
|
|
console.warn('当前平台不支持截图功能,跳过截图')
|
|||
|
|
reject(new Error('当前平台不支持截图功能'))
|
|||
|
|
// #endif
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* App环境截图(推荐)
|
|||
|
|
*/
|
|||
|
|
captureScreenshotApp(resolve, reject) {
|
|||
|
|
// #ifdef APP-PLUS
|
|||
|
|
try {
|
|||
|
|
// 使用plus.screen.capture截图
|
|||
|
|
if (typeof plus !== 'undefined' && plus.screen) {
|
|||
|
|
plus.screen.capture((bitmap) => {
|
|||
|
|
// 保存为临时文件
|
|||
|
|
const timestamp = Date.now()
|
|||
|
|
const filePath = `_doc/screenshot_${timestamp}.png`
|
|||
|
|
|
|||
|
|
bitmap.save(filePath, (success) => {
|
|||
|
|
resolve(filePath)
|
|||
|
|
}, (error) => {
|
|||
|
|
console.error('保存截图失败:', error)
|
|||
|
|
reject(error)
|
|||
|
|
})
|
|||
|
|
}, (error) => {
|
|||
|
|
console.error('截图失败:', error)
|
|||
|
|
reject(error)
|
|||
|
|
})
|
|||
|
|
} else {
|
|||
|
|
// 降级方案:提示用户
|
|||
|
|
console.warn('plus.screen不可用,跳过截图')
|
|||
|
|
reject(new Error('截图功能不可用'))
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('截图异常:', error)
|
|||
|
|
reject(error)
|
|||
|
|
}
|
|||
|
|
// #endif
|
|||
|
|
|
|||
|
|
// #ifndef APP-PLUS
|
|||
|
|
reject(new Error('App环境不可用'))
|
|||
|
|
// #endif
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* H5环境截图(简化方案)
|
|||
|
|
* 注意:H5环境截图功能有限,主要用于开发测试
|
|||
|
|
*/
|
|||
|
|
captureScreenshotH5(resolve, reject) {
|
|||
|
|
// #ifdef H5
|
|||
|
|
try {
|
|||
|
|
// H5环境可以使用html2canvas库,但这里使用简单的canvas方式
|
|||
|
|
// 注意:H5环境截图功能有限,建议在App环境中使用
|
|||
|
|
const canvas = document.createElement('canvas')
|
|||
|
|
const ctx = canvas.getContext('2d')
|
|||
|
|
canvas.width = window.innerWidth
|
|||
|
|
canvas.height = window.innerHeight
|
|||
|
|
|
|||
|
|
// 绘制当前页面内容(简化版,实际需要更复杂的实现)
|
|||
|
|
ctx.fillStyle = '#fff'
|
|||
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
|||
|
|
ctx.fillStyle = '#000'
|
|||
|
|
ctx.font = '16px Arial'
|
|||
|
|
ctx.fillText('学习监控截图', 20, 40)
|
|||
|
|
ctx.fillText(new Date().toLocaleString(), 20, 60)
|
|||
|
|
ctx.fillText('H5环境截图功能有限', 20, 80)
|
|||
|
|
|
|||
|
|
// 转换为图片
|
|||
|
|
canvas.toBlob((blob) => {
|
|||
|
|
if (typeof URL !== 'undefined' && URL.createObjectURL) {
|
|||
|
|
const filePath = URL.createObjectURL(blob)
|
|||
|
|
resolve(filePath)
|
|||
|
|
} else {
|
|||
|
|
// Fallback: convert blob to data URL
|
|||
|
|
const reader = new FileReader()
|
|||
|
|
reader.onloadend = () => {
|
|||
|
|
resolve(reader.result)
|
|||
|
|
}
|
|||
|
|
reader.onerror = (error) => {
|
|||
|
|
reject(error)
|
|||
|
|
}
|
|||
|
|
reader.readAsDataURL(blob)
|
|||
|
|
}
|
|||
|
|
}, 'image/png', 0.8)
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('H5截图失败:', error)
|
|||
|
|
reject(error)
|
|||
|
|
}
|
|||
|
|
// #endif
|
|||
|
|
|
|||
|
|
// #ifndef H5
|
|||
|
|
reject(new Error('H5环境不可用'))
|
|||
|
|
// #endif
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取网络状态
|
|||
|
|
*/
|
|||
|
|
getNetworkType() {
|
|||
|
|
return new Promise((resolve) => {
|
|||
|
|
uni.getNetworkType({
|
|||
|
|
success: (res) => {
|
|||
|
|
resolve(res.networkType)
|
|||
|
|
},
|
|||
|
|
fail: () => {
|
|||
|
|
resolve('unknown')
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理上传队列
|
|||
|
|
*/
|
|||
|
|
async processUploadQueue() {
|
|||
|
|
if (this.isUploading || this.uploadQueue.length === 0) {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.isUploading = true
|
|||
|
|
|
|||
|
|
while (this.uploadQueue.length > 0) {
|
|||
|
|
const item = this.uploadQueue.shift()
|
|||
|
|
try {
|
|||
|
|
await this.uploadScreenshot(item.filePath, item.courseId)
|
|||
|
|
console.log('截图上传成功')
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('截图上传失败:', error)
|
|||
|
|
// 上传失败,重新加入队列(最多重试3次)
|
|||
|
|
if (item.retryCount === undefined) {
|
|||
|
|
item.retryCount = 0
|
|||
|
|
}
|
|||
|
|
if (item.retryCount < 3) {
|
|||
|
|
item.retryCount++
|
|||
|
|
this.uploadQueue.push(item)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.isUploading = false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 上传截图
|
|||
|
|
*/
|
|||
|
|
async uploadScreenshot(filePath, courseId = null) {
|
|||
|
|
const formData = {}
|
|||
|
|
if (courseId) {
|
|||
|
|
formData.courseId = courseId
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return await request.upload('/study/monitor/screenshot', filePath, formData, {
|
|||
|
|
name: 'file'
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 手动截图上传(用于测试)
|
|||
|
|
*/
|
|||
|
|
async manualCapture() {
|
|||
|
|
try {
|
|||
|
|
const screenshotPath = await this.captureScreenshot()
|
|||
|
|
if (screenshotPath) {
|
|||
|
|
await this.uploadScreenshot(screenshotPath, this.currentCourseId)
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '截图上传成功',
|
|||
|
|
icon: 'success'
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
uni.showToast({
|
|||
|
|
title: '截图失败',
|
|||
|
|
icon: 'none'
|
|||
|
|
})
|
|||
|
|
console.error('手动截图失败:', error)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 创建单例
|
|||
|
|
const monitor = new Monitor()
|
|||
|
|
|
|||
|
|
export default monitor
|
|||
|
|
|