ucloud-Evolved

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

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

  1. // ==UserScript==
  2. // @name ucloud-Evolved
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.24
  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. settings.autoUpdate = document.getElementById("autoUpdate").checked;
  560.  
  561. GM_setValue("autoDownload", settings.autoDownload);
  562. GM_setValue("autoSwitchOffice", settings.autoSwitchOffice);
  563. GM_setValue("autoClosePopup", settings.autoClosePopup);
  564. GM_setValue("hideTimer", settings.hideTimer);
  565. GM_setValue("unlockCopy", settings.unlockCopy);
  566. GM_setValue("showMoreNotification", settings.showMoreNotification);
  567. GM_setValue("useBiggerButton", settings.useBiggerButton);
  568. GM_setValue("autoUpdate", settings.autoUpdate);
  569.  
  570. settingsPanel.classList.remove("visible");
  571. setTimeout(() => {
  572. settingsPanel.style.display = "none";
  573. showNotification("设置已保存", "刷新页面后生效");
  574. }, 300);
  575. });
  576.  
  577. // 通知函数
  578. function showNotification(title, message) {
  579. const notification = document.createElement("div");
  580. notification.style.cssText = `
  581. position: fixed;
  582. bottom: 80px;
  583. right: 20px;
  584. background: #4CAF50;
  585. color: white;
  586. padding: 15px 20px;
  587. border-radius: 8px;
  588. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  589. z-index: 10000;
  590. font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
  591. max-width: 300px;
  592. opacity: 0;
  593. transform: translateY(-10px);
  594. transition: all 0.3s ease;
  595. `;
  596.  
  597. notification.innerHTML = `
  598. <div style="font-weight: bold; margin-bottom: 5px;">${title}</div>
  599. <div style="font-size: 14px;">${message}</div>
  600. `;
  601.  
  602. document.body.appendChild(notification);
  603.  
  604. void notification.offsetWidth;
  605.  
  606. notification.style.opacity = "1";
  607. notification.style.transform = "translateY(0)";
  608.  
  609. setTimeout(() => {
  610. notification.style.opacity = "0";
  611. notification.style.transform = "translateY(-10px)";
  612. setTimeout(() => {
  613. document.body.removeChild(notification);
  614. }, 300);
  615. }, 3000);
  616. }
  617. }
  618. // 获取Token
  619. function getToken() {
  620. const cookieMap = new Map();
  621. document.cookie.split("; ").forEach((cookie) => {
  622. const [key, value] = cookie.split("=");
  623. cookieMap.set(key, value);
  624. });
  625. const token = cookieMap.get("iClass-token");
  626. const userid = cookieMap.get("iClass-uuid");
  627. return [userid, token];
  628. }
  629.  
  630. // 文件下载相关函数
  631. async function downloadFile(url, filename) {
  632. console.log("Call download");
  633. downloading = true;
  634. await jsp;
  635. NProgress.configure({ trickle: false, speed: 0 });
  636. try {
  637. const response = await fetch(url);
  638.  
  639. if (!response.ok) {
  640. throw new Error(`HTTP error! status: ${response.status}`);
  641. }
  642.  
  643. const contentLength = response.headers.get("content-length");
  644. if (!contentLength) {
  645. throw new Error("Content-Length response header unavailable");
  646. }
  647.  
  648. const total = parseInt(contentLength, 10);
  649. sumBytes += total;
  650. const reader = response.body.getReader();
  651. const chunks = [];
  652. while (true) {
  653. const { done, value } = await reader.read();
  654. if (done) break;
  655. if (!downloading) {
  656. NProgress.done();
  657. return;
  658. }
  659. chunks.push(value);
  660. loadedBytes += value.length;
  661. NProgress.set(loadedBytes / sumBytes);
  662. }
  663. NProgress.done();
  664. sumBytes -= total;
  665. loadedBytes -= total;
  666. const blob = new Blob(chunks);
  667. const downloadUrl = window.URL.createObjectURL(blob);
  668. const a = document.createElement("a");
  669. a.href = downloadUrl;
  670. a.download = filename;
  671. document.body.appendChild(a);
  672. a.click();
  673. window.URL.revokeObjectURL(downloadUrl);
  674. } catch (error) {
  675. console.error("Download failed:", error);
  676. }
  677. }
  678.  
  679. // 任务搜索函数
  680. async function searchTask(siteId, keyword, token) {
  681. const res = await fetch(
  682. "https://apiucloud.bupt.edu.cn/ykt-site/work/student/list",
  683. {
  684. headers: {
  685. authorization: "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
  686. "blade-auth": token,
  687. "content-type": "application/json;charset=UTF-8",
  688. },
  689. body: JSON.stringify({
  690. siteId,
  691. keyword,
  692. current: 1,
  693. size: 5,
  694. }),
  695. method: "POST",
  696. }
  697. );
  698. const json = await res.json();
  699. return json;
  700. }
  701.  
  702. // 课程搜索函数
  703. async function searchCourse(userId, id, keyword, token) {
  704. const res = await fetch(
  705. "https://apiucloud.bupt.edu.cn/ykt-site/site/list/student/current?size=999999&current=1&userId=" +
  706. userId +
  707. "&siteRoleCode=2",
  708. {
  709. headers: {
  710. authorization: "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
  711. "blade-auth": token,
  712. },
  713. body: null,
  714. method: "GET",
  715. }
  716. );
  717. const json = await res.json();
  718. const list = json.data.records.map((x) => ({
  719. id: x.id,
  720. name: x.siteName,
  721. teachers: x.teachers.map((y) => y.name).join(", "),
  722. }));
  723.  
  724. async function searchWithLimit(list, id, keyword, token, limit = 5) {
  725. for (let i = 0; i < list.length; i += limit) {
  726. const batch = list.slice(i, i + limit);
  727. const jobs = batch.map((x) => searchTask(x.id, keyword, token));
  728. const ress = await Promise.all(jobs);
  729. for (let j = 0; j < ress.length; j++) {
  730. const res = ress[j];
  731. if (res.data && res.data.records && res.data.records.length > 0) {
  732. for (const item of res.data.records) {
  733. if (item.id == id) {
  734. return batch[j];
  735. }
  736. }
  737. }
  738. }
  739. }
  740. return null;
  741. }
  742. return await searchWithLimit(list, id, keyword, token);
  743. }
  744.  
  745. // 获取任务列表
  746. async function getTasks(siteId, token) {
  747. const res = await fetch(
  748. "https://apiucloud.bupt.edu.cn/ykt-site/work/student/list",
  749. {
  750. headers: {
  751. authorization: "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
  752. "blade-auth": token,
  753. "content-type": "application/json;charset=UTF-8",
  754. },
  755. body: JSON.stringify({
  756. siteId,
  757. current: 1,
  758. size: 9999,
  759. }),
  760. method: "POST",
  761. }
  762. );
  763. const json = await res.json();
  764. return json;
  765. }
  766.  
  767. // 搜索课程
  768. async function searchCourses(nids) {
  769. const result = {};
  770. let ids = [];
  771. for (let id of nids) {
  772. const r = get(id);
  773. if (r) result[id] = r;
  774. else ids.push(id);
  775. }
  776.  
  777. if (ids.length == 0) return result;
  778. const [userid, token] = getToken();
  779. const res = await fetch(
  780. "https://apiucloud.bupt.edu.cn/ykt-site/site/list/student/current?size=999999&current=1&userId=" +
  781. userid +
  782. "&siteRoleCode=2",
  783. {
  784. headers: {
  785. authorization: "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
  786. "blade-auth": token,
  787. },
  788. body: null,
  789. method: "GET",
  790. }
  791. );
  792. const json = await res.json();
  793. const list = json.data.records.map((x) => ({
  794. id: x.id,
  795. name: x.siteName,
  796. teachers: x.teachers.map((y) => y.name).join(", "),
  797. }));
  798. const hashMap = new Map();
  799. let count = ids.length;
  800. for (let i = 0; i < ids.length; i++) {
  801. hashMap.set(ids[i], i);
  802. }
  803.  
  804. async function searchWithLimit(list, limit = 5) {
  805. for (let i = 0; i < list.length; i += limit) {
  806. const batch = list.slice(i, i + limit);
  807. const jobs = batch.map((x) => getTasks(x.id, token));
  808. const ress = await Promise.all(jobs);
  809. for (let j = 0; j < ress.length; j++) {
  810. const res = ress[j];
  811. if (res.data && res.data.records && res.data.records.length > 0) {
  812. for (const item of res.data.records) {
  813. if (hashMap.has(item.id)) {
  814. result[item.id] = batch[j];
  815. set(item.id, batch[j]);
  816. if (--count == 0) {
  817. return result;
  818. }
  819. }
  820. }
  821. }
  822. }
  823. }
  824. return result;
  825. }
  826. return await searchWithLimit(list);
  827. }
  828.  
  829. // 获取未完成列表
  830. async function getUndoneList() {
  831. const [userid, token] = getToken();
  832. const res = await fetch(
  833. "https://apiucloud.bupt.edu.cn/ykt-site/site/student/undone?userId=" +
  834. userid,
  835. {
  836. headers: {
  837. authorization: "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
  838. "blade-auth": token,
  839. },
  840. method: "GET",
  841. }
  842. );
  843. const json = await res.json();
  844. return json;
  845. }
  846.  
  847. // 获取详情
  848. async function getDetail(id) {
  849. const [userid, token] = getToken();
  850. const res = await fetch(
  851. "https://apiucloud.bupt.edu.cn/ykt-site/work/detail?assignmentId=" + id,
  852. {
  853. headers: {
  854. authorization: "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
  855. "blade-auth": token,
  856. },
  857. body: null,
  858. method: "GET",
  859. }
  860. );
  861. const json = await res.json();
  862. return json;
  863. }
  864.  
  865. // 获取站点资源
  866. async function getSiteResource(id) {
  867. const [userid, token] = getToken();
  868. const res = await fetch(
  869. "https://apiucloud.bupt.edu.cn/ykt-site/site-resource/tree/student?siteId=" +
  870. id +
  871. "&userId=" +
  872. userid,
  873. {
  874. headers: {
  875. authorization: "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
  876. "blade-auth": token,
  877. },
  878. body: null,
  879. method: "POST",
  880. }
  881. );
  882. const json = await res.json();
  883. const result = [];
  884. function foreach(data) {
  885. if (!data || !Array.isArray(data)) return;
  886. data.forEach((x) => {
  887. if (x.attachmentVOs && Array.isArray(x.attachmentVOs)) {
  888. x.attachmentVOs.forEach((y) => {
  889. if (y.type !== 2 && y.resource) result.push(y.resource);
  890. });
  891. }
  892. if (x.children) foreach(x.children);
  893. });
  894. }
  895. foreach(json.data);
  896. return result;
  897. }
  898.  
  899. // 更新作业显示
  900. async function updateAssignmentDisplay(list, page) {
  901. if (!list || list.length === 0) return;
  902.  
  903. // 获取当前页的作业
  904. const tlist = list.slice((page - 1) * 6, page * 6);
  905. if (tlist.length === 0) return;
  906.  
  907. // 获取课程信息
  908. const ids = tlist.map((x) => x.activityId);
  909. const infos = await searchCourses(ids);
  910.  
  911. // 确保所有信息都已获取到
  912. if (Object.keys(infos).length === 0) return;
  913.  
  914. // 准备显示文本
  915. const texts = tlist.map((x) => {
  916. const info = infos[x.activityId];
  917. return info ? `${info.name}(${info.teachers})` : "加载中...";
  918. });
  919.  
  920. // 等待作业元素显示
  921. const timeout = 5000; // 5秒超时
  922. const startTime = Date.now();
  923.  
  924. let nodes;
  925. while (Date.now() - startTime < timeout) {
  926. nodes = $x(
  927. '//*[@id="layout-container"]/div[2]/div[2]/div/div[2]/div[1]/div[3]/div[2]/div/div'
  928. );
  929. if (
  930. nodes.length > 0 &&
  931. nodes.some((node) => node.children[0] && node.children[0].innerText)
  932. ) {
  933. break;
  934. }
  935. await sleep(100);
  936. }
  937.  
  938. // 更新课程信息显示
  939. for (let i = 0; i < Math.min(nodes.length, texts.length); i++) {
  940. if (nodes[i] && nodes[i].children[1]) {
  941. if (nodes[i].children[1].children.length === 0) {
  942. const p = document.createElement("div");
  943. const t = document.createTextNode(texts[i]);
  944. p.appendChild(t);
  945. p.style.color = "#0066cc";
  946. nodes[i].children[1].insertAdjacentElement("afterbegin", p);
  947. } else {
  948. nodes[i].children[1].children[0].innerHTML = texts[i];
  949. nodes[i].children[1].children[0].style.color = "#0066cc";
  950. }
  951. }
  952. }
  953. }
  954.  
  955. // XPath选择器
  956. function $x(xpath, context = document) {
  957. const iterator = document.evaluate(
  958. xpath,
  959. context,
  960. null,
  961. XPathResult.ANY_TYPE,
  962. null
  963. );
  964. const results = [];
  965. let item;
  966. while ((item = iterator.iterateNext())) {
  967. results.push(item);
  968. }
  969. return results;
  970. }
  971.  
  972. // 本地存储
  973. function set(k, v) {
  974. const h = JSON.parse(localStorage.getItem("zzxw") || "{}");
  975. h[k] = v;
  976. localStorage.setItem("zzxw", JSON.stringify(h));
  977. }
  978.  
  979. function get(k) {
  980. const h = JSON.parse(localStorage.getItem("zzxw") || "{}");
  981. return h[k];
  982. }
  983.  
  984. // 插入课程信息
  985. function insert(x) {
  986. if (!x) return;
  987. if (
  988. $x(
  989. "/html/body/div[1]/div/div[2]/div[2]/div/div/div[2]/div/div[2]/div[1]/div/div/div[1]/div/p"
  990. ).length > 2
  991. )
  992. return;
  993. const d = $x(
  994. "/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]"
  995. );
  996. if (!d.length) {
  997. setTimeout(() => insert(x), 50);
  998. return;
  999. }
  1000. // 检查是否已经插入过
  1001. const existingText = Array.from(d[0].parentNode.childNodes).some(
  1002. (node) => node.textContent && node.textContent.includes(x.name)
  1003. );
  1004.  
  1005. if (!existingText) {
  1006. const p = document.createElement("p");
  1007. const t = document.createTextNode(x.name + "(" + x.teachers + ")");
  1008. p.appendChild(t);
  1009. d[0].after(p);
  1010. }
  1011. }
  1012.  
  1013. // 辅助函数
  1014. function sleep(n) {
  1015. return new Promise((res) => setTimeout(res, n));
  1016. }
  1017.  
  1018. async function wait(func) {
  1019. let r = func();
  1020. if (r instanceof Promise) r = await r;
  1021. if (r) return r;
  1022. await sleep(50);
  1023. return await wait(func);
  1024. }
  1025.  
  1026. async function waitChange(func, value) {
  1027. const r = value;
  1028. while (1) {
  1029. let t = func();
  1030. if (t instanceof Promise) t = await t;
  1031. if (t != r) return t;
  1032. await sleep(50);
  1033. }
  1034. }
  1035.  
  1036. // 预览URL相关
  1037. async function getPreviewURL(storageId) {
  1038. const res = await fetch(
  1039. "https://apiucloud.bupt.edu.cn/blade-source/resource/preview-url?resourceId=" +
  1040. storageId
  1041. );
  1042. const json = await res.json();
  1043. onlinePreview = json.data.onlinePreview;
  1044. return json.data.previewUrl;
  1045. }
  1046.  
  1047. // 启用文本选择 修改按钮尺寸
  1048. function addFunctionalCSS() {
  1049. GM_addStyle(`
  1050. .teacher-home-page .home-left-container .in-progress-section .in-progress-body .in-progress-item .activity-box .activity-title {
  1051. height: auto !important;
  1052. }
  1053. `);
  1054. if (settings.enableTextSelection) {
  1055. GM_addStyle(`
  1056. .el-checkbox, .el-checkbox-button__inner, .el-empty__image img, .el-radio,
  1057. div, span, p, a, h1, h2, h3, h4, h5, h6, li, td, th {
  1058. -webkit-user-select: auto !important;
  1059. -moz-user-select: auto !important;
  1060. -ms-user-select: auto !important;
  1061. user-select: auto !important;
  1062. }
  1063. `);
  1064. document.addEventListener(
  1065. "copy",
  1066. function (e) {
  1067. e.stopImmediatePropagation();
  1068. },
  1069. true
  1070. );
  1071.  
  1072. document.addEventListener(
  1073. "selectstart",
  1074. function (e) {
  1075. e.stopImmediatePropagation();
  1076. },
  1077. true
  1078. );
  1079. }
  1080. if (settings.useBiggerButton) {
  1081. GM_addStyle(`
  1082. .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 {
  1083. width: 60px !important;
  1084. height: 30px !important;
  1085. background: #f2f2f2 !important;
  1086. line-height: auto !important;
  1087. }
  1088. .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 {
  1089. font-size: 22px !important;
  1090. }
  1091. `);
  1092. }
  1093. }
  1094.  
  1095. // 主函数
  1096. async function main() {
  1097. "use strict";
  1098. // ticket跳转
  1099. if (new URLSearchParams(location.search).get("ticket")?.length) {
  1100. setTimeout(() => {
  1101. location.href = "https://ucloud.bupt.edu.cn/uclass/#/student/homePage";
  1102. }, 500);
  1103. return;
  1104. }
  1105.  
  1106. // 课件预览页面
  1107. if (
  1108. location.href.startsWith(
  1109. "https://ucloud.bupt.edu.cn/uclass/course.html#/resourceLearn"
  1110. )
  1111. ) {
  1112. if (settings.autoClosePopup) {
  1113. const dialogBox = document.querySelector("div.el-message-box__wrapper");
  1114.  
  1115. if (
  1116. dialogBox &&
  1117. window.getComputedStyle(dialogBox).display !== "none"
  1118. ) {
  1119. const messageElement = dialogBox.querySelector(
  1120. ".el-message-box__message p"
  1121. );
  1122. if (
  1123. messageElement &&
  1124. (messageElement.textContent.includes("您正在学习其他课件") ||
  1125. messageElement.textContent.includes("您已经在学习此课件了"))
  1126. ) {
  1127. const confirmButton = dialogBox.querySelector(
  1128. ".el-button--primary"
  1129. );
  1130. if (confirmButton) {
  1131. confirmButton.click();
  1132. } else {
  1133. console.log("未找到确认按钮");
  1134. }
  1135. }
  1136. }
  1137. }
  1138. if (settings.hideTimer) {
  1139. GM_addStyle(`
  1140. .preview-container .time {
  1141. display: none !important;
  1142. }
  1143. `);
  1144. }
  1145. }
  1146.  
  1147. // 作业详情页面
  1148. if (
  1149. location.href.startsWith(
  1150. "https://ucloud.bupt.edu.cn/uclass/course.html#/student/assignmentDetails_fullpage"
  1151. )
  1152. ) {
  1153. const q = new URLSearchParams(location.href);
  1154. const id = q.get("assignmentId");
  1155. const r = get(id);
  1156. const [userid, token] = getToken();
  1157.  
  1158. // 显示相关课程信息
  1159. if (r) {
  1160. insert(r);
  1161. } else {
  1162. const title = q.get("assignmentTitle");
  1163. if (!id || !title) return;
  1164. try {
  1165. const courseInfo = await searchCourse(userid, id, title, token);
  1166. if (courseInfo) {
  1167. insert(courseInfo);
  1168. set(id, courseInfo);
  1169. }
  1170. } catch (e) {
  1171. console.error("获取课程信息失败", e);
  1172. }
  1173. }
  1174.  
  1175. // 处理资源预览和下载
  1176. try {
  1177. const detail = (await getDetail(id)).data;
  1178. if (!detail || !detail.assignmentResource) return;
  1179.  
  1180. const filenames = detail.assignmentResource.map((x) => x.resourceName);
  1181. const urls = await Promise.all(
  1182. detail.assignmentResource.map((x) => {
  1183. return getPreviewURL(x.resourceId);
  1184. })
  1185. );
  1186.  
  1187. await wait(
  1188. () =>
  1189. $x('//*[@id="assignment-info"]/div[2]/div[2]/div[2]/div').length > 0
  1190. );
  1191.  
  1192. $x('//*[@id="assignment-info"]/div[2]/div[2]/div[2]/div').forEach(
  1193. (x, index) => {
  1194. if (
  1195. x.querySelector(".by-icon-eye-grey") ||
  1196. x.querySelector(".by-icon-yundown-grey")
  1197. ) {
  1198. x.querySelector(".by-icon-eye-grey").remove();
  1199. x.querySelector(".by-icon-yundown-grey").remove();
  1200. }
  1201.  
  1202. // 添加预览按钮
  1203. const i = document.createElement("i");
  1204. i.title = "预览";
  1205. i.classList.add("by-icon-eye-grey");
  1206. i.addEventListener("click", () => {
  1207. const url = urls[index];
  1208. const filename = filenames[index];
  1209. if (settings.autoDownload) {
  1210. downloadFile(url, filename);
  1211. console.log("Autodownload");
  1212. }
  1213. if (
  1214. filename.endsWith(".xls") ||
  1215. filename.endsWith(".xlsx") ||
  1216. url.endsWith(".doc") ||
  1217. url.endsWith(".docx") ||
  1218. url.endsWith(".ppt") ||
  1219. url.endsWith(".pptx")
  1220. )
  1221. openTab(
  1222. "https://view.officeapps.live.com/op/view.aspx?src=" +
  1223. encodeURIComponent(url),
  1224. { active: true, insert: true }
  1225. );
  1226. else if (onlinePreview !== null)
  1227. openTab(onlinePreview + encodeURIComponent(url), {
  1228. active: true,
  1229. insert: true,
  1230. });
  1231. });
  1232.  
  1233. // 添加下载按钮
  1234. const i2 = document.createElement("i");
  1235. i2.title = "下载";
  1236. i2.classList.add("by-icon-yundown-grey");
  1237. i2.addEventListener("click", () => {
  1238. downloadFile(urls[index], filenames[index]);
  1239. });
  1240.  
  1241. // 插入按钮
  1242. if (x.children.length >= 3) {
  1243. x.children[3]?.remove();
  1244. x.children[2]?.insertAdjacentElement("afterend", i);
  1245. x.children[2]?.remove();
  1246. x.children[1]?.insertAdjacentElement("afterend", i2);
  1247. } else {
  1248. x.appendChild(i2);
  1249. x.appendChild(i);
  1250. }
  1251. }
  1252. );
  1253. } catch (e) {
  1254. console.error("处理资源失败", e);
  1255. }
  1256. }
  1257.  
  1258. // 主页面
  1259. else if (
  1260. location.href.startsWith(
  1261. "https://ucloud.bupt.edu.cn/uclass/#/student/homePage"
  1262. ) ||
  1263. location.href.startsWith(
  1264. "https://ucloud.bupt.edu.cn/uclass/index.html#/student/homePage"
  1265. )
  1266. ) {
  1267. try {
  1268. // 未完成任务列表
  1269. const list = glist || (await getUndoneList()).data.undoneList;
  1270. if (!list || !Array.isArray(list)) return;
  1271. glist = list;
  1272.  
  1273. const observer = new MutationObserver(async (mutations) => {
  1274. // 当前页码
  1275. const pageElement = document.querySelector(
  1276. "#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"
  1277. );
  1278.  
  1279. if (!pageElement) return;
  1280.  
  1281. // 解析页码
  1282. const currentPage = parseInt(
  1283. pageElement.innerHTML.trim().split("/")[0]
  1284. );
  1285. if (isNaN(currentPage)) return;
  1286.  
  1287. // 页码变化则更新显示
  1288. if (currentPage !== gpage) {
  1289. gpage = currentPage;
  1290. await updateAssignmentDisplay(list, currentPage);
  1291. }
  1292. });
  1293.  
  1294. observer.observe(document.body, {
  1295. childList: true,
  1296. subtree: true,
  1297. attributes: false,
  1298. characterData: true,
  1299. });
  1300.  
  1301. // 初始化页码
  1302. let page = 1;
  1303. const pageElement = document.querySelector(
  1304. "#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"
  1305. );
  1306.  
  1307. if (pageElement) {
  1308. page = parseInt(pageElement.innerHTML.trim().split("/")[0]);
  1309. gpage = page;
  1310. }
  1311.  
  1312. // 更新作业显示
  1313. await updateAssignmentDisplay(list, page);
  1314.  
  1315. // 本学期课程点击事件
  1316. document.querySelectorAll('div[class="header-label"]').forEach((el) => {
  1317. if (el.textContent.includes("本学期课程")) {
  1318. el.style.cursor = "pointer";
  1319. el.addEventListener("click", (e) => {
  1320. e.preventDefault();
  1321. window.location.href =
  1322. "https://ucloud.bupt.edu.cn/uclass/index.html#/student/myCourse";
  1323. });
  1324. }
  1325. });
  1326. } catch (e) {
  1327. console.error("主页处理失败", e);
  1328. }
  1329. }
  1330.  
  1331. // 课程主页
  1332. else if (
  1333. location.href.startsWith(
  1334. "https://ucloud.bupt.edu.cn/uclass/course.html#/student/courseHomePage"
  1335. )
  1336. ) {
  1337. try {
  1338. const site = JSON.parse(localStorage.getItem("site"));
  1339. if (!site || !site.id) return;
  1340.  
  1341. const id = site.id;
  1342. const resources = await getSiteResource(id);
  1343.  
  1344. // 添加下载按钮到每个资源
  1345. const resourceItems = $x(
  1346. '//div[@class="resource-item"]/div[@class="right"]'
  1347. );
  1348. const previewItems = $x(
  1349. '//div[@class="resource-item"]/div[@class="left"]'
  1350. );
  1351.  
  1352. if (resourceItems.length > 0) {
  1353. resourceItems.forEach((x, index) => {
  1354. if (index >= resources.length) return;
  1355.  
  1356. if (settings.autoDownload) {
  1357. previewItems[index].addEventListener(
  1358. "click",
  1359. async (e) => {
  1360. const url = await getPreviewURL(resources[index].id);
  1361. downloadFile(url, resources[index].name);
  1362. console.log("Autodownload");
  1363. },
  1364. false
  1365. );
  1366. }
  1367.  
  1368. const i = document.createElement("i");
  1369. i.title = "下载";
  1370. i.classList.add("by-icon-download");
  1371. i.classList.add("btn-icon");
  1372. i.classList.add("visible");
  1373. i.style.cssText = `
  1374. display: inline-block !important;
  1375. visibility: visible !important;
  1376. cursor: pointer !important;
  1377. `;
  1378.  
  1379. // 获取data-v属性
  1380. const dataAttr = Array.from(x.attributes).find((attr) =>
  1381. attr.localName.startsWith("data-v")
  1382. );
  1383. if (dataAttr) {
  1384. i.setAttribute(dataAttr.localName, "");
  1385. }
  1386.  
  1387. i.addEventListener(
  1388. "click",
  1389. async (e) => {
  1390. e.stopPropagation();
  1391. const url = await getPreviewURL(resources[index].id);
  1392. downloadFile(url, resources[index].name);
  1393. },
  1394. false
  1395. );
  1396.  
  1397. if (x.children.length) x.children[0].remove();
  1398. x.insertAdjacentElement("afterbegin", i);
  1399. });
  1400.  
  1401. // "下载全部"按钮
  1402. if (
  1403. !document.getElementById("downloadAllButton") &&
  1404. resources.length > 0
  1405. ) {
  1406. const downloadAllButton = `<div style="display: flex;flex-direction: row;justify-content: end;margin-right: 24px;margin-top: 20px;">
  1407. <button type="button" class="el-button submit-btn el-button--primary" id="downloadAllButton">
  1408. 下载全部
  1409. </button>
  1410. </div>`;
  1411.  
  1412. const resourceList = $x(
  1413. "/html/body/div/div/div[2]/div[2]/div/div/div"
  1414. );
  1415. if (resourceList.length > 0) {
  1416. const containerElement = document.createElement("div");
  1417. containerElement.innerHTML = downloadAllButton;
  1418. resourceList[0].before(containerElement);
  1419.  
  1420. document.getElementById("downloadAllButton").onclick =
  1421. async () => {
  1422. downloading = !downloading;
  1423. if (downloading) {
  1424. document.getElementById("downloadAllButton").innerHTML =
  1425. "取消下载";
  1426. for (let file of resources) {
  1427. if (!downloading) return;
  1428. await downloadFile(
  1429. await getPreviewURL(file.id),
  1430. file.name
  1431. );
  1432. }
  1433. // 下载完成后重置按钮
  1434. if (downloading) {
  1435. downloading = false;
  1436. document.getElementById("downloadAllButton").innerHTML =
  1437. "下载全部";
  1438. }
  1439. } else {
  1440. document.getElementById("downloadAllButton").innerHTML =
  1441. "下载全部";
  1442. }
  1443. };
  1444. }
  1445. }
  1446. }
  1447. } catch (e) {
  1448. console.error("课程主页处理失败", e);
  1449. }
  1450. }
  1451. }
  1452. })();