masu.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. // 马鞍山学院教务系统(jwxt.masu.edu.cn)拾光课表导入适配脚本
  2. // 青果/URP 金刚教务:课表渲染后直接解析 #manualArrangeCourseTable 表格,
  3. // 课程文本格式:课程名(序号) (教师) (周次,教室),一个单元格可能含多门课
  4. // 预设作息时间(该校实际 11 节)
  5. const MASU_TIME_SLOTS = [
  6. { number: 1, startTime: "08:20", endTime: "09:05" },
  7. { number: 2, startTime: "09:10", endTime: "09:55" },
  8. { number: 3, startTime: "10:15", endTime: "11:00" },
  9. { number: 4, startTime: "11:05", endTime: "11:50" },
  10. { number: 5, startTime: "13:50", endTime: "14:35" },
  11. { number: 6, startTime: "14:40", endTime: "15:25" },
  12. { number: 7, startTime: "15:45", endTime: "16:30" },
  13. { number: 8, startTime: "16:35", endTime: "17:20" },
  14. { number: 9, startTime: "18:20", endTime: "19:05" },
  15. { number: 10, startTime: "19:10", endTime: "19:55" },
  16. { number: 11, startTime: "20:00", endTime: "20:45" }
  17. ];
  18. function getErrorMessage(error) {
  19. if (error && typeof error.message === "string" && error.message.trim()) return error.message;
  20. if (typeof error === "string" && error.trim()) return error;
  21. try {
  22. const serialized = JSON.stringify(error);
  23. if (serialized && serialized !== "{}") return serialized;
  24. } catch (_) {
  25. // Ignore serialization failures and use the generic fallback below.
  26. }
  27. return "未知错误";
  28. }
  29. // 从课表页 HTML 提取当前学期 id(semesterCalendar 配置中的 value:"403"),仅用于日志
  30. function extractCurrentSemesterId() {
  31. const html = document.documentElement.outerHTML;
  32. const m = html.match(/value:"(\d+)"/);
  33. return m ? m[1] : null;
  34. }
  35. // 去掉课程名末尾的序号括号:"形势与政策1(202620271.11202001.002)" → "形势与政策1"
  36. function cleanCourseName(name) {
  37. return String(name || "").replace(/[((][^(())]*[))]\s*$/, "").trim();
  38. }
  39. // 解析周次文本:支持 "2-5"、"双周2-6、10"、"2-3、6-7、9-14"、"6-7、9-11、13、16" 等
  40. function parseWeeksText(weekStr) {
  41. let isOdd = false;
  42. let isEven = false;
  43. let s = String(weekStr || "").trim();
  44. if (s.includes("双周") || s.startsWith("双")) isEven = true;
  45. if (s.includes("单周") || s.startsWith("单")) isOdd = true;
  46. s = s.replace(/单周|双周|[单双]|第/g, "").replace(/周/g, "");
  47. const weeks = new Set();
  48. s.split(/[、,,;;]/).forEach(part => {
  49. part = part.trim();
  50. if (!part) return;
  51. const range = part.match(/^(\d+)-(\d+)$/);
  52. if (range) {
  53. const start = Number(range[1]);
  54. const end = Number(range[2]);
  55. for (let i = start; i <= end; i++) {
  56. if (isEven && i % 2 !== 0) continue;
  57. if (isOdd && i % 2 === 0) continue;
  58. weeks.add(i);
  59. }
  60. } else if (/^\d+$/.test(part)) {
  61. const n = Number(part);
  62. if (isEven && n % 2 !== 0) return;
  63. if (isOdd && n % 2 === 0) return;
  64. weeks.add(n);
  65. }
  66. });
  67. return Array.from(weeks).sort((a, b) => a - b);
  68. }
  69. // 解析单元格文本中的单门课
  70. // 文本格式:课程名(序号) (教师) (周次,教室),教室可能含括号如 数智楼(3号实验楼)309
  71. const COURSE_RE = /([^\s()]+)\([^()]*\)\s*\(([^()]*)\)\s*\(([^()]*(?:\([^()]*\)[^()]*)*)\)/g;
  72. function parseCourseText(text, day, startSection, endSection, courses) {
  73. let m;
  74. COURSE_RE.lastIndex = 0;
  75. while ((m = COURSE_RE.exec(text)) !== null) {
  76. const name = cleanCourseName(m[1]);
  77. const teacher = String(m[2] || "").trim();
  78. const weeksRoom = m[3];
  79. if (!name || !weeksRoom) continue;
  80. // 周次段 "周次,教室":按第一个逗号切分(教室可能含括号但无逗号)
  81. const commaIdx = weeksRoom.indexOf(",");
  82. if (commaIdx === -1) continue;
  83. const weeksStr = weeksRoom.slice(0, commaIdx).trim();
  84. const room = weeksRoom.slice(commaIdx + 1).trim();
  85. const weeks = parseWeeksText(weeksStr);
  86. if (weeks.length === 0) continue;
  87. courses.push({
  88. name,
  89. teacher: teacher || "未知",
  90. position: room || "待定",
  91. day,
  92. startSection,
  93. endSection,
  94. weeks
  95. });
  96. }
  97. }
  98. // 从渲染后的课表表格解析全部课程
  99. function parseCourseTableFromDom() {
  100. const table = document.querySelector("#manualArrangeCourseTable");
  101. if (!table) return null;
  102. const unitCount = 11; // 该校 11 节课
  103. const courses = [];
  104. const seen = new Set();
  105. table.querySelectorAll("td[id^='TD']").forEach(td => {
  106. const idMatch = td.id.match(/^TD(\d+)_/);
  107. if (!idMatch) return;
  108. const n = Number(idMatch[1]);
  109. const day = Math.floor(n / unitCount) + 1;
  110. const startSection = (n % unitCount) + 1;
  111. const rowspan = Number(td.getAttribute("rowspan")) || 1;
  112. const endSection = startSection + rowspan - 1;
  113. const text = (td.textContent || "").replace(/\s+/g, " ").trim();
  114. if (!text) return;
  115. parseCourseText(text, day, startSection, endSection, courses);
  116. });
  117. // 去重(同一课名/星期/节次/教师/教室可能重复出现)
  118. const deduped = courses.filter(c => {
  119. const key = `${c.name}|${c.day}|${c.startSection}|${c.endSection}|${c.teacher}|${c.position}|${c.weeks.join(',')}`;
  120. if (seen.has(key)) return false;
  121. seen.add(key);
  122. return true;
  123. });
  124. return deduped.sort((a, b) => a.day - b.day || a.startSection - b.startSection || a.name.localeCompare(b.name));
  125. }
  126. // 保存预设作息时间(失败仅告警,不阻断课程导入)
  127. async function saveTimeSlots(timeSlots) {
  128. if (!timeSlots || timeSlots.length === 0) return;
  129. try {
  130. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  131. console.log("JS: 作息时间保存成功");
  132. } catch (error) {
  133. console.error("JS: 作息时间保存失败:", error);
  134. }
  135. }
  136. async function runImportFlow() {
  137. const alertConfirmed = await window.shiguangBridgePromise.showAlert(
  138. "教务系统课表导入",
  139. "导入前请确保您已登录教务系统,建议在课表页面进行导入",
  140. "好的,开始导入"
  141. );
  142. if (!alertConfirmed) {
  143. window.shiguangBridge.showToast("用户取消了导入。");
  144. return;
  145. }
  146. window.shiguangBridge.showToast("正在解析课表...");
  147. try {
  148. const semesterId = extractCurrentSemesterId();
  149. const courses = parseCourseTableFromDom();
  150. if (!courses || courses.length === 0) throw new Error("未在课表页面检测到有效课程,请确认课表已加载。");
  151. console.log(`JS: 当前学期 ${semesterId || "未知"},解析到 ${courses.length} 条课程`);
  152. window.shiguangBridge.showToast(`正在保存 ${courses.length} 门课程...`);
  153. await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses, null, 2));
  154. await saveTimeSlots(MASU_TIME_SLOTS);
  155. window.shiguangBridge.showToast(`课程导入成功,共导入 ${courses.length} 门课程!`);
  156. window.shiguangBridge.notifyTaskCompletion();
  157. } catch (error) {
  158. window.shiguangBridge.showToast(`导入失败:${getErrorMessage(error)}`);
  159. console.error("JS: Import Error:", error);
  160. }
  161. }
  162. runImportFlow();