fjnu_01.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. // 福建师范大学研究生教务系统(gedu.fjnu.edu.cn)拾光课程表适配。
  2. // 读取当前学期课表 DOM,不读取账号、密码、Cookie 或浏览器存储。
  3. (function () {
  4. "use strict";
  5. // 福建师范大学作息时间,由本校使用者核对提供;每节课 45 分钟。
  6. const presetTimeSlots = [
  7. { number: 1, startTime: "08:20", endTime: "09:05" },
  8. { number: 2, startTime: "09:15", endTime: "10:00" },
  9. { number: 3, startTime: "10:20", endTime: "11:05" },
  10. { number: 4, startTime: "11:15", endTime: "12:00" },
  11. { number: 5, startTime: "14:00", endTime: "14:45" },
  12. { number: 6, startTime: "14:55", endTime: "15:40" },
  13. { number: 7, startTime: "15:50", endTime: "16:35" },
  14. { number: 8, startTime: "16:45", endTime: "17:30" },
  15. { number: 9, startTime: "18:30", endTime: "19:15" },
  16. { number: 10, startTime: "19:25", endTime: "20:10" },
  17. { number: 11, startTime: "20:20", endTime: "21:05" },
  18. { number: 12, startTime: "21:15", endTime: "22:00" }
  19. ];
  20. function text(element) {
  21. return (element ? element.textContent : "").replace(/\u00a0/g, " ").trim();
  22. }
  23. function parseWeeks(value, parity) {
  24. const normalized = value.replace(/第|周|\s/g, "")
  25. .replace(/[,、;;]/g, ",").replace(/[-—–~~至]/g, "-");
  26. const mode = parity.replace(/\s/g, "");
  27. if (!["全部", "单", "双", "单周", "双周"].includes(mode)) {
  28. throw new Error("无法识别单双周:" + parity);
  29. }
  30. const weeks = new Set();
  31. for (const part of normalized.split(",")) {
  32. const match = /^(\d{1,2})(?:-(\d{1,2}))?$/.exec(part);
  33. if (!match) throw new Error("无法识别起始周:" + value);
  34. const start = Number(match[1]);
  35. const end = Number(match[2] || match[1]);
  36. if (start < 1 || end > 53 || end < start) {
  37. throw new Error("起始周超出有效范围:" + value);
  38. }
  39. for (let week = start; week <= end; week++) {
  40. if (mode.startsWith("单") && week % 2 === 0) continue;
  41. if (mode.startsWith("双") && week % 2 !== 0) continue;
  42. weeks.add(week);
  43. }
  44. }
  45. if (weeks.size === 0) throw new Error("起始周与单双周设置没有对应的上课周次。");
  46. return Array.from(weeks).sort((a, b) => a - b);
  47. }
  48. function readFields(block) {
  49. const fields = {};
  50. const details = block.querySelector("table");
  51. if (!details) throw new Error("课程详情结构已变化,请更新适配脚本。");
  52. for (const row of Array.from(details.rows)) {
  53. if (row.cells.length !== 2) throw new Error("课程详情字段结构异常。");
  54. const key = text(row.cells[0]).replace(/[\s::]/g, "");
  55. if (Object.prototype.hasOwnProperty.call(fields, key)) {
  56. throw new Error("课程详情出现重复字段:" + key);
  57. }
  58. fields[key] = text(row.cells[1]);
  59. }
  60. for (const key of ["课程", "校区", "教室", "起始周", "单双周", "教师"]) {
  61. if (!Object.prototype.hasOwnProperty.call(fields, key)) {
  62. throw new Error("课程详情缺少字段:" + key);
  63. }
  64. }
  65. if (!fields["课程"]) throw new Error("课表中存在未命名课程。");
  66. return fields;
  67. }
  68. function parseTable(table) {
  69. const rows = Array.from(table.rows);
  70. const header = rows.shift();
  71. const weekdays = header ? Array.from(header.cells).slice(1).map(text) : [];
  72. if (weekdays.join(",") !== "周一,周二,周三,周四,周五,周六,周日,自定") {
  73. throw new Error("课表星期表头已变化,无法安全确定课程位置。");
  74. }
  75. const courses = [];
  76. const sections = rows.map(row => {
  77. const label = Array.from(row.cells).find(cell => /^第\d+节$/.test(text(cell)));
  78. return label ? Number(text(label).match(/\d+/)[0]) : null;
  79. });
  80. if (sections.filter(n => n !== null).join(",") !== "1,2,3,4,5,6,7,8,9,10,11,12") {
  81. throw new Error("课表节次结构已变化,请更新适配脚本。");
  82. }
  83. const occupied = Array.from({ length: 8 }, () => new Set());
  84. let blockCount = 0;
  85. rows.forEach((row, rowIndex) => {
  86. // 学校保留 rowspan 覆盖的 displaynone 占位格。不能过滤后再按列下标计算星期。
  87. const cells = Array.from(row.cells).filter(cell => cell.classList.contains("cellStyle"));
  88. if (cells.length !== 8) throw new Error("课表列数异常,请等待课表加载完成后重试。");
  89. cells.forEach((cell, dayIndex) => {
  90. const blocks = Array.from(cell.querySelectorAll(".tddiv"));
  91. blockCount += blocks.length;
  92. if (cell.classList.contains("displaynone")) {
  93. if (blocks.length || !occupied[dayIndex].has(rowIndex)) {
  94. throw new Error("课表隐藏单元格与连续节次不一致,请重新查询课表。");
  95. }
  96. return;
  97. }
  98. if (occupied[dayIndex].has(rowIndex)) throw new Error("课表连续节次重叠,无法确定排课位置。");
  99. if (!blocks.length) {
  100. if (text(cell)) throw new Error("存在无法识别的课程内容,请更新适配脚本。");
  101. return;
  102. }
  103. if (dayIndex === 7 || sections[rowIndex] === null) {
  104. throw new Error("课表含自定时间课程,尚无明确星期或节次。请先确认排课后再导入,避免遗漏课程。");
  105. }
  106. const span = Number(cell.getAttribute("rowspan") || "1");
  107. if (!Number.isInteger(span) || span < 1 || rowIndex + span > sections.length ||
  108. sections[rowIndex + span - 1] !== sections[rowIndex] + span - 1) {
  109. throw new Error("课程连续节次无效。");
  110. }
  111. for (let offset = 1; offset < span; offset++) occupied[dayIndex].add(rowIndex + offset);
  112. for (const block of blocks) {
  113. const fields = readFields(block);
  114. courses.push({
  115. name: fields["课程"],
  116. teacher: fields["教师"],
  117. position: [fields["校区"], fields["教室"]].filter(Boolean).join(" "),
  118. day: dayIndex + 1,
  119. startSection: sections[rowIndex],
  120. endSection: sections[rowIndex] + span - 1,
  121. weeks: parseWeeks(fields["起始周"], fields["单双周"])
  122. });
  123. }
  124. });
  125. });
  126. if (blockCount !== table.querySelectorAll(".tddiv").length) {
  127. throw new Error("存在未能解析的课程块,已停止导入。");
  128. }
  129. if (!courses.length) throw new Error("当前学期课表为空,请选择有课程的学期并等待加载完成。");
  130. const merged = new Map();
  131. for (const course of courses) {
  132. const key = JSON.stringify([course.name, course.teacher, course.position,
  133. course.day, course.startSection, course.endSection]);
  134. if (merged.has(key)) {
  135. const previous = merged.get(key);
  136. previous.weeks = Array.from(new Set(previous.weeks.concat(course.weeks))).sort((a, b) => a - b);
  137. } else merged.set(key, course);
  138. }
  139. return Array.from(merged.values());
  140. }
  141. async function runImportFlow() {
  142. const bridge = window.shiguangBridgePromise;
  143. if (window.location.origin !== "https://gedu.fjnu.edu.cn" ||
  144. !/^#\/timeTable(?:[/?]|$)/.test(window.location.hash) ||
  145. window.location.pathname !== "/index") {
  146. throw new Error("请先登录福建师范大学研究生教务系统,进入“课程表”并选择目标学期,再执行导入。");
  147. }
  148. const table = document.querySelector("table#table1.timeTable");
  149. if (!table || document.querySelector(".el-loading-mask:not([style*='display: none'])")) {
  150. throw new Error("课表尚未加载完成,请稍后重试。");
  151. }
  152. const semester = document.querySelector('input[placeholder="学期"]');
  153. if (!semester || !semester.value.trim()) throw new Error("未识别到当前学期,请重新查询课表。");
  154. const snapshot = table.outerHTML;
  155. const semesterName = semester.value;
  156. const courses = parseTable(table);
  157. const lastWeek = Math.max(...courses.flatMap(course => course.weeks));
  158. const confirmed = await bridge.showAlert("福建师范大学研究生课表",
  159. "当前学期:" + semesterName + "\n识别到 " + courses.length +
  160. " 条课程安排,最晚上课周为第 " + lastWeek + " 周。将替换所选课表中的课程。" +
  161. "\n\n同时导入学校作息时间,共 12 节(08:20—22:00),替换所选课表的节次时间。" +
  162. "本页不提供第1周日期,导入后请在课表设置中核对学期开始日期和总周数。", "确认并导入");
  163. if (!confirmed) return;
  164. if (table.outerHTML !== snapshot || semester.value !== semesterName || !table.isConnected) {
  165. throw new Error("导入期间课表发生变化,请等待加载完成后重新导入。");
  166. }
  167. // saveCourseConfig 会重置未提供的日期/时长字段,因此保留用户手动设置的配置。
  168. await bridge.saveImportedCourses(JSON.stringify(courses));
  169. try {
  170. await bridge.savePresetTimeSlots(JSON.stringify(presetTimeSlots));
  171. } catch (error) {
  172. throw new Error("课程已保存,但作息时间导入失败,请重新执行导入:" + error.message);
  173. }
  174. window.shiguangBridge.showToast("福建师范大学研究生课表导入成功。");
  175. window.shiguangBridge.notifyTaskCompletion();
  176. }
  177. runImportFlow().catch(async function (error) {
  178. if (window.shiguangBridgePromise) {
  179. await window.shiguangBridgePromise.showAlert("导入未完成", error.message, "知道了");
  180. } else if (window.shiguangBridge) {
  181. window.shiguangBridge.showToast("请在拾光课程表内执行适配脚本。");
  182. }
  183. });
  184. })();