bupt_01.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. // 北京邮电大学本科教务管理系统拾光课表适配脚本
  2. // 适配页面:https://jwgl.bupt.edu.cn/jsxsd/xskb/xskb_list.do
  3. // 当前版本只解析已打开的“学期理论课表”页面,不主动请求接口。
  4. (function () {
  5. function toast(message) {
  6. if (window.AndroidBridge && AndroidBridge.showToast) {
  7. AndroidBridge.showToast(message);
  8. } else {
  9. console.log(message);
  10. }
  11. }
  12. async function alertUser(title, message) {
  13. if (window.AndroidBridgePromise && window.AndroidBridgePromise.showAlert) {
  14. return await window.AndroidBridgePromise.showAlert(title, message, "确定");
  15. }
  16. alert(title + "\n" + message);
  17. return true;
  18. }
  19. function normalizeText(text) {
  20. return String(text || "")
  21. .replace(/\u00a0/g, " ")
  22. .replace(/ /gi, " ")
  23. .replace(/[0-9]/g, function (ch) {
  24. return String.fromCharCode(ch.charCodeAt(0) - 0xFEE0);
  25. })
  26. .replace(/[,、]/g, ",")
  27. .replace(/[-–—~~至到]/g, "-")
  28. .replace(/[()]/g, function (ch) {
  29. return ch === "(" ? "(" : ")";
  30. })
  31. .replace(/\s+/g, " ")
  32. .trim();
  33. }
  34. function findScheduleDocument() {
  35. if (document.querySelector("#kbtable")) return document;
  36. const frames = Array.from(document.querySelectorAll("iframe"));
  37. for (const frame of frames) {
  38. try {
  39. const frameDoc = frame.contentDocument || frame.contentWindow.document;
  40. if (frameDoc && frameDoc.querySelector("#kbtable")) return frameDoc;
  41. } catch (e) {
  42. // Ignore cross-origin or inaccessible frames.
  43. }
  44. }
  45. return null;
  46. }
  47. function getTitleText(container, title) {
  48. const node = container.querySelector(
  49. `font[title="${title}"], span[title="${title}"], div[title="${title}"]`
  50. );
  51. return normalizeText(node ? node.textContent : "");
  52. }
  53. function extractCourseName(courseDiv) {
  54. const clone = courseDiv.cloneNode(true);
  55. Array.from(clone.querySelectorAll("font[title], span[title], div[title]")).forEach(function (node) {
  56. node.remove();
  57. });
  58. Array.from(clone.querySelectorAll("span")).forEach(function (node) {
  59. const text = normalizeText(node.textContent);
  60. if (/^[A-Z]$/.test(text) || /^[●★○]+$/.test(text)) node.remove();
  61. });
  62. const holder = document.createElement("div");
  63. holder.innerHTML = clone.innerHTML.replace(/<br\s*\/?>/gi, "\n");
  64. const lines = holder.textContent
  65. .split(/\n+/)
  66. .map(normalizeText)
  67. .filter(function (line) {
  68. return line && line !== "-" && !/^\(\d+\)$/.test(line);
  69. });
  70. return normalizeText((lines[0] || "").replace(/[●★○]/g, ""));
  71. }
  72. function parseDay(courseDiv, fallbackDay) {
  73. const id = courseDiv.getAttribute("id") || "";
  74. const match = id.match(/-(\d)-\d$/);
  75. if (match) return parseInt(match[1], 10);
  76. return fallbackDay || 0;
  77. }
  78. function parseWeeks(weekText) {
  79. const text = normalizeText(weekText)
  80. .replace(/\[[^\]]*\]/g, "")
  81. .replace(/\(周\)/g, "")
  82. .replace(/周/g, "")
  83. .replace(/\s/g, "");
  84. const weeks = new Set();
  85. text.split(/[;,;]/).forEach(function (part) {
  86. if (!part) return;
  87. const isOdd = /单/.test(part);
  88. const isEven = /双/.test(part);
  89. const ranges = part.match(/\d+(?:-\d+)?/g) || [];
  90. ranges.forEach(function (rangeText) {
  91. const range = rangeText.split("-").map(function (value) {
  92. return parseInt(value, 10);
  93. });
  94. const start = range[0];
  95. const end = range.length > 1 ? range[1] : start;
  96. if (!start || !end || start > end) return;
  97. for (let week = start; week <= end; week++) {
  98. if (isOdd && week % 2 === 0) continue;
  99. if (isEven && week % 2 !== 0) continue;
  100. weeks.add(week);
  101. }
  102. });
  103. });
  104. return Array.from(weeks).sort(function (a, b) { return a - b; });
  105. }
  106. function parseSections(weekText) {
  107. const text = normalizeText(weekText).replace(/\s/g, "");
  108. const match = text.match(/\[([^\]]+)\]/);
  109. if (!match) return [];
  110. const numbers = match[1].match(/\d+/g) || [];
  111. if (numbers.length === 0) return [];
  112. const start = parseInt(numbers[0], 10);
  113. const end = parseInt(numbers[numbers.length - 1], 10);
  114. if (!start || !end || start > end) return [];
  115. const sections = [];
  116. for (let section = start; section <= end; section++) {
  117. sections.push(section);
  118. }
  119. return sections;
  120. }
  121. function parseCourseDiv(courseDiv, fallbackDay) {
  122. const rawText = normalizeText(courseDiv.textContent);
  123. if (!rawText || rawText === "&nbsp;" || rawText.length < 2) return null;
  124. const name = extractCourseName(courseDiv);
  125. const teacher = getTitleText(courseDiv, "老师") || getTitleText(courseDiv, "教师");
  126. const weekText = getTitleText(courseDiv, "周次(节次)");
  127. const position = getTitleText(courseDiv, "教室") || "未知地点";
  128. const weeks = parseWeeks(weekText);
  129. const sections = parseSections(weekText);
  130. const day = parseDay(courseDiv, fallbackDay);
  131. if (!name || !day || weeks.length === 0 || sections.length === 0) return null;
  132. return {
  133. name: name,
  134. teacher: teacher || "未知教师",
  135. position: position,
  136. day: day,
  137. startSection: sections[0],
  138. endSection: sections[sections.length - 1],
  139. weeks: weeks
  140. };
  141. }
  142. function parseCourses(doc) {
  143. const table = doc.querySelector("#kbtable");
  144. if (!table) return [];
  145. const courses = [];
  146. Array.from(table.querySelectorAll("tr")).forEach(function (row) {
  147. const cells = Array.from(row.querySelectorAll("td"));
  148. cells.forEach(function (cell, index) {
  149. const fallbackDay = index + 1;
  150. Array.from(cell.querySelectorAll("div.kbcontent")).forEach(function (courseDiv) {
  151. if (courseDiv.classList.contains("sykb2")) return;
  152. const course = parseCourseDiv(courseDiv, fallbackDay);
  153. if (course) courses.push(course);
  154. });
  155. });
  156. });
  157. return mergeCourses(courses);
  158. }
  159. function mergeCourses(courses) {
  160. const map = new Map();
  161. courses.forEach(function (course) {
  162. const key = [
  163. course.name,
  164. course.teacher,
  165. course.position,
  166. course.day,
  167. course.startSection,
  168. course.endSection
  169. ].join("|");
  170. if (!map.has(key)) {
  171. map.set(key, {
  172. name: course.name,
  173. teacher: course.teacher,
  174. position: course.position,
  175. day: course.day,
  176. startSection: course.startSection,
  177. endSection: course.endSection,
  178. weeks: course.weeks.slice()
  179. });
  180. return;
  181. }
  182. const existing = map.get(key);
  183. existing.weeks = Array.from(new Set(existing.weeks.concat(course.weeks)));
  184. });
  185. return Array.from(map.values())
  186. .map(function (course) {
  187. course.weeks = course.weeks.sort(function (a, b) { return a - b; });
  188. return course;
  189. })
  190. .sort(function (a, b) {
  191. return a.day - b.day ||
  192. a.startSection - b.startSection ||
  193. a.name.localeCompare(b.name);
  194. });
  195. }
  196. function parseTimeSlots(doc) {
  197. const table = doc.querySelector("#kbtable");
  198. if (!table) return [];
  199. const map = new Map();
  200. Array.from(table.querySelectorAll("tr")).forEach(function (row) {
  201. const header = row.querySelector("th");
  202. if (!header) return;
  203. const text = normalizeText(header.textContent);
  204. const match = text.match(/^(\d+).*?(\d{1,2}:\d{2})-(\d{1,2}:\d{2})/);
  205. if (!match) return;
  206. const number = parseInt(match[1], 10);
  207. if (!number || map.has(number)) return;
  208. map.set(number, {
  209. number: number,
  210. startTime: match[2].padStart(5, "0"),
  211. endTime: match[3].padStart(5, "0")
  212. });
  213. });
  214. return Array.from(map.values()).sort(function (a, b) { return a.number - b.number; });
  215. }
  216. async function saveToApp(courses, timeSlots) {
  217. const maxWeek = Math.max.apply(null, courses.flatMap(function (course) { return course.weeks; }));
  218. const config = {
  219. semesterTotalWeeks: Number.isFinite(maxWeek) && maxWeek > 0 ? maxWeek : 20,
  220. firstDayOfWeek: 1,
  221. defaultClassDuration: 45,
  222. defaultBreakDuration: 5
  223. };
  224. if (window.AndroidBridgePromise && window.AndroidBridgePromise.saveCourseConfig) {
  225. await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(config));
  226. }
  227. if (timeSlots.length > 0 && window.AndroidBridgePromise && window.AndroidBridgePromise.savePresetTimeSlots) {
  228. await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  229. }
  230. if (window.AndroidBridgePromise && window.AndroidBridgePromise.saveImportedCourses) {
  231. return await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
  232. }
  233. console.log("BUPT parsed courses:", JSON.stringify(courses, null, 2));
  234. console.log("BUPT parsed time slots:", JSON.stringify(timeSlots, null, 2));
  235. return true;
  236. }
  237. async function runImportFlow() {
  238. try {
  239. const doc = findScheduleDocument();
  240. if (!doc) {
  241. await alertUser(
  242. "未找到课表",
  243. "请不要在教务系统主页直接导入。请先进入“学期理论课表”页面,并等待课表加载完成后再点击导入。"
  244. );
  245. return;
  246. }
  247. const confirmed = await alertUser(
  248. "北邮课表导入",
  249. "请确认当前不是教务系统主页,而是已经进入“学期理论课表”页面。脚本将直接解析当前页面显示的课表,请确认学期正确且页面已加载完成。"
  250. );
  251. if (!confirmed) return;
  252. const courses = parseCourses(doc);
  253. const timeSlots = parseTimeSlots(doc);
  254. if (courses.length === 0) {
  255. await alertUser(
  256. "未解析到课程",
  257. "当前页面没有解析到有效课程。请确认课表页面中存在课程块,或把一段 kbcontent HTML 发给我继续微调。"
  258. );
  259. return;
  260. }
  261. const saved = await saveToApp(courses, timeSlots);
  262. if (!saved) {
  263. toast("课程保存失败,请重试");
  264. return;
  265. }
  266. toast(`导入成功:${courses.length} 个课程时段${timeSlots.length ? ",已同步作息时间" : ""}`);
  267. if (window.AndroidBridge && AndroidBridge.notifyTaskCompletion) {
  268. AndroidBridge.notifyTaskCompletion();
  269. }
  270. } catch (error) {
  271. console.error("BUPT import failed:", error);
  272. await alertUser("导入失败", error && error.message ? error.message : String(error));
  273. }
  274. }
  275. runImportFlow();
  276. })();