// pages/dashboard/dashboard.js const gpaPredictor = require('../../utils/gpaPredictor'); const util = require('../../utils/util'); const learningTracker = require('../../utils/learningTracker'); Page({ data: { // 设备信息 windowWidth: 375, windowHeight: 667, pixelRatio: 2, // 顶部统计 stats: { totalDays: 0, totalHours: 0, avgGPA: 0 }, // 雷达图数据 radarData: { indicators: [ { name: '专注度', max: 100 }, { name: '活跃度', max: 100 }, { name: '学习时长', max: 100 }, { name: '知识广度', max: 100 }, { name: '互动性', max: 100 }, { name: '坚持度', max: 100 } ], values: [85, 90, 75, 88, 70, 95] }, // GPA预测数据 gpaHistory: [], prediction: { nextSemester: 0, trend: 0 }, // 饼图数据 pieData: [], // 柱状图数据 barData: { aboveAvg: 0, ranking: 0 }, updateTime: '' }, onLoad() { // 清理可能存在的旧模拟数据 this.cleanOldData(); // 获取设备信息 const systemInfo = wx.getSystemInfoSync(); this.setData({ windowWidth: systemInfo.windowWidth, windowHeight: systemInfo.windowHeight, pixelRatio: systemInfo.pixelRatio }); this.loadAllData(); }, /** * 清理旧的模拟数据 */ cleanOldData() { // 检查是否需要清理(可通过版本号判断) const dataVersion = wx.getStorageSync('data_version'); if (dataVersion !== '2.0') { console.log('[Dashboard] 清理旧数据...'); // 只清理GPA历史记录,让其从真实课程重新生成 wx.removeStorageSync('gpa_history'); // 标记数据版本 wx.setStorageSync('data_version', '2.0'); console.log('[Dashboard] 数据清理完成'); } }, onShow() { // 开始跟踪学习时间 learningTracker.onPageShow('tools') // 刷新数据 this.loadAllData(); }, onHide() { // 停止跟踪学习时间 learningTracker.onPageHide() }, onUnload() { // 记录学习时长 learningTracker.onPageUnload() }, onPullDownRefresh() { this.loadAllData(); setTimeout(() => { wx.stopPullDownRefresh(); }, 1000); }, /** * 加载所有数据 */ loadAllData() { this.loadStats(); this.loadRadarData(); this.loadGPAData(); this.loadPieData(); this.loadBarData(); this.setData({ updateTime: util.formatTime(new Date(), 'yyyy-MM-dd hh:mm') }); // 延迟绘制图表,确保DOM已渲染 setTimeout(() => { this.drawRadarChart(); this.drawLineChart(); this.drawPieChart(); this.drawBarChart(); }, 300); }, /** * 加载统计数据 */ loadStats() { // 从学习追踪器获取真实数据 const stats = learningTracker.getStats(); // 加载GPA数据 const gpaCourses = wx.getStorageSync('gpaCourses') || []; const avgGPA = gpaPredictor.calculateAverageGPA(gpaCourses); this.setData({ stats: { totalDays: stats.continuousDays, totalHours: stats.totalHours, avgGPA: avgGPA || 0 } }); }, /** * 计算连续学习天数 */ calculateContinuousDays(records) { if (!records || records.length === 0) return 0; // 按日期排序 const sortedRecords = records.sort((a, b) => new Date(b.date) - new Date(a.date)); let continuousDays = 0; const today = new Date(); today.setHours(0, 0, 0, 0); for (let i = 0; i < sortedRecords.length; i++) { const recordDate = new Date(sortedRecords[i].date); recordDate.setHours(0, 0, 0, 0); const daysDiff = Math.floor((today - recordDate) / (1000 * 60 * 60 * 24)); if (daysDiff === i) { continuousDays++; } else { break; } } return continuousDays; }, /** * 加载雷达图数据 */ loadRadarData() { // 从存储加载真实计算的学习画像 const learningProfile = wx.getStorageSync('learning_profile') || { focus: 0, // 专注度 activity: 0, // 活跃度 duration: 0, // 学习时长 breadth: 0, // 知识广度 interaction: 0, // 互动性 persistence: 0 // 坚持度 }; this.setData({ 'radarData.values': [ Math.min(100, Math.round(learningProfile.focus)), Math.min(100, Math.round(learningProfile.activity)), Math.min(100, Math.round(learningProfile.duration)), Math.min(100, Math.round(learningProfile.breadth)), Math.min(100, Math.round(learningProfile.interaction)), Math.min(100, Math.round(learningProfile.persistence)) ] }); }, /** * 加载GPA数据和预测 */ loadGPAData() { // 从GPA计算器获取真实数据 const gpaCourses = wx.getStorageSync('gpaCourses') || []; console.log('[Dashboard] 读取到的课程数据:', gpaCourses); // 强制从课程数据重新生成历史记录(不使用缓存) let gpaHistory = []; if (gpaCourses.length > 0) { // 按学期分组计算平均GPA const semesterMap = {}; gpaCourses.forEach(course => { const semester = course.semester || '2024-2'; if (!semesterMap[semester]) { semesterMap[semester] = { courses: [] }; } semesterMap[semester].courses.push(course); }); console.log('[Dashboard] 学期分组:', semesterMap); // 计算每个学期的加权平均GPA Object.keys(semesterMap).sort().forEach(semester => { const data = semesterMap[semester]; const avgGPA = gpaPredictor.calculateAverageGPA(data.courses); gpaHistory.push({ semester: semester, gpa: parseFloat(avgGPA) }); }); // 保存到存储 if (gpaHistory.length > 0) { wx.setStorageSync('gpa_history', gpaHistory); console.log('[Dashboard] GPA历史记录:', gpaHistory); } } // 设置数据(即使为空也设置,避免undefined) this.setData({ gpaHistory: gpaHistory.length > 0 ? gpaHistory : [] }); // 只有在有足够数据时才进行预测(至少2个学期) if (gpaHistory.length >= 2) { const predictionResult = gpaPredictor.polynomialRegression(gpaHistory, 2, 3); console.log('[Dashboard] GPA预测结果:', predictionResult); this.setData({ prediction: { nextSemester: predictionResult.predictions[0]?.gpa || 0, trend: predictionResult.trend, confidence: predictionResult.confidence } }); } else if (gpaHistory.length === 1) { // 只有一个学期,无法预测,显示当前GPA console.log('[Dashboard] 只有一个学期数据,无法预测'); this.setData({ prediction: { nextSemester: gpaHistory[0].gpa, trend: 0, confidence: 0 } }); } else { // 没有数据 console.log('[Dashboard] 没有GPA数据'); this.setData({ prediction: { nextSemester: 0, trend: 0, confidence: 0 } }); } }, /** * 加载饼图数据 */ loadPieData() { // 从学习追踪器获取真实的模块使用数据 const moduleUsage = wx.getStorageSync('module_usage') || { course: 0, forum: 0, tools: 0, ai: 0 }; const total = Object.values(moduleUsage).reduce((sum, val) => sum + val, 0); // 如果没有数据,显示提示 if (total === 0) { this.setData({ pieData: [ { name: '课程中心', time: 0, percent: 25, color: '#4A90E2' }, { name: '学科论坛', time: 0, percent: 25, color: '#50C878' }, { name: '学习工具', time: 0, percent: 25, color: '#9B59B6' }, { name: '启思AI', time: 0, percent: 25, color: '#6C5CE7' } ] }); return; } const pieData = [ { name: '课程中心', time: parseFloat(moduleUsage.course.toFixed(1)), percent: Math.round((moduleUsage.course / total) * 100), color: '#4A90E2' }, { name: '学科论坛', time: parseFloat(moduleUsage.forum.toFixed(1)), percent: Math.round((moduleUsage.forum / total) * 100), color: '#50C878' }, { name: '学习工具', time: parseFloat(moduleUsage.tools.toFixed(1)), percent: Math.round((moduleUsage.tools / total) * 100), color: '#9B59B6' }, { name: '启思AI', time: parseFloat(moduleUsage.ai.toFixed(1)), percent: Math.round((moduleUsage.ai / total) * 100), color: '#6C5CE7' } ]; this.setData({ pieData }); }, /** * 加载柱状图数据 */ loadBarData() { const gpaCourses = wx.getStorageSync('gpaCourses') || []; // 计算超过平均分的课程数 let aboveAvg = 0; gpaCourses.forEach(course => { if (course.score > 75) { // 假设75为平均分 aboveAvg++; } }); // 模拟排名(基于平均GPA) const avgGPA = parseFloat(this.data.stats.avgGPA); let ranking = 30; // 默认前30% if (avgGPA >= 3.5) ranking = 10; else if (avgGPA >= 3.0) ranking = 20; this.setData({ barData: { aboveAvg, ranking } }); }, /** * 绘制雷达图 */ drawRadarChart() { const query = wx.createSelectorQuery(); query.select('#radarChart').boundingClientRect(); query.exec((res) => { if (!res[0]) return; const canvasWidth = res[0].width; const canvasHeight = res[0].height; const ctx = wx.createCanvasContext('radarChart', this); const centerX = canvasWidth / 2; const centerY = canvasHeight / 2; const radius = Math.min(canvasWidth, canvasHeight) / 2 - 60; const data = this.data.radarData.values; const indicators = this.data.radarData.indicators; const angleStep = (Math.PI * 2) / indicators.length; // 清空画布 ctx.clearRect(0, 0, canvasWidth, canvasHeight); // 绘制背景网格 ctx.setLineWidth(1); ctx.setStrokeStyle('#E0E0E0'); for (let level = 1; level <= 5; level++) { ctx.beginPath(); const r = (radius / 5) * level; for (let i = 0; i <= indicators.length; i++) { const angle = i * angleStep - Math.PI / 2; const x = centerX + r * Math.cos(angle); const y = centerY + r * Math.sin(angle); if (i === 0) { ctx.moveTo(x, y); } else { ctx.lineTo(x, y); } } ctx.closePath(); ctx.stroke(); } // 绘制轴线和标签 ctx.setFillStyle('#333333'); ctx.setFontSize(24); ctx.setTextAlign('center'); for (let i = 0; i < indicators.length; i++) { const angle = i * angleStep - Math.PI / 2; const x = centerX + radius * Math.cos(angle); const y = centerY + radius * Math.sin(angle); ctx.beginPath(); ctx.moveTo(centerX, centerY); ctx.lineTo(x, y); ctx.stroke(); // 绘制维度标签 const labelDistance = radius + 30; const labelX = centerX + labelDistance * Math.cos(angle); const labelY = centerY + labelDistance * Math.sin(angle); ctx.fillText(indicators[i].name, labelX, labelY); } // 绘制数据区域 ctx.beginPath(); ctx.setFillStyle('rgba(102, 126, 234, 0.3)'); ctx.setStrokeStyle('#667eea'); ctx.setLineWidth(2); for (let i = 0; i <= data.length; i++) { const index = i % data.length; const value = data[index]; const angle = index * angleStep - Math.PI / 2; const r = (value / 100) * radius; const x = centerX + r * Math.cos(angle); const y = centerY + r * Math.sin(angle); if (i === 0) { ctx.moveTo(x, y); } else { ctx.lineTo(x, y); } // 绘制数据点 ctx.arc(x, y, 4, 0, Math.PI * 2); ctx.moveTo(x, y); } ctx.closePath(); ctx.fill(); ctx.stroke(); ctx.draw(); }); }, /** * 绘制折线图(GPA趋势) */ drawLineChart() { const query = wx.createSelectorQuery(); query.select('#lineChart').boundingClientRect(); query.exec((res) => { if (!res[0]) return; const canvasWidth = res[0].width; const canvasHeight = res[0].height; const ctx = wx.createCanvasContext('lineChart', this); const history = this.data.gpaHistory; const prediction = this.data.prediction; // 清空画布 ctx.clearRect(0, 0, canvasWidth, canvasHeight); // 如果没有数据,显示提示 if (!history || history.length === 0) { ctx.setFillStyle('#999999'); ctx.setFontSize(24); ctx.setTextAlign('center'); ctx.fillText('暂无GPA数据', canvasWidth / 2, canvasHeight / 2); ctx.fillText('请先在GPA工具中录入成绩', canvasWidth / 2, canvasHeight / 2 + 30); ctx.draw(); return; } console.log('[绘制折线图] 历史数据:', history); const padding = { top: 40, right: 40, bottom: 60, left: 60 }; const width = canvasWidth - padding.left - padding.right; const height = canvasHeight - padding.top - padding.bottom; // 合并历史和预测数据 const allData = [...history]; const predictionResult = gpaPredictor.polynomialRegression(history, 2, 3); if (predictionResult.predictions && predictionResult.predictions.length > 0) { allData.push(...predictionResult.predictions); console.log('[绘制折线图] 预测数据:', predictionResult.predictions); } const maxGPA = 4.5; // 调整最大值为4.5 const minGPA = 0; const stepX = allData.length > 1 ? width / (allData.length - 1) : width / 2; const scaleY = height / (maxGPA - minGPA); // 绘制坐标轴 ctx.setStrokeStyle('#CCCCCC'); ctx.setLineWidth(1); ctx.beginPath(); ctx.moveTo(padding.left, padding.top); ctx.lineTo(padding.left, padding.top + height); ctx.lineTo(padding.left + width, padding.top + height); ctx.stroke(); // 绘制网格线和Y轴刻度 ctx.setStrokeStyle('#F0F0F0'); ctx.setFillStyle('#999999'); ctx.setFontSize(20); ctx.setTextAlign('right'); for (let i = 0; i <= 4; i++) { const gpaValue = i * 1.0; const y = padding.top + height - (gpaValue * scaleY); ctx.beginPath(); ctx.moveTo(padding.left, y); ctx.lineTo(padding.left + width, y); ctx.stroke(); ctx.fillText(gpaValue.toFixed(1), padding.left - 10, y + 6); } // 绘制历史数据折线 if (history.length > 0) { ctx.setStrokeStyle('#667eea'); ctx.setLineWidth(3); ctx.beginPath(); history.forEach((item, index) => { const x = padding.left + index * stepX; const y = padding.top + height - (item.gpa * scaleY); if (index === 0) { ctx.moveTo(x, y); } else { ctx.lineTo(x, y); } }); ctx.stroke(); // 绘制历史数据点和数值 ctx.setFillStyle('#667eea'); history.forEach((item, index) => { const x = padding.left + index * stepX; const y = padding.top + height - (item.gpa * scaleY); ctx.beginPath(); ctx.arc(x, y, 5, 0, Math.PI * 2); ctx.fill(); // 显示GPA值 ctx.setFillStyle('#333333'); ctx.setFontSize(18); ctx.setTextAlign('center'); ctx.fillText(item.gpa.toFixed(2), x, y - 15); }); } // 绘制预测数据折线(虚线) if (predictionResult.predictions && predictionResult.predictions.length > 0 && history.length > 0) { ctx.setStrokeStyle('#A29BFE'); ctx.setLineDash([5, 5]); ctx.setLineWidth(2); ctx.beginPath(); const startIndex = history.length - 1; const startX = padding.left + startIndex * stepX; const startY = padding.top + height - (history[startIndex].gpa * scaleY); ctx.moveTo(startX, startY); predictionResult.predictions.forEach((item, index) => { const x = padding.left + (startIndex + index + 1) * stepX; const y = padding.top + height - (item.gpa * scaleY); ctx.lineTo(x, y); }); ctx.stroke(); ctx.setLineDash([]); // 绘制预测数据点和数值 ctx.setFillStyle('#A29BFE'); predictionResult.predictions.forEach((item, index) => { const x = padding.left + (startIndex + index + 1) * stepX; const y = padding.top + height - (item.gpa * scaleY); ctx.beginPath(); ctx.arc(x, y, 4, 0, Math.PI * 2); ctx.fill(); // 显示预测GPA值 ctx.setFillStyle('#A29BFE'); ctx.setFontSize(18); ctx.setTextAlign('center'); ctx.fillText(item.gpa.toFixed(2), x, y - 15); }); } // 绘制X轴标签(旋转避免重叠) ctx.setFillStyle('#999999'); ctx.setFontSize(16); ctx.setTextAlign('left'); allData.forEach((item, index) => { const x = padding.left + index * stepX; const y = padding.top + height + 20; // 旋转文字45度 ctx.save(); ctx.translate(x, y); ctx.rotate(Math.PI / 4); ctx.fillText(item.semester, 0, 0); ctx.restore(); }); ctx.draw(); }); }, /** * 绘制饼图 */ drawPieChart() { const query = wx.createSelectorQuery(); query.select('#pieChart').boundingClientRect(); query.exec((res) => { if (!res[0]) return; const canvasWidth = res[0].width; const canvasHeight = res[0].height; const ctx = wx.createCanvasContext('pieChart', this); const data = this.data.pieData; const centerX = canvasWidth / 2; const centerY = canvasHeight / 2; const radius = Math.min(canvasWidth, canvasHeight) / 2 - 40; // 清空画布 ctx.clearRect(0, 0, canvasWidth, canvasHeight); // 计算总数(用于处理全0情况) const total = data.reduce((sum, item) => sum + item.time, 0); if (total === 0) { // 如果没有数据,绘制完整的灰色圆环 ctx.beginPath(); ctx.arc(centerX, centerY, radius, 0, Math.PI * 2); ctx.setFillStyle('#EEEEEE'); ctx.fill(); // 绘制中心圆 ctx.beginPath(); ctx.arc(centerX, centerY, radius * 0.5, 0, Math.PI * 2); ctx.setFillStyle('#FFFFFF'); ctx.fill(); } else { // 有数据时正常绘制 let startAngle = -Math.PI / 2; data.forEach((item, index) => { if (item.time > 0) { // 只绘制有数据的扇形 const angle = (item.percent / 100) * Math.PI * 2; const endAngle = startAngle + angle; // 绘制扇形 ctx.beginPath(); ctx.moveTo(centerX, centerY); ctx.arc(centerX, centerY, radius, startAngle, endAngle); ctx.closePath(); ctx.setFillStyle(item.color); ctx.fill(); startAngle = endAngle; } }); // 绘制中心圆(甜甜圈效果) ctx.beginPath(); ctx.arc(centerX, centerY, radius * 0.5, 0, Math.PI * 2); ctx.setFillStyle('#FFFFFF'); ctx.fill(); } ctx.draw(); }); }, /** * 绘制柱状图 */ drawBarChart() { const query = wx.createSelectorQuery(); query.select('#barChart').boundingClientRect(); query.exec((res) => { if (!res[0]) return; const canvasWidth = res[0].width; const canvasHeight = res[0].height; const ctx = wx.createCanvasContext('barChart', this); const gpaCourses = wx.getStorageSync('gpaCourses') || []; // 清空画布 ctx.clearRect(0, 0, canvasWidth, canvasHeight); // 取前6门课程 const courses = gpaCourses.slice(0, 6); if (courses.length === 0) { ctx.setFillStyle('#999999'); ctx.setFontSize(28); ctx.fillText('暂无成绩数据', canvasWidth / 2 - 70, canvasHeight / 2); ctx.draw(); return; } const padding = { top: 40, right: 40, bottom: 80, left: 60 }; const width = canvasWidth - padding.left - padding.right; const height = canvasHeight - padding.top - padding.bottom; const barWidth = width / (courses.length * 2 + 1); const maxScore = 100; // 绘制坐标轴 ctx.setStrokeStyle('#CCCCCC'); ctx.setLineWidth(1); ctx.beginPath(); ctx.moveTo(padding.left, padding.top); ctx.lineTo(padding.left, padding.top + height); ctx.lineTo(padding.left + width, padding.top + height); ctx.stroke(); // 绘制网格线 ctx.setStrokeStyle('#F0F0F0'); for (let i = 0; i <= 5; i++) { const y = padding.top + (height / 5) * i; ctx.beginPath(); ctx.moveTo(padding.left, y); ctx.lineTo(padding.left + width, y); ctx.stroke(); // Y轴刻度 ctx.setFillStyle('#999999'); ctx.setFontSize(20); ctx.fillText((100 - i * 20).toString(), padding.left - 35, y + 6); } // 绘制柱状图 courses.forEach((course, index) => { const x = padding.left + (index * 2 + 1) * barWidth; const scoreHeight = (course.score / maxScore) * height; const avgHeight = (75 / maxScore) * height; // 假设75为平均分 // 个人成绩 ctx.setFillStyle('#667eea'); ctx.fillRect(x, padding.top + height - scoreHeight, barWidth * 0.8, scoreHeight); // 平均成绩(对比) ctx.setFillStyle('#CCCCCC'); ctx.fillRect(x + barWidth, padding.top + height - avgHeight, barWidth * 0.8, avgHeight); // 课程名称 ctx.setFillStyle('#333333'); ctx.setFontSize(18); ctx.save(); ctx.translate(x + barWidth, padding.top + height + 20); ctx.rotate(Math.PI / 4); const courseName = course.name.length > 4 ? course.name.substring(0, 4) + '...' : course.name; ctx.fillText(courseName, 0, 0); ctx.restore(); }); // 图例 ctx.setFillStyle('#667eea'); ctx.fillRect(padding.left + width - 120, padding.top - 30, 20, 15); ctx.setFillStyle('#333333'); ctx.setFontSize(20); ctx.fillText('个人', padding.left + width - 95, padding.top - 18); ctx.setFillStyle('#CCCCCC'); ctx.fillRect(padding.left + width - 50, padding.top - 30, 20, 15); ctx.fillText('平均', padding.left + width - 25, padding.top - 18); ctx.draw(); }); }, /** * 显示某天详情 */ showDayDetail(e) { const { date } = e.currentTarget.dataset; const dailyActivity = wx.getStorageSync('daily_activity') || {}; const activity = dailyActivity[date] || 0; wx.showModal({ title: date, content: `学习活跃度: ${activity}分钟`, showCancel: false }); } });