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
});
}
});

View File

@@ -0,0 +1,7 @@
{
"navigationBarTitleText": "学习数据",
"navigationBarBackgroundColor": "#667eea",
"navigationBarTextStyle": "white",
"enablePullDownRefresh": true,
"backgroundColor": "#F5F6FA"
}

View File

@@ -0,0 +1,123 @@
<!--pages/dashboard/dashboard.wxml-->
<view class="dashboard-container">
<!-- 顶部统计卡片 -->
<view class="stats-header">
<view class="stats-card">
<view class="stat-value">{{stats.totalDays}}</view>
<view class="stat-label">连续学习天数</view>
<view class="stat-icon">🔥</view>
</view>
<view class="stats-card">
<view class="stat-value">{{stats.totalHours}}</view>
<view class="stat-label">累计学习小时</view>
<view class="stat-icon">⏰</view>
</view>
<view class="stats-card">
<view class="stat-value">{{stats.avgGPA}}</view>
<view class="stat-label">平均绩点</view>
<view class="stat-icon">📊</view>
</view>
</view>
<!-- 学习画像雷达图 -->
<view class="chart-section">
<view class="section-header">
<view class="section-title">
<text class="title-icon">🎯</text>
<text class="title-text">学习能力画像</text>
</view>
<view class="section-desc">6维度综合评估</view>
</view>
<view class="chart-container">
<canvas canvas-id="radarChart" class="chart-canvas" id="radarChart"></canvas>
<view class="chart-legend">
<view class="legend-item" wx:for="{{radarData.indicators}}" wx:key="name">
<view class="legend-dot" style="background: #667eea;"></view>
<text class="legend-name">{{item.name}}</text>
<text class="legend-value">{{radarData.values[index]}}</text>
</view>
</view>
</view>
</view>
<!-- GPA趋势与预测 -->
<view class="chart-section">
<view class="section-header">
<view class="section-title">
<text class="title-icon">📈</text>
<text class="title-text">GPA趋势预测</text>
</view>
<view class="section-desc">基于多项式回归分析</view>
</view>
<view class="chart-container">
<canvas canvas-id="lineChart" class="chart-canvas" id="lineChart"></canvas>
<view class="prediction-info">
<view class="prediction-item">
<text class="pred-label">预测下学期:</text>
<text class="pred-value">{{prediction.nextSemester}}</text>
</view>
<view class="prediction-item">
<text class="pred-label">趋势:</text>
<text class="pred-value {{prediction.trend > 0 ? 'trend-up' : 'trend-down'}}">
{{prediction.trend > 0 ? '上升' : '下降'}} {{prediction.trend}}%
</text>
</view>
</view>
</view>
</view>
<!-- 模块使用时长饼图 -->
<view class="chart-section">
<view class="section-header">
<view class="section-title">
<text class="title-icon">⏱️</text>
<text class="title-text">时间分配</text>
</view>
<view class="section-desc">各功能使用占比</view>
</view>
<view class="chart-container">
<canvas canvas-id="pieChart" class="chart-canvas pie-canvas" id="pieChart"></canvas>
<view class="pie-legend">
<view class="pie-legend-item" wx:for="{{pieData}}" wx:key="name">
<view class="pie-dot" style="background: {{item.color}};"></view>
<text class="pie-name">{{item.name}}</text>
<text class="pie-percent">{{item.percent}}%</text>
<text class="pie-time">{{item.time}}h</text>
</view>
</view>
</view>
</view>
<!-- 成绩对比柱状图 -->
<view class="chart-section">
<view class="section-header">
<view class="section-title">
<text class="title-icon">📊</text>
<text class="title-text">成绩对比</text>
</view>
<view class="section-desc">个人 vs 班级平均</view>
</view>
<view class="chart-container">
<canvas canvas-id="barChart" class="chart-canvas" id="barChart"></canvas>
<view class="bar-summary">
<view class="summary-item">
<text class="summary-label">超过平均:</text>
<text class="summary-value">{{barData.aboveAvg}}门课程</text>
</view>
<view class="summary-item">
<text class="summary-label">排名:</text>
<text class="summary-value">前{{barData.ranking}}%</text>
</view>
</view>
</view>
</view>
<!-- 数据来源说明 -->
<view class="data-source">
<view class="source-icon"></view>
<view class="source-text">
<text class="source-title">数据说明</text>
<text class="source-desc">以上数据基于您的学习记录自动生成,更新时间: {{updateTime}}</text>
</view>
</view>
</view>

View File

@@ -0,0 +1,279 @@
/* pages/dashboard/dashboard.wxss */
@import "/styles/design-tokens.wxss";
@import "/styles/premium-animations.wxss";
.dashboard-container {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 30rpx;
padding-bottom: calc(env(safe-area-inset-bottom) + 30rpx);
}
/* 顶部统计卡片 */
.stats-header {
display: flex;
justify-content: space-between;
margin-bottom: 30rpx;
gap: 20rpx;
}
.stats-card {
flex: 1;
background: rgba(255, 255, 255, 0.95);
border-radius: 20rpx;
padding: 30rpx 20rpx;
text-align: center;
position: relative;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
animation: fadeInUp 0.6s ease;
}
.stat-value {
font-size: 48rpx;
font-weight: bold;
color: #667eea;
margin-bottom: 8rpx;
}
.stat-label {
font-size: 22rpx;
color: #666666;
}
.stat-icon {
position: absolute;
top: 20rpx;
right: 20rpx;
font-size: 32rpx;
opacity: 0.3;
}
/* 图表区域 */
.chart-section {
background: rgba(255, 255, 255, 0.95);
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
animation: fadeInUp 0.6s ease;
}
.section-header {
margin-bottom: 30rpx;
padding-bottom: 20rpx;
border-bottom: 2rpx solid #F0F0F0;
}
.section-title {
display: flex;
align-items: center;
margin-bottom: 8rpx;
}
.title-icon {
font-size: 36rpx;
margin-right: 12rpx;
}
.title-text {
font-size: 32rpx;
font-weight: bold;
color: #333333;
}
.section-desc {
font-size: 24rpx;
color: #999999;
margin-left: 48rpx;
}
/* 画布容器 */
.chart-container {
width: 100%;
}
.chart-canvas {
width: 100%;
height: 500rpx;
}
.pie-canvas {
height: 400rpx;
}
/* 雷达图图例 */
.chart-legend {
margin-top: 30rpx;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
.legend-item {
display: flex;
align-items: center;
font-size: 24rpx;
}
.legend-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
margin-right: 12rpx;
}
.legend-name {
flex: 1;
color: #666666;
}
.legend-value {
font-weight: bold;
color: #667eea;
}
/* 预测信息 */
.prediction-info {
margin-top: 30rpx;
display: flex;
justify-content: space-around;
padding: 20rpx;
background: #F8F9FA;
border-radius: 16rpx;
}
.prediction-item {
text-align: center;
}
.pred-label {
font-size: 24rpx;
color: #999999;
display: block;
margin-bottom: 8rpx;
}
.pred-value {
font-size: 32rpx;
font-weight: bold;
color: #667eea;
}
.trend-up {
color: #4CAF50;
}
.trend-down {
color: #FF5252;
}
/* 饼图图例 */
.pie-legend {
margin-top: 30rpx;
}
.pie-legend-item {
display: flex;
align-items: center;
padding: 16rpx 0;
border-bottom: 1rpx solid #F0F0F0;
font-size: 26rpx;
}
.pie-legend-item:last-child {
border-bottom: none;
}
.pie-dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
margin-right: 16rpx;
}
.pie-name {
flex: 1;
color: #333333;
}
.pie-percent {
font-weight: bold;
color: #667eea;
margin-right: 20rpx;
}
.pie-time {
color: #999999;
}
/* 柱状图总结 */
.bar-summary {
margin-top: 30rpx;
display: flex;
justify-content: space-around;
padding: 20rpx;
background: #F8F9FA;
border-radius: 16rpx;
}
.summary-item {
text-align: center;
}
.summary-label {
font-size: 24rpx;
color: #999999;
display: block;
margin-bottom: 8rpx;
}
.summary-value {
font-size: 32rpx;
font-weight: bold;
color: #667eea;
}
/* 数据来源说明 */
.data-source {
display: flex;
align-items: flex-start;
padding: 30rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 16rpx;
margin-top: 20rpx;
}
.source-icon {
font-size: 36rpx;
margin-right: 16rpx;
}
.source-text {
flex: 1;
}
.source-title {
font-size: 26rpx;
font-weight: bold;
color: #FFFFFF;
display: block;
margin-bottom: 8rpx;
}
.source-desc {
font-size: 22rpx;
color: rgba(255, 255, 255, 0.9);
line-height: 1.6;
}
/* 动画 */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(40rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}