您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
🚀 一键管理小雅平台任务,智能跟踪作业进度!✨ 支持自主学习任务的便捷提交,直观展示任务状态,帮你更高效完成学习目标! 📈
当前为
// ==UserScript== // @name 小雅做做做 // @version 1.1 // @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://zygame1314.site // @namespace https://gf.qytechs.cn/users/1268039 // ==/UserScript== (function () { 'use strict'; const domain = window.location.hostname; function getGroupIdFromUrl() { const url = window.location.href; const match = url.match(/mycourse\/(\d+)/); return match ? match[1] : 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('获取资源列表失败'); } } }); }); } function showTaskList(container, tasks, resources) { const resourceMap = new Map(); resources.forEach(resource => { if (resource.is_task) { resourceMap.set(resource.task_id, resource); } }); const fragment = document.createDocumentFragment(); 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 += ` <div style=" position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 60px; height: 3px; background: linear-gradient(90deg, #3B82F6, #60A5FA); border-radius: 2px; "></div> `; 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 = ` <div class="stats-toggle-icon" style=" position: absolute; top: 24px; right: 24px; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; background: linear-gradient(145deg, rgba(59,130,246,0.12), rgba(37,99,235,0.12)); border: 1px solid rgba(59,130,246,0.15); border-radius: 8px; box-shadow: 0 2px 6px rgba(37,99,235,0.08); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); color: #3b82f6; backdrop-filter: blur(4px); "> <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" style="transform: rotate(0deg); transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);"> <path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/> </svg> </div> `; 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 = ` <div style=" display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin-bottom: 24px; "> <div class="stat-card" style=" display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20px; border-radius: 16px; background: linear-gradient(135deg, rgba(16,185,129,0.15), rgba(5,150,105,0.15)); border: 1px solid rgba(16,185,129,0.2); cursor: pointer; "> <div style=" font-size: 32px; font-weight: bold; color: #059669; margin-bottom: 8px; ">${autoSubmitTasks.length}</div> <div style=" font-size: 14px; color: #065f46; font-weight: bold; display: flex; align-items: center; gap: 6px; "> <span style="font-size: 16px">🚀</span> 可自动提交 </div> </div> <div class="stat-card" style=" display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20px; border-radius: 16px; background: linear-gradient(135deg, rgba(59,130,246,0.15), rgba(37,99,235,0.15)); border: 1px solid rgba(59,130,246,0.2); cursor: pointer; "> <div style=" font-size: 32px; font-weight: bold; color: #2563eb; margin-bottom: 8px; ">${manualTasks.length}</div> <div style=" font-size: 14px; color: #1e40af; font-weight: bold; display: flex; align-items: center; gap: 6px; "> <span style="font-size: 16px">✍️</span> 需手动完成 </div> </div> <div class="stat-card" style=" display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20px; border-radius: 16px; background: linear-gradient(135deg, rgba(239,68,68,0.15), rgba(220,38,38,0.15)); border: 1px solid rgba(239,68,68,0.2); cursor: pointer; "> <div style=" font-size: 32px; font-weight: bold; color: #dc2626; margin-bottom: 8px; ">${deadlineTasks.length}</div> <div style=" font-size: 14px; color: #991b1b; font-weight: bold; display: flex; align-items: center; gap: 6px; "> <span style="font-size: 16px">⏰</span> 即将截止 </div> </div> </div> <div style=" background: rgba(255,255,255,0.8); border-radius: 16px; padding: 20px; margin-bottom: 24px; border: 1px solid rgba(0,0,0,0.05); "> <div style=" display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; "> <div style=" display: flex; align-items: center; gap: 12px; "> <div style=" width: 40px; height: 40px; border-radius: 12px; background: linear-gradient(135deg, #3b82f6, #2563eb); display: flex; align-items: center; justify-content: center; color: white; font-size: 18px; ">📊</div> <div> <div style="font-size: 16px; font-weight: 600; color: #1f2937">总体进度</div> <div style="font-size: 13px; color: #6b7280">已完成 ${completedTasks.length} / ${tasks.length} 个任务</div> </div> </div> <div style=" background: ${parseFloat(completionRate) >= 80 ? '#dcfce7' : '#fee2e2'}; color: ${parseFloat(completionRate) >= 80 ? '#166534' : '#991b1b'}; padding: 6px 12px; border-radius: 20px; font-size: 14px; font-weight: 600; "> ${completionRate}% </div> </div> <div style=" height: 8px; background: #e5e7eb; border-radius: 4px; overflow: hidden; position: relative; display: flex; margin-bottom: 8px; "> <div style=" width: ${completedPercent}%; height: 100%; background: linear-gradient(90deg, #22c55e, #16a34a); position: relative; "> <div style=" position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient( 90deg, transparent 0%, rgba(255,255,255,0.3) 50%, transparent 100% ); animation: shimmer 2s infinite; "></div> </div> <div style=" width: ${ongoingPercent}%; height: 100%; background: linear-gradient(90deg, #fbbf24, #f59e0b); position: relative; "> <div style=" position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient( 90deg, transparent 0%, rgba(255,255,255,0.3) 50%, transparent 100% ); animation: shimmer 2s infinite; "></div> </div> <div style=" width: ${overduePercent}%; height: 100%; background: linear-gradient(90deg, #ef4444, #dc2626); position: relative; "> <div style=" position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient( 90deg, transparent 0%, rgba(255,255,255,0.3) 50%, transparent 100% ); animation: shimmer 2s infinite; "></div> </div> </div> <div style=" display: flex; justify-content: space-between; font-size: 12px; color: #6b7280; "> <div>已完成: ${completedPercent}%</div> <div>进行中: ${ongoingPercent}%</div> <div>已截止: ${overduePercent}%</div> </div> <div style=" display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; "> ${Object.entries({ "自主观看": [typeStats.document, "👁️"], "作业": [typeStats.homework, "✍️"], "课堂练习": [typeStats.exercise, "📚"], "测验": [typeStats.quiz, "💯"], "问卷": [typeStats.survey, "📋"], "讨论": [typeStats.discussion, "💭"] }).map(([name, [count, icon]]) => ` <div style=" background: rgba(255,255,255,0.8); border-radius: 12px; padding: 16px; text-align: center; border: 1px solid rgba(0,0,0,0.05); transition: all 0.3s ease; "> <div style="font-size: 20px; margin-bottom: 4px">${icon}</div> <div style="font-size: 20px; font-weight: 600; color: #1f2937; margin-bottom: 4px"> ${count} </div> <div style="font-size: 13px; color: #6b7280">${name}</div> </div> `).join('')} </div> `; 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 = ` <div style=" background: linear-gradient(145deg, rgba(220,252,231,0.8), rgba(187,247,208,0.8)); padding: 12px 16px; border-radius: 12px; margin-bottom: 20px; display: flex; align-items: center; gap: 8px; box-shadow: 0 2px 6px rgba(34,197,94,0.1); "> <span style="font-size: 18px">👁️</span> <span style=" color: #166534; font-size: 13px; font-weight: bold; flex: 1; ">自主观看可以自动提交,但不会记录时长</span> </div> `; fragment.appendChild(docnotice); } if (exerciseTasks.length > 0) { let excrcisenotice = document.createElement('div'); excrcisenotice.innerHTML = ` <div style=" background: linear-gradient(145deg, rgba(254,226,226,0.8), rgba(254,202,202,0.8)); padding: 12px 16px; border-radius: 12px; margin-bottom: 20px; display: flex; align-items: center; gap: 8px; box-shadow: 0 2px 6px rgba(239,68,68,0.1); "> <span style="font-size: 18px">✍️</span> <span style=" color: #991B1B; font-size: 13px; font-weight: bold; flex: 1; ">作业需要手动完成,无法自动提交答案</span> </div> `; fragment.appendChild(excrcisenotice); } if (paperTasks.length > 0) { let paperNotice = document.createElement('div'); paperNotice.innerHTML = ` <div style=" background: linear-gradient(145deg, rgba(219,234,254,0.8), rgba(191,219,254,0.8)); padding: 12px 16px; border-radius: 12px; margin-bottom: 20px; display: flex; align-items: center; gap: 8px; box-shadow: 0 2px 6px rgba(59,130,246,0.1); "> <span style="font-size: 18px">📚</span> <span style=" color: #1e40af; font-size: 13px; font-weight: bold; flex: 1; ">课堂练习由课堂动态发布,请进入相应页面作答</span> </div> `; fragment.appendChild(paperNotice); } if (quizTasks.length > 0) { let quizNotice = document.createElement('div'); quizNotice.innerHTML = ` <div style=" background: linear-gradient(145deg, rgba(243,232,255,0.8), rgba(233,213,255,0.8)); padding: 12px 16px; border-radius: 12px; margin-bottom: 20px; display: flex; align-items: center; gap: 8px; box-shadow: 0 2px 6px rgba(147,51,234,0.1); "> <span style="font-size: 18px">💯</span> <span style=" color: #6B21A8; font-size: 13px; font-weight: bold; flex: 1; ">测验需要亲自作答,脚本无法自动完成</span> </div> `; fragment.appendChild(quizNotice); } if (surveyTasks.length > 0) { let surveyNotice = document.createElement('div'); surveyNotice.innerHTML = ` <div style=" background: linear-gradient(145deg, rgba(253,230,138,0.8), rgba(252,211,77,0.8)); padding: 12px 16px; border-radius: 12px; margin-bottom: 20px; display: flex; align-items: center; gap: 8px; box-shadow: 0 2px 6px rgba(217,119,6,0.1); "> <span style="font-size: 18px">📋</span> <span style=" color: #92400E; font-size: 13px; font-weight: bold; flex: 1; ">问卷需要手动填写,无法自动提交</span> </div> `; fragment.appendChild(surveyNotice); } if (discussionTasks.length > 0) { let discussionNotice = document.createElement('div'); discussionNotice.innerHTML = ` <div style=" background: linear-gradient(145deg, rgba(236,254,255,0.8), rgba(199,246,249,0.8)); padding: 12px 16px; border-radius: 12px; margin-bottom: 20px; display: flex; align-items: center; gap: 8px; box-shadow: 0 2px 6px rgba(6,182,212,0.1); "> <span style="font-size: 18px">💭</span> <span style=" color: #155E75; font-size: 13px; font-weight: bold; flex: 1; ">讨论需要发表评论或回复他人才能完成</span> </div> `; fragment.appendChild(discussionNotice); } const separator = document.createElement('div'); separator.innerHTML = ` <div style=" margin: 32px 0; padding: 16px 20px; background: linear-gradient(145deg, rgba(249,250,251,0.97), rgba(243,244,246,0.97)); border-radius: 16px; border: 1px solid rgba(0,0,0,0.05); box-shadow: 0 4px 16px rgba(0,0,0,0.03); backdrop-filter: blur(8px); "> <div style=" display: flex; align-items: center; gap: 12px; "> <div style=" display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; background: linear-gradient(145deg, #FEF3C7, #FDE68A); border-radius: 10px; color: #92400E; font-size: 18px; ">💡</div> <div style="flex: 1;"> <div style=" font-weight: 600; color: #92400E; margin-bottom: 4px; font-size: 15px; ">在以下区域勾选作业</div> <div style=" color: #B45309; font-size: 13px; line-height: 1.5; ">只有自主观看作业可以自动提交,其它作业请手动完成</div> </div> </div> </div> `; 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 = ` <div style=" display: flex; align-items: center; gap: 12px; margin-bottom: 16px; padding: 12px 16px; background: linear-gradient(145deg, rgba(255,255,255,0.95), rgba(249,250,251,0.95)); border-radius: 16px; box-shadow: 0 4px 16px rgba(0,0,0,0.03); border: 1px solid rgba(0,0,0,0.05); backdrop-filter: blur(8px); transform-origin: center; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); "> <span class="expand-icon" style=" display: inline-flex; justify-content: center; align-items: center; width: 28px; height: 28px; background: ${defaultExpanded ? 'linear-gradient(145deg, #3B82F6, #2563EB)' : 'linear-gradient(145deg, #f3f4f6, #e5e7eb)'}; border-radius: 8px; color: ${defaultExpanded ? '#fff' : '#6B7280'}; font-size: 14px; font-weight: bold; transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); transform: rotate(${defaultExpanded ? '90' : '0'}deg); box-shadow: ${defaultExpanded ? '0 4px 12px rgba(59,130,246,0.2)' : '0 2px 6px rgba(0,0,0,0.05)'}; ">▶</span> <h4 style=" font-size: 16px; font-weight: 600; color: #374151; margin: 0; flex: 1; transition: all 0.3s ease; ">${text}</h4> <span style=" background: ${defaultExpanded ? 'linear-gradient(145deg, #EFF6FF, #DBEAFE)' : 'linear-gradient(145deg, #F3F4F6, #E5E7EB)'}; color: ${defaultExpanded ? '#3B82F6' : '#6B7280'}; padding: 6px 12px; border-radius: 20px; font-size: 13px; font-weight: 600; transition: all 0.3s ease; border: 1px solid ${defaultExpanded ? 'rgba(59,130,246,0.1)' : 'rgba(107,114,128,0.1)'}; ">${count}个任务</span> </div> `; 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 = ` <div style=" display: flex; align-items: center; gap: 8px; "> <h5 style=" font-size: 14px; font-weight: 500; color: #6B7280; margin: 0; ">${title}</h5> <span style=" background: #F3F4F6; color: #9CA3AF; padding: 1px 6px; border-radius: 10px; font-size: 12px; ">${count}</span> </div> `; 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)); } } 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 = ` <span style="margin-right: 8px">📤</span> 提交完成状态 `; 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 = ` <span style="margin-right: 8px">⏳</span> 正在提交... `; submitTasks(selectedTasks); setTimeout(() => { submitButton.style.opacity = '1'; submitButton.innerHTML = ` <span style="margin-right: 8px">📤</span> 提交完成状态 `; }, 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; checkbox.disabled = !enableCheckbox; Object.assign(checkbox.style, { appearance: 'none', '-webkit-appearance': 'none', width: '22px', height: '22px', border: '2px solid #e2e8f0', borderRadius: '6px', cursor: enableCheckbox ? 'pointer' : 'not-allowed', transition: 'all 0.2s ease', backgroundColor: enableCheckbox ? '#fff' : '#f0f0f0', position: 'relative', margin: '0' }); 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 = '#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 = ` <span style=" display: inline-flex; align-items: center; padding: 4px 8px; border-radius: 6px; font-size: 12px; font-weight: 600; color: ${statusStyle.color}; background: ${statusStyle.bg}; border: 1px solid ${statusStyle.border}; "> ${task.finish === 2 ? '✓ 已完成' : (isOverdue ? '⚠️ 已截止' : '⏳ 进行中')} </span> `; const taskTypeText = { 1: '自主观看', 2: '作业', 3: '课堂练习', 4: '测验', 5: '问卷', 6: '讨论' }[task.task_type]; taskInfo.innerHTML = ` <div style="margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center"> <div style="font-size: 16px; font-weight: 600; color: #1a1a1a; flex: 1"> ${resourceInfo ? resourceInfo.name : '未知作业名称'} </div> ${status} </div> <div style="display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 8px"> <span style="display: inline-flex; align-items: center; color: #666"> <span style="margin-right: 6px; opacity: 0.7">📝</span> ${taskTypeText} </span> <span style="display: inline-flex; align-items: center; color: #666"> <span style="margin-right: 6px; opacity: 0.7">⏰</span> ${new Date(task.start_time).toLocaleString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })} </span> <span style="display: inline-flex; align-items: center; color: #666"> <span style="margin-right: 6px; opacity: 0.7">🔚</span> ${new Date(task.end_time).toLocaleString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })} </span> </div> ${task.finish_time ? `<div style="font-size: 13px; color: #10B981; margin-top: 8px; display: flex; align-items: center"> <span style="margin-right: 6px">✅</span> 完成于: ${new Date(task.finish_time).toLocaleString('zh-CN')} </div>` : ''} ${task.task_type === 2 || task.task_type === 3 || task.task_type === 4 || task.task_type === 5 || task.task_type === 6 ? `<div style="color: #EF4444; font-size: 13px; margin-top: 8px; display: flex; align-items: center"> <span style="margin-right: 6px">⚠️</span> 需要手动完成 </div>` : ''} `; 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 += ` <div style=" display: flex; align-items: center; gap: 6px; background: rgba(239,68,68,0.1); color: #dc2626; font-size: 13px; font-weight: bold; margin-top: 8px; padding: 6px 10px; border-radius: 6px; border: 1px solid rgba(239,68,68,0.2); "> <span style="font-size: 16px">❗</span> <span>还剩 ${daysLeft} 天截止</span> </div> `; } } 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, `<span style="${highlightStyle}" onmouseover="this.style.backgroundColor='${highlightColor}15'; this.style.borderBottomColor='${highlightColor}'" onmouseout="this.style.backgroundColor='transparent'; this.style.borderBottomColor='${highlightColor}50'" >${keyword}</span>`); } 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); } } function initTaskList() { let container = document.getElementById('task-container'); let controller = document.getElementById('task-controller'); 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 = ` <div class="controller-icon" style=" width: 24px; height: 24px; position: relative; transition: transform 0.3s ease; "> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="white"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/> </svg> </div> `; 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 = ` <div class="empty-container" style=" display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 200px; padding: 20px; "> <div class="empty-state" style=" display: flex; flex-direction: column; align-items: center; transform-origin: center; "> <svg width="120" height="120" viewBox="0 0 24 24" style="margin-bottom: 20px;"> <path fill="#93c5fd" d="M12 3L1 9l4 2.18v6L12 21l7-3.82v-6l2-1.09V17h2V9L12 3zm6.82 6L12 12.72 5.18 9 12 5.28 18.82 9zM17 15.99l-5 2.73-5-2.73v-3.72L12 15l5-2.73v3.72z"/> </svg> <div style=" font-size: 18px; font-weight: 600; color: #3b82f6; text-align: center; margin-bottom: 8px; "> 当前页面暂无作业 </div> <div style=" font-size: 14px; font-weight: bold; color: #6b7280; text-align: center; "> 休息一下,马上回来 ☕ </div> </div> </div> <style> @keyframes float { 0% { transform: translateY(0px); } 50% { transform: translateY(-10px); } 100% { transform: translateY(0px); } } .empty-state { animation: float 3s ease-in-out infinite; } </style> `; 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; } 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); } else { showNotification('当前课程暂无作业', { type: 'info', keywords: ['课程', '作业'], duration: 3000 }); } }).catch(error => { showNotification('获取数据失败:' + error, { type: 'error', keywords: ['获取', '失败'] }); }); }).catch(error => { showNotification('无法获取token,请确保已登录(不可用)并且cookie中包含prd-access-token', { type: 'error', keywords: ['token', '登录(不可用)', 'cookie'] }); }); } function onUrlChange() { if (location.href.includes('mycourse')) { const now = Date.now(); if (now - (window.lastUrlChange || 0) < 500) { return; } window.lastUrlChange = now; const oldContainer = document.getElementById('task-container'); const oldController = document.getElementById('task-controller'); if (oldContainer) oldContainer.remove(); if (oldController) oldController.remove(); initTaskList(); } } (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(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址