1
This commit is contained in:
823
pages/dashboard/dashboard.js
Normal file
823
pages/dashboard/dashboard.js
Normal 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
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user