// ==UserScript== // @name 小雅做做做 // @version 1.3.3 // @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('#xy_app_content > div.ta-frame > div.ta_panel.ta_panel_group.ta_group > section > section > main > div > div.group-resource-body > div'); 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 = `
⚠️

${title}

${message}
`; modal.appendChild(content); document.body.appendChild(modal); requestAnimationFrame(() => { modal.style.opacity = '1'; }); const closeModal = () => { const modalContent = content.firstElementChild; modalContent.style.animation = 'modalHide 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards'; modal.style.opacity = '0'; setTimeout(() => { modal.remove(); }, 300); }; modal.addEventListener('click', (e) => { if (e.target === modal) { closeModal(); onCancel?.(); } }); content.querySelector('.modal-cancel').onclick = () => { closeModal(); onCancel?.(); }; content.querySelector('.modal-confirm').onclick = () => { closeModal(); onConfirm?.(); }; return modal; } function showTaskList(container, tasks, resources) { const fragment = document.createDocumentFragment(); const freedomModeToggle = document.createElement('div'); freedomModeToggle.innerHTML = `
⚠️ 自由模式
开启后可以自由选择任务,但也伴随着各种风险。
`; const toggleSwitch = freedomModeToggle.querySelector('input[type="checkbox"]'); toggleSwitch.addEventListener('change', function () { if (this.checked) { createModal( '开启自由模式 🎭', `
自由模式允许你快捷提交和补交部分作业。但请注意以下风险:
❌ 盲目使用可能会出现以下情况:
• 上传了空作业被老师点名批斗 😅
• 显示"已完成"但仍处于"正在答题"的量子叠加态 😢
• 被系统标记为异常行为,账号被小雅制裁 💀
• 作业造假导致平时分太低,期末在天台凌乱 😭
• 老师发现异常:"为什么2秒看完2小时的视频?" 🤔
• 提前完成后发现后续无法再次提交 😱
✅ 建议使用场景:
• 补交错过的自主观看任务
• 完成一些不计分也没人管的水课作业
⚠️ 注意:对于已截止的作业,需要老师开启"允许补交"才能提交
确认要开启自由模式吗?
`, () => { isFreedomMode = true; toggleSwitch.parentElement.querySelector('span').style.backgroundColor = '#92400E'; toggleSwitch.parentElement.querySelector('span > span').style.transform = 'translateX(24px)'; getAuthToken().then(authToken => { Promise.all([ fetchTaskList(authToken), fetchResourceList(authToken) ]).then(([tasks, resources]) => { showTaskList(container, tasks, resources); }); }); }, () => { this.checked = false; } ); } else { isFreedomMode = false; toggleSwitch.parentElement.querySelector('span').style.backgroundColor = '#D1D5DB'; toggleSwitch.parentElement.querySelector('span > span').style.transform = 'translateX(0)'; getAuthToken().then(authToken => { Promise.all([ fetchTaskList(authToken), fetchResourceList(authToken) ]).then(([tasks, resources]) => { showTaskList(container, tasks, resources); }); }); } }); fragment.appendChild(freedomModeToggle); const resourceMap = new Map(); resources.forEach(resource => { if (resource.is_task) { resourceMap.set(resource.task_id, resource); } }); let title = document.createElement('h3'); title.innerText = '选择要完成的任务'; Object.assign(title.style, { fontSize: '24px', fontWeight: '600', color: '#1a1a1a', marginBottom: '16px', textAlign: 'center', position: 'relative', padding: '0 0 12px' }); title.innerHTML += `
`; fragment.appendChild(title); const stats = document.createElement('div'); Object.assign(stats.style, { marginBottom: '24px', padding: '24px', borderRadius: '20px', background: 'linear-gradient(145deg, rgba(255,255,255,0.95), rgba(249,250,251,0.95))', boxShadow: '0 8px 32px rgba(0,0,0,0.06)', border: '1px solid rgba(255,255,255,0.2)', backdropFilter: 'blur(8px)', position: 'relative', overflow: 'hidden', cursor: 'pointer', transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)' }); const toggleIcon = document.createElement('div'); toggleIcon.innerHTML = `
`; stats.appendChild(toggleIcon); const statsContent = document.createElement('div'); statsContent.style.maxHeight = '130px'; statsContent.style.overflow = 'hidden'; statsContent.style.transition = 'max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1)'; const now = new Date(); const threeDaysLater = new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000); const autoSubmitTasks = tasks.filter(task => { const deadline = new Date(task.end_time); return task.task_type === 1 && task.finish !== 2 && deadline > now; }); const manualTasks = tasks.filter(task => { const deadline = new Date(task.end_time); return task.task_type !== 1 && task.finish !== 2 && deadline > now; }); const deadlineTasks = tasks.filter(task => { const deadline = new Date(task.end_time); return task.finish !== 2 && deadline > now && deadline <= threeDaysLater; }); const completedTasks = tasks.filter(task => task.finish === 2); const overdueTasks = tasks.filter(task => task.finish !== 2 && new Date(task.end_time) <= now ); const typeStats = { document: tasks.filter(t => t.task_type === 1).length, homework: tasks.filter(t => t.task_type === 2).length, exercise: tasks.filter(t => t.task_type === 3).length, quiz: tasks.filter(t => t.task_type === 4).length, survey: tasks.filter(t => t.task_type === 5).length, discussion: tasks.filter(t => t.task_type === 6).length }; const completionRate = (completedTasks.length / tasks.length * 100).toFixed(1); const completedPercent = (+Math.max(0, (completedTasks.length / tasks.length * 100)).toFixed(1)) || 0; const overduePercent = (+Math.max(0, (overdueTasks.length / tasks.length * 100)).toFixed(1)) || 0; const ongoingPercent = (+Math.max(0, (100 - completedPercent - overduePercent)).toFixed(1)) || 0; const content = document.createElement('div'); content.style.position = 'relative'; content.style.zIndex = '1'; const cardHTML = `
${autoSubmitTasks.length}
🚀 可自动提交
${manualTasks.length}
✍️ 需手动完成
${deadlineTasks.length}
即将截止
📊
总体进度
已完成 ${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]]) => `
${icon}
${count}
${name}
`).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 = `
⏱️
学习时长记录
每30秒自动记录一次
📊
请求次数
0
🕒
上次记录时间
暂无记录
📚
有效时长
0:00
⏱️
实际计时
0:00:00
`; 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]) => `

📑 ${courseName}

${tasks.length} 个待完成
${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(); })();