// ==UserScript==
// @name 小雅做做做
// @version 1.3.4
// @description 🚀 一键管理小雅平台任务,智能跟踪作业进度!✨ 支持自主学习任务的便捷提交,直观展示任务状态,帮你更高效完成学习目标! 📈
// @author Yi
// @license MIT
// @match https://*.ai-augmented.com/*
// @icon https://www.ai-augmented.com/static/logo3.1dbbea8f.png
// @run-at document-end
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @connect *.ai-augmented.com
// @homepageURL https://blog.zygame1314.site
// ==/UserScript==
(function () {
'use strict';
const domain = window.location.hostname;
let xiaoYaRecorder = null;
function getGroupIdFromUrl() {
const url = window.location.href;
const match = url.match(/mycourse\/(\d+)/);
return match ? match[1] : null;
}
function getResourceIdFromUrl() {
const resourceElement = document.querySelector('.group-resource-body');
if (!resourceElement) {
return null;
}
const url = window.location.href;
const match = url.match(/resource\/\d+\/(\d+)$/);
return match ? match[1] : null;
}
const userInfoCache = {
data: null,
timestamp: 0,
ttl: 5 * 60 * 1000,
set(data) {
this.data = data;
this.timestamp = Date.now();
},
get() {
if (!this.data) return null;
if (Date.now() - this.timestamp > this.ttl) {
this.clear();
return null;
}
return this.data;
},
clear() {
this.data = null;
this.timestamp = 0;
}
};
async function getUserInfo() {
const cachedInfo = userInfoCache.get();
if (cachedInfo) {
return cachedInfo;
}
try {
const token = getCookie('prd-access-token');
if (!token) {
console.error("未找到访问令牌");
return null;
}
const response = await fetch(`https://${domain}/api/jx-auth/oauth2/info`, {
headers: {
"accept": "/",
"content-type": "application/json; charset=utf-8",
"authorization": `Bearer ${token}`
},
method: "GET",
credentials: "include"
});
const data = await response.json();
if (data.success) {
const userId = data.data.info.id;
userInfoCache.set(userId);
return userId;
}
} catch (error) {
console.error("获取用户信息失败:", error);
return null;
}
}
function getCookie(keyword = 'prd-access-token') {
const cookies = document.cookie.split('; ');
for (const cookie of cookies) {
const [name, value] = cookie.split('=');
if (name.includes(keyword)) {
return value;
}
}
return null;
}
function getAuthToken() {
return new Promise((resolve, reject) => {
const token = getCookie();
if (token) {
resolve(token);
} else {
reject('未找到授权令牌');
}
});
}
function fetchTaskList(authToken) {
const GROUP_ID = getGroupIdFromUrl();
if (!GROUP_ID) {
console.log('当前页面不是课程页面');
return Promise.resolve([]);
}
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: `https://${domain}/api/jx-stat/group/task/queryTaskNotices?group_id=${GROUP_ID}&role=1`,
headers: {
"authorization": "Bearer " + authToken
},
onload: function (response) {
if (response.status === 200) {
let data = JSON.parse(response.responseText);
if (data.success) {
resolve(data.data.student_tasks);
} else {
reject(data.message);
}
} else {
reject('获取任务列表失败');
}
}
});
});
}
function fetchResourceList(authToken) {
const GROUP_ID = getGroupIdFromUrl();
if (!GROUP_ID) {
return Promise.resolve([]);
}
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: `https://${domain}/api/jx-iresource/resource/queryCourseResources?group_id=${GROUP_ID}`,
headers: {
"authorization": "Bearer " + authToken
},
onload: function (response) {
if (response.status === 200) {
let data = JSON.parse(response.responseText);
if (data.success) {
resolve(data.data);
} else {
reject(data.message);
}
} else {
reject('获取资源列表失败');
}
}
});
});
}
let isFreedomMode = false;
function createModal(title, message, onConfirm, onCancel) {
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.3);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
opacity: 0;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: blur(8px);
padding: 20px;
`;
const content = document.createElement('div');
content.innerHTML = `
📊
总体进度
已完成 ${completedTasks.length} / ${tasks.length} 个任务
${completionRate}%
已完成: ${completedPercent}%
进行中: ${ongoingPercent}%
已截止: ${overduePercent}%
${Object.entries({
"自主观看": [typeStats.document, "👁️"],
"作业": [typeStats.homework, "✍️"],
"课堂练习": [typeStats.exercise, "📚"],
"测验": [typeStats.quiz, "💯"],
"问卷": [typeStats.survey, "📋"],
"讨论": [typeStats.discussion, "💭"]
}).map(([name, [count, icon]]) => `
`).join('')}
`;
content.innerHTML = cardHTML;
statsContent.appendChild(content);
stats.appendChild(statsContent);
let isExpanded = false;
stats.addEventListener('click', () => {
isExpanded = !isExpanded;
if (isExpanded) {
statsContent.style.maxHeight = statsContent.scrollHeight + 'px';
toggleIcon.querySelector('svg').style.transform = 'rotate(180deg)';
stats.style.boxShadow = '0 12px 36px rgba(0,0,0,0.1)';
} else {
statsContent.style.maxHeight = '130px';
toggleIcon.querySelector('svg').style.transform = 'rotate(0deg)';
stats.style.boxShadow = '0 8px 32px rgba(0,0,0,0.06)';
}
const ripple = document.createElement('div');
ripple.style.cssText = `
position: absolute;
top: ${event.offsetY}px;
left: ${event.offsetX}px;
width: 5px;
height: 5px;
background: rgba(59,130,246,0.3);
border-radius: 50%;
pointer-events: none;
transform: scale(1);
opacity: 1;
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
`;
stats.appendChild(ripple);
requestAnimationFrame(() => {
ripple.style.transform = 'scale(100)';
ripple.style.opacity = '0';
setTimeout(() => ripple.remove(), 500);
});
});
stats.addEventListener('mouseenter', () => {
toggleIcon.querySelector('.stats-toggle-icon').style.background = 'rgba(59,130,246,0.2)';
stats.style.boxShadow = '0 0 10px rgba(0,0,0,0.1)';
});
stats.addEventListener('mouseleave', () => {
toggleIcon.querySelector('.stats-toggle-icon').style.background = 'rgba(59,130,246,0.1)';
stats.style.boxShadow = 'none';
});
const style = document.createElement('style');
style.textContent = `
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
`;
stats.appendChild(style);
fragment.appendChild(stats);
const documentTasks = tasks.filter(task => task.task_type === 1);
const exerciseTasks = tasks.filter(task => task.task_type === 2);
const paperTasks = tasks.filter(task => task.task_type === 3);
const quizTasks = tasks.filter(task => task.task_type === 4);
const surveyTasks = tasks.filter(task => task.task_type === 5);
const discussionTasks = tasks.filter(task => task.task_type === 6);
if (documentTasks.length > 0) {
let docnotice = document.createElement('div');
docnotice.innerHTML = `
📺
自主观看可以自动提交,但不会记录时长
`;
fragment.appendChild(docnotice);
}
if (exerciseTasks.length > 0) {
let excrcisenotice = document.createElement('div');
excrcisenotice.innerHTML = `
✍️
作业需要手动完成,无法自动提交答案
`;
fragment.appendChild(excrcisenotice);
}
if (paperTasks.length > 0) {
let paperNotice = document.createElement('div');
paperNotice.innerHTML = `
📚
课堂练习由课堂动态发布,请进入相应页面作答
`;
fragment.appendChild(paperNotice);
}
if (quizTasks.length > 0) {
let quizNotice = document.createElement('div');
quizNotice.innerHTML = `
💯
测验需要亲自作答,脚本无法自动完成
`;
fragment.appendChild(quizNotice);
}
if (surveyTasks.length > 0) {
let surveyNotice = document.createElement('div');
surveyNotice.innerHTML = `
📋
问卷需要手动填写,无法自动提交
`;
fragment.appendChild(surveyNotice);
}
if (discussionTasks.length > 0) {
let discussionNotice = document.createElement('div');
discussionNotice.innerHTML = `
💭
讨论需要发表评论或回复他人才能完成
`;
fragment.appendChild(discussionNotice);
}
const separator = document.createElement('div');
separator.innerHTML = `
💡
在以下区域勾选作业
只有自主观看作业可以自动提交,其它作业请手动完成
`;
fragment.appendChild(separator);
const createSectionTitle = (text, count, defaultExpanded = false) => {
const wrapper = document.createElement('div');
wrapper.style.marginBottom = '28px';
wrapper.style.marginTop = '28px';
wrapper.style.position = 'relative';
const header = document.createElement('div');
header.style.cursor = 'pointer';
header.style.userSelect = 'none';
header.style.transition = 'transform 0.3s ease';
header.innerHTML = `
▶
${text}
${count}个任务
`;
const contentWrapper = document.createElement('div');
contentWrapper.style.position = 'relative';
contentWrapper.style.overflow = 'hidden';
contentWrapper.style.transition = 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
contentWrapper.style.marginBottom = '8px';
contentWrapper.style.paddingLeft = '20px';
contentWrapper.style.paddingRight = '20px';
const content = document.createElement('div');
content.style.position = 'relative';
content.style.transition = 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
contentWrapper.appendChild(content);
let isAnimating = false;
const updateExpandState = (expanded) => {
if (isAnimating) return;
isAnimating = true;
const headerDiv = header.querySelector('div');
const icon = header.querySelector('.expand-icon');
const title = header.querySelector('h4');
const badge = header.querySelector('span:last-child');
if (expanded) {
contentWrapper.style.display = 'block';
contentWrapper.style.height = 'auto';
const targetHeight = contentWrapper.scrollHeight;
contentWrapper.style.height = '0px';
requestAnimationFrame(() => {
contentWrapper.style.height = targetHeight + 'px';
content.style.opacity = '1';
content.style.transform = 'translateY(0)';
headerDiv.style.background = 'linear-gradient(145deg, #EFF6FF, #DBEAFE)';
headerDiv.style.borderColor = 'rgba(59,130,246,0.1)';
icon.style.transform = 'rotate(90deg)';
icon.style.background = 'linear-gradient(145deg, #3B82F6, #2563EB)';
icon.style.color = '#fff';
icon.style.boxShadow = '0 4px 12px rgba(59,130,246,0.2)';
title.style.color = '#2563EB';
badge.style.background = 'linear-gradient(145deg, #EFF6FF, #DBEAFE)';
badge.style.color = '#3B82F6';
badge.style.borderColor = 'rgba(59,130,246,0.1)';
});
} else {
contentWrapper.style.height = contentWrapper.scrollHeight + 'px';
requestAnimationFrame(() => {
contentWrapper.style.height = '0';
content.style.opacity = '0';
content.style.transform = 'translateY(-10px)';
headerDiv.style.background = 'linear-gradient(145deg, rgba(255,255,255,0.95), rgba(249,250,251,0.95))';
headerDiv.style.borderColor = 'rgba(0,0,0,0.05)';
icon.style.transform = 'rotate(0deg)';
icon.style.background = 'linear-gradient(145deg, #f3f4f6, #e5e7eb)';
icon.style.color = '#6B7280';
icon.style.boxShadow = '0 2px 6px rgba(0,0,0,0.05)';
title.style.color = '#374151';
badge.style.background = 'linear-gradient(145deg, #F3F4F6, #E5E7EB)';
badge.style.color = '#6B7280';
badge.style.borderColor = 'rgba(107,114,128,0.1)';
});
}
contentWrapper.addEventListener('transitionend', function handler() {
if (expanded) {
contentWrapper.style.height = 'auto';
} else {
contentWrapper.style.display = 'none';
}
isAnimating = false;
contentWrapper.removeEventListener('transitionend', handler);
}, { once: true });
};
if (defaultExpanded) {
contentWrapper.style.display = 'block';
contentWrapper.style.height = 'auto';
content.style.opacity = '1';
content.style.transform = 'translateY(0)';
} else {
contentWrapper.style.display = 'none';
contentWrapper.style.height = '0';
content.style.opacity = '0';
content.style.transform = 'translateY(-10px)';
}
header.addEventListener('click', () => {
const isExpanded = contentWrapper.style.display !== 'none';
updateExpandState(!isExpanded);
});
header.addEventListener('mouseenter', () => {
const headerDiv = header.querySelector('div');
headerDiv.style.transform = 'scale(1.01)';
headerDiv.style.boxShadow = '0 6px 20px rgba(0,0,0,0.05)';
});
header.addEventListener('mouseleave', () => {
const headerDiv = header.querySelector('div');
headerDiv.style.transform = 'scale(1)';
headerDiv.style.boxShadow = '0 4px 16px rgba(0,0,0,0.03)';
});
wrapper.appendChild(header);
wrapper.appendChild(contentWrapper);
return { wrapper, content };
};
function createTaskSubTitle(title, count) {
const subTitleWrapper = document.createElement('div');
Object.assign(subTitleWrapper.style, {
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
marginBottom: '12px',
marginTop: '16px',
marginLeft: '12px',
marginRight: '12px'
});
const subTitle = document.createElement('div');
subTitle.innerHTML = `
${title}
${count}
`;
subTitleWrapper.appendChild(subTitle);
return subTitleWrapper;
}
if (documentTasks.length > 0) {
const { wrapper: docWrapper, content: docContent } = createSectionTitle('自主观看 (可自动提交)', documentTasks.length, true);
fragment.appendChild(docWrapper);
const ongoingDocTasks = documentTasks.filter(task =>
task.finish !== 2 && new Date(task.end_time) > now
);
const completedDocTasks = documentTasks.filter(task =>
task.finish === 2
);
const overdueDocTasks = documentTasks.filter(task =>
task.finish !== 2 && new Date(task.end_time) <= now
);
if (ongoingDocTasks.length > 0) {
const { wrapper: ongoingWrapper, content: ongoingContent } = createSectionTitle('进行中', ongoingDocTasks.length, true);
docContent.appendChild(ongoingWrapper);
const subTitleWrapper = createTaskSubTitle('进行中', ongoingDocTasks.length);
Object.assign(subTitleWrapper.style, {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: '12px',
marginTop: '16px',
marginLeft: '12px',
marginRight: '12px'
});
const selectAllWrapper = document.createElement('div');
selectAllWrapper.style.order = '-1';
selectAllWrapper.style.display = 'flex';
selectAllWrapper.style.alignItems = 'center';
selectAllWrapper.style.gap = '6px';
const selectAllCheckbox = document.createElement('input');
selectAllCheckbox.type = 'checkbox';
selectAllCheckbox.className = 'select-all-checkbox';
Object.assign(selectAllCheckbox.style, {
appearance: 'none',
'-webkit-appearance': 'none',
width: '18px',
height: '18px',
border: '2px solid #e2e8f0',
borderRadius: '4px',
cursor: 'pointer',
transition: 'all 0.2s ease',
backgroundColor: '#fff',
position: 'relative',
margin: '0'
});
const selectAllLabel = document.createElement('span');
selectAllLabel.textContent = '全选';
selectAllLabel.style.fontSize = '12px';
selectAllLabel.style.color = '#6B7280';
selectAllLabel.style.cursor = 'pointer';
selectAllCheckbox.addEventListener('change', function () {
const taskCheckboxes = container.querySelectorAll('.task-checkbox:not(:disabled)[data-node-id]');
taskCheckboxes.forEach(checkbox => {
checkbox.checked = this.checked;
checkbox.dispatchEvent(new Event('change'));
});
});
const observer = new MutationObserver(() => {
const taskCheckboxes = container.querySelectorAll('.task-checkbox:not(:disabled)');
const checkedCount = container.querySelectorAll('.task-checkbox:checked').length;
selectAllCheckbox.checked = checkedCount === taskCheckboxes.length && taskCheckboxes.length > 0;
});
observer.observe(container, { childList: true, subtree: true });
selectAllCheckbox.addEventListener('change', function () {
if (this.checked) {
this.style.backgroundColor = '#4CAF50';
this.style.borderColor = '#4CAF50';
this.style.backgroundImage = "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z'/%3E%3C/svg%3E\")";
this.style.backgroundSize = '14px';
this.style.backgroundPosition = 'center';
this.style.backgroundRepeat = 'no-repeat';
} else {
this.style.backgroundColor = '#fff';
this.style.borderColor = '#e2e8f0';
this.style.backgroundImage = 'none';
}
});
selectAllWrapper.appendChild(selectAllCheckbox);
selectAllWrapper.appendChild(selectAllLabel);
subTitleWrapper.appendChild(selectAllWrapper);
ongoingContent.appendChild(subTitleWrapper);
ongoingDocTasks.forEach(task => createTaskElement(task, resourceMap, ongoingContent, true));
}
if (completedDocTasks.length > 0) {
const { wrapper: completedWrapper, content: completedContent } = createSectionTitle('已完成', completedDocTasks.length, false);
docContent.appendChild(completedWrapper);
const subTitleWrapper = createTaskSubTitle('已完成', completedDocTasks.length);
completedContent.appendChild(subTitleWrapper);
completedDocTasks.forEach(task => createTaskElement(task, resourceMap, completedContent, false));
}
if (overdueDocTasks.length > 0) {
const { wrapper: overdueWrapper, content: overdueContent } = createSectionTitle('已截止', overdueDocTasks.length, false);
docContent.appendChild(overdueWrapper);
const subTitleWrapper = createTaskSubTitle('已截止', overdueDocTasks.length);
overdueContent.appendChild(subTitleWrapper);
overdueDocTasks.forEach(task => createTaskElement(task, resourceMap, overdueContent, false));
}
}
if (exerciseTasks.length > 0) {
const { wrapper: exerciseWrapper, content: exerciseContent } = createSectionTitle('作业 (需手动完成)', exerciseTasks.length, false);
fragment.appendChild(exerciseWrapper);
const ongoingExerciseTasks = exerciseTasks.filter(task =>
task.finish !== 2 && new Date(task.end_time) > now
);
const completedExerciseTasks = exerciseTasks.filter(task =>
task.finish === 2
);
const overdueExerciseTasks = exerciseTasks.filter(task =>
task.finish !== 2 && new Date(task.end_time) <= now
);
if (ongoingExerciseTasks.length > 0) {
const { wrapper: ongoingWrapper, content: ongoingContent } = createSectionTitle('进行中', ongoingExerciseTasks.length, true);
exerciseContent.appendChild(ongoingWrapper);
const subTitleWrapper = createTaskSubTitle('进行中', ongoingExerciseTasks.length);
ongoingContent.appendChild(subTitleWrapper);
ongoingExerciseTasks.forEach(task => createTaskElement(task, resourceMap, ongoingContent, false));
}
if (completedExerciseTasks.length > 0) {
const { wrapper: completedWrapper, content: completedContent } = createSectionTitle('已完成', completedExerciseTasks.length, false);
exerciseContent.appendChild(completedWrapper);
const subTitleWrapper = createTaskSubTitle('已完成', completedExerciseTasks.length);
completedContent.appendChild(subTitleWrapper);
completedExerciseTasks.forEach(task => createTaskElement(task, resourceMap, completedContent, false));
}
if (overdueExerciseTasks.length > 0) {
const { wrapper: overdueWrapper, content: overdueContent } = createSectionTitle('已截止', overdueExerciseTasks.length, false);
exerciseContent.appendChild(overdueWrapper);
const subTitleWrapper = createTaskSubTitle('已截止', overdueExerciseTasks.length);
overdueContent.appendChild(subTitleWrapper);
overdueExerciseTasks.forEach(task => createTaskElement(task, resourceMap, overdueContent, false));
}
}
if (paperTasks.length > 0) {
const { wrapper: paperWrapper, content: paperContent } = createSectionTitle('课堂练习 (需手动完成)', paperTasks.length, false);
fragment.appendChild(paperWrapper);
const ongoingPaperTasks = paperTasks.filter(task =>
task.finish !== 2 && new Date(task.end_time) > now
);
const completedPaperTasks = paperTasks.filter(task =>
task.finish === 2
);
const overduePaperTasks = paperTasks.filter(task =>
task.finish !== 2 && new Date(task.end_time) <= now
);
if (ongoingPaperTasks.length > 0) {
const { wrapper: ongoingWrapper, content: ongoingContent } = createSectionTitle('进行中', ongoingPaperTasks.length, true);
paperContent.appendChild(ongoingWrapper);
const subTitleWrapper = createTaskSubTitle('进行中', ongoingPaperTasks.length);
ongoingContent.appendChild(subTitleWrapper);
ongoingPaperTasks.forEach(task => createTaskElement(task, resourceMap, ongoingContent, false));
}
if (completedPaperTasks.length > 0) {
const { wrapper: completedWrapper, content: completedContent } = createSectionTitle('已完成', completedPaperTasks.length, false);
paperContent.appendChild(completedWrapper);
const subTitleWrapper = createTaskSubTitle('已完成', completedPaperTasks.length);
completedContent.appendChild(subTitleWrapper);
completedPaperTasks.forEach(task => createTaskElement(task, resourceMap, completedContent, false));
}
if (overduePaperTasks.length > 0) {
const { wrapper: overdueWrapper, content: overdueContent } = createSectionTitle('已截止', overduePaperTasks.length, false);
paperContent.appendChild(overdueWrapper);
const subTitleWrapper = createTaskSubTitle('已截止', overduePaperTasks.length);
overdueContent.appendChild(subTitleWrapper);
overduePaperTasks.forEach(task => createTaskElement(task, resourceMap, overdueContent, false));
}
}
if (quizTasks.length > 0) {
const { wrapper: quizWrapper, content: quizContent } = createSectionTitle('测验 (需手动完成)', quizTasks.length, false);
fragment.appendChild(quizWrapper);
const ongoingQuizTasks = quizTasks.filter(task =>
task.finish !== 2 && new Date(task.end_time) > now
);
const completedQuizTasks = quizTasks.filter(task =>
task.finish === 2
);
const overdueQuizTasks = quizTasks.filter(task =>
task.finish !== 2 && new Date(task.end_time) <= now
);
if (ongoingQuizTasks.length > 0) {
const { wrapper: ongoingWrapper, content: ongoingContent } = createSectionTitle('进行中', ongoingQuizTasks.length, true);
quizContent.appendChild(ongoingWrapper);
const subTitleWrapper = createTaskSubTitle('进行中', ongoingQuizTasks.length);
ongoingContent.appendChild(subTitleWrapper);
ongoingQuizTasks.forEach(task => createTaskElement(task, resourceMap, ongoingContent, false));
}
if (completedQuizTasks.length > 0) {
const { wrapper: completedWrapper, content: completedContent } = createSectionTitle('已完成', completedQuizTasks.length, false);
quizContent.appendChild(completedWrapper);
const subTitleWrapper = createTaskSubTitle('已完成', completedQuizTasks.length);
completedContent.appendChild(subTitleWrapper);
completedQuizTasks.forEach(task => createTaskElement(task, resourceMap, completedContent, false));
}
if (overdueQuizTasks.length > 0) {
const { wrapper: overdueWrapper, content: overdueContent } = createSectionTitle('已截止', overdueQuizTasks.length, false);
quizContent.appendChild(overdueWrapper);
const subTitleWrapper = createTaskSubTitle('已截止', overdueQuizTasks.length);
overdueContent.appendChild(subTitleWrapper);
overdueQuizTasks.forEach(task => createTaskElement(task, resourceMap, overdueContent, false));
}
}
if (surveyTasks.length > 0) {
const { wrapper: surveyWrapper, content: surveyContent } = createSectionTitle('问卷 (需手动完成)', surveyTasks.length, false);
fragment.appendChild(surveyWrapper);
const ongoingSurveyTasks = surveyTasks.filter(task =>
task.finish !== 2 && new Date(task.end_time) > now
);
const completedSurveyTasks = surveyTasks.filter(task =>
task.finish === 2
);
const overdueSurveyTasks = surveyTasks.filter(task =>
task.finish !== 2 && new Date(task.end_time) <= now
);
if (ongoingSurveyTasks.length > 0) {
const { wrapper: ongoingWrapper, content: ongoingContent } = createSectionTitle('进行中', ongoingSurveyTasks.length, true);
surveyContent.appendChild(ongoingWrapper);
const subTitleWrapper = createTaskSubTitle('进行中', ongoingSurveyTasks.length);
ongoingContent.appendChild(subTitleWrapper);
ongoingSurveyTasks.forEach(task => createTaskElement(task, resourceMap, ongoingContent, false));
}
if (completedSurveyTasks.length > 0) {
const { wrapper: completedWrapper, content: completedContent } = createSectionTitle('已完成', completedSurveyTasks.length, false);
surveyContent.appendChild(completedWrapper);
const subTitleWrapper = createTaskSubTitle('已完成', completedSurveyTasks.length);
completedContent.appendChild(subTitleWrapper);
completedSurveyTasks.forEach(task => createTaskElement(task, resourceMap, completedContent, false));
}
if (overdueSurveyTasks.length > 0) {
const { wrapper: overdueWrapper, content: overdueContent } = createSectionTitle('已截止', overdueSurveyTasks.length, false);
surveyContent.appendChild(overdueWrapper);
const subTitleWrapper = createTaskSubTitle('已截止', overdueSurveyTasks.length);
overdueContent.appendChild(subTitleWrapper);
overdueSurveyTasks.forEach(task => createTaskElement(task, resourceMap, overdueContent, false));
}
}
if (discussionTasks.length > 0) {
const { wrapper: discussionWrapper, content: discussionContent } = createSectionTitle('讨论 (需手动完成)', discussionTasks.length, false);
fragment.appendChild(discussionWrapper);
const ongoingDiscussionTasks = discussionTasks.filter(task =>
task.finish !== 2 && new Date(task.end_time) > now
);
const completedDiscussionTasks = discussionTasks.filter(task =>
task.finish === 2
);
const overdueDiscussionTasks = discussionTasks.filter(task =>
task.finish !== 2 && new Date(task.end_time) <= now
);
if (ongoingDiscussionTasks.length > 0) {
const { wrapper: ongoingWrapper, content: ongoingContent } = createSectionTitle('进行中', ongoingDiscussionTasks.length, true);
discussionContent.appendChild(ongoingWrapper);
const subTitleWrapper = createTaskSubTitle('进行中', ongoingDiscussionTasks.length);
ongoingContent.appendChild(subTitleWrapper);
ongoingDiscussionTasks.forEach(task => createTaskElement(task, resourceMap, ongoingContent, false));
}
if (completedDiscussionTasks.length > 0) {
const { wrapper: completedWrapper, content: completedContent } = createSectionTitle('已完成', completedDiscussionTasks.length, false);
discussionContent.appendChild(completedWrapper);
const subTitleWrapper = createTaskSubTitle('已完成', completedDiscussionTasks.length);
completedContent.appendChild(subTitleWrapper);
completedDiscussionTasks.forEach(task => createTaskElement(task, resourceMap, completedContent, false));
}
if (overdueDiscussionTasks.length > 0) {
const { wrapper: overdueWrapper, content: overdueContent } = createSectionTitle('已截止', overdueDiscussionTasks.length, false);
discussionContent.appendChild(overdueWrapper);
const subTitleWrapper = createTaskSubTitle('已截止', overdueDiscussionTasks.length);
overdueContent.appendChild(subTitleWrapper);
overdueDiscussionTasks.forEach(task => createTaskElement(task, resourceMap, overdueContent, false));
}
}
const recorderComponent = createRecorderComponent();
fragment.appendChild(recorderComponent);
let buttonContainer = document.createElement('div');
Object.assign(buttonContainer.style, {
position: 'sticky',
bottom: '20px',
left: '0',
right: '0',
padding: '0 20px',
marginTop: '30px',
zIndex: '1002',
backgroundColor: 'rgba(255, 255, 255, 0.9)',
backdropFilter: 'blur(8px)',
borderTop: '1px solid rgba(0,0,0,0.05)',
paddingTop: '20px'
});
let submitButton = document.createElement('button');
submitButton.innerHTML = `
📤
提交完成状态
`;
Object.assign(submitButton.style, {
width: '100%',
padding: '14px',
border: 'none',
borderRadius: '12px',
background: 'linear-gradient(145deg, #3B82F6, #2563EB)',
color: 'white',
fontSize: '15px',
fontWeight: '600',
cursor: 'pointer',
transition: 'all 0.3s ease',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 4px 6px rgba(37,99,235,0.1)'
});
submitButton.onmouseenter = () => {
submitButton.style.transform = 'translateY(-2px)';
submitButton.style.boxShadow = '0 6px 12px rgba(37,99,235,0.2)';
};
submitButton.onmouseleave = () => {
submitButton.style.transform = 'translateY(0)';
submitButton.style.boxShadow = '0 4px 6px rgba(37,99,235,0.1)';
};
submitButton.onmousedown = () => {
submitButton.style.transform = 'translateY(1px)';
};
submitButton.onmouseup = () => {
submitButton.style.transform = 'translateY(-2px)';
};
submitButton.onclick = () => {
let selectedTasks = Array.from(container.querySelectorAll('input[type="checkbox"]:checked'));
if (selectedTasks.length > 0) {
submitButton.style.opacity = '0.7';
submitButton.innerHTML = `
⏳
正在提交...
`;
submitTasks(selectedTasks);
setTimeout(() => {
submitButton.style.opacity = '1';
submitButton.innerHTML = `
📤
提交完成状态
`;
}, 1000);
} else {
showNotification('请至少选择一个任务', { type: 'warning', keywords: ['选择', '任务'] });
}
};
buttonContainer.appendChild(submitButton);
fragment.appendChild(buttonContainer);
container.replaceChildren(fragment);
}
function createTaskElement(task, resourceMap, fragment, enableCheckbox) {
const resourceInfo = resourceMap.get(task.task_id);
let taskItem = document.createElement('div');
taskItem.className = 'task-item';
Object.assign(taskItem.style, {
marginBottom: '20px',
padding: '20px',
borderRadius: '16px',
backgroundColor: enableCheckbox ? '#fff' : '#f8f9fa',
boxShadow: '0 4px 12px rgba(0,0,0,0.03)',
border: '1px solid rgba(238,240,242,0.8)',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
display: 'flex',
alignItems: 'flex-start',
cursor: enableCheckbox ? 'pointer' : 'default',
position: 'relative',
overflow: 'hidden'
});
if (enableCheckbox) {
taskItem.style.background = `
linear-gradient(135deg,
rgba(255,255,255,1) 0%,
rgba(250,252,255,0.95) 100%)
`;
}
taskItem.onmouseenter = () => {
if (enableCheckbox) {
taskItem.style.transform = 'translateY(-3px) scale(1.01)';
taskItem.style.boxShadow = '0 8px 24px rgba(0,0,0,0.06)';
taskItem.style.borderColor = 'rgba(66,153,225,0.2)';
}
};
taskItem.onmouseleave = () => {
taskItem.style.transform = 'translateY(0) scale(1)';
taskItem.style.boxShadow = '0 4px 12px rgba(0,0,0,0.03)';
taskItem.style.borderColor = 'rgba(238,240,242,0.8)';
};
let checkboxWrapper = document.createElement('div');
checkboxWrapper.style.marginRight = '16px';
checkboxWrapper.style.position = 'relative';
let checkbox = document.createElement('input');
checkbox.className = 'task-checkbox';
checkbox.type = 'checkbox';
checkbox.value = task.task_id;
checkbox.dataset.nodeId = task.node_id;
const shouldEnableCheckbox = enableCheckbox || isFreedomMode;
checkbox.disabled = !shouldEnableCheckbox;
Object.assign(checkbox.style, {
appearance: 'none',
'-webkit-appearance': 'none',
width: '22px',
height: '22px',
border: '2px solid ' + (isFreedomMode && !enableCheckbox ? '#ef4444' : '#e2e8f0'),
borderRadius: '6px',
cursor: shouldEnableCheckbox ? 'pointer' : 'not-allowed',
transition: 'all 0.2s ease',
backgroundColor: '#fff',
position: 'relative',
margin: '0',
zIndex: '1'
});
checkbox.addEventListener('change', function () {
if (this.checked) {
this.style.backgroundColor = '#4CAF50';
this.style.borderColor = '#4CAF50';
this.style.backgroundImage = "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z'/%3E%3C/svg%3E\")";
this.style.backgroundSize = '16px';
this.style.backgroundPosition = 'center';
this.style.backgroundRepeat = 'no-repeat';
} else {
this.style.backgroundColor = '#fff';
this.style.borderColor = isFreedomMode && !enableCheckbox ? '#DC2626' : '#e2e8f0';
this.style.backgroundImage = 'none';
}
});
let taskInfo = document.createElement('div');
taskInfo.style.flex = '1';
const deadlineDate = new Date(task.end_time);
const isOverdue = deadlineDate < new Date();
const statusStyles = {
completed: {
color: '#10B981',
bg: '#ECFDF5',
border: '#A7F3D0'
},
overdue: {
color: '#EF4444',
bg: '#FEF2F2',
border: '#FECACA'
},
ongoing: {
color: '#3B82F6',
bg: '#EFF6FF',
border: '#BFDBFE'
}
};
const getStatusStyle = () => {
if (task.finish === 2) return statusStyles.completed;
return isOverdue ? statusStyles.overdue : statusStyles.ongoing;
};
const statusStyle = getStatusStyle();
const status = `
${task.finish === 2 ? '✓ 已完成' : (isOverdue ? '⚠️ 已截止' : '⏳ 进行中')}
`;
const taskTypeText = {
1: '自主观看',
2: '作业',
3: '课堂练习',
4: '测验',
5: '问卷',
6: '讨论'
}[task.task_type];
taskInfo.innerHTML = `
${resourceInfo ? resourceInfo.name : '未知作业名称'}
${isFreedomMode && !enableCheckbox ? `
高风险操作
` : ''}
${status}
📝
${taskTypeText}
⏰
${new Date(task.start_time).toLocaleString('zh-CN', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
🔚
${new Date(task.end_time).toLocaleString('zh-CN', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
${task.finish_time ?
`
✅
完成于: ${new Date(task.finish_time).toLocaleString('zh-CN')}
` : ''}
${task.task_type === 2 || task.task_type === 3 || task.task_type === 4 || task.task_type === 5 || task.task_type === 6 ?
`
⚠️
需要手动完成
` : ''}
`;
checkboxWrapper.appendChild(checkbox);
taskItem.appendChild(checkboxWrapper);
taskItem.appendChild(taskInfo);
fragment.appendChild(taskItem);
const deadline = new Date(task.end_time);
const now = new Date();
const threeDaysFromNow = new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000);
if (deadline <= threeDaysFromNow && deadline > now && task.finish !== 2) {
const daysLeft = Math.ceil((deadline - now) / (1000 * 60 * 60 * 24));
taskInfo.innerHTML += `
❗
还剩 ${daysLeft} 天截止
`;
}
}
function submitTasks(selectedTasks) {
const validTasks = selectedTasks.filter(task => task.dataset.nodeId && task.value);
if (validTasks.length === 0) {
showNotification('没有有效的任务可提交', { type: 'warning' });
return;
}
getAuthToken().then(authToken => {
fetchResourceList(authToken).then(resources => {
const resourceMap = new Map();
resources.forEach(resource => {
if (resource.is_task) {
resourceMap.set(resource.task_id, resource);
}
});
validTasks.forEach(taskElement => {
const GROUP_ID = getGroupIdFromUrl();
const taskId = taskElement.value;
const nodeId = taskElement.dataset.nodeId;
const resource = resourceMap.get(taskId);
if (!resource) {
showNotification(`未找到任务 ${taskId} 的资源信息`, {
type: 'warning',
keywords: ['未找到', '资源']
});
return;
}
const taskName = resource.name;
GM_xmlhttpRequest({
method: "POST",
url: `https://${domain}/api/jx-iresource/resource/finishActivity`,
headers: {
"authorization": "Bearer " + authToken,
"Content-Type": "application/json; charset=UTF-8"
},
data: JSON.stringify({
"group_id": GROUP_ID,
"node_id": nodeId,
"task_id": taskId
}),
onload: function (response) {
if (response.status === 200) {
const data = JSON.parse(response.responseText);
if (data.success) {
showNotification(`"${taskName}" 已完成`, {
type: 'success',
keywords: [taskName, '完成']
});
setTimeout(() => {
location.reload();
}, 1500);
} else {
showNotification(`"${taskName}" 完成失败:${data.message}`, {
type: 'error',
keywords: [taskName, '失败']
});
}
} else {
showNotification(`"${taskName}" 提交失败`, {
type: 'error',
keywords: [taskName, '失败']
});
}
}
});
});
}).catch(error => {
showNotification('获取资源列表失败:' + error, {
type: 'error',
keywords: ['获取', '失败']
});
});
}).catch(error => {
showNotification('获取认证令牌失败:' + error, {
type: 'error',
keywords: ['认证', '失败']
});
});
}
function getNotificationContainer() {
let container = document.getElementById('notification-container');
if (!container) {
container = document.createElement('div');
container.id = 'notification-container';
container.style.position = 'fixed';
container.style.top = '20px';
container.style.left = '50%';
container.style.transform = 'translateX(-50%)';
container.style.zIndex = '10000';
container.style.width = '400px';
container.style.maxHeight = 'calc(100vh - 40px)';
container.style.overflowY = 'auto';
container.style.pointerEvents = 'none';
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.alignItems = 'center';
document.body.appendChild(container);
}
return container;
}
function showNotification(message, options = {}) {
const {
type = 'info',
duration = 3000,
keywords = [],
} = options;
if (!globalThis._notificationCache) {
globalThis._notificationCache = new Map();
}
const notificationKey = `${message}-${type}`;
const existingNotification = globalThis._notificationCache.get(notificationKey);
if (existingNotification) {
const now = Date.now();
if (now - existingNotification < 1500) {
return;
}
}
globalThis._notificationCache.set(notificationKey, Date.now());
const CACHE_CLEANUP_DELAY = 10000;
setTimeout(() => {
globalThis._notificationCache.delete(notificationKey);
}, CACHE_CLEANUP_DELAY);
const highlightColors = {
success: '#ffba08',
error: '#14b8a6',
warning: '#8b5cf6',
info: '#f472b6'
};
const highlightColor = highlightColors[type] || highlightColors.info;
const highlightStyle = `
color: ${highlightColor};
font-weight: bold;
border-bottom: 2px solid ${highlightColor}50;
transition: all 0.3s ease;
border-radius: 3px;
`;
const highlightedMessage = keywords.reduce((msg, keyword) => {
if (keyword && keyword.trim()) {
const regex = new RegExp(keyword.trim(), 'g');
return msg.replace(regex, `
${keyword}`);
}
return msg;
}, message);
const notification = document.createElement('div');
notification.style.position = 'relative';
notification.style.marginBottom = '10px';
notification.style.padding = '15px 20px';
notification.style.borderRadius = '12px';
notification.style.color = '#333';
notification.style.fontSize = '16px';
notification.style.fontWeight = 'bold';
notification.style.boxShadow = '0 8px 16px rgba(0,0,0,0.08), 0 4px 8px rgba(0,0,0,0.06)';
notification.style.pointerEvents = 'auto';
notification.style.opacity = '0';
notification.style.transform = 'translateY(-20px)';
notification.style.transition = 'all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55)';
notification.style.display = 'flex';
notification.style.alignItems = 'center';
notification.style.backdropFilter = 'blur(8px)';
const typeStyles = {
success: {
background: 'linear-gradient(145deg, rgba(104, 214, 156, 0.95), rgba(89, 186, 134, 0.95))',
icon: '🎉'
},
error: {
background: 'linear-gradient(145deg, rgba(248, 113, 113, 0.95), rgba(220, 38, 38, 0.95))',
icon: '❌'
},
warning: {
background: 'linear-gradient(145deg, rgba(251, 191, 36, 0.95), rgba(245, 158, 11, 0.95))',
icon: '⚠️'
},
info: {
background: 'linear-gradient(145deg, rgba(96, 165, 250, 0.95), rgba(59, 130, 246, 0.95))',
icon: 'ℹ️'
}
};
const currentType = typeStyles[type] || typeStyles.info;
notification.style.background = currentType.background;
notification.style.color = type === 'info' || type === 'success' ? '#fff' : '#000';
const progressBar = document.createElement('div');
progressBar.style.position = 'absolute';
progressBar.style.bottom = '0';
progressBar.style.left = '0';
progressBar.style.height = '4px';
progressBar.style.width = '100%';
progressBar.style.background = 'rgba(255, 255, 255, 0.3)';
progressBar.style.borderRadius = '0 0 12px 12px';
progressBar.style.transition = `width ${duration}ms cubic-bezier(0.4, 0, 0.2, 1)`;
const icon = document.createElement('span');
icon.style.marginRight = '12px';
icon.style.fontSize = '20px';
icon.textContent = currentType.icon;
icon.style.filter = 'saturate(1.2)';
const messageContainer = document.createElement('div');
messageContainer.innerHTML = highlightedMessage;
messageContainer.style.flex = '1';
messageContainer.style.fontWeight = 'bold';
const closeBtn = document.createElement('span');
closeBtn.textContent = '×';
closeBtn.style.marginLeft = '12px';
closeBtn.style.fontSize = '24px';
closeBtn.style.cursor = 'pointer';
closeBtn.style.opacity = '0.8';
closeBtn.style.transition = 'opacity 0.2s';
closeBtn.addEventListener('mouseover', () => {
closeBtn.style.opacity = '1';
});
closeBtn.addEventListener('mouseout', () => {
closeBtn.style.opacity = '0.8';
});
notification.addEventListener('mouseenter', () => {
notification.style.transform = 'translateY(0) scale(1.02)';
progressBar.style.transition = 'none';
});
notification.addEventListener('mouseleave', () => {
notification.style.transform = 'translateY(0) scale(1)';
progressBar.style.transition = `width ${duration}ms linear`;
});
notification.appendChild(icon);
notification.appendChild(messageContainer);
notification.appendChild(closeBtn);
notification.appendChild(progressBar);
const container = getNotificationContainer();
container.appendChild(notification);
requestAnimationFrame(() => {
notification.style.opacity = '1';
notification.style.transform = 'translateY(0)';
requestAnimationFrame(() => {
progressBar.style.width = '0';
});
});
function hideNotification(notification) {
notification.style.opacity = '0';
notification.style.transform = 'translateY(-20px)';
setTimeout(() => {
container.removeChild(notification);
if (container.children.length === 0) {
document.body.removeChild(container);
}
}, 300);
}
closeBtn.addEventListener('click', (e) => {
e.stopPropagation();
hideNotification(notification);
});
notification.addEventListener('click', () => {
hideNotification(notification);
});
if (duration > 0) {
setTimeout(() => {
if (container.contains(notification)) {
hideNotification(notification);
}
}, duration);
}
}
class LearnRecorder {
constructor() {
this.apiUrl = `https://${domain}/api/jx-iresource/learnLength/learnRecord`;
this.interval = 30000;
this.timer = null;
this.recordCount = 0;
this.lastRecordTime = null;
this.totalTime = 0;
this.isFirstRecord = true;
this.realTimer = null;
this.realTimeSeconds = 0;
this.destroy = () => {
this.stop();
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
if (this.realTimer) {
clearInterval(this.realTimer);
this.realTimer = null;
}
};
setTimeout(() => {
this.updateStatus('info', '记录器已就绪,点击开始记录按钮开始记录学习时长');
}, 500);
}
async createSignature(message, timestamp = Date.now().toString(), nonce = crypto.randomUUID()) {
const arr = [
encodeURIComponent(message),
timestamp,
nonce,
"--xy-create-signature--"
];
const sortedStr = arr.sort().join("");
const msgBuffer = new TextEncoder().encode(sortedStr);
const hashBuffer = await crypto.subtle.digest('SHA-1', msgBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const signature = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return signature;
}
async sendRecord() {
try {
const params = await this.getCurrentPageParams();
if (!params || !params.userId || !params.groupId || !params.resourceId) {
console.log('缺少必要参数:', params);
this.updateStatus('error', '记录失败:缺少必要参数');
return;
}
const message = JSON.stringify({
user_id: params.userId,
group_id: params.groupId,
clientType: 1,
roleType: 1,
resourceId: params.resourceId
});
const token = getCookie('prd-access-token');
if (!token) {
console.error('未找到认证令牌');
return;
}
const timestamp = Date.now().toString();
const nonce = crypto.randomUUID();
const signature = await this.createSignature(message, timestamp, nonce);
const body = {
message: message,
signature: signature,
timestamp: timestamp,
nonce: nonce
};
const response = await fetch(this.apiUrl, {
method: 'POST',
headers: {
'authorization': `Bearer ${token}`,
'content-type': 'application/json; charset=UTF-8'
},
body: JSON.stringify(body)
});
const result = await response.json();
if (result.code === 200 || result.success) {
this.recordCount++;
this.lastRecordTime = new Date();
if (!this.isFirstRecord) {
this.totalTime += 30;
}
this.isFirstRecord = false;
this.updateStatus('success', '记录成功');
} else {
this.updateStatus('error', '记录失败:' + (result.message || '未知错误'));
}
} catch (error) {
console.error('记录失败:', error);
this.updateStatus('error', '记录失败:' + (error.message || '未知错误'));
this.failureCount = (this.failureCount || 0) + 1;
if (this.failureCount >= 3) {
this.stop();
this.updateStatus('error', '由于连续记录失败,已自动停止记录');
}
}
}
async getCurrentPageParams() {
const userId = await getUserInfo();
return {
userId: userId,
groupId: getGroupIdFromUrl(),
resourceId: getResourceIdFromUrl()
};
}
updateStatus(type, message) {
const event = new CustomEvent('recordStatus', {
detail: {
type,
message,
count: this.recordCount,
time: this.lastRecordTime,
totalTime: this.totalTime,
realTimeSeconds: this.realTimeSeconds
}
});
document.dispatchEvent(event);
}
reset() {
this.recordCount = 0;
this.lastRecordTime = null;
this.totalTime = 0;
this.isFirstRecord = true;
this.realTimeSeconds = 0;
this.updateStatus('info', '记录已重置');
}
startRealTimer() {
if (this.realTimer) return;
this.realTimer = setInterval(() => {
this.realTimeSeconds++;
const event = new CustomEvent('timeUpdate', {
detail: {
realTimeSeconds: this.realTimeSeconds
}
});
document.dispatchEvent(event);
}, 1000);
}
stopRealTimer() {
if (this.realTimer) {
clearInterval(this.realTimer);
this.realTimer = null;
}
this.realTimeSeconds = 0;
}
async start() {
try {
const params = await this.getCurrentPageParams();
if (!params || !params.userId || !params.groupId || !params.resourceId) {
this.updateStatus('error', '无法开始记录:缺少必要参数');
return false;
}
const token = getCookie('prd-access-token');
if (!token) {
this.updateStatus('error', '无法开始记录:未找到访问令牌');
return false;
}
if (this.timer) return true;
await this.sendRecord();
this.timer = setInterval(() => this.sendRecord(), this.interval);
this.startRealTimer();
console.log('开始记录学习时长');
return true;
} catch (error) {
this.updateStatus('error', '启动记录器失败:' + error.message);
return false;
}
}
stop() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
this.stopRealTimer();
this.reset();
console.log('停止记录学习时长');
}
}
}
function createRecorderComponent() {
const recorderSection = document.createElement('div');
const recorder = xiaoYaRecorder;
if (!recorder) {
console.error('未找到全局记录器实例');
return recorderSection;
}
recorderSection.innerHTML = `
`;
const container = recorderSection.querySelector('div');
const timerIcon = recorderSection.querySelector('.timer-icon');
const recordStatus = recorderSection.querySelector('.record-status');
const startBtn = recorderSection.querySelector('#start-record');
const stopBtn = recorderSection.querySelector('#stop-record');
startBtn.onclick = async () => {
const success = await recorder.start();
if (!success) {
return;
}
startBtn.style.opacity = '0.5';
startBtn.disabled = true;
stopBtn.style.opacity = '1';
stopBtn.disabled = false;
container.classList.add('recording');
recordStatus.style.display = 'inline-block';
timerIcon.style.transform = 'rotate(360deg)';
setTimeout(() => {
timerIcon.style.transition = 'transform 1s linear';
timerIcon.style.transform = 'rotate(0deg)';
}, 300);
};
stopBtn.onclick = () => {
recorder.stop();
startBtn.style.opacity = '1';
startBtn.disabled = false;
stopBtn.style.opacity = '0.5';
stopBtn.disabled = true;
container.classList.remove('recording');
recordStatus.style.display = 'none';
const statusInfo = recorderSection.querySelector('.status-info');
const countElement = statusInfo.querySelector('.record-count span');
const timeElement = statusInfo.querySelector('.last-record-time span');
countElement.textContent = '0';
timeElement.textContent = '暂无记录';
timerIcon.style.transform = 'scale(0.8)';
setTimeout(() => {
timerIcon.style.transform = 'scale(1)';
}, 200);
};
function formatTime(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const remainingSeconds = seconds % 60;
return `${hours}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
}
document.addEventListener('timeUpdate', (e) => {
const { realTimeSeconds } = e.detail;
const realTimeElement = recorderSection.querySelector('.real-time span');
realTimeElement.textContent = formatTime(realTimeSeconds);
});
document.addEventListener('recordStatus', (e) => {
const { type, message, count, time, totalTime, realTimeSeconds } = e.detail;
const statusInfo = recorderSection.querySelector('.status-info');
const countElement = statusInfo.querySelector('.record-count span');
const timeElement = statusInfo.querySelector('.last-record-time span');
const messageElement = statusInfo.querySelector('.record-status-message');
const timeStudiedElement = statusInfo.querySelector('.total-study-time span');
const realTimeElement = statusInfo.querySelector('.real-time span');
timeStudiedElement.textContent = formatTime(totalTime);
realTimeElement.textContent = formatTime(realTimeSeconds);
if (type === 'success') {
const progressBar = statusInfo.querySelector('.progress-bar');
progressBar.style.animation = 'none';
void progressBar.offsetWidth;
progressBar.style.animation = null;
}
countElement.classList.remove('count-update');
void countElement.offsetWidth;
countElement.classList.add('count-update');
countElement.textContent = count;
timeElement.textContent = time ? time.toLocaleString() : '暂无记录';
messageElement.textContent = message;
messageElement.style.color = (() => {
switch (type) {
case 'success':
return '#10B981';
case 'error':
return '#EF4444';
case 'info':
default:
return '#3B82F6';
}
})();
messageElement.style.background = (() => {
switch (type) {
case 'success':
return 'rgba(16,185,129,0.05)';
case 'error':
return 'rgba(239,68,68,0.05)';
case 'info':
default:
return 'rgba(59,130,246,0.05)';
}
})();
messageElement.classList.remove('message-show');
void messageElement.offsetWidth;
messageElement.classList.add('message-show');
});
return recorderSection;
}
async function fetchUnfinishedTasks() {
try {
const token = getCookie('prd-access-token');
const response = await fetch(`https://${domain}/api/jx-stat/group/task/un_finish`, {
headers: {
"authorization": `Bearer ${token}`,
"content-type": "application/json; charset=UTF-8"
}
});
const data = await response.json();
if (data.success) {
renderUnfinishedTasks(data.data);
}
} catch (error) {
console.error('获取未完成任务失败:', error);
showEmptyFallback();
}
}
function getTaskWeight(endTime) {
const now = new Date();
const endDate = new Date(endTime);
const daysLeft = (endDate - now) / (1000 * 60 * 60 * 24);
if (endDate < now) return -1;
if (daysLeft < 1) return 100;
if (daysLeft < 3) return 50;
return 10;
}
function renderUnfinishedTasks(tasks) {
const container = document.getElementById('unfinished-tasks');
if (!tasks?.length) {
showEmptyFallback();
return;
}
const sortedTasks = [...tasks].sort((a, b) => {
const weightA = getTaskWeight(a.end_time);
const weightB = getTaskWeight(b.end_time);
return weightB - weightA;
});
const groupedTasks = sortedTasks.reduce((acc, task) => {
if (!acc[task.group_name]) {
acc[task.group_name] = [];
}
acc[task.group_name].push(task);
return acc;
}, {});
container.innerHTML = Object.entries(groupedTasks).map(([courseName, tasks]) => `
${tasks.map(task => `
${getTaskTypeIcon(task.task_type)}
${task.name}
${getTaskUrgencyText(task.end_time)}
截止时间: ${new Date(task.end_time).toLocaleString('zh-CN')}
`).join('')}
`).join('');
container.querySelectorAll('.course-tasks').forEach(courseEl => {
const header = courseEl.querySelector('.course-header');
const taskList = courseEl.querySelector('.task-list');
const expandIcon = courseEl.querySelector('.expand-icon');
let isExpanded = true;
let isAnimating = false;
header.addEventListener('click', () => {
if (isAnimating) return;
isAnimating = true;
isExpanded = !isExpanded;
if (isExpanded) {
taskList.style.maxHeight = 'none';
const actualHeight = taskList.scrollHeight;
taskList.style.maxHeight = '0';
requestAnimationFrame(() => {
taskList.style.maxHeight = actualHeight + 'px';
});
} else {
taskList.style.maxHeight = taskList.scrollHeight + 'px';
requestAnimationFrame(() => {
taskList.style.maxHeight = '0';
});
}
expandIcon.style.transform = isExpanded ? 'rotate(0)' : 'rotate(-180deg)';
taskList.addEventListener('transitionend', () => {
isAnimating = false;
if (isExpanded) {
taskList.style.maxHeight = 'none';
}
}, { once: true });
});
header.addEventListener('mouseenter', () => {
expandIcon.style.background = '#eef2ff';
expandIcon.querySelector('svg').style.stroke = '#4f46e5';
});
header.addEventListener('mouseleave', () => {
expandIcon.style.background = '#f3f4f6';
expandIcon.querySelector('svg').style.stroke = 'currentColor';
});
});
}
function getTaskTypeColor(type) {
const colors = {
1: '#22c55e',
2: '#ef4444',
3: '#3b82f6',
4: '#a855f7',
5: '#f97316',
6: '#06b6d4'
};
return colors[type] || '#9ca3af';
}
function getTaskTypeIcon(type) {
const icons = {
1: '📺',
2: '✍️',
3: '📚',
4: '💯',
5: '📋',
6: '💭'
};
return icons[type] || '📌';
}
function getTaskUrgencyBg(endTime) {
const now = new Date();
const endDate = new Date(endTime);
if (endDate < now) return '#f3f4f6';
const daysLeft = (endDate - now) / (1000 * 60 * 60 * 24);
if (daysLeft < 1) return '#fee2e2';
if (daysLeft < 3) return '#fff7ed';
return '#ecfdf5';
}
function getTaskUrgencyColor(endTime) {
const now = new Date();
const endDate = new Date(endTime);
if (endDate < now) return '#6b7280';
const daysLeft = (endDate - now) / (1000 * 60 * 60 * 24);
if (daysLeft < 1) return '#ef4444';
if (daysLeft < 3) return '#f59e0b';
return '#10b981';
}
function getTaskUrgencyText(endTime) {
const now = new Date();
const endDate = new Date(endTime);
if (endDate < now) return '已过期';
const daysLeft = Math.ceil((endDate - now) / (1000 * 60 * 60 * 24));
if (daysLeft < 1) {
const hoursLeft = Math.ceil((endDate - now) / (1000 * 60 * 60));
if (hoursLeft <= 0) return '即将截止';
return `剩余 ${hoursLeft} 小时`;
}
return `剩余 ${daysLeft} 天`;
}
function showEmptyFallback() {
document.getElementById('unfinished-tasks').innerHTML = `
太棒了!任务全部完成
你已经完成了所有学习任务,好好放松一下吧! ✨
`;
}
function initTaskList() {
let container = document.getElementById('task-container');
let controller = document.getElementById('task-controller');
if (xiaoYaRecorder) {
xiaoYaRecorder.stop();
xiaoYaRecorder = null;
}
xiaoYaRecorder = new LearnRecorder();
if (!controller) {
controller = document.createElement('div');
controller.id = 'task-controller';
Object.assign(controller.style, {
position: 'fixed',
right: '20px',
top: '10%',
width: '48px',
height: '48px',
borderRadius: '50%',
background: 'linear-gradient(145deg, #3B82F6, #2563EB)',
boxShadow: '0 4px 12px rgba(37,99,235,0.2)',
cursor: 'pointer',
zIndex: '1001',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
transform: 'scale(0)',
});
controller.innerHTML = `
`;
document.body.appendChild(controller);
requestAnimationFrame(() => controller.style.transform = 'scale(1)');
}
if (!container) {
container = document.createElement('div');
container.id = 'task-container';
controller.title = '展开任务列表';
Object.assign(container.style, {
position: 'fixed',
top: '10%',
right: '20px',
backgroundColor: 'rgba(255, 255, 255, 0.95)',
borderRadius: '16px',
padding: '20px',
zIndex: '1000',
maxHeight: '80vh',
width: '600px',
overflowY: 'auto',
scrollbarGutter: 'stable',
boxShadow: '0 10px 30px rgba(0,0,0,0.1)',
transition: 'all 0.3s ease',
transform: 'translateX(450px)',
opacity: '0',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.1)',
pointerEvents: 'none'
});
container.innerHTML = `
`;
document.body.appendChild(container);
}
let isExpanded = false;
controller.onclick = () => {
isExpanded = !isExpanded;
controller.style.transform = isExpanded ? 'scale(0.9)' : 'scale(1)';
controller.querySelector('.controller-icon').style.transform = isExpanded ? 'rotate(-180deg)' : 'rotate(0)';
if (isExpanded) {
container.style.transform = 'translateX(0) scale(1)';
container.style.opacity = '1';
container.style.pointerEvents = 'auto';
container.style.animation = 'expandIn 0.5s cubic-bezier(0.4, 0, 0.2, 1)';
controller.style.right = '640px';
controller.title = '收起任务列表';
} else {
container.style.transform = 'translateX(450px) scale(0.9)';
container.style.opacity = '0';
container.style.pointerEvents = 'none';
container.style.animation = 'expandOut 0.5s cubic-bezier(0.4, 0, 0.2, 1)';
controller.style.right = '20px';
controller.title = '展开任务列表';
}
};
controller.onmouseenter = () => {
controller.style.transform = 'scale(1.1)';
controller.style.boxShadow = '0 6px 16px rgba(37,99,235,0.3)';
};
controller.onmouseleave = () => {
controller.style.transform = isExpanded ? 'scale(0.9)' : 'scale(1)';
controller.style.boxShadow = '0 4px 12px rgba(37,99,235,0.2)';
};
const GROUP_ID = getGroupIdFromUrl();
if (!GROUP_ID) {
console.log('不在课程页面,跳过初始化');
return;
}
const recorderComponent = createRecorderComponent();
container.appendChild(recorderComponent);
getAuthToken().then(authToken => {
Promise.all([
fetchTaskList(authToken),
fetchResourceList(authToken)
]).then(([tasks, resources]) => {
console.log('任务数量:', tasks.length);
if (Array.isArray(tasks) && tasks.length > 0) {
showTaskList(container, tasks, resources);
}
}).catch(error => {
showNotification('获取数据失败:' + error, {
type: 'error',
keywords: ['获取', '失败']
});
});
}).catch(error => {
showNotification('无法获取token,请确保已登录并且cookie中包含prd-access-token', {
type: 'error',
keywords: ['token', '登录', 'cookie']
});
});
window.learnRecorder = xiaoYaRecorder;
}
function onUrlChange() {
if (location.href.includes('mycourse')) {
const now = Date.now();
if (now - (window.lastUrlChange || 0) < 500) {
return;
}
window.lastUrlChange = now;
if (xiaoYaRecorder) {
xiaoYaRecorder.stop();
xiaoYaRecorder = null;
}
const oldContainer = document.getElementById('task-container');
const oldController = document.getElementById('task-controller');
if (oldContainer) oldContainer.remove();
if (oldController) oldController.remove();
initTaskList();
fetchUnfinishedTasks();
}
}
(function (history) {
var pushState = history.pushState;
history.pushState = function () {
var ret = pushState.apply(history, arguments);
onUrlChange();
return ret;
};
})(window.history);
(function (history) {
var replaceState = history.replaceState;
history.replaceState = function () {
var ret = replaceState.apply(history, arguments);
onUrlChange();
return ret;
};
})(window.history);
window.addEventListener('popstate', onUrlChange);
window.addEventListener('hashchange', onUrlChange);
onUrlChange();
})();