ucloud-Evolved

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

当前为 2025-04-11 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name ucloud-Evolved
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.23
  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. // 拦截 Office 预览页面
  28. if (
  29. location.href.startsWith("https://ucloud.bupt.edu.cn/office/") &&
  30. GM_getValue("autoSwitchOffice", false)
  31. ) {
  32. const url = new URLSearchParams(location.search).get("furl");
  33. const filename =
  34. new URLSearchParams(location.search).get("fullfilename") || url;
  35. const viewURL = new URL(url);
  36. if (new URLSearchParams(location.search).get("oauthKey")) {
  37. const viewURLsearch = new URLSearchParams(viewURL.search);
  38. viewURLsearch.set(
  39. "oauthKey",
  40. new URLSearchParams(location.search).get("oauthKey")
  41. );
  42. viewURL.search = viewURLsearch.toString();
  43. }
  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 (window.stop) window.stop();
  54. location.href =
  55. "https://view.officeapps.live.com/op/view.aspx?src=" +
  56. encodeURIComponent(viewURL.toString());
  57. return;
  58. } else if (filename.endsWith(".pdf")) {
  59. if (window.stop) window.stop();
  60. // 使用浏览器内置预览器,转blob避免出现下载动作
  61. fetch(viewURL.toString())
  62. .then((response) => response.blob())
  63. .then((blob) => {
  64. const blobUrl = URL.createObjectURL(blob);
  65. location.href = blobUrl;
  66. })
  67. .catch((err) => console.error("PDF加载失败:", err));
  68. return;
  69. }
  70. return;
  71. }
  72. })();
  73. (function interceptXHR() {
  74. const originalOpen = XMLHttpRequest.prototype.open;
  75.  
  76. XMLHttpRequest.prototype.open = function (
  77. method,
  78. url,
  79. async,
  80. user,
  81. password
  82. ) {
  83. // hook XMR
  84. if (GM_getValue("showMoreNotification", true)) {
  85. if (
  86. typeof url === "string" &&
  87. url.includes("/ykt-basics/api/inform/news/list")
  88. ) {
  89. url = url.replace(/size=\d+/, "size=1000");
  90. } else if (
  91. typeof url === "string" &&
  92. url.includes("/ykt-site/site/list/student/history")
  93. ) {
  94. url = url.replace(/size=\d+/, "size=15");
  95. }
  96. }
  97.  
  98. return originalOpen.call(this, method, url, async, user, password);
  99. };
  100. })();
  101. (function () {
  102. // 等待页面DOM加载完成
  103. document.addEventListener("DOMContentLoaded", initializeExtension);
  104.  
  105. // 用户设置
  106. const settings = {
  107. autoDownload: GM_getValue("autoDownload", false),
  108. autoSwitchOffice: GM_getValue("autoSwitchOffice", false),
  109. autoClosePopup: GM_getValue("autoClosePopup", true),
  110. hideTimer: GM_getValue("hideTimer", true),
  111. unlockCopy: GM_getValue("unlockCopy", true),
  112. showMoreNotification: GM_getValue("showMoreNotification", true),
  113. useBiggerButton: GM_getValue("useBiggerButton", true),
  114. autoUpdate: GM_getValue("autoUpdate", false),
  115. showConfigButton: GM_getValue("showConfigButton", true),
  116. };
  117.  
  118. // 辅助变量
  119. let jsp;
  120. let sumBytes = 0,
  121. loadedBytes = 0,
  122. downloading = false;
  123. let setClicked = false;
  124. let gpage = -1;
  125. let glist = null;
  126. let onlinePreview = null;
  127.  
  128. // 初始化扩展功能
  129. function initializeExtension() {
  130. // 注册菜单命令
  131. registerMenuCommands();
  132.  
  133. const nprogressCSS = GM_getResourceText("NPROGRESS_CSS");
  134. GM_addStyle(nprogressCSS);
  135.  
  136. if (settings.showConfigButton) {
  137. loadui();
  138. }
  139. addFunctionalCSS();
  140. main();
  141.  
  142. if (settings.autoUpdate) {
  143. checkForUpdates();
  144. }
  145.  
  146. // 监听URL哈希变化
  147. let hash = location.hash;
  148. setInterval(() => {
  149. if (location.hash != hash) {
  150. hash = location.hash;
  151. main();
  152. }
  153. }, 50);
  154. }
  155.  
  156. // 注册菜单命令
  157. function registerMenuCommands() {
  158. GM_registerMenuCommand(
  159. (settings.showConfigButton ? "✅" : "❌") +
  160. "显示配置按钮:" +
  161. (settings.showConfigButton ? "已启用" : "已禁用"),
  162. () => {
  163. settings.showConfigButton = !settings.showConfigButton;
  164. GM_setValue("showConfigButton", settings.showConfigButton);
  165. location.reload();
  166. }
  167. );
  168. GM_registerMenuCommand(
  169. (settings.autoDownload ? "✅" : "❌") +
  170. "预览课件时自动下载:" +
  171. (settings.autoDownload ? "已启用" : "已禁用"),
  172. () => {
  173. settings.autoDownload = !settings.autoDownload;
  174. GM_setValue("autoDownload", settings.autoDownload);
  175. location.reload();
  176. }
  177. );
  178.  
  179. GM_registerMenuCommand(
  180. (settings.autoSwitchOffice ? "✅" : "❌") +
  181. "使用 Office365 预览课件:" +
  182. (settings.autoSwitchOffice ? "已启用" : "已禁用"),
  183. () => {
  184. settings.autoSwitchOffice = !settings.autoSwitchOffice;
  185. GM_setValue("autoSwitchOffice", settings.autoSwitchOffice);
  186. location.reload();
  187. }
  188. );
  189. }
  190. /**
  191. * 通用标签页打开函数
  192. * @param {string} url - 要打开的URL
  193. * @param {Object} options - 选项参数
  194. * @param {boolean} [options.active=true] - 新标签页是否获得焦点
  195. * @param {boolean} [options.insert=true] - 是否在当前标签页旁边插入新标签页
  196. * @param {boolean} [options.setParent=true] - 新标签页是否将当前标签页设为父页面
  197. * @param {string} [options.windowName="_blank"] - window.open的窗口名称
  198. * @param {string} [options.windowFeatures=""] - window.open的窗口特性
  199. * @returns {Object|Window|null} 打开的标签页对象
  200. */
  201. function openTab(url, options = {}) {
  202. const defaultOptions = {
  203. active: true,
  204. insert: true,
  205. setParent: true,
  206. windowName: "_blank",
  207. windowFeatures: "",
  208. };
  209. const finalOptions = { ...defaultOptions, ...options };
  210. if (typeof GM_openInTab === "function") {
  211. try {
  212. return GM_openInTab(url, {
  213. active: finalOptions.active,
  214. insert: finalOptions.insert,
  215. setParent: finalOptions.setParent,
  216. });
  217. } catch (error) {
  218. return window.open(
  219. url,
  220. finalOptions.windowName,
  221. finalOptions.windowFeatures
  222. );
  223. }
  224. }
  225. }
  226. function showUpdateNotification(newVersion) {
  227. const notification = document.createElement("div");
  228. notification.style.cssText = `
  229. position: fixed;
  230. bottom: 80px;
  231. right: 20px;
  232. background: #4a6cf7;
  233. color: white;
  234. padding: 15px 20px;
  235. border-radius: 8px;
  236. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  237. z-index: 10000;
  238. font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
  239. max-width: 300px;
  240. `;
  241.  
  242. notification.innerHTML = `
  243. <div style="font-weight: bold; margin-bottom: 5px;">发现新版本 v${newVersion}</div>
  244. <div style="font-size: 14px; margin-bottom: 10px;">当前版本 v${GM_info.script.version}</div>
  245. <button id="updateNow" style="background: white; color: #4a6cf7; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; margin-right: 10px;">立即更新</button>
  246. <button id="updateLater" style="background: transparent; color: white; border: 1px solid white; padding: 5px 10px; border-radius: 4px; cursor: pointer;">稍后提醒</button>
  247. `;
  248.  
  249. document.body.appendChild(notification);
  250.  
  251. document.getElementById("updateNow").addEventListener("click", function () {
  252. openTab(GM_info.script.downloadURL, { active: true });
  253. document.body.removeChild(notification);
  254. });
  255.  
  256. document
  257. .getElementById("updateLater")
  258. .addEventListener("click", function () {
  259. document.body.removeChild(notification);
  260. });
  261. }
  262.  
  263. function checkForUpdates() {
  264. const lastCheckTime = GM_getValue("lastUpdateCheck", 0);
  265. const now = Date.now();
  266. const ONE_DAY = 24 * 60 * 60 * 1000; // 一天的毫秒数
  267.  
  268. if (now - lastCheckTime > ONE_DAY) {
  269. GM_setValue("lastUpdateCheck", now);
  270. GM_xmlhttpRequest({
  271. method: "GET",
  272. url: GM_info.script.updateURL,
  273. onload: function (response) {
  274. const versionMatch = response.responseText.match(
  275. /@version\s+(\d+\.\d+)/
  276. );
  277. if (versionMatch && versionMatch[1]) {
  278. const latestVersion = versionMatch[1];
  279. const currentVersion = GM_info.script.version;
  280. if (latestVersion > currentVersion) {
  281. showUpdateNotification(latestVersion);
  282. }
  283. }
  284. },
  285. });
  286. }
  287. }
  288.  
  289. function loadui() {
  290. GM_addStyle(`
  291. #yzHelper-settings {
  292. position: fixed;
  293. bottom: 20px;
  294. right: 20px;
  295. background: #ffffff;
  296. box-shadow: 0 5px 25px rgba(0, 0, 0, 0.15);
  297. border-radius: 12px;
  298. padding: 20px;
  299. z-index: 9999;
  300. display: none;
  301. width: 300px;
  302. font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
  303. transition: all 0.3s ease;
  304. opacity: 0;
  305. transform: translateY(10px);
  306. color: #333;
  307. }
  308. #yzHelper-settings.visible {
  309. opacity: 1;
  310. transform: translateY(0);
  311. }
  312. #yzHelper-settings h3 {
  313. margin-top: 0;
  314. margin-bottom: 15px;
  315. font-size: 18px;
  316. font-weight: 600;
  317. color: #2c3e50;
  318. padding-bottom: 10px;
  319. border-bottom: 1px solid #eee;
  320. }
  321. #yzHelper-settings .setting-item {
  322. margin-bottom: 16px;
  323. display: flex;
  324. align-items: center;
  325. }
  326. #yzHelper-settings .setting-item:last-of-type {
  327. margin-bottom: 20px;
  328. }
  329. #yzHelper-settings .switch {
  330. position: relative;
  331. display: inline-block;
  332. width: 44px;
  333. height: 24px;
  334. margin-right: 10px;
  335. }
  336. #yzHelper-settings .switch input {
  337. opacity: 0;
  338. width: 0;
  339. height: 0;
  340. }
  341. #yzHelper-settings .slider {
  342. position: absolute;
  343. cursor: pointer;
  344. top: 0;
  345. left: 0;
  346. right: 0;
  347. bottom: 0;
  348. background-color: #ccc;
  349. transition: .3s;
  350. border-radius: 24px;
  351. }
  352. #yzHelper-settings .slider:before {
  353. position: absolute;
  354. content: "";
  355. height: 18px;
  356. width: 18px;
  357. left: 3px;
  358. bottom: 3px;
  359. background-color: white;
  360. transition: .3s;
  361. border-radius: 50%;
  362. }
  363. #yzHelper-settings input:checked + .slider {
  364. background-color: #ffbe00;
  365. }
  366. #yzHelper-settings input:focus + .slider {
  367. box-shadow: 0 0 1px #ffbe00;
  368. }
  369. #yzHelper-settings input:checked + .slider:before {
  370. transform: translateX(20px);
  371. }
  372. #yzHelper-settings .setting-label {
  373. font-size: 14px;
  374. }
  375. #yzHelper-settings .buttons {
  376. display: flex;
  377. justify-content: flex-end;
  378. gap: 10px;
  379. }
  380. #yzHelper-settings button {
  381. background: #ffbe00;
  382. border: none;
  383. padding: 8px 16px;
  384. border-radius: 6px;
  385. cursor: pointer;
  386. font-weight: 500;
  387. color: #fff;
  388. transition: all 0.2s ease;
  389. outline: none;
  390. font-size: 14px;
  391. }
  392. #yzHelper-settings button:hover {
  393. background: #e9ad00;
  394. }
  395. #yzHelper-settings button.cancel {
  396. background: #f1f1f1;
  397. color: #666;
  398. }
  399. #yzHelper-settings button.cancel:hover {
  400. background: #e5e5e5;
  401. }
  402. #yzHelper-settings-toggle {
  403. position: fixed;
  404. bottom: 20px;
  405. right: 20px;
  406. background: #ffbe00;
  407. color: #fff;
  408. width: 50px;
  409. height: 50px;
  410. border-radius: 50%;
  411. display: flex;
  412. align-items: center;
  413. justify-content: center;
  414. font-size: 24px;
  415. cursor: pointer;
  416. z-index: 9998;
  417. box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
  418. transition: all 0.3s ease;
  419. }
  420. #yzHelper-settings-toggle:hover {
  421. transform: rotate(30deg);
  422. box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
  423. }
  424. #yzHelper-version {
  425. position: absolute;
  426. bottom: 15px;
  427. left: 20px;
  428. font-size: 12px;
  429. color: #999;
  430. }
  431. `);
  432.  
  433. // 设置面板
  434. const settingsToggle = document.createElement("div");
  435. settingsToggle.id = "yzHelper-settings-toggle";
  436. settingsToggle.innerHTML = "⚙️";
  437. settingsToggle.title = "云邮助手设置";
  438. document.body.appendChild(settingsToggle);
  439.  
  440. const settingsPanel = document.createElement("div");
  441. settingsPanel.id = "yzHelper-settings";
  442. settingsPanel.innerHTML = `
  443. <h3>云邮教学空间助手设置</h3>
  444. <div class="setting-item">
  445. <label class="switch">
  446. <input type="checkbox" id="autoDownload" ${
  447. settings.autoDownload ? "checked" : ""
  448. }>
  449. <span class="slider"></span>
  450. </label>
  451. <span class="setting-label">预览课件时自动下载</span>
  452. </div>
  453. <div class="setting-item">
  454. <label class="switch">
  455. <input type="checkbox" id="autoSwitchOffice" ${
  456. settings.autoSwitchOffice ? "checked" : ""
  457. }>
  458. <span class="slider"></span>
  459. </label>
  460. <span class="setting-label">使用 Office365 预览课件</span>
  461. </div>
  462. <div class="setting-item">
  463. <label class="switch">
  464. <input type="checkbox" id="autoClosePopup" ${
  465. settings.autoClosePopup ? "checked" : ""
  466. }>
  467. <span class="slider"></span>
  468. </label>
  469. <span class="setting-label">自动关闭预览弹窗</span>
  470. </div>
  471. <div class="setting-item">
  472. <label class="switch">
  473. <input type="checkbox" id="hideTimer" ${
  474. settings.hideTimer ? "checked" : ""
  475. }>
  476. <span class="slider"></span>
  477. </label>
  478. <span class="setting-label">隐藏预览界面倒计时</span>
  479. </div>
  480. <div class="setting-item">
  481. <label class="switch">
  482. <input type="checkbox" id="unlockCopy" ${
  483. settings.unlockCopy ? "checked" : ""
  484. }>
  485. <span class="slider"></span>
  486. </label>
  487. <span class="setting-label">解除复制限制</span>
  488. </div>
  489. <div class="setting-item">
  490. <label class="switch">
  491. <input type="checkbox" id="showMoreNotification" ${
  492. settings.showMoreNotification ? "checked" : ""
  493. }>
  494. <span class="slider"></span>
  495. </label>
  496. <span class="setting-label">显示更多的通知</span>
  497. </div>
  498. <div class="setting-item">
  499. <label class="switch">
  500. <input type="checkbox" id="useBiggerButton" ${
  501. settings.useBiggerButton ? "checked" : ""
  502. }>
  503. <span class="slider"></span>
  504. </label>
  505. <span class="setting-label">加大翻页按钮尺寸</span>
  506. </div>
  507. <div class="setting-item">
  508. <label class="switch">
  509. <input type="checkbox" id="autoUpdate" ${
  510. settings.autoUpdate ? "checked" : ""
  511. }>
  512. <span class="slider"></span>
  513. </label>
  514. <span class="setting-label">内置更新检查</span>
  515. </div>
  516. <div class="buttons">
  517. <button id="cancelSettings" class="cancel">取消</button>
  518. <button id="saveSettings">保存设置</button>
  519. </div>
  520. <div id="yzHelper-version">当前版本:${GM_info.script.version}</div>
  521. `;
  522. document.body.appendChild(settingsPanel);
  523.  
  524. // 面板交互
  525. settingsToggle.addEventListener("click", () => {
  526. const isVisible = settingsPanel.classList.contains("visible");
  527. if (isVisible) {
  528. settingsPanel.classList.remove("visible");
  529. setTimeout(() => {
  530. settingsPanel.style.display = "none";
  531. }, 300);
  532. } else {
  533. settingsPanel.style.display = "block";
  534. void settingsPanel.offsetWidth;
  535. settingsPanel.classList.add("visible");
  536. }
  537. });
  538.  
  539. document.getElementById("cancelSettings").addEventListener("click", () => {
  540. settingsPanel.classList.remove("visible");
  541. setTimeout(() => {
  542. settingsPanel.style.display = "none";
  543. }, 300);
  544. });
  545.  
  546. document.getElementById("saveSettings").addEventListener("click", () => {
  547. settings.autoDownload = document.getElementById("autoDownload").checked;
  548. settings.autoSwitchOffice =
  549. document.getElementById("autoSwitchOffice").checked;
  550. settings.autoClosePopup =
  551. document.getElementById("autoClosePopup").checked;
  552. settings.hideTimer = document.getElementById("hideTimer").checked;
  553. settings.unlockCopy = document.getElementById("unlockCopy").checked;
  554. settings.showMoreNotification = document.getElementById(
  555. "showMoreNotification"
  556. ).checked;
  557. settings.useBiggerButton =
  558. document.getElementById("useBiggerButton").checked;
  559.  
  560. GM_setValue("autoDownload", settings.autoDownload);
  561. GM_setValue("autoSwitchOffice", settings.autoSwitchOffice);
  562. GM_setValue("autoClosePopup", settings.autoClosePopup);
  563. GM_setValue("hideTimer", settings.hideTimer);
  564. GM_setValue("unlockCopy", settings.unlockCopy);
  565. GM_setValue("showMoreNotification", settings.showMoreNotification);
  566. GM_setValue("useBiggerButton", settings.useBiggerButton);
  567.  
  568. settingsPanel.classList.remove("visible");
  569. setTimeout(() => {
  570. settingsPanel.style.display = "none";
  571. showNotification("设置已保存", "刷新页面后生效");
  572. }, 300);
  573. });
  574.  
  575. // 通知函数
  576. function showNotification(title, message) {
  577. const notification = document.createElement("div");
  578. notification.style.cssText = `
  579. position: fixed;
  580. bottom: 80px;
  581. right: 20px;
  582. background: #4CAF50;
  583. color: white;
  584. padding: 15px 20px;
  585. border-radius: 8px;
  586. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  587. z-index: 10000;
  588. font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
  589. max-width: 300px;
  590. opacity: 0;
  591. transform: translateY(-10px);
  592. transition: all 0.3s ease;
  593. `;
  594.  
  595. notification.innerHTML = `
  596. <div style="font-weight: bold; margin-bottom: 5px;">${title}</div>
  597. <div style="font-size: 14px;">${message}</div>
  598. `;
  599.  
  600. document.body.appendChild(notification);
  601.  
  602. void notification.offsetWidth;
  603.  
  604. notification.style.opacity = "1";
  605. notification.style.transform = "translateY(0)";
  606.  
  607. setTimeout(() => {
  608. notification.style.opacity = "0";
  609. notification.style.transform = "translateY(-10px)";
  610. setTimeout(() => {
  611. document.body.removeChild(notification);
  612. }, 300);
  613. }, 3000);
  614. }
  615. }
  616. // 获取Token
  617. function getToken() {
  618. const cookieMap = new Map();
  619. document.cookie.split("; ").forEach((cookie) => {
  620. const [key, value] = cookie.split("=");
  621. cookieMap.set(key, value);
  622. });
  623. const token = cookieMap.get("iClass-token");
  624. const userid = cookieMap.get("iClass-uuid");
  625. return [userid, token];
  626. }
  627.  
  628. // 文件下载相关函数
  629. async function downloadFile(url, filename) {
  630. console.log("Call download");
  631. downloading = true;
  632. await jsp;
  633. NProgress.configure({ trickle: false, speed: 0 });
  634. try {
  635. const response = await fetch(url);
  636.  
  637. if (!response.ok) {
  638. throw new Error(`HTTP error! status: ${response.status}`);
  639. }
  640.  
  641. const contentLength = response.headers.get("content-length");
  642. if (!contentLength) {
  643. throw new Error("Content-Length response header unavailable");
  644. }
  645.  
  646. const total = parseInt(contentLength, 10);
  647. sumBytes += total;
  648. const reader = response.body.getReader();
  649. const chunks = [];
  650. while (true) {
  651. const { done, value } = await reader.read();
  652. if (done) break;
  653. if (!downloading) {
  654. NProgress.done();
  655. return;
  656. }
  657. chunks.push(value);
  658. loadedBytes += value.length;
  659. NProgress.set(loadedBytes / sumBytes);
  660. }
  661. NProgress.done();
  662. sumBytes -= total;
  663. loadedBytes -= total;
  664. const blob = new Blob(chunks);
  665. const downloadUrl = window.URL.createObjectURL(blob);
  666. const a = document.createElement("a");
  667. a.href = downloadUrl;
  668. a.download = filename;
  669. document.body.appendChild(a);
  670. a.click();
  671. window.URL.revokeObjectURL(downloadUrl);
  672. } catch (error) {
  673. console.error("Download failed:", error);
  674. }
  675. }
  676.  
  677. // 任务搜索函数
  678. async function searchTask(siteId, keyword, token) {
  679. const res = await fetch(
  680. "https://apiucloud.bupt.edu.cn/ykt-site/work/student/list",
  681. {
  682. headers: {
  683. authorization: "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
  684. "blade-auth": token,
  685. "content-type": "application/json;charset=UTF-8",
  686. },
  687. body: JSON.stringify({
  688. siteId,
  689. keyword,
  690. current: 1,
  691. size: 5,
  692. }),
  693. method: "POST",
  694. }
  695. );
  696. const json = await res.json();
  697. return json;
  698. }
  699.  
  700. // 课程搜索函数
  701. async function searchCourse(userId, id, keyword, token) {
  702. const res = await fetch(
  703. "https://apiucloud.bupt.edu.cn/ykt-site/site/list/student/current?size=999999&current=1&userId=" +
  704. userId +
  705. "&siteRoleCode=2",
  706. {
  707. headers: {
  708. authorization: "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
  709. "blade-auth": token,
  710. },
  711. body: null,
  712. method: "GET",
  713. }
  714. );
  715. const json = await res.json();
  716. const list = json.data.records.map((x) => ({
  717. id: x.id,
  718. name: x.siteName,
  719. teachers: x.teachers.map((y) => y.name).join(", "),
  720. }));
  721.  
  722. async function searchWithLimit(list, id, keyword, token, limit = 5) {
  723. for (let i = 0; i < list.length; i += limit) {
  724. const batch = list.slice(i, i + limit);
  725. const jobs = batch.map((x) => searchTask(x.id, keyword, token));
  726. const ress = await Promise.all(jobs);
  727. for (let j = 0; j < ress.length; j++) {
  728. const res = ress[j];
  729. if (res.data && res.data.records && res.data.records.length > 0) {
  730. for (const item of res.data.records) {
  731. if (item.id == id) {
  732. return batch[j];
  733. }
  734. }
  735. }
  736. }
  737. }
  738. return null;
  739. }
  740. return await searchWithLimit(list, id, keyword, token);
  741. }
  742.  
  743. // 获取任务列表
  744. async function getTasks(siteId, token) {
  745. const res = await fetch(
  746. "https://apiucloud.bupt.edu.cn/ykt-site/work/student/list",
  747. {
  748. headers: {
  749. authorization: "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
  750. "blade-auth": token,
  751. "content-type": "application/json;charset=UTF-8",
  752. },
  753. body: JSON.stringify({
  754. siteId,
  755. current: 1,
  756. size: 9999,
  757. }),
  758. method: "POST",
  759. }
  760. );
  761. const json = await res.json();
  762. return json;
  763. }
  764.  
  765. // 搜索课程
  766. async function searchCourses(nids) {
  767. const result = {};
  768. let ids = [];
  769. for (let id of nids) {
  770. const r = get(id);
  771. if (r) result[id] = r;
  772. else ids.push(id);
  773. }
  774.  
  775. if (ids.length == 0) return result;
  776. const [userid, token] = getToken();
  777. const res = await fetch(
  778. "https://apiucloud.bupt.edu.cn/ykt-site/site/list/student/current?size=999999&current=1&userId=" +
  779. userid +
  780. "&siteRoleCode=2",
  781. {
  782. headers: {
  783. authorization: "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
  784. "blade-auth": token,
  785. },
  786. body: null,
  787. method: "GET",
  788. }
  789. );
  790. const json = await res.json();
  791. const list = json.data.records.map((x) => ({
  792. id: x.id,
  793. name: x.siteName,
  794. teachers: x.teachers.map((y) => y.name).join(", "),
  795. }));
  796. const hashMap = new Map();
  797. let count = ids.length;
  798. for (let i = 0; i < ids.length; i++) {
  799. hashMap.set(ids[i], i);
  800. }
  801.  
  802. async function searchWithLimit(list, limit = 5) {
  803. for (let i = 0; i < list.length; i += limit) {
  804. const batch = list.slice(i, i + limit);
  805. const jobs = batch.map((x) => getTasks(x.id, token));
  806. const ress = await Promise.all(jobs);
  807. for (let j = 0; j < ress.length; j++) {
  808. const res = ress[j];
  809. if (res.data && res.data.records && res.data.records.length > 0) {
  810. for (const item of res.data.records) {
  811. if (hashMap.has(item.id)) {
  812. result[item.id] = batch[j];
  813. set(item.id, batch[j]);
  814. if (--count == 0) {
  815. return result;
  816. }
  817. }
  818. }
  819. }
  820. }
  821. }
  822. return result;
  823. }
  824. return await searchWithLimit(list);
  825. }
  826.  
  827. // 获取未完成列表
  828. async function getUndoneList() {
  829. const [userid, token] = getToken();
  830. const res = await fetch(
  831. "https://apiucloud.bupt.edu.cn/ykt-site/site/student/undone?userId=" +
  832. userid,
  833. {
  834. headers: {
  835. authorization: "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
  836. "blade-auth": token,
  837. },
  838. method: "GET",
  839. }
  840. );
  841. const json = await res.json();
  842. return json;
  843. }
  844.  
  845. // 获取详情
  846. async function getDetail(id) {
  847. const [userid, token] = getToken();
  848. const res = await fetch(
  849. "https://apiucloud.bupt.edu.cn/ykt-site/work/detail?assignmentId=" + id,
  850. {
  851. headers: {
  852. authorization: "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
  853. "blade-auth": token,
  854. },
  855. body: null,
  856. method: "GET",
  857. }
  858. );
  859. const json = await res.json();
  860. return json;
  861. }
  862.  
  863. // 获取站点资源
  864. async function getSiteResource(id) {
  865. const [userid, token] = getToken();
  866. const res = await fetch(
  867. "https://apiucloud.bupt.edu.cn/ykt-site/site-resource/tree/student?siteId=" +
  868. id +
  869. "&userId=" +
  870. userid,
  871. {
  872. headers: {
  873. authorization: "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
  874. "blade-auth": token,
  875. },
  876. body: null,
  877. method: "POST",
  878. }
  879. );
  880. const json = await res.json();
  881. const result = [];
  882. function foreach(data) {
  883. if (!data || !Array.isArray(data)) return;
  884. data.forEach((x) => {
  885. if (x.attachmentVOs && Array.isArray(x.attachmentVOs)) {
  886. x.attachmentVOs.forEach((y) => {
  887. if (y.type !== 2 && y.resource) result.push(y.resource);
  888. });
  889. }
  890. if (x.children) foreach(x.children);
  891. });
  892. }
  893. foreach(json.data);
  894. return result;
  895. }
  896.  
  897. // 更新作业显示
  898. async function updateAssignmentDisplay(list, page) {
  899. if (!list || list.length === 0) return;
  900.  
  901. // 获取当前页的作业
  902. const tlist = list.slice((page - 1) * 6, page * 6);
  903. if (tlist.length === 0) return;
  904.  
  905. // 获取课程信息
  906. const ids = tlist.map((x) => x.activityId);
  907. const infos = await searchCourses(ids);
  908.  
  909. // 确保所有信息都已获取到
  910. if (Object.keys(infos).length === 0) return;
  911.  
  912. // 准备显示文本
  913. const texts = tlist.map((x) => {
  914. const info = infos[x.activityId];
  915. return info ? `${info.name}(${info.teachers})` : "加载中...";
  916. });
  917.  
  918. // 等待作业元素显示
  919. const timeout = 5000; // 5秒超时
  920. const startTime = Date.now();
  921.  
  922. let nodes;
  923. while (Date.now() - startTime < timeout) {
  924. nodes = $x(
  925. '//*[@id="layout-container"]/div[2]/div[2]/div/div[2]/div[1]/div[3]/div[2]/div/div'
  926. );
  927. if (
  928. nodes.length > 0 &&
  929. nodes.some((node) => node.children[0] && node.children[0].innerText)
  930. ) {
  931. break;
  932. }
  933. await sleep(100);
  934. }
  935.  
  936. // 更新课程信息显示
  937. for (let i = 0; i < Math.min(nodes.length, texts.length); i++) {
  938. if (nodes[i] && nodes[i].children[1]) {
  939. if (nodes[i].children[1].children.length === 0) {
  940. const p = document.createElement("div");
  941. const t = document.createTextNode(texts[i]);
  942. p.appendChild(t);
  943. p.style.color = "#0066cc";
  944. nodes[i].children[1].insertAdjacentElement("afterbegin", p);
  945. } else {
  946. nodes[i].children[1].children[0].innerHTML = texts[i];
  947. nodes[i].children[1].children[0].style.color = "#0066cc";
  948. }
  949. }
  950. }
  951. }
  952.  
  953. // XPath选择器
  954. function $x(xpath, context = document) {
  955. const iterator = document.evaluate(
  956. xpath,
  957. context,
  958. null,
  959. XPathResult.ANY_TYPE,
  960. null
  961. );
  962. const results = [];
  963. let item;
  964. while ((item = iterator.iterateNext())) {
  965. results.push(item);
  966. }
  967. return results;
  968. }
  969.  
  970. // 本地存储
  971. function set(k, v) {
  972. const h = JSON.parse(localStorage.getItem("zzxw") || "{}");
  973. h[k] = v;
  974. localStorage.setItem("zzxw", JSON.stringify(h));
  975. }
  976.  
  977. function get(k) {
  978. const h = JSON.parse(localStorage.getItem("zzxw") || "{}");
  979. return h[k];
  980. }
  981.  
  982. // 插入课程信息
  983. function insert(x) {
  984. if (!x) return;
  985. if (
  986. $x(
  987. "/html/body/div[1]/div/div[2]/div[2]/div/div/div[2]/div/div[2]/div[1]/div/div/div[1]/div/p"
  988. ).length > 2
  989. )
  990. return;
  991. const d = $x(
  992. "/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]"
  993. );
  994. if (!d.length) {
  995. setTimeout(() => insert(x), 50);
  996. return;
  997. }
  998. // 检查是否已经插入过
  999. const existingText = Array.from(d[0].parentNode.childNodes).some(
  1000. (node) => node.textContent && node.textContent.includes(x.name)
  1001. );
  1002.  
  1003. if (!existingText) {
  1004. const p = document.createElement("p");
  1005. const t = document.createTextNode(x.name + "(" + x.teachers + ")");
  1006. p.appendChild(t);
  1007. d[0].after(p);
  1008. }
  1009. }
  1010.  
  1011. // 辅助函数
  1012. function sleep(n) {
  1013. return new Promise((res) => setTimeout(res, n));
  1014. }
  1015.  
  1016. async function wait(func) {
  1017. let r = func();
  1018. if (r instanceof Promise) r = await r;
  1019. if (r) return r;
  1020. await sleep(50);
  1021. return await wait(func);
  1022. }
  1023.  
  1024. async function waitChange(func, value) {
  1025. const r = value;
  1026. while (1) {
  1027. let t = func();
  1028. if (t instanceof Promise) t = await t;
  1029. if (t != r) return t;
  1030. await sleep(50);
  1031. }
  1032. }
  1033.  
  1034. // 预览URL相关
  1035. async function getPreviewURL(storageId) {
  1036. const res = await fetch(
  1037. "https://apiucloud.bupt.edu.cn/blade-source/resource/preview-url?resourceId=" +
  1038. storageId
  1039. );
  1040. const json = await res.json();
  1041. onlinePreview = json.data.onlinePreview;
  1042. return json.data.previewUrl;
  1043. }
  1044.  
  1045. // 启用文本选择 修改按钮尺寸
  1046. function addFunctionalCSS() {
  1047. GM_addStyle(`
  1048. .teacher-home-page .home-left-container .in-progress-section .in-progress-body .in-progress-item .activity-box .activity-title {
  1049. height: auto !important;
  1050. }
  1051. `);
  1052. if (settings.enableTextSelection) {
  1053. GM_addStyle(`
  1054. .el-checkbox, .el-checkbox-button__inner, .el-empty__image img, .el-radio,
  1055. div, span, p, a, h1, h2, h3, h4, h5, h6, li, td, th {
  1056. -webkit-user-select: auto !important;
  1057. -moz-user-select: auto !important;
  1058. -ms-user-select: auto !important;
  1059. user-select: auto !important;
  1060. }
  1061. `);
  1062. document.addEventListener(
  1063. "copy",
  1064. function (e) {
  1065. e.stopImmediatePropagation();
  1066. },
  1067. true
  1068. );
  1069.  
  1070. document.addEventListener(
  1071. "selectstart",
  1072. function (e) {
  1073. e.stopImmediatePropagation();
  1074. },
  1075. true
  1076. );
  1077. }
  1078. if (settings.useBiggerButton) {
  1079. GM_addStyle(`
  1080. .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 {
  1081. width: 60px !important;
  1082. height: 30px !important;
  1083. background: #f2f2f2 !important;
  1084. line-height: auto !important;
  1085. }
  1086. .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 {
  1087. font-size: 22px !important;
  1088. }
  1089. `);
  1090. }
  1091. }
  1092.  
  1093. // 主函数
  1094. async function main() {
  1095. "use strict";
  1096. // ticket跳转
  1097. if (new URLSearchParams(location.search).get("ticket")?.length) {
  1098. setTimeout(() => {
  1099. location.href = "https://ucloud.bupt.edu.cn/uclass/#/student/homePage";
  1100. }, 500);
  1101. return;
  1102. }
  1103.  
  1104. // 课件预览页面
  1105. if (
  1106. location.href.startsWith(
  1107. "https://ucloud.bupt.edu.cn/uclass/course.html#/resourceLearn"
  1108. )
  1109. ) {
  1110. if (settings.autoClosePopup) {
  1111. const dialogBox = document.querySelector("div.el-message-box__wrapper");
  1112.  
  1113. if (
  1114. dialogBox &&
  1115. window.getComputedStyle(dialogBox).display !== "none"
  1116. ) {
  1117. const messageElement = dialogBox.querySelector(
  1118. ".el-message-box__message p"
  1119. );
  1120. if (
  1121. messageElement &&
  1122. (messageElement.textContent.includes("您正在学习其他课件") ||
  1123. messageElement.textContent.includes("您已经在学习此课件了"))
  1124. ) {
  1125. const confirmButton = dialogBox.querySelector(
  1126. ".el-button--primary"
  1127. );
  1128. if (confirmButton) {
  1129. confirmButton.click();
  1130. } else {
  1131. console.log("未找到确认按钮");
  1132. }
  1133. }
  1134. }
  1135. }
  1136. if (settings.hideTimer) {
  1137. GM_addStyle(`
  1138. .preview-container .time {
  1139. display: none !important;
  1140. }
  1141. `);
  1142. }
  1143. }
  1144.  
  1145. // 作业详情页面
  1146. if (
  1147. location.href.startsWith(
  1148. "https://ucloud.bupt.edu.cn/uclass/course.html#/student/assignmentDetails_fullpage"
  1149. )
  1150. ) {
  1151. const q = new URLSearchParams(location.href);
  1152. const id = q.get("assignmentId");
  1153. const r = get(id);
  1154. const [userid, token] = getToken();
  1155.  
  1156. // 显示相关课程信息
  1157. if (r) {
  1158. insert(r);
  1159. } else {
  1160. const title = q.get("assignmentTitle");
  1161. if (!id || !title) return;
  1162. try {
  1163. const courseInfo = await searchCourse(userid, id, title, token);
  1164. if (courseInfo) {
  1165. insert(courseInfo);
  1166. set(id, courseInfo);
  1167. }
  1168. } catch (e) {
  1169. console.error("获取课程信息失败", e);
  1170. }
  1171. }
  1172.  
  1173. // 处理资源预览和下载
  1174. try {
  1175. const detail = (await getDetail(id)).data;
  1176. if (!detail || !detail.assignmentResource) return;
  1177.  
  1178. const filenames = detail.assignmentResource.map((x) => x.resourceName);
  1179. const urls = await Promise.all(
  1180. detail.assignmentResource.map((x) => {
  1181. return getPreviewURL(x.resourceId);
  1182. })
  1183. );
  1184.  
  1185. await wait(
  1186. () =>
  1187. $x('//*[@id="assignment-info"]/div[2]/div[2]/div[2]/div').length > 0
  1188. );
  1189.  
  1190. $x('//*[@id="assignment-info"]/div[2]/div[2]/div[2]/div').forEach(
  1191. (x, index) => {
  1192. if (
  1193. x.querySelector(".by-icon-eye-grey") ||
  1194. x.querySelector(".by-icon-yundown-grey")
  1195. ) {
  1196. x.querySelector(".by-icon-eye-grey").remove();
  1197. x.querySelector(".by-icon-yundown-grey").remove();
  1198. }
  1199.  
  1200. // 添加预览按钮
  1201. const i = document.createElement("i");
  1202. i.title = "预览";
  1203. i.classList.add("by-icon-eye-grey");
  1204. i.addEventListener("click", () => {
  1205. const url = urls[index];
  1206. const filename = filenames[index];
  1207. if (settings.autoDownload) {
  1208. downloadFile(url, filename);
  1209. console.log("Autodownload");
  1210. }
  1211. if (
  1212. filename.endsWith(".xls") ||
  1213. filename.endsWith(".xlsx") ||
  1214. url.endsWith(".doc") ||
  1215. url.endsWith(".docx") ||
  1216. url.endsWith(".ppt") ||
  1217. url.endsWith(".pptx")
  1218. )
  1219. openTab(
  1220. "https://view.officeapps.live.com/op/view.aspx?src=" +
  1221. encodeURIComponent(url),
  1222. { active: true, insert: true }
  1223. );
  1224. else if (onlinePreview !== null)
  1225. openTab(onlinePreview + encodeURIComponent(url), {
  1226. active: true,
  1227. insert: true,
  1228. });
  1229. });
  1230.  
  1231. // 添加下载按钮
  1232. const i2 = document.createElement("i");
  1233. i2.title = "下载";
  1234. i2.classList.add("by-icon-yundown-grey");
  1235. i2.addEventListener("click", () => {
  1236. downloadFile(urls[index], filenames[index]);
  1237. });
  1238.  
  1239. // 插入按钮
  1240. if (x.children.length >= 3) {
  1241. x.children[3]?.remove();
  1242. x.children[2]?.insertAdjacentElement("afterend", i);
  1243. x.children[2]?.remove();
  1244. x.children[1]?.insertAdjacentElement("afterend", i2);
  1245. } else {
  1246. x.appendChild(i2);
  1247. x.appendChild(i);
  1248. }
  1249. }
  1250. );
  1251. } catch (e) {
  1252. console.error("处理资源失败", e);
  1253. }
  1254. }
  1255.  
  1256. // 主页面
  1257. else if (
  1258. location.href.startsWith(
  1259. "https://ucloud.bupt.edu.cn/uclass/#/student/homePage"
  1260. ) ||
  1261. location.href.startsWith(
  1262. "https://ucloud.bupt.edu.cn/uclass/index.html#/student/homePage"
  1263. )
  1264. ) {
  1265. try {
  1266. // 未完成任务列表
  1267. const list = glist || (await getUndoneList()).data.undoneList;
  1268. if (!list || !Array.isArray(list)) return;
  1269. glist = list;
  1270.  
  1271. const observer = new MutationObserver(async (mutations) => {
  1272. // 当前页码
  1273. const pageElement = document.querySelector(
  1274. "#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"
  1275. );
  1276.  
  1277. if (!pageElement) return;
  1278.  
  1279. // 解析页码
  1280. const currentPage = parseInt(
  1281. pageElement.innerHTML.trim().split("/")[0]
  1282. );
  1283. if (isNaN(currentPage)) return;
  1284.  
  1285. // 页码变化则更新显示
  1286. if (currentPage !== gpage) {
  1287. gpage = currentPage;
  1288. await updateAssignmentDisplay(list, currentPage);
  1289. }
  1290. });
  1291.  
  1292. observer.observe(document.body, {
  1293. childList: true,
  1294. subtree: true,
  1295. attributes: false,
  1296. characterData: true,
  1297. });
  1298.  
  1299. // 初始化页码
  1300. let page = 1;
  1301. const pageElement = document.querySelector(
  1302. "#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"
  1303. );
  1304.  
  1305. if (pageElement) {
  1306. page = parseInt(pageElement.innerHTML.trim().split("/")[0]);
  1307. gpage = page;
  1308. }
  1309.  
  1310. // 更新作业显示
  1311. await updateAssignmentDisplay(list, page);
  1312.  
  1313. // 本学期课程点击事件
  1314. document.querySelectorAll('div[class="header-label"]').forEach((el) => {
  1315. if (el.textContent.includes("本学期课程")) {
  1316. el.style.cursor = "pointer";
  1317. el.addEventListener("click", (e) => {
  1318. e.preventDefault();
  1319. window.location.href =
  1320. "https://ucloud.bupt.edu.cn/uclass/index.html#/student/myCourse";
  1321. });
  1322. }
  1323. });
  1324. } catch (e) {
  1325. console.error("主页处理失败", e);
  1326. }
  1327. }
  1328.  
  1329. // 课程主页
  1330. else if (
  1331. location.href.startsWith(
  1332. "https://ucloud.bupt.edu.cn/uclass/course.html#/student/courseHomePage"
  1333. )
  1334. ) {
  1335. try {
  1336. const site = JSON.parse(localStorage.getItem("site"));
  1337. if (!site || !site.id) return;
  1338.  
  1339. const id = site.id;
  1340. const resources = await getSiteResource(id);
  1341.  
  1342. // 添加下载按钮到每个资源
  1343. const resourceItems = $x(
  1344. '//div[@class="resource-item"]/div[@class="right"]'
  1345. );
  1346. const previewItems = $x(
  1347. '//div[@class="resource-item"]/div[@class="left"]'
  1348. );
  1349.  
  1350. if (resourceItems.length > 0) {
  1351. resourceItems.forEach((x, index) => {
  1352. if (index >= resources.length) return;
  1353.  
  1354. if (settings.autoDownload) {
  1355. previewItems[index].addEventListener(
  1356. "click",
  1357. async (e) => {
  1358. const url = await getPreviewURL(resources[index].id);
  1359. downloadFile(url, resources[index].name);
  1360. console.log("Autodownload");
  1361. },
  1362. false
  1363. );
  1364. }
  1365.  
  1366. const i = document.createElement("i");
  1367. i.title = "下载";
  1368. i.classList.add("by-icon-download");
  1369. i.classList.add("btn-icon");
  1370. i.classList.add("visible");
  1371. i.style.cssText = `
  1372. display: inline-block !important;
  1373. visibility: visible !important;
  1374. cursor: pointer !important;
  1375. `;
  1376.  
  1377. // 获取data-v属性
  1378. const dataAttr = Array.from(x.attributes).find((attr) =>
  1379. attr.localName.startsWith("data-v")
  1380. );
  1381. if (dataAttr) {
  1382. i.setAttribute(dataAttr.localName, "");
  1383. }
  1384.  
  1385. i.addEventListener(
  1386. "click",
  1387. async (e) => {
  1388. e.stopPropagation();
  1389. const url = await getPreviewURL(resources[index].id);
  1390. downloadFile(url, resources[index].name);
  1391. },
  1392. false
  1393. );
  1394.  
  1395. if (x.children.length) x.children[0].remove();
  1396. x.insertAdjacentElement("afterbegin", i);
  1397. });
  1398.  
  1399. // "下载全部"按钮
  1400. if (
  1401. !document.getElementById("downloadAllButton") &&
  1402. resources.length > 0
  1403. ) {
  1404. const downloadAllButton = `<div style="display: flex;flex-direction: row;justify-content: end;margin-right: 24px;margin-top: 20px;">
  1405. <button type="button" class="el-button submit-btn el-button--primary" id="downloadAllButton">
  1406. 下载全部
  1407. </button>
  1408. </div>`;
  1409.  
  1410. const resourceList = $x(
  1411. "/html/body/div/div/div[2]/div[2]/div/div/div"
  1412. );
  1413. if (resourceList.length > 0) {
  1414. const containerElement = document.createElement("div");
  1415. containerElement.innerHTML = downloadAllButton;
  1416. resourceList[0].before(containerElement);
  1417.  
  1418. document.getElementById("downloadAllButton").onclick =
  1419. async () => {
  1420. downloading = !downloading;
  1421. if (downloading) {
  1422. document.getElementById("downloadAllButton").innerHTML =
  1423. "取消下载";
  1424. for (let file of resources) {
  1425. if (!downloading) return;
  1426. await downloadFile(
  1427. await getPreviewURL(file.id),
  1428. file.name
  1429. );
  1430. }
  1431. // 下载完成后重置按钮
  1432. if (downloading) {
  1433. downloading = false;
  1434. document.getElementById("downloadAllButton").innerHTML =
  1435. "下载全部";
  1436. }
  1437. } else {
  1438. document.getElementById("downloadAllButton").innerHTML =
  1439. "下载全部";
  1440. }
  1441. };
  1442. }
  1443. }
  1444. }
  1445. } catch (e) {
  1446. console.error("课程主页处理失败", e);
  1447. }
  1448. }
  1449. }
  1450. })();