ucloud-Evolved

主页作业显示所属课程,使用Office 365预览课件,增加通知显示数量,通知按时间排序,去除悬浮窗,解除复制限制,课件自动下载,批量下载,资源页展示全部下载按钮,更好的页面标题

  1. // ==UserScript==
  2. // @name ucloud-Evolved
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.31
  5. // @description 主页作业显示所属课程,使用Office 365预览课件,增加通知显示数量,通知按时间排序,去除悬浮窗,解除复制限制,课件自动下载,批量下载,资源页展示全部下载按钮,更好的页面标题
  6. // @author Quarix
  7. // @match https://ucloud.bupt.edu.cn/*
  8. // @match https://ucloud.bupt.edu.cn/uclass/course.html*
  9. // @match https://ucloud.bupt.edu.cn/uclass/*
  10. // @match https://ucloud.bupt.edu.cn/office/*
  11. // @icon https://ucloud.bupt.edu.cn/favicon.ico
  12. // @require https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/nprogress/0.2.0/nprogress.min.js#sha256-XWzSUJ+FIQ38dqC06/48sNRwU1Qh3/afjmJ080SneA8=
  13. // @resource NPROGRESS_CSS https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/nprogress/0.2.0/nprogress.min.css#sha256-pMhcV6/TBDtqH9E9PWKgS+P32PVguLG8IipkPyqMtfY=
  14. // @connect github.com
  15. // @grant GM_getResourceText
  16. // @grant GM_addStyle
  17. // @grant GM_setValue
  18. // @grant GM_getValue
  19. // @grant GM_registerMenuCommand
  20. // @grant GM_xmlhttpRequest
  21. // @grant GM_openInTab
  22. // @run-at document-start
  23. // @license MIT
  24. // ==/UserScript==
  25.  
  26. (function () {
  27. if (location.href.startsWith("https://ucloud.bupt.edu.cn/office/")) {
  28. if (
  29. GM_getValue("preview_autoSwitchOffice", true) ||
  30. GM_getValue("preview_autoSwitchPdf", true) ||
  31. GM_getValue("preview_autoSwitchImg", true)
  32. ) {
  33. const url = new URLSearchParams(location.search).get("furl");
  34. const filename =
  35. new URLSearchParams(location.search).get("fullfilename") || url;
  36. const viewURL = new URL(url);
  37. if (new URLSearchParams(location.search).get("oauthKey")) {
  38. const viewURLsearch = new URLSearchParams(viewURL.search);
  39. viewURLsearch.set(
  40. "oauthKey",
  41. new URLSearchParams(location.search).get("oauthKey")
  42. );
  43. viewURL.search = viewURLsearch.toString();
  44. }
  45. if (
  46. filename.endsWith(".xls") ||
  47. filename.endsWith(".xlsx") ||
  48. filename.endsWith(".doc") ||
  49. filename.endsWith(".docx") ||
  50. filename.endsWith(".ppt") ||
  51. filename.endsWith(".pptx")
  52. ) {
  53. if (!GM_getValue("preview_autoSwitchOffice", true)) {
  54. return;
  55. }
  56. if (window.stop) window.stop();
  57. location.href =
  58. "https://view.officeapps.live.com/op/view.aspx?src=" +
  59. encodeURIComponent(viewURL.toString());
  60. return;
  61. } else if (filename.endsWith(".pdf")) {
  62. if (!GM_getValue("preview_autoSwitchPdf", true)) {
  63. return;
  64. }
  65. if (window.stop) window.stop();
  66. // 使用浏览器内置预览器,转blob避免出现下载动作
  67. fetch(viewURL.toString())
  68. .then((response) => response.blob())
  69. .then((blob) => {
  70. const blobUrl = URL.createObjectURL(blob);
  71. location.href = blobUrl;
  72. })
  73. .catch((err) => console.error("PDF加载失败:", err));
  74. return;
  75. } else if (
  76. filename.endsWith(".jpg") ||
  77. filename.endsWith(".png") ||
  78. filename.endsWith(".jpeg") ||
  79. filename.endsWith(".gif") ||
  80. filename.endsWith(".webp") ||
  81. filename.endsWith(".bmp") ||
  82. filename.endsWith(".tiff") ||
  83. filename.endsWith(".svg")
  84. ) {
  85. if (!GM_getValue("preview_autoSwitchImg", true)) {
  86. return;
  87. }
  88. if (window.stop) window.stop();
  89. function createModernImageViewer(imageUrl) {
  90. const style = document.createElement("style");
  91. style.textContent = `
  92. .modern-image-viewer {
  93. position: fixed;
  94. top: 0;
  95. left: 0;
  96. width: 100%;
  97. height: 100%;
  98. background-color: rgba(0, 0, 0, 0.9);
  99. z-index: 9999;
  100. display: flex;
  101. flex-direction: column;
  102. color: white;
  103. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  104. }
  105. .viewer-header {
  106. display: flex;
  107. justify-content: space-between;
  108. align-items: center;
  109. padding: 12px 20px;
  110. background-color: rgba(0, 0, 0, 0.7);
  111. z-index: 1;
  112. }
  113. .viewer-title {
  114. font-size: 16px;
  115. font-weight: 500;
  116. white-space: nowrap;
  117. overflow: hidden;
  118. text-overflow: ellipsis;
  119. max-width: 70%;
  120. }
  121. .viewer-controls {
  122. display: flex;
  123. gap: 15px;
  124. }
  125. .viewer-button {
  126. background: none;
  127. border: none;
  128. color: white;
  129. cursor: pointer;
  130. font-size: 16px;
  131. padding: 5px;
  132. border-radius: 4px;
  133. display: flex;
  134. align-items: center;
  135. justify-content: center;
  136. transition: background-color 0.2s;
  137. }
  138. .viewer-button:hover {
  139. background-color: rgba(255, 255, 255, 0.1);
  140. }
  141. .viewer-content {
  142. flex: 1;
  143. display: flex;
  144. align-items: center;
  145. justify-content: center;
  146. position: relative;
  147. overflow: hidden;
  148. }
  149. .viewer-image {
  150. max-width: 100%;
  151. max-height: 100%;
  152. object-fit: contain;
  153. transform-origin: center center;
  154. transition: transform 0.05s linear;
  155. cursor: grab;
  156. }
  157. .viewer-image.dragging {
  158. cursor: grabbing;
  159. transition: none;
  160. }
  161. .viewer-toolbar {
  162. display: flex;
  163. justify-content: center;
  164. align-items: center;
  165. padding: 12px;
  166. background-color: rgba(0, 0, 0, 0.7);
  167. gap: 20px;
  168. }
  169. .zoom-level {
  170. font-size: 14px;
  171. min-width: 60px;
  172. text-align: center;
  173. }
  174. .viewer-help {
  175. position: absolute;
  176. bottom: 80px;
  177. left: 50%;
  178. transform: translateX(-50%);
  179. background-color: rgba(0, 0, 0, 0.7);
  180. padding: 15px 20px;
  181. border-radius: 8px;
  182. max-width: 400px;
  183. font-size: 14px;
  184. display: none;
  185. z-index: 2;
  186. }
  187. .viewer-help h3 {
  188. margin-top: 0;
  189. margin-bottom: 10px;
  190. font-size: 16px;
  191. }
  192. .viewer-help ul {
  193. margin: 0;
  194. padding-left: 20px;
  195. }
  196. .viewer-help li {
  197. margin-bottom: 5px;
  198. }
  199. .keyboard-shortcut {
  200. display: inline-block;
  201. background-color: rgba(255, 255, 255, 0.1);
  202. padding: 2px 6px;
  203. border-radius: 3px;
  204. margin: 0 2px;
  205. }
  206. @media (max-width: 768px) {
  207. .viewer-controls {
  208. gap: 10px;
  209. }
  210. .viewer-button {
  211. font-size: 14px;
  212. }
  213. .viewer-toolbar {
  214. padding: 10px;
  215. gap: 15px;
  216. }
  217. }
  218. `;
  219. document.head.appendChild(style);
  220.  
  221. // 创建预览器DOM结构
  222. document.body.innerHTML = `
  223. <div class="modern-image-viewer">
  224. <div class="viewer-header">
  225. <div class="viewer-title">${getImageFileName(imageUrl)}</div>
  226. <div class="viewer-controls">
  227. <button class="viewer-button" id="help-btn" title="帮助">
  228. <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
  229. <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-4h2v2h-2zm2.07-7.75l-.9.92c-.5.51-.86.97-1.04 1.69-.08.32-.13.68-.13 1.14h2c0-.47.08-.91.22-1.31.2-.58.53-.97.98-1.42l.9-.92c.35-.36.58-.82.58-1.35 0-1.1-.9-2-2-2s-2 .9-2 2h2c0-.55.45-1 1-1s1 .45 1 1c0 .28-.12.53-.31.72z"/>
  230. </svg>
  231. </button>
  232. <button class="viewer-button" id="download-btn" title="下载">
  233. <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
  234. <path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
  235. </svg>
  236. </button>
  237. </div>
  238. </div>
  239. <div class="viewer-content">
  240. <img id="viewer-img" class="viewer-image" src="${imageUrl}" alt="预览图片" draggable="false">
  241. </div>
  242. <div class="viewer-toolbar">
  243. <button class="viewer-button" id="rotate-left" title="向左旋转">
  244. <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
  245. <path d="M7.11 8.53L5.7 7.11C4.8 8.27 4.24 9.61 4.07 11h2.02c.14-.87.49-1.72 1.02-2.47zM6.09 13H4.07c.17 1.39.72 2.73 1.62 3.89l1.41-1.42c-.52-.75-.87-1.59-1.01-2.47zm1.01 5.32c1.16.9 2.51 1.44 3.9 1.61V17.9c-.87-.15-1.71-.49-2.46-1.03L7.1 18.32zM13 4.07V1L8.45 5.55 13 10V6.09c2.84.48 5 2.94 5 5.91s-2.16 5.43-5 5.91v2.02c3.95-.49 7-3.85 7-7.93s-3.05-7.44-7-7.93z"/>
  246. </svg>
  247. </button>
  248. <button class="viewer-button" id="zoom-out" title="缩小">
  249. <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
  250. <path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14zM7 9h5v1H7z"/>
  251. </svg>
  252. </button>
  253. <span class="zoom-level" id="zoom-level">100%</span>
  254. <button class="viewer-button" id="zoom-in" title="放大">
  255. <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
  256. <path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
  257. <path d="M12 10h-2v2H9v-2H7V9h2V7h1v2h2z"/>
  258. </svg>
  259. </button>
  260. <button class="viewer-button" id="zoom-reset" title="重置">
  261. <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
  262. <path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>
  263. </svg>
  264. </button>
  265. <button class="viewer-button" id="rotate-right" title="向右旋转">
  266. <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
  267. <path d="M15.55 5.55L11 1v3.07C7.06 4.56 4 7.92 4 12s3.05 7.44 7 7.93v-2.02c-2.84-.48-5-2.94-5-5.91s2.16-5.43 5-5.91V10l4.55-4.45zM19.93 11c-.17-1.39-.72-2.73-1.62-3.89l-1.42 1.42c.54.75.88 1.6 1.02 2.47h2.02zM13 17.9v2.02c1.39-.17 2.74-.71 3.9-1.61l-1.44-1.44c-.75.54-1.59.89-2.46 1.03zm3.89-2.42l1.42 1.41c.9-1.16 1.45-2.5 1.62-3.89h-2.02c-.14.87-.48 1.72-1.02 2.48z"/>
  268. </svg>
  269. </button>
  270. </div>
  271. <div class="viewer-help" id="help-panel">
  272. <h3>键盘快捷键</h3>
  273. <ul>
  274. <li><span class="keyboard-shortcut">+</span> 或 <span class="keyboard-shortcut">-</span> 放大/缩小</li>
  275. <li><span class="keyboard-shortcut">0</span> 重置缩放</li>
  276. <li><span class="keyboard-shortcut">←</span> <span class="keyboard-shortcut">→</span> 左右旋转</li>
  277. <li><span class="keyboard-shortcut">R</span> 重置所有变换</li>
  278. <li><span class="keyboard-shortcut">D</span> 下载图片</li>
  279. <li><span class="keyboard-shortcut">Esc</span> 关闭预览器</li>
  280. </ul>
  281. </div>
  282. </div>
  283. `;
  284.  
  285. document.body.style.overflow = "hidden";
  286.  
  287. // 获取元素引用
  288. const viewer = document.querySelector(".modern-image-viewer");
  289. const image = document.getElementById("viewer-img");
  290. const helpBtn = document.getElementById("help-btn");
  291. const helpPanel = document.getElementById("help-panel");
  292. const downloadBtn = document.getElementById("download-btn");
  293. const rotateLeftBtn = document.getElementById("rotate-left");
  294. const rotateRightBtn = document.getElementById("rotate-right");
  295. const zoomInBtn = document.getElementById("zoom-in");
  296. const zoomOutBtn = document.getElementById("zoom-out");
  297. const zoomResetBtn = document.getElementById("zoom-reset");
  298. const zoomLevelDisplay = document.getElementById("zoom-level");
  299.  
  300. // 图片变换状态
  301. const state = {
  302. scale: 1,
  303. rotation: 0,
  304. translateX: 0,
  305. translateY: 0,
  306. dragging: false,
  307. lastX: 0,
  308. lastY: 0,
  309. loaded: false,
  310. };
  311.  
  312. // 图片加载完成事件
  313. image.onload = () => {
  314. state.loaded = true;
  315. image.style.opacity = 1;
  316. applyTransform();
  317. };
  318.  
  319. // 应用变换
  320. function applyTransform() {
  321. image.style.transform = `translate(${state.translateX}px, ${state.translateY}px) rotate(${state.rotation}deg) scale(${state.scale})`;
  322. zoomLevelDisplay.textContent = `${Math.round(state.scale * 100)}%`;
  323. }
  324.  
  325. // 缩放图片
  326. function zoom(delta) {
  327. if (!state.loaded) return;
  328.  
  329. const oldScale = state.scale;
  330. state.scale = Math.max(0.1, Math.min(10, state.scale + delta));
  331.  
  332. // Only adjust position if we have mouse coordinates
  333. if (state.lastClientX !== undefined) {
  334. const imageRect = image.getBoundingClientRect();
  335.  
  336. // Get the position relative to the image's natural center
  337. const naturalWidth = image.naturalWidth;
  338. const naturalHeight = image.naturalHeight;
  339.  
  340. // Calculate the point on the original image that was under the cursor
  341. const viewportX = state.lastClientX - imageRect.left;
  342. const viewportY = state.lastClientY - imageRect.top;
  343.  
  344. // Convert to coordinates relative to the image center in the current scale
  345. const imageX = viewportX - imageRect.width / 2;
  346. const imageY = viewportY - imageRect.height / 2;
  347.  
  348. // Calculate how this point's position changes with the new scale
  349. const scaleDiff = state.scale - oldScale;
  350.  
  351. // Adjust translation to keep the point under cursor
  352. state.translateX -= (imageX * scaleDiff) / oldScale;
  353. state.translateY -= (imageY * scaleDiff) / oldScale;
  354. }
  355.  
  356. applyTransform();
  357. }
  358.  
  359. // 旋转图片
  360. function rotate(degrees) {
  361. if (!state.loaded) return;
  362. state.rotation = (state.rotation + degrees) % 360;
  363. applyTransform();
  364. }
  365.  
  366. // 重置变换
  367. function resetTransform() {
  368. if (!state.loaded) return;
  369. state.scale = 1;
  370. state.rotation = 0;
  371. state.translateX = 0;
  372. state.translateY = 0;
  373. applyTransform();
  374. }
  375.  
  376. // 下载图片
  377. function downloadImage() {
  378. const a = document.createElement("a");
  379. a.href = imageUrl;
  380. a.download = getImageFileName(imageUrl);
  381. document.body.appendChild(a);
  382. a.click();
  383. document.body.removeChild(a);
  384. }
  385.  
  386. // 从URL中提取文件名
  387. function getImageFileName(url) {
  388. try {
  389. const urlObj = new URL(url);
  390. const pathParts = urlObj.pathname.split("/");
  391. const fileName = decodeURIComponent(
  392. pathParts[pathParts.length - 1]
  393. );
  394. return fileName || "图片预览";
  395. } catch (e) {
  396. return "图片预览";
  397. }
  398. }
  399.  
  400. image.addEventListener("mousedown", (e) => {
  401. if (!state.loaded) return;
  402. e.preventDefault();
  403. state.dragging = true;
  404. state.lastX = e.clientX;
  405. state.lastY = e.clientY;
  406. image.classList.add("dragging");
  407. });
  408.  
  409. document.addEventListener("mousemove", (e) => {
  410. state.lastClientX = e.clientX;
  411. state.lastClientY = e.clientY;
  412.  
  413. if (!state.dragging) return;
  414. e.preventDefault();
  415.  
  416. state.translateX += e.clientX - state.lastX;
  417. state.translateY += e.clientY - state.lastY;
  418. state.lastX = e.clientX;
  419. state.lastY = e.clientY;
  420.  
  421. applyTransform();
  422. });
  423.  
  424. document.addEventListener("mouseup", () => {
  425. state.dragging = false;
  426. image.classList.remove("dragging");
  427. });
  428.  
  429. let lastTouchDistance = 0;
  430. let touchRotationStart = 0;
  431.  
  432. image.addEventListener(
  433. "touchstart",
  434. (e) => {
  435. if (!state.loaded) return;
  436. if (e.touches.length === 1) {
  437. e.preventDefault();
  438. state.dragging = true;
  439. state.lastX = e.touches[0].clientX;
  440. state.lastY = e.touches[0].clientY;
  441. image.classList.add("dragging");
  442. } else if (e.touches.length === 2) {
  443. const dx = e.touches[0].clientX - e.touches[1].clientX;
  444. const dy = e.touches[0].clientY - e.touches[1].clientY;
  445. const touchDistance = Math.sqrt(dx * dx + dy * dy);
  446. const scaleFactor = touchDistance / lastTouchDistance;
  447.  
  448. if (
  449. !isNaN(scaleFactor) &&
  450. isFinite(scaleFactor) &&
  451. scaleFactor > 0
  452. ) {
  453. const oldScale = state.scale;
  454. state.scale = Math.max(
  455. 0.1,
  456. Math.min(10, state.scale * scaleFactor)
  457. );
  458.  
  459. const centerX =
  460. (e.touches[0].clientX + e.touches[1].clientX) / 2;
  461. const centerY =
  462. (e.touches[0].clientY + e.touches[1].clientY) / 2;
  463.  
  464. const imageRect = image.getBoundingClientRect();
  465.  
  466. const imageX =
  467. centerX - (imageRect.left + imageRect.width / 2);
  468. const imageY =
  469. centerY - (imageRect.top + imageRect.height / 2);
  470.  
  471. state.translateX += imageX * (1 - scaleFactor);
  472. state.translateY += imageY * (1 - scaleFactor);
  473.  
  474. lastTouchDistance = touchDistance;
  475. }
  476.  
  477. touchRotationStart =
  478. (Math.atan2(
  479. e.touches[1].clientY - e.touches[0].clientY,
  480. e.touches[1].clientX - e.touches[0].clientX
  481. ) *
  482. 180) /
  483. Math.PI;
  484. }
  485. },
  486. { passive: false }
  487. );
  488.  
  489. image.addEventListener(
  490. "touchmove",
  491. (e) => {
  492. if (!state.loaded) return;
  493. e.preventDefault();
  494.  
  495. if (e.touches.length === 1 && state.dragging) {
  496. state.translateX += e.touches[0].clientX - state.lastX;
  497. state.translateY += e.touches[0].clientY - state.lastY;
  498. state.lastX = e.touches[0].clientX;
  499. state.lastY = e.touches[0].clientY;
  500. applyTransform();
  501. } else if (e.touches.length === 2) {
  502. const dx = e.touches[0].clientX - e.touches[1].clientX;
  503. const dy = e.touches[0].clientY - e.touches[1].clientY;
  504. const touchDistance = Math.sqrt(dx * dx + dy * dy);
  505. const scaleFactor = touchDistance / lastTouchDistance;
  506.  
  507. if (
  508. !isNaN(scaleFactor) &&
  509. isFinite(scaleFactor) &&
  510. scaleFactor > 0
  511. ) {
  512. const oldScale = state.scale;
  513. state.scale = Math.max(
  514. 0.1,
  515. Math.min(10, state.scale * scaleFactor)
  516. );
  517.  
  518. const centerX =
  519. (e.touches[0].clientX + e.touches[1].clientX) / 2;
  520. const centerY =
  521. (e.touches[0].clientY + e.touches[1].clientY) / 2;
  522. const imageRect = image.getBoundingClientRect();
  523.  
  524. const x = centerX - imageRect.left;
  525. const y = centerY - imageRect.top;
  526.  
  527. // 这是关键修复 - 调整以保持缩放点位置不变
  528. state.translateX +=
  529. (1 - scaleFactor) * (x - state.translateX);
  530. state.translateY +=
  531. (1 - scaleFactor) * (y - state.translateY);
  532.  
  533. lastTouchDistance = touchDistance;
  534. }
  535.  
  536. const touchRotation =
  537. (Math.atan2(
  538. e.touches[1].clientY - e.touches[0].clientY,
  539. e.touches[1].clientX - e.touches[0].clientX
  540. ) *
  541. 180) /
  542. Math.PI;
  543.  
  544. const rotationDelta = touchRotation - touchRotationStart;
  545. if (!isNaN(rotationDelta) && isFinite(rotationDelta)) {
  546. state.rotation = (state.rotation + rotationDelta) % 360;
  547. touchRotationStart = touchRotation;
  548. }
  549.  
  550. applyTransform();
  551. }
  552. },
  553. { passive: false }
  554. );
  555.  
  556. image.addEventListener("touchend", () => {
  557. state.dragging = false;
  558. image.classList.remove("dragging");
  559. });
  560.  
  561. // 鼠标滚轮缩放
  562. viewer.addEventListener(
  563. "wheel",
  564. (e) => {
  565. if (!state.loaded) return;
  566. e.preventDefault();
  567. const delta = e.deltaY < 0 ? 0.1 : -0.1;
  568.  
  569. // 保存鼠标位置
  570. state.lastClientX = e.clientX;
  571. state.lastClientY = e.clientY;
  572.  
  573. zoom(delta);
  574. },
  575. { passive: false }
  576. );
  577.  
  578. // 按钮点击事件
  579. helpBtn.addEventListener("click", () => {
  580. helpPanel.style.display =
  581. helpPanel.style.display === "block" ? "none" : "block";
  582. });
  583. downloadBtn.addEventListener("click", downloadImage);
  584. rotateLeftBtn.addEventListener("click", () => rotate(-90));
  585. rotateRightBtn.addEventListener("click", () => rotate(90));
  586. zoomInBtn.addEventListener("click", () => zoom(0.1));
  587. zoomOutBtn.addEventListener("click", () => zoom(-0.1));
  588. zoomResetBtn.addEventListener("click", resetTransform);
  589.  
  590. // 键盘快捷键
  591. document.addEventListener("keydown", (e) => {
  592. if (!state.loaded) return;
  593.  
  594. switch (e.key) {
  595. case "Escape":
  596. closeViewer();
  597. break;
  598. case "+":
  599. case "=":
  600. zoom(0.1);
  601. break;
  602. case "-":
  603. zoom(-0.1);
  604. break;
  605. case "0":
  606. state.scale = 1;
  607. applyTransform();
  608. break;
  609. case "ArrowLeft":
  610. rotate(-90);
  611. break;
  612. case "ArrowRight":
  613. rotate(90);
  614. break;
  615. case "r":
  616. case "R":
  617. resetTransform();
  618. break;
  619. case "d":
  620. case "D":
  621. downloadImage();
  622. break;
  623. case "h":
  624. case "H":
  625. case "?":
  626. helpPanel.style.display =
  627. helpPanel.style.display === "block" ? "none" : "block";
  628. break;
  629. }
  630. });
  631.  
  632. // 点击背景关闭帮助面板
  633. viewer.addEventListener("click", (e) => {
  634. if (e.target === viewer && helpPanel.style.display === "block") {
  635. helpPanel.style.display = "none";
  636. }
  637. });
  638.  
  639. // 双击图片重置缩放
  640. image.addEventListener("dblclick", (e) => {
  641. if (!state.loaded) return;
  642. e.preventDefault();
  643.  
  644. if (state.scale === 1) {
  645. state.lastClientX = e.clientX;
  646. state.lastClientY = e.clientY;
  647. zoom(1);
  648. } else {
  649. resetTransform();
  650. }
  651. });
  652. }
  653. createModernImageViewer(viewURL.toString());
  654. return;
  655. }
  656. return;
  657. }
  658. }
  659. })();
  660. (function interceptXHR() {
  661. const originalOpen = XMLHttpRequest.prototype.open;
  662.  
  663. XMLHttpRequest.prototype.open = function (
  664. method,
  665. url,
  666. async,
  667. user,
  668. password
  669. ) {
  670. // hook XMR
  671. if (GM_getValue("notification_showMoreNotification", true)) {
  672. if (
  673. typeof url === "string" &&
  674. url.includes("/ykt-basics/api/inform/news/list")
  675. ) {
  676. url = url.replace(/size=\d+/, "size=1000");
  677. } else if (
  678. typeof url === "string" &&
  679. url.includes("/ykt-site/site/list/student/history")
  680. ) {
  681. url = url.replace(/size=\d+/, "size=15");
  682. }
  683. }
  684.  
  685. return originalOpen.call(this, method, url, async, user, password);
  686. };
  687. })();
  688. (function () {
  689. // 等待页面DOM加载完成
  690. document.addEventListener("DOMContentLoaded", initializeExtension);
  691.  
  692. // 用户设置
  693. const settings = {
  694. home: {
  695. addHomeworkSource: GM_getValue("home_addHomeworkSource", true),
  696. useBiggerButton: GM_getValue("home_useBiggerButton", true),
  697. makeClassClickable: GM_getValue("home_makeClassClickable", true),
  698. useWheelPageTurner: GM_getValue("home_useWheelPageTurner", true),
  699. },
  700. course: {
  701. addBatchDownload: GM_getValue("course_addBatchDownload", true),
  702. showAllDownloadButoon: GM_getValue("course_showAllDownloadButoon", false),
  703. },
  704. homework: {
  705. showHomeworkSource: GM_getValue("homework_showHomeworkSource", true),
  706. },
  707. notification: {
  708. showMoreNotification: GM_getValue(
  709. "notification_showMoreNotification",
  710. true
  711. ),
  712. sortNotificationsByTime: GM_getValue(
  713. "notification_sortNotificationsByTime",
  714. true
  715. ),
  716. betterNotificationHighlight: GM_getValue(
  717. "notification_betterNotificationHighlight",
  718. true
  719. ),
  720. },
  721. preview: {
  722. autoDownload: GM_getValue("preview_autoDownload", false),
  723. autoSwitchOffice: GM_getValue("preview_autoSwitchOffice", false),
  724. autoSwitchPdf: GM_getValue("preview_autoSwitchPdf", true),
  725. autoSwitchImg: GM_getValue("preview_autoSwitchImg", true),
  726. autoClosePopup: GM_getValue("preview_autoClosePopup", true),
  727. hideTimer: GM_getValue("preview_hideTimer", true),
  728. },
  729. system: {
  730. betterTitle: GM_getValue("system_betterTitle", true),
  731. unlockCopy: GM_getValue("system_unlockCopy", true),
  732. autoUpdate: GM_getValue("system_autoUpdate", false),
  733. showConfigButton: GM_getValue("system_showConfigButton", true),
  734. },
  735. };
  736.  
  737. // 辅助变量
  738. let jsp;
  739. let sumBytes = 0,
  740. loadedBytes = 0,
  741. downloading = false;
  742. let setClicked = false;
  743. let gpage = -1;
  744. let glist = null;
  745. let onlinePreview = null;
  746.  
  747. // 初始化扩展功能
  748. function initializeExtension() {
  749. // 注册菜单命令
  750. registerMenuCommands();
  751.  
  752. const nprogressCSS = GM_getResourceText("NPROGRESS_CSS");
  753. GM_addStyle(nprogressCSS);
  754.  
  755. loadui();
  756.  
  757. addFunctionalCSS();
  758. main();
  759.  
  760. if (settings.system.autoUpdate) {
  761. checkForUpdates();
  762. }
  763.  
  764. main();
  765.  
  766. // 监听URL哈希变化
  767. let hash = location.hash;
  768. setInterval(() => {
  769. if (location.hash != hash) {
  770. hash = location.hash;
  771. main();
  772. }
  773. }, 100);
  774. }
  775.  
  776. // 注册菜单命令
  777. function registerMenuCommands() {
  778. GM_registerMenuCommand(
  779. (settings.system.showConfigButton ? "✅ " : "❌ ") +
  780. "显示插件悬浮窗:" +
  781. (settings.system.showConfigButton ? "已启用" : "已禁用"),
  782. () => {
  783. settings.system.showConfigButton = !settings.system.showConfigButton;
  784. GM_setValue(
  785. "system_showConfigButton",
  786. settings.system.showConfigButton
  787. );
  788. location.reload();
  789. }
  790. );
  791. GM_registerMenuCommand("⚙️ 打开插件设置", () => {
  792. if (
  793. document
  794. .getElementById("yzHelper-settings")
  795. .classList.contains("visible")
  796. ) {
  797. document
  798. .getElementById("yzHelper-settings")
  799. .classList.remove("visible");
  800. setTimeout(() => {
  801. document.getElementById("yzHelper-settings").style.display = "none";
  802. }, 300);
  803. return;
  804. }
  805. document.getElementById("yzHelper-settings").style.display = "flex";
  806. void document.getElementById("yzHelper-settings").offsetWidth;
  807. document.getElementById("yzHelper-settings").classList.add("visible");
  808. });
  809. }
  810. /**
  811. * 通用标签页打开函数
  812. * @param {string} url - 要打开的URL
  813. * @param {Object} options - 选项参数
  814. * @param {boolean} [options.active=true] - 新标签页是否获得焦点
  815. * @param {boolean} [options.insert=true] - 是否在当前标签页旁边插入新标签页
  816. * @param {boolean} [options.setParent=true] - 新标签页是否将当前标签页设为父页面
  817. * @param {string} [options.windowName="_blank"] - window.open的窗口名称
  818. * @param {string} [options.windowFeatures=""] - window.open的窗口特性
  819. * @returns {Object|Window|null} 打开的标签页对象
  820. */
  821. function openTab(url, options = {}) {
  822. const defaultOptions = {
  823. active: true,
  824. insert: true,
  825. setParent: true,
  826. windowName: "_blank",
  827. windowFeatures: "",
  828. };
  829. const finalOptions = { ...defaultOptions, ...options };
  830. if (typeof GM_openInTab === "function") {
  831. try {
  832. return GM_openInTab(url, {
  833. active: finalOptions.active,
  834. insert: finalOptions.insert,
  835. setParent: finalOptions.setParent,
  836. });
  837. } catch (error) {
  838. return window.open(
  839. url,
  840. finalOptions.windowName,
  841. finalOptions.windowFeatures
  842. );
  843. }
  844. }
  845. }
  846. function showUpdateNotification(newVersion) {
  847. const notification = document.createElement("div");
  848. notification.style.cssText = `
  849. position: fixed;
  850. bottom: 80px;
  851. right: 20px;
  852. background: #4a6cf7;
  853. color: white;
  854. padding: 15px 20px;
  855. border-radius: 8px;
  856. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  857. z-index: 10000;
  858. font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
  859. max-width: 300px;
  860. `;
  861.  
  862. notification.innerHTML = `
  863. <div style="font-weight: bold; margin-bottom: 5px;">发现新版本 v${newVersion}</div>
  864. <div style="font-size: 14px; margin-bottom: 10px;">当前版本 v${GM_info.script.version}</div>
  865. <button id="updateNow" style="background: white; color: #4a6cf7; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; margin-right: 10px;">立即更新</button>
  866. <button id="updateLater" style="background: transparent; color: white; border: 1px solid white; padding: 5px 10px; border-radius: 4px; cursor: pointer;">稍后提醒</button>
  867. `;
  868.  
  869. document.body.appendChild(notification);
  870.  
  871. document.getElementById("updateNow").addEventListener("click", function () {
  872. openTab(GM_info.script.downloadURL, { active: true });
  873. document.body.removeChild(notification);
  874. });
  875.  
  876. document
  877. .getElementById("updateLater")
  878. .addEventListener("click", function () {
  879. document.body.removeChild(notification);
  880. });
  881. }
  882.  
  883. function checkForUpdates() {
  884. const lastCheckTime = GM_getValue("lastUpdateCheck", 0);
  885. const now = Date.now();
  886. const ONE_DAY = 24 * 60 * 60 * 1000; // 一天的毫秒数
  887.  
  888. if (now - lastCheckTime > ONE_DAY) {
  889. GM_setValue("lastUpdateCheck", now);
  890. GM_xmlhttpRequest({
  891. method: "GET",
  892. url: GM_info.script.updateURL,
  893. onload: function (response) {
  894. const versionMatch = response.responseText.match(
  895. /@version\s+(\d+\.\d+)/
  896. );
  897. if (versionMatch && versionMatch[1]) {
  898. const latestVersion = versionMatch[1];
  899. const currentVersion = GM_info.script.version;
  900. if (latestVersion > currentVersion) {
  901. showUpdateNotification(latestVersion);
  902. }
  903. }
  904. },
  905. });
  906. }
  907. }
  908.  
  909. function loadui() {
  910. GM_addStyle(`
  911. #yzHelper-settings {
  912. position: fixed;
  913. bottom: 20px;
  914. right: 20px;
  915. background: #ffffff;
  916. box-shadow: 0 5px 25px rgba(0, 0, 0, 0.15);
  917. border-radius: 12px;
  918. z-index: 9999;
  919. width: 500px;
  920. height: 450px;
  921. font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
  922. transition: all 0.3s ease;
  923. opacity: 0;
  924. transform: translateY(10px);
  925. color: #333;
  926. overflow: hidden;
  927. display: flex;
  928. flex-direction: column;
  929. display: none;
  930. }
  931. #yzHelper-settings.visible {
  932. opacity: 1;
  933. transform: translateY(0);
  934. }
  935. #yzHelper-header {
  936. padding: 15px 20px;
  937. border-bottom: 1px solid #eee;
  938. background-color: #ecb000;
  939. color: white;
  940. font-weight: bold;
  941. font-size: 16px;
  942. display: flex;
  943. justify-content: space-between;
  944. align-items: center;
  945. }
  946. #yzHelper-main {
  947. display: flex;
  948. flex: 1;
  949. overflow: hidden;
  950. }
  951. #yzHelper-settings-sidebar {
  952. width: 140px;
  953. background: #f7f7f7;
  954. padding: 15px 0;
  955. border-right: 1px solid #eee;
  956. overflow-y: auto;
  957. }
  958. #yzHelper-settings-sidebar .menu-item {
  959. padding: 12px 15px;
  960. cursor: pointer;
  961. transition: all 0.2s ease;
  962. font-size: 14px;
  963. color: #666;
  964. display: flex;
  965. align-items: center;
  966. gap: 8px;
  967. }
  968. #yzHelper-settings-sidebar .menu-item:hover {
  969. background: #efefef;
  970. color: #333;
  971. }
  972. #yzHelper-settings-sidebar .menu-item.active {
  973. background: #ffbe00;
  974. color: #fff;
  975. font-weight: 500;
  976. }
  977. #yzHelper-settings-sidebar .emoji {
  978. font-size: 16px;
  979. }
  980. #yzHelper-settings-content {
  981. flex: 1;
  982. padding: 20px;
  983. overflow-y: auto;
  984. position: relative;
  985. padding-bottom: 70px; /* Space for buttons */
  986. }
  987. #yzHelper-settings-content .settings-section {
  988. display: none;
  989. }
  990. #yzHelper-settings-content .settings-section.active {
  991. display: block;
  992. }
  993.  
  994. #section-about .about-content {
  995. line-height: 1.6;
  996. font-size: 14px;
  997. }
  998. #section-about h4 {
  999. margin: 16px 0 8px;
  1000. font-size: 15px;
  1001. }
  1002. #section-about ul {
  1003. margin: 8px 0;
  1004. padding-left: 20px;
  1005. }
  1006. #section-about li {
  1007. margin-bottom: 4px;
  1008. }
  1009. #section-about .github-link {
  1010. display: inline-flex;
  1011. align-items: center;
  1012. padding: 6px 12px;
  1013. background: #f6f8fa;
  1014. border: 1px solid rgba(27, 31, 36, 0.15);
  1015. border-radius: 6px;
  1016. color: #24292f;
  1017. text-decoration: none;
  1018. font-weight: 500;
  1019. transition: background-color 0.2s;
  1020. }
  1021. #section-about .github-link:hover {
  1022. background-color: #f3f4f6;
  1023. }
  1024. #section-about .github-icon {
  1025. margin-right: 6px;
  1026. fill: currentColor;
  1027. }
  1028. #section-about .feedback-note {
  1029. margin-top: 14px;
  1030. border-top: 1px solid #eaecef;
  1031. padding-top: 14px;
  1032. font-size: 13px;
  1033. color: #57606a;
  1034. }
  1035. #section-about code {
  1036. background: rgba(175, 184, 193, 0.2);
  1037. padding: 0.2em 0.4em;
  1038. border-radius: 6px;
  1039. font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
  1040. font-size: 85%;
  1041. }
  1042. #yzHelper-settings h3 {
  1043. margin-top: 0;
  1044. margin-bottom: 15px;
  1045. font-size: 18px;
  1046. font-weight: 600;
  1047. color: #2c3e50;
  1048. padding-bottom: 10px;
  1049. border-bottom: 1px solid #eee;
  1050. }
  1051. #yzHelper-settings .setting-item {
  1052. margin-bottom: 16px;
  1053. }
  1054. #yzHelper-settings .setting-toggle {
  1055. display: flex;
  1056. align-items: center;
  1057. }
  1058. #yzHelper-settings .setting-item:last-of-type {
  1059. margin-bottom: 20px;
  1060. }
  1061. #yzHelper-settings .switch {
  1062. position: relative;
  1063. display: inline-block;
  1064. width: 44px;
  1065. height: 24px;
  1066. margin-right: 10px;
  1067. }
  1068. #yzHelper-settings .switch input {
  1069. opacity: 0;
  1070. width: 0;
  1071. height: 0;
  1072. }
  1073. #yzHelper-settings .slider {
  1074. position: absolute;
  1075. cursor: pointer;
  1076. top: 0;
  1077. left: 0;
  1078. right: 0;
  1079. bottom: 0;
  1080. background-color: #ccc;
  1081. transition: .3s;
  1082. border-radius: 24px;
  1083. }
  1084. #yzHelper-settings .slider:before {
  1085. position: absolute;
  1086. content: "";
  1087. height: 18px;
  1088. width: 18px;
  1089. left: 3px;
  1090. bottom: 3px;
  1091. background-color: white;
  1092. transition: .3s;
  1093. border-radius: 50%;
  1094. }
  1095. #yzHelper-settings input:checked + .slider {
  1096. background-color: #ffbe00;
  1097. }
  1098. #yzHelper-settings input:focus + .slider {
  1099. box-shadow: 0 0 1px #ffbe00;
  1100. }
  1101. #yzHelper-settings input:checked + .slider:before {
  1102. transform: translateX(20px);
  1103. }
  1104. #yzHelper-settings .setting-label {
  1105. font-size: 14px;
  1106. cursor: pointer;
  1107. }
  1108. #yzHelper-settings .setting-description {
  1109. display: block; /* 始终保持在DOM中 */
  1110. margin-left: 54px;
  1111. font-size: 12px;
  1112. color: #666;
  1113. background: #f9f9f9;
  1114. border-left: 3px solid #ffbe00;
  1115. border-radius: 0 4px 4px 0;
  1116. max-height: 0;
  1117. overflow: hidden;
  1118. opacity: 0;
  1119. transition: all 0.3s ease;
  1120. padding: 0 12px;
  1121. }
  1122. #yzHelper-settings .setting-description.visible {
  1123. max-height: 100px;
  1124. opacity: 1;
  1125. margin-top: 8px;
  1126. padding: 8px 12px;
  1127. }
  1128. #yzHelper-settings .buttons {
  1129. display: flex;
  1130. justify-content: flex-end;
  1131. gap: 10px;
  1132. position: fixed;
  1133. bottom: 0px;
  1134. right: 25px;
  1135. background: white;
  1136. padding: 10px 0;
  1137. width: calc(100% - 180px);
  1138. border-top: 1px solid #f5f5f5;
  1139. box-sizing: border-box;
  1140. }
  1141. #yzHelper-settings button {
  1142. background: #ffbe00;
  1143. border: none;
  1144. padding: 8px 16px;
  1145. border-radius: 6px;
  1146. cursor: pointer;
  1147. font-weight: 500;
  1148. color: #fff;
  1149. transition: all 0.2s ease;
  1150. outline: none;
  1151. font-size: 14px;
  1152. }
  1153. #yzHelper-settings button:hover {
  1154. background: #e9ad00;
  1155. }
  1156. #yzHelper-settings button.cancel {
  1157. background: #f1f1f1;
  1158. color: #666;
  1159. }
  1160. #yzHelper-settings button.cancel:hover {
  1161. background: #e5e5e5;
  1162. }
  1163. #yzHelper-settings-toggle {
  1164. position: fixed;
  1165. bottom: 20px;
  1166. right: 20px;
  1167. background: #ffbe00;
  1168. color: #fff;
  1169. width: 50px;
  1170. height: 50px;
  1171. border-radius: 50%;
  1172. display: flex;
  1173. align-items: center;
  1174. justify-content: center;
  1175. font-size: 24px;
  1176. cursor: pointer;
  1177. z-index: 9998;
  1178. box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
  1179. transition: all 0.3s ease;
  1180. }
  1181. #yzHelper-settings-toggle:hover {
  1182. transform: rotate(30deg);
  1183. box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
  1184. }
  1185. #yzHelper-settings .setting-item.disabled .setting-toggle,
  1186. #yzHelper-settings .setting-item .setting-toggle:has(input:disabled) {
  1187. opacity: 0.7;
  1188. }
  1189.  
  1190. #yzHelper-settings input:disabled + .slider {
  1191. background-color: #ffbe00;
  1192. opacity: 0.5;
  1193. cursor: not-allowed;
  1194. }
  1195.  
  1196. #yzHelper-settings input:disabled + .slider:before {
  1197. background-color: #f0f0f0;
  1198. }
  1199.  
  1200. #yzHelper-settings .setting-item:has(input:disabled) .setting-label:after {
  1201. content: " 🔒";
  1202. font-size: 12px;
  1203. }
  1204.  
  1205. #yzHelper-settings .setting-item:has(input:disabled) .setting-description {
  1206. border-left-color: #ccc;
  1207. font-style: italic;
  1208. }
  1209. #yzHelper-version {
  1210. position: absolute;
  1211. bottom: 15px;
  1212. left: 20px;
  1213. font-size: 12px;
  1214. color: #999;
  1215. }
  1216. `);
  1217.  
  1218. // 设置面板
  1219. const settingsToggle = document.createElement("div");
  1220. settingsToggle.id = "yzHelper-settings-toggle";
  1221. settingsToggle.innerHTML = "⚙️";
  1222. settingsToggle.title = "云邮助手设置";
  1223. if (settings.system.showConfigButton) {
  1224. document.body.appendChild(settingsToggle);
  1225. }
  1226.  
  1227. const settingsPanel = document.createElement("div");
  1228. settingsPanel.id = "yzHelper-settings";
  1229.  
  1230. const header = `
  1231. <div id="yzHelper-header">
  1232. <span>云邮教学空间助手</span>
  1233. <span id="yzHelper-version">v${GM_info.script.version}</span>
  1234. </div>
  1235. `;
  1236.  
  1237. const mainContent = `
  1238. <div id="yzHelper-main">
  1239. <div id="yzHelper-settings-sidebar">
  1240. <div class="menu-item active" data-section="home">
  1241. <span class="emoji">👤</span>
  1242. <span>个人主页</span>
  1243. </div>
  1244. <div class="menu-item" data-section="preview">
  1245. <span class="emoji">🖼️</span>
  1246. <span>课件预览</span>
  1247. </div>
  1248. <div class="menu-item" data-section="course">
  1249. <span class="emoji">📚</span>
  1250. <span>课程详情</span>
  1251. </div>
  1252. <div class="menu-item" data-section="homework">
  1253. <span class="emoji">📝</span>
  1254. <span>作业详情</span>
  1255. </div>
  1256. <div class="menu-item" data-section="notification">
  1257. <span class="emoji">📢</span>
  1258. <span>消息通知</span>
  1259. </div>
  1260. <div class="menu-item" data-section="system">
  1261. <span class="emoji">⚙️</span>
  1262. <span>系统设置</span>
  1263. </div>
  1264. <div class="menu-item" data-section="about">
  1265. <span class="emoji">ℹ️</span>
  1266. <span>关于助手</span>
  1267. </div>
  1268. </div>
  1269.  
  1270. <div id="yzHelper-settings-content">
  1271. <!-- 个人主页设置 -->
  1272. <div class="settings-section active" id="section-home">
  1273. <h3>👤 个人主页设置</h3>
  1274. <div class="setting-item">
  1275. <div class="setting-toggle">
  1276. <label class="switch">
  1277. <input type="checkbox" id="home_useBiggerButton" ${
  1278. settings.home.useBiggerButton ? "checked" : ""
  1279. }>
  1280. <span class="slider"></span>
  1281. </label>
  1282. <span class="setting-label" data-for="description-home_useBiggerButton">加大翻页按钮尺寸</span>
  1283. </div>
  1284. <div class="setting-description" id="description-home_useBiggerButton">
  1285. 增大页面翻页按钮的尺寸和点击区域,提升操作便捷性。
  1286. </div>
  1287. </div>
  1288. <div class="setting-item">
  1289. <div class="setting-toggle">
  1290. <label class="switch">
  1291. <input type="checkbox" id="home_addHomeworkSource" ${
  1292. settings.home.addHomeworkSource ? "checked" : ""
  1293. }>
  1294. <span class="slider"></span>
  1295. </label>
  1296. <span class="setting-label" data-for="description-home_addHomeworkSource">显示作业来源</span>
  1297. </div>
  1298. <div class="setting-description" id="description-home_addHomeworkSource">
  1299. 为作业添加来源,直观显示发布作业的课程。
  1300. </div>
  1301. </div>
  1302. <div class="setting-item">
  1303. <div class="setting-toggle">
  1304. <label class="switch">
  1305. <input type="checkbox" id="home_makeClassClickable" ${
  1306. settings.home.makeClassClickable ? "checked" : ""
  1307. }>
  1308. <span class="slider"></span>
  1309. </label>
  1310. <span class="setting-label" data-for="description-home_makeClassClickable">便捷跳转</span>
  1311. </div>
  1312. <div class="setting-description" id="description-home_makeClassClickable">
  1313. 点击"我的课程"跳转课程页。
  1314. </div>
  1315. </div>
  1316. <div class="setting-item">
  1317. <div class="setting-toggle">
  1318. <label class="switch">
  1319. <input type="checkbox" id="home_useWheelPageTurner" ${
  1320. settings.home.useWheelPageTurner ? "checked" : ""
  1321. }>
  1322. <span class="slider"></span>
  1323. </label>
  1324. <span class="setting-label" data-for="description-home_useWheelPageTurner">使用鼠标滚轮翻页</span>
  1325. </div>
  1326. <div class="setting-description" id="description-home_useWheelPageTurner">
  1327. 可以使用鼠标滚轮来翻动个人主页的“本学期课程”和“待办”。
  1328. </div>
  1329. </div>
  1330. </div>
  1331.  
  1332. <!-- 课件预览设置 -->
  1333. <div class="settings-section" id="section-preview">
  1334. <h3>🖼️ 课件预览设置</h3>
  1335. <div class="setting-item">
  1336. <div class="setting-toggle">
  1337. <label class="switch">
  1338. <input type="checkbox" id="preview_autoDownload" ${
  1339. settings.preview.autoDownload ? "checked" : ""
  1340. }>
  1341. <span class="slider"></span>
  1342. </label>
  1343. <span class="setting-label" data-for="description-preview_autoDownload">预览课件时自动下载</span>
  1344. </div>
  1345. <div class="setting-description" id="description-preview_autoDownload">
  1346. 当打开课件预览时,自动触发下载操作,方便存储课件到本地。
  1347. </div>
  1348. </div>
  1349. <div class="setting-item">
  1350. <div class="setting-toggle">
  1351. <label class="switch">
  1352. <input type="checkbox" id="preview_autoSwitchOffice" ${
  1353. settings.preview.autoSwitchOffice ? "checked" : ""
  1354. }>
  1355. <span class="slider"></span>
  1356. </label>
  1357. <span class="setting-label" data-for="description-preview_autoSwitchOffice">使用 Office365 预览 Office 文件</span>
  1358. </div>
  1359. <div class="setting-description" id="description-preview_autoSwitchOffice">
  1360. 使用微软 Office365 在线服务预览 Office 文档,提供更好的浏览体验。
  1361. </div>
  1362. </div>
  1363. <div class="setting-item">
  1364. <div class="setting-toggle">
  1365. <label class="switch">
  1366. <input type="checkbox" id="preview_autoSwitchPdf" ${
  1367. settings.preview.autoSwitchPdf ? "checked" : ""
  1368. }>
  1369. <span class="slider"></span>
  1370. </label>
  1371. <span class="setting-label" data-for="description-preview_autoSwitchPdf">使用 浏览器原生阅读器 预览 PDF 文件</span>
  1372. </div>
  1373. <div class="setting-description" id="description-preview_autoSwitchPdf">
  1374. 使用系统(浏览器)原生的阅读器预览PDF文档,提供更好的浏览体验。移动端及部分平板可能不支持。
  1375. </div>
  1376. </div>
  1377. <div class="setting-item">
  1378. <div class="setting-toggle">
  1379. <label class="switch">
  1380. <input type="checkbox" id="preview_autoSwitchImg" ${
  1381. settings.preview.autoSwitchImg ? "checked" : ""
  1382. }>
  1383. <span class="slider"></span>
  1384. </label>
  1385. <span class="setting-label" data-for="description-preview_autoSwitchImg">使用 脚本内置的阅读器 预览 图片 文件</span>
  1386. </div>
  1387. <div class="setting-description" id="description-preview_autoSwitchImg">
  1388. 使用脚本内置的阅读器预览图片文件,提供更好的浏览体验。
  1389. </div>
  1390. </div>
  1391. <div class="setting-item">
  1392. <div class="setting-toggle">
  1393. <label class="switch">
  1394. <input type="checkbox" id="preview_autoClosePopup" ${
  1395. settings.preview.autoClosePopup ? "checked" : ""
  1396. }>
  1397. <span class="slider"></span>
  1398. </label>
  1399. <span class="setting-label" data-for="description-preview_autoClosePopup">自动关闭弹窗</span>
  1400. </div>
  1401. <div class="setting-description" id="description-preview_autoClosePopup">
  1402. 自动关闭预览时出现的"您已经在学习"及同类弹窗。
  1403. </div>
  1404. </div>
  1405. <div class="setting-item">
  1406. <div class="setting-toggle">
  1407. <label class="switch">
  1408. <input type="checkbox" id="preview_hideTimer" ${
  1409. settings.preview.hideTimer ? "checked" : ""
  1410. }>
  1411. <span class="slider"></span>
  1412. </label>
  1413. <span class="setting-label" data-for="description-preview_hideTimer">隐藏预览界面倒计时</span>
  1414. </div>
  1415. <div class="setting-description" id="description-preview_hideTimer">
  1416. 隐藏预览界面中的倒计时提示,获得无干扰的阅读体验。
  1417. </div>
  1418. </div>
  1419. </div>
  1420. <!-- 课程详情设置 -->
  1421. <div class="settings-section" id="section-course">
  1422. <h3>📚 课程详情设置</h3>
  1423. <div class="setting-item">
  1424. <div class="setting-toggle">
  1425. <label class="switch">
  1426. <input type="checkbox" id="course_addBatchDownload" ${
  1427. settings.course.addBatchDownload ? "checked" : ""
  1428. }>
  1429. <span class="slider"></span>
  1430. </label>
  1431. <span class="setting-label" data-for="description-course_addBatchDownload">增加批量下载按钮</span>
  1432. </div>
  1433. <div class="setting-description" id="description-course_addBatchDownload">
  1434. 增加批量下载按钮,方便一键下载课程中的所有课件。
  1435. </div>
  1436. </div>
  1437. <div class="setting-item">
  1438. <div class="setting-toggle">
  1439. <label class="switch">
  1440. <input type="checkbox" id="course_showAllDownloadButoon" ${
  1441. settings.course.showAllDownloadButoon
  1442. ? "checked"
  1443. : ""
  1444. }>
  1445. <span class="slider"></span>
  1446. </label>
  1447. <span class="setting-label" data-for="description-course_showAllDownloadButoon">显示所有下载按钮</span>
  1448. </div>
  1449. <div class="setting-description" id="description-course_showAllDownloadButoon">
  1450. 使每个课件文件都有下载按钮,不允许下载的课件在启用后也可以下载。
  1451. </div>
  1452. </div>
  1453. </div>
  1454.  
  1455. <!-- 作业详情设置 -->
  1456. <div class="settings-section" id="section-homework">
  1457. <h3>📝 作业详情设置</h3>
  1458. <div class="setting-item">
  1459. <div class="setting-toggle">
  1460. <label class="switch">
  1461. <input type="checkbox" id="homework_showHomeworkSource" ${
  1462. settings.homework.showHomeworkSource
  1463. ? "checked"
  1464. : ""
  1465. }>
  1466. <span class="slider"></span>
  1467. </label>
  1468. <span class="setting-label" data-for="description-homework_showHomeworkSource">显示作业所属课程</span>
  1469. </div>
  1470. <div class="setting-description" id="description-homework_showHomeworkSource">
  1471. 在作业详情页显示作业所属的课程名称,便于区分不同课程的作业。
  1472. </div>
  1473. </div>
  1474. </div>
  1475.  
  1476. <!-- 消息通知设置 -->
  1477. <div class="settings-section" id="section-notification">
  1478. <h3>📢 消息通知设置</h3>
  1479. <div class="setting-item">
  1480. <div class="setting-toggle">
  1481. <label class="switch">
  1482. <input type="checkbox" id="notification_showMoreNotification" ${
  1483. settings.notification.showMoreNotification
  1484. ? "checked"
  1485. : ""
  1486. }>
  1487. <span class="slider"></span>
  1488. </label>
  1489. <span class="setting-label" data-for="description-notification_showMoreNotification">显示更多的通知</span>
  1490. </div>
  1491. <div class="setting-description" id="description-notification_showMoreNotification">
  1492. 在通知列表中显示更多的历史通知,不再受限于默认显示数量。
  1493. </div>
  1494. </div>
  1495. <div class="setting-item">
  1496. <div class="setting-toggle">
  1497. <label class="switch">
  1498. <input type="checkbox" id="notification_sortNotificationsByTime" ${
  1499. settings.notification.sortNotificationsByTime
  1500. ? "checked"
  1501. : ""
  1502. }>
  1503. <span class="slider"></span>
  1504. </label>
  1505. <span class="setting-label" data-for="description-notification_sortNotificationsByTime">通知按照时间排序</span>
  1506. </div>
  1507. <div class="setting-description" id="description-notification_sortNotificationsByTime">
  1508. 将通知按照时间先后顺序排列,更容易找到最新或最早的通知。
  1509. </div>
  1510. </div>
  1511. <div class="setting-item">
  1512. <div class="setting-toggle">
  1513. <label class="switch">
  1514. <input type="checkbox" id="notification_betterNotificationHighlight" ${
  1515. settings.notification.betterNotificationHighlight
  1516. ? "checked"
  1517. : ""
  1518. }>
  1519. <span class="slider"></span>
  1520. </label>
  1521. <span class="setting-label" data-for="description-notification_betterNotificationHighlight">优化未读通知高亮</span>
  1522. </div>
  1523. <div class="setting-description" id="description-notification_betterNotificationHighlight">
  1524. 增强未读通知的视觉提示,使未读消息更加醒目,不易遗漏重要信息。
  1525. </div>
  1526. </div>
  1527. </div>
  1528. <!-- 系统设置 -->
  1529. <div class="settings-section" id="section-system">
  1530. <h3>⚙️ 系统设置</h3>
  1531. <div class="setting-item">
  1532. <div class="setting-toggle">
  1533. <label class="switch">
  1534. <input type="checkbox" id="system_fixTicketBug" checked disabled>
  1535. <span class="slider"></span>
  1536. </label>
  1537. <span class="setting-label" data-for="description-system_fixTicketBug">修复ticket跳转问题</span>
  1538. </div>
  1539. <div class="setting-description" id="description-system_fixTicketBug">
  1540. 修复登录过期后,重新登录出现无法获取ticket提示的问题。
  1541. </div>
  1542. </div>
  1543. <div class="setting-item">
  1544. <div class="setting-toggle">
  1545. <label class="switch">
  1546. <input type="checkbox" id="system_betterTitle" ${
  1547. settings.system.betterTitle ? "checked" : ""
  1548. }>
  1549. <span class="slider"></span>
  1550. </label>
  1551. <span class="setting-label" data-for="description-system_betterTitle">优化页面标题</span>
  1552. </div>
  1553. <div class="setting-description" id="description-system_betterTitle">
  1554. 优化浏览器标签页的标题显示,更直观地反映当前页面内容。
  1555. </div>
  1556. </div>
  1557. <div class="setting-item">
  1558. <div class="setting-toggle">
  1559. <label class="switch">
  1560. <input type="checkbox" id="system_unlockCopy" ${
  1561. settings.system.unlockCopy ? "checked" : ""
  1562. }>
  1563. <span class="slider"></span>
  1564. </label>
  1565. <span class="setting-label" data-for="description-system_unlockCopy">解除复制限制</span>
  1566. </div>
  1567. <div class="setting-description" id="description-system_unlockCopy">
  1568. 解除全局的复制限制,方便摘录内容进行学习笔记。
  1569. </div>
  1570. </div>
  1571. <div class="setting-item">
  1572. <div class="setting-toggle">
  1573. <label class="switch">
  1574. <input type="checkbox" id="system_autoUpdate" ${
  1575. settings.system.autoUpdate ? "checked" : ""
  1576. }>
  1577. <span class="slider"></span>
  1578. </label>
  1579. <span class="setting-label" data-for="description-system_autoUpdate">内置更新检查</span>
  1580. </div>
  1581. <div class="setting-description" id="description-system_autoUpdate">
  1582. 定期检查脚本更新,确保您始终使用最新版本的功能和修复。
  1583. </div>
  1584. </div>
  1585. <div class="setting-item">
  1586. <div class="setting-toggle">
  1587. <label class="switch">
  1588. <input type="checkbox" id="system_showConfigButton" ${
  1589. settings.system.showConfigButton ? "checked" : ""
  1590. }>
  1591. <span class="slider"></span>
  1592. </label>
  1593. <span class="setting-label" data-for="description-system_showConfigButton">显示插件悬浮窗</span>
  1594. </div>
  1595. <div class="setting-description" id="description-system_showConfigButton">
  1596. 在网页界面显示助手配置按钮,方便随时调整设置。
  1597. </div>
  1598. </div>
  1599. </div>
  1600. <!-- 关于助手 -->
  1601. <div class="settings-section" id="section-about">
  1602. <h3>ℹ️ 关于云邮教学空间助手</h3>
  1603. <div class="about-content">
  1604. <p>云邮教学空间助手是一款专为云邮教学空间平台设计的浏览器增强脚本。</p>
  1605. <h4>🚀 主要功能</h4>
  1606. <ul>
  1607. <li>📍 个人主页优化 - 智能布局,提升交互体验</li>
  1608. <li>📄 课件预览增强 - 流畅浏览,轻松获取学习资源</li>
  1609. <li>📥 课程管理优化 - 批量下载,多样化下载选项</li>
  1610. <li>📋 作业管理助手 - 精准显示课程归属,提高管理效率</li>
  1611. <li>🔔 通知管理优化 - 智能整理,突出重点通知</li>
  1612. <li>🛠️ 系统功能增强 - 页面标题优化,解除复制限制等实用功能</li>
  1613. </ul>
  1614. <h4>🔗 相关链接</h4>
  1615. <p>
  1616. <a href="https://github.com/uarix/ucloud-Evolved/" target="_blank" class="github-link">
  1617. <svg class="github-icon" height="16" width="16" viewBox="0 0 16 16" aria-hidden="true">
  1618. <path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path>
  1619. </svg>
  1620. <span>GitHub 项目主页</span>
  1621. </a>
  1622. </p>
  1623. <p class="feedback-note">
  1624. 如有问题或建议,请通过
  1625. <a href="https://github.com/uarix/ucloud-Evolved/issues" target="_blank">GitHub Issues</a>
  1626. 提交反馈。
  1627. </p>
  1628. </div>
  1629. </div>
  1630. <div class="buttons">
  1631. <button id="cancelSettings" class="cancel">取消</button>
  1632. <button id="saveSettings">保存设置</button>
  1633. </div>
  1634. </div>
  1635. </div>
  1636. `;
  1637.  
  1638. settingsPanel.innerHTML = header + mainContent;
  1639. document.body.appendChild(settingsPanel);
  1640.  
  1641. // 菜单切换功能
  1642. document
  1643. .querySelectorAll("#yzHelper-settings-sidebar .menu-item")
  1644. .forEach((item) => {
  1645. item.addEventListener("click", function () {
  1646. // 移除所有菜单项的活动状态
  1647. document
  1648. .querySelectorAll("#yzHelper-settings-sidebar .menu-item")
  1649. .forEach((i) => {
  1650. i.classList.remove("active");
  1651. });
  1652. document
  1653. .querySelectorAll("#yzHelper-settings-content .settings-section")
  1654. .forEach((section) => {
  1655. section.classList.remove("active");
  1656. });
  1657.  
  1658. this.classList.add("active");
  1659. const sectionId = "section-" + this.getAttribute("data-section");
  1660. document.getElementById(sectionId).classList.add("active");
  1661.  
  1662. // 隐藏所有设置描述
  1663. document.querySelectorAll(".setting-description").forEach((desc) => {
  1664. desc.classList.remove("visible");
  1665. });
  1666. });
  1667. });
  1668.  
  1669. // 设置描述显示/隐藏功能
  1670. document.querySelectorAll(".setting-label").forEach((label) => {
  1671. label.addEventListener("click", function () {
  1672. const descriptionId = this.getAttribute("data-for");
  1673. const description = document.getElementById(descriptionId);
  1674.  
  1675. // 隐藏所有其他描述
  1676. document.querySelectorAll(".setting-description").forEach((desc) => {
  1677. if (desc.id !== descriptionId) {
  1678. desc.classList.remove("visible");
  1679. }
  1680. });
  1681.  
  1682. // 切换当前描述的可见性
  1683. description.classList.toggle("visible");
  1684. });
  1685. });
  1686.  
  1687. function settingsTrigger() {
  1688. const isVisible = settingsPanel.classList.contains("visible");
  1689. if (isVisible) {
  1690. settingsPanel.classList.remove("visible");
  1691. setTimeout(() => {
  1692. settingsPanel.style.display = "none";
  1693. }, 300);
  1694. } else {
  1695. settingsPanel.style.display = "flex";
  1696. void settingsPanel.offsetWidth;
  1697. settingsPanel.classList.add("visible");
  1698. }
  1699. }
  1700.  
  1701. settingsToggle.addEventListener("click", settingsTrigger);
  1702.  
  1703. document.getElementById("cancelSettings").addEventListener("click", () => {
  1704. settingsPanel.classList.remove("visible");
  1705. setTimeout(() => {
  1706. settingsPanel.style.display = "none";
  1707. }, 300);
  1708. });
  1709.  
  1710. document.getElementById("saveSettings").addEventListener("click", () => {
  1711. Array.from(
  1712. document
  1713. .querySelector("#yzHelper-settings-content")
  1714. .querySelectorAll('input[type="checkbox"]:not(:disabled)')
  1715. ).forEach((checkbox) => {
  1716. const checkboxId = checkbox.id;
  1717. if (checkboxId.includes("_")) {
  1718. const [category, settingName] = checkboxId.split("_");
  1719. if (settings[category] && settingName) {
  1720. settings[category][settingName] = checkbox.checked;
  1721. GM_setValue(`${category}_${settingName}`, checkbox.checked);
  1722. }
  1723. } else {
  1724. settings[checkboxId] = checkbox.checked;
  1725. GM_setValue(checkboxId, checkbox.checked);
  1726. }
  1727. });
  1728. settingsPanel.classList.remove("visible");
  1729. setTimeout(() => {
  1730. settingsPanel.style.display = "none";
  1731. showNotification("设置已保存", "刷新页面后生效");
  1732. }, 300);
  1733. });
  1734.  
  1735. const ul_observer = new MutationObserver((mutations) => {
  1736. const dropdownMenus = document.querySelectorAll(
  1737. 'ul.el-dropdown-menu.el-popper.dropdown-info[id*="dropdown-menu"]'
  1738. );
  1739.  
  1740. if (dropdownMenus.length > 0) {
  1741. dropdownMenus.forEach((menu) => {
  1742. if (!menu.querySelector(".plugin-settings-item")) {
  1743. const settingsItem = document.createElement("li");
  1744. settingsItem.setAttribute("tabindex", "-1");
  1745. settingsItem.classList.add(
  1746. "el-dropdown-menu__item",
  1747. "plugin-settings-item"
  1748. );
  1749. settingsItem.innerHTML = "<!---->插件设置";
  1750. settingsItem.addEventListener("click", settingsTrigger);
  1751. menu.appendChild(settingsItem);
  1752. }
  1753. });
  1754. ul_observer.disconnect();
  1755. }
  1756. });
  1757. ul_observer.observe(document.body, {
  1758. childList: true,
  1759. subtree: true,
  1760. });
  1761. }
  1762.  
  1763. // 通知函数
  1764. function showNotification(title, message) {
  1765. const notification = document.createElement("div");
  1766. notification.style.cssText = `
  1767. position: fixed;
  1768. bottom: 80px;
  1769. right: 20px;
  1770. background: #4CAF50;
  1771. color: white;
  1772. padding: 15px 20px;
  1773. border-radius: 8px;
  1774. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  1775. z-index: 10000;
  1776. font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
  1777. max-width: 300px;
  1778. opacity: 0;
  1779. transform: translateY(-10px);
  1780. transition: all 0.3s ease;
  1781. `;
  1782.  
  1783. notification.innerHTML = `
  1784. <div style="font-weight: bold; margin-bottom: 5px;">${title}</div>
  1785. <div style="font-size: 14px;">${message}</div>
  1786. `;
  1787.  
  1788. document.body.appendChild(notification);
  1789.  
  1790. void notification.offsetWidth;
  1791.  
  1792. notification.style.opacity = "1";
  1793. notification.style.transform = "translateY(0)";
  1794.  
  1795. setTimeout(() => {
  1796. notification.style.opacity = "0";
  1797. notification.style.transform = "translateY(-10px)";
  1798. setTimeout(() => {
  1799. document.body.removeChild(notification);
  1800. }, 300);
  1801. }, 3000);
  1802. }
  1803.  
  1804. // 获取Token
  1805. function getToken() {
  1806. const cookieMap = new Map();
  1807. document.cookie.split("; ").forEach((cookie) => {
  1808. const [key, value] = cookie.split("=");
  1809. cookieMap.set(key, value);
  1810. });
  1811. const token = cookieMap.get("iClass-token");
  1812. const userid = cookieMap.get("iClass-uuid");
  1813. return [userid, token];
  1814. }
  1815.  
  1816. // 文件下载相关函数
  1817. async function downloadFile(url, filename) {
  1818. console.log("Call download");
  1819. downloading = true;
  1820. await jsp;
  1821. NProgress.configure({ trickle: false, speed: 0 });
  1822. try {
  1823. const response = await fetch(url);
  1824.  
  1825. if (!response.ok) {
  1826. throw new Error(`HTTP error! status: ${response.status}`);
  1827. }
  1828.  
  1829. const contentLength = response.headers.get("content-length");
  1830. if (!contentLength) {
  1831. throw new Error("Content-Length response header unavailable");
  1832. }
  1833.  
  1834. const total = parseInt(contentLength, 10);
  1835. sumBytes += total;
  1836. const reader = response.body.getReader();
  1837. const chunks = [];
  1838. while (true) {
  1839. const { done, value } = await reader.read();
  1840. if (done) break;
  1841. if (!downloading) {
  1842. NProgress.done();
  1843. return;
  1844. }
  1845. chunks.push(value);
  1846. loadedBytes += value.length;
  1847. NProgress.set(loadedBytes / sumBytes);
  1848. }
  1849. NProgress.done();
  1850. sumBytes -= total;
  1851. loadedBytes -= total;
  1852. const blob = new Blob(chunks);
  1853. const downloadUrl = window.URL.createObjectURL(blob);
  1854. const a = document.createElement("a");
  1855. a.href = downloadUrl;
  1856. a.download = filename;
  1857. document.body.appendChild(a);
  1858. a.click();
  1859. window.URL.revokeObjectURL(downloadUrl);
  1860. } catch (error) {
  1861. console.error("Download failed:", error);
  1862. }
  1863. }
  1864.  
  1865. // 任务搜索函数
  1866. async function searchTask(siteId, keyword, token) {
  1867. const res = await fetch(
  1868. "https://apiucloud.bupt.edu.cn/ykt-site/work/student/list",
  1869. {
  1870. headers: {
  1871. authorization: "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
  1872. "blade-auth": token,
  1873. "content-type": "application/json;charset=UTF-8",
  1874. },
  1875. body: JSON.stringify({
  1876. siteId,
  1877. keyword,
  1878. current: 1,
  1879. size: 5,
  1880. }),
  1881. method: "POST",
  1882. }
  1883. );
  1884. const json = await res.json();
  1885. return json;
  1886. }
  1887.  
  1888. // 课程搜索函数
  1889. async function searchCourse(userId, id, keyword, token) {
  1890. const res = await fetch(
  1891. "https://apiucloud.bupt.edu.cn/ykt-site/site/list/student/current?size=999999&current=1&userId=" +
  1892. userId +
  1893. "&siteRoleCode=2",
  1894. {
  1895. headers: {
  1896. authorization: "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
  1897. "blade-auth": token,
  1898. },
  1899. body: null,
  1900. method: "GET",
  1901. }
  1902. );
  1903. const json = await res.json();
  1904. const list = json.data.records.map((x) => ({
  1905. id: x.id,
  1906. name: x.siteName,
  1907. teachers: x.teachers.map((y) => y.name).join(", "),
  1908. }));
  1909.  
  1910. async function searchWithLimit(list, id, keyword, token, limit = 5) {
  1911. for (let i = 0; i < list.length; i += limit) {
  1912. const batch = list.slice(i, i + limit);
  1913. const jobs = batch.map((x) => searchTask(x.id, keyword, token));
  1914. const ress = await Promise.all(jobs);
  1915. for (let j = 0; j < ress.length; j++) {
  1916. const res = ress[j];
  1917. if (res.data && res.data.records && res.data.records.length > 0) {
  1918. for (const item of res.data.records) {
  1919. if (item.id == id) {
  1920. return batch[j];
  1921. }
  1922. }
  1923. }
  1924. }
  1925. }
  1926. return null;
  1927. }
  1928. return await searchWithLimit(list, id, keyword, token);
  1929. }
  1930.  
  1931. // 获取任务列表
  1932. async function getTasks(siteId, token) {
  1933. const res = await fetch(
  1934. "https://apiucloud.bupt.edu.cn/ykt-site/work/student/list",
  1935. {
  1936. headers: {
  1937. authorization: "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
  1938. "blade-auth": token,
  1939. "content-type": "application/json;charset=UTF-8",
  1940. },
  1941. body: JSON.stringify({
  1942. siteId,
  1943. current: 1,
  1944. size: 9999,
  1945. }),
  1946. method: "POST",
  1947. }
  1948. );
  1949. const json = await res.json();
  1950. return json;
  1951. }
  1952.  
  1953. // 搜索课程
  1954. async function searchCourses(nids) {
  1955. const result = {};
  1956. let ids = [];
  1957. for (let id of nids) {
  1958. const r = get(id);
  1959. if (r) result[id] = r;
  1960. else ids.push(id);
  1961. }
  1962.  
  1963. if (ids.length == 0) return result;
  1964. const [userid, token] = getToken();
  1965. const res = await fetch(
  1966. "https://apiucloud.bupt.edu.cn/ykt-site/site/list/student/current?size=999999&current=1&userId=" +
  1967. userid +
  1968. "&siteRoleCode=2",
  1969. {
  1970. headers: {
  1971. authorization: "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
  1972. "blade-auth": token,
  1973. },
  1974. body: null,
  1975. method: "GET",
  1976. }
  1977. );
  1978. const json = await res.json();
  1979. const list = json.data.records.map((x) => ({
  1980. id: x.id,
  1981. name: x.siteName,
  1982. teachers: x.teachers.map((y) => y.name).join(", "),
  1983. }));
  1984. const hashMap = new Map();
  1985. let count = ids.length;
  1986. for (let i = 0; i < ids.length; i++) {
  1987. hashMap.set(ids[i], i);
  1988. }
  1989.  
  1990. async function searchWithLimit(list, limit = 5) {
  1991. for (let i = 0; i < list.length; i += limit) {
  1992. const batch = list.slice(i, i + limit);
  1993. const jobs = batch.map((x) => getTasks(x.id, token));
  1994. const ress = await Promise.all(jobs);
  1995. for (let j = 0; j < ress.length; j++) {
  1996. const res = ress[j];
  1997. if (res.data && res.data.records && res.data.records.length > 0) {
  1998. for (const item of res.data.records) {
  1999. if (hashMap.has(item.id)) {
  2000. result[item.id] = batch[j];
  2001. set(item.id, batch[j]);
  2002. if (--count == 0) {
  2003. return result;
  2004. }
  2005. }
  2006. }
  2007. }
  2008. }
  2009. }
  2010. return result;
  2011. }
  2012. return await searchWithLimit(list);
  2013. }
  2014.  
  2015. // 获取未完成列表
  2016. async function getUndoneList() {
  2017. const [userid, token] = getToken();
  2018. const res = await fetch(
  2019. "https://apiucloud.bupt.edu.cn/ykt-site/site/student/undone?userId=" +
  2020. userid,
  2021. {
  2022. headers: {
  2023. authorization: "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
  2024. "blade-auth": token,
  2025. },
  2026. method: "GET",
  2027. }
  2028. );
  2029. const json = await res.json();
  2030. return json;
  2031. }
  2032.  
  2033. // 获取详情
  2034. async function getDetail(id) {
  2035. const [userid, token] = getToken();
  2036. const res = await fetch(
  2037. "https://apiucloud.bupt.edu.cn/ykt-site/work/detail?assignmentId=" + id,
  2038. {
  2039. headers: {
  2040. authorization: "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
  2041. "blade-auth": token,
  2042. },
  2043. body: null,
  2044. method: "GET",
  2045. }
  2046. );
  2047. const json = await res.json();
  2048. return json;
  2049. }
  2050.  
  2051. // 获取站点资源
  2052. async function getSiteResource(id) {
  2053. const [userid, token] = getToken();
  2054. const res = await fetch(
  2055. "https://apiucloud.bupt.edu.cn/ykt-site/site-resource/tree/student?siteId=" +
  2056. id +
  2057. "&userId=" +
  2058. userid,
  2059. {
  2060. headers: {
  2061. authorization: "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
  2062. "blade-auth": token,
  2063. },
  2064. body: null,
  2065. method: "POST",
  2066. }
  2067. );
  2068. const json = await res.json();
  2069. const result = [];
  2070. function foreach(data) {
  2071. if (!data || !Array.isArray(data)) return;
  2072. data.forEach((x) => {
  2073. if (x.attachmentVOs && Array.isArray(x.attachmentVOs)) {
  2074. x.attachmentVOs.forEach((y) => {
  2075. if (y.type !== 2 && y.resource) result.push(y.resource);
  2076. });
  2077. }
  2078. if (x.children) foreach(x.children);
  2079. });
  2080. }
  2081. foreach(json.data);
  2082. return result;
  2083. }
  2084.  
  2085. // 更新作业显示
  2086. async function updateAssignmentDisplay(list, page) {
  2087. if (!list || list.length === 0) return;
  2088.  
  2089. // 获取当前页的作业
  2090. const tlist = list.slice((page - 1) * 6, page * 6);
  2091. if (tlist.length === 0) return;
  2092.  
  2093. // 获取课程信息
  2094. const ids = tlist.map((x) => x.activityId);
  2095. const infos = await searchCourses(ids);
  2096.  
  2097. // 确保所有信息都已获取到
  2098. if (Object.keys(infos).length === 0) return;
  2099.  
  2100. // 准备显示文本
  2101. const texts = tlist.map((x) => {
  2102. const info = infos[x.activityId];
  2103. return info ? `${info.name}(${info.teachers})` : "加载中...";
  2104. });
  2105.  
  2106. // 等待作业元素显示
  2107. const timeout = 5000; // 5秒超时
  2108. const startTime = Date.now();
  2109.  
  2110. let nodes;
  2111. while (Date.now() - startTime < timeout) {
  2112. nodes = $x(
  2113. '//*[@id="layout-container"]/div[2]/div[2]/div/div[2]/div[1]/div[3]/div[2]/div/div'
  2114. );
  2115. if (
  2116. nodes.length > 0 &&
  2117. nodes.some((node) => node.children[0] && node.children[0].innerText)
  2118. ) {
  2119. break;
  2120. }
  2121. await sleep(100);
  2122. }
  2123.  
  2124. // 更新课程信息显示
  2125. for (let i = 0; i < Math.min(nodes.length, texts.length); i++) {
  2126. if (nodes[i] && nodes[i].children[1]) {
  2127. if (nodes[i].children[1].children.length === 0) {
  2128. const p = document.createElement("div");
  2129. const t = document.createTextNode(texts[i]);
  2130. p.appendChild(t);
  2131. p.style.color = "#0066cc";
  2132. nodes[i].children[1].insertAdjacentElement("afterbegin", p);
  2133. } else {
  2134. nodes[i].children[1].children[0].innerHTML = texts[i];
  2135. nodes[i].children[1].children[0].style.color = "#0066cc";
  2136. }
  2137. }
  2138. }
  2139. }
  2140.  
  2141. // XPath选择器
  2142. function $x(xpath, context = document) {
  2143. const iterator = document.evaluate(
  2144. xpath,
  2145. context,
  2146. null,
  2147. XPathResult.ANY_TYPE,
  2148. null
  2149. );
  2150. const results = [];
  2151. let item;
  2152. while ((item = iterator.iterateNext())) {
  2153. results.push(item);
  2154. }
  2155. return results;
  2156. }
  2157.  
  2158. // 本地存储
  2159. function set(k, v) {
  2160. const h = JSON.parse(localStorage.getItem("zzxw") || "{}");
  2161. h[k] = v;
  2162. localStorage.setItem("zzxw", JSON.stringify(h));
  2163. }
  2164.  
  2165. function get(k) {
  2166. const h = JSON.parse(localStorage.getItem("zzxw") || "{}");
  2167. return h[k];
  2168. }
  2169.  
  2170. // 插入课程信息
  2171. function insert(x) {
  2172. if (!x) return;
  2173. if (
  2174. $x(
  2175. "/html/body/div[1]/div/div[2]/div[2]/div/div/div[2]/div/div[2]/div[1]/div/div/div[1]/div/p"
  2176. ).length > 2
  2177. )
  2178. return;
  2179. const d = $x(
  2180. "/html/body/div[1]/div/div[2]/div[2]/div/div/div[2]/div/div[2]/div[1]/div/div/div[1]/div/p[1]"
  2181. );
  2182. if (!d.length) {
  2183. setTimeout(() => insert(x), 50);
  2184. return;
  2185. }
  2186. // 检查是否已经插入过
  2187. const existingText = Array.from(d[0].parentNode.childNodes).some(
  2188. (node) => node.textContent && node.textContent.includes(x.name)
  2189. );
  2190.  
  2191. if (!existingText) {
  2192. const p = document.createElement("p");
  2193. const t = document.createTextNode(x.name + "(" + x.teachers + ")");
  2194. p.appendChild(t);
  2195. d[0].after(p);
  2196. }
  2197. }
  2198.  
  2199. // 辅助函数
  2200. function sleep(n) {
  2201. return new Promise((res) => setTimeout(res, n));
  2202. }
  2203.  
  2204. async function wait(func) {
  2205. let r = func();
  2206. if (r instanceof Promise) r = await r;
  2207. if (r) return r;
  2208. await sleep(50);
  2209. return await wait(func);
  2210. }
  2211.  
  2212. async function waitChange(func, value) {
  2213. const r = value;
  2214. while (1) {
  2215. let t = func();
  2216. if (t instanceof Promise) t = await t;
  2217. if (t != r) return t;
  2218. await sleep(50);
  2219. }
  2220. }
  2221.  
  2222. // 预览URL相关
  2223. async function getPreviewURL(storageId) {
  2224. const res = await fetch(
  2225. "https://apiucloud.bupt.edu.cn/blade-source/resource/preview-url?resourceId=" +
  2226. storageId
  2227. );
  2228. const json = await res.json();
  2229. onlinePreview = json.data.onlinePreview;
  2230. return json.data.previewUrl;
  2231. }
  2232.  
  2233. // 启用文本选择 修改按钮尺寸
  2234. function addFunctionalCSS() {
  2235. GM_addStyle(`
  2236. .teacher-home-page .home-left-container .in-progress-section .in-progress-body .in-progress-item .activity-box .activity-title {
  2237. height: auto !important;
  2238. }
  2239. #layout-container > div.main-content > div.router-container > div > div.my-course-page {
  2240. max-height: none !important;
  2241. }
  2242. `);
  2243. if (settings.notification.betterNotificationHighlight) {
  2244. GM_addStyle(`
  2245. .notification-with-dot {
  2246. background-color: #fff8f8 !important;
  2247. border-left: 5px solid #f56c6c !important;
  2248. box-shadow: 0 2px 6px rgba(245, 108, 108, 0.2) !important;
  2249. padding: 0 22px !important;
  2250. margin-bottom: 8px !important;
  2251. border-radius: 4px !important;
  2252. transition: all 0.3s ease !important;
  2253. }
  2254. .notification-with-dot:hover {
  2255. background-color: #fff0f0 !important;
  2256. box-shadow: 0 4px 12px rgba(245, 108, 108, 0.3) !important;
  2257. transform: translateY(-2px) !important;
  2258. }
  2259. `);
  2260. }
  2261. if (settings.system.unlockCopy) {
  2262. GM_addStyle(`
  2263. .el-checkbox, .el-checkbox-button__inner, .el-empty__image img, .el-radio,
  2264. div, span, p, a, h1, h2, h3, h4, h5, h6, li, td, th {
  2265. -webkit-user-select: auto !important;
  2266. -moz-user-select: auto !important;
  2267. -ms-user-select: auto !important;
  2268. user-select: auto !important;
  2269. }
  2270. `);
  2271. document.addEventListener(
  2272. "copy",
  2273. function (e) {
  2274. e.stopImmediatePropagation();
  2275. },
  2276. true
  2277. );
  2278.  
  2279. document.addEventListener(
  2280. "selectstart",
  2281. function (e) {
  2282. e.stopImmediatePropagation();
  2283. },
  2284. true
  2285. );
  2286. }
  2287. if (settings.home.useBiggerButton) {
  2288. GM_addStyle(`
  2289. .teacher-home-page .home-left-container .my-lesson-section .my-lesson-header .header-control .banner-control-btn, .teacher-home-page .home-left-container .in-progress-section .in-progress-header .header-control .banner-control-btn {
  2290. width: 60px !important;
  2291. height: 30px !important;
  2292. background: #f2f2f2 !important;
  2293. line-height: auto !important;
  2294. }
  2295. .teacher-home-page .home-left-container .my-lesson-section .my-lesson-header .header-control .banner-control-btn span,.teacher-home-page .home-left-container .in-progress-section .in-progress-header .header-control .banner-control-btn span {
  2296. font-size: 22px !important;
  2297. }
  2298. .el-icon-arrow-left, .el-icon-arrow-right {
  2299. height: 100%;
  2300. display: flex;
  2301. align-items: center;
  2302. justify-content: center;
  2303. }
  2304. `);
  2305. }
  2306. }
  2307.  
  2308. // 主函数
  2309. async function main() {
  2310. "use strict";
  2311. // ticket跳转
  2312. if (new URLSearchParams(location.search).get("ticket")?.length) {
  2313. setTimeout(() => {
  2314. location.href = "https://ucloud.bupt.edu.cn/uclass/#/student/homePage";
  2315. }, 500);
  2316. return;
  2317. }
  2318.  
  2319. // 课件预览页面
  2320. if (
  2321. location.href.startsWith(
  2322. "https://ucloud.bupt.edu.cn/uclass/course.html#/resourceLearn"
  2323. )
  2324. ) {
  2325. if (settings.system.betterTitle) {
  2326. function extractFilename(url) {
  2327. try {
  2328. const match = url.match(/previewUrl=([^&]+)/);
  2329. if (!match) return null;
  2330.  
  2331. const previewUrl = decodeURIComponent(match[1]);
  2332.  
  2333. // 从content-disposition中提取文件名
  2334. const filenameMatch = previewUrl.match(/filename%3D([^&]+)/);
  2335. if (!filenameMatch) return null;
  2336.  
  2337. return decodeURIComponent(decodeURIComponent(filenameMatch[1]));
  2338. } catch (e) {
  2339. return null;
  2340. }
  2341. }
  2342. const url = location.href;
  2343. const filename = extractFilename(url);
  2344. const pageTitle = "[预览] " + (filename || "课件") + " - 教学云空间";
  2345. document.title = pageTitle;
  2346. }
  2347. if (settings.preview.autoClosePopup) {
  2348. const dialogBox = document.querySelector("div.el-message-box__wrapper");
  2349.  
  2350. if (
  2351. dialogBox &&
  2352. window.getComputedStyle(dialogBox).display !== "none"
  2353. ) {
  2354. const messageElement = dialogBox.querySelector(
  2355. ".el-message-box__message p"
  2356. );
  2357. if (
  2358. messageElement &&
  2359. (messageElement.textContent.includes("您正在学习其他课件") ||
  2360. messageElement.textContent.includes("您已经在学习此课件了"))
  2361. ) {
  2362. const confirmButton = dialogBox.querySelector(
  2363. ".el-button--primary"
  2364. );
  2365. if (confirmButton) {
  2366. confirmButton.click();
  2367. } else {
  2368. console.log("未找到确认按钮");
  2369. }
  2370. }
  2371. }
  2372. }
  2373. if (settings.preview.hideTimer) {
  2374. GM_addStyle(`
  2375. .preview-container .time {
  2376. display: none !important;
  2377. }
  2378. `);
  2379. }
  2380. }
  2381.  
  2382. // 作业详情页面
  2383. if (
  2384. location.href.startsWith(
  2385. "https://ucloud.bupt.edu.cn/uclass/course.html#/student/assignmentDetails_fullpage"
  2386. )
  2387. ) {
  2388. const q = new URLSearchParams(location.href);
  2389. const id = q.get("assignmentId");
  2390. const r = get(id);
  2391. const [userid, token] = getToken();
  2392. const title = q.get("assignmentTitle");
  2393. if (settings.system.betterTitle) {
  2394. const pageTitle = "[作业] " + title + " - 教学云空间";
  2395. document.title = pageTitle;
  2396. }
  2397. if (settings.homework.showHomeworkSource) {
  2398. // 显示相关课程信息
  2399. if (r) {
  2400. insert(r);
  2401. } else {
  2402. if (!id || !title) return;
  2403. try {
  2404. const courseInfo = await searchCourse(userid, id, title, token);
  2405. if (courseInfo) {
  2406. insert(courseInfo);
  2407. set(id, courseInfo);
  2408. }
  2409. } catch (e) {
  2410. console.error("获取课程信息失败", e);
  2411. }
  2412. }
  2413. }
  2414.  
  2415. // 处理资源预览和下载
  2416. try {
  2417. const detail = (await getDetail(id)).data;
  2418. if (!detail || !detail.assignmentResource) return;
  2419.  
  2420. const filenames = detail.assignmentResource.map((x) => x.resourceName);
  2421. const urls = await Promise.all(
  2422. detail.assignmentResource.map((x) => {
  2423. return getPreviewURL(x.resourceId);
  2424. })
  2425. );
  2426.  
  2427. await wait(
  2428. () =>
  2429. $x('//*[@id="assignment-info"]/div[2]/div[2]/div[2]/div').length > 0
  2430. );
  2431.  
  2432. $x('//*[@id="assignment-info"]/div[2]/div[2]/div[2]/div').forEach(
  2433. (x, index) => {
  2434. if (
  2435. x.querySelector(".by-icon-eye-grey") ||
  2436. x.querySelector(".by-icon-yundown-grey")
  2437. ) {
  2438. x.querySelector(".by-icon-eye-grey").remove();
  2439. x.querySelector(".by-icon-yundown-grey").remove();
  2440. }
  2441.  
  2442. // 添加预览按钮
  2443. const i = document.createElement("i");
  2444. i.title = "预览";
  2445. i.classList.add("by-icon-eye-grey");
  2446. i.addEventListener("click", () => {
  2447. const url = urls[index];
  2448. const filename = filenames[index];
  2449. if (settings.preview.autoDownload) {
  2450. downloadFile(url, filename);
  2451. console.log("Autodownload");
  2452. }
  2453. if (
  2454. filename.endsWith(".xls") ||
  2455. filename.endsWith(".xlsx") ||
  2456. url.endsWith(".doc") ||
  2457. url.endsWith(".docx") ||
  2458. url.endsWith(".ppt") ||
  2459. url.endsWith(".pptx")
  2460. )
  2461. openTab(
  2462. "https://view.officeapps.live.com/op/view.aspx?src=" +
  2463. encodeURIComponent(url),
  2464. { active: true, insert: true }
  2465. );
  2466. else if (onlinePreview !== null)
  2467. openTab(onlinePreview + encodeURIComponent(url), {
  2468. active: true,
  2469. insert: true,
  2470. });
  2471. });
  2472.  
  2473. // 添加下载按钮
  2474. const i2 = document.createElement("i");
  2475. i2.title = "下载";
  2476. i2.classList.add("by-icon-yundown-grey");
  2477. i2.addEventListener("click", () => {
  2478. downloadFile(urls[index], filenames[index]);
  2479. });
  2480.  
  2481. // 插入按钮
  2482. if (x.children.length >= 3) {
  2483. x.children[3]?.remove();
  2484. x.children[2]?.insertAdjacentElement("afterend", i);
  2485. x.children[2]?.remove();
  2486. x.children[1]?.insertAdjacentElement("afterend", i2);
  2487. } else {
  2488. x.appendChild(i2);
  2489. x.appendChild(i);
  2490. }
  2491. }
  2492. );
  2493. } catch (e) {
  2494. console.error("处理资源失败", e);
  2495. }
  2496. }
  2497.  
  2498. // 主页面
  2499. else if (
  2500. location.href.startsWith(
  2501. "https://ucloud.bupt.edu.cn/uclass/#/student/homePage"
  2502. ) ||
  2503. location.href.startsWith(
  2504. "https://ucloud.bupt.edu.cn/uclass/index.html#/student/homePage"
  2505. )
  2506. ) {
  2507. if (settings.system.betterTitle) {
  2508. const pageTitle = "个人主页 - 教学云空间";
  2509. document.title = pageTitle;
  2510. }
  2511. if (settings.home.addHomeworkSource) {
  2512. // 未完成任务列表
  2513. const list = glist || (await getUndoneList()).data.undoneList;
  2514. if (!list || !Array.isArray(list)) return;
  2515. glist = list;
  2516.  
  2517. const observer = new MutationObserver(async (mutations) => {
  2518. // 当前页码
  2519. const pageElement = document.querySelector(
  2520. "#layout-container > div.main-content > div.router-container > div > div.teacher-home-page > div.home-left-container.home-inline-block > div.in-progress-section.home-card > div.in-progress-header > div > div:nth-child(2) > div > div.banner-indicator.home-inline-block"
  2521. );
  2522.  
  2523. if (!pageElement) return;
  2524.  
  2525. // 解析页码
  2526. const currentPage = parseInt(
  2527. pageElement.innerHTML.trim().split("/")[0]
  2528. );
  2529. if (isNaN(currentPage)) return;
  2530.  
  2531. // 页码变化则更新显示
  2532. if (currentPage !== gpage) {
  2533. gpage = currentPage;
  2534. await updateAssignmentDisplay(list, currentPage);
  2535. }
  2536. });
  2537.  
  2538. observer.observe(document.body, {
  2539. childList: true,
  2540. subtree: true,
  2541. attributes: false,
  2542. characterData: true,
  2543. });
  2544.  
  2545. // 初始化页码
  2546. let page = 1;
  2547. const pageElement = document.querySelector(
  2548. "#layout-container > div.main-content > div.router-container > div > div.teacher-home-page > div.home-left-container.home-inline-block > div.in-progress-section.home-card > div.in-progress-header > div > div:nth-child(2) > div > div.banner-indicator.home-inline-block"
  2549. );
  2550.  
  2551. if (pageElement) {
  2552. page = parseInt(pageElement.innerHTML.trim().split("/")[0]);
  2553. gpage = page;
  2554. }
  2555.  
  2556. // 更新作业显示
  2557. await updateAssignmentDisplay(list, page);
  2558. }
  2559. if (settings.home.makeClassClickable) {
  2560. // 本学期课程点击事件
  2561. document.querySelectorAll('div[class="header-label"]').forEach((el) => {
  2562. if (el.textContent.includes("本学期课程")) {
  2563. el.style.cursor = "pointer";
  2564. el.addEventListener("click", (e) => {
  2565. e.preventDefault();
  2566. window.location.href =
  2567. "https://ucloud.bupt.edu.cn/uclass/index.html#/student/myCourse";
  2568. });
  2569. }
  2570. });
  2571. }
  2572. function wheelPageTurner() {
  2573. const pageConfigs = [
  2574. {
  2575. // 待办
  2576. targetSelector:
  2577. "#layout-container > div.main-content > div.router-container > div > div.teacher-home-page > div.home-left-container.home-inline-block > div.in-progress-section.home-card > div.in-progress-body",
  2578. prevPageSelector:
  2579. '#layout-container > div.main-content > div.router-container > div > div.teacher-home-page > div.home-left-container.home-inline-block > div.in-progress-section.home-card > div.in-progress-header div[title="上一页"]',
  2580. nextPageSelector:
  2581. '#layout-container > div.main-content > div.router-container > div > div.teacher-home-page > div.home-left-container.home-inline-block > div.in-progress-section.home-card > div.in-progress-header div[title="下一页"]',
  2582. pageIndicatorSelector:
  2583. "#layout-container > div.main-content > div.router-container > div > div.teacher-home-page > div.home-left-container.home-inline-block > div.in-progress-section.home-card > div.in-progress-header div.banner-indicator.home-inline-block",
  2584. },
  2585. {
  2586. // 本学期课程
  2587. targetSelector:
  2588. "#layout-container > div.main-content > div.router-container > div > div.teacher-home-page > div.home-left-container.home-inline-block > div.my-lesson-section.home-card > div.my-lesson-body",
  2589. prevPageSelector:
  2590. '#layout-container > div.main-content > div.router-container > div > div.teacher-home-page > div.home-left-container.home-inline-block > div.my-lesson-section.home-card > div.my-lesson-header div[title="上一页"]',
  2591. nextPageSelector:
  2592. '#layout-container > div.main-content > div.router-container > div > div.teacher-home-page > div.home-left-container.home-inline-block > div.my-lesson-section.home-card > div.my-lesson-header div[title="下一页"]',
  2593. pageIndicatorSelector:
  2594. "#layout-container > div.main-content > div.router-container > div > div.teacher-home-page > div.home-left-container.home-inline-block > div.my-lesson-section.home-card > div.my-lesson-header div.banner-indicator.home-inline-block",
  2595. },
  2596. ];
  2597. function parsePageIndicator(pageIndicator) {
  2598. const text = pageIndicator.textContent.trim();
  2599. const [currentPage, totalPages] = text.split("/").map(Number);
  2600. return { currentPage, totalPages };
  2601. }
  2602. function createWheelHandler(
  2603. prevPageElement,
  2604. nextPageElement,
  2605. pageIndicator
  2606. ) {
  2607. return function (event) {
  2608. const { currentPage, totalPages } =
  2609. parsePageIndicator(pageIndicator);
  2610. if (event.deltaY > 0 && currentPage < totalPages) {
  2611. event.preventDefault();
  2612. nextPageElement.click();
  2613. } else if (event.deltaY < 0 && currentPage > 1) {
  2614. event.preventDefault();
  2615. prevPageElement.click();
  2616. }
  2617. };
  2618. }
  2619. pageConfigs.forEach((config) => {
  2620. const targetDiv = document.querySelector(config.targetSelector);
  2621. const prevPageElement = document.querySelector(
  2622. config.prevPageSelector
  2623. );
  2624. const nextPageElement = document.querySelector(
  2625. config.nextPageSelector
  2626. );
  2627. const pageIndicator = document.querySelector(
  2628. config.pageIndicatorSelector
  2629. );
  2630.  
  2631. if (
  2632. !targetDiv ||
  2633. !prevPageElement ||
  2634. !nextPageElement ||
  2635. !pageIndicator
  2636. )
  2637. return;
  2638. targetDiv.addEventListener(
  2639. "wheel",
  2640. createWheelHandler(prevPageElement, nextPageElement, pageIndicator),
  2641. { passive: false }
  2642. );
  2643. });
  2644. }
  2645. if (settings.home.useWheelPageTurner) wheelPageTurner();
  2646. }
  2647.  
  2648. // 课程主页
  2649. else if (
  2650. location.href.startsWith(
  2651. "https://ucloud.bupt.edu.cn/uclass/course.html#/student/courseHomePage"
  2652. )
  2653. ) {
  2654. try {
  2655. const site = JSON.parse(localStorage.getItem("site"));
  2656. if (!site || !site.id) return;
  2657. if (settings.system.betterTitle) {
  2658. const pageTitle = "[课程] " + site.siteName + " - 教学云空间";
  2659. document.title = pageTitle;
  2660. }
  2661.  
  2662. const id = site.id;
  2663. const resources = await getSiteResource(id);
  2664.  
  2665. // 添加下载按钮到每个资源
  2666. const resourceItems = $x(
  2667. '//div[@class="resource-item"]/div[@class="right"]'
  2668. );
  2669. const previewItems = $x(
  2670. '//div[@class="resource-item"]/div[@class="left"]'
  2671. );
  2672.  
  2673. if (resourceItems.length > 0) {
  2674. resourceItems.forEach((x, index) => {
  2675. if (index >= resources.length) return;
  2676.  
  2677. if (settings.preview.autoDownload) {
  2678. previewItems[index].addEventListener(
  2679. "click",
  2680. async (e) => {
  2681. const url = await getPreviewURL(resources[index].id);
  2682. downloadFile(url, resources[index].name);
  2683. console.log("Autodownload");
  2684. },
  2685. false
  2686. );
  2687. }
  2688. if (settings.course.showAllDownloadButoon) {
  2689. const i = document.createElement("i");
  2690. i.title = "下载";
  2691. i.classList.add("by-icon-download");
  2692. i.classList.add("btn-icon");
  2693. i.classList.add("visible");
  2694. i.style.cssText = `
  2695. display: inline-block !important;
  2696. visibility: visible !important;
  2697. cursor: pointer !important;
  2698. `;
  2699.  
  2700. // 获取data-v属性
  2701. const dataAttr = Array.from(x.attributes).find((attr) =>
  2702. attr.localName.startsWith("data-v")
  2703. );
  2704. if (dataAttr) {
  2705. i.setAttribute(dataAttr.localName, "");
  2706. }
  2707.  
  2708. i.addEventListener(
  2709. "click",
  2710. async (e) => {
  2711. e.stopPropagation();
  2712. const url = await getPreviewURL(resources[index].id);
  2713. downloadFile(url, resources[index].name);
  2714. },
  2715. false
  2716. );
  2717. if (x.children.length) x.children[0].remove();
  2718. x.insertAdjacentElement("afterbegin", i);
  2719. }
  2720. });
  2721.  
  2722. // "下载全部"按钮
  2723. if (
  2724. !document.getElementById("downloadAllButton") &&
  2725. resources.length > 0 &&
  2726. settings.course.addBatchDownload
  2727. ) {
  2728. const downloadAllButton = `<div style="display: flex;flex-direction: row;justify-content: end;margin-right: 24px;margin-top: 20px;">
  2729. <button type="button" class="el-button submit-btn el-button--primary" id="downloadAllButton">
  2730. 下载全部
  2731. </button>
  2732. </div>`;
  2733.  
  2734. const resourceList = $x(
  2735. "/html/body/div/div/div[2]/div[2]/div/div/div"
  2736. );
  2737. if (resourceList.length > 0) {
  2738. const containerElement = document.createElement("div");
  2739. containerElement.innerHTML = downloadAllButton;
  2740. resourceList[0].before(containerElement);
  2741.  
  2742. document.getElementById("downloadAllButton").onclick =
  2743. async () => {
  2744. downloading = !downloading;
  2745. if (downloading) {
  2746. document.getElementById("downloadAllButton").innerHTML =
  2747. "取消下载";
  2748. for (let file of resources) {
  2749. if (!downloading) return;
  2750. await downloadFile(
  2751. await getPreviewURL(file.id),
  2752. file.name
  2753. );
  2754. }
  2755. // 下载完成后重置按钮
  2756. if (downloading) {
  2757. downloading = false;
  2758. document.getElementById("downloadAllButton").innerHTML =
  2759. "下载全部";
  2760. }
  2761. } else {
  2762. document.getElementById("downloadAllButton").innerHTML =
  2763. "下载全部";
  2764. }
  2765. };
  2766. }
  2767. }
  2768. }
  2769. } catch (e) {
  2770. console.error("课程主页处理失败", e);
  2771. }
  2772. } else if (location.href == "https://ucloud.bupt.edu.cn/#/") {
  2773. if (settings.system.betterTitle) {
  2774. const pageTitle = "首页 - 教学云空间";
  2775. document.title = pageTitle;
  2776. }
  2777. }
  2778. // 通知页
  2779. else if (
  2780. location.href ==
  2781. "https://ucloud.bupt.edu.cn/uclass/index.html#/set/notice_fullpage"
  2782. ) {
  2783. if (settings.system.betterTitle) {
  2784. const pageTitle = "通知 - 教学云空间";
  2785. document.title = pageTitle;
  2786. }
  2787.  
  2788. function processNotifications() {
  2789. const noticeContainer = document.querySelector(
  2790. "#layout-container > div.main-content > div.router-container > div > div > div.setNotice-body > ul"
  2791. );
  2792. if (!noticeContainer) {
  2793. console.log("通知容器未找到");
  2794. return;
  2795. }
  2796. const noticeItems = Array.from(noticeContainer.querySelectorAll("li"));
  2797. if (noticeItems.length === 0) {
  2798. console.log("未找到通知项");
  2799. return;
  2800. }
  2801. if (settings.notification.sortNotificationsByTime) {
  2802. noticeItems.sort((a, b) => {
  2803. const timeA = a.querySelector("span._left-time");
  2804. const timeB = b.querySelector("span._left-time");
  2805. if (!timeA || !timeB) {
  2806. return 0;
  2807. }
  2808. const timeTextA = timeA.textContent.trim();
  2809. const timeTextB = timeB.textContent.trim();
  2810. const dateA = new Date(timeTextA);
  2811. const dateB = new Date(timeTextB);
  2812. return dateB - dateA;
  2813. });
  2814. }
  2815. noticeItems.forEach((item) => {
  2816. if (settings.notification.betterNotificationHighlight) {
  2817. const hasRedDot = item.querySelector(
  2818. "div.el-badge sup.el-badge__content.is-dot"
  2819. );
  2820. if (hasRedDot) {
  2821. item.classList.remove("notification-with-dot");
  2822. item.classList.add("notification-with-dot");
  2823. } else {
  2824. item.classList.remove("notification-with-dot");
  2825. }
  2826. }
  2827. noticeContainer.appendChild(item);
  2828. });
  2829. }
  2830. if (
  2831. settings.notification.sortNotificationsByTime ||
  2832. settings.notification.betterNotificationHighlight
  2833. ) {
  2834. // 等待通知元素加载好了再处理
  2835. const loadingMaskSelector =
  2836. "#layout-container > div.main-content > div.router-container > div > div > div.setNotice-body > div.el-loading-mask";
  2837. const observer = new MutationObserver((mutations) => {
  2838. const loadingMask = document.querySelector(loadingMaskSelector);
  2839. if (loadingMask && loadingMask.style.display === "none") {
  2840. processNotifications();
  2841. observer.disconnect();
  2842. }
  2843. });
  2844.  
  2845. const loadingMask = document.querySelector(loadingMaskSelector);
  2846. if (loadingMask && loadingMask.style.display === "none") {
  2847. processNotifications();
  2848. } else {
  2849. observer.observe(document.body, {
  2850. attributes: true,
  2851. attributeFilter: ["style"],
  2852. subtree: true,
  2853. });
  2854. setTimeout(() => observer.disconnect(), 10000);
  2855. }
  2856. }
  2857. }
  2858. }
  2859. })();