Files
ZhiQiXiaoYuan/pages/dashboard/dashboard.js
ChuXun eaab9a762a 1
2025-10-19 20:28:31 +08:00

824 lines
23 KiB
JavaScript
Raw Permalink 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.
// 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
});
}
});