jdzvua.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. // 景德镇艺术职业大学 (jdzvua.edu.cn) 拾光课程表适配脚本
  2. // 纯 HTML 抓取:课表在 iframe 内 #manualArrangeCourseTable,切换学期无网络请求,
  3. // 请先在页面上切换到目标学期课表再执行导入
  4. // 课程信息在 td.infoTitle 的 title 属性:`课程名(编码) (教师);(周次,地点;...` 成对出现,
  5. // td id 形如 TD{星期0-6}{节次-1}_0,rowspan 为跨节数
  6. // 作息按楼栋分两组(08:00 起 / 08:20 起),课程覆盖多的一组写入预设作息,
  7. // 少的一组课程按其楼栋时间写入自定义时间
  8. const TIME_SLOT_A = [
  9. { number: 1, startTime: "08:20", endTime: "09:05" },
  10. { number: 2, startTime: "09:15", endTime: "10:00" },
  11. { number: 3, startTime: "10:35", endTime: "11:20" },
  12. { number: 4, startTime: "11:30", endTime: "12:15" },
  13. { number: 5, startTime: "14:20", endTime: "15:05" },
  14. { number: 6, startTime: "15:15", endTime: "16:00" },
  15. { number: 7, startTime: "16:35", endTime: "17:20" },
  16. { number: 8, startTime: "17:30", endTime: "18:15" },
  17. { number: 9, startTime: "19:20", endTime: "20:05" },
  18. { number: 10, startTime: "20:15", endTime: "21:00" }
  19. ];
  20. const TIME_SLOT_B = [
  21. { number: 1, startTime: "08:00", endTime: "08:45" },
  22. { number: 2, startTime: "08:55", endTime: "09:40" },
  23. { number: 3, startTime: "10:25", endTime: "11:10" },
  24. { number: 4, startTime: "11:20", endTime: "12:05" },
  25. { number: 5, startTime: "14:00", endTime: "14:45" },
  26. { number: 6, startTime: "14:55", endTime: "15:40" },
  27. { number: 7, startTime: "16:25", endTime: "17:10" },
  28. { number: 8, startTime: "17:20", endTime: "18:05" },
  29. { number: 9, startTime: "19:20", endTime: "20:05" },
  30. { number: 10, startTime: "20:15", endTime: "21:00" }
  31. ];
  32. // 08:20 起的楼栋组
  33. const BUILDINGS_A = ["上茶苑教6栋", "上茶苑教8栋", "芭蕉苑教14栋", "芭蕉苑教16栋",
  34. "芭蕉苑教18栋", "芭蕉苑教20栋", "长芗苑教10栋", "长芗苑教12栋", "香樟苑教2栋", "香樟苑教4栋"];
  35. // 08:00 起的楼栋组
  36. const BUILDINGS_B = ["上茶苑教7栋", "上茶苑教9栋", "体育馆", "梧桐苑", "芭蕉苑教15栋",
  37. "芭蕉苑教17栋", "芭蕉苑教19栋", "芭蕉苑教21栋", "长芗苑教11栋", "长芗苑教13栋",
  38. "香樟苑教1栋", "香樟苑教3栋", "香樟苑教5栋"];
  39. // 周次:"16-17" / "2-6 9-12" / "18",空格分隔多段
  40. function parseWeeks(text) {
  41. const weeks = [];
  42. for (const part of String(text).trim().split(/\s+/)) {
  43. const range = part.match(/^(\d+)-(\d+)$/);
  44. if (range) {
  45. for (let w = Number(range[1]); w <= Number(range[2]); w++) weeks.push(w);
  46. } else if (/^\d+$/.test(part)) {
  47. weeks.push(Number(part));
  48. }
  49. }
  50. return [...new Set(weeks)].sort((a, b) => a - b);
  51. }
  52. // 地点末尾的校区标记 (景艺) 在 title 里常缺右括号或多一个右括号,按实际形态归一
  53. function normalizeRoom(room) {
  54. const text = String(room).trim();
  55. if (/\(景艺$/.test(text)) return text + ")";
  56. if (/\(景艺\)\)$/.test(text)) return text.slice(0, -1);
  57. return text;
  58. }
  59. // 解析单个课表格子,title 按分号切成「课程/周次地点」交替的 token
  60. function parseCell(id, rowspan, title) {
  61. const matched = String(id).match(/^TD(\d+)_/);
  62. if (!matched) return [];
  63. const n = Number(matched[1]);
  64. const day = Math.floor(n / 10) + 1;
  65. const startSection = (n % 10) + 1;
  66. const endSection = startSection + (Number(rowspan) || 1) - 1;
  67. if (day > 7 || endSection > 10) return [];
  68. const courses = [];
  69. const tokens = String(title).split(";").map(s => s.trim()).filter(Boolean);
  70. for (let i = 0; i < tokens.length; i++) {
  71. if (tokens[i].charAt(0) === "(") continue;
  72. const info = tokens[i + 1];
  73. if (!info || info.charAt(0) !== "(") continue;
  74. i++;
  75. const body = info.slice(1);
  76. const comma = body.indexOf(",");
  77. if (comma < 0) continue;
  78. const weeks = parseWeeks(body.slice(0, comma));
  79. if (weeks.length === 0) continue;
  80. const head = tokens[i - 1].match(/^(.+?)\(([^)]+)\)\s*\(([^)]*)\)/);
  81. courses.push({
  82. name: head ? head[1] : tokens[i - 1],
  83. teacher: head ? head[3] : "",
  84. position: normalizeRoom(body.slice(comma + 1)),
  85. day: day,
  86. startSection: startSection,
  87. endSection: endSection,
  88. weeks: weeks
  89. });
  90. }
  91. return courses;
  92. }
  93. // 在当前文档与各级 frame 中查找课表
  94. function findCourseTable(doc) {
  95. const table = doc.querySelector("#manualArrangeCourseTable");
  96. if (table) return table;
  97. for (const frame of doc.querySelectorAll("iframe,frame")) {
  98. try {
  99. const found = findCourseTable(frame.contentDocument);
  100. if (found) return found;
  101. } catch (e) {
  102. // 跨域 frame 取不到 contentDocument,跳过
  103. }
  104. }
  105. return null;
  106. }
  107. // 楼栋匹配作息组,未匹配(如实践场地)返回 null 用预设作息
  108. function groupOfPosition(position) {
  109. for (const building of BUILDINGS_A) if (position.includes(building)) return TIME_SLOT_A;
  110. for (const building of BUILDINGS_B) if (position.includes(building)) return TIME_SLOT_B;
  111. return null;
  112. }
  113. // 按所在作息组写入自定义时间(两组作息仅少数节次不同)
  114. function applyCustomTime(course, group) {
  115. const start = group.find(s => s.number === course.startSection);
  116. const end = group.find(s => s.number === course.endSection);
  117. if (!start || !end) return course;
  118. return { ...course, isCustomTime: true, customStartTime: start.startTime, customEndTime: end.endTime };
  119. }
  120. async function runImportFlow() {
  121. const confirmed = await window.shiguangBridgePromise.showAlert(
  122. "教务系统课表导入",
  123. "导入前请先在页面上切换到要导入的学期课表,再点击导入",
  124. "好的,开始导入");
  125. if (!confirmed) { window.shiguangBridge.showToast("用户取消了导入。"); return; }
  126. const table = findCourseTable(document);
  127. if (!table) {
  128. window.shiguangBridge.showToast("导入失败:未找到课表,请确认已打开课表页面。");
  129. return;
  130. }
  131. const courses = [];
  132. for (const td of table.querySelectorAll('td[id^="TD"]')) {
  133. const title = td.getAttribute("title");
  134. if (!title) continue;
  135. courses.push(...parseCell(td.id, td.getAttribute("rowspan"), title));
  136. }
  137. if (courses.length === 0) {
  138. window.shiguangBridge.showToast("未解析到课程,请确认课表已加载。");
  139. return;
  140. }
  141. // 统计两组楼栋各覆盖多少课程:覆盖多的一组直接写入预设作息,
  142. // 少的一组课程按其楼栋时间写入自定义时间,数量相同时默认 08:00 组
  143. let countA = 0;
  144. let countB = 0;
  145. for (const course of courses) {
  146. const group = groupOfPosition(course.position);
  147. if (group === TIME_SLOT_A) countA++;
  148. else if (group === TIME_SLOT_B) countB++;
  149. }
  150. const majority = countA > countB ? TIME_SLOT_A : TIME_SLOT_B;
  151. const result = courses.map(course => {
  152. const group = groupOfPosition(course.position);
  153. return group && group !== majority ? applyCustomTime(course, group) : course;
  154. });
  155. result.sort((a, b) => a.day - b.day || a.startSection - b.startSection || a.name.localeCompare(b.name));
  156. try {
  157. await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(result));
  158. } catch (e) {
  159. window.shiguangBridge.showToast("课程保存失败: " + e.message);
  160. return;
  161. }
  162. try { await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(majority)); } catch (e) {}
  163. window.shiguangBridge.showToast("课程导入成功,共导入 " + result.length + " 门课程!");
  164. window.shiguangBridge.notifyTaskCompletion();
  165. }
  166. runImportFlow();