📖 课程目录 - ${currentCourseName}
${showMismatchWarning ? `
⚠️ 缓存的课程与当前课程不匹配,建议重新读取
` : ''}
${taskDescText}
${isFromCache ? `(缓存于 ${cacheTime})` : ''}
${tasksHtml}
已选择: 0 个任务
`;
const rerender = () => renderTaskList(availableTasks, showMismatchWarning, isFromCache);
bindTaskListEvents(overlay, currentCourseName, availableTasks, createModeState, rerender, currentCourseId);
};
// 从页面刷新读取目录(以页面为准,清理错误的本地记录)
const refreshDirectory = async () => {
showLoading();
// 等待展开所有目录
await expandAllCategories();
// 使用页面真实状态,忽略本地记录
const tasks = getCourseTaskList({ ignoreLocalCompleted: true });
const availableTasks = tasks.filter(t => !t.isDisabled);
// 清理本地记录中与页面状态不一致的任务
const completed = loadBatchCompleted();
const ourCompletedTasks = completed[currentCourseName] || [];
if (ourCompletedTasks.length > 0) {
const pageCompletedIds = tasks.filter(t => t.isCompleted).map(t => t.id);
const tasksToRemove = ourCompletedTasks.filter(id => !pageCompletedIds.includes(id));
if (tasksToRemove.length > 0) {
completed[currentCourseName] = ourCompletedTasks.filter(id => !tasksToRemove.includes(id));
if (completed[currentCourseName].length === 0) {
delete completed[currentCourseName];
}
saveBatchCompleted(completed);
console.log('[WeLearn-Go] 清理了本地完成记录中的错误任务:', tasksToRemove);
}
}
// 保存到缓存
saveCourseDirectoryCache(currentCourseId, currentCourseName, availableTasks);
renderTaskList(availableTasks, false, false);
showToast(`已读取 ${availableTasks.length} 个任务`, { duration: 2000 });
};
// 刷新完成状态(从页面重新扫描任务状态,以页面为准)
const refreshCompletionStatus = async (cachedTasks) => {
showLoading();
// 重新扫描页面获取最新任务状态(忽略本地记录,只看页面真实状态)
const freshTasks = getCourseTaskList({ ignoreLocalCompleted: true });
const freshTaskMap = new Map(freshTasks.map(t => [t.id, t]));
// 加载本地完成记录
const completed = loadBatchCompleted();
const ourCompletedTasks = completed[currentCourseName] || [];
// 找出本地记录中标记完成但页面显示未完成的任务(需要清理)
const tasksToRemove = [];
// 更新任务的完成状态(只使用页面真实状态)
const updatedTasks = cachedTasks.map(task => {
const freshTask = freshTaskMap.get(task.id);
const pageCompleted = freshTask?.isCompleted || false;
// 如果本地记录说已完成,但页面显示未完成,需要清理
if (ourCompletedTasks.includes(task.id) && !pageCompleted) {
tasksToRemove.push(task.id);
}
return {
...task,
isCompleted: pageCompleted
};
});
// 清理本地记录中错误标记的任务
if (tasksToRemove.length > 0 && completed[currentCourseName]) {
completed[currentCourseName] = completed[currentCourseName].filter(id => !tasksToRemove.includes(id));
if (completed[currentCourseName].length === 0) {
delete completed[currentCourseName];
}
saveBatchCompleted(completed);
console.log('[WeLearn-Go] 清理了本地完成记录中的错误任务:', tasksToRemove);
}
// 更新缓存
saveCourseDirectoryCache(currentCourseId, currentCourseName, updatedTasks);
renderTaskList(updatedTasks, false, true);
// 统计完成数量(只计算页面真实状态)
const completedCount = updatedTasks.filter(t => t.isCompleted).length;
const cleanedMsg = tasksToRemove.length > 0 ? `,已清理 ${tasksToRemove.length} 条错误记录` : '';
showToast(`已刷新完成状态 (${completedCount}/${updatedTasks.length} 已完成)${cleanedMsg}`, { duration: 3000 });
};
document.body.appendChild(overlay);
// 点击遮罩关闭
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
overlay.remove();
}
});
// 如果强制刷新或没有缓存,直接读取
if (forceRefresh || !hasCacheForCurrentCourse) {
if (courseIdMismatch) {
// 课程不匹配,显示警告并读取
showLoading();
await refreshDirectory();
} else {
// 无缓存,直接读取
showLoading();
await refreshDirectory();
}
} else {
// 有缓存,先刷新完成状态
await refreshCompletionStatus(cache.tasks);
}
};
/** 绑定任务列表事件 */
const bindTaskListEvents = (overlay, courseName, availableTasks, createModeState, rerender, courseId) => {
const taskCheckboxes = overlay.querySelectorAll('.welearn-task-checkbox:not([disabled])');
const unitCheckboxes = overlay.querySelectorAll('.welearn-unit-checkbox');
const selectedCountEl = overlay.querySelector('.welearn-selected-count');
const confirmButton = overlay.querySelector('.welearn-modal-confirm');
const cancelButton = overlay.querySelector('.welearn-modal-cancel');
const selectAllBtn = overlay.querySelector('.welearn-btn-select-all');
const deselectAllBtn = overlay.querySelector('.welearn-btn-deselect-all');
const refreshBtn = overlay.querySelector('.welearn-btn-refresh');
const refreshStatusBtn = overlay.querySelector('.welearn-btn-refresh-status');
const createListBtn = overlay.querySelector('.welearn-btn-create-list');
const exportListBtn = overlay.querySelector('.welearn-btn-export-list');
const importListBtn = overlay.querySelector('.welearn-btn-import-list');
const importInput = overlay.querySelector('.welearn-task-import-input');
const remarkInput = overlay.querySelector('.welearn-task-remark');
const getCheckedIds = () =>
Array.from(overlay.querySelectorAll('.welearn-task-checkbox:checked')).map(cb => cb.dataset.taskId);
/** 更新选中数量、按钮状态与行高亮 */
const updateSelectionState = () => {
const checkedIds = getCheckedIds();
selectedCountEl.textContent = checkedIds.length;
confirmButton.disabled = checkedIds.length === 0;
if (createModeState.active) {
createModeState.selectedIds = new Set(checkedIds);
} else {
createModeState.manualSelectedIds = checkedIds;
}
// 行高亮状态
overlay.querySelectorAll('.welearn-task-item').forEach((item) => {
const checkbox = item.querySelector('.welearn-task-checkbox');
if (!checkbox) return;
const isSelected = checkbox.checked && !checkbox.disabled;
item.classList.toggle('selected', isSelected);
});
// 更新单元复选框状态
unitCheckboxes.forEach(unitCb => {
const unitContainer = unitCb.closest('.welearn-task-unit');
const unitTasks = unitContainer?.querySelectorAll('.welearn-task-checkbox:not([disabled])') || [];
const checkedInUnit = unitContainer?.querySelectorAll('.welearn-task-checkbox:checked').length || 0;
unitCb.checked = unitTasks.length > 0 && checkedInUnit === unitTasks.length;
unitCb.indeterminate = checkedInUnit > 0 && checkedInUnit < unitTasks.length;
});
};
// 初始化选中状态
updateSelectionState();
// 任务复选框事件
taskCheckboxes.forEach(cb => {
cb.addEventListener('change', updateSelectionState);
});
// 单元复选框事件
unitCheckboxes.forEach(unitCb => {
unitCb.addEventListener('change', () => {
const unitContainer = unitCb.closest('.welearn-task-unit');
const unitTasks = unitContainer?.querySelectorAll('.welearn-task-checkbox:not([disabled])') || [];
unitTasks.forEach(cb => {
cb.checked = unitCb.checked;
});
updateSelectionState();
});
});
// 全选按钮
selectAllBtn?.addEventListener('click', () => {
taskCheckboxes.forEach(cb => { cb.checked = true; });
updateSelectionState();
});
// 取消全选按钮
deselectAllBtn?.addEventListener('click', () => {
taskCheckboxes.forEach(cb => { cb.checked = false; });
updateSelectionState();
});
// 创建任务列表模式切换
createListBtn?.addEventListener('click', () => {
const checkedIds = getCheckedIds();
if (createModeState.active) {
createModeState.selectedIds = new Set(checkedIds);
createModeState.remark = remarkInput?.value?.trim() || '';
} else {
createModeState.manualSelectedIds = checkedIds;
}
createModeState.active = !createModeState.active;
rerender();
});
// 导出任务列表
exportListBtn?.addEventListener('click', () => {
const checkedIds = getCheckedIds();
if (checkedIds.length === 0) {
showToast('请先勾选要导出的任务');
return;
}
const taskMap = new Map(availableTasks.map(t => [String(t.id), t]));
const tasks = checkedIds
.map(id => taskMap.get(String(id)))
.filter(Boolean)
.map(task => ({ id: task.id, title: task.title }));
const exportData = {
type: 'welearn-task-list',
version: 1,
courseId,
courseName,
remark: remarkInput?.value?.trim() || '',
tasks,
exportedAt: new Date().toISOString()
};
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
const safeCourseName = (courseName || '')
.replace(/[\\/:*?"<>|]/g, '_')
.replace(/\s+/g, '_')
.replace(/^_+|_+$/g, '');
const courseNamePart = safeCourseName ? `-${safeCourseName}` : '';
const now = new Date();
const timestamp = now.toISOString().slice(0, 16).replace(/[-:]/g, '').replace('T', '_');
link.download = `welearn-task-list-${courseId || 'unknown'}${courseNamePart}-${timestamp}.json`;
document.body.appendChild(link);
link.click();
link.remove();
setTimeout(() => URL.revokeObjectURL(url), 1000);
});
// 导入任务列表
importListBtn?.addEventListener('click', () => {
importInput?.click();
});
importInput?.addEventListener('change', () => {
const file = importInput.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
try {
const data = JSON.parse(String(reader.result || ''));
if (!data || data.type !== 'welearn-task-list') {
showToast('导入失败:文件格式不正确');
return;
}
if (String(data.courseId || '') !== String(courseId || '')) {
showToast('导入失败:课程不匹配');
return;
}
if (!Array.isArray(data.tasks)) {
showToast('导入失败:任务列表为空');
return;
}
const taskMap = new Map(availableTasks.map(t => [String(t.id), t]));
const importedIds = data.tasks.map(t => String(t.id)).filter(Boolean);
const existingIds = importedIds.filter(id => taskMap.has(id));
const missingCount = importedIds.length - existingIds.length;
createModeState.active = false;
createModeState.remark = typeof data.remark === 'string' ? data.remark : '';
createModeState.selectedIds = new Set(existingIds);
createModeState.manualSelectedIds = existingIds;
rerender();
const exportedAt = data.exportedAt ? new Date(data.exportedAt) : new Date();
const exportedAtText = Number.isNaN(exportedAt.getTime())
? ''
: exportedAt.toLocaleString('zh-CN');
const remarkText = typeof data.remark === 'string' && data.remark.trim()
? `备注:${data.remark.trim()}`
: '备注:无';
const timestampText = exportedAtText ? `时间:${exportedAtText}` : '时间:未知';
const summaryText = `${remarkText},${timestampText}`;
if (missingCount > 0) {
showToast(`已导入,忽略 ${missingCount} 个不存在任务