cqut_01.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. /**
  2. * 重庆理工大学课表导入脚本
  3. * author: Dawn Drizzle
  4. */
  5. const API_BASE = 'https://timetable-cfc.cqut.edu.cn/api/courseSchedule';
  6. // 仅允许在课表站点内执行,避免跨站点误触发
  7. const checkLogin = () => window.location.hostname === 'timetable-cfc.cqut.edu.cn';
  8. // 统一的接口请求封装:POST + JSON + 携带 Cookie,并在失败时提示原因
  9. const baseFetch = async (path, body, description) => {
  10. const response = await fetch(`${API_BASE}/${path}`, {
  11. method: 'POST',
  12. credentials: 'include',
  13. headers: {
  14. 'Content-Type': 'application/json',
  15. },
  16. body: body === undefined ? undefined : JSON.stringify(body),
  17. });
  18. if (!response.ok) {
  19. AndroidBridge.showToast(`获取${description}失败,请确认已登录后重试`);
  20. throw new Error(`获取${description}失败: ${response.status} ${response.statusText}`);
  21. }
  22. try {
  23. return await response.json();
  24. } catch (error) {
  25. AndroidBridge.showToast(`解析${description}失败,请确认当前页面登录状态`);
  26. throw new Error(`解析${description}失败: ${error.message}`);
  27. }
  28. };
  29. // 获取当前登录用户信息(包含 username、校区等)
  30. const getUserInfo = async () => await baseFetch('getUserInfo', {}, '用户信息');
  31. // 获取指定校区的节次时间表
  32. const getCampusTimeInfo = async (campusName) => await baseFetch('getCampusTimeInfo', { campusName }, '时间表');
  33. // 获取指定周课程事件列表;weekNum/yearTerm 为空时,接口返回当前学期/当前周信息
  34. const getWeekEvents = async (userID, weekNum, yearTerm, description) => await baseFetch(
  35. 'listWeekEvents',
  36. {
  37. userID: String(userID),
  38. weekNum,
  39. yearTerm,
  40. },
  41. description,
  42. );
  43. // 将接口的节次时间转换为统一结构,并按节次序号升序排序
  44. const parseTimeSlots = (timeSlots) => timeSlots
  45. .slice()
  46. .sort((left, right) => Number(left.sessionNum) - Number(right.sessionNum))
  47. .map((timeSlot) => ({
  48. number: Number(timeSlot.sessionNum) || 0,
  49. startTime: timeSlot.startTime ?? '',
  50. endTime: timeSlot.endTime ?? '',
  51. }));
  52. // 推算学期开始日期(YYYY-MM-DD):使用 weekDayList 第一条的月/日 + yearTerm 中的学年信息
  53. const parseSemesterStartDate = (yearTerm, weekDayList) => {
  54. const firstWeekDate = weekDayList?.[0]?.weekDate;
  55. if (!yearTerm || !firstWeekDate) {
  56. return null;
  57. }
  58. const [startYear, endYear, termPart] = String(yearTerm).split('-');
  59. const [month, day] = String(firstWeekDate).split('/').map(Number);
  60. if (!startYear || !endYear || !termPart || Number.isNaN(month) || Number.isNaN(day)) {
  61. return null;
  62. }
  63. const year = termPart === '1' ? Number(startYear) : Number(endYear);
  64. if (Number.isNaN(year)) {
  65. return null;
  66. }
  67. const date = new Date(Date.UTC(year, month - 1, day));
  68. if (Number.isNaN(date.getTime())) {
  69. return null;
  70. }
  71. return date.toISOString().split('T')[0];
  72. };
  73. // 将接口的 event 解析为课程结构(节次、星期、周次等)
  74. const parseCourse = (event) => {
  75. const sessionList = (event.sessionList ?? []).map(Number).filter((session) => !Number.isNaN(session));
  76. const startSection = Number(event.sessionStart) || sessionList[0] || 0;
  77. const endSection = sessionList[sessionList.length - 1] || startSection + (Number(event.sessionLast) || 1) - 1;
  78. return {
  79. name: event.eventName ?? '',
  80. teacher: event.memberName ?? '',
  81. position: event.address ?? '',
  82. day: Number(event.weekDay) || 0,
  83. startSection,
  84. endSection,
  85. weeks: (event.weekList ?? []).map(Number).filter((week) => !Number.isNaN(week)),
  86. };
  87. };
  88. // 合并完全相同(课程名/老师/地点/星期/节次范围一致)的课程,将周次去重合并
  89. const mergeCourses = (events) => {
  90. const mergedCourses = new Map();
  91. for (const event of events) {
  92. const course = parseCourse(event);
  93. const key = [
  94. course.name,
  95. course.teacher,
  96. course.position,
  97. course.day,
  98. course.startSection,
  99. course.endSection,
  100. ].join('||');
  101. if (!mergedCourses.has(key)) {
  102. mergedCourses.set(key, course);
  103. continue;
  104. }
  105. const existingCourse = mergedCourses.get(key);
  106. existingCourse.weeks = [...new Set([...existingCourse.weeks, ...course.weeks])].sort((left, right) => left - right);
  107. }
  108. return [...mergedCourses.values()];
  109. };
  110. // 将解析后的结构写入 App(配置、课程列表、节次时间表)
  111. const saveSchedule = (parsedSchedule) => Promise.all([
  112. window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(parsedSchedule.courseConfig)),
  113. window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(parsedSchedule.courses)),
  114. window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(parsedSchedule.timeSlots)),
  115. ]);
  116. // 主流程:校验页面 → 拉取用户/校区 → 拉取节次/当前学期信息 → 拉取全学期每周课程 → 合并保存
  117. (async () => {
  118. if (!checkLogin()) {
  119. AndroidBridge.showToast('请先打开重庆理工大学课表页面并登录');
  120. throw new Error('当前不在重庆理工大学课表页面');
  121. }
  122. const userInfo = await getUserInfo();
  123. const userID = userInfo?.username;
  124. const campusName = userInfo?.userCustomSetting?.campusName;
  125. if (!userID || !campusName) {
  126. AndroidBridge.showToast('未获取到完整的用户信息,请确认已登录');
  127. throw new Error('用户信息不完整');
  128. }
  129. const [timeSlotData, currentWeekData] = await Promise.all([
  130. getCampusTimeInfo(campusName),
  131. getWeekEvents(userID, null, null, '当前学期信息'),
  132. ]);
  133. const yearTerm = currentWeekData?.yearTerm;
  134. const weekList = Array.isArray(currentWeekData?.weekList) ? currentWeekData.weekList : [];
  135. const currentWeekNum = currentWeekData?.weekNum;
  136. const semesterStartDate = parseSemesterStartDate(yearTerm, currentWeekData?.weekDayList);
  137. if (!yearTerm || weekList.length === 0) {
  138. AndroidBridge.showToast('未获取到当前学期信息');
  139. throw new Error('当前学期信息不完整');
  140. }
  141. const weekResults = await Promise.all(
  142. weekList.map((weekNum) => String(weekNum) === String(currentWeekNum)
  143. ? currentWeekData
  144. : getWeekEvents(userID, String(weekNum), yearTerm, `第${weekNum}周课程`))
  145. );
  146. await saveSchedule({
  147. courseConfig: {
  148. semesterStartDate,
  149. semesterTotalWeeks: weekList.length,
  150. },
  151. timeSlots: parseTimeSlots(timeSlotData),
  152. courses: mergeCourses(weekResults.flatMap((result) => result?.eventList ?? [])),
  153. });
  154. AndroidBridge.notifyTaskCompletion();
  155. })();