This commit is contained in:
ChuXun
2025-10-19 20:28:31 +08:00
parent c81f8a8b03
commit eaab9a762a
100 changed files with 23416 additions and 0 deletions

View File

@@ -0,0 +1,823 @@
// 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
});
}
});