hust.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. /**
  2. * 华中科技大学 HUB 系统课表适配器。
  3. *
  4. * 接口与解析规则参考 StayUP-Calendar 的 HUST importer:
  5. * https://github.com/Lucas04-nhr/StayUP-Calendar/blob/main/lib/src/pages/crawler/hust.dart
  6. */
  7. const HUST_COURSE_API = new URL("/LsController/findNameCourse", location.origin).toString();
  8. const HUST_DEFAULT_SEMESTER_WEEKS = 20;
  9. function getDefaultTerm(now = new Date()) {
  10. return {
  11. academicYear: now.getMonth() === 0 ? now.getFullYear() - 1 : now.getFullYear(),
  12. semester: now.getMonth() >= 1 && now.getMonth() <= 6 ? 2 : 1
  13. };
  14. }
  15. /**
  16. * 生成与原 importer 相同范围的学期:当前年份前 4 年至后 1 年,共 12 个。
  17. * semester=1 为秋季,semester=2 为春季;倒序排列便于优先选择近期学期。
  18. */
  19. function buildTermOptions(now = new Date()) {
  20. const currentYear = now.getFullYear();
  21. const terms = [];
  22. for (let year = currentYear + 1; year >= currentYear - 4; year--) {
  23. terms.push({ academicYear: year, semester: 1, label: `${year} 年秋季学期` });
  24. terms.push({ academicYear: year, semester: 2, label: `${year} 年春季学期` });
  25. }
  26. return terms;
  27. }
  28. async function selectTerm() {
  29. const terms = buildTermOptions();
  30. const defaultTerm = getDefaultTerm();
  31. const defaultIndex = Math.max(0, terms.findIndex((term) =>
  32. term.academicYear === defaultTerm.academicYear &&
  33. term.semester === defaultTerm.semester
  34. ));
  35. const selectedIndex = await window.AndroidBridgePromise.showSingleSelection(
  36. "选择学期",
  37. JSON.stringify(terms.map((term) => term.label)),
  38. defaultIndex
  39. );
  40. if (selectedIndex === null || selectedIndex === -1) {
  41. return null;
  42. }
  43. if (!Number.isInteger(selectedIndex) || selectedIndex < 0 || selectedIndex >= terms.length) {
  44. throw new Error("学期选择结果无效");
  45. }
  46. return terms[selectedIndex];
  47. }
  48. function parsePositiveInteger(value) {
  49. const parsed = Number.parseInt(String(value), 10);
  50. return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
  51. }
  52. function expandWeeks(startWeek, endWeek) {
  53. const weeks = [];
  54. for (let week = startWeek; week <= endWeek; week++) {
  55. weeks.push(week);
  56. }
  57. return weeks;
  58. }
  59. /**
  60. * HUST 的一门课程可包含多个 tr 时段。本项目没有 extraSlots 字段,
  61. * 因此每个有效时段转换成一个标准课程块。
  62. */
  63. function parseCourses(payload) {
  64. if (!payload || String(payload.code) !== "200" || !Array.isArray(payload.data)) {
  65. throw new Error(`课表接口返回异常(code: ${payload?.code ?? "未知"})`);
  66. }
  67. const courses = [];
  68. for (const item of payload.data) {
  69. const name = String(item?.KCMC ?? "").trim();
  70. if (!name || !Array.isArray(item?.tr)) {
  71. continue;
  72. }
  73. for (const slot of item.tr) {
  74. if (slot?.XQS === "<待定>" || slot?.QSJC === "<待定>") {
  75. continue;
  76. }
  77. const day = parsePositiveInteger(slot?.XQS);
  78. const startSection = parsePositiveInteger(slot?.QSJC);
  79. const endSection = parsePositiveInteger(slot?.JSJC);
  80. const startWeek = parsePositiveInteger(slot?.QSZC);
  81. const endWeek = parsePositiveInteger(slot?.JSZC);
  82. if (
  83. day === null || day > 7 ||
  84. startSection === null || endSection === null || endSection < startSection ||
  85. startWeek === null || endWeek === null || endWeek < startWeek
  86. ) {
  87. console.warn("跳过无法解析的 HUST 课程时段:", name, slot);
  88. continue;
  89. }
  90. courses.push({
  91. name,
  92. teacher: String(slot?.XM ?? "").trim(),
  93. position: String(slot?.JSMC ?? "").trim(),
  94. day,
  95. startSection,
  96. endSection,
  97. weeks: expandWeeks(startWeek, endWeek)
  98. });
  99. }
  100. }
  101. return courses;
  102. }
  103. async function fetchCourses(term) {
  104. const termCode = `${term.academicYear}${term.semester}`;
  105. const response = await fetch(`${HUST_COURSE_API}?kcbxqh=${encodeURIComponent(termCode)}`, {
  106. method: "GET",
  107. credentials: "include",
  108. headers: {
  109. Accept: "application/json, text/plain, */*"
  110. }
  111. });
  112. if (!response.ok) {
  113. throw new Error(`课表请求失败(HTTP ${response.status})`);
  114. }
  115. const raw = await response.text();
  116. if (!raw.trim() || raw.trim().startsWith("<")) {
  117. throw new Error("未读取到课表数据,请确认已登录 HUB 系统并选择了正确的学期");
  118. }
  119. try {
  120. return parseCourses(JSON.parse(raw));
  121. } catch (error) {
  122. if (error instanceof SyntaxError) {
  123. throw new Error("课表接口未返回有效 JSON,请重新登录后再试");
  124. }
  125. throw error;
  126. }
  127. }
  128. async function saveCourses(courses) {
  129. await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
  130. const semesterTotalWeeks = courses.reduce((maximum, course) => {
  131. const courseMaximum = course.weeks.length > 0 ? Math.max(...course.weeks) : 0;
  132. return Math.max(maximum, courseMaximum);
  133. }, HUST_DEFAULT_SEMESTER_WEEKS);
  134. await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify({ semesterTotalWeeks }));
  135. }
  136. async function runImportFlow() {
  137. try {
  138. const term = await selectTerm();
  139. if (term === null) {
  140. AndroidBridge.showToast("已取消导入");
  141. return;
  142. }
  143. AndroidBridge.showToast(`正在获取 ${term.label} 课表...`);
  144. const courses = await fetchCourses(term);
  145. if (courses.length === 0) {
  146. AndroidBridge.showToast("该学期未查询到有效课程,请检查学期或登录状态");
  147. return;
  148. }
  149. await saveCourses(courses);
  150. AndroidBridge.showToast(`成功导入 ${courses.length} 个课程时段,请按校历设置开学日期`);
  151. AndroidBridge.notifyTaskCompletion();
  152. } catch (error) {
  153. console.error("课表导入失败:", error);
  154. AndroidBridge.showToast(`导入失败:${error?.message ?? error}`);
  155. }
  156. }
  157. runImportFlow();