hbmzu.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. // 湖北民族大学教务系统(jwgl.hbmzu.edu.cn/edu)拾光课表导入适配脚本
  2. // 课表查询页(/edu/report/mySchedule.mvc)直接渲染课表 DOM:
  3. // #container 内每行 tr:首 td 为节次("上午1-2节"…"晚上11-12节"),后续 7 个 td 对应周一~周日
  4. // 每格 .course-block 含 span:课程名、教室、周次、教师(无教室时仅 3 个)
  5. // 注意:无开学日期/总周数接口,config 仅保存作息时间
  6. // 预设作息时间(12 节,含午间/晚间)
  7. const HBMZU_TIME_SLOTS = [
  8. { number: 1, startTime: "08:00", endTime: "08:45" },
  9. { number: 2, startTime: "08:50", endTime: "09:35" },
  10. { number: 3, startTime: "10:05", endTime: "10:50" },
  11. { number: 4, startTime: "10:55", endTime: "11:40" },
  12. { number: 5, startTime: "14:00", endTime: "14:45" },
  13. { number: 6, startTime: "14:50", endTime: "15:35" },
  14. { number: 7, startTime: "16:05", endTime: "16:50" },
  15. { number: 8, startTime: "16:55", endTime: "17:40" },
  16. { number: 9, startTime: "18:30", endTime: "19:15" },
  17. { number: 10, startTime: "19:20", endTime: "20:05" },
  18. { number: 11, startTime: "20:15", endTime: "21:00" },
  19. { number: 12, startTime: "21:05", endTime: "21:50" }
  20. ];
  21. function getErrorMessage(error) {
  22. if (error && typeof error.message === "string" && error.message.trim()) return error.message;
  23. if (typeof error === "string" && error.trim()) return error;
  24. try {
  25. const serialized = JSON.stringify(error);
  26. if (serialized && serialized !== "{}") return serialized;
  27. } catch (_) {
  28. // Ignore serialization failures.
  29. }
  30. return "未知错误";
  31. }
  32. // 收集可访问的 document(frameset 用 <frame>,普通页面用 <iframe>)
  33. function collectDocs() {
  34. const docs = [document];
  35. const frames = document.querySelectorAll("iframe, frame");
  36. for (const f of frames) {
  37. try {
  38. if (f.contentDocument) docs.push(f.contentDocument);
  39. } catch (_) {
  40. // 跨域 frame 无法访问,忽略。
  41. }
  42. }
  43. return docs;
  44. }
  45. // 从各 frame 中查找课表表格(#container 内含 .course-block)
  46. // 注入时机早于 frame 加载完成,故轮询等待 frame 就绪(最长 6 秒)
  47. async function findCourseTable() {
  48. const deadline = Date.now() + 6000;
  49. while (Date.now() < deadline) {
  50. for (const doc of collectDocs()) {
  51. const container = doc.getElementById("container");
  52. if (container && container.querySelector(".course-block")) {
  53. return container;
  54. }
  55. }
  56. await new Promise(resolve => setTimeout(resolve, 300));
  57. }
  58. return null;
  59. }
  60. // 解析周次文本:"3-14周"→3..14;"3-13单周"→3,5,7,9,11,13;"4-14双周"→4,6..14;"14-15周"→14,15
  61. function parseWeeksText(weekStr) {
  62. const text = String(weekStr || "").replace(/\s+/g, "");
  63. const m = text.match(/^(\d+)-(\d+)(单|双)?周$/);
  64. if (!m) return [];
  65. const start = Number(m[1]);
  66. const end = Number(m[2]);
  67. const parity = m[3]; // "单" / "双" / undefined
  68. const weeks = [];
  69. for (let w = start; w <= end; w++) {
  70. if (parity === "单" && w % 2 === 0) continue;
  71. if (parity === "双" && w % 2 === 1) continue;
  72. weeks.push(w);
  73. }
  74. return weeks;
  75. }
  76. // 是否为周次文本("3-14周" / "3-13单周" / "4-14双周" / "12-12周")
  77. // 用于在 3 个 span 时区分"无教室(名-周次-教师)"和"无教师(名-教室-周次)"两种结构
  78. function isWeeksText(text) {
  79. return /^\d+-\d+(单|双)?周$/.test(String(text || "").replace(/\s+/g, ""));
  80. }
  81. // 解析课表表格
  82. // 每行首 td 为节次文本("上午1-2节"→1-2),其后 7 个 td 依次为周一~周日
  83. function parseCourseTable(container) {
  84. const courses = [];
  85. container.querySelectorAll("tr").forEach(row => {
  86. const tds = row.querySelectorAll("td");
  87. if (tds.length < 2) return;
  88. const sectionText = (tds[0].textContent || "").replace(/\s+/g, "");
  89. const sectionMatch = sectionText.match(/(\d+)-(\d+)节/);
  90. if (!sectionMatch) return;
  91. const startSection = Number(sectionMatch[1]);
  92. const endSection = Number(sectionMatch[2]);
  93. for (let i = 0; i < 7; i++) {
  94. const dayTd = tds[i + 1];
  95. if (!dayTd) continue;
  96. const blocks = dayTd.querySelectorAll(".course-block");
  97. for (const block of blocks) {
  98. const cells = [];
  99. block.querySelectorAll("span").forEach(function (s) {
  100. cells.push((s.textContent || "").trim());
  101. });
  102. if (cells.length < 3) continue;
  103. const name = cells[0];
  104. if (!name) continue;
  105. // span 结构:[名称, 教室?, 周次, 教师],教室或教师可能缺失:
  106. // 3 个 span 可能是 [名称, 周次, 教师](无教室),也可能是 [名称, 教室, 周次](无教师)
  107. // 先找出长得像周次的 span:它前面的第一个 span 是教室,后面的第一个 span 是教师
  108. let weekIndex = -1;
  109. let weekStr = "";
  110. for (let j = 1; j < cells.length; j++) {
  111. if (isWeeksText(cells[j])) {
  112. weekIndex = j;
  113. weekStr = cells[j];
  114. break;
  115. }
  116. }
  117. if (weekIndex < 0) continue;
  118. const position = weekIndex >= 2 ? cells[weekIndex - 1] : "";
  119. const teacher = weekIndex + 1 < cells.length ? cells[weekIndex + 1] : "";
  120. const weeks = parseWeeksText(weekStr);
  121. if (weeks.length === 0) continue;
  122. courses.push({
  123. name,
  124. teacher: teacher || "未知",
  125. position: position || "待定",
  126. day: i + 1,
  127. startSection,
  128. endSection,
  129. weeks
  130. });
  131. }
  132. }
  133. });
  134. return courses;
  135. }
  136. // 合并同课程同时间同教室的条目(如单双周分教室的保持两条)
  137. function mergeCourses(courses) {
  138. const merged = new Map();
  139. for (const c of courses) {
  140. const key = `${c.name}|${c.teacher}|${c.position}|${c.day}|${c.startSection}|${c.endSection}`;
  141. const holder = merged.get(key);
  142. if (holder) {
  143. holder.weeks = Array.from(new Set([...holder.weeks, ...c.weeks])).sort((a, b) => a - b);
  144. } else {
  145. merged.set(key, { ...c, weeks: [...c.weeks].sort((a, b) => a - b) });
  146. }
  147. }
  148. return Array.from(merged.values()).sort(
  149. (a, b) => a.day - b.day || a.startSection - b.startSection || a.name.localeCompare(b.name)
  150. );
  151. }
  152. // 保存作息时间
  153. async function saveTimeSlots(timeSlots) {
  154. try {
  155. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  156. } catch (error) {
  157. console.error("JS: 作息时间保存失败", error);
  158. }
  159. }
  160. async function runImportFlow() {
  161. const alertConfirmed = await window.shiguangBridgePromise.showAlert(
  162. "课表导入",
  163. "导入前请确保已登录并打开课表查询页面。",
  164. "好的,开始导入"
  165. );
  166. if (!alertConfirmed) {
  167. window.shiguangBridge.showToast("用户取消了导入。");
  168. return;
  169. }
  170. window.shiguangBridge.showToast("正在获取课表数据...");
  171. try {
  172. const container = await findCourseTable();
  173. if (!container) throw new Error("未找到课表,请确认已登录并打开课表查询页面。");
  174. const courses = parseCourseTable(container);
  175. if (courses.length === 0) throw new Error("课表中未解析到有效课程,请确认当前学期有课。");
  176. const merged = mergeCourses(courses);
  177. window.shiguangBridge.showToast(`正在保存 ${merged.length} 门课程...`);
  178. await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(merged, null, 2));
  179. await saveTimeSlots(HBMZU_TIME_SLOTS);
  180. window.shiguangBridge.showToast(`课程导入成功,共导入 ${merged.length} 门课程!`);
  181. window.shiguangBridge.notifyTaskCompletion();
  182. } catch (error) {
  183. window.shiguangBridge.showToast(`导入失败:${getErrorMessage(error)}`);
  184. console.error("JS: Import Error", error);
  185. }
  186. }
  187. runImportFlow();