lidapoly.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. // 上海立达学院 (lidapoly.edu.cn) 拾光课程表适配脚本
  2. // 强智教务 jsxsd,课表与校历均通过接口请求 HTML 解析,不依赖当前页面:
  3. // 课表 GET /shldzyjsxy_jsxsd/xskb/xskb_list.do?xnxq01id=<学期>
  4. // 校历 POST /shldzyjsxy_jsxsd/jxzl/jxzl_query (xnxq01id)
  5. // #kbtable 每大节一行、每天一格;详情 div (class kbcontent) 含 课名[学分][性质] / 老师 /
  6. // 班级(其内文本含 `周次(全部)[节次]`) / 上课地点;同一课程重复出现在覆盖的多个大节行,需去重
  7. // 校历 #kbtable 行 td[0]=周次、td[1].title=该周周一日期
  8. const LIDAPOLY_TIME_SLOTS = [
  9. { number: 1, startTime: "08:30", endTime: "09:10" },
  10. { number: 2, startTime: "09:15", endTime: "09:55" },
  11. { number: 3, startTime: "10:05", endTime: "10:45" },
  12. { number: 4, startTime: "10:50", endTime: "11:30" },
  13. { number: 5, startTime: "12:30", endTime: "13:10" },
  14. { number: 6, startTime: "13:15", endTime: "13:55" },
  15. { number: 7, startTime: "14:05", endTime: "14:45" },
  16. { number: 8, startTime: "14:50", endTime: "15:30" },
  17. { number: 9, startTime: "15:40", endTime: "16:20" },
  18. { number: 10, startTime: "16:25", endTime: "17:05" },
  19. { number: 11, startTime: "17:15", endTime: "17:55" },
  20. { number: 12, startTime: "18:00", endTime: "18:40" },
  21. { number: 13, startTime: "18:50", endTime: "19:30" },
  22. { number: 14, startTime: "19:35", endTime: "20:15" },
  23. { number: 15, startTime: "20:25", endTime: "21:05" },
  24. { number: 16, startTime: "21:10", endTime: "21:50" }
  25. ];
  26. // 周次串:"1-16" / "1,3-5" → [1,3,4,5]
  27. function parseWeeks(text) {
  28. const weeks = [];
  29. for (const part of String(text).split(/[,,]/)) {
  30. const range = part.match(/^(\d+)-(\d+)$/);
  31. if (range) {
  32. for (let w = Number(range[1]); w <= Number(range[2]); w++) weeks.push(w);
  33. } else if (/^\d+$/.test(part)) {
  34. weeks.push(Number(part));
  35. }
  36. }
  37. return [...new Set(weeks)].sort((a, b) => a - b);
  38. }
  39. // 课名 = 详情 div 第一个 <br> 前的文本,去掉尾部 [学分][性质]
  40. function courseNameOf(div) {
  41. let text = "";
  42. for (const node of div.childNodes) {
  43. if (node.nodeName === "BR") break;
  44. text += node.textContent || "";
  45. }
  46. return text.trim().replace(/\s*\[[0-9.]+\]\[[^\]]+\]\s*$/, "").trim();
  47. }
  48. // 解析详情 div:周次节次取自 班级 font 内的 `1-16(全部)[01-02-03节]`
  49. function parseDetailDiv(div, day) {
  50. const classFont = div.querySelector("font[title=班级]");
  51. if (!classFont) return null;
  52. const info = (classFont.textContent || "").replace(/\s+/g, "");
  53. const matched = info.match(/([\d,\-]+)\(([^)]*)\)\[(\d[\d\-]*)节\]$/);
  54. if (!matched) return null;
  55. const isOdd = matched[2].includes("单");
  56. const isEven = matched[2].includes("双");
  57. const weeks = parseWeeks(matched[1])
  58. .filter(w => (!isOdd || w % 2 === 1) && (!isEven || w % 2 === 0));
  59. if (weeks.length === 0) return null;
  60. const nums = matched[3].split("-").map(Number);
  61. const startSection = Math.min(...nums);
  62. const endSection = Math.max(...nums);
  63. if (!startSection || endSection > 16) return null;
  64. const name = courseNameOf(div);
  65. if (!name) return null;
  66. const teacherFont = div.querySelector("font[title=老师]");
  67. const roomFont = div.querySelector("font[title=上课地点]");
  68. return {
  69. name: name,
  70. teacher: teacherFont ? teacherFont.textContent.trim() : "",
  71. position: roomFont ? roomFont.textContent.trim() : "",
  72. day: day,
  73. startSection: startSection,
  74. endSection: endSection,
  75. weeks: weeks
  76. };
  77. }
  78. // 请求课表页 HTML(不带参数时为系统默认学期)
  79. async function fetchScheduleDoc(semesterId) {
  80. let url = "/shldzyjsxy_jsxsd/xskb/xskb_list.do";
  81. if (semesterId) url += "?xnxq01id=" + encodeURIComponent(semesterId);
  82. const resp = await fetch(url, { method: "GET", credentials: "include" });
  83. if (!resp.ok) throw new Error("课表请求失败(HTTP " + resp.status + ")");
  84. return new DOMParser().parseFromString(await resp.text(), "text/html");
  85. }
  86. // 课表页学年学期下拉:全部选项 + 选中项
  87. function readSemesterOptions(doc) {
  88. const sel = doc.querySelector("#xnxq01id");
  89. if (!sel) return null;
  90. const options = Array.from(sel.options)
  91. .map(opt => String(opt.value).trim())
  92. .filter(value => /^\d{4}-\d{4}-\d$/.test(value));
  93. if (options.length === 0) return null;
  94. const selectedValue = sel.selectedOptions && sel.selectedOptions[0]
  95. ? String(sel.selectedOptions[0].value).trim() : "";
  96. const defaultIndex = Math.max(0, options.indexOf(selectedValue));
  97. return { options, defaultIndex };
  98. }
  99. // 从课表页 #kbtable 解析课程,同一课程重复出现在多个大节行需去重
  100. function parseCourses(doc) {
  101. const map = new Map();
  102. const table = doc.querySelector("#kbtable");
  103. for (const row of table.rows) {
  104. if (row.cells.length < 8) continue; // 表头、备注行
  105. for (let i = 1; i <= 7; i++) {
  106. const td = row.cells[i];
  107. if (!td) continue;
  108. for (const div of td.querySelectorAll("div.kbcontent")) {
  109. const course = parseDetailDiv(div, i);
  110. if (!course) continue;
  111. const key = [course.name, course.teacher, course.position, course.day,
  112. course.startSection, course.endSection, course.weeks.join(",")].join("|");
  113. if (!map.has(key)) map.set(key, course);
  114. }
  115. }
  116. }
  117. return Array.from(map.values()).sort((a, b) =>
  118. a.day - b.day || a.startSection - b.startSection || a.name.localeCompare(b.name));
  119. }
  120. // 教学周历:第 1 周周一即开学日期,有效行数即总周数
  121. async function fetchSemesterConfig(semesterId) {
  122. try {
  123. const resp = await fetch("/shldzyjsxy_jsxsd/jxzl/jxzl_query?Ves632DSdyV=NEW_XSD_WDZM", {
  124. method: "POST",
  125. credentials: "include",
  126. headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" },
  127. body: "xnxq01id=" + encodeURIComponent(semesterId)
  128. });
  129. if (!resp.ok) return null;
  130. const doc = new DOMParser().parseFromString(await resp.text(), "text/html");
  131. let startDate = "";
  132. let totalWeeks = 0;
  133. for (const row of doc.querySelectorAll("#kbtable tr")) {
  134. const cells = row.cells;
  135. if (!cells || cells.length < 2) continue;
  136. const weekNo = cells[0].textContent.trim();
  137. if (!/^\d+$/.test(weekNo)) continue;
  138. totalWeeks++;
  139. if (Number(weekNo) === 1) {
  140. const matched = (cells[1].getAttribute("title") || "").match(/(\d{4})年(\d{2})月(\d{2})/);
  141. if (matched) startDate = matched[1] + "-" + matched[2] + "-" + matched[3];
  142. }
  143. }
  144. if (!startDate || !totalWeeks) return null;
  145. return { semesterStartDate: startDate, semesterTotalWeeks: totalWeeks };
  146. } catch (e) { return null; }
  147. }
  148. async function runImportFlow() {
  149. const confirmed = await window.shiguangBridgePromise.showAlert(
  150. "教务系统课表导入",
  151. "导入前请确保您已在浏览器中成功登录上海立达学院教务系统",
  152. "好的,开始导入");
  153. if (!confirmed) { window.shiguangBridge.showToast("用户取消了导入。"); return; }
  154. try {
  155. window.shiguangBridge.showToast("正在请求课表数据...");
  156. // 第一次请求不带学期,拿默认学期的课表页(含学期下拉列表)
  157. const defaultDoc = await fetchScheduleDoc(null);
  158. const semesterOptions = readSemesterOptions(defaultDoc);
  159. if (!semesterOptions) throw new Error("未能读取学期列表,请确认已登录教务系统。");
  160. const index = await window.shiguangBridgePromise.showSingleSelection(
  161. "选择学期", JSON.stringify(semesterOptions.options), semesterOptions.defaultIndex);
  162. if (index === null || index === -1) { window.shiguangBridge.showToast("用户取消了导入。"); return; }
  163. const semesterId = semesterOptions.options[index];
  164. // 若所选学期即默认学期,直接复用第一次结果;否则带参数重新请求
  165. const selectedDoc = semesterId === semesterOptions.options[semesterOptions.defaultIndex]
  166. ? defaultDoc
  167. : await fetchScheduleDoc(semesterId);
  168. const courses = parseCourses(selectedDoc);
  169. if (courses.length === 0) throw new Error("未解析到课程,请确认所选学期有课。");
  170. const config = await fetchSemesterConfig(semesterId);
  171. await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
  172. if (config) {
  173. try { await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config)); } catch (e) {}
  174. } else {
  175. window.shiguangBridge.showToast("未取到开学日期,已跳过课表配置,请在应用内手动设置。");
  176. }
  177. try { await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(LIDAPOLY_TIME_SLOTS)); } catch (e) {}
  178. window.shiguangBridge.showToast("课程导入成功,共导入 " + courses.length + " 门课程!");
  179. window.shiguangBridge.notifyTaskCompletion();
  180. } catch (e) {
  181. window.shiguangBridge.showToast("导入失败:" + e.message);
  182. console.error("JS: Import Error", e);
  183. }
  184. }
  185. runImportFlow();