|
|
|
|
@ -65,14 +65,18 @@ Page({
|
|
|
|
|
*/
|
|
|
|
|
data: {
|
|
|
|
|
// 遗忘曲线数据 - 显示未来7天的复习计划
|
|
|
|
|
复习计划数据: [],
|
|
|
|
|
复习计划数据: [],
|
|
|
|
|
// 需要复习的古诗列表
|
|
|
|
|
poemsToReview: [],
|
|
|
|
|
// Canvas尺寸
|
|
|
|
|
canvasWidth: 0,
|
|
|
|
|
canvasHeight: 300,
|
|
|
|
|
// 今日需要复习的总数
|
|
|
|
|
todayReviewCount: 0
|
|
|
|
|
todayReviewCount: 0,
|
|
|
|
|
// 用户是否有背诵记录
|
|
|
|
|
hasRecitationRecords: false,
|
|
|
|
|
// 加载状态
|
|
|
|
|
isLoading: true
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@ -91,21 +95,31 @@ Page({
|
|
|
|
|
*/
|
|
|
|
|
async loadPoemsToReview() {
|
|
|
|
|
try {
|
|
|
|
|
wx.showLoading({ title: '加载复习计划...' })
|
|
|
|
|
this.setData({ isLoading: true });
|
|
|
|
|
|
|
|
|
|
// 从数据库加载真实背诵记录
|
|
|
|
|
let reciteRecords = await this.loadReciteRecordsFromDatabase();
|
|
|
|
|
|
|
|
|
|
// 如果没有记录或记录为空,使用模拟数据
|
|
|
|
|
let recordsToUse = reciteRecords && reciteRecords.length > 0 ? reciteRecords : this.getMockReciteRecords();
|
|
|
|
|
|
|
|
|
|
// 增强:为每条记录获取完整的诗歌信息,特别是作者信息
|
|
|
|
|
if (reciteRecords && reciteRecords.length > 0) {
|
|
|
|
|
recordsToUse = await this.enrichRecordsWithPoemDetails(reciteRecords);
|
|
|
|
|
console.log('获取到的背诵记录数量:', reciteRecords ? reciteRecords.length : 0);
|
|
|
|
|
|
|
|
|
|
// 检查是否有背诵记录
|
|
|
|
|
if (!reciteRecords || reciteRecords.length === 0) {
|
|
|
|
|
console.log('用户暂无背诵记录');
|
|
|
|
|
this.setData({
|
|
|
|
|
poemsToReview: [],
|
|
|
|
|
复习计划数据: [],
|
|
|
|
|
todayReviewCount: 0,
|
|
|
|
|
hasRecitationRecords: false,
|
|
|
|
|
isLoading: false
|
|
|
|
|
});
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 为每条记录获取完整的诗歌信息,特别是作者信息
|
|
|
|
|
reciteRecords = await this.enrichRecordsWithPoemDetails(reciteRecords);
|
|
|
|
|
|
|
|
|
|
// 使用SM2算法计算复习计划
|
|
|
|
|
const poemsWithReviewPlan = this.calculateReviewPlan(recordsToUse);
|
|
|
|
|
const poemsWithReviewPlan = this.calculateReviewPlan(reciteRecords);
|
|
|
|
|
|
|
|
|
|
// 生成未来7天的复习计划数据(用于图表显示)
|
|
|
|
|
const reviewPlanData = this.generateReviewPlanData(poemsWithReviewPlan);
|
|
|
|
|
@ -116,27 +130,23 @@ Page({
|
|
|
|
|
this.setData({
|
|
|
|
|
poemsToReview: poemsWithReviewPlan,
|
|
|
|
|
复习计划数据: reviewPlanData,
|
|
|
|
|
todayReviewCount
|
|
|
|
|
todayReviewCount,
|
|
|
|
|
hasRecitationRecords: true,
|
|
|
|
|
isLoading: false
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
wx.hideLoading()
|
|
|
|
|
return poemsWithReviewPlan;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
wx.hideLoading()
|
|
|
|
|
console.error('加载复习数据失败:', error);
|
|
|
|
|
// 出错时使用模拟数据
|
|
|
|
|
const mockRecords = this.getMockReciteRecords();
|
|
|
|
|
const poemsWithReviewPlan = this.calculateReviewPlan(mockRecords);
|
|
|
|
|
const reviewPlanData = this.generateReviewPlanData(poemsWithReviewPlan);
|
|
|
|
|
const todayReviewCount = poemsWithReviewPlan.filter(p => p.needsReviewToday).length;
|
|
|
|
|
|
|
|
|
|
this.setData({
|
|
|
|
|
poemsToReview: poemsWithReviewPlan,
|
|
|
|
|
复习计划数据: reviewPlanData,
|
|
|
|
|
todayReviewCount
|
|
|
|
|
poemsToReview: [],
|
|
|
|
|
复习计划数据: [],
|
|
|
|
|
todayReviewCount: 0,
|
|
|
|
|
hasRecitationRecords: false,
|
|
|
|
|
isLoading: false
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return poemsWithReviewPlan;
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
@ -198,59 +208,7 @@ Page({
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取模拟的背诵记录数据
|
|
|
|
|
*/
|
|
|
|
|
getMockReciteRecords() {
|
|
|
|
|
// 仅在数据库加载失败时使用模拟数据
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
const dayMs = 24 * 60 * 60 * 1000;
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
poemId: 'poet_001',
|
|
|
|
|
poemTitle: '静夜思',
|
|
|
|
|
poemAuthor: '李白',
|
|
|
|
|
lastReciteTime: now - dayMs, // 1天前
|
|
|
|
|
accuracy: 85,
|
|
|
|
|
reciteCount: 2,
|
|
|
|
|
ef: 2.3,
|
|
|
|
|
nextReviewInterval: 1
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
poemId: 'poet_002',
|
|
|
|
|
poemTitle: '春晓',
|
|
|
|
|
poemAuthor: '孟浩然',
|
|
|
|
|
lastReciteTime: now - 3 * dayMs, // 3天前
|
|
|
|
|
accuracy: 60,
|
|
|
|
|
reciteCount: 1,
|
|
|
|
|
ef: 2.0,
|
|
|
|
|
nextReviewInterval: 1
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
poemId: 'poet_003',
|
|
|
|
|
poemTitle: '望庐山瀑布',
|
|
|
|
|
poemAuthor: '李白',
|
|
|
|
|
lastReciteTime: now - 0.5 * dayMs, // 0.5天前
|
|
|
|
|
accuracy: 95,
|
|
|
|
|
reciteCount: 3,
|
|
|
|
|
ef: 2.7,
|
|
|
|
|
nextReviewInterval: 6
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
poemId: 'poet_004',
|
|
|
|
|
poemTitle: '登鹳雀楼',
|
|
|
|
|
poemAuthor: '王之涣',
|
|
|
|
|
lastReciteTime: now - 7 * dayMs, // 7天前
|
|
|
|
|
accuracy: 70,
|
|
|
|
|
reciteCount: 2,
|
|
|
|
|
ef: 2.1,
|
|
|
|
|
nextReviewInterval: 3
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 从数据库加载真实的背诵记录
|
|
|
|
|
* 从数据库加载真实的背诵记录 - 修改为每首诗只返回最新记录
|
|
|
|
|
*/
|
|
|
|
|
async loadReciteRecordsFromDatabase() {
|
|
|
|
|
try {
|
|
|
|
|
@ -263,19 +221,39 @@ Page({
|
|
|
|
|
throw new Error('获取用户信息失败')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 查询用户的背诵记录
|
|
|
|
|
console.log('当前用户openid:', result.openid);
|
|
|
|
|
|
|
|
|
|
// 查询用户的背诵记录,按诗歌ID分组,获取每组的最新记录
|
|
|
|
|
const db = wx.cloud.database()
|
|
|
|
|
const records = await db.collection('Review')
|
|
|
|
|
const allRecords = await db.collection('Review')
|
|
|
|
|
.where({
|
|
|
|
|
openid: result.openid
|
|
|
|
|
})
|
|
|
|
|
.orderBy('reciteDateTime', 'desc')
|
|
|
|
|
.get()
|
|
|
|
|
|
|
|
|
|
console.log('从数据库获取的背诵记录:', records.data);
|
|
|
|
|
console.log('从数据库获取的所有背诵记录:', allRecords.data);
|
|
|
|
|
|
|
|
|
|
// 按poemId分组,只保留每个poemId的最新记录
|
|
|
|
|
const latestRecordsMap = new Map();
|
|
|
|
|
|
|
|
|
|
allRecords.data.forEach(record => {
|
|
|
|
|
const poemId = record.poemId;
|
|
|
|
|
// 如果还没有这个poemId的记录,或者当前记录时间更晚,则更新
|
|
|
|
|
if (!latestRecordsMap.has(poemId) ||
|
|
|
|
|
new Date(record.reciteDateTime) > new Date(latestRecordsMap.get(poemId).reciteDateTime)) {
|
|
|
|
|
latestRecordsMap.set(poemId, record);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 将Map转换为数组
|
|
|
|
|
const uniqueRecords = Array.from(latestRecordsMap.values());
|
|
|
|
|
|
|
|
|
|
console.log('去重后的最新记录数量:', uniqueRecords.length);
|
|
|
|
|
console.log('去重后的记录:', uniqueRecords);
|
|
|
|
|
|
|
|
|
|
// 转换数据库记录格式,确保字段名一致
|
|
|
|
|
const formattedRecords = records.data.map(record => {
|
|
|
|
|
const formattedRecords = uniqueRecords.map(record => {
|
|
|
|
|
return {
|
|
|
|
|
poemId: record.poemId,
|
|
|
|
|
poemTitle: record.poemName || '未知标题',
|
|
|
|
|
@ -291,58 +269,18 @@ Page({
|
|
|
|
|
return formattedRecords;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('加载背诵记录失败:', error);
|
|
|
|
|
// 如果加载失败,返回空数组,让上层函数处理
|
|
|
|
|
// 如果加载失败,返回空数组
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 手动初始化数据库
|
|
|
|
|
*/
|
|
|
|
|
async initDatabaseManually() {
|
|
|
|
|
try {
|
|
|
|
|
wx.showLoading({ title: '初始化数据库...' })
|
|
|
|
|
|
|
|
|
|
const result = await wx.cloud.callFunction({
|
|
|
|
|
name: 'initDatabase'
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
wx.hideLoading()
|
|
|
|
|
|
|
|
|
|
if (result.result && result.result.success) {
|
|
|
|
|
wx.showToast({
|
|
|
|
|
title: '数据库初始化成功',
|
|
|
|
|
icon: 'success'
|
|
|
|
|
})
|
|
|
|
|
// 重新加载数据
|
|
|
|
|
this.loadPoemsToReview()
|
|
|
|
|
} else {
|
|
|
|
|
wx.showToast({
|
|
|
|
|
title: result.result?.message || '初始化失败',
|
|
|
|
|
icon: 'none'
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
wx.hideLoading()
|
|
|
|
|
wx.showToast({
|
|
|
|
|
title: '初始化失败: ' + error.message,
|
|
|
|
|
icon: 'none'
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 基于SM2算法计算复习计划和优先级
|
|
|
|
|
*/
|
|
|
|
|
calculateReviewPlan(records) {
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
const sm2 = new SM2();
|
|
|
|
|
|
|
|
|
|
// 去重处理,确保每首诗只出现一次
|
|
|
|
|
// 去重处理,确保每首诗只出现一次(这里已经是去重后的数据,但再加一层保障)
|
|
|
|
|
const uniqueRecords = [];
|
|
|
|
|
const poemIdSet = new Set();
|
|
|
|
|
|
|
|
|
|
// 优先保留最新的记录(根据传入的顺序,假设前面的记录更新)
|
|
|
|
|
records.forEach(record => {
|
|
|
|
|
const poemId = record.poemId || record._id;
|
|
|
|
|
if (poemId && !poemIdSet.has(poemId)) {
|
|
|
|
|
@ -351,20 +289,20 @@ Page({
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log('去重后的记录数量:', uniqueRecords.length);
|
|
|
|
|
console.log('最终去重后的记录数量:', uniqueRecords.length);
|
|
|
|
|
|
|
|
|
|
// 计算每首诗的复习状态和下次复习时间
|
|
|
|
|
const poemsWithReviewStatus = uniqueRecords.map(record => {
|
|
|
|
|
// 标准化字段名,确保兼容性
|
|
|
|
|
// 标准化字段名
|
|
|
|
|
const poem = {
|
|
|
|
|
id: record.poemId || record._id,
|
|
|
|
|
title: record.poemTitle || record.poemName || '未知标题',
|
|
|
|
|
author: record.poemAuthor || '未知作者',
|
|
|
|
|
lastReciteTime: record.lastReciteTime || (record.reciteDateTime ? new Date(record.reciteDateTime).getTime() : now - 24 * 60 * 60 * 1000),
|
|
|
|
|
accuracy: record.accuracy || record.matchRate || 0,
|
|
|
|
|
lastReciteTime: record.lastReciteTime,
|
|
|
|
|
accuracy: record.accuracy || 0,
|
|
|
|
|
reciteCount: record.reciteCount || 0,
|
|
|
|
|
ef: record.ef || 2.5, // 易度因子
|
|
|
|
|
nextReviewInterval: record.nextReviewInterval || 1 // 下次复习间隔
|
|
|
|
|
ef: record.ef || 2.5,
|
|
|
|
|
nextReviewInterval: record.nextReviewInterval || 1
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 计算距离上次背诵的时间(天)
|
|
|
|
|
@ -378,32 +316,39 @@ Page({
|
|
|
|
|
else if (poem.accuracy >= 40) quality = 2;
|
|
|
|
|
else if (poem.accuracy >= 20) quality = 1;
|
|
|
|
|
|
|
|
|
|
// 使用SM2计算下次复习间隔
|
|
|
|
|
// 使用SM2计算下次复习间隔(传入当前状态)
|
|
|
|
|
const sm2 = new SM2();
|
|
|
|
|
sm2.ef = poem.ef;
|
|
|
|
|
sm2.reps = poem.reciteCount;
|
|
|
|
|
sm2.lastInterval = poem.nextReviewInterval;
|
|
|
|
|
|
|
|
|
|
const reviewResult = sm2.calculateNextReview(quality);
|
|
|
|
|
|
|
|
|
|
// 判断是否需要今天复习
|
|
|
|
|
const needsReviewToday = daysSinceLastRecite >= poem.nextReviewInterval - 0.1; // 允许0.1天的误差
|
|
|
|
|
|
|
|
|
|
// 计算记忆保留率(基于艾宾浩斯公式)
|
|
|
|
|
const hoursSinceLastReview = daysSinceLastRecite * 24;
|
|
|
|
|
const retentionRate = sm2.calculateRetentionRate(hoursSinceLastReview, poem.ef * 10);
|
|
|
|
|
// 计算距离下次复习还有多少天
|
|
|
|
|
const daysUntilNextReview = Math.max(0, Math.ceil(reviewResult.nextInterval - daysSinceLastRecite));
|
|
|
|
|
|
|
|
|
|
// 判断是否需要今天复习 - 统一使用一个变量声明
|
|
|
|
|
const needsReviewToday = daysUntilNextReview === 0;
|
|
|
|
|
|
|
|
|
|
console.log(`《${poem.title}》:
|
|
|
|
|
距离上次: ${daysSinceLastRecite.toFixed(1)}天
|
|
|
|
|
SM2建议间隔: ${reviewResult.nextInterval}天
|
|
|
|
|
距离下次复习: ${daysUntilNextReview}天
|
|
|
|
|
需要今天复习: ${needsReviewToday}`);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...poem,
|
|
|
|
|
daysSinceLastRecite: Math.round(daysSinceLastRecite * 10) / 10, // 保留一位小数
|
|
|
|
|
daysSinceLastRecite: Math.round(daysSinceLastRecite * 10) / 10,
|
|
|
|
|
nextReviewInterval: reviewResult.nextInterval,
|
|
|
|
|
newEF: reviewResult.newEF,
|
|
|
|
|
needsReviewToday,
|
|
|
|
|
retentionRate,
|
|
|
|
|
reviewUrgency: needsReviewToday ? (100 - retentionRate) : 0
|
|
|
|
|
daysUntilNextReview, // 这个字段用于显示
|
|
|
|
|
retentionRate: sm2.calculateRetentionRate(daysSinceLastRecite * 24, poem.ef * 10),
|
|
|
|
|
reviewUrgency: needsReviewToday ? (100 - (sm2.calculateRetentionRate(daysSinceLastRecite * 24, poem.ef * 10) || 0)) : daysUntilNextReview
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 按复习紧急程度排序(今天需要复习的排前面,按记忆保留率从低到高)
|
|
|
|
|
// 按复习紧急程度排序
|
|
|
|
|
poemsWithReviewStatus.sort((a, b) => {
|
|
|
|
|
if (a.needsReviewToday && !b.needsReviewToday) return -1;
|
|
|
|
|
if (!a.needsReviewToday && b.needsReviewToday) return 1;
|
|
|
|
|
@ -412,17 +357,18 @@ Page({
|
|
|
|
|
|
|
|
|
|
return poemsWithReviewStatus;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 生成未来7天的复习计划数据(用于图表显示)
|
|
|
|
|
*/
|
|
|
|
|
generateReviewPlanData(poems) {
|
|
|
|
|
const planData = [];
|
|
|
|
|
const now = new Date();
|
|
|
|
|
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
|
|
|
|
|
|
|
|
// 初始化未来7天的数据
|
|
|
|
|
for (let i = 0; i < 7; i++) {
|
|
|
|
|
const date = new Date(now);
|
|
|
|
|
const date = new Date(todayStart);
|
|
|
|
|
date.setDate(date.getDate() + i);
|
|
|
|
|
planData.push({
|
|
|
|
|
day: i === 0 ? '今天' : i === 1 ? '明天' : `${i}天后`,
|
|
|
|
|
@ -433,27 +379,18 @@ Page({
|
|
|
|
|
|
|
|
|
|
// 统计每天需要复习的古诗数量
|
|
|
|
|
poems.forEach(poem => {
|
|
|
|
|
// 优先使用needsReviewToday标志来判断是否今天需要复习
|
|
|
|
|
if (poem.needsReviewToday) {
|
|
|
|
|
// 如果标记为今天需要复习,直接计入今天的数量
|
|
|
|
|
planData[0].count++;
|
|
|
|
|
return; // 跳过后续计算
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 对于不是今天需要复习的,正常计算下次复习日期
|
|
|
|
|
const lastReviewDate = new Date(poem.lastReciteTime);
|
|
|
|
|
const nextReviewDate = new Date(lastReviewDate);
|
|
|
|
|
nextReviewDate.setDate(nextReviewDate.getDate() + poem.nextReviewInterval);
|
|
|
|
|
// 直接使用计算好的 daysUntilNextReview
|
|
|
|
|
const daysUntilNextReview = poem.daysUntilNextReview;
|
|
|
|
|
|
|
|
|
|
// 计算下次复习日期距离今天的天数(使用宽松计算,不四舍五入)
|
|
|
|
|
const daysUntilNextReview = Math.floor((nextReviewDate - now) / (24 * 60 * 60 * 1000));
|
|
|
|
|
console.log(`《${poem.title}》: ${daysUntilNextReview}天后复习`);
|
|
|
|
|
|
|
|
|
|
// 更新对应天数的复习数量
|
|
|
|
|
// 分配到对应的天数
|
|
|
|
|
if (daysUntilNextReview >= 0 && daysUntilNextReview < 7) {
|
|
|
|
|
planData[daysUntilNextReview].count++;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log('复习计划数据:', planData);
|
|
|
|
|
return planData;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
@ -578,157 +515,169 @@ Page({
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 绘制复习计划图表
|
|
|
|
|
* 显示未来7天每天需要复习的古诗数量
|
|
|
|
|
*/
|
|
|
|
|
drawReviewPlanChart() {
|
|
|
|
|
const ctx = wx.createCanvasContext('reviewPlanChart');
|
|
|
|
|
const { canvasWidth, canvasHeight } = this.data;
|
|
|
|
|
const data = this.data.复习计划数据;
|
|
|
|
|
|
|
|
|
|
// 确保有数据
|
|
|
|
|
if (!data || data.length === 0) {
|
|
|
|
|
// 绘制无数据状态
|
|
|
|
|
ctx.setFillStyle('#ffffff');
|
|
|
|
|
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
|
|
|
|
|
ctx.setFillStyle('#999999');
|
|
|
|
|
ctx.setFontSize(14);
|
|
|
|
|
ctx.setTextAlign('center');
|
|
|
|
|
ctx.fillText('暂无复习计划数据', canvasWidth / 2, canvasHeight / 2);
|
|
|
|
|
ctx.draw();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 设置画布背景
|
|
|
|
|
* 绘制复习计划图表
|
|
|
|
|
* 显示未来7天每天需要复习的古诗数量
|
|
|
|
|
*/
|
|
|
|
|
drawReviewPlanChart() {
|
|
|
|
|
const ctx = wx.createCanvasContext('reviewPlanChart');
|
|
|
|
|
const { canvasWidth, canvasHeight } = this.data;
|
|
|
|
|
const data = this.data.复习计划数据;
|
|
|
|
|
|
|
|
|
|
// 确保有数据
|
|
|
|
|
if (!data || data.length === 0 || !this.data.hasRecitationRecords) {
|
|
|
|
|
// 绘制无数据状态
|
|
|
|
|
ctx.setFillStyle('#ffffff');
|
|
|
|
|
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
|
|
|
|
|
|
|
|
|
|
// 计算坐标转换参数
|
|
|
|
|
const padding = 40;
|
|
|
|
|
const plotWidth = canvasWidth - 2 * padding;
|
|
|
|
|
const plotHeight = canvasHeight - 2 * padding;
|
|
|
|
|
const maxCount = Math.max(...data.map(item => item.count), 1); // 至少为1,避免除以0
|
|
|
|
|
|
|
|
|
|
// 绘制坐标轴
|
|
|
|
|
ctx.setStrokeStyle('#e0e0e0');
|
|
|
|
|
ctx.setLineWidth(1);
|
|
|
|
|
|
|
|
|
|
// 垂直网格线
|
|
|
|
|
for (let i = 0; i <= 6; i++) {
|
|
|
|
|
const x = padding + (plotWidth / 6) * i;
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.moveTo(x, padding);
|
|
|
|
|
ctx.lineTo(x, canvasHeight - padding);
|
|
|
|
|
ctx.stroke();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 水平网格线
|
|
|
|
|
const maxYAxisValue = Math.ceil(maxCount * 1.2); // 留出20%的顶部空间
|
|
|
|
|
const yAxisSteps = Math.min(5, maxYAxisValue); // 最多5条水平线
|
|
|
|
|
for (let i = 0; i <= yAxisSteps; i++) {
|
|
|
|
|
const y = padding + (plotHeight / yAxisSteps) * i;
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.moveTo(padding, y);
|
|
|
|
|
ctx.lineTo(canvasWidth - padding, y);
|
|
|
|
|
ctx.stroke();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 绘制坐标轴标签
|
|
|
|
|
ctx.setFillStyle('#666666');
|
|
|
|
|
ctx.setFontSize(12);
|
|
|
|
|
ctx.setFillStyle('#999999');
|
|
|
|
|
ctx.setFontSize(14);
|
|
|
|
|
ctx.setTextAlign('center');
|
|
|
|
|
ctx.fillText('暂无复习计划数据', canvasWidth / 2, canvasHeight / 2);
|
|
|
|
|
ctx.draw();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 设置画布背景
|
|
|
|
|
ctx.setFillStyle('#ffffff');
|
|
|
|
|
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
|
|
|
|
|
|
|
|
|
|
// 计算坐标转换参数
|
|
|
|
|
const padding = 40;
|
|
|
|
|
const plotWidth = canvasWidth - 2 * padding;
|
|
|
|
|
const plotHeight = canvasHeight - 2 * padding;
|
|
|
|
|
|
|
|
|
|
// 计算Y轴最大值(至少为1,避免除以0)
|
|
|
|
|
const maxCount = Math.max(...data.map(item => item.count), 1);
|
|
|
|
|
const maxYAxisValue = Math.max(maxCount, 5); // 确保Y轴至少有5的刻度,避免只有1个数据点时图表太扁
|
|
|
|
|
|
|
|
|
|
// 绘制坐标轴
|
|
|
|
|
ctx.setStrokeStyle('#e0e0e0');
|
|
|
|
|
ctx.setLineWidth(1);
|
|
|
|
|
|
|
|
|
|
// 垂直网格线 - 修正:当只有1个数据点时也要正确绘制
|
|
|
|
|
const dataPoints = data.length;
|
|
|
|
|
const xStep = dataPoints > 1 ? plotWidth / (dataPoints - 1) : plotWidth;
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < dataPoints; i++) {
|
|
|
|
|
const x = padding + (dataPoints > 1 ? (plotWidth / (dataPoints - 1)) * i : 0);
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.moveTo(x, padding);
|
|
|
|
|
ctx.lineTo(x, canvasHeight - padding);
|
|
|
|
|
ctx.stroke();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 水平网格线
|
|
|
|
|
const yAxisSteps = Math.min(5, maxYAxisValue);
|
|
|
|
|
for (let i = 0; i <= yAxisSteps; i++) {
|
|
|
|
|
const y = padding + (plotHeight / yAxisSteps) * i;
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.moveTo(padding, y);
|
|
|
|
|
ctx.lineTo(canvasWidth - padding, y);
|
|
|
|
|
ctx.stroke();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 绘制坐标轴标签
|
|
|
|
|
ctx.setFillStyle('#666666');
|
|
|
|
|
ctx.setFontSize(12);
|
|
|
|
|
ctx.setTextAlign('center');
|
|
|
|
|
|
|
|
|
|
// X轴标签(显示日期)- 修正:根据实际数据点数量计算位置
|
|
|
|
|
data.forEach((item, index) => {
|
|
|
|
|
const x = padding + (dataPoints > 1 ? (plotWidth / (dataPoints - 1)) * index : plotWidth / 2);
|
|
|
|
|
|
|
|
|
|
// X轴标签(显示日期)
|
|
|
|
|
data.forEach((item, index) => {
|
|
|
|
|
const x = padding + (plotWidth / 6) * index;
|
|
|
|
|
|
|
|
|
|
// 绘制日期
|
|
|
|
|
ctx.fillText(item.dateStr, x, canvasHeight - padding + 15);
|
|
|
|
|
|
|
|
|
|
// 绘制天数
|
|
|
|
|
ctx.fillText(item.day, x, canvasHeight - padding + 30);
|
|
|
|
|
});
|
|
|
|
|
// 绘制日期
|
|
|
|
|
ctx.fillText(item.dateStr, x, canvasHeight - padding + 15);
|
|
|
|
|
|
|
|
|
|
// Y轴标签
|
|
|
|
|
ctx.setTextAlign('right');
|
|
|
|
|
for (let i = 0; i <= yAxisSteps; i++) {
|
|
|
|
|
const count = maxYAxisValue * (1 - i / yAxisSteps);
|
|
|
|
|
const y = padding + (plotHeight / yAxisSteps) * i;
|
|
|
|
|
ctx.fillText(Math.round(count), padding - 10, y + 5);
|
|
|
|
|
}
|
|
|
|
|
// 绘制天数
|
|
|
|
|
ctx.fillText(item.day, x, canvasHeight - padding + 30);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Y轴标签
|
|
|
|
|
ctx.setTextAlign('right');
|
|
|
|
|
for (let i = 0; i <= yAxisSteps; i++) {
|
|
|
|
|
const count = Math.round(maxYAxisValue * (1 - i / yAxisSteps));
|
|
|
|
|
const y = padding + (plotHeight / yAxisSteps) * i;
|
|
|
|
|
ctx.fillText(count.toString(), padding - 10, y + 5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 绘制数据点和连线
|
|
|
|
|
ctx.setStrokeStyle('#4CAF50');
|
|
|
|
|
ctx.setLineWidth(2);
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
|
|
|
|
|
// 先绘制所有点
|
|
|
|
|
const points = data.map((item, index) => {
|
|
|
|
|
const x = padding + (dataPoints > 1 ? (plotWidth / (dataPoints - 1)) * index : plotWidth / 2);
|
|
|
|
|
const valueRatio = item.count / maxYAxisValue;
|
|
|
|
|
const y = canvasHeight - padding - (valueRatio * plotHeight);
|
|
|
|
|
|
|
|
|
|
// 绘制曲线图
|
|
|
|
|
if (data.length > 1) {
|
|
|
|
|
ctx.setStrokeStyle('#4CAF50'); // 使用绿色作为主色调
|
|
|
|
|
ctx.setLineWidth(2);
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
|
|
|
|
|
data.forEach((item, index) => {
|
|
|
|
|
const x = padding + (plotWidth / 6) * index;
|
|
|
|
|
const valueRatio = item.count / maxYAxisValue;
|
|
|
|
|
const y = canvasHeight - padding - (valueRatio * plotHeight);
|
|
|
|
|
|
|
|
|
|
// 绘制点
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.arc(x, y, 4, 0, 2 * Math.PI);
|
|
|
|
|
ctx.fillStyle = '#4CAF50';
|
|
|
|
|
ctx.fill();
|
|
|
|
|
return { x, y, count: item.count };
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 绘制连线(只有在有多个数据点时)
|
|
|
|
|
if (dataPoints > 1) {
|
|
|
|
|
points.forEach((point, index) => {
|
|
|
|
|
if (index === 0) {
|
|
|
|
|
ctx.moveTo(point.x, point.y);
|
|
|
|
|
} else {
|
|
|
|
|
// 使用贝塞尔曲线使线条更平滑
|
|
|
|
|
const prevPoint = points[index - 1];
|
|
|
|
|
const cpx1 = prevPoint.x + (point.x - prevPoint.x) / 2;
|
|
|
|
|
const cpy1 = prevPoint.y;
|
|
|
|
|
const cpx2 = prevPoint.x + (point.x - prevPoint.x) / 2;
|
|
|
|
|
const cpy2 = point.y;
|
|
|
|
|
|
|
|
|
|
// 连接点形成曲线
|
|
|
|
|
if (index === 0) {
|
|
|
|
|
ctx.moveTo(x, y);
|
|
|
|
|
} else {
|
|
|
|
|
// 使用贝塞尔曲线使线条更平滑
|
|
|
|
|
const prevItem = data[index - 1];
|
|
|
|
|
const prevX = padding + (plotWidth / 6) * (index - 1);
|
|
|
|
|
const prevY = canvasHeight - padding - ((prevItem.count / maxYAxisValue) * plotHeight);
|
|
|
|
|
|
|
|
|
|
const cpx1 = prevX + (x - prevX) / 2;
|
|
|
|
|
const cpy1 = prevY;
|
|
|
|
|
const cpx2 = prevX + (x - prevX) / 2;
|
|
|
|
|
const cpy2 = y;
|
|
|
|
|
|
|
|
|
|
ctx.bezierCurveTo(cpx1, cpy1, cpx2, cpy2, x, y);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
ctx.stroke();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 绘制每个数据点的值
|
|
|
|
|
ctx.setFillStyle('#333333');
|
|
|
|
|
ctx.setFontSize(12);
|
|
|
|
|
ctx.setTextAlign('center');
|
|
|
|
|
|
|
|
|
|
data.forEach((item, index) => {
|
|
|
|
|
const x = padding + (plotWidth / 6) * index;
|
|
|
|
|
const valueRatio = item.count / maxYAxisValue;
|
|
|
|
|
const y = canvasHeight - padding - (valueRatio * plotHeight);
|
|
|
|
|
|
|
|
|
|
// 在点的上方显示数值
|
|
|
|
|
ctx.fillText(item.count.toString(), x, y - 10);
|
|
|
|
|
ctx.bezierCurveTo(cpx1, cpy1, cpx2, cpy2, point.x, point.y);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 绘制标题
|
|
|
|
|
ctx.setFillStyle('#333333');
|
|
|
|
|
ctx.setFontSize(16);
|
|
|
|
|
ctx.setTextAlign('center');
|
|
|
|
|
ctx.fillText('未来7天复习计划', canvasWidth / 2, padding - 10);
|
|
|
|
|
|
|
|
|
|
ctx.draw();
|
|
|
|
|
},
|
|
|
|
|
ctx.stroke();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 删除不再需要的辅助函数,所有功能现在在drawReviewPlanChart中集中实现
|
|
|
|
|
// 绘制数据点
|
|
|
|
|
points.forEach(point => {
|
|
|
|
|
ctx.beginPath();
|
|
|
|
|
ctx.arc(point.x, point.y, 4, 0, 2 * Math.PI);
|
|
|
|
|
ctx.fillStyle = '#4CAF50';
|
|
|
|
|
ctx.fill();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 绘制每个数据点的值
|
|
|
|
|
ctx.setFillStyle('#333333');
|
|
|
|
|
ctx.setFontSize(12);
|
|
|
|
|
ctx.setTextAlign('center');
|
|
|
|
|
|
|
|
|
|
points.forEach(point => {
|
|
|
|
|
// 在点的上方显示数值
|
|
|
|
|
ctx.fillText(point.count.toString(), point.x, point.y - 10);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 绘制标题
|
|
|
|
|
ctx.setFillStyle('#333333');
|
|
|
|
|
ctx.setFontSize(16);
|
|
|
|
|
ctx.setTextAlign('center');
|
|
|
|
|
ctx.fillText('未来7天复习计划', canvasWidth / 2, padding - 10);
|
|
|
|
|
|
|
|
|
|
// 如果没有数据点连线,显示提示
|
|
|
|
|
if (dataPoints === 1) {
|
|
|
|
|
ctx.setFillStyle('#666666');
|
|
|
|
|
ctx.setFontSize(12);
|
|
|
|
|
ctx.fillText('(只有今天有复习计划)', canvasWidth / 2, padding - 25);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx.draw();
|
|
|
|
|
},
|
|
|
|
|
/**
|
|
|
|
|
* 页面相关事件处理函数--监听用户下拉动作
|
|
|
|
|
*/
|
|
|
|
|
onPullDownRefresh() {
|
|
|
|
|
// 刷新页面数据
|
|
|
|
|
this.drawForgettingCurve();
|
|
|
|
|
wx.stopPullDownRefresh();
|
|
|
|
|
this.loadPoemsToReview().then(() => {
|
|
|
|
|
// 数据加载完成后绘制图表
|
|
|
|
|
this.drawReviewPlanChart();
|
|
|
|
|
wx.stopPullDownRefresh();
|
|
|
|
|
}).catch(error => {
|
|
|
|
|
console.error('下拉刷新失败:', error);
|
|
|
|
|
wx.stopPullDownRefresh();
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|