wust.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. // 武汉科技大学教务系统课程表导入脚本
  2. (function () {
  3. "use strict";
  4. // 教务系统课表接口地址。
  5. var SCHEDULE_PATH = "/jsxsd/xskb/xskb_list.do";
  6. // 黄家湖校区的默认节次时间。
  7. var DEFAULT_TIME_SLOTS = [
  8. { number: 1, startTime: "08:20", endTime: "09:05" },
  9. { number: 2, startTime: "09:15", endTime: "10:00" },
  10. { number: 3, startTime: "10:20", endTime: "11:05" },
  11. { number: 4, startTime: "11:15", endTime: "12:00" },
  12. { number: 5, startTime: "14:00", endTime: "14:45" },
  13. { number: 6, startTime: "14:55", endTime: "15:40" },
  14. { number: 7, startTime: "16:00", endTime: "16:45" },
  15. { number: 8, startTime: "16:55", endTime: "17:40" },
  16. { number: 9, startTime: "18:40", endTime: "19:25" },
  17. { number: 10, startTime: "19:35", endTime: "20:20" },
  18. { number: 11, startTime: "20:40", endTime: "21:25" },
  19. { number: 12, startTime: "21:35", endTime: "22:20" }
  20. ];
  21. // 校区选项,选择结果用于确定课程时间。
  22. var CAMPUS_OPTIONS = ["黄家湖校区", "青山校区"];
  23. // 根据校区生成对应的节次时间,青山校区上午前四节提前。
  24. function getCampusTimeSlots(campusIndex) {
  25. var timeSlots = DEFAULT_TIME_SLOTS.map(function (slot) {
  26. return {
  27. number: slot.number,
  28. startTime: slot.startTime,
  29. endTime: slot.endTime
  30. };
  31. });
  32. if (campusIndex !== 1) return timeSlots;
  33. return timeSlots.map(function (slot) {
  34. var minutes = slot.number <= 2 ? 20 : (slot.number <= 4 ? 10 : 0);
  35. if (minutes === 0) return slot;
  36. return {
  37. number: slot.number,
  38. startTime: shiftTime(slot.startTime, -minutes),
  39. endTime: shiftTime(slot.endTime, -minutes)
  40. };
  41. });
  42. }
  43. // 按指定分钟数调整 HH:mm 格式的时间。
  44. function shiftTime(time, offsetMinutes) {
  45. var parts = time.split(":").map(Number);
  46. var totalMinutes = parts[0] * 60 + parts[1] + offsetMinutes;
  47. var hours = Math.floor(totalMinutes / 60).toString().padStart(2, "0");
  48. var minutes = (totalMinutes % 60).toString().padStart(2, "0");
  49. return hours + ":" + minutes;
  50. }
  51. // 通过应用桥接 API 显示提示信息。
  52. function toast(message) {
  53. if (window.shiguangBridge && window.shiguangBridge.showToast) {
  54. window.shiguangBridge.showToast(message);
  55. }
  56. }
  57. // 清理教务页面文本中的空格和非断行空格。
  58. function normalizeText(value) {
  59. return (value || "").replace(/\u00a0/g, " ").replace(/\s+/g, " ").trim();
  60. }
  61. // 将周次文本解析为排序后的周次数组。
  62. function parseWeeks(value) {
  63. var text = normalizeText(value).replace(/\(周\)|周/g, "");
  64. var weeks = new Set();
  65. text.split(",").forEach(function (part) {
  66. var range = part.trim().split("-").map(function (item) {
  67. return parseInt(item, 10);
  68. });
  69. if (!Number.isFinite(range[0])) return;
  70. var end = Number.isFinite(range[1]) ? range[1] : range[0];
  71. for (var week = range[0]; week <= end; week++) weeks.add(week);
  72. });
  73. return Array.from(weeks).sort(function (a, b) { return a - b; });
  74. }
  75. // 将节次文本解析为连续的节次数组。
  76. function parsePeriods(value) {
  77. var match = normalizeText(value).match(/\[([^\]]+)\]/);
  78. if (!match) return [];
  79. var numbers = match[1].match(/\d+/g);
  80. if (!numbers || numbers.length === 0) return [];
  81. var start = parseInt(numbers[0], 10);
  82. var end = parseInt(numbers[numbers.length - 1], 10);
  83. var periods = [];
  84. for (var period = start; period <= end; period++) periods.push(period);
  85. return periods;
  86. }
  87. // 查找主页面或同源 iframe 中的课表文档。
  88. function findScheduleDocument() {
  89. if (document.querySelector("#kbtable")) return document;
  90. return Array.from(document.querySelectorAll("iframe")).reduce(function (found, frame) {
  91. if (found) return found;
  92. try {
  93. var frameDoc = frame.contentDocument || frame.contentWindow.document;
  94. return frameDoc && frameDoc.querySelector("#kbtable") ? frameDoc : null;
  95. } catch (error) {
  96. return null;
  97. }
  98. }, null);
  99. }
  100. // 解析单个课程块,生成应用需要的课程对象。
  101. function parseCourseBlock(block) {
  102. var holder = document.createElement("div");
  103. holder.innerHTML = block;
  104. var lines = Array.from(holder.querySelectorAll("font[title]")).reduce(function (result, font) {
  105. result[font.getAttribute("title")] = normalizeText(font.textContent);
  106. return result;
  107. }, {});
  108. var firstLine = holder.innerHTML.split(/<br\s*\/?\s*>/i)[0];
  109. var nameHolder = document.createElement("div");
  110. nameHolder.innerHTML = firstLine;
  111. var name = normalizeText(nameHolder.textContent);
  112. var weekPeriod = lines["周次(节次)"] || "";
  113. var periods = parsePeriods(weekPeriod);
  114. var weeks = parseWeeks(weekPeriod.split("[")[0]);
  115. if (!name || weeks.length === 0 || periods.length === 0) return null;
  116. return {
  117. name: name,
  118. teacher: lines["老师"] || "",
  119. position: lines["教室"] || "未指定",
  120. weeks: weeks,
  121. startSection: periods[0],
  122. endSection: periods[periods.length - 1]
  123. };
  124. }
  125. // 遍历课表单元格,解析课程并合并重复的课程记录。
  126. function parseCourses(doc) {
  127. var table = doc.querySelector("#kbtable");
  128. if (!table) return [];
  129. var courses = new Map();
  130. Array.from(table.querySelectorAll("div.kbcontent")).forEach(function (div) {
  131. var idParts = (div.id || "").split("-");
  132. var weekday = parseInt(idParts[idParts.length - 2], 10);
  133. if (!Number.isFinite(weekday) || weekday < 1 || weekday > 7) return;
  134. var blocks = div.innerHTML.split(/<br\s*\/?\s*>\s*[-—]{10,}\s*<br\s*\/?\s*>/i);
  135. blocks.forEach(function (block) {
  136. var course = parseCourseBlock(block);
  137. if (!course) return;
  138. course.day = weekday;
  139. var key = [course.name, course.teacher, course.position, weekday, course.weeks.join(",")].join("|");
  140. if (!courses.has(key)) {
  141. courses.set(key, course);
  142. return;
  143. }
  144. var existing = courses.get(key);
  145. existing.startSection = Math.min(existing.startSection, course.startSection);
  146. existing.endSection = Math.max(existing.endSection, course.endSection);
  147. });
  148. });
  149. return Array.from(courses.values()).sort(function (a, b) {
  150. return a.day - b.day || a.startSection - b.startSection || a.name.localeCompare(b.name);
  151. });
  152. }
  153. // 使用 v2 桥接 API 让用户选择上课校区。
  154. async function selectCampus() {
  155. var selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
  156. "选择上课校区",
  157. JSON.stringify(CAMPUS_OPTIONS),
  158. 0
  159. );
  160. if (selectedIndex === null || selectedIndex < 0 || selectedIndex >= CAMPUS_OPTIONS.length) {
  161. toast("已取消导入");
  162. return null;
  163. }
  164. return selectedIndex;
  165. }
  166. // 获取当前页面选择的学期编号。
  167. function getSemesterId(doc) {
  168. var select = doc.querySelector("#xnxq01id");
  169. return select ? select.value : "";
  170. }
  171. // 请求指定学期的课表页面并解析为文档对象。
  172. async function fetchScheduleDocument(semesterId) {
  173. var body = "cj0701id=&zc=&demo=&xnxq01id=" + encodeURIComponent(semesterId) + "&sfFD=1&wkbkc=1";
  174. var response = await fetch(SCHEDULE_PATH, {
  175. method: "POST",
  176. headers: { "Content-Type": "application/x-www-form-urlencoded" },
  177. credentials: "include",
  178. body: body
  179. });
  180. if (!response.ok) throw new Error("课表请求失败:HTTP " + response.status);
  181. return new DOMParser().parseFromString(await response.text(), "text/html");
  182. }
  183. // 保存课程配置、校区时间段和课程数据。
  184. async function saveData(courses, timeSlots) {
  185. var maxWeek = Math.max.apply(null, courses.map(function (course) {
  186. return Math.max.apply(null, course.weeks);
  187. }));
  188. var config = {
  189. semesterTotalWeeks: Number.isFinite(maxWeek) && maxWeek > 0 ? maxWeek : 20,
  190. firstDayOfWeek: 1,
  191. defaultClassDuration: 45,
  192. defaultBreakDuration: 5
  193. };
  194. if (window.shiguangBridgePromise && window.shiguangBridgePromise.saveCourseConfig) {
  195. await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
  196. }
  197. if (timeSlots.length > 0 && window.shiguangBridgePromise && window.shiguangBridgePromise.savePresetTimeSlots) {
  198. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  199. }
  200. return await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
  201. }
  202. // 编排校区选择、课表获取、解析和保存的完整导入流程。
  203. async function runImport() {
  204. try {
  205. var campusIndex = await selectCampus();
  206. if (campusIndex === null) return;
  207. var currentDoc = findScheduleDocument() || document;
  208. var semesterId = getSemesterId(currentDoc);
  209. var scheduleDoc = currentDoc.querySelector("#kbtable") ? currentDoc : await fetchScheduleDocument(semesterId);
  210. var courses = parseCourses(scheduleDoc);
  211. if (courses.length === 0) {
  212. toast("未解析到课程,请确认已登录且课表页面已加载完成");
  213. return;
  214. }
  215. var saved = await saveData(courses, getCampusTimeSlots(campusIndex));
  216. if (!saved) {
  217. toast("课程导入失败");
  218. return;
  219. }
  220. toast("成功导入 " + courses.length + " 门课程");
  221. if (window.shiguangBridge.notifyTaskCompletion) window.shiguangBridge.notifyTaskCompletion();
  222. } catch (error) {
  223. console.error("WUST import failed:", error);
  224. toast("导入失败:" + error.message);
  225. }
  226. }
  227. // 检查桥接 API 可用后启动导入流程。
  228. if (window.shiguangBridgePromise && window.shiguangBridgePromise.saveImportedCourses) {
  229. runImport();
  230. }
  231. })();