suda.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. // 苏州大学(suda.edu.cn)拾光课程表适配脚本
  2. // Author:BernardYan2357
  3. // ========================== 作息时间表 ==========================
  4. const TimeSlots = [
  5. { number: 1, startTime: "08:00", endTime: "08:45" },
  6. { number: 2, startTime: "08:50", endTime: "09:35" },
  7. { number: 3, startTime: "09:55", endTime: "10:40" },
  8. { number: 4, startTime: "10:45", endTime: "11:30" },
  9. { number: 5, startTime: "11:35", endTime: "12:20" },
  10. { number: 6, startTime: "14:00", endTime: "14:45" },
  11. { number: 7, startTime: "14:50", endTime: "15:35" },
  12. { number: 8, startTime: "15:55", endTime: "16:40" },
  13. { number: 9, startTime: "16:45", endTime: "17:30" },
  14. { number: 10, startTime: "18:30", endTime: "19:15" },
  15. { number: 11, startTime: "19:25", endTime: "20:10" },
  16. { number: 12, startTime: "20:20", endTime: "21:05" }
  17. ];
  18. // ========================== 解析函数 ==========================
  19. /**
  20. * 解析时间行,提取星期、节次、周次
  21. * 示例: "周一第3,4,5节{第1-17周}" / "周二第8,9节{第2-16周|双周}"
  22. */
  23. function parseTimeLine(timeLine) {
  24. const dayMap = { "一": 1, "二": 2, "三": 3, "四": 4, "五": 5, "六": 6, "日": 7, "天": 7 };
  25. const dayM = timeLine.match(/周([一二三四五六日天])/);
  26. if (!dayM) return null;
  27. const day = dayMap[dayM[1]];
  28. const secM = timeLine.match(/第([0-9]+(?:,[0-9]+)*)节/);
  29. if (!secM) return null;
  30. const sections = secM[1].split(",").map(s => parseInt(s, 10)).filter(Number.isFinite);
  31. if (!sections.length) return null;
  32. const braceM = timeLine.match(/\{([^}]*)\}/);
  33. if (!braceM) return null;
  34. const brace = braceM[1];
  35. const rangeM = brace.match(/第(\d+)-(\d+)周/);
  36. if (!rangeM) return null;
  37. const startWeek = parseInt(rangeM[1], 10);
  38. const endWeek = parseInt(rangeM[2], 10);
  39. if (!Number.isFinite(startWeek) || !Number.isFinite(endWeek) || endWeek < startWeek) return null;
  40. let oddEven = "all";
  41. if (/\|单周/.test(brace)) oddEven = "odd";
  42. if (/\|双周/.test(brace)) oddEven = "even";
  43. const weeks = [];
  44. for (let w = startWeek; w <= endWeek; w++) {
  45. if (oddEven === "odd" && w % 2 === 0) continue;
  46. if (oddEven === "even" && w % 2 === 1) continue;
  47. weeks.push(w);
  48. }
  49. return { day, sections, weeks };
  50. }
  51. /**
  52. * 从课表 table 元素中解析所有课程
  53. */
  54. function parseCourseTable(table) {
  55. const anchors = Array.from(table.querySelectorAll("a"));
  56. const courses = [];
  57. for (const a of anchors) {
  58. // innerHTML 确保 <br> 被转为换行符
  59. const rawText = (a.innerHTML || "")
  60. .replace(/<br\s*\/?>/gi, "\n")
  61. .replace(/<[^>]*>/g, "")
  62. .replace(/&nbsp;/gi, " ")
  63. .replace(/\u00a0/g, " ")
  64. .replace(/\r/g, "")
  65. .trim();
  66. // 每个 <a> 的文本结构: 课程名 / 时间行 / 老师 / 地点
  67. const lines = rawText.split("\n").map(s => s.trim()).filter(Boolean);
  68. if (lines.length < 2) continue;
  69. const name = lines[0] || "";
  70. const timeLine = lines[1] || "";
  71. const teacher = lines[2] || "";
  72. const position = lines[3] || "";
  73. const parsed = parseTimeLine(timeLine);
  74. if (!parsed) continue;
  75. courses.push({
  76. name,
  77. teacher,
  78. position,
  79. day: parsed.day,
  80. startSection: Math.min(...parsed.sections),
  81. endSection: Math.max(...parsed.sections),
  82. weeks: parsed.weeks
  83. });
  84. }
  85. // 去重 → 合并相邻节次
  86. return mergeAdjacentSections(deduplicateCourses(courses));
  87. }
  88. /**
  89. * 去除重复课程(同名同老师同地点同时间同周次视为重复)
  90. */
  91. function deduplicateCourses(list) {
  92. const seen = new Set();
  93. return list.filter(c => {
  94. const key = `${c.name}|${c.teacher}|${c.position}|${c.day}|${c.startSection}|${c.endSection}|${c.weeks.join(",")}`;
  95. if (seen.has(key)) return false;
  96. seen.add(key);
  97. return true;
  98. });
  99. }
  100. /**
  101. * 合并同一课程相邻/连续的节次
  102. * 例如: 电工学(二)周一 第2节 + 第3,4,5节 → startSection=2, endSection=5
  103. * 不同 weeks 不合并(如单双周保持独立)
  104. */
  105. function mergeAdjacentSections(list) {
  106. const groupMap = new Map();
  107. for (const c of list) {
  108. const key = `${c.name}|${c.teacher}|${c.position}|${c.day}|${c.weeks.join(",")}`;
  109. if (!groupMap.has(key)) groupMap.set(key, []);
  110. groupMap.get(key).push(c);
  111. }
  112. const merged = [];
  113. for (const entries of groupMap.values()) {
  114. entries.sort((a, b) => a.startSection - b.startSection);
  115. let cur = { ...entries[0] };
  116. for (let i = 1; i < entries.length; i++) {
  117. const next = entries[i];
  118. if (next.startSection <= cur.endSection + 1) {
  119. cur.endSection = Math.max(cur.endSection, next.endSection);
  120. } else {
  121. merged.push(cur);
  122. cur = { ...next };
  123. }
  124. }
  125. merged.push(cur);
  126. }
  127. return merged;
  128. }
  129. /**
  130. * 检测学期开始日期
  131. * 读取 select#xqd(学期1/2)和 select#xnd(学年如"2025-2026")
  132. * @param {Document} [doc=document] - 课表所在的 document(可能来自 iframe)
  133. */
  134. function detectSemesterStartDate(doc) {
  135. try {
  136. doc = doc || document;
  137. const xqdSelect = doc.querySelector('select#xqd');
  138. const xndSelect = doc.querySelector('select#xnd');
  139. if (!xqdSelect) return null;
  140. const semester = parseInt(xqdSelect.value, 10);
  141. let startYear = new Date().getFullYear();
  142. if (xndSelect && xndSelect.value) {
  143. const m = xndSelect.value.match(/(\d{4})/);
  144. if (m) startYear = parseInt(m[1], 10);
  145. }
  146. if (semester === 1) return `${startYear}-09-01`;
  147. if (semester === 2) return `${startYear + 1}-03-02`;
  148. return null;
  149. } catch (e) {
  150. console.warn("检测学期开始日期失败:", e);
  151. return null;
  152. }
  153. }
  154. /**
  155. * 查找课表 table,兼容直接打开课表页和通过 iframe 嵌套的情况
  156. * @returns {HTMLTableElement|null}
  157. */
  158. function findScheduleTable() {
  159. const selector = 'table#Table1.schedule';
  160. // 1. 先在当前文档查找
  161. let table = document.querySelector(selector);
  162. if (table) return table;
  163. // 2. 遍历 iframe 查找(同源才能访问)
  164. const iframes = document.querySelectorAll('iframe');
  165. for (const iframe of iframes) {
  166. try {
  167. const doc = iframe.contentDocument || iframe.contentWindow?.document;
  168. if (!doc) continue;
  169. table = doc.querySelector(selector);
  170. if (table) return table;
  171. // 再查一层嵌套 iframe
  172. for (const inner of doc.querySelectorAll('iframe')) {
  173. try {
  174. const innerDoc = inner.contentDocument || inner.contentWindow?.document;
  175. if (!innerDoc) continue;
  176. table = innerDoc.querySelector(selector);
  177. if (table) return table;
  178. } catch (_) { /* 跨域忽略 */ }
  179. }
  180. } catch (_) { /* 跨域忽略 */ }
  181. }
  182. return null;
  183. }
  184. // ========================== 主流程 ==========================
  185. async function runImportFlow() {
  186. // 1. 开始提示
  187. const confirmed = await AndroidBridgePromise.showAlert(
  188. "苏大课表导入",
  189. "请确保当前页面已显示「学生个人课表」\n导入前请先在页面上选好学年和学期",
  190. "开始导入"
  191. );
  192. if (!confirmed) {
  193. AndroidBridge.showToast("用户取消了导入。");
  194. return;
  195. }
  196. // 2. 查找课表
  197. AndroidBridge.showToast("正在查找课表...");
  198. const table = findScheduleTable();
  199. if (!table) {
  200. AndroidBridge.showToast("未找到课表,请先打开「学生个人课表」页面。");
  201. return;
  202. }
  203. // 3. 解析课程
  204. AndroidBridge.showToast("正在解析课程数据...");
  205. const courses = parseCourseTable(table);
  206. if (courses.length === 0) {
  207. AndroidBridge.showToast("未解析到任何课程,请确认课表已正确加载。");
  208. return;
  209. }
  210. // 4. 保存课程
  211. AndroidBridge.showToast(`正在保存 ${courses.length} 条课程...`);
  212. await AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
  213. // 5. 保存作息时间
  214. AndroidBridge.showToast(`正在导入 ${TimeSlots.length} 个时间段...`);
  215. await AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(TimeSlots));
  216. // 6. 保存配置(select 在课表同一文档中,用 ownerDocument 确保 iframe 场景正确)
  217. const semesterStartDate = detectSemesterStartDate(table.ownerDocument);
  218. const config = {
  219. semesterStartDate: semesterStartDate,
  220. semesterTotalWeeks: 20,
  221. firstDayOfWeek: 1
  222. };
  223. await AndroidBridgePromise.saveCourseConfig(JSON.stringify(config));
  224. // 7. 完成
  225. AndroidBridge.showToast(`课程导入成功,共导入 ${courses.length} 条课程!`);
  226. AndroidBridge.notifyTaskCompletion();
  227. }
  228. runImportFlow();