ucloud-Evolved

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

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

  1. // ==UserScript==
  2. // @name ucloud-Evolved
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.28
  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. sortNotificationsByTime: GM_getValue("sortNotificationsByTime", true),
  117. betterNotificationHighlight: GM_getValue(
  118. "betterNotificationHighlight",
  119. true
  120. ),
  121. };
  122.  
  123. // 辅助变量
  124. let jsp;
  125. let sumBytes = 0,
  126. loadedBytes = 0,
  127. downloading = false;
  128. let setClicked = false;
  129. let gpage = -1;
  130. let glist = null;
  131. let onlinePreview = null;
  132.  
  133. // 初始化扩展功能
  134. function initializeExtension() {
  135. // 注册菜单命令
  136. registerMenuCommands();
  137.  
  138. const nprogressCSS = GM_getResourceText("NPROGRESS_CSS");
  139. GM_addStyle(nprogressCSS);
  140.  
  141. if (settings.showConfigButton) {
  142. loadui();
  143. }
  144. addFunctionalCSS();
  145. main();
  146.  
  147. if (settings.autoUpdate) {
  148. checkForUpdates();
  149. }
  150.  
  151. // 监听URL哈希变化
  152. window.addEventListener(
  153. "hashchange",
  154. function () {
  155. main();
  156. },
  157. false
  158. );
  159.  
  160. // 初始加载
  161. main();
  162. }
  163.  
  164. // 注册菜单命令
  165. function registerMenuCommands() {
  166. GM_registerMenuCommand(
  167. (settings.showConfigButton ? "✅" : "❌") +
  168. "显示配置按钮:" +
  169. (settings.showConfigButton ? "已启用" : "已禁用"),
  170. () => {
  171. settings.showConfigButton = !settings.showConfigButton;
  172. GM_setValue("showConfigButton", settings.showConfigButton);
  173. location.reload();
  174. }
  175. );
  176. GM_registerMenuCommand(
  177. (settings.autoDownload ? "✅" : "❌") +
  178. "预览课件时自动下载:" +
  179. (settings.autoDownload ? "已启用" : "已禁用"),
  180. () => {
  181. settings.autoDownload = !settings.autoDownload;
  182. GM_setValue("autoDownload", settings.autoDownload);
  183. location.reload();
  184. }
  185. );
  186.  
  187. GM_registerMenuCommand(
  188. (settings.autoSwitchOffice ? "✅" : "❌") +
  189. "使用 Office365 预览课件:" +
  190. (settings.autoSwitchOffice ? "已启用" : "已禁用"),
  191. () => {
  192. settings.autoSwitchOffice = !settings.autoSwitchOffice;
  193. GM_setValue("autoSwitchOffice", settings.autoSwitchOffice);
  194. location.reload();
  195. }
  196. );
  197. }
  198. /**
  199. * 通用标签页打开函数
  200. * @param {string} url - 要打开的URL
  201. * @param {Object} options - 选项参数
  202. * @param {boolean} [options.active=true] - 新标签页是否获得焦点
  203. * @param {boolean} [options.insert=true] - 是否在当前标签页旁边插入新标签页
  204. * @param {boolean} [options.setParent=true] - 新标签页是否将当前标签页设为父页面
  205. * @param {string} [options.windowName="_blank"] - window.open的窗口名称
  206. * @param {string} [options.windowFeatures=""] - window.open的窗口特性
  207. * @returns {Object|Window|null} 打开的标签页对象
  208. */
  209. function openTab(url, options = {}) {
  210. const defaultOptions = {
  211. active: true,
  212. insert: true,
  213. setParent: true,
  214. windowName: "_blank",
  215. windowFeatures: "",
  216. };
  217. const finalOptions = { ...defaultOptions, ...options };
  218. if (typeof GM_openInTab === "function") {
  219. try {
  220. return GM_openInTab(url, {
  221. active: finalOptions.active,
  222. insert: finalOptions.insert,
  223. setParent: finalOptions.setParent,
  224. });
  225. } catch (error) {
  226. return window.open(
  227. url,
  228. finalOptions.windowName,
  229. finalOptions.windowFeatures
  230. );
  231. }
  232. }
  233. }
  234. function showUpdateNotification(newVersion) {
  235. const notification = document.createElement("div");
  236. notification.style.cssText = `
  237. position: fixed;
  238. bottom: 80px;
  239. right: 20px;
  240. background: #4a6cf7;
  241. color: white;
  242. padding: 15px 20px;
  243. border-radius: 8px;
  244. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  245. z-index: 10000;
  246. font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
  247. max-width: 300px;
  248. `;
  249.  
  250. notification.innerHTML = `
  251. <div style="font-weight: bold; margin-bottom: 5px;">发现新版本 v${newVersion}</div>
  252. <div style="font-size: 14px; margin-bottom: 10px;">当前版本 v${GM_info.script.version}</div>
  253. <button id="updateNow" style="background: white; color: #4a6cf7; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; margin-right: 10px;">立即更新</button>
  254. <button id="updateLater" style="background: transparent; color: white; border: 1px solid white; padding: 5px 10px; border-radius: 4px; cursor: pointer;">稍后提醒</button>
  255. `;
  256.  
  257. document.body.appendChild(notification);
  258.  
  259. document.getElementById("updateNow").addEventListener("click", function () {
  260. openTab(GM_info.script.downloadURL, { active: true });
  261. document.body.removeChild(notification);
  262. });
  263.  
  264. document
  265. .getElementById("updateLater")
  266. .addEventListener("click", function () {
  267. document.body.removeChild(notification);
  268. });
  269. }
  270.  
  271. function checkForUpdates() {
  272. const lastCheckTime = GM_getValue("lastUpdateCheck", 0);
  273. const now = Date.now();
  274. const ONE_DAY = 24 * 60 * 60 * 1000; // 一天的毫秒数
  275.  
  276. if (now - lastCheckTime > ONE_DAY) {
  277. GM_setValue("lastUpdateCheck", now);
  278. GM_xmlhttpRequest({
  279. method: "GET",
  280. url: GM_info.script.updateURL,
  281. onload: function (response) {
  282. const versionMatch = response.responseText.match(
  283. /@version\s+(\d+\.\d+)/
  284. );
  285. if (versionMatch && versionMatch[1]) {
  286. const latestVersion = versionMatch[1];
  287. const currentVersion = GM_info.script.version;
  288. if (latestVersion > currentVersion) {
  289. showUpdateNotification(latestVersion);
  290. }
  291. }
  292. },
  293. });
  294. }
  295. }
  296.  
  297. function loadui() {
  298. GM_addStyle(`
  299. #yzHelper-settings {
  300. position: fixed;
  301. bottom: 20px;
  302. right: 20px;
  303. background: #ffffff;
  304. box-shadow: 0 5px 25px rgba(0, 0, 0, 0.15);
  305. border-radius: 12px;
  306. z-index: 9999;
  307. width: 500px;
  308. height: 450px;
  309. font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
  310. transition: all 0.3s ease;
  311. opacity: 0;
  312. transform: translateY(10px);
  313. color: #333;
  314. overflow: hidden;
  315. display: flex;
  316. flex-direction: column;
  317. display: none;
  318. }
  319. #yzHelper-settings.visible {
  320. opacity: 1;
  321. transform: translateY(0);
  322. }
  323. #yzHelper-header {
  324. padding: 15px 20px;
  325. border-bottom: 1px solid #eee;
  326. background-color: #ecb000;
  327. color: white;
  328. font-weight: bold;
  329. font-size: 16px;
  330. display: flex;
  331. justify-content: space-between;
  332. align-items: center;
  333. }
  334. #yzHelper-main {
  335. display: flex;
  336. flex: 1;
  337. overflow: hidden;
  338. }
  339. #yzHelper-settings-sidebar {
  340. width: 140px;
  341. background: #f7f7f7;
  342. padding: 15px 0;
  343. border-right: 1px solid #eee;
  344. overflow-y: auto;
  345. }
  346. #yzHelper-settings-sidebar .menu-item {
  347. padding: 12px 15px;
  348. cursor: pointer;
  349. transition: all 0.2s ease;
  350. font-size: 14px;
  351. color: #666;
  352. display: flex;
  353. align-items: center;
  354. gap: 8px;
  355. }
  356. #yzHelper-settings-sidebar .menu-item:hover {
  357. background: #efefef;
  358. color: #333;
  359. }
  360. #yzHelper-settings-sidebar .menu-item.active {
  361. background: #ffbe00;
  362. color: #fff;
  363. font-weight: 500;
  364. }
  365. #yzHelper-settings-sidebar .emoji {
  366. font-size: 16px;
  367. }
  368. #yzHelper-settings-content {
  369. flex: 1;
  370. padding: 20px;
  371. overflow-y: auto;
  372. position: relative;
  373. padding-bottom: 70px; /* Space for buttons */
  374. }
  375. #yzHelper-settings-content .settings-section {
  376. display: none;
  377. }
  378. #yzHelper-settings-content .settings-section.active {
  379. display: block;
  380. }
  381.  
  382. #section-about .about-content {
  383. line-height: 1.6;
  384. font-size: 14px;
  385. }
  386. #section-about h4 {
  387. margin: 16px 0 8px;
  388. font-size: 15px;
  389. }
  390. #section-about ul {
  391. margin: 8px 0;
  392. padding-left: 20px;
  393. }
  394. #section-about li {
  395. margin-bottom: 4px;
  396. }
  397. #section-about .github-link {
  398. display: inline-flex;
  399. align-items: center;
  400. padding: 6px 12px;
  401. background: #f6f8fa;
  402. border: 1px solid rgba(27, 31, 36, 0.15);
  403. border-radius: 6px;
  404. color: #24292f;
  405. text-decoration: none;
  406. font-weight: 500;
  407. transition: background-color 0.2s;
  408. }
  409. #section-about .github-link:hover {
  410. background-color: #f3f4f6;
  411. }
  412. #section-about .github-icon {
  413. margin-right: 6px;
  414. fill: currentColor;
  415. }
  416. #section-about .feedback-note {
  417. margin-top: 14px;
  418. border-top: 1px solid #eaecef;
  419. padding-top: 14px;
  420. font-size: 13px;
  421. color: #57606a;
  422. }
  423. #section-about code {
  424. background: rgba(175, 184, 193, 0.2);
  425. padding: 0.2em 0.4em;
  426. border-radius: 6px;
  427. font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
  428. font-size: 85%;
  429. }
  430. #yzHelper-settings h3 {
  431. margin-top: 0;
  432. margin-bottom: 15px;
  433. font-size: 18px;
  434. font-weight: 600;
  435. color: #2c3e50;
  436. padding-bottom: 10px;
  437. border-bottom: 1px solid #eee;
  438. }
  439. #yzHelper-settings .setting-item {
  440. margin-bottom: 16px;
  441. }
  442. #yzHelper-settings .setting-toggle {
  443. display: flex;
  444. align-items: center;
  445. }
  446. #yzHelper-settings .setting-item:last-of-type {
  447. margin-bottom: 20px;
  448. }
  449. #yzHelper-settings .switch {
  450. position: relative;
  451. display: inline-block;
  452. width: 44px;
  453. height: 24px;
  454. margin-right: 10px;
  455. }
  456. #yzHelper-settings .switch input {
  457. opacity: 0;
  458. width: 0;
  459. height: 0;
  460. }
  461. #yzHelper-settings .slider {
  462. position: absolute;
  463. cursor: pointer;
  464. top: 0;
  465. left: 0;
  466. right: 0;
  467. bottom: 0;
  468. background-color: #ccc;
  469. transition: .3s;
  470. border-radius: 24px;
  471. }
  472. #yzHelper-settings .slider:before {
  473. position: absolute;
  474. content: "";
  475. height: 18px;
  476. width: 18px;
  477. left: 3px;
  478. bottom: 3px;
  479. background-color: white;
  480. transition: .3s;
  481. border-radius: 50%;
  482. }
  483. #yzHelper-settings input:checked + .slider {
  484. background-color: #ffbe00;
  485. }
  486. #yzHelper-settings input:focus + .slider {
  487. box-shadow: 0 0 1px #ffbe00;
  488. }
  489. #yzHelper-settings input:checked + .slider:before {
  490. transform: translateX(20px);
  491. }
  492. #yzHelper-settings .setting-label {
  493. font-size: 14px;
  494. cursor: pointer;
  495. }
  496. #yzHelper-settings .setting-description {
  497. display: block; /* 始终保持在DOM中 */
  498. margin-left: 54px;
  499. font-size: 12px;
  500. color: #666;
  501. background: #f9f9f9;
  502. border-left: 3px solid #ffbe00;
  503. border-radius: 0 4px 4px 0;
  504. max-height: 0;
  505. overflow: hidden;
  506. opacity: 0;
  507. transition: all 0.3s ease;
  508. padding: 0 12px;
  509. }
  510. #yzHelper-settings .setting-description.visible {
  511. max-height: 100px;
  512. opacity: 1;
  513. margin-top: 8px;
  514. padding: 8px 12px;
  515. }
  516. #yzHelper-settings .buttons {
  517. display: flex;
  518. justify-content: flex-end;
  519. gap: 10px;
  520. position: fixed;
  521. bottom: 0px;
  522. right: 25px;
  523. background: white;
  524. padding: 10px 0;
  525. width: calc(100% - 180px);
  526. border-top: 1px solid #f5f5f5;
  527. box-sizing: border-box;
  528. }
  529. #yzHelper-settings button {
  530. background: #ffbe00;
  531. border: none;
  532. padding: 8px 16px;
  533. border-radius: 6px;
  534. cursor: pointer;
  535. font-weight: 500;
  536. color: #fff;
  537. transition: all 0.2s ease;
  538. outline: none;
  539. font-size: 14px;
  540. }
  541. #yzHelper-settings button:hover {
  542. background: #e9ad00;
  543. }
  544. #yzHelper-settings button.cancel {
  545. background: #f1f1f1;
  546. color: #666;
  547. }
  548. #yzHelper-settings button.cancel:hover {
  549. background: #e5e5e5;
  550. }
  551. #yzHelper-settings-toggle {
  552. position: fixed;
  553. bottom: 20px;
  554. right: 20px;
  555. background: #ffbe00;
  556. color: #fff;
  557. width: 50px;
  558. height: 50px;
  559. border-radius: 50%;
  560. display: flex;
  561. align-items: center;
  562. justify-content: center;
  563. font-size: 24px;
  564. cursor: pointer;
  565. z-index: 9998;
  566. box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
  567. transition: all 0.3s ease;
  568. }
  569. #yzHelper-settings-toggle:hover {
  570. transform: rotate(30deg);
  571. box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
  572. }
  573. #yzHelper-settings .setting-item.disabled .setting-toggle,
  574. #yzHelper-settings .setting-item .setting-toggle:has(input:disabled) {
  575. opacity: 0.7;
  576. }
  577.  
  578. #yzHelper-settings input:disabled + .slider {
  579. background-color: #ffbe00;
  580. opacity: 0.5;
  581. cursor: not-allowed;
  582. }
  583.  
  584. #yzHelper-settings input:disabled + .slider:before {
  585. background-color: #f0f0f0;
  586. }
  587.  
  588. #yzHelper-settings .setting-item:has(input:disabled) .setting-label:after {
  589. content: " 🔒";
  590. font-size: 12px;
  591. }
  592.  
  593. #yzHelper-settings .setting-item:has(input:disabled) .setting-description {
  594. border-left-color: #ccc;
  595. font-style: italic;
  596. }
  597. #yzHelper-version {
  598. position: absolute;
  599. bottom: 15px;
  600. left: 20px;
  601. font-size: 12px;
  602. color: #999;
  603. }
  604. `);
  605.  
  606. // 设置面板
  607. const settingsToggle = document.createElement("div");
  608. settingsToggle.id = "yzHelper-settings-toggle";
  609. settingsToggle.innerHTML = "⚙️";
  610. settingsToggle.title = "云邮助手设置";
  611. document.body.appendChild(settingsToggle);
  612.  
  613. const settingsPanel = document.createElement("div");
  614. settingsPanel.id = "yzHelper-settings";
  615.  
  616. const header = `
  617. <div id="yzHelper-header">
  618. <span>云邮教学空间助手</span>
  619. <span id="yzHelper-version">v${GM_info.script.version}</span>
  620. </div>
  621. `;
  622.  
  623. const mainContent = `
  624. <div id="yzHelper-main">
  625. <div id="yzHelper-settings-sidebar">
  626. <div class="menu-item active" data-section="preview">
  627. <span class="emoji">🖼️</span>
  628. <span>预览功能</span>
  629. </div>
  630. <div class="menu-item" data-section="notification">
  631. <span class="emoji">📢</span>
  632. <span>通知功能</span>
  633. </div>
  634. <div class="menu-item" data-section="ui">
  635. <span class="emoji">🎨</span>
  636. <span>界面优化</span>
  637. </div>
  638. <div class="menu-item" data-section="system">
  639. <span class="emoji">⚙️</span>
  640. <span>系统设置</span>
  641. </div>
  642. <div class="menu-item" data-section="about">
  643. <span class="emoji">ℹ️</span>
  644. <span>关于助手</span>
  645. </div>
  646. </div>
  647. <div id="yzHelper-settings-content">
  648. <!-- 预览功能设置 -->
  649. <div class="settings-section active" id="section-preview">
  650. <h3>🖼️ 预览功能设置</h3>
  651. <div class="setting-item">
  652. <div class="setting-toggle">
  653. <label class="switch">
  654. <input type="checkbox" id="autoDownload" ${
  655. settings.autoDownload ? "checked" : ""
  656. }>
  657. <span class="slider"></span>
  658. </label>
  659. <span class="setting-label" data-for="description-autoDownload">预览课件时自动下载</span>
  660. </div>
  661. <div class="setting-description" id="description-autoDownload">
  662. 当打开课件预览时,自动触发下载操作,方便存储课件到本地。
  663. </div>
  664. </div>
  665. <div class="setting-item">
  666. <div class="setting-toggle">
  667. <label class="switch">
  668. <input type="checkbox" id="autoSwitchOffice" ${
  669. settings.autoSwitchOffice ? "checked" : ""
  670. }>
  671. <span class="slider"></span>
  672. </label>
  673. <span class="setting-label" data-for="description-autoSwitchOffice">使用 Office365 预览课件</span>
  674. </div>
  675. <div class="setting-description" id="description-autoSwitchOffice">
  676. 使用微软 Office365 在线服务预览 Office 文档,提供更好的浏览体验。
  677. </div>
  678. </div>
  679. <div class="setting-item">
  680. <div class="setting-toggle">
  681. <label class="switch">
  682. <input type="checkbox" id="autoClosePopup" ${
  683. settings.autoClosePopup ? "checked" : ""
  684. }>
  685. <span class="slider"></span>
  686. </label>
  687. <span class="setting-label" data-for="description-autoClosePopup">自动关闭预览弹窗</span>
  688. </div>
  689. <div class="setting-description" id="description-autoClosePopup">
  690. 下载课件后自动关闭预览弹窗,简化操作流程。
  691. </div>
  692. </div>
  693. <div class="setting-item">
  694. <div class="setting-toggle">
  695. <label class="switch">
  696. <input type="checkbox" id="hideTimer" ${
  697. settings.hideTimer ? "checked" : ""
  698. }>
  699. <span class="slider"></span>
  700. </label>
  701. <span class="setting-label" data-for="description-hideTimer">隐藏预览界面倒计时</span>
  702. </div>
  703. <div class="setting-description" id="description-hideTimer">
  704. 隐藏预览界面中的倒计时提示,获得无干扰的阅读体验。
  705. </div>
  706. </div>
  707. <div class="setting-item">
  708. <div class="setting-toggle">
  709. <label class="switch">
  710. <input type="checkbox" id="unlockCopy" ${
  711. settings.unlockCopy ? "checked" : ""
  712. }>
  713. <span class="slider"></span>
  714. </label>
  715. <span class="setting-label" data-for="description-unlockCopy">解除复制限制</span>
  716. </div>
  717. <div class="setting-description" id="description-unlockCopy">
  718. 解除课件预览时的复制限制,方便摘录内容进行学习笔记。
  719. </div>
  720. </div>
  721. </div>
  722. <!-- 通知功能设置 -->
  723. <div class="settings-section" id="section-notification">
  724. <h3>📢 通知功能设置</h3>
  725. <div class="setting-item">
  726. <div class="setting-toggle">
  727. <label class="switch">
  728. <input type="checkbox" id="showMoreNotification" ${
  729. settings.showMoreNotification ? "checked" : ""
  730. }>
  731. <span class="slider"></span>
  732. </label>
  733. <span class="setting-label" data-for="description-showMoreNotification">显示更多的通知</span>
  734. </div>
  735. <div class="setting-description" id="description-showMoreNotification">
  736. 在通知列表中显示更多的历史通知,不再受限于默认显示数量。
  737. </div>
  738. </div>
  739. <div class="setting-item">
  740. <div class="setting-toggle">
  741. <label class="switch">
  742. <input type="checkbox" id="sortNotificationsByTime" ${
  743. settings.sortNotificationsByTime ? "checked" : ""
  744. }>
  745. <span class="slider"></span>
  746. </label>
  747. <span class="setting-label" data-for="description-sortNotificationsByTime">通知按照时间排序</span>
  748. </div>
  749. <div class="setting-description" id="description-sortNotificationsByTime">
  750. 将通知按照时间先后顺序排列,更容易找到最新或最早的通知。
  751. </div>
  752. </div>
  753. <div class="setting-item">
  754. <div class="setting-toggle">
  755. <label class="switch">
  756. <input type="checkbox" id="betterNotificationHighlight" ${
  757. settings.betterNotificationHighlight
  758. ? "checked"
  759. : ""
  760. }>
  761. <span class="slider"></span>
  762. </label>
  763. <span class="setting-label" data-for="description-betterNotificationHighlight">优化未读通知高亮</span>
  764. </div>
  765. <div class="setting-description" id="description-betterNotificationHighlight">
  766. 增强未读通知的视觉提示,使未读消息更加醒目,不易遗漏重要信息。
  767. </div>
  768. </div>
  769. </div>
  770. <!-- 界面优化设置 -->
  771. <div class="settings-section" id="section-ui">
  772. <h3>🎨 界面优化设置</h3>
  773. <div class="setting-item">
  774. <div class="setting-toggle">
  775. <label class="switch">
  776. <input type="checkbox" id="useBiggerButton" ${
  777. settings.useBiggerButton ? "checked" : ""
  778. }>
  779. <span class="slider"></span>
  780. </label>
  781. <span class="setting-label" data-for="description-useBiggerButton">加大翻页按钮尺寸</span>
  782. </div>
  783. <div class="setting-description" id="description-useBiggerButton">
  784. 增大页面翻页按钮的尺寸和点击区域,提升操作便捷性。
  785. </div>
  786. </div>
  787. <div class="setting-item">
  788. <div class="setting-toggle">
  789. <label class="switch">
  790. <input type="checkbox" id="betterTitle" ${
  791. settings.betterTitle ? "checked" : ""
  792. }>
  793. <span class="slider"></span>
  794. </label>
  795. <span class="setting-label" data-for="description-betterTitle">优化页面标题</span>
  796. </div>
  797. <div class="setting-description" id="description-betterTitle">
  798. 优化浏览器标签页的标题显示,更直观地反映当前页面内容。
  799. </div>
  800. </div>
  801. </div>
  802. <!-- 系统设置 -->
  803. <div class="settings-section" id="section-system">
  804. <h3>⚙️ 系统设置</h3>
  805. <div class="setting-item">
  806. <div class="setting-toggle">
  807. <label class="switch">
  808. <input type="checkbox" id="fixTicketBug" checked disabled>
  809. <span class="slider"></span>
  810. </label>
  811. <span class="setting-label" data-for="description-fixTicketBug">修复ticket跳转问题</span>
  812. </div>
  813. <div class="setting-description" id="description-fixTicketBug">
  814. 修复登录过期后,重新登录出现无法获取ticket提示的问题。
  815. </div>
  816. </div>
  817. <div class="setting-item">
  818. <div class="setting-toggle">
  819. <label class="switch">
  820. <input type="checkbox" id="autoUpdate" ${
  821. settings.autoUpdate ? "checked" : ""
  822. }>
  823. <span class="slider"></span>
  824. </label>
  825. <span class="setting-label" data-for="description-autoUpdate">内置更新检查</span>
  826. </div>
  827. <div class="setting-description" id="description-autoUpdate">
  828. 定期检查脚本更新,确保您始终使用最新版本的功能和修复。
  829. </div>
  830. </div>
  831. </div>
  832. <!-- 关于助手 -->
  833. <div class="settings-section" id="section-about">
  834. <h3>ℹ️ 关于云邮教学空间助手</h3>
  835. <div class="about-content">
  836. <p>云邮教学空间助手是一款专为云邮教学空间平台设计的浏览器增强脚本。</p>
  837. <h4>🚀 主要功能</h4>
  838. <ul>
  839. <li>课件预览增强 - 提供更流畅的课件浏览体验</li>
  840. <li>通知管理优化 - 更清晰地整理和显示重要通知</li>
  841. <li>界面体验提升 - 优化布局与交互,提高使用效率</li>
  842. </ul>
  843. <h4>🔗 相关链接</h4>
  844. <p>
  845. <a href="https://github.com/uarix/ucloud-Evolved/" target="_blank" class="github-link">
  846. <svg class="github-icon" height="16" width="16" viewBox="0 0 16 16" aria-hidden="true">
  847. <path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path>
  848. </svg>
  849. <span>GitHub 项目主页</span>
  850. </a>
  851. </p>
  852. <p class="feedback-note">
  853. 如有问题或建议,请通过
  854. <a href="https://github.com/uarix/ucloud-Evolved/issues" target="_blank">GitHub Issues</a>
  855. 提交反馈。
  856. </p>
  857. </div>
  858. </div>
  859. <div class="buttons">
  860. <button id="cancelSettings" class="cancel">取消</button>
  861. <button id="saveSettings">保存设置</button>
  862. </div>
  863. </div>
  864. </div>
  865. `;
  866.  
  867. settingsPanel.innerHTML = header + mainContent;
  868. document.body.appendChild(settingsPanel);
  869.  
  870. // 菜单切换功能
  871. document
  872. .querySelectorAll("#yzHelper-settings-sidebar .menu-item")
  873. .forEach((item) => {
  874. item.addEventListener("click", function () {
  875. // 移除所有菜单项的活动状态
  876. document
  877. .querySelectorAll("#yzHelper-settings-sidebar .menu-item")
  878. .forEach((i) => {
  879. i.classList.remove("active");
  880. });
  881. document
  882. .querySelectorAll("#yzHelper-settings-content .settings-section")
  883. .forEach((section) => {
  884. section.classList.remove("active");
  885. });
  886.  
  887. this.classList.add("active");
  888. const sectionId = "section-" + this.getAttribute("data-section");
  889. document.getElementById(sectionId).classList.add("active");
  890.  
  891. // 隐藏所有设置描述
  892. document.querySelectorAll(".setting-description").forEach((desc) => {
  893. desc.classList.remove("visible");
  894. });
  895. });
  896. });
  897.  
  898. // 设置描述显示/隐藏功能
  899. document.querySelectorAll(".setting-label").forEach((label) => {
  900. label.addEventListener("click", function () {
  901. const descriptionId = this.getAttribute("data-for");
  902. const description = document.getElementById(descriptionId);
  903.  
  904. // 隐藏所有其他描述
  905. document.querySelectorAll(".setting-description").forEach((desc) => {
  906. if (desc.id !== descriptionId) {
  907. desc.classList.remove("visible");
  908. }
  909. });
  910.  
  911. // 切换当前描述的可见性
  912. description.classList.toggle("visible");
  913. });
  914. });
  915.  
  916. settingsToggle.addEventListener("click", () => {
  917. const isVisible = settingsPanel.classList.contains("visible");
  918. if (isVisible) {
  919. settingsPanel.classList.remove("visible");
  920. setTimeout(() => {
  921. settingsPanel.style.display = "none";
  922. }, 300);
  923. } else {
  924. settingsPanel.style.display = "flex";
  925. void settingsPanel.offsetWidth;
  926. settingsPanel.classList.add("visible");
  927. }
  928. });
  929.  
  930. document.getElementById("cancelSettings").addEventListener("click", () => {
  931. settingsPanel.classList.remove("visible");
  932. setTimeout(() => {
  933. settingsPanel.style.display = "none";
  934. }, 300);
  935. });
  936.  
  937. document.getElementById("saveSettings").addEventListener("click", () => {
  938. Array.from(
  939. document
  940. .querySelector("#yzHelper-settings-content")
  941. .querySelectorAll('input[type="checkbox"]:not(:disabled)')
  942. ).forEach((checkbox) => {
  943. settings[checkbox.id] = checkbox.checked;
  944. GM_setValue(checkbox.id, checkbox.checked);
  945. });
  946.  
  947. settingsPanel.classList.remove("visible");
  948. setTimeout(() => {
  949. settingsPanel.style.display = "none";
  950. showNotification("设置已保存", "刷新页面后生效");
  951. }, 300);
  952. });
  953. }
  954.  
  955. // 通知函数
  956. function showNotification(title, message) {
  957. const notification = document.createElement("div");
  958. notification.style.cssText = `
  959. position: fixed;
  960. bottom: 80px;
  961. right: 20px;
  962. background: #4CAF50;
  963. color: white;
  964. padding: 15px 20px;
  965. border-radius: 8px;
  966. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  967. z-index: 10000;
  968. font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
  969. max-width: 300px;
  970. opacity: 0;
  971. transform: translateY(-10px);
  972. transition: all 0.3s ease;
  973. `;
  974.  
  975. notification.innerHTML = `
  976. <div style="font-weight: bold; margin-bottom: 5px;">${title}</div>
  977. <div style="font-size: 14px;">${message}</div>
  978. `;
  979.  
  980. document.body.appendChild(notification);
  981.  
  982. void notification.offsetWidth;
  983.  
  984. notification.style.opacity = "1";
  985. notification.style.transform = "translateY(0)";
  986.  
  987. setTimeout(() => {
  988. notification.style.opacity = "0";
  989. notification.style.transform = "translateY(-10px)";
  990. setTimeout(() => {
  991. document.body.removeChild(notification);
  992. }, 300);
  993. }, 3000);
  994. }
  995.  
  996. // 获取Token
  997. function getToken() {
  998. const cookieMap = new Map();
  999. document.cookie.split("; ").forEach((cookie) => {
  1000. const [key, value] = cookie.split("=");
  1001. cookieMap.set(key, value);
  1002. });
  1003. const token = cookieMap.get("iClass-token");
  1004. const userid = cookieMap.get("iClass-uuid");
  1005. return [userid, token];
  1006. }
  1007.  
  1008. // 文件下载相关函数
  1009. async function downloadFile(url, filename) {
  1010. console.log("Call download");
  1011. downloading = true;
  1012. await jsp;
  1013. NProgress.configure({ trickle: false, speed: 0 });
  1014. try {
  1015. const response = await fetch(url);
  1016.  
  1017. if (!response.ok) {
  1018. throw new Error(`HTTP error! status: ${response.status}`);
  1019. }
  1020.  
  1021. const contentLength = response.headers.get("content-length");
  1022. if (!contentLength) {
  1023. throw new Error("Content-Length response header unavailable");
  1024. }
  1025.  
  1026. const total = parseInt(contentLength, 10);
  1027. sumBytes += total;
  1028. const reader = response.body.getReader();
  1029. const chunks = [];
  1030. while (true) {
  1031. const { done, value } = await reader.read();
  1032. if (done) break;
  1033. if (!downloading) {
  1034. NProgress.done();
  1035. return;
  1036. }
  1037. chunks.push(value);
  1038. loadedBytes += value.length;
  1039. NProgress.set(loadedBytes / sumBytes);
  1040. }
  1041. NProgress.done();
  1042. sumBytes -= total;
  1043. loadedBytes -= total;
  1044. const blob = new Blob(chunks);
  1045. const downloadUrl = window.URL.createObjectURL(blob);
  1046. const a = document.createElement("a");
  1047. a.href = downloadUrl;
  1048. a.download = filename;
  1049. document.body.appendChild(a);
  1050. a.click();
  1051. window.URL.revokeObjectURL(downloadUrl);
  1052. } catch (error) {
  1053. console.error("Download failed:", error);
  1054. }
  1055. }
  1056.  
  1057. // 任务搜索函数
  1058. async function searchTask(siteId, keyword, token) {
  1059. const res = await fetch(
  1060. "https://apiucloud.bupt.edu.cn/ykt-site/work/student/list",
  1061. {
  1062. headers: {
  1063. authorization: "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
  1064. "blade-auth": token,
  1065. "content-type": "application/json;charset=UTF-8",
  1066. },
  1067. body: JSON.stringify({
  1068. siteId,
  1069. keyword,
  1070. current: 1,
  1071. size: 5,
  1072. }),
  1073. method: "POST",
  1074. }
  1075. );
  1076. const json = await res.json();
  1077. return json;
  1078. }
  1079.  
  1080. // 课程搜索函数
  1081. async function searchCourse(userId, id, keyword, token) {
  1082. const res = await fetch(
  1083. "https://apiucloud.bupt.edu.cn/ykt-site/site/list/student/current?size=999999&current=1&userId=" +
  1084. userId +
  1085. "&siteRoleCode=2",
  1086. {
  1087. headers: {
  1088. authorization: "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
  1089. "blade-auth": token,
  1090. },
  1091. body: null,
  1092. method: "GET",
  1093. }
  1094. );
  1095. const json = await res.json();
  1096. const list = json.data.records.map((x) => ({
  1097. id: x.id,
  1098. name: x.siteName,
  1099. teachers: x.teachers.map((y) => y.name).join(", "),
  1100. }));
  1101.  
  1102. async function searchWithLimit(list, id, keyword, token, limit = 5) {
  1103. for (let i = 0; i < list.length; i += limit) {
  1104. const batch = list.slice(i, i + limit);
  1105. const jobs = batch.map((x) => searchTask(x.id, keyword, token));
  1106. const ress = await Promise.all(jobs);
  1107. for (let j = 0; j < ress.length; j++) {
  1108. const res = ress[j];
  1109. if (res.data && res.data.records && res.data.records.length > 0) {
  1110. for (const item of res.data.records) {
  1111. if (item.id == id) {
  1112. return batch[j];
  1113. }
  1114. }
  1115. }
  1116. }
  1117. }
  1118. return null;
  1119. }
  1120. return await searchWithLimit(list, id, keyword, token);
  1121. }
  1122.  
  1123. // 获取任务列表
  1124. async function getTasks(siteId, token) {
  1125. const res = await fetch(
  1126. "https://apiucloud.bupt.edu.cn/ykt-site/work/student/list",
  1127. {
  1128. headers: {
  1129. authorization: "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
  1130. "blade-auth": token,
  1131. "content-type": "application/json;charset=UTF-8",
  1132. },
  1133. body: JSON.stringify({
  1134. siteId,
  1135. current: 1,
  1136. size: 9999,
  1137. }),
  1138. method: "POST",
  1139. }
  1140. );
  1141. const json = await res.json();
  1142. return json;
  1143. }
  1144.  
  1145. // 搜索课程
  1146. async function searchCourses(nids) {
  1147. const result = {};
  1148. let ids = [];
  1149. for (let id of nids) {
  1150. const r = get(id);
  1151. if (r) result[id] = r;
  1152. else ids.push(id);
  1153. }
  1154.  
  1155. if (ids.length == 0) return result;
  1156. const [userid, token] = getToken();
  1157. const res = await fetch(
  1158. "https://apiucloud.bupt.edu.cn/ykt-site/site/list/student/current?size=999999&current=1&userId=" +
  1159. userid +
  1160. "&siteRoleCode=2",
  1161. {
  1162. headers: {
  1163. authorization: "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
  1164. "blade-auth": token,
  1165. },
  1166. body: null,
  1167. method: "GET",
  1168. }
  1169. );
  1170. const json = await res.json();
  1171. const list = json.data.records.map((x) => ({
  1172. id: x.id,
  1173. name: x.siteName,
  1174. teachers: x.teachers.map((y) => y.name).join(", "),
  1175. }));
  1176. const hashMap = new Map();
  1177. let count = ids.length;
  1178. for (let i = 0; i < ids.length; i++) {
  1179. hashMap.set(ids[i], i);
  1180. }
  1181.  
  1182. async function searchWithLimit(list, limit = 5) {
  1183. for (let i = 0; i < list.length; i += limit) {
  1184. const batch = list.slice(i, i + limit);
  1185. const jobs = batch.map((x) => getTasks(x.id, token));
  1186. const ress = await Promise.all(jobs);
  1187. for (let j = 0; j < ress.length; j++) {
  1188. const res = ress[j];
  1189. if (res.data && res.data.records && res.data.records.length > 0) {
  1190. for (const item of res.data.records) {
  1191. if (hashMap.has(item.id)) {
  1192. result[item.id] = batch[j];
  1193. set(item.id, batch[j]);
  1194. if (--count == 0) {
  1195. return result;
  1196. }
  1197. }
  1198. }
  1199. }
  1200. }
  1201. }
  1202. return result;
  1203. }
  1204. return await searchWithLimit(list);
  1205. }
  1206.  
  1207. // 获取未完成列表
  1208. async function getUndoneList() {
  1209. const [userid, token] = getToken();
  1210. const res = await fetch(
  1211. "https://apiucloud.bupt.edu.cn/ykt-site/site/student/undone?userId=" +
  1212. userid,
  1213. {
  1214. headers: {
  1215. authorization: "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
  1216. "blade-auth": token,
  1217. },
  1218. method: "GET",
  1219. }
  1220. );
  1221. const json = await res.json();
  1222. return json;
  1223. }
  1224.  
  1225. // 获取详情
  1226. async function getDetail(id) {
  1227. const [userid, token] = getToken();
  1228. const res = await fetch(
  1229. "https://apiucloud.bupt.edu.cn/ykt-site/work/detail?assignmentId=" + id,
  1230. {
  1231. headers: {
  1232. authorization: "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
  1233. "blade-auth": token,
  1234. },
  1235. body: null,
  1236. method: "GET",
  1237. }
  1238. );
  1239. const json = await res.json();
  1240. return json;
  1241. }
  1242.  
  1243. // 获取站点资源
  1244. async function getSiteResource(id) {
  1245. const [userid, token] = getToken();
  1246. const res = await fetch(
  1247. "https://apiucloud.bupt.edu.cn/ykt-site/site-resource/tree/student?siteId=" +
  1248. id +
  1249. "&userId=" +
  1250. userid,
  1251. {
  1252. headers: {
  1253. authorization: "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
  1254. "blade-auth": token,
  1255. },
  1256. body: null,
  1257. method: "POST",
  1258. }
  1259. );
  1260. const json = await res.json();
  1261. const result = [];
  1262. function foreach(data) {
  1263. if (!data || !Array.isArray(data)) return;
  1264. data.forEach((x) => {
  1265. if (x.attachmentVOs && Array.isArray(x.attachmentVOs)) {
  1266. x.attachmentVOs.forEach((y) => {
  1267. if (y.type !== 2 && y.resource) result.push(y.resource);
  1268. });
  1269. }
  1270. if (x.children) foreach(x.children);
  1271. });
  1272. }
  1273. foreach(json.data);
  1274. return result;
  1275. }
  1276.  
  1277. // 更新作业显示
  1278. async function updateAssignmentDisplay(list, page) {
  1279. if (!list || list.length === 0) return;
  1280.  
  1281. // 获取当前页的作业
  1282. const tlist = list.slice((page - 1) * 6, page * 6);
  1283. if (tlist.length === 0) return;
  1284.  
  1285. // 获取课程信息
  1286. const ids = tlist.map((x) => x.activityId);
  1287. const infos = await searchCourses(ids);
  1288.  
  1289. // 确保所有信息都已获取到
  1290. if (Object.keys(infos).length === 0) return;
  1291.  
  1292. // 准备显示文本
  1293. const texts = tlist.map((x) => {
  1294. const info = infos[x.activityId];
  1295. return info ? `${info.name}(${info.teachers})` : "加载中...";
  1296. });
  1297.  
  1298. // 等待作业元素显示
  1299. const timeout = 5000; // 5秒超时
  1300. const startTime = Date.now();
  1301.  
  1302. let nodes;
  1303. while (Date.now() - startTime < timeout) {
  1304. nodes = $x(
  1305. '//*[@id="layout-container"]/div[2]/div[2]/div/div[2]/div[1]/div[3]/div[2]/div/div'
  1306. );
  1307. if (
  1308. nodes.length > 0 &&
  1309. nodes.some((node) => node.children[0] && node.children[0].innerText)
  1310. ) {
  1311. break;
  1312. }
  1313. await sleep(100);
  1314. }
  1315.  
  1316. // 更新课程信息显示
  1317. for (let i = 0; i < Math.min(nodes.length, texts.length); i++) {
  1318. if (nodes[i] && nodes[i].children[1]) {
  1319. if (nodes[i].children[1].children.length === 0) {
  1320. const p = document.createElement("div");
  1321. const t = document.createTextNode(texts[i]);
  1322. p.appendChild(t);
  1323. p.style.color = "#0066cc";
  1324. nodes[i].children[1].insertAdjacentElement("afterbegin", p);
  1325. } else {
  1326. nodes[i].children[1].children[0].innerHTML = texts[i];
  1327. nodes[i].children[1].children[0].style.color = "#0066cc";
  1328. }
  1329. }
  1330. }
  1331. }
  1332.  
  1333. // XPath选择器
  1334. function $x(xpath, context = document) {
  1335. const iterator = document.evaluate(
  1336. xpath,
  1337. context,
  1338. null,
  1339. XPathResult.ANY_TYPE,
  1340. null
  1341. );
  1342. const results = [];
  1343. let item;
  1344. while ((item = iterator.iterateNext())) {
  1345. results.push(item);
  1346. }
  1347. return results;
  1348. }
  1349.  
  1350. // 本地存储
  1351. function set(k, v) {
  1352. const h = JSON.parse(localStorage.getItem("zzxw") || "{}");
  1353. h[k] = v;
  1354. localStorage.setItem("zzxw", JSON.stringify(h));
  1355. }
  1356.  
  1357. function get(k) {
  1358. const h = JSON.parse(localStorage.getItem("zzxw") || "{}");
  1359. return h[k];
  1360. }
  1361.  
  1362. // 插入课程信息
  1363. function insert(x) {
  1364. if (!x) return;
  1365. if (
  1366. $x(
  1367. "/html/body/div[1]/div/div[2]/div[2]/div/div/div[2]/div/div[2]/div[1]/div/div/div[1]/div/p"
  1368. ).length > 2
  1369. )
  1370. return;
  1371. const d = $x(
  1372. "/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]"
  1373. );
  1374. if (!d.length) {
  1375. setTimeout(() => insert(x), 50);
  1376. return;
  1377. }
  1378. // 检查是否已经插入过
  1379. const existingText = Array.from(d[0].parentNode.childNodes).some(
  1380. (node) => node.textContent && node.textContent.includes(x.name)
  1381. );
  1382.  
  1383. if (!existingText) {
  1384. const p = document.createElement("p");
  1385. const t = document.createTextNode(x.name + "(" + x.teachers + ")");
  1386. p.appendChild(t);
  1387. d[0].after(p);
  1388. }
  1389. }
  1390.  
  1391. // 辅助函数
  1392. function sleep(n) {
  1393. return new Promise((res) => setTimeout(res, n));
  1394. }
  1395.  
  1396. async function wait(func) {
  1397. let r = func();
  1398. if (r instanceof Promise) r = await r;
  1399. if (r) return r;
  1400. await sleep(50);
  1401. return await wait(func);
  1402. }
  1403.  
  1404. async function waitChange(func, value) {
  1405. const r = value;
  1406. while (1) {
  1407. let t = func();
  1408. if (t instanceof Promise) t = await t;
  1409. if (t != r) return t;
  1410. await sleep(50);
  1411. }
  1412. }
  1413.  
  1414. // 预览URL相关
  1415. async function getPreviewURL(storageId) {
  1416. const res = await fetch(
  1417. "https://apiucloud.bupt.edu.cn/blade-source/resource/preview-url?resourceId=" +
  1418. storageId
  1419. );
  1420. const json = await res.json();
  1421. onlinePreview = json.data.onlinePreview;
  1422. return json.data.previewUrl;
  1423. }
  1424.  
  1425. // 启用文本选择 修改按钮尺寸
  1426. function addFunctionalCSS() {
  1427. GM_addStyle(`
  1428. .teacher-home-page .home-left-container .in-progress-section .in-progress-body .in-progress-item .activity-box .activity-title {
  1429. height: auto !important;
  1430. }
  1431. #layout-container > div.main-content > div.router-container > div > div.my-course-page {
  1432. max-height: none !important;
  1433. }
  1434. `);
  1435. if (settings.betterNotificationHighlight) {
  1436. GM_addStyle(`
  1437. .notification-with-dot {
  1438. background-color: #fff8f8 !important;
  1439. border-left: 5px solid #f56c6c !important;
  1440. box-shadow: 0 2px 6px rgba(245, 108, 108, 0.2) !important;
  1441. padding: 0 22px !important;
  1442. margin-bottom: 8px !important;
  1443. border-radius: 4px !important;
  1444. transition: all 0.3s ease !important;
  1445. }
  1446. .notification-with-dot:hover {
  1447. background-color: #fff0f0 !important;
  1448. box-shadow: 0 4px 12px rgba(245, 108, 108, 0.3) !important;
  1449. transform: translateY(-2px) !important;
  1450. }
  1451. `);
  1452. }
  1453. if (settings.enableTextSelection) {
  1454. GM_addStyle(`
  1455. .el-checkbox, .el-checkbox-button__inner, .el-empty__image img, .el-radio,
  1456. div, span, p, a, h1, h2, h3, h4, h5, h6, li, td, th {
  1457. -webkit-user-select: auto !important;
  1458. -moz-user-select: auto !important;
  1459. -ms-user-select: auto !important;
  1460. user-select: auto !important;
  1461. }
  1462. `);
  1463. document.addEventListener(
  1464. "copy",
  1465. function (e) {
  1466. e.stopImmediatePropagation();
  1467. },
  1468. true
  1469. );
  1470.  
  1471. document.addEventListener(
  1472. "selectstart",
  1473. function (e) {
  1474. e.stopImmediatePropagation();
  1475. },
  1476. true
  1477. );
  1478. }
  1479. if (settings.useBiggerButton) {
  1480. GM_addStyle(`
  1481. .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 {
  1482. width: 60px !important;
  1483. height: 30px !important;
  1484. background: #f2f2f2 !important;
  1485. line-height: auto !important;
  1486. }
  1487. .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 {
  1488. font-size: 22px !important;
  1489. }
  1490. .el-icon-arrow-left, .el-icon-arrow-right {
  1491. height: 100%;
  1492. display: flex;
  1493. align-items: center;
  1494. justify-content: center;
  1495. }
  1496. `);
  1497. }
  1498. }
  1499.  
  1500. // 主函数
  1501. async function main() {
  1502. "use strict";
  1503. // ticket跳转
  1504. if (new URLSearchParams(location.search).get("ticket")?.length) {
  1505. setTimeout(() => {
  1506. location.href = "https://ucloud.bupt.edu.cn/uclass/#/student/homePage";
  1507. }, 500);
  1508. return;
  1509. }
  1510.  
  1511. // 课件预览页面
  1512. if (
  1513. location.href.startsWith(
  1514. "https://ucloud.bupt.edu.cn/uclass/course.html#/resourceLearn"
  1515. )
  1516. ) {
  1517. if (settings.betterTitle) {
  1518. function extractFilename(url) {
  1519. try {
  1520. const match = url.match(/previewUrl=([^&]+)/);
  1521. if (!match) return null;
  1522.  
  1523. const previewUrl = decodeURIComponent(match[1]);
  1524.  
  1525. // 从content-disposition中提取文件名
  1526. const filenameMatch = previewUrl.match(/filename%3D([^&]+)/);
  1527. if (!filenameMatch) return null;
  1528.  
  1529. return decodeURIComponent(decodeURIComponent(filenameMatch[1]));
  1530. } catch (e) {
  1531. return null;
  1532. }
  1533. }
  1534. const url = location.href;
  1535. const filename = extractFilename(url);
  1536. const site = JSON.parse(localStorage.getItem("site"));
  1537. const pageTitle =
  1538. "[预览] " +
  1539. (filename || "课件") +
  1540. " - " +
  1541. site.siteName +
  1542. " - 教学云空间";
  1543. document.title = pageTitle;
  1544. }
  1545. if (settings.autoClosePopup) {
  1546. const dialogBox = document.querySelector("div.el-message-box__wrapper");
  1547.  
  1548. if (
  1549. dialogBox &&
  1550. window.getComputedStyle(dialogBox).display !== "none"
  1551. ) {
  1552. const messageElement = dialogBox.querySelector(
  1553. ".el-message-box__message p"
  1554. );
  1555. if (
  1556. messageElement &&
  1557. (messageElement.textContent.includes("您正在学习其他课件") ||
  1558. messageElement.textContent.includes("您已经在学习此课件了"))
  1559. ) {
  1560. const confirmButton = dialogBox.querySelector(
  1561. ".el-button--primary"
  1562. );
  1563. if (confirmButton) {
  1564. confirmButton.click();
  1565. } else {
  1566. console.log("未找到确认按钮");
  1567. }
  1568. }
  1569. }
  1570. }
  1571. if (settings.hideTimer) {
  1572. GM_addStyle(`
  1573. .preview-container .time {
  1574. display: none !important;
  1575. }
  1576. `);
  1577. }
  1578. }
  1579.  
  1580. // 作业详情页面
  1581. if (
  1582. location.href.startsWith(
  1583. "https://ucloud.bupt.edu.cn/uclass/course.html#/student/assignmentDetails_fullpage"
  1584. )
  1585. ) {
  1586. const q = new URLSearchParams(location.href);
  1587. const id = q.get("assignmentId");
  1588. const r = get(id);
  1589. const [userid, token] = getToken();
  1590. const title = q.get("assignmentTitle");
  1591. if (settings.betterTitle) {
  1592. const pageTitle = "[作业] " + title + " - " + r.name + " - 教学云空间";
  1593. document.title = pageTitle;
  1594. }
  1595. // 显示相关课程信息
  1596. if (r) {
  1597. insert(r);
  1598. } else {
  1599. if (!id || !title) return;
  1600. try {
  1601. const courseInfo = await searchCourse(userid, id, title, token);
  1602. if (courseInfo) {
  1603. insert(courseInfo);
  1604. set(id, courseInfo);
  1605. }
  1606. } catch (e) {
  1607. console.error("获取课程信息失败", e);
  1608. }
  1609. }
  1610.  
  1611. // 处理资源预览和下载
  1612. try {
  1613. const detail = (await getDetail(id)).data;
  1614. if (!detail || !detail.assignmentResource) return;
  1615.  
  1616. const filenames = detail.assignmentResource.map((x) => x.resourceName);
  1617. const urls = await Promise.all(
  1618. detail.assignmentResource.map((x) => {
  1619. return getPreviewURL(x.resourceId);
  1620. })
  1621. );
  1622.  
  1623. await wait(
  1624. () =>
  1625. $x('//*[@id="assignment-info"]/div[2]/div[2]/div[2]/div').length > 0
  1626. );
  1627.  
  1628. $x('//*[@id="assignment-info"]/div[2]/div[2]/div[2]/div').forEach(
  1629. (x, index) => {
  1630. if (
  1631. x.querySelector(".by-icon-eye-grey") ||
  1632. x.querySelector(".by-icon-yundown-grey")
  1633. ) {
  1634. x.querySelector(".by-icon-eye-grey").remove();
  1635. x.querySelector(".by-icon-yundown-grey").remove();
  1636. }
  1637.  
  1638. // 添加预览按钮
  1639. const i = document.createElement("i");
  1640. i.title = "预览";
  1641. i.classList.add("by-icon-eye-grey");
  1642. i.addEventListener("click", () => {
  1643. const url = urls[index];
  1644. const filename = filenames[index];
  1645. if (settings.autoDownload) {
  1646. downloadFile(url, filename);
  1647. console.log("Autodownload");
  1648. }
  1649. if (
  1650. filename.endsWith(".xls") ||
  1651. filename.endsWith(".xlsx") ||
  1652. url.endsWith(".doc") ||
  1653. url.endsWith(".docx") ||
  1654. url.endsWith(".ppt") ||
  1655. url.endsWith(".pptx")
  1656. )
  1657. openTab(
  1658. "https://view.officeapps.live.com/op/view.aspx?src=" +
  1659. encodeURIComponent(url),
  1660. { active: true, insert: true }
  1661. );
  1662. else if (onlinePreview !== null)
  1663. openTab(onlinePreview + encodeURIComponent(url), {
  1664. active: true,
  1665. insert: true,
  1666. });
  1667. });
  1668.  
  1669. // 添加下载按钮
  1670. const i2 = document.createElement("i");
  1671. i2.title = "下载";
  1672. i2.classList.add("by-icon-yundown-grey");
  1673. i2.addEventListener("click", () => {
  1674. downloadFile(urls[index], filenames[index]);
  1675. });
  1676.  
  1677. // 插入按钮
  1678. if (x.children.length >= 3) {
  1679. x.children[3]?.remove();
  1680. x.children[2]?.insertAdjacentElement("afterend", i);
  1681. x.children[2]?.remove();
  1682. x.children[1]?.insertAdjacentElement("afterend", i2);
  1683. } else {
  1684. x.appendChild(i2);
  1685. x.appendChild(i);
  1686. }
  1687. }
  1688. );
  1689. } catch (e) {
  1690. console.error("处理资源失败", e);
  1691. }
  1692. }
  1693.  
  1694. // 主页面
  1695. else if (
  1696. location.href.startsWith(
  1697. "https://ucloud.bupt.edu.cn/uclass/#/student/homePage"
  1698. ) ||
  1699. location.href.startsWith(
  1700. "https://ucloud.bupt.edu.cn/uclass/index.html#/student/homePage"
  1701. )
  1702. ) {
  1703. try {
  1704. if (settings.betterTitle) {
  1705. const pageTitle = "个人主页 - 教学云空间";
  1706. document.title = pageTitle;
  1707. }
  1708. // 未完成任务列表
  1709. const list = glist || (await getUndoneList()).data.undoneList;
  1710. if (!list || !Array.isArray(list)) return;
  1711. glist = list;
  1712.  
  1713. const observer = new MutationObserver(async (mutations) => {
  1714. // 当前页码
  1715. const pageElement = document.querySelector(
  1716. "#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"
  1717. );
  1718.  
  1719. if (!pageElement) return;
  1720.  
  1721. // 解析页码
  1722. const currentPage = parseInt(
  1723. pageElement.innerHTML.trim().split("/")[0]
  1724. );
  1725. if (isNaN(currentPage)) return;
  1726.  
  1727. // 页码变化则更新显示
  1728. if (currentPage !== gpage) {
  1729. gpage = currentPage;
  1730. await updateAssignmentDisplay(list, currentPage);
  1731. }
  1732. });
  1733.  
  1734. observer.observe(document.body, {
  1735. childList: true,
  1736. subtree: true,
  1737. attributes: false,
  1738. characterData: true,
  1739. });
  1740.  
  1741. // 初始化页码
  1742. let page = 1;
  1743. const pageElement = document.querySelector(
  1744. "#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"
  1745. );
  1746.  
  1747. if (pageElement) {
  1748. page = parseInt(pageElement.innerHTML.trim().split("/")[0]);
  1749. gpage = page;
  1750. }
  1751.  
  1752. // 更新作业显示
  1753. await updateAssignmentDisplay(list, page);
  1754.  
  1755. // 本学期课程点击事件
  1756. document.querySelectorAll('div[class="header-label"]').forEach((el) => {
  1757. if (el.textContent.includes("本学期课程")) {
  1758. el.style.cursor = "pointer";
  1759. el.addEventListener("click", (e) => {
  1760. e.preventDefault();
  1761. window.location.href =
  1762. "https://ucloud.bupt.edu.cn/uclass/index.html#/student/myCourse";
  1763. });
  1764. }
  1765. });
  1766. } catch (e) {
  1767. console.error("主页处理失败", e);
  1768. }
  1769. }
  1770.  
  1771. // 课程主页
  1772. else if (
  1773. location.href.startsWith(
  1774. "https://ucloud.bupt.edu.cn/uclass/course.html#/student/courseHomePage"
  1775. )
  1776. ) {
  1777. try {
  1778. const site = JSON.parse(localStorage.getItem("site"));
  1779. if (!site || !site.id) return;
  1780. if (settings.betterTitle) {
  1781. const pageTitle = "[课程] " + site.siteName + " - 教学云空间";
  1782. document.title = pageTitle;
  1783. }
  1784.  
  1785. const id = site.id;
  1786. const resources = await getSiteResource(id);
  1787.  
  1788. // 添加下载按钮到每个资源
  1789. const resourceItems = $x(
  1790. '//div[@class="resource-item"]/div[@class="right"]'
  1791. );
  1792. const previewItems = $x(
  1793. '//div[@class="resource-item"]/div[@class="left"]'
  1794. );
  1795.  
  1796. if (resourceItems.length > 0) {
  1797. resourceItems.forEach((x, index) => {
  1798. if (index >= resources.length) return;
  1799.  
  1800. if (settings.autoDownload) {
  1801. previewItems[index].addEventListener(
  1802. "click",
  1803. async (e) => {
  1804. const url = await getPreviewURL(resources[index].id);
  1805. downloadFile(url, resources[index].name);
  1806. console.log("Autodownload");
  1807. },
  1808. false
  1809. );
  1810. }
  1811.  
  1812. const i = document.createElement("i");
  1813. i.title = "下载";
  1814. i.classList.add("by-icon-download");
  1815. i.classList.add("btn-icon");
  1816. i.classList.add("visible");
  1817. i.style.cssText = `
  1818. display: inline-block !important;
  1819. visibility: visible !important;
  1820. cursor: pointer !important;
  1821. `;
  1822.  
  1823. // 获取data-v属性
  1824. const dataAttr = Array.from(x.attributes).find((attr) =>
  1825. attr.localName.startsWith("data-v")
  1826. );
  1827. if (dataAttr) {
  1828. i.setAttribute(dataAttr.localName, "");
  1829. }
  1830.  
  1831. i.addEventListener(
  1832. "click",
  1833. async (e) => {
  1834. e.stopPropagation();
  1835. const url = await getPreviewURL(resources[index].id);
  1836. downloadFile(url, resources[index].name);
  1837. },
  1838. false
  1839. );
  1840.  
  1841. if (x.children.length) x.children[0].remove();
  1842. x.insertAdjacentElement("afterbegin", i);
  1843. });
  1844.  
  1845. // "下载全部"按钮
  1846. if (
  1847. !document.getElementById("downloadAllButton") &&
  1848. resources.length > 0
  1849. ) {
  1850. const downloadAllButton = `<div style="display: flex;flex-direction: row;justify-content: end;margin-right: 24px;margin-top: 20px;">
  1851. <button type="button" class="el-button submit-btn el-button--primary" id="downloadAllButton">
  1852. 下载全部
  1853. </button>
  1854. </div>`;
  1855.  
  1856. const resourceList = $x(
  1857. "/html/body/div/div/div[2]/div[2]/div/div/div"
  1858. );
  1859. if (resourceList.length > 0) {
  1860. const containerElement = document.createElement("div");
  1861. containerElement.innerHTML = downloadAllButton;
  1862. resourceList[0].before(containerElement);
  1863.  
  1864. document.getElementById("downloadAllButton").onclick =
  1865. async () => {
  1866. downloading = !downloading;
  1867. if (downloading) {
  1868. document.getElementById("downloadAllButton").innerHTML =
  1869. "取消下载";
  1870. for (let file of resources) {
  1871. if (!downloading) return;
  1872. await downloadFile(
  1873. await getPreviewURL(file.id),
  1874. file.name
  1875. );
  1876. }
  1877. // 下载完成后重置按钮
  1878. if (downloading) {
  1879. downloading = false;
  1880. document.getElementById("downloadAllButton").innerHTML =
  1881. "下载全部";
  1882. }
  1883. } else {
  1884. document.getElementById("downloadAllButton").innerHTML =
  1885. "下载全部";
  1886. }
  1887. };
  1888. }
  1889. }
  1890. }
  1891. } catch (e) {
  1892. console.error("课程主页处理失败", e);
  1893. }
  1894. } else if (location.href == "https://ucloud.bupt.edu.cn/#/") {
  1895. if (settings.betterTitle) {
  1896. const pageTitle = "首页 - 教学云空间";
  1897. document.title = pageTitle;
  1898. }
  1899. }
  1900. // 通知页
  1901. else if (
  1902. location.href ==
  1903. "https://ucloud.bupt.edu.cn/uclass/index.html#/set/notice_fullpage"
  1904. ) {
  1905. if (settings.betterTitle) {
  1906. const pageTitle = "通知 - 教学云空间";
  1907. document.title = pageTitle;
  1908. }
  1909.  
  1910. function processNotifications() {
  1911. const noticeContainer = document.querySelector(
  1912. "#layout-container > div.main-content > div.router-container > div > div > div.setNotice-body > ul"
  1913. );
  1914. if (!noticeContainer) {
  1915. console.log("通知容器未找到");
  1916. return;
  1917. }
  1918. const noticeItems = Array.from(noticeContainer.querySelectorAll("li"));
  1919. if (noticeItems.length === 0) {
  1920. console.log("未找到通知项");
  1921. return;
  1922. }
  1923. if (settings.sortNotificationsByTime) {
  1924. noticeItems.sort((a, b) => {
  1925. const timeA = a.querySelector("span._left-time");
  1926. const timeB = b.querySelector("span._left-time");
  1927. if (!timeA || !timeB) {
  1928. return 0;
  1929. }
  1930. const timeTextA = timeA.textContent.trim();
  1931. const timeTextB = timeB.textContent.trim();
  1932. const dateA = new Date(timeTextA);
  1933. const dateB = new Date(timeTextB);
  1934. return dateB - dateA;
  1935. });
  1936. }
  1937. noticeItems.forEach((item) => {
  1938. if (settings.betterNotificationHighlight) {
  1939. const hasRedDot = item.querySelector(
  1940. "div.el-badge sup.el-badge__content.is-dot"
  1941. );
  1942. if (hasRedDot) {
  1943. item.classList.remove("notification-with-dot");
  1944. item.classList.add("notification-with-dot");
  1945. } else {
  1946. item.classList.remove("notification-with-dot");
  1947. }
  1948. }
  1949. noticeContainer.appendChild(item);
  1950. });
  1951. }
  1952. if (
  1953. settings.sortNotificationsByTime ||
  1954. settings.betterNotificationHighlight
  1955. ) {
  1956. // 等待通知元素加载好了再处理
  1957. const loadingMaskSelector =
  1958. "#layout-container > div.main-content > div.router-container > div > div > div.setNotice-body > div.el-loading-mask";
  1959. const observer = new MutationObserver((mutations) => {
  1960. const loadingMask = document.querySelector(loadingMaskSelector);
  1961. if (loadingMask && loadingMask.style.display === "none") {
  1962. processNotifications();
  1963. observer.disconnect();
  1964. }
  1965. });
  1966.  
  1967. const loadingMask = document.querySelector(loadingMaskSelector);
  1968. if (loadingMask && loadingMask.style.display === "none") {
  1969. processNotifications();
  1970. } else {
  1971. observer.observe(document.body, {
  1972. attributes: true,
  1973. attributeFilter: ["style"],
  1974. subtree: true,
  1975. });
  1976. setTimeout(() => observer.disconnect(), 10000);
  1977. }
  1978. }
  1979. }
  1980. }
  1981. })();