bjtu.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. // 北京交通大学 (bjtu.edu.cn) 拾光课程表适配脚本
  2. // 教学支撑平台 aa.bjtu.edu.cn(Django),课表页 GET /course_selection/courseselect/stuschedule/
  3. // 学期切换为页面下拉链接 ?xnxq=<学期>,作息时间在表格每行首格(第N节 [HH:MM-HH:MM]),直接提取
  4. // 课程块:课号 / span=color000 课名 / div 内 周次+<i>教师 / span.text-muted 地点;同格可多门(单双周)
  5. // 解析周次:"第01-16周" / "第01, 03, 05周" / "第05-12周"
  6. function parseWeeks(text) {
  7. const matched = String(text).match(/第(.+?)周/);
  8. if (!matched) return [];
  9. const weeks = [];
  10. for (const part of matched[1].split(/[,,]/)) {
  11. const range = part.trim().match(/^(\d+)-(\d+)$/);
  12. if (range) {
  13. for (let w = Number(range[1]); w <= Number(range[2]); w++) weeks.push(w);
  14. } else if (/^\d+$/.test(part.trim())) {
  15. weeks.push(Number(part.trim()));
  16. }
  17. }
  18. return [...new Set(weeks)].sort((a, b) => a - b);
  19. }
  20. // 解析课表 HTML:返回 { timeSlots, courses, semesterOptions }
  21. function parseScheduleHtml(html) {
  22. const doc = new DOMParser().parseFromString(html, "text/html");
  23. const semesterOptions = Array.from(doc.querySelectorAll("a[href*='xnxq=']"))
  24. .map(a => ({
  25. href: a.getAttribute("href"),
  26. text: a.textContent.trim().replace(/\s+学期$/, "")
  27. }))
  28. .filter(o => /^\d{4}-\d{4}-\d$/.test(o.text));
  29. // 下拉列表首位是下学期(无课),当前学期以标题按钮文本为准
  30. const currentText = (doc.querySelector(".widget-header button") || {}).textContent || "";
  31. const currentSemester = currentText.match(/\d{4}-\d{4}-\d/);
  32. const defaultIndex = currentSemester
  33. ? Math.max(0, semesterOptions.findIndex(o => o.text === currentSemester[0]))
  34. : 0;
  35. const table = doc.querySelector("table.table-bordered");
  36. if (!table) throw new Error("未找到课表格子,请确认已登录教学支撑平台。");
  37. const timeSlots = [];
  38. const courses = [];
  39. const seen = new Set();
  40. const rows = Array.from(table.rows);
  41. for (const row of rows) {
  42. if (row.cells.length < 8) continue;
  43. const head = row.cells[0].textContent;
  44. const sectionMatch = head.match(/第(\d+)节/);
  45. const timeMatch = head.match(/\[(\d{2}:\d{2})-(\d{2}:\d{2})\]/);
  46. if (!sectionMatch || !timeMatch) continue;
  47. const section = Number(sectionMatch[1]);
  48. timeSlots.push({ number: section, startTime: timeMatch[1], endTime: timeMatch[2] });
  49. for (let day = 1; day <= 7; day++) {
  50. const td = row.cells[day];
  51. if (!td) continue;
  52. for (const block of Array.from(td.children).filter(el => el.tagName === "DIV")) {
  53. const nameEl = block.querySelector("span[style*='color:#000']");
  54. const teacherEl = block.querySelector("i");
  55. const locationEl = block.querySelector("span.text-muted");
  56. if (!nameEl || !teacherEl || !locationEl) continue;
  57. const weekDiv = teacherEl.parentElement;
  58. const weeks = parseWeeks(weekDiv ? weekDiv.textContent : "");
  59. if (!weeks.length) continue;
  60. const name = nameEl.textContent.trim().replace(/\s*\[[^\]]+\]\s*$/, "");
  61. if (!name) continue;
  62. // 地点"海淀东校区, 东区一教, DQ409"只取最后一段,"地点见…通知"等整句保留
  63. const locationText = locationEl.textContent.trim();
  64. const location = locationText.split(",").pop().trim();
  65. const course = {
  66. name: name,
  67. teacher: teacherEl.textContent.trim(),
  68. position: location,
  69. day: day,
  70. startSection: section,
  71. endSection: section,
  72. weeks: weeks
  73. };
  74. const key = JSON.stringify(course);
  75. if (seen.has(key)) continue;
  76. seen.add(key);
  77. courses.push(course);
  78. }
  79. }
  80. }
  81. if (!timeSlots.length) throw new Error("课表为空或结构异常。");
  82. courses.sort((a, b) => a.day - b.day || a.startSection - b.startSection || a.name.localeCompare(b.name));
  83. return { timeSlots, courses, semesterOptions, defaultIndex };
  84. }
  85. async function promptUserToStart() {
  86. return await window.shiguangBridgePromise.showAlert(
  87. "教务系统课表导入", "导入前请确保您已在浏览器中成功登录北京交通大学教学支撑平台", "好的,开始导入");
  88. }
  89. // 请求课表页(学期列表与默认学期都在首屏返回里)
  90. async function fetchSchedulePage(query) {
  91. const resp = await fetch("/course_selection/courseselect/stuschedule/" + (query || ""),
  92. { method: "GET", credentials: "include" });
  93. if (!resp.ok) throw new Error("课表请求失败(HTTP " + resp.status + ")");
  94. return resp.text();
  95. }
  96. async function runImportFlow() {
  97. if (!await promptUserToStart()) { window.shiguangBridge.showToast("用户取消了导入。"); return; }
  98. try {
  99. window.shiguangBridge.showToast("正在请求课表数据...");
  100. const defaultHtml = await fetchSchedulePage("");
  101. const { semesterOptions, defaultIndex } = parseScheduleHtml(defaultHtml);
  102. let html = defaultHtml;
  103. if (semesterOptions.length > 1) {
  104. const index = await window.shiguangBridgePromise.showSingleSelection(
  105. "选择学期", JSON.stringify(semesterOptions.map(o => o.text)), defaultIndex);
  106. if (index === null || index === -1) { window.shiguangBridge.showToast("用户取消了导入。"); return; }
  107. if (index !== defaultIndex) html = await fetchSchedulePage(semesterOptions[index].href);
  108. }
  109. const { timeSlots, courses } = parseScheduleHtml(html);
  110. if (!courses.length) throw new Error("未解析到课程,请确认所选学期有课。");
  111. await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
  112. try { await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots)); } catch (e) {}
  113. window.shiguangBridge.showToast("课程导入成功,共导入 " + courses.length + " 门课程!");
  114. window.shiguangBridge.notifyTaskCompletion();
  115. } catch (e) {
  116. window.shiguangBridge.showToast("导入失败:" + e.message);
  117. console.error("JS: Import Error", e);
  118. }
  119. }
  120. runImportFlow();