diff --git a/popup.html b/popup.html
index 38cbfba..e3577b1 100644
--- a/popup.html
+++ b/popup.html
@@ -518,6 +518,38 @@
Active Reminder
2. Disable to stop all pop-up reminders
+
+
+
+
+
+
+
Fsrs Param Optim
+
+
+
+
+
+
+
+
+
+
+ Current review count: 0
+
+
+
+ Click to optimize FSRS parameters
+
+
+ (Fit best parameters when you have enough data.)
+
+
+
diff --git a/src/popup/daily-review.js b/src/popup/daily-review.js
index 18279b3..c42b161 100644
--- a/src/popup/daily-review.js
+++ b/src/popup/daily-review.js
@@ -1,1050 +1,1358 @@
-import { renderAll } from './view/view.js';
-import { getAllProblems, syncProblems } from "./service/problemService.js";
-import { getLevelColor,getCurrentRetrievability } from './util/utils.js';
-import { handleFeedbackSubmission, handleAddBlankProblem } from './script/submission.js';
-import './popup.css';
-import { isCloudSyncEnabled, loadConfigs, setCloudSyncEnabled, setProblemSorter,setDefaultCardLimit,setReminderEnabled } from "./service/configService";
-import { store,daily_store } from './store';
-import { optionPageFeedbackMsgDOM } from './util/doms';
-import { descriptionOf, idOf, problemSorterArr } from "./util/sort";
-import {handleAddProblem} from "./script/submission.js"
-// 在文件顶部导入 SweetAlert2
-import Swal from 'sweetalert2';
-// 导入 getAllRevlogs 函数
-import { getAllRevlogs, exportRevlogsToCSV } from './util/fsrs.js';
-
-// 在文件开头添加
-const LAST_AVERAGE_KEY = 'lastRetrievabilityAverage';
-const LAST_UPDATE_TIME_KEY = 'lastUpdateTime';
-let yesterdayRetrievabilityAverage = 0.00;
-
-
-
-async function loadProblemList() {
- await renderAll();
-}
-
-
-// 获取上次存储的平均值和时间
-function loadLastAverageData() {
- const lastData = {
- average: parseFloat(localStorage.getItem(LAST_AVERAGE_KEY)) || 0.00,
- timestamp: parseInt(localStorage.getItem(LAST_UPDATE_TIME_KEY)) || 0
- };
- return lastData;
-}
-
-async function loadDailyReviewData() {
- const problems = Object.values(await getAllProblems()).filter(p => p.isDeleted !== true);
- daily_store.reviewScheduledProblems = problems
- .sort((a, b) => {
- const retrievabilityA = getCurrentRetrievability(a);
- const retrievabilityB = getCurrentRetrievability(b);
- return retrievabilityA - retrievabilityB; // 升序排序,最小值在前
- });
-
- // 获取今天已复习和待复习的题目
- daily_store.dailyReviewProblems = daily_store.reviewScheduledProblems
- .filter(problem => isReviewedToday(problem) || isReviewDueToday(problem))
- .sort((a, b) => {
- // 首先按照是否已复习排序(已复习的排在前面)
- const aReviewed = isReviewedToday(a);
- const bReviewed = isReviewedToday(b);
- if (aReviewed !== bReviewed) {
- return bReviewed ? 1 : -1;
- }
- // 如果复习状态相同,则按可检索性排序
- const retrievabilityA = getCurrentRetrievability(a);
- const retrievabilityB = getCurrentRetrievability(b);
- return retrievabilityA - retrievabilityB;
- });
-
-
- console.log('总题目数:', problems.length);
- console.log('今日待复习题目数:', daily_store.dailyReviewProblems.length);
-
- // 添加调试日志
- daily_store.dailyReviewProblems.forEach(problem => {
- const isReviewed = isReviewedToday(problem);
- const isDue = isReviewDueToday(problem);
- console.log('题目状态:', {
- name: problem.name,
- lastReview: problem.fsrsState?.lastReview,
- nextReview: problem.fsrsState?.nextReview,
- isReviewedToday: isReviewed,
- isDueToday: isDue,
- retrievability: getCurrentRetrievability(problem)
- });
- });
-}
-
-// 存储当前的平均值和时间
-function saveCurrentAverageData(average) {
- localStorage.setItem(LAST_AVERAGE_KEY, average.toString());
- localStorage.setItem(LAST_UPDATE_TIME_KEY, Date.now().toString());
-}
-
-// 设置当前日期
-function setCurrentDate() {
- const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
- const today = new Date().toLocaleDateString('en-US', options);
- document.getElementById('currentDate').textContent = today;
-}
-
-
-// 判断是否是今天需要复习的题目
-function isReviewDueToday(problem) {
- if (!problem.fsrsState?.nextReview) {
- console.log('题目没有下次复习时间:', problem.name);
- return false;
- }
-
- const today = new Date();
- today.setHours(0, 0, 0, 0);
-
- const nextReview = new Date(problem.fsrsState.nextReview);
- nextReview.setHours(0, 0, 0, 0);
-
- const isDue = nextReview <= today;
-
- console.log('复习时间检查:', {
- problemName: problem.name,
- nextReview: nextReview.toISOString(),
- today: today.toISOString(),
- isDue: isDue
- });
-
- return isDue;
-}
-
-function isReviewedToday(problem) {
- if (!problem.fsrsState?.lastReview) return false;
-
- const today = new Date();
- today.setHours(0, 0, 0, 0);
-
- const lastReview = new Date(problem.fsrsState.lastReview);
- lastReview.setHours(0, 0, 0, 0);
-
- return lastReview.getTime() === today.getTime();
-}
-
-
-
-
-// 计算可检索性均值
-function calculateRetrievabilityAverage() {
- const problems = daily_store.reviewScheduledProblems;
- if (!problems || problems.length === 0) return 0;
-
- const totalRetrievability = problems.reduce((sum, problem) => {
- const retrievability = getCurrentRetrievability(problem);
- return sum + retrievability;
- }, 0);
-
- return Number((totalRetrievability / problems.length).toFixed(2));
-}
-
-
-// 更新顶部统计信息
-function updateStats() {
- console.log('更新统计信息');
- // 设置默认值
- let completedCount = 0;
- let totalProblems = 0;
- // 添加空值检查
- if (!daily_store || !daily_store.dailyReviewProblems) {
- console.log('daily_store 或 dailyReviewProblems 为空:', {
- daily_store: daily_store,
- problems: daily_store?.dailyReviewProblems
- });
-
-
- // 更新显示
- document.getElementById('completedCount').textContent = completedCount;
- document.getElementById('totalCount').textContent = totalProblems;
- document.getElementById('completionRate').textContent = '0%';
- updateProgressCircle(0);
- return;
- }
-
-
-
- // 获取当前显示的卡片数量
- let cardLimit = parseInt(document.getElementById('cardLimit').value, 10)|| store.defaultCardLimit || 1;
- console.log('当前卡片限制值:', {
- rawValue: document.getElementById('cardLimit').value,
- parsedCardLimit: cardLimit,
- element: document.getElementById('cardLimit')
- });
-
-
-
- // 计算今日已复习的题目数量
- completedCount = daily_store.dailyReviewProblems.filter(problem =>
- isReviewedToday(problem)
- ).length;
-
-
- totalProblems = daily_store.dailyReviewProblems?.length || 0;
- if (cardLimit > totalProblems) {
- cardLimit = totalProblems;
- }
-
- // 添加空状态提示
- const addProblemWrapper = document.querySelector('.add-problem-wrapper');
- // 先移除可能存在的空状态提示
- const existingEmptyState = document.querySelector('.empty-state');
- if (existingEmptyState) {
- existingEmptyState.remove();
- }
-
- if (totalProblems === 0 || cardLimit === 0) {
- const emptyState = document.createElement('div');
- emptyState.className = 'empty-state';
- emptyState.innerHTML = `
-
-
- Time to learn something new!
- `;
- addProblemWrapper.insertAdjacentElement('beforebegin', emptyState);
- }
-
-
- // 更新显示的已复习数量
- document.getElementById('completedCount').textContent = completedCount;
- document.getElementById('totalCount').textContent = cardLimit; // 使用当前的卡片数量
-
- // 更新进度条
- const completionRate = cardLimit > 0 ? Math.round((completedCount / cardLimit) * 100) : 0;
- updateProgressCircle(completionRate);
- document.getElementById('completionRate').textContent = `${completionRate}%`;
- // document.querySelector('.completion-circle').style.setProperty('--percentage', `${completionRate}%`);
- // 计算当前的可检索性均值,并确保是数字类型
- const currentRetrievabilityAverage = parseFloat(calculateRetrievabilityAverage()) || 0;
- console.log('当前可检索性均值:', {
- raw: calculateRetrievabilityAverage(),
- parsed: currentRetrievabilityAverage,
- type: typeof currentRetrievabilityAverage
- });
- const retrievabilityElement = document.getElementById('retrievabilityAverage');
- retrievabilityElement.textContent = currentRetrievabilityAverage;
-
-
- // 获取上次存储的数据
- const lastData = loadLastAverageData();
- const hoursSinceLastUpdate = (Date.now() - lastData.timestamp) / (1000 * 60 * 60);
-
- // 如果超过24小时,更新存储的数据
- if (hoursSinceLastUpdate >= 24) {
- console.log('距离上次更新已超过24小时:', {
- hoursSinceLastUpdate: hoursSinceLastUpdate.toFixed(2) + '小时',
- lastUpdateTime: new Date(lastData.timestamp).toLocaleString(),
- lastAverage: lastData.average.toFixed(2),
- currentAverage: currentRetrievabilityAverage.toFixed(2)
- });
-
- yesterdayRetrievabilityAverage = lastData.average;
- saveCurrentAverageData(currentRetrievabilityAverage);
-
- console.log('已更新存储数据:', {
- newYesterdayAverage: yesterdayRetrievabilityAverage.toFixed(2),
- savedCurrentAverage: currentRetrievabilityAverage.toFixed(2),
- saveTime: new Date().toLocaleString()
- });
- } else {
- console.log('距离上次更新未超过24小时:', {
- hoursSinceLastUpdate: hoursSinceLastUpdate.toFixed(2) + '小时',
- lastUpdateTime: new Date(lastData.timestamp).toLocaleString(),
- usingLastAverage: lastData.average.toFixed(2)
- });
- yesterdayRetrievabilityAverage = lastData.average;
- }
-
- // 更新趋势图标
- const trendIcon = document.getElementById('trendIcon');
- if (currentRetrievabilityAverage > yesterdayRetrievabilityAverage) {
- trendIcon.className = 'fas fa-arrow-up trend-icon trend-up';
- } else if (currentRetrievabilityAverage < yesterdayRetrievabilityAverage) {
- trendIcon.className = 'fas fa-arrow-down trend-icon trend-down';
- } else {
- trendIcon.className = '';
- }
-
- // 根据可检索性均值调整颜色和背景提示
- const lowMemoryWarning = document.getElementById('lowMemoryWarning');
- if (currentRetrievabilityAverage < 0.90) {
- retrievabilityElement.classList.add('low');
- lowMemoryWarning.classList.add('active');
- } else {
- retrievabilityElement.classList.remove('low');
- lowMemoryWarning.classList.remove('active');
- }
- updateCardLimitDisplay(); // 这里也添加一次调用
-}
-
-function updateProgressCircle(completionRate) {
- const progressCircle = document.querySelector('.completion-circle');
- const radius = 54; // 圆的半径
- const circumference = 2 * Math.PI * radius; // 圆的周长
-
- // 计算偏移量
- const offset = circumference - (completionRate / 100) * circumference;
- progressCircle.style.strokeDasharray = `${circumference} ${circumference}`;
- progressCircle.style.strokeDashoffset = offset;
-
- // 更新显示的百分比
- // document.getElementById('completionRate').textContent = `${completionRate}%`;
- document.querySelector('.completion-circle').style.setProperty('--percentage', `${completionRate}%`);
-
- // 添加动画效果
- const innerCircle = document.querySelector('.inner-circle');
- innerCircle.style.transform = `scale(1.1)`; // 放大内圈
- setTimeout(() => {
- innerCircle.style.transform = `scale(1)`; // 恢复原状
- }, 500); // 动画持续时间
-}
-
-
-
-
-// 更新卡片限制和显示
-export function updateCardLimitDisplay() {
- const input = document.getElementById('cardLimit');
- const totalDisplay = document.querySelector('.total-problems');
- const totalProblems = daily_store.dailyReviewProblems?.length || 0;
-
- // 更新最大值和总数显示
- input.max = Math.max(totalProblems, 1);
- totalDisplay.textContent = `/ ${totalProblems}`;
-
- // 使用保存的默认值或回退到3
- let currentValue = store.defaultCardLimit || 1;
- if (currentValue > totalProblems && totalProblems > 0) {
- currentValue = totalProblems;
- // store.defaultCardLimit = totalProblems;
- // setDefaultCardLimit(totalProblems);
- }
- input.value = currentValue;
-
- // 禁用条件
- if (totalProblems === 0) {
- input.value = 0;
- input.disabled = true;
- totalDisplay.textContent = "/ 0";
- } else {
- input.disabled = false;
- }
-
- console.log('更新卡片限制显示:', {
- currentValue: input.value,
- max: input.max,
- totalProblems
- });
-}
-
-// 更新卡片显示
-export function updateCardDisplay() {
- console.log('更新卡片显示');
-
- updateStats(); // 更新统计信息,传递当前显示的卡片数量
-
-
- createReviewCards(); // 创建新的卡片
-}
-
-
-
-
-// 改变卡片数量
-// 所有功能函数
-export async function changeCardLimit(delta) {
- console.log('执行 changeCardLimit, delta:', delta);
- const input = document.getElementById('cardLimit');
- const currentValue = parseInt(input.value, 10);
- const newValue = currentValue + delta;
- const maxValue = daily_store.dailyReviewProblems?.length || 0;
-
- if (newValue >= 1 && newValue <= maxValue) {
- input.value = newValue;
- await setDefaultCardLimit(newValue);
- store.defaultCardLimit = newValue;
- updateCardDisplay();
- }
-}
-
-
-
-
-// 标记题目为已复习
-async function markAsReviewed(button, problem) {
- console.log('执行 markAsReviewed', button, problem);
-
- const card = button.closest('.review-card');
- if (!card) {
- console.log('未找到对应的卡片');
- return;
- }
-
- console.log('找到卡片,开始更新状态');
-
- // 更换图标并更改样式
- const icon = button.querySelector('i');
- icon.classList.remove('fa-check-circle');
- icon.classList.add('fa-circle-check');
- icon.style.color = '#0D6E6E';
-
- // 禁用按钮
- button.disabled = true;
- card.style.opacity = '0.4';
-
-
-
- // 更新统计信息
- updateCardDisplay();
- console.log('更新完成');
-}
-
-
-// 创建题目卡片时的事件绑定
-function createReviewCards() {
- console.log('开始创建卡片');
- const reviewList = document.getElementById('reviewList');
- const template = document.getElementById('reviewCardTemplate');
- const cardLimit = parseInt(document.getElementById('cardLimit').value, 10);
-
- reviewList.innerHTML = '';
-
- const problems = daily_store.dailyReviewProblems || [];
- problems.slice(0, cardLimit).forEach((problem, index) => {
- const cardNode = template.content.cloneNode(true);
- const card = cardNode.querySelector('.review-card');
-
- // 安全地访问 fsrsState
- const fsrsState = problem.fsrsState || {};
-
-
- // 设置题目信息
- const problemName = card.querySelector('.problem-name');
- problemName.textContent = problem.name || 'unknown';
-
- // 设置难度和复习信息
- const difficultySpan = card.querySelector('.difficulty');
- const level = problem.level || 'Unknown';
- difficultySpan.textContent = level;
- // 使用现有的 CSS 类
- difficultySpan.classList.add(`difficulty-${level}`);
-
- // 设置可检索性
- const retrievability = getCurrentRetrievability(problem);
- const retrievabilitySpan = card.querySelector('.retrievability');
- retrievabilitySpan.textContent = `${retrievability.toFixed(1)}`;
- retrievabilitySpan.classList.add(retrievability < 0.9 ? 'text-danger' : 'text-success');
-
-
- // 设置下次复习时间
- const nextReviewTips = fsrsState.nextReview
- ? (() => {
- const nextReviewDate = new Date(fsrsState.nextReview);
- const now = new Date();
-
- // 获取当前日期和下次复习日期(不含时间)
- const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
- const reviewDay = new Date(nextReviewDate.getFullYear(), nextReviewDate.getMonth(), nextReviewDate.getDate());
-
- // 计算日期差(天数)
- const diffTime = reviewDay.getTime() - today.getTime();
- const diffDays = Math.round(diffTime / (1000 * 60 * 60 * 24));
-
- if (diffDays < 0) {
- // 已经过了计划复习日期
- const daysOverdue = Math.abs(diffDays);
- return `Delay by ${daysOverdue} day${daysOverdue > 1 ? 's' : ''}`;
- } else if (diffDays === 0) {
- // 今天需要复习
- return 'Review today';
- } else if (diffDays === 1) {
- // 明天需要复习
- return 'Review tomorrow';
- } else {
- // x天后复习
- return `Review in ${diffDays} days`;
- }
- })()
- : 'Not scheduled';
- card.querySelector('.next-review').textContent = nextReviewTips;
-
- // 格式化上次复习时间
- const lastReviewText = fsrsState.lastReview
- ? new Date(fsrsState.lastReview).toLocaleDateString('en-US', {
- year: 'numeric',
- month: 'short',
- day: 'numeric',
- hour: '2-digit',
- minute: '2-digit'
- })
- : 'Never reviewed';
-
- // 格式化上次复习时间
- const nextReviewText = fsrsState.nextReview
- ? new Date(fsrsState.nextReview).toLocaleDateString('en-US', {
- year: 'numeric',
- month: 'short',
- day: 'numeric',
- hour: '2-digit',
- minute: '2-digit'
- })
- : 'Never reviewed';
-
-
-
- // 设置hover提示
- const tooltipContent = [
- `Last Review: ${lastReviewText}`,
- `Next Review: ${nextReviewText}`,
- problem.url ? 'Click to open problem' : ''
- ].filter(Boolean).join('\n');
-
- card.title = tooltipContent;
-
- // 检查今日是否已复习
- const isReviewedToday = fsrsState.lastReview &&
- new Date(fsrsState.lastReview).toDateString() === new Date().toDateString();
-
- // 设置按钮状态
- const reviewButton = card.querySelector('.btn-review');
- if (reviewButton) {
- if (isReviewedToday) {
- const icon = reviewButton.querySelector('i');
- icon.classList.remove('fa-check-circle');
- icon.classList.add('fa-circle-check');
- icon.style.color = '#0D6E6E';
- reviewButton.disabled = true;
- card.style.opacity = '0.4';
- }
-
- reviewButton.onclick = async function(e) {
- e.preventDefault();
- e.stopPropagation();
- console.log('复习按钮被点击');
-
- const updatedProblem = await handleFeedbackSubmission(problem);
- if (updatedProblem) {
- markAsReviewed(this, updatedProblem);
- }
- // markAsReviewed(this, problem); // 修改这里,传入按钮元素和问题对象
- };
- }
-
- // 添加题目链接功能
- if (problem.url) {
- card.style.cursor = 'pointer';
- card.onclick = function(e) {
- if (!e.target.closest('.btn-review')) {
- window.open(problem.url, '_blank');
- }
- };
- }
-
- reviewList.appendChild(cardNode);
- });
-}
-
-
-
-
-
-
-
-
-
-
-
-
-// 显示/隐藏添加题目弹窗
-function toggleAddProblemDialog(show) {
- const dialog = document.getElementById('addProblemDialog');
- if (!dialog) return;
-
- if (show) {
- dialog.style.display = 'block';
- } else {
- dialog.style.display = 'none';
-
- // 清除所有输入字段
- const problemUrl = document.getElementById('problemUrl');
- const problemName = document.getElementById('problemName');
- const customUrl = document.getElementById('customUrl');
-
- if (problemUrl) problemUrl.value = '';
- if (problemName) problemName.value = '';
- if (customUrl) customUrl.value = '';
-
- // 重置选项卡到默认状态
- const urlTabButton = document.getElementById('urlTabButton');
- const manualTabButton = document.getElementById('manualTabButton');
- const urlTab = document.getElementById('urlTab');
- const manualTab = document.getElementById('manualTab');
-
- if (urlTabButton && manualTabButton && urlTab && manualTab) {
- urlTabButton.classList.add('active');
- manualTabButton.classList.remove('active');
- urlTab.classList.add('active');
- manualTab.classList.remove('active');
- }
- }
-}
-
-
-
-// 初始化添加题目功能
-function initializeAddProblem() {
- const addButton = document.querySelector('.gear-button.add-problem');
- if (!addButton) return;
-
- // 添加选项卡切换样式
- const style = document.createElement('style');
- style.textContent = `
- .tab-container {
- margin-bottom: 15px;
- }
-
- .tab-buttons {
- display: flex;
- border-bottom: 1px solid #3a4a5c;
- margin-bottom: 15px;
- }
-
- .tab-button {
- background: none;
- border: none;
- padding: 8px 15px;
- color: #a0aec0;
- cursor: pointer;
- transition: all 0.3s;
- border-bottom: 2px solid transparent;
- }
-
- .tab-button.active {
- color: #4a9d9c;
- border-bottom: 2px solid #4a9d9c;
- }
-
- .tab-content {
- display: none;
- }
-
- .tab-content.active {
- display: block;
- }
-
- /* 修复弹窗背景色 - 使用更强的选择器 */
- #addProblemDialog .modal-content {
- background-color: #1d2e3d !important;
- color: #ffffff !important;
- }
-
- #addProblemDialog .tab-content,
- #addProblemDialog .form-group {
- background-color: #1d2e3d !important;
- color: #ffffff !important;
- }
-
- #addProblemDialog input.form-control,
- #addProblemDialog select.form-control {
- background-color: #2d3e4d !important;
- color: #ffffff !important;
- border: 1px solid #3a4a5c !important;
- }
-
- #addProblemDialog input.form-control::placeholder {
- color: #8096a8 !important;
- }
-
- #addProblemDialog label {
- color: #a0aec0 !important;
- }
- `;
- document.head.appendChild(style);
-
- // 点击添加按钮显示弹窗
- addButton.addEventListener('click', () => {
- toggleAddProblemDialog(true);
- });
-
- // 选项卡切换功能
- const urlTabButton = document.getElementById('urlTabButton');
- const manualTabButton = document.getElementById('manualTabButton');
- const urlTab = document.getElementById('urlTab');
- const manualTab = document.getElementById('manualTab');
-
- if (urlTabButton && manualTabButton) {
- urlTabButton.addEventListener('click', () => {
- urlTabButton.classList.add('active');
- manualTabButton.classList.remove('active');
- urlTab.classList.add('active');
- manualTab.classList.remove('active');
- });
-
- manualTabButton.addEventListener('click', () => {
- manualTabButton.classList.add('active');
- urlTabButton.classList.remove('active');
- manualTab.classList.add('active');
- urlTab.classList.remove('active');
- });
- }
-
- // 取消按钮
- const cancelButton = document.getElementById('cancelAdd');
- if (cancelButton) {
- cancelButton.addEventListener('click', () => {
- toggleAddProblemDialog(false);
- });
- }
-
- // 确认添加按钮
- const confirmButton = document.getElementById('confirmAdd');
- if (confirmButton) {
- confirmButton.addEventListener('click', async () => {
- try {
- let result;
-
- // 判断当前激活的是哪个选项卡
- if (urlTab.classList.contains('active')) {
- // 从URL添加
- const url = document.getElementById('problemUrl').value.trim();
- if (!url) {
- throw new Error('Please enter a valid problem URL.');
- }
- result = await handleAddProblem(url);
- } else {
- // 创建空白卡片
- const name = document.getElementById('problemName').value.trim();
- const level = document.getElementById('problemLevel').value;
- const customUrl = document.getElementById('customUrl').value.trim();
-
- if (!name) {
- throw new Error('Please enter the problem name.');
- }
-
- if (!level) {
- throw new Error('Please select a difficulty level.');
- }
-
- // 如果提供了URL,检查其格式是否有效
- if (customUrl && !customUrl.match(/^https?:\/\/.+/)) {
- throw new Error('Please enter a valid URL starting with http:// or https://');
- }
-
- result = await handleAddBlankProblem(name, level, customUrl);
- }
-
- toggleAddProblemDialog(false);
- await loadDailyReviewData();
- updateCardDisplay();
-
- // 显示成功提示
- Swal.fire({
- icon: 'success',
- title: 'SUCCESS',
- text: 'Problem added to review list.',
- showConfirmButton: false,
- timer: 1500,
- background: '#1d2e3d',
- color: '#ffffff',
- toast: true,
- position: 'center-end',
- customClass: {
- popup: 'colored-toast'
- }
- });
- } catch (error) {
- // 显示错误提示
- Swal.fire({
- icon: 'error',
- title: 'ADD FAIL',
- text: error.message,
- background: '#1d2e3d',
- color: '#ffffff',
- confirmButtonColor: '#4a9d9c'
- });
- }
- });
- }
-
- // 点击弹窗外部关闭弹窗
- const dialog = document.getElementById('addProblemDialog');
- if (dialog) {
- dialog.addEventListener('click', (e) => {
- if (e.target === dialog) {
- toggleAddProblemDialog(false);
- }
- });
- }
-}
-
-// 添加设置相关的初始化函数
-async function initializeOptions() {
- await loadConfigs();
-
- const optionsForm = document.getElementById('optionsForm');
- if (!optionsForm) return; // 如果找不到表单元素,直接返回
-
- // 初始化题目排序选择器
- const problemSorterSelect = document.getElementById('problemSorterSelect');
- if (problemSorterSelect) {
- const problemSorterMetaArr = problemSorterArr.map(sorter => ({
- id: idOf(sorter),
- text: descriptionOf(sorter)
- }));
-
- problemSorterMetaArr.forEach(sorterMeta => {
- const optionElement = document.createElement('option');
- optionElement.value = sorterMeta.id;
- optionElement.textContent = sorterMeta.text;
- problemSorterSelect.append(optionElement);
- });
- }
-
- // 初始化云同步开关
- const syncToggle = document.getElementById('syncToggle');
- if (syncToggle) {
- syncToggle.checked = store.isCloudSyncEnabled || false;
- }
-
-
- // 初始化提醒开关
- const reminderToggle = document.getElementById('reminderToggle');
- if (reminderToggle) {
- reminderToggle.checked = store.isReminderEnabled || false;
- }
-
- // 修改保存成功提示
- optionsForm.addEventListener('submit', async e => {
- e.preventDefault();
- const selectedSorterId = problemSorterSelect.value;
- const isCloudSyncEnabled = syncToggle.checked;
- const isReminderEnabled = reminderToggle.checked;
-
- await setProblemSorter(Number(selectedSorterId));
- await setCloudSyncEnabled(isCloudSyncEnabled);
- await setReminderEnabled(isReminderEnabled);
-
- // 使用 SweetAlert2 显示保存成功提示
- Swal.fire({
- icon: 'success',
- title: '设置已保存',
- text: '您的设置已成功更新',
- showConfirmButton: false,
- timer: 1500,
- background: '#1d2e3d',
- color: '#ffffff',
- toast: true,
- position: 'center-end',
- customClass: {
- popup: 'colored-toast'
- }
- });
- });
-}
-
-
-
-// 初始化函数
-export async function initializeReviewPage() {
- console.log('初始化复习页面');
- // 首先加载配置
- await loadConfigs();
- console.log('加载的默认卡片数量:', store.defaultCardLimit);
- await loadDailyReviewData(); // 加载真实数据
- const gearButtons = document.querySelectorAll('.gear-button');
- gearButtons.forEach(button => {
- button.replaceWith(button.cloneNode(true));
- });
-
-
- // 绑定齿轮按钮事件
- document.querySelectorAll('.gear-button').forEach(button => {
- button.addEventListener('click', function() {
- console.log('齿轮按钮被点击');
- const delta = this.classList.contains('left') ? -1 : 1;
- changeCardLimit(delta);
- });
- });
-
- // 绑定卡片数量输入框变化事件
- const cardLimitInput = document.getElementById('cardLimit');
- cardLimitInput.addEventListener('change', function() {
- console.log('卡片数量改变');
- updateCardDisplay();
- });
-
- // 初始化显示
- setCurrentDate();
- updateStats();
- // updateCardLimitDisplay();
- createReviewCards();
- initializeAddProblem();
-}
-
-export function initializeFeedbackButton() {
- const button = document.querySelector('.feedback-btn'); // 使用新的类名
- if (!button) return;
-
- button.addEventListener('mouseenter', function() {
- this.style.background = '#1a3244';
- this.style.borderColor = '#61dafb';
- this.style.boxShadow = '0 0 10px rgba(97, 218, 251, 0.5)';
- this.style.color = '#61dafb';
- this.querySelector('.btn-content').style.transform = 'translateX(2px)';
- this.querySelector('i').style.color = '#61dafb';
- });
-
- button.addEventListener('mouseleave', function() {
- this.style.background = '#1d2e3d';
- this.style.borderColor = 'rgba(97, 218, 251, 0.3)';
- this.style.boxShadow = 'none';
- this.style.color = '#61dafb';
- this.querySelector('.btn-content').style.transform = 'translateX(0)';
- this.querySelector('i').style.color = '#61dafb';
- });
- const buttonReview = document.querySelector('.feedback-btn-review'); // 使用新的类名
- if (!buttonReview) return;
-
- buttonReview.addEventListener('mouseenter', function() {
- this.style.background = '#1a3244';
- this.style.borderColor = '#61dafb';
- this.style.boxShadow = '0 0 10px rgba(97, 218, 251, 0.5)';
- this.style.color = '#61dafb';
- this.querySelector('.btn-content').style.transform = 'translateX(2px)';
- this.querySelector('i').style.color = '#61dafb';
- });
-
- buttonReview.addEventListener('mouseleave', function() {
- this.style.background = '#1d2e3d';
- this.style.borderColor = 'rgba(97, 218, 251, 0.3)';
- this.style.boxShadow = 'none';
- this.style.color = '#61dafb';
- this.querySelector('.btn-content').style.transform = 'translateX(0)';
- this.querySelector('i').style.color = '#61dafb';
- });
-}
-
-
-
-// 页面切换功能
-document.addEventListener('DOMContentLoaded', async function() {
- console.log('DOM加载完成,开始初始化复习页面和切换绑定');
- await initializeReviewPage();
- // 添加设置初始化
- initializeFeedbackButton();
-
-
- // 检查是否找到导航按钮
- const navButtons = document.querySelectorAll('.nav-btn');
- console.log('找到导航按钮数量:', navButtons.length);
-
- // 检查是否找到视图
- const views = document.querySelectorAll('.view');
- console.log('找到视图数量:', views.length);
-
- // 打印所有视图的ID
- views.forEach(view => console.log('视图ID:', view.id));
-
- navButtons.forEach((button, index) => {
- console.log(`为第 ${index + 1} 个按钮绑定点击事件:`, button.textContent);
-
- button.addEventListener('click', async function(e) {
- e.preventDefault(); // 阻止默认行为
- e.stopPropagation(); // 阻止事件冒泡
-
- console.log('按钮被点击:', this.textContent);
-
- // 移除所有按钮的激活状态
- navButtons.forEach(btn => btn.classList.remove('active'));
- // 添加当前按钮的激活状态
- this.classList.add('active');
-
- // 获取目标视图
- const targetView = this.textContent.trim();
- console.log('目标视图:', targetView);
-
- let viewId;
- switch(targetView) {
- case 'Review':
- viewId = 'reviewView';
- await initializeReviewPage();
- break;
- case 'Problems':
- viewId = 'problemListView';
- await loadProblemList(); // 加载题目列表
- initializeFeedbackButton();
- // renderAll();
- break;
- case 'Settings':
- viewId = 'moreView';
- await initializeOptions();
- break;
- }
-
- console.log('切换到视图ID:', viewId);
-
- // 切换视图
- views.forEach(view => {
- console.log('检查视图:', view.id);
- if(view.id === viewId) {
- view.classList.add('active');
- view.style.display = 'block';
- console.log('激活视图:', view.id);
- } else {
- view.classList.remove('active');
- view.style.display = 'none';
- console.log('隐藏视图:', view.id);
- }
- });
- });
- });
-
- // 调试 revlogs
- try {
- console.log('===== 开始调试 revlogs =====');
- const allRevlogs = await getAllRevlogs();
- console.log('所有复习日志:', allRevlogs);
-
- // 计算总复习次数
- let totalReviews = 0;
- Object.keys(allRevlogs).forEach(cardId => {
- totalReviews += allRevlogs[cardId]?.length || 0;
- });
- console.log(`总复习次数: ${totalReviews}`);
-
- // 导出 CSV 并打印
- const csvContent = await exportRevlogsToCSV();
- console.log('CSV 格式的复习日志:');
- console.log(csvContent);
- console.log('===== 结束调试 revlogs =====');
- } catch (error) {
- console.error('调试 revlogs 时出错:', error);
- }
-});
-
-
-
-
-
-
-
-
-// 以防万一,也添加 window.onload
-window.onload = function() {
- console.log('页面完全加载完成');
- if (!document.querySelector('.review-card')) {
- console.log('卡片未创建,重新初始化');
- setCurrentDate();
- updateStats();
- updateCardLimitDisplay();
- createReviewCards();
- }
-
-
-};
+import { renderAll } from './view/view.js';
+import { getAllProblems, syncProblems } from "./service/problemService.js";
+import { getLevelColor,getCurrentRetrievability } from './util/utils.js';
+import { handleFeedbackSubmission, handleAddBlankProblem } from './script/submission.js';
+import './popup.css';
+import { isCloudSyncEnabled, loadConfigs, setCloudSyncEnabled, setProblemSorter,setDefaultCardLimit,setReminderEnabled } from "./service/configService";
+import { store,daily_store } from './store';
+import { optionPageFeedbackMsgDOM } from './util/doms';
+import { descriptionOf, idOf, problemSorterArr } from "./util/sort";
+import {handleAddProblem} from "./script/submission.js"
+// 在文件顶部导入 SweetAlert2
+import Swal from 'sweetalert2';
+// 导入 getAllRevlogs 函数
+import { getAllRevlogs, exportRevlogsToCSV,saveFSRSParams } from './util/fsrs.js';
+import { getRevlogCount, optimizeParameters,updateFSRSInstance } from './service/fsrsService.js';
+
+// 在文件开头添加
+const LAST_AVERAGE_KEY = 'lastRetrievabilityAverage';
+const LAST_UPDATE_TIME_KEY = 'lastUpdateTime';
+let yesterdayRetrievabilityAverage = 0.00;
+
+
+
+async function loadProblemList() {
+ await renderAll();
+}
+
+
+// 获取上次存储的平均值和时间
+function loadLastAverageData() {
+ const lastData = {
+ average: parseFloat(localStorage.getItem(LAST_AVERAGE_KEY)) || 0.00,
+ timestamp: parseInt(localStorage.getItem(LAST_UPDATE_TIME_KEY)) || 0
+ };
+ return lastData;
+}
+
+async function loadDailyReviewData() {
+ const problems = Object.values(await getAllProblems()).filter(p => p.isDeleted !== true);
+ daily_store.reviewScheduledProblems = problems
+ .sort((a, b) => {
+ const retrievabilityA = getCurrentRetrievability(a);
+ const retrievabilityB = getCurrentRetrievability(b);
+ return retrievabilityA - retrievabilityB; // 升序排序,最小值在前
+ });
+
+ // 获取今天已复习和待复习的题目
+ daily_store.dailyReviewProblems = daily_store.reviewScheduledProblems
+ .filter(problem => isReviewedToday(problem) || isReviewDueToday(problem))
+ .sort((a, b) => {
+ // 首先按照是否已复习排序(已复习的排在前面)
+ const aReviewed = isReviewedToday(a);
+ const bReviewed = isReviewedToday(b);
+ if (aReviewed !== bReviewed) {
+ return bReviewed ? 1 : -1;
+ }
+ // 如果复习状态相同,则按可检索性排序
+ const retrievabilityA = getCurrentRetrievability(a);
+ const retrievabilityB = getCurrentRetrievability(b);
+ return retrievabilityA - retrievabilityB;
+ });
+
+
+ console.log('总题目数:', problems.length);
+ console.log('今日待复习题目数:', daily_store.dailyReviewProblems.length);
+
+ // 添加调试日志
+ daily_store.dailyReviewProblems.forEach(problem => {
+ const isReviewed = isReviewedToday(problem);
+ const isDue = isReviewDueToday(problem);
+ console.log('题目状态:', {
+ name: problem.name,
+ lastReview: problem.fsrsState?.lastReview,
+ nextReview: problem.fsrsState?.nextReview,
+ isReviewedToday: isReviewed,
+ isDueToday: isDue,
+ retrievability: getCurrentRetrievability(problem)
+ });
+ });
+}
+
+// 存储当前的平均值和时间
+function saveCurrentAverageData(average) {
+ localStorage.setItem(LAST_AVERAGE_KEY, average.toString());
+ localStorage.setItem(LAST_UPDATE_TIME_KEY, Date.now().toString());
+}
+
+// 设置当前日期
+function setCurrentDate() {
+ const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
+ const today = new Date().toLocaleDateString('en-US', options);
+ document.getElementById('currentDate').textContent = today;
+}
+
+
+// 判断是否是今天需要复习的题目
+function isReviewDueToday(problem) {
+ if (!problem.fsrsState?.nextReview) {
+ console.log('题目没有下次复习时间:', problem.name);
+ return false;
+ }
+
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+
+ const nextReview = new Date(problem.fsrsState.nextReview);
+ nextReview.setHours(0, 0, 0, 0);
+
+ const isDue = nextReview <= today;
+
+ console.log('复习时间检查:', {
+ problemName: problem.name,
+ nextReview: nextReview.toISOString(),
+ today: today.toISOString(),
+ isDue: isDue
+ });
+
+ return isDue;
+}
+
+function isReviewedToday(problem) {
+ if (!problem.fsrsState?.lastReview) return false;
+
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+
+ const lastReview = new Date(problem.fsrsState.lastReview);
+ lastReview.setHours(0, 0, 0, 0);
+
+ return lastReview.getTime() === today.getTime();
+}
+
+
+
+
+// 计算可检索性均值
+function calculateRetrievabilityAverage() {
+ const problems = daily_store.reviewScheduledProblems;
+ if (!problems || problems.length === 0) return 0;
+
+ const totalRetrievability = problems.reduce((sum, problem) => {
+ const retrievability = getCurrentRetrievability(problem);
+ return sum + retrievability;
+ }, 0);
+
+ return Number((totalRetrievability / problems.length).toFixed(2));
+}
+
+
+// 更新顶部统计信息
+function updateStats() {
+ console.log('更新统计信息');
+ // 设置默认值
+ let completedCount = 0;
+ let totalProblems = 0;
+ // 添加空值检查
+ if (!daily_store || !daily_store.dailyReviewProblems) {
+ console.log('daily_store 或 dailyReviewProblems 为空:', {
+ daily_store: daily_store,
+ problems: daily_store?.dailyReviewProblems
+ });
+
+
+ // 更新显示
+ document.getElementById('completedCount').textContent = completedCount;
+ document.getElementById('totalCount').textContent = totalProblems;
+ document.getElementById('completionRate').textContent = '0%';
+ updateProgressCircle(0);
+ return;
+ }
+
+
+
+ // 获取当前显示的卡片数量
+ let cardLimit = parseInt(document.getElementById('cardLimit').value, 10)|| store.defaultCardLimit || 1;
+ console.log('当前卡片限制值:', {
+ rawValue: document.getElementById('cardLimit').value,
+ parsedCardLimit: cardLimit,
+ element: document.getElementById('cardLimit')
+ });
+
+
+
+ // 计算今日已复习的题目数量
+ completedCount = daily_store.dailyReviewProblems.filter(problem =>
+ isReviewedToday(problem)
+ ).length;
+
+
+ totalProblems = daily_store.dailyReviewProblems?.length || 0;
+ if (cardLimit > totalProblems) {
+ cardLimit = totalProblems;
+ }
+
+ // 添加空状态提示
+ const addProblemWrapper = document.querySelector('.add-problem-wrapper');
+ // 先移除可能存在的空状态提示
+ const existingEmptyState = document.querySelector('.empty-state');
+ if (existingEmptyState) {
+ existingEmptyState.remove();
+ }
+
+ if (totalProblems === 0 || cardLimit === 0) {
+ const emptyState = document.createElement('div');
+ emptyState.className = 'empty-state';
+ emptyState.innerHTML = `
+
+
+ Time to learn something new!
+ `;
+ addProblemWrapper.insertAdjacentElement('beforebegin', emptyState);
+ }
+
+
+ // 更新显示的已复习数量
+ document.getElementById('completedCount').textContent = completedCount;
+ document.getElementById('totalCount').textContent = cardLimit; // 使用当前的卡片数量
+
+ // 更新进度条
+ const completionRate = cardLimit > 0 ? Math.round((completedCount / cardLimit) * 100) : 0;
+ updateProgressCircle(completionRate);
+ document.getElementById('completionRate').textContent = `${completionRate}%`;
+ // document.querySelector('.completion-circle').style.setProperty('--percentage', `${completionRate}%`);
+ // 计算当前的可检索性均值,并确保是数字类型
+ const currentRetrievabilityAverage = parseFloat(calculateRetrievabilityAverage()) || 0;
+ console.log('当前可检索性均值:', {
+ raw: calculateRetrievabilityAverage(),
+ parsed: currentRetrievabilityAverage,
+ type: typeof currentRetrievabilityAverage
+ });
+ const retrievabilityElement = document.getElementById('retrievabilityAverage');
+ retrievabilityElement.textContent = currentRetrievabilityAverage;
+
+
+ // 获取上次存储的数据
+ const lastData = loadLastAverageData();
+ const hoursSinceLastUpdate = (Date.now() - lastData.timestamp) / (1000 * 60 * 60);
+
+ // 如果超过24小时,更新存储的数据
+ if (hoursSinceLastUpdate >= 24) {
+ console.log('距离上次更新已超过24小时:', {
+ hoursSinceLastUpdate: hoursSinceLastUpdate.toFixed(2) + '小时',
+ lastUpdateTime: new Date(lastData.timestamp).toLocaleString(),
+ lastAverage: lastData.average.toFixed(2),
+ currentAverage: currentRetrievabilityAverage.toFixed(2)
+ });
+
+ yesterdayRetrievabilityAverage = lastData.average;
+ saveCurrentAverageData(currentRetrievabilityAverage);
+
+ console.log('已更新存储数据:', {
+ newYesterdayAverage: yesterdayRetrievabilityAverage.toFixed(2),
+ savedCurrentAverage: currentRetrievabilityAverage.toFixed(2),
+ saveTime: new Date().toLocaleString()
+ });
+ } else {
+ console.log('距离上次更新未超过24小时:', {
+ hoursSinceLastUpdate: hoursSinceLastUpdate.toFixed(2) + '小时',
+ lastUpdateTime: new Date(lastData.timestamp).toLocaleString(),
+ usingLastAverage: lastData.average.toFixed(2)
+ });
+ yesterdayRetrievabilityAverage = lastData.average;
+ }
+
+ // 更新趋势图标
+ const trendIcon = document.getElementById('trendIcon');
+ if (currentRetrievabilityAverage > yesterdayRetrievabilityAverage) {
+ trendIcon.className = 'fas fa-arrow-up trend-icon trend-up';
+ } else if (currentRetrievabilityAverage < yesterdayRetrievabilityAverage) {
+ trendIcon.className = 'fas fa-arrow-down trend-icon trend-down';
+ } else {
+ trendIcon.className = '';
+ }
+
+ // 根据可检索性均值调整颜色和背景提示
+ const lowMemoryWarning = document.getElementById('lowMemoryWarning');
+ if (currentRetrievabilityAverage < 0.90) {
+ retrievabilityElement.classList.add('low');
+ lowMemoryWarning.classList.add('active');
+ } else {
+ retrievabilityElement.classList.remove('low');
+ lowMemoryWarning.classList.remove('active');
+ }
+ updateCardLimitDisplay(); // 这里也添加一次调用
+}
+
+function updateProgressCircle(completionRate) {
+ const progressCircle = document.querySelector('.completion-circle');
+ const radius = 54; // 圆的半径
+ const circumference = 2 * Math.PI * radius; // 圆的周长
+
+ // 计算偏移量
+ const offset = circumference - (completionRate / 100) * circumference;
+ progressCircle.style.strokeDasharray = `${circumference} ${circumference}`;
+ progressCircle.style.strokeDashoffset = offset;
+
+ // 更新显示的百分比
+ // document.getElementById('completionRate').textContent = `${completionRate}%`;
+ document.querySelector('.completion-circle').style.setProperty('--percentage', `${completionRate}%`);
+
+ // 添加动画效果
+ const innerCircle = document.querySelector('.inner-circle');
+ innerCircle.style.transform = `scale(1.1)`; // 放大内圈
+ setTimeout(() => {
+ innerCircle.style.transform = `scale(1)`; // 恢复原状
+ }, 500); // 动画持续时间
+}
+
+
+
+
+// 更新卡片限制和显示
+export function updateCardLimitDisplay() {
+ const input = document.getElementById('cardLimit');
+ const totalDisplay = document.querySelector('.total-problems');
+ const totalProblems = daily_store.dailyReviewProblems?.length || 0;
+
+ // 更新最大值和总数显示
+ input.max = Math.max(totalProblems, 1);
+ totalDisplay.textContent = `/ ${totalProblems}`;
+
+ // 使用保存的默认值或回退到3
+ let currentValue = store.defaultCardLimit || 1;
+ if (currentValue > totalProblems && totalProblems > 0) {
+ currentValue = totalProblems;
+ // store.defaultCardLimit = totalProblems;
+ // setDefaultCardLimit(totalProblems);
+ }
+ input.value = currentValue;
+
+ // 禁用条件
+ if (totalProblems === 0) {
+ input.value = 0;
+ input.disabled = true;
+ totalDisplay.textContent = "/ 0";
+ } else {
+ input.disabled = false;
+ }
+
+ console.log('更新卡片限制显示:', {
+ currentValue: input.value,
+ max: input.max,
+ totalProblems
+ });
+}
+
+// 更新卡片显示
+export function updateCardDisplay() {
+ console.log('更新卡片显示');
+
+ updateStats(); // 更新统计信息,传递当前显示的卡片数量
+
+
+ createReviewCards(); // 创建新的卡片
+}
+
+
+
+
+// 改变卡片数量
+// 所有功能函数
+export async function changeCardLimit(delta) {
+ console.log('执行 changeCardLimit, delta:', delta);
+ const input = document.getElementById('cardLimit');
+ const currentValue = parseInt(input.value, 10);
+ const newValue = currentValue + delta;
+ const maxValue = daily_store.dailyReviewProblems?.length || 0;
+
+ if (newValue >= 1 && newValue <= maxValue) {
+ input.value = newValue;
+ await setDefaultCardLimit(newValue);
+ store.defaultCardLimit = newValue;
+ updateCardDisplay();
+ }
+}
+
+
+
+
+// 标记题目为已复习
+async function markAsReviewed(button, problem) {
+ console.log('执行 markAsReviewed', button, problem);
+
+ const card = button.closest('.review-card');
+ if (!card) {
+ console.log('未找到对应的卡片');
+ return;
+ }
+
+ console.log('找到卡片,开始更新状态');
+
+ // 更换图标并更改样式
+ const icon = button.querySelector('i');
+ icon.classList.remove('fa-check-circle');
+ icon.classList.add('fa-circle-check');
+ icon.style.color = '#0D6E6E';
+
+ // 禁用按钮
+ button.disabled = true;
+ card.style.opacity = '0.4';
+
+
+
+ // 更新统计信息
+ updateCardDisplay();
+ console.log('更新完成');
+}
+
+
+// 创建题目卡片时的事件绑定
+function createReviewCards() {
+ console.log('开始创建卡片');
+ const reviewList = document.getElementById('reviewList');
+ const template = document.getElementById('reviewCardTemplate');
+ const cardLimit = parseInt(document.getElementById('cardLimit').value, 10);
+
+ reviewList.innerHTML = '';
+
+ const problems = daily_store.dailyReviewProblems || [];
+ problems.slice(0, cardLimit).forEach((problem, index) => {
+ const cardNode = template.content.cloneNode(true);
+ const card = cardNode.querySelector('.review-card');
+
+ // 安全地访问 fsrsState
+ const fsrsState = problem.fsrsState || {};
+
+
+ // 设置题目信息
+ const problemName = card.querySelector('.problem-name');
+ problemName.textContent = problem.name || 'unknown';
+
+ // 设置难度和复习信息
+ const difficultySpan = card.querySelector('.difficulty');
+ const level = problem.level || 'Unknown';
+ difficultySpan.textContent = level;
+ // 使用现有的 CSS 类
+ difficultySpan.classList.add(`difficulty-${level}`);
+
+ // 设置可检索性
+ const retrievability = getCurrentRetrievability(problem);
+ const retrievabilitySpan = card.querySelector('.retrievability');
+ retrievabilitySpan.textContent = `${retrievability.toFixed(1)}`;
+ retrievabilitySpan.classList.add(retrievability < 0.9 ? 'text-danger' : 'text-success');
+
+
+ // 设置下次复习时间
+ const nextReviewTips = fsrsState.nextReview
+ ? (() => {
+ const nextReviewDate = new Date(fsrsState.nextReview);
+ const now = new Date();
+
+ // 获取当前日期和下次复习日期(不含时间)
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
+ const reviewDay = new Date(nextReviewDate.getFullYear(), nextReviewDate.getMonth(), nextReviewDate.getDate());
+
+ // 计算日期差(天数)
+ const diffTime = reviewDay.getTime() - today.getTime();
+ const diffDays = Math.round(diffTime / (1000 * 60 * 60 * 24));
+
+ if (diffDays < 0) {
+ // 已经过了计划复习日期
+ const daysOverdue = Math.abs(diffDays);
+ return `Delay by ${daysOverdue} day${daysOverdue > 1 ? 's' : ''}`;
+ } else if (diffDays === 0) {
+ // 今天需要复习
+ return 'Review today';
+ } else if (diffDays === 1) {
+ // 明天需要复习
+ return 'Review tomorrow';
+ } else {
+ // x天后复习
+ return `Review in ${diffDays} days`;
+ }
+ })()
+ : 'Not scheduled';
+ card.querySelector('.next-review').textContent = nextReviewTips;
+
+ // 格式化上次复习时间
+ const lastReviewText = fsrsState.lastReview
+ ? new Date(fsrsState.lastReview).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit'
+ })
+ : 'Never reviewed';
+
+ // 格式化上次复习时间
+ const nextReviewText = fsrsState.nextReview
+ ? new Date(fsrsState.nextReview).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit'
+ })
+ : 'Never reviewed';
+
+
+
+ // 设置hover提示
+ const tooltipContent = [
+ `Last Review: ${lastReviewText}`,
+ `Next Review: ${nextReviewText}`,
+ problem.url ? 'Click to open problem' : ''
+ ].filter(Boolean).join('\n');
+
+ card.title = tooltipContent;
+
+ // 检查今日是否已复习
+ const isReviewedToday = fsrsState.lastReview &&
+ new Date(fsrsState.lastReview).toDateString() === new Date().toDateString();
+
+ // 设置按钮状态
+ const reviewButton = card.querySelector('.btn-review');
+ if (reviewButton) {
+ if (isReviewedToday) {
+ const icon = reviewButton.querySelector('i');
+ icon.classList.remove('fa-check-circle');
+ icon.classList.add('fa-circle-check');
+ icon.style.color = '#0D6E6E';
+ reviewButton.disabled = true;
+ card.style.opacity = '0.4';
+ }
+
+ reviewButton.onclick = async function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ console.log('复习按钮被点击');
+
+ const updatedProblem = await handleFeedbackSubmission(problem);
+ if (updatedProblem) {
+ markAsReviewed(this, updatedProblem);
+ }
+ // markAsReviewed(this, problem); // 修改这里,传入按钮元素和问题对象
+ };
+ }
+
+ // 添加题目链接功能
+ if (problem.url) {
+ card.style.cursor = 'pointer';
+ card.onclick = function(e) {
+ if (!e.target.closest('.btn-review')) {
+ window.open(problem.url, '_blank');
+ }
+ };
+ }
+
+ reviewList.appendChild(cardNode);
+ });
+}
+
+
+
+
+
+
+
+
+
+
+
+
+// 显示/隐藏添加题目弹窗
+function toggleAddProblemDialog(show) {
+ const dialog = document.getElementById('addProblemDialog');
+ if (!dialog) return;
+
+ if (show) {
+ dialog.style.display = 'block';
+ } else {
+ dialog.style.display = 'none';
+
+ // 清除所有输入字段
+ const problemUrl = document.getElementById('problemUrl');
+ const problemName = document.getElementById('problemName');
+ const customUrl = document.getElementById('customUrl');
+
+ if (problemUrl) problemUrl.value = '';
+ if (problemName) problemName.value = '';
+ if (customUrl) customUrl.value = '';
+
+ // 重置选项卡到默认状态
+ const urlTabButton = document.getElementById('urlTabButton');
+ const manualTabButton = document.getElementById('manualTabButton');
+ const urlTab = document.getElementById('urlTab');
+ const manualTab = document.getElementById('manualTab');
+
+ if (urlTabButton && manualTabButton && urlTab && manualTab) {
+ urlTabButton.classList.add('active');
+ manualTabButton.classList.remove('active');
+ urlTab.classList.add('active');
+ manualTab.classList.remove('active');
+ }
+ }
+}
+
+
+
+// 初始化添加题目功能
+function initializeAddProblem() {
+ const addButton = document.querySelector('.gear-button.add-problem');
+ if (!addButton) return;
+
+ // 添加选项卡切换样式
+ const style = document.createElement('style');
+ style.textContent = `
+ .tab-container {
+ margin-bottom: 15px;
+ }
+
+ .tab-buttons {
+ display: flex;
+ border-bottom: 1px solid #3a4a5c;
+ margin-bottom: 15px;
+ }
+
+ .tab-button {
+ background: none;
+ border: none;
+ padding: 8px 15px;
+ color: #a0aec0;
+ cursor: pointer;
+ transition: all 0.3s;
+ border-bottom: 2px solid transparent;
+ }
+
+ .tab-button.active {
+ color: #4a9d9c;
+ border-bottom: 2px solid #4a9d9c;
+ }
+
+ .tab-content {
+ display: none;
+ }
+
+ .tab-content.active {
+ display: block;
+ }
+
+ /* 修复弹窗背景色 - 使用更强的选择器 */
+ #addProblemDialog .modal-content {
+ background-color: #1d2e3d !important;
+ color: #ffffff !important;
+ }
+
+ #addProblemDialog .tab-content,
+ #addProblemDialog .form-group {
+ background-color: #1d2e3d !important;
+ color: #ffffff !important;
+ }
+
+ #addProblemDialog input.form-control,
+ #addProblemDialog select.form-control {
+ background-color: #2d3e4d !important;
+ color: #ffffff !important;
+ border: 1px solid #3a4a5c !important;
+ }
+
+ #addProblemDialog input.form-control::placeholder {
+ color: #8096a8 !important;
+ }
+
+ #addProblemDialog label {
+ color: #a0aec0 !important;
+ }
+ `;
+ document.head.appendChild(style);
+
+ // 点击添加按钮显示弹窗
+ addButton.addEventListener('click', () => {
+ toggleAddProblemDialog(true);
+ });
+
+ // 选项卡切换功能
+ const urlTabButton = document.getElementById('urlTabButton');
+ const manualTabButton = document.getElementById('manualTabButton');
+ const urlTab = document.getElementById('urlTab');
+ const manualTab = document.getElementById('manualTab');
+
+ if (urlTabButton && manualTabButton) {
+ urlTabButton.addEventListener('click', () => {
+ urlTabButton.classList.add('active');
+ manualTabButton.classList.remove('active');
+ urlTab.classList.add('active');
+ manualTab.classList.remove('active');
+ });
+
+ manualTabButton.addEventListener('click', () => {
+ manualTabButton.classList.add('active');
+ urlTabButton.classList.remove('active');
+ manualTab.classList.add('active');
+ urlTab.classList.remove('active');
+ });
+ }
+
+ // 取消按钮
+ const cancelButton = document.getElementById('cancelAdd');
+ if (cancelButton) {
+ cancelButton.addEventListener('click', () => {
+ toggleAddProblemDialog(false);
+ });
+ }
+
+ // 确认添加按钮
+ const confirmButton = document.getElementById('confirmAdd');
+ if (confirmButton) {
+ confirmButton.addEventListener('click', async () => {
+ try {
+ let result;
+
+ // 判断当前激活的是哪个选项卡
+ if (urlTab.classList.contains('active')) {
+ // 从URL添加
+ const url = document.getElementById('problemUrl').value.trim();
+ if (!url) {
+ throw new Error('Please enter a valid problem URL.');
+ }
+ result = await handleAddProblem(url);
+ } else {
+ // 创建空白卡片
+ const name = document.getElementById('problemName').value.trim();
+ const level = document.getElementById('problemLevel').value;
+ const customUrl = document.getElementById('customUrl').value.trim();
+
+ if (!name) {
+ throw new Error('Please enter the problem name.');
+ }
+
+ if (!level) {
+ throw new Error('Please select a difficulty level.');
+ }
+
+ // 如果提供了URL,检查其格式是否有效
+ if (customUrl && !customUrl.match(/^https?:\/\/.+/)) {
+ throw new Error('Please enter a valid URL starting with http:// or https://');
+ }
+
+ result = await handleAddBlankProblem(name, level, customUrl);
+ }
+
+ toggleAddProblemDialog(false);
+ await loadDailyReviewData();
+ updateCardDisplay();
+
+ // 显示成功提示
+ Swal.fire({
+ icon: 'success',
+ title: 'SUCCESS',
+ text: 'Problem added to review list.',
+ showConfirmButton: false,
+ timer: 1500,
+ background: '#1d2e3d',
+ color: '#ffffff',
+ toast: true,
+ position: 'center-end',
+ customClass: {
+ popup: 'colored-toast'
+ }
+ });
+ } catch (error) {
+ // 显示错误提示
+ Swal.fire({
+ icon: 'error',
+ title: 'ADD FAIL',
+ text: error.message,
+ background: '#1d2e3d',
+ color: '#ffffff',
+ confirmButtonColor: '#4a9d9c'
+ });
+ }
+ });
+ }
+
+ // 点击弹窗外部关闭弹窗
+ const dialog = document.getElementById('addProblemDialog');
+ if (dialog) {
+ dialog.addEventListener('click', (e) => {
+ if (e.target === dialog) {
+ toggleAddProblemDialog(false);
+ }
+ });
+ }
+}
+
+// 显示弹窗函数
+function showModal(title, content, buttons = null) {
+ const modalOptions = {
+ title: title,
+ html: content,
+ background: '#1d2e3d',
+ color: '#ffffff',
+ confirmButtonColor: '#4a9d9c',
+ width: '600px'
+ };
+
+ // 如果有自定义按钮,则使用自定义按钮
+ if (buttons && Array.isArray(buttons)) {
+ modalOptions.showConfirmButton = false;
+ modalOptions.showCloseButton = true;
+ modalOptions.html += `
+
+ ${buttons.map(btn => `
+
+ `).join('')}
+
+ `;
+
+ // 使用SweetAlert2显示模态框
+ Swal.fire(modalOptions);
+
+ // 为每个按钮添加点击事件 - 移到这里,在Swal.fire之后立即绑定
+ setTimeout(() => {
+ buttons.forEach(btn => {
+ const btnElement = document.getElementById(`modal-btn-${btn.text}`);
+ if (btnElement && btn.onClick) {
+ btnElement.addEventListener('click', async (e) => {
+ e.preventDefault();
+ try {
+ // 执行按钮点击事件处理程序
+ await btn.onClick();
+ // 关闭弹窗
+ Swal.close();
+ } catch (error) {
+ console.error('按钮点击事件处理程序执行失败:', error);
+ }
+ });
+ }
+ });
+ }, 100); // 添加一个小延迟确保DOM已更新
+ } else {
+ // 如果没有自定义按钮,则使用默认按钮
+ modalOptions.showConfirmButton = true;
+ modalOptions.confirmButtonText = '确定';
+
+ // 使用SweetAlert2显示模态框
+ Swal.fire(modalOptions);
+ }
+}
+
+// 初始化FSRS参数优化卡片
+async function initializeFSRSOptimization() {
+ try {
+ // 获取并显示复习记录数量
+ const count = await getRevlogCount();
+ const revlogCountElement = document.getElementById('revlogCount');
+ const revlogCountEnElement = document.getElementById('revlogCount_en');
+ if (revlogCountElement) {
+ revlogCountElement.textContent = count;
+ }
+ if (revlogCountEnElement) {
+ revlogCountEnElement.textContent = count;
+ }
+
+ // 添加导出按钮点击事件
+ const exportRevlogsBtn = document.getElementById('exportRevlogsBtn');
+ if (exportRevlogsBtn) {
+ exportRevlogsBtn.addEventListener('click', async () => {
+ // 保存原始按钮内容
+ const originalContent = exportRevlogsBtn.innerHTML;
+
+ try {
+ // 显示加载中提示
+ exportRevlogsBtn.disabled = true;
+ exportRevlogsBtn.innerHTML = '';
+
+ // 导出CSV
+ const csvContent = await exportRevlogsToCSV();
+
+ // 创建下载链接
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.setAttribute('href', url);
+ link.setAttribute('download', `fsrs_revlogs_${new Date().toISOString().slice(0, 10)}.csv`);
+ link.style.display = 'none';
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+
+ // 显示成功提示
+ Swal.fire({
+ icon: 'success',
+ title: 'Export Success',
+ html: `
+
+ 已成功导出 ${count} 条复习记录
+
+
+ Successfully exported ${count} review records to CSV file
+
+
+ `,
+ background: '#1d2e3d',
+ color: '#ffffff',
+ toast: true,
+ position: 'center-end',
+ customClass: {
+ popup: 'colored-toast'
+ },
+ confirmButtonColor: '#4a9d9c',
+ confirmButtonText: 'OK'
+ });
+ } catch (error) {
+ console.error('Error exporting revlogs:', error);
+ Swal.fire({
+ icon: 'error',
+ title: 'Export Failed',
+ html: `
+
+ 导出复习记录时发生错误
+
+
+ Error occurred while exporting review records:
+
+
+
+ ${error.message}
+
+
+ `,
+ background: '#1d2e3d',
+ color: '#ffffff',
+ confirmButtonColor: '#4a9d9c',
+ confirmButtonText: 'OK'
+ });
+ } finally {
+ // 恢复按钮状态
+ exportRevlogsBtn.disabled = false;
+ exportRevlogsBtn.innerHTML = originalContent;
+ }
+ });
+ }
+
+ // 添加优化按钮点击事件
+ const optimizeParamsBtn = document.getElementById('optimizeParamsBtn');
+ if (optimizeParamsBtn) {
+ optimizeParamsBtn.addEventListener('click', async () => {
+ // 保存原始按钮内容
+ const originalContent = optimizeParamsBtn.innerHTML;
+
+ // 创建进度显示元素
+ const progressContainer = document.createElement('div');
+ progressContainer.className = 'progress optimize-progress';
+ progressContainer.innerHTML = `
+
+
+ `;
+ optimizeParamsBtn.parentNode.appendChild(progressContainer);
+
+ // 更改按钮状态
+ optimizeParamsBtn.disabled = true;
+ optimizeParamsBtn.innerHTML = '';
+
+ try {
+ // 进度回调函数
+ const onProgress = (progress) => {
+ console.log('Progress update:', progress);
+ const percent = Math.round(progress.percent * 100);
+ const progressBar = progressContainer.querySelector('.progress-bar');
+ if (progressBar) {
+ progressBar.style.width = `${percent}%`;
+ progressBar.setAttribute('aria-valuenow', percent);
+ progressBar.textContent = `${percent}%`;
+ }
+ };
+
+ // 调用优化API
+ const result = await optimizeParameters(onProgress);
+
+ // 显示结果弹窗
+ if (result && result.type === 'Success' && result.params) {
+ // 生成唯一ID
+ const detailId = `paramsDetail_${Date.now()}`;
+
+ // 显示优化后的参数,并添加保存按钮
+ const modalResult = await Swal.fire({
+ title: 'SUCCESS',
+ html: `
+
+
+ 参数优化完成!点击确认将自动保存并应用新参数。
+
+
+ Optimization done! Click OK to save and use the new settings.
+
+
+
+
+
+
+
${JSON.stringify(result.params, null, 2)}
+
+
+
+
+ `,
+ background: '#1d2e3d',
+ color: '#ffffff',
+ confirmButtonColor: '#4a9d9c',
+ confirmButtonText: 'OK',
+ showCloseButton: true,
+ closeButtonHtml: '',
+ didRender: () => {
+ // 在弹窗渲染后绑定事件
+ const toggleBtn = document.getElementById(`toggleDetail_${detailId}`);
+ const detailDiv = document.getElementById(detailId);
+ if (toggleBtn && detailDiv) {
+ toggleBtn.addEventListener('click', () => {
+ detailDiv.classList.toggle('d-none');
+ const icon = toggleBtn.querySelector('i');
+ if (icon) {
+ icon.classList.toggle('fa-chevron-right');
+ icon.classList.toggle('fa-chevron-down');
+ }
+ });
+ }
+ }
+ });
+
+ if (modalResult.isConfirmed) {
+ try {
+ // 保存参数到本地存储
+ await saveFSRSParams(result.params);
+ // 更新FSRS实例
+ await updateFSRSInstance(result.params);
+ // 显示成功提示
+ Swal.fire({
+ icon: 'success',
+ title: 'Save Success',
+ text: '参数已成功应用 /New settings applied.',
+ background: '#1d2e3d',
+ showConfirmButton: false,
+ timer: 3000,
+ color: '#ffffff',
+ toast: true,
+ position: 'center-end',
+ customClass: {
+ popup: 'colored-toast'
+ }
+ });
+ } catch (error) {
+ console.error('Error saving FSRS parameters:', error);
+ Swal.fire({
+ icon: 'error',
+ title: 'Save Failed',
+ text: `Error saving parameters: ${error.message}`,
+ background: '#1d2e3d',
+ color: '#ffffff',
+ confirmButtonColor: '#4a9d9c'
+ });
+ }
+ }
+ } else {
+ // 显示其他类型的结果
+ showModal('FSRS参数优化结果', `
+
+
${JSON.stringify(result, null, 2)}
+
+ `);
+ }
+ } catch (error) {
+ console.error('Error optimizing FSRS parameters:', error);
+ showModal('Error', `Error optimizing parameters: ${error.message}`);
+ } finally {
+ // 恢复按钮状态
+ optimizeParamsBtn.disabled = false;
+ optimizeParamsBtn.innerHTML = originalContent;
+ // 移除进度条
+ progressContainer.remove();
+ }
+ });
+ }
+ } catch (error) {
+ console.error('Error initializing FSRS optimization:', error);
+ }
+}
+
+// 添加设置相关的初始化函数
+async function initializeOptions() {
+ await loadConfigs();
+
+ const optionsForm = document.getElementById('optionsForm');
+ if (!optionsForm) return; // 如果找不到表单元素,直接返回
+
+ // 初始化题目排序选择器
+ const problemSorterSelect = document.getElementById('problemSorterSelect');
+ if (problemSorterSelect) {
+ const problemSorterMetaArr = problemSorterArr.map(sorter => ({
+ id: idOf(sorter),
+ text: descriptionOf(sorter)
+ }));
+
+ problemSorterMetaArr.forEach(sorterMeta => {
+ const optionElement = document.createElement('option');
+ optionElement.value = sorterMeta.id;
+ optionElement.textContent = sorterMeta.text;
+ problemSorterSelect.append(optionElement);
+ });
+ }
+
+ // 初始化云同步开关
+ const syncToggle = document.getElementById('syncToggle');
+ if (syncToggle) {
+ syncToggle.checked = store.isCloudSyncEnabled || false;
+ }
+
+
+ // 初始化提醒开关
+ const reminderToggle = document.getElementById('reminderToggle');
+ if (reminderToggle) {
+ reminderToggle.checked = store.isReminderEnabled || false;
+ }
+
+ // 初始化FSRS参数优化卡片
+ await initializeFSRSOptimization();
+
+ // 修改保存成功提示
+ optionsForm.addEventListener('submit', async e => {
+ e.preventDefault();
+ const selectedSorterId = problemSorterSelect.value;
+ const isCloudSyncEnabled = syncToggle.checked;
+ const isReminderEnabled = reminderToggle.checked;
+
+ await setProblemSorter(Number(selectedSorterId));
+ await setCloudSyncEnabled(isCloudSyncEnabled);
+ await setReminderEnabled(isReminderEnabled);
+
+ // 使用 SweetAlert2 显示保存成功提示
+ Swal.fire({
+ icon: 'success',
+ title: 'Settings Saved',
+ text: 'Your settings have been successfully updated',
+ showConfirmButton: false,
+ timer: 1500,
+ background: '#1d2e3d',
+ color: '#ffffff',
+ toast: true,
+ position: 'center-end',
+ customClass: {
+ popup: 'colored-toast'
+ }
+ });
+ });
+}
+
+
+
+// 初始化函数
+export async function initializeReviewPage() {
+ console.log('初始化复习页面');
+ // 首先加载配置
+ await loadConfigs();
+ console.log('加载的默认卡片数量:', store.defaultCardLimit);
+ await loadDailyReviewData(); // 加载真实数据
+ const gearButtons = document.querySelectorAll('.gear-button');
+ gearButtons.forEach(button => {
+ button.replaceWith(button.cloneNode(true));
+ });
+
+
+ // 绑定齿轮按钮事件
+ document.querySelectorAll('.gear-button').forEach(button => {
+ button.addEventListener('click', function() {
+ console.log('齿轮按钮被点击');
+ const delta = this.classList.contains('left') ? -1 : 1;
+ changeCardLimit(delta);
+ });
+ });
+
+ // 绑定卡片数量输入框变化事件
+ const cardLimitInput = document.getElementById('cardLimit');
+ cardLimitInput.addEventListener('change', function() {
+ console.log('卡片数量改变');
+ updateCardDisplay();
+ });
+
+ // 初始化显示
+ setCurrentDate();
+ updateStats();
+ // updateCardLimitDisplay();
+ createReviewCards();
+ initializeAddProblem();
+}
+
+export function initializeFeedbackButton() {
+ const button = document.querySelector('.feedback-btn'); // 使用新的类名
+ if (!button) return;
+
+ button.addEventListener('mouseenter', function() {
+ this.style.background = '#1a3244';
+ this.style.borderColor = '#61dafb';
+ this.style.boxShadow = '0 0 10px rgba(97, 218, 251, 0.5)';
+ this.style.color = '#61dafb';
+ this.querySelector('.btn-content').style.transform = 'translateX(2px)';
+ this.querySelector('i').style.color = '#61dafb';
+ });
+
+ button.addEventListener('mouseleave', function() {
+ this.style.background = '#1d2e3d';
+ this.style.borderColor = 'rgba(97, 218, 251, 0.3)';
+ this.style.boxShadow = 'none';
+ this.style.color = '#61dafb';
+ this.querySelector('.btn-content').style.transform = 'translateX(0)';
+ this.querySelector('i').style.color = '#61dafb';
+ });
+ const buttonReview = document.querySelector('.feedback-btn-review'); // 使用新的类名
+ if (!buttonReview) return;
+
+ buttonReview.addEventListener('mouseenter', function() {
+ this.style.background = '#1a3244';
+ this.style.borderColor = '#61dafb';
+ this.style.boxShadow = '0 0 10px rgba(97, 218, 251, 0.5)';
+ this.style.color = '#61dafb';
+ this.querySelector('.btn-content').style.transform = 'translateX(2px)';
+ this.querySelector('i').style.color = '#61dafb';
+ });
+
+ buttonReview.addEventListener('mouseleave', function() {
+ this.style.background = '#1d2e3d';
+ this.style.borderColor = 'rgba(97, 218, 251, 0.3)';
+ this.style.boxShadow = 'none';
+ this.style.color = '#61dafb';
+ this.querySelector('.btn-content').style.transform = 'translateX(0)';
+ this.querySelector('i').style.color = '#61dafb';
+ });
+}
+
+
+
+// 页面切换功能
+document.addEventListener('DOMContentLoaded', async function() {
+ console.log('DOM加载完成,开始初始化复习页面和切换绑定');
+ await initializeReviewPage();
+ // 添加设置初始化
+ initializeFeedbackButton();
+
+
+ // 检查是否找到导航按钮
+ const navButtons = document.querySelectorAll('.nav-btn');
+ console.log('找到导航按钮数量:', navButtons.length);
+
+ // 检查是否找到视图
+ const views = document.querySelectorAll('.view');
+ console.log('找到视图数量:', views.length);
+
+ // 打印所有视图的ID
+ views.forEach(view => console.log('视图ID:', view.id));
+
+ navButtons.forEach((button, index) => {
+ console.log(`为第 ${index + 1} 个按钮绑定点击事件:`, button.textContent);
+
+ button.addEventListener('click', async function(e) {
+ e.preventDefault(); // 阻止默认行为
+ e.stopPropagation(); // 阻止事件冒泡
+
+ console.log('按钮被点击:', this.textContent);
+
+ // 移除所有按钮的激活状态
+ navButtons.forEach(btn => btn.classList.remove('active'));
+ // 添加当前按钮的激活状态
+ this.classList.add('active');
+
+ // 获取目标视图
+ const targetView = this.textContent.trim();
+ console.log('目标视图:', targetView);
+
+ let viewId;
+ switch(targetView) {
+ case 'Review':
+ viewId = 'reviewView';
+ await initializeReviewPage();
+ break;
+ case 'Problems':
+ viewId = 'problemListView';
+ await loadProblemList(); // 加载题目列表
+ initializeFeedbackButton();
+ // renderAll();
+ break;
+ case 'Settings':
+ viewId = 'moreView';
+ await initializeOptions();
+ break;
+ }
+
+ console.log('切换到视图ID:', viewId);
+
+ // 切换视图
+ views.forEach(view => {
+ console.log('检查视图:', view.id);
+ if(view.id === viewId) {
+ view.classList.add('active');
+ view.style.display = 'block';
+ console.log('激活视图:', view.id);
+ } else {
+ view.classList.remove('active');
+ view.style.display = 'none';
+ console.log('隐藏视图:', view.id);
+ }
+ });
+ });
+ });
+
+ // 调试 revlogs
+ try {
+ console.log('===== 开始调试 revlogs =====');
+ const allRevlogs = await getAllRevlogs();
+ console.log('所有复习日志:', allRevlogs);
+
+ // 计算总复习次数
+ let totalReviews = 0;
+ Object.keys(allRevlogs).forEach(cardId => {
+ totalReviews += allRevlogs[cardId]?.length || 0;
+ });
+ console.log(`总复习次数: ${totalReviews}`);
+
+ // 导出 CSV 并打印
+ const csvContent = await exportRevlogsToCSV();
+ console.log('CSV 格式的复习日志:');
+ console.log(csvContent);
+ console.log('===== 结束调试 revlogs =====');
+ } catch (error) {
+ console.error('调试 revlogs 时出错:', error);
+ }
+});
+
+
+
+
+
+
+
+
+// 以防万一,也添加 window.onload
+window.onload = function() {
+ console.log('页面完全加载完成');
+ if (!document.querySelector('.review-card')) {
+ console.log('卡片未创建,重新初始化');
+ setCurrentDate();
+ updateStats();
+ updateCardLimitDisplay();
+ createReviewCards();
+ }
+
+
+};
diff --git a/src/popup/delegate/fsrsDelegate.js b/src/popup/delegate/fsrsDelegate.js
new file mode 100644
index 0000000..e120b3b
--- /dev/null
+++ b/src/popup/delegate/fsrsDelegate.js
@@ -0,0 +1,102 @@
+// FSRS参数优化相关的API请求处理
+export const optimizeFSRSParams = async (csvContent, onProgress) => {
+ try {
+ const formData = new FormData();
+ const csvBlob = new Blob([csvContent], { type: 'text/csv' });
+ formData.append('file', csvBlob, 'revlog.csv');
+ formData.append('sse', '1');
+ formData.append('hour_offset', '4');
+ formData.append('enable_short_term', '0');
+ formData.append('timezone', 'Asia/Shanghai');
+
+ const response = await fetch('https://ishiko732-fsrs-online-training.hf.space/api/train', {
+ method: 'POST',
+ body: formData
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ // 手动解析SSE响应
+ const reader = response.body.getReader();
+ const decoder = new TextDecoder();
+ let result = null;
+ let lastProgress = null;
+ let doneParams = null;
+
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+
+ const chunk = decoder.decode(value, { stream: true });
+ const lines = chunk.split('\n');
+
+ // 处理SSE响应
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+
+ // 处理事件类型
+ if (line.startsWith('event: ')) {
+ const eventType = line.substring(7);
+ console.log('事件类型:', eventType);
+
+ // 查找下一个data行
+ let dataLine = '';
+ for (let j = i + 1; j < lines.length; j++) {
+ if (lines[j].startsWith('data: ')) {
+ dataLine = lines[j];
+ break;
+ }
+ }
+
+ if (dataLine) {
+ try {
+ const data = JSON.parse(dataLine.substring(6));
+
+ // 处理进度信息
+ if (eventType === 'progress') {
+ lastProgress = data;
+ // 如果提供了进度回调函数,则调用它
+ if (onProgress) {
+ onProgress(data);
+ }
+ }
+
+ // 处理完成事件
+ if (eventType === 'done') {
+ doneParams = data;
+ console.log('捕获到done事件中的参数:', doneParams);
+ }
+
+ // 处理训练结果
+ if (eventType === 'info' && data.type === 'Train') {
+ result = data;
+ }
+ } catch (e) {
+ console.warn('Error parsing SSE data:', e, dataLine);
+ }
+ }
+ }
+ }
+ }
+
+ // 优先返回done标签中的参数
+ if (doneParams) {
+ return doneParams;
+ }
+
+ // 如果没有获取到done参数,但有进度信息,则返回进度信息
+ if (!result && lastProgress) {
+ result = {
+ type: 'Progress',
+ progress: lastProgress
+ };
+ }
+
+ return result || { type: 'Error', message: 'No result received' };
+ } catch (error) {
+ console.error('Error optimizing FSRS parameters:', error);
+ throw error;
+ }
+};
\ No newline at end of file
diff --git a/src/popup/popup.css b/src/popup/popup.css
index c77c743..bfb54d3 100644
--- a/src/popup/popup.css
+++ b/src/popup/popup.css
@@ -1109,4 +1109,68 @@ td, th {
.update-summary a:hover {
text-decoration: underline;
color: #afffff;
+}
+
+/* 图标按钮样式 */
+.btn-icon {
+ background: none;
+ border: none;
+ color: #4a9d9c;
+ font-size: 1em;
+ width: 28px;
+ height: 28px;
+ border-radius: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ background-color: transparent;
+}
+
+.btn-icon:hover {
+ background-color: rgba(74, 157, 156, 0.1);
+ color: #61dafb;
+ transform: translateY(-1px);
+}
+
+.btn-icon:active {
+ transform: translateY(0);
+}
+
+.btn-icon-sm {
+ width: 24px;
+ height: 24px;
+}
+
+/* 优化参数进度条样式 */
+.optimize-progress {
+ height: 3px !important;
+ background-color: rgba(74, 157, 156, 0.1) !important;
+ border-radius: 4px !important;
+ margin-top: 12px !important;
+ overflow: hidden !important;
+}
+
+.optimize-progress .progress-bar {
+ background: linear-gradient(90deg, #4a9d9c, #61dafb) !important;
+ transition: width 0.3s ease !important;
+}
+
+.optimize-progress .progress-bar-animated {
+ animation: progress-bar-stripes 1s linear infinite !important;
+}
+
+.optimize-progress .progress-bar-striped {
+ background-image: linear-gradient(
+ 45deg,
+ rgba(255, 255, 255, 0.15) 25%,
+ transparent 25%,
+ transparent 50%,
+ rgba(255, 255, 255, 0.15) 50%,
+ rgba(255, 255, 255, 0.15) 75%,
+ transparent 75%,
+ transparent
+ ) !important;
+ background-size: 1rem 1rem !important;
}
\ No newline at end of file
diff --git a/src/popup/script/submission.js b/src/popup/script/submission.js
index 7c33bbd..354dd51 100644
--- a/src/popup/script/submission.js
+++ b/src/popup/script/submission.js
@@ -1,7 +1,7 @@
import { getDifficultyBasedSteps, getSubmissionResult, isSubmissionSuccess, isSubmitButton, needReview, updateProblemUponSuccessSubmission } from "../util/utils";
import { getAllProblems, createOrUpdateProblem, getCurrentProblemInfoFromLeetCodeByHref,getCurrentProblemInfoFromLeetCodeByUrl, syncProblems } from "../service/problemService";
import { Problem } from "../entity/problem";
-import { updateProblemWithFSRS } from "../util/fsrs";
+import { updateProblemWithFSRS } from "../service/fsrsService";
@@ -226,7 +226,7 @@ export async function handleFeedbackSubmission(problem = null) {
}
}
- problem = updateProblemWithFSRS(problem, feedback);
+ problem = await updateProblemWithFSRS(problem, feedback);
await createOrUpdateProblem(problem);
// 只有在页面提交时才显示成功提示
diff --git a/src/popup/service/fsrsService.js b/src/popup/service/fsrsService.js
new file mode 100644
index 0000000..7ed8128
--- /dev/null
+++ b/src/popup/service/fsrsService.js
@@ -0,0 +1,251 @@
+import { FSRS, Rating, S_MIN, State, TypeConvert, createEmptyCard } from 'ts-fsrs';
+import { defaultParams, qualityToRating, getFSRSParams, saveFSRSParams, saveRevlog, getAllRevlogs, exportRevlogsToCSV } from '../util/fsrs.js';
+import { optimizeFSRSParams } from '../delegate/fsrsDelegate.js';
+import { syncLocalAndCloudStorage } from '../util/utils.js';
+import localStorageDelegate from '../delegate/localStorageDelegate.js';
+import { store } from "../store";
+import { mergeFSRSParams, mergeRevlogs } from '../util/utils';
+
+
+
+// 创建FSRS实例
+let fsrsInstance = null;
+
+// 获取FSRS实例
+export const getFSRSInstance = async () => {
+ if (fsrsInstance) {
+ return fsrsInstance;
+ }
+
+ // 获取本地参数
+ const localParams = await getFSRSParams();
+
+ // 创建新的FSRS实例
+ fsrsInstance = new FSRS(localParams);
+ console.log('创建新的FSRS实例,参数:', localParams);
+
+ return fsrsInstance;
+};
+
+// 更新FSRS实例
+export const updateFSRSInstance = async (newParams) => {
+ // 创建新的FSRS实例
+ fsrsInstance = new FSRS(newParams);
+ console.log('更新FSRS实例,新参数:', newParams);
+
+ return fsrsInstance;
+};
+
+// 计算下次复习时间
+export const calculateNextReview = async (problem, feedback) => {
+ try {
+ const now = new Date();
+
+ // 确保有一个有效的 lastReview 日期
+ let lastReview;
+ if (problem.fsrsState && problem.fsrsState.lastReview) {
+ lastReview = new Date(problem.fsrsState.lastReview);
+ } else if (problem.submissionTime) {
+ lastReview = new Date(problem.submissionTime);
+ } else {
+ lastReview = new Date(now.getTime()); // 默认为昨天
+ }
+
+ // 检查日期是否有效
+ if (isNaN(lastReview.getTime())) {
+ lastReview = new Date(now.getTime()); // 如果无效,使用昨天
+ }
+
+ // 如果没有 fsrsState,创建一个默认的
+ if (!problem.fsrsState) {
+ problem.fsrsState = createEmptyCard(lastReview, (card) => {
+ return {
+ nextReview: +card.due,
+ stability: card.stability,
+ difficulty: card.difficulty,
+ state: card.state,
+ reviewCount: card.reps,
+ lapses: card.lapses,
+ lastReview: +lastReview // 存储为时间戳
+ }
+ });
+ }
+ let card = problem.fsrsState;
+
+ // 确保 nextReview 有效
+ if (!card.nextReview || isNaN(card.nextReview)) {
+ card.nextReview = +lastReview; // 默认为一天后
+ }
+
+ const rating = qualityToRating(feedback.quality);
+
+ // 确保所有参数都有有效值
+ const scheduledDays = Math.max(0, Math.floor((card.nextReview - card.lastReview) / (1000 * 60 * 60 * 24)));
+ const elapsedDays = Math.max(0, (now.getTime() - lastReview.getTime()) / (1000 * 60 * 60 * 24));
+
+ // 获取FSRS实例
+ const fsrs = await getFSRSInstance();
+
+ const result = fsrs.next({
+ due: card.nextReview,
+ stability: card.stability,
+ difficulty: card.difficulty,
+ elapsed_days: elapsedDays,
+ scheduled_days: scheduledDays,
+ reps: card.reviewCount,
+ lapse_count: card.lapses,
+ state: card.state,
+ last_review: lastReview, // 使用已经转换好的 Date 对象
+ }, now, rating);
+
+ return {
+ /**长期调度模式,ivl一定大于1d */
+ nextReview: +result.card.due,
+ stability: result.card.stability,
+ difficulty: result.card.difficulty,
+ state: result.card.state,
+ reviewCount: result.card.reps,
+ lapses: result.card.lapses
+ };
+ } catch (error) {
+ console.error('Error in calculateNextReview:', error);
+ const now = new Date(); // 在 catch 块中定义 now 变量
+ return {
+ nextReview: now.getTime() + (24 * 60 * 60 * 1000),
+ stability: problem.fsrsState.stability || S_MIN,
+ /** ref: https://github.com/open-spaced-repetition/ts-fsrs/blob/5eabd189d4740027ce1018cc968e67ca46c048a3/src/fsrs/default.ts#L20-L40 */
+ difficulty: problem.fsrsState.difficulty || defaultParams.w[4],
+ /** 长期调度下状态一定是New或Review */
+ state: problem.fsrsState.state || State.Review,
+ reviewCount: (problem.fsrsState.reviewCount || 0) + 1,
+ lapses: problem.fsrsState.lapses || 0
+ };
+ }
+};
+
+// 更新问题状态
+export const updateProblemWithFSRS = async (problem, feedback) => {
+ const now = Date.now();
+ const fsrsResult = await calculateNextReview(problem, feedback);
+
+ // 创建新的复习日志条目,只包含必要字段
+ const newRevlog = {
+ card_id: problem.index, // 使用问题索引作为卡片ID
+ review_time: now, // 复习时间(毫秒时间戳)
+ review_rating: qualityToRating(feedback.quality), // 复习评分 (1-4)
+ review_state: TypeConvert.state(problem.fsrsState ? problem.fsrsState?.state ?? State.New : 'New') // 复习状态 (0-3)
+ };
+
+ // 将复习日志存储到单独的 localStorage 键中
+ await saveRevlog(problem.index, newRevlog);
+
+ // 更新问题状态(不修改原有结构)
+ problem.fsrsState = {
+ ...problem.fsrsState,
+ difficulty: fsrsResult.difficulty,
+ stability: fsrsResult.stability,
+ state: fsrsResult.state,
+ lastReview: now,
+ nextReview: fsrsResult.nextReview,
+ reviewCount: fsrsResult.reps,
+ lapses: fsrsResult.lapses,
+ quality: feedback.quality
+ };
+
+ problem.modificationTime = now;
+ return problem;
+};
+
+// 获取复习记录数量
+export const getRevlogCount = async () => {
+ try {
+ const allRevlogs = await getAllRevlogs();
+ let totalCount = 0;
+
+ // 计算所有卡片的复习记录总数
+ Object.values(allRevlogs).forEach(cardRevlogs => {
+ totalCount += cardRevlogs.length;
+ });
+
+ return totalCount;
+ } catch (error) {
+ console.error('Error getting revlog count:', error);
+ return 0;
+ }
+};
+
+// 优化FSRS参数
+export const optimizeParameters = async (onProgress) => {
+ try {
+ // 获取并导出CSV格式的复习记录
+ const csvContent = await exportRevlogsToCSV();
+
+ // 调用API进行参数优化
+ const result = await optimizeFSRSParams(csvContent, onProgress);
+
+ // 检查结果是否包含params字段(来自done标签)
+ if (result && result.params) {
+ console.log('获取到优化后的FSRS参数:', result.params);
+
+ // 不再自动保存参数,而是返回结果供用户确认
+ return {
+ type: 'Success',
+ params: result.params,
+ metrics: result.metrics || {}
+ };
+ }
+
+ // 如果是进度信息
+ if (result && result.type === 'Progress') {
+ return result;
+ }
+
+ // 如果是训练结果
+ if (result && result.type === 'Train') {
+ return {
+ type: 'Train',
+ message: '训练完成,但未获取到完整参数'
+ };
+ }
+
+ // 其他情况
+ return result;
+ } catch (error) {
+ console.error('Error optimizing parameters:', error);
+ throw error;
+ }
+};
+
+// 同步FSRS历史记录
+export const syncFSRSHistory = async () => {
+ try {
+ // 检查是否启用了云同步
+ if (!store.isCloudSyncEnabled) {
+ console.log('云同步未启用,跳过FSRS历史记录同步');
+ return;
+ }
+
+ // 同步FSRS参数和复习日志
+ await syncFSRSParams();
+ await syncRevlogs();
+
+ // 更新FSRS实例
+ const updatedParams = await getFSRSParams();
+ await updateFSRSInstance(updatedParams);
+
+ console.log('FSRS历史记录同步完成');
+ } catch (error) {
+ console.error('同步FSRS历史记录失败:', error);
+ }
+};
+
+
+export const syncFSRSParams = async () => {
+ if (!store.isCloudSyncEnabled) return;
+ await syncLocalAndCloudStorage('fsrs_params', mergeFSRSParams);
+}
+
+export const syncRevlogs = async () => {
+ if (!store.isCloudSyncEnabled) return;
+ await syncLocalAndCloudStorage('fsrs_revlogs', mergeRevlogs);
+}
\ No newline at end of file
diff --git a/src/popup/util/fsrs.js b/src/popup/util/fsrs.js
index 4bc57e8..f7801c3 100644
--- a/src/popup/util/fsrs.js
+++ b/src/popup/util/fsrs.js
@@ -1,18 +1,18 @@
import { FSRS, Rating, S_MIN, State, TypeConvert, createEmptyCard, dateDiffInDays, generatorParameters } from 'ts-fsrs';
+import localStorageDelegate from '../delegate/localStorageDelegate.js';
+import cloudStorageDelegate from '../delegate/cloudStorageDelegate.js';
+import { store } from '../store';
// 1. 创建自定义参数
-const params = generatorParameters({
+export const defaultParams = generatorParameters({
request_retention: 0.9, // 期望记忆保持率 90%
maximum_interval: 365, // 最大间隔天数
enable_fuzz: false, // 禁用时间模糊化
enable_short_term: false // 启用短期记忆影响
});
-// 2. 创建 FSRS 实例
-const fsrs = new FSRS(params);
-
-// 3. 评分映射(4个等级)
-const qualityToRating = (quality) => {
+// 2. 评分映射(4个等级)
+export const qualityToRating = (quality) => {
switch(quality) {
case 1: return Rating.Again; // 完全不会
case 2: return Rating.Hard; // 有点难
@@ -22,127 +22,63 @@ const qualityToRating = (quality) => {
}
};
-// 4. 计算下次复习时间
-export const calculateNextReview = (problem, feedback) => {
+// 3. 获取本地FSRS参数
+export const getFSRSParams = async () => {
try {
- const now = new Date();
-
- // 确保有一个有效的 lastReview 日期
- let lastReview;
- if (problem.fsrsState && problem.fsrsState.lastReview) {
- lastReview = new Date(problem.fsrsState.lastReview);
- } else if (problem.submissionTime) {
- lastReview = new Date(problem.submissionTime);
- } else {
- lastReview = new Date(now.getTime()); // 默认为昨天
+ const result = await localStorageDelegate.get('fsrs_params');
+ console.log('找到本地FSRS参数:', result);
+ if (!result) {
+ console.log('未找到本地FSRS参数,使用默认参数');
+ return defaultParams;
}
- // 检查日期是否有效
- if (isNaN(lastReview.getTime())) {
- lastReview = new Date(now.getTime()); // 如果无效,使用昨天
+ // 如果结果是字符串,尝试解析它
+ if (typeof result === 'string') {
+ try {
+ const localParams = JSON.parse(result);
+ console.log('获取到本地FSRS参数:', localParams);
+ return localParams;
+ } catch (e) {
+ console.error('解析本地FSRS参数失败:', e);
+ return defaultParams;
+ }
}
+
+ // 如果结果已经是对象,直接返回
+ return result;
+ } catch (error) {
+ console.error('获取本地FSRS参数失败:', error);
+ return defaultParams;
+ }
+};
- // 如果没有 fsrsState,创建一个默认的
- if (!problem.fsrsState) {
- problem.fsrsState = createEmptyCard(lastReview, (card) => {
- return {
- nextReview: +card.due,
- stability: card.stability,
- difficulty: card.difficulty,
- state: card.state,
- reviewCount: card.reps,
- lapses: card.lapses,
- lastReview: +lastReview // 存储为时间戳
- }
- });
- }
- let card = problem.fsrsState;
+// 4. 保存FSRS参数到本地存储
+export const saveFSRSParams = async (newParams) => {
+ try {
+ // 为参数添加时间戳
+ const paramsWithTimestamp = {
+ ...newParams,
+ timestamp: Date.now()
+ };
-
+ // 保存到本地存储(字符串格式)
+ await localStorageDelegate.set('fsrs_params', JSON.stringify(paramsWithTimestamp));
+ console.log('FSRS参数已保存到本地存储');
- // 确保 nextReview 有效
- if (!card.nextReview || isNaN(card.nextReview)) {
- card.nextReview = +lastReview; // 默认为一天后
+ // 保存到云端存储(对象格式)
+ if (store.isCloudSyncEnabled) {
+ await cloudStorageDelegate.set('fsrs_params', paramsWithTimestamp);
+ console.log('FSRS参数已保存到云端存储');
}
-
- const rating = qualityToRating(feedback.quality);
-
- // 确保所有参数都有有效值
- const scheduledDays = Math.max(0, Math.floor((card.nextReview - card.lastReview) / (1000 * 60 * 60 * 24)));
- const elapsedDays = Math.max(0, (now.getTime() - lastReview.getTime()) / (1000 * 60 * 60 * 24));
-
- const result = fsrs.next({
- due: card.nextReview,
- stability: card.stability,
- difficulty: card.difficulty,
- elapsed_days: elapsedDays,
- scheduled_days: scheduledDays,
- reps: card.reviewCount,
- lapse_count: card.lapses,
- state: card.state,
- last_review: lastReview, // 使用已经转换好的 Date 对象
- }, now, rating);
-
- return {
- /**长期调度模式,ivl一定大于1d */
- nextReview: +result.card.due,
- stability: result.card.stability,
- difficulty: result.card.difficulty,
- state: result.card.state,
- reviewCount: result.card.reps,
- lapses: result.card.lapses
- };
+
+ return true;
} catch (error) {
- console.error('Error in calculateNextReview:', error);
- const now = new Date(); // 在 catch 块中定义 now 变量
- return {
- nextReview: now.getTime() + (24 * 60 * 60 * 1000),
- stability: problem.fsrsState.stability || S_MIN,
- /** ref: https://github.com/open-spaced-repetition/ts-fsrs/blob/5eabd189d4740027ce1018cc968e67ca46c048a3/src/fsrs/default.ts#L20-L40 */
- difficulty: problem.fsrsState.difficulty || params.w[4],
- /** 长期调度下状态一定是New或Review */
- state: problem.fsrsState.state || State.Review,
- reviewCount: (problem.fsrsState.reviewCount || 0) + 1,
- lapses: problem.fsrsState.lapses || 0
- };
+ console.error('保存FSRS参数失败:', error);
+ return false;
}
};
-
-// 5. 更新问题状态
-export const updateProblemWithFSRS = (problem, feedback) => {
- const now = Date.now();
- const fsrsResult = calculateNextReview(problem, feedback);
-
- // 创建新的复习日志条目,只包含必要字段
- const newRevlog = {
- card_id: problem.index, // 使用问题索引作为卡片ID
- review_time: now, // 复习时间(毫秒时间戳)
- review_rating: qualityToRating(feedback.quality), // 复习评分 (1-4)
- review_state: TypeConvert.state(problem.fsrsState ? problem.fsrsState?.state ?? State.New : 'New') // 复习状态 (0-3)
- };
-
- // 将复习日志存储到单独的 localStorage 键中
- saveRevlog(problem.index, newRevlog);
-
- // 更新问题状态(不修改原有结构)
- problem.fsrsState = {
- ...problem.fsrsState,
- difficulty: fsrsResult.difficulty,
- stability: fsrsResult.stability,
- state: fsrsResult.state,
- lastReview: now,
- nextReview: fsrsResult.nextReview,
- reviewCount: fsrsResult.reps,
- lapses: fsrsResult.lapses,
- quality: feedback.quality
- };
-
- problem.modificationTime = now;
- return problem;
-};
-
-// 保存单个复习日志
+// 5. 保存单个复习日志
export const saveRevlog = async (cardId, revlog) => {
try {
// 从 localStorage 获取现有的复习日志
@@ -168,12 +104,17 @@ export const saveRevlog = async (cardId, revlog) => {
// 添加新的复习日志
existingRevlogs[cardId].push(revlog);
- // 保存回 localStorage
+ // 保存到本地存储
await new Promise((resolve) => {
chrome.storage.local.set({ 'fsrs_revlogs': JSON.stringify(existingRevlogs) });
resolve();
});
+ // 如果启用了云同步,同时保存到云端
+ if (store.isCloudSyncEnabled) {
+ await cloudStorageDelegate.set('fsrs_revlogs', existingRevlogs);
+ }
+
return true;
} catch (error) {
console.error('Error saving revlog:', error);
@@ -181,31 +122,46 @@ export const saveRevlog = async (cardId, revlog) => {
}
};
-// 获取所有复习日志
+// 6. 获取所有复习日志
export const getAllRevlogs = async () => {
try {
- const revlogsStr = await new Promise((resolve) => {
+ let result;
+
+ // 如果启用了云同步,优先从云端获取
+ if (store.isCloudSyncEnabled) {
+ result = await cloudStorageDelegate.get('fsrs_revlogs');
+ if (result && Object.keys(result).length > 0) {
+ console.log('从云端获取复习日志:', result);
+ return result;
+ }
+ }
+
+ // 如果云端没有数据或未启用云同步,从本地获取
+ result = await new Promise((resolve) => {
chrome.storage.local.get(['fsrs_revlogs'], (result) => {
resolve(result.fsrs_revlogs || '{}');
});
});
- let allRevlogs;
- try {
- allRevlogs = JSON.parse(revlogsStr);
- } catch (e) {
- console.error('Error parsing revlogs:', e);
- return {};
+ // 如果结果是字符串,尝试解析它
+ if (typeof result === 'string') {
+ try {
+ return JSON.parse(result);
+ } catch (e) {
+ console.error('Error parsing revlogs:', e);
+ return {};
+ }
}
- return allRevlogs;
+ // 如果结果已经是对象,直接返回
+ return result || {};
} catch (error) {
console.error('Error getting revlogs:', error);
return {};
}
};
-// 导出复习日志为CSV格式
+// 7. 导出复习日志为CSV格式
export const exportRevlogsToCSV = async () => {
try {
// 获取所有复习日志
diff --git a/src/popup/util/utils.js b/src/popup/util/utils.js
index 31cb824..4273dca 100644
--- a/src/popup/util/utils.js
+++ b/src/popup/util/utils.js
@@ -173,3 +173,71 @@ export const getCurrentRetrievability = (problem) => {
const elapsedDays = dateDiffInDays(new Date(problem.fsrsState.lastReview), new Date());
return forgetting_curve(elapsedDays, problem.fsrsState.stability);
};
+
+export const mergeFSRSParams = (params1, params2) => {
+ if (params2 === undefined || params2 === null) return params1;
+ if (params1 === undefined || params1 === null) return params2;
+
+ // 如果云端数据比本地数据新,使用云端数据
+ const timestamp1 = params1.timestamp || 0;
+ const timestamp2 = params2.timestamp || 0;
+
+ // 返回较新的数据
+ const mergedParams = timestamp1 > timestamp2 ? params1 : params2;
+
+ // 确保返回的数据包含最新的时间戳
+ mergedParams.timestamp = Date.now();
+
+ return mergedParams;
+}
+
+export const mergeRevlogs = (revlogs1, revlogs2) => {
+ if (revlogs2 === undefined || revlogs2 === null) return revlogs1 || {};
+ if (revlogs1 === undefined || revlogs1 === null) return revlogs2 || {};
+
+ // 确保 revlogs1 和 revlogs2 是对象
+ revlogs1 = typeof revlogs1 === 'object' ? revlogs1 : {};
+ revlogs2 = typeof revlogs2 === 'object' ? revlogs2 : {};
+
+ // 合并复习日志
+ const mergedRevlogs = { ...revlogs1 };
+
+ // 遍历第二个复习日志集合
+ Object.keys(revlogs2).forEach(cardId => {
+ if (!mergedRevlogs[cardId]) {
+ // 如果第一个集合没有该卡片的复习日志,直接使用第二个集合的
+ mergedRevlogs[cardId] = Array.isArray(revlogs2[cardId]) ? revlogs2[cardId] : [];
+ } else {
+ // 如果两个集合都有该卡片的复习日志,合并两边的日志
+ const logs2 = Array.isArray(revlogs2[cardId]) ? revlogs2[cardId] : [];
+ const logs1 = Array.isArray(mergedRevlogs[cardId]) ? mergedRevlogs[cardId] : [];
+
+ // 创建一个Map来存储唯一的复习日志
+ const uniqueLogsMap = new Map();
+
+ // 添加第一个集合的日志
+ logs1.forEach(log => {
+ if (log && typeof log === 'object') {
+ const key = `${log.card_id}_${log.review_time}_${log.review_rating}`;
+ uniqueLogsMap.set(key, log);
+ }
+ });
+
+ // 添加第二个集合的日志
+ logs2.forEach(log => {
+ if (log && typeof log === 'object') {
+ const key = `${log.card_id}_${log.review_time}_${log.review_rating}`;
+ uniqueLogsMap.set(key, log);
+ }
+ });
+
+ // 转换回数组并按时间排序
+ mergedRevlogs[cardId] = Array.from(uniqueLogsMap.values())
+ .sort((a, b) => b.review_time - a.review_time);
+ }
+ });
+
+ return mergedRevlogs;
+}
+
+
diff --git a/src/popup/view/view.js b/src/popup/view/view.js
index 2fb8a58..8ba8e86 100644
--- a/src/popup/view/view.js
+++ b/src/popup/view/view.js
@@ -8,6 +8,7 @@ import { registerAllHandlers } from "../handler/handlerRegister";
import { hasOperationHistory } from "../service/operationHistoryService";
import { loadConfigs } from "../service/configService";
import { getLocalStorageData, setLocalStorageData } from "../../popup/delegate/localStorageDelegate";
+import { syncFSRSHistory } from "../service/fsrsService";
/*
Tag for problem records
@@ -422,6 +423,7 @@ export const renderAll = async () => {
await loadConfigs();
await renderSiteMode();
await syncProblems();
+ // await syncFSRSHistory();
// 创建笔记模态框