ucloud-Evolved

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

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

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