🏠 

Display Homework and Exam Scores for Courses (Filtered & Sorted)

在页面上显示各课程的作业和考试分数,并按 activity_id 升序排列,仅显示特定 API 中的作业和考试


安装此脚本?
  1. // ==UserScript==
  2. // @name Display Homework and Exam Scores for Courses (Filtered & Sorted)
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.7
  5. // @description 在页面上显示各课程的作业和考试分数,并按 activity_id 升序排列,仅显示特定 API 中的作业和考试
  6. // @author Cold_Ink
  7. // @match *://courses.zju.edu.cn/course/*/*
  8. // @grant none
  9. // @license MIT
  10. // ==/UserScript==
  11. (function() {
  12. 'use strict';
  13. // 从 URL 中提取课程 ID
  14. const courseUrlMatch = window.location.pathname.match(/\/course\/(\d+)\//);
  15. if (!courseUrlMatch) {
  16. console.error('无法从 URL 中找到课程 ID。');
  17. return;
  18. }
  19. const courseId = courseUrlMatch[1];
  20. // 创建一个容器来显示分数
  21. const scoreContainer = document.createElement('div');
  22. scoreContainer.id = 'score-container';
  23. scoreContainer.style.position = 'fixed';
  24. scoreContainer.style.bottom = '10px';
  25. scoreContainer.style.right = '10px';
  26. scoreContainer.style.zIndex = '1000';
  27. scoreContainer.style.backgroundColor = 'rgba(255, 255, 255, 0.95)';
  28. scoreContainer.style.border = '1px solid #ccc';
  29. scoreContainer.style.borderRadius = '5px';
  30. scoreContainer.style.boxShadow = '0 0 5px rgba(0,0,0,0.1)';
  31. scoreContainer.style.maxHeight = '400px';
  32. scoreContainer.style.overflowY = 'auto';
  33. scoreContainer.style.padding = '10px';
  34. scoreContainer.style.width = '300px';
  35. scoreContainer.style.fontFamily = 'Arial, sans-serif';
  36. scoreContainer.style.fontSize = '12px';
  37. scoreContainer.style.lineHeight = '1.4';
  38. scoreContainer.style.color = '#333';
  39. scoreContainer.innerHTML = '<h4 style="margin: 0 0 10px 0; font-size: 14px; text-align: center;">作业与考试分数</h4>';
  40. document.body.appendChild(scoreContainer);
  41. // 定义 API URL
  42. const activityReadsUrl = `https://courses.zju.edu.cn/api/course/${courseId}/activity-reads-for-user`;
  43. const homeworkScoresUrl = `https://courses.zju.edu.cn/api/course/${courseId}/homework-scores?fields=id,title`;
  44. const examScoresUrl = `https://courses.zju.edu.cn/api/courses/${courseId}/exam-scores?no-intercept=true`;
  45. const examsUrl = `https://courses.zju.edu.cn/api/courses/${courseId}/exams`;
  46. // 同时获取四个 API 的数据
  47. Promise.all([
  48. fetch(activityReadsUrl).then(response => {
  49. if (!response.ok) {
  50. throw new Error(`无法获取 activity_reads 数据: ${response.statusText}`);
  51. }
  52. return response.json();
  53. }),
  54. fetch(homeworkScoresUrl).then(response => {
  55. if (!response.ok) {
  56. throw new Error(`无法获取 homework_scores 数据: ${response.statusText}`);
  57. }
  58. return response.json();
  59. }),
  60. fetch(examScoresUrl).then(response => {
  61. if (!response.ok) {
  62. throw new Error(`无法获取 exam_scores 数据: ${response.statusText}`);
  63. }
  64. return response.json();
  65. }),
  66. fetch(examsUrl).then(response => {
  67. if (!response.ok) {
  68. throw new Error(`无法获取 exams 数据: ${response.statusText}`);
  69. }
  70. return response.json();
  71. })
  72. ])
  73. .then(([activityReadsData, homeworkScoresData, examScoresData, examsData]) => {
  74. displayScores(activityReadsData, homeworkScoresData, examScoresData, examsData);
  75. })
  76. .catch(error => {
  77. console.error('获取数据时出错:', error);
  78. scoreContainer.innerHTML += `<p style="color: red; text-align: center; margin: 0;">获取分数数据时出错</p>`;
  79. });
  80. function displayScores(activityReadsData, homeworkScoresData, examScoresData, examsData) {
  81. const activityReads = activityReadsData.activity_reads;
  82. const homeworkActivities = homeworkScoresData.homework_activities;
  83. const examScores = examScoresData.exam_scores;
  84. const exams = examsData.exams;
  85. if ((!activityReads || activityReads.length === 0) && (!examScores || examScores.length === 0)) {
  86. scoreContainer.innerHTML += '<p style="text-align: center; margin: 0;"><strong>当前课程没有作业或考试数据。</strong></p>';
  87. return;
  88. }
  89. if ((!homeworkActivities || homeworkActivities.length === 0) && (!exams || exams.length === 0)) {
  90. scoreContainer.innerHTML += '<p style="text-align: center; margin: 0;"><strong>没有作业或考试活动标题数据。</strong></p>';
  91. return;
  92. }
  93. // 创建一个活动 ID 到作业标题的映射
  94. const homeworkMap = new Map();
  95. if (homeworkActivities && homeworkActivities.length > 0) {
  96. homeworkActivities.forEach(activity => {
  97. homeworkMap.set(activity.id, activity.title);
  98. });
  99. }
  100. // 创建一个活动 ID 到考试标题的映射
  101. const examsMap = new Map();
  102. if (exams && exams.length > 0) {
  103. exams.forEach(exam => {
  104. examsMap.set(exam.id, exam.title);
  105. });
  106. }
  107. // 创建一个活动 ID 到作业分数的映射(来自 activity_reads-for-user)
  108. const homeworkScoresMap = new Map();
  109. if (activityReads && activityReads.length > 0) {
  110. activityReads.forEach(activityRead => {
  111. const activityId = activityRead.activity_id;
  112. if (homeworkMap.has(activityId)) { // 仅处理在 homeworkMap 中存在的 activity_id
  113. const score = (activityRead.data && activityRead.data.score !== undefined && activityRead.data.score !== null) ? activityRead.data.score : '—';
  114. homeworkScoresMap.set(activityId, score);
  115. }
  116. });
  117. }
  118. // 创建一个活动 ID 到考试分数的映射(来自 exam-scores)
  119. const examScoresMap = new Map();
  120. if (examScores && examScores.length > 0) {
  121. examScores.forEach(examScore => {
  122. const activityId = examScore.activity_id;
  123. if (activityId !== 0) { // 过滤掉无效的 activity_id
  124. const score = (examScore.score !== undefined && examScore.score !== null) ? examScore.score : '—';
  125. examScoresMap.set(activityId, score);
  126. }
  127. });
  128. }
  129. // 合并作业和考试的 activity_id
  130. const combinedActivityIds = new Set([
  131. ...homeworkScoresMap.keys(),
  132. ...examScoresMap.keys()
  133. ]);
  134. if (combinedActivityIds.size === 0) {
  135. scoreContainer.innerHTML += '<p style="text-align: center; margin: 0;"><strong>没有匹配的作业或考试数据。</strong></p>';
  136. return;
  137. }
  138. // 按 activity_id 升序排序
  139. const sortedActivityIds = Array.from(combinedActivityIds).sort((a, b) => a - b);
  140. // 使用表格布局来紧凑显示
  141. const table = document.createElement('table');
  142. table.style.width = '100%';
  143. table.style.borderCollapse = 'collapse';
  144. sortedActivityIds.forEach(activityId => {
  145. let title = '';
  146. let score = '—';
  147. if (homeworkScoresMap.has(activityId)) {
  148. title = homeworkMap.get(activityId) || `作业 ID ${activityId}`;
  149. score = homeworkScoresMap.get(activityId);
  150. } else if (examScoresMap.has(activityId)) {
  151. title = examsMap.get(activityId) || `考试 ID ${activityId}`;
  152. score = examScoresMap.get(activityId);
  153. }
  154. const row = document.createElement('tr');
  155. const titleCell = document.createElement('td');
  156. titleCell.textContent = title;
  157. titleCell.style.padding = '4px 6px';
  158. titleCell.style.borderBottom = '1px solid #eee';
  159. titleCell.style.width = '70%';
  160. titleCell.style.wordBreak = 'break-word';
  161. titleCell.style.fontWeight = 'bold';
  162. titleCell.style.fontSize = '13px';
  163. const scoreCell = document.createElement('td');
  164. scoreCell.textContent = score;
  165. scoreCell.style.padding = '4px 6px';
  166. scoreCell.style.borderBottom = '1px solid #eee';
  167. scoreCell.style.textAlign = 'right';
  168. scoreCell.style.width = '30%';
  169. scoreCell.style.fontSize = '13px';
  170. scoreCell.style.color = score === '—' ? '#999' : '#000';
  171. row.appendChild(titleCell);
  172. row.appendChild(scoreCell);
  173. table.appendChild(row);
  174. });
  175. scoreContainer.appendChild(table);
  176. }
  177. })();