xauat.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. // 西安建筑科技大学(XAUAT)拾光课程表适配脚本 · 树维教务平台 · 本科
  2. "use strict";
  3. (function () {
  4. // bizTypeId=2:本科课表(接口约定,勿改)
  5. const BIZ_TYPE_ID = 2;
  6. // datum 接口要求 studentId 传字符串 "null"(接口约定,勿改)
  7. const DATUM_STUDENT_ID = "null";
  8. const LIMITS = { timeout: 15000, minWeeks: 16, maxWeeks: 25, fallbackWeeks: 20 };
  9. const MS_PER_DAY = 86400000;
  10. const DAYS_PER_WEEK = 7;
  11. const ENABLE_LOG = false; // release:关闭普通日志,仅保留 error
  12. const API = {
  13. courseTablePage: "/student/for-std/course-table",
  14. lessonIds: (s) => "/student/for-std/course-table/get-data?bizTypeId=" + BIZ_TYPE_ID + "&semesterId=" + encodeURIComponent(s) + "&dataId=",
  15. datum: "/student/ws/schedule-table/datum",
  16. semester: (s) => "/student/ws/semester/get/" + encodeURIComponent(s),
  17. };
  18. const log = {
  19. info: (m) => ENABLE_LOG && console.log("[XAUAT] " + m),
  20. warn: (m) => ENABLE_LOG && console.warn("[XAUAT] " + m),
  21. error: (m) => console.error("[XAUAT] " + m),
  22. };
  23. // 桥接调用统一封装,其余代码不直接访问 window.shiguangBridge
  24. const ui = {
  25. toast: (m) => window.shiguangBridge.showToast(m),
  26. alert: (t, m, b) => window.shiguangBridgePromise.showAlert(t, m, b),
  27. select: (t, items, d) => window.shiguangBridgePromise.showSingleSelection(t, JSON.stringify(items), d),
  28. saveConfig: (j) => window.shiguangBridgePromise.saveCourseConfig(j),
  29. saveCourses: (j) => window.shiguangBridgePromise.saveImportedCourses(j),
  30. done: () => window.shiguangBridge.notifyTaskCompletion(),
  31. };
  32. async function requestText(url, options) {
  33. const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
  34. const timer = controller ? setTimeout(() => controller.abort(), LIMITS.timeout) : null;
  35. try {
  36. const res = await fetch(url, Object.assign(
  37. { credentials: "include" },
  38. options,
  39. controller ? { signal: controller.signal } : {}
  40. ));
  41. if (!res.ok) {
  42. if (res.status === 401 || res.status === 403) throw new Error("登录状态已失效,请重新登录教务系统后重试");
  43. throw new Error("网络请求失败,请稍后重试");
  44. }
  45. return await res.text();
  46. } catch (e) {
  47. if (e.name === "AbortError") throw new Error("请求超时,请检查网络后重试");
  48. throw e;
  49. } finally {
  50. if (timer) clearTimeout(timer);
  51. }
  52. }
  53. const requestJson = async (url, options) => JSON.parse(await requestText(url, options));
  54. const pad2 = (n) => String(n).padStart(2, "0");
  55. const formatTime = (hhmm) => pad2(Math.floor(hhmm / 100)) + ":" + pad2(hhmm % 100);
  56. const toDateString = (d) => d.getFullYear() + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate());
  57. const weekdayOf = (dateStr) => { const d = new Date(dateStr).getDay(); return d === 0 ? 7 : d; };
  58. const clampWeeks = (w) => Math.min(Math.max(w, LIMITS.minWeeks), LIMITS.maxWeeks);
  59. // 周次去重升序:值域有界,用桶排序
  60. function uniqueSortedWeeks(weeks) {
  61. const seen = new Array(LIMITS.maxWeeks + 1).fill(false);
  62. for (const w of weeks) if (w >= 1 && w <= LIMITS.maxWeeks) seen[w] = true;
  63. const out = [];
  64. for (let w = 1; w <= LIMITS.maxWeeks; w++) if (seen[w]) out.push(w);
  65. return out;
  66. }
  67. function calculateTotalWeeks(start, end) {
  68. if (!start || !end) return LIMITS.fallbackWeeks;
  69. return Math.ceil(Math.ceil((new Date(end) - new Date(start)) / MS_PER_DAY) / DAYS_PER_WEEK);
  70. }
  71. // 由最早上课日期反推开学日(所在周周一)。局限:假设最早上课日在第一周。
  72. function deriveSemesterStartDate(scheduleList) {
  73. let minDate = null;
  74. for (const it of scheduleList) if (it.date && (minDate === null || it.date < minDate)) minDate = it.date;
  75. if (!minDate) return null;
  76. const d = new Date(minDate);
  77. d.setDate(d.getDate() - ((d.getDay() + 6) % 7));
  78. return toDateString(d);
  79. }
  80. function getMaxCourseWeek(courses) {
  81. let max = 0;
  82. for (const c of courses) for (const w of c.weeks || []) if (w > max) max = w;
  83. return max;
  84. }
  85. function readPosition(room) {
  86. if (!room) return "未知地点";
  87. return (typeof room === "object" ? room.nameZh : room) || "未知地点";
  88. }
  89. function convertCourses(lessonList, scheduleList, semesterStartDate) {
  90. const names = new Map(lessonList.map((l) => [l.id, l.courseName]));
  91. const startMs = new Date(semesterStartDate).getTime(); // 预计算,避免循环内重复解析
  92. const groups = new Map();
  93. for (const it of scheduleList) {
  94. if (!it.date || typeof it.startTime !== "number" || typeof it.endTime !== "number") continue;
  95. const diffDays = Math.floor((new Date(it.date).getTime() - startMs) / MS_PER_DAY);
  96. if (diffDays < 0) continue;
  97. const week = Math.floor(diffDays / DAYS_PER_WEEK) + 1;
  98. const day = weekdayOf(it.date);
  99. const key = it.lessonId + "|" + day + "|" + it.startTime + "|" + it.endTime;
  100. if (!groups.has(key)) {
  101. groups.set(key, {
  102. name: names.get(it.lessonId) || "未知课程",
  103. teacher: it.personName || "未知教师",
  104. position: readPosition(it.room),
  105. day, startTime: it.startTime, endTime: it.endTime, weeks: [],
  106. });
  107. }
  108. groups.get(key).weeks.push(week);
  109. }
  110. const courses = [];
  111. for (const g of groups.values()) {
  112. courses.push({
  113. name: g.name, teacher: g.teacher, position: g.position, day: g.day,
  114. weeks: uniqueSortedWeeks(g.weeks), isCustomTime: true,
  115. customStartTime: formatTime(g.startTime), customEndTime: formatTime(g.endTime),
  116. });
  117. }
  118. log.info("转换完成,共 " + courses.length + " 条课程");
  119. return courses;
  120. }
  121. async function getSemesters() {
  122. log.info("获取学期列表...");
  123. const select = new DOMParser().parseFromString(await requestText(API.courseTablePage), "text/html")
  124. .querySelector("#allSemesters, #semesters");
  125. if (!select) throw new Error("未找到学期选择框,请确认已登录教务系统");
  126. const semesters = [];
  127. for (const opt of select.querySelectorAll("option")) {
  128. const id = opt.getAttribute("value");
  129. const name = opt.textContent.trim();
  130. if (id && id !== "all" && name) semesters.push({ id, name });
  131. }
  132. if (!semesters.length) throw new Error("学期列表为空");
  133. return semesters;
  134. }
  135. async function fetchLessonIds(semesterId) {
  136. const json = await requestJson(API.lessonIds(semesterId));
  137. if (!json || !Array.isArray(json.lessonIds)) throw new Error("课程数据异常,请稍后重试");
  138. return json.lessonIds;
  139. }
  140. async function fetchScheduleDatum(lessonIds) {
  141. if (!lessonIds || !lessonIds.length) throw new Error("课程列表为空");
  142. const json = await requestJson(API.datum, {
  143. method: "POST",
  144. headers: { "Content-Type": "application/json" },
  145. body: JSON.stringify({ studentId: DATUM_STUDENT_ID, lessonIds }),
  146. });
  147. if (!json || !json.result) throw new Error("课程详情数据异常,请稍后重试");
  148. return { lessonList: json.result.lessonList || [], scheduleList: json.result.scheduleList || [] };
  149. }
  150. // 学期日期仅作辅助,失败降级为空,不中断主流程
  151. async function fetchSemesterInfo(semesterId) {
  152. try {
  153. const json = await requestJson(API.semester(semesterId));
  154. return { startDate: json.startDate || null, endDate: json.endDate || null };
  155. } catch (e) {
  156. log.warn("学期信息获取失败: " + e.message);
  157. return { startDate: null, endDate: null };
  158. }
  159. }
  160. async function saveConfig(config) {
  161. try { await ui.saveConfig(JSON.stringify(config)); }
  162. catch (e) { throw new Error("保存配置失败,请重试"); }
  163. ui.toast("课表配置导入成功");
  164. }
  165. async function saveCourses(courses) {
  166. try { await ui.saveCourses(JSON.stringify(courses)); }
  167. catch (e) { throw new Error("保存课程失败,请重试"); }
  168. ui.toast("成功导入 " + courses.length + " 条课程");
  169. }
  170. async function chooseSemester(semesters) {
  171. const names = semesters.map((s) => s.name);
  172. for (;;) {
  173. const index = await ui.select("选择学期", names, 0);
  174. if (index === null || index < 0 || index >= semesters.length) return null;
  175. const semester = semesters[index];
  176. let lessonIds = [];
  177. try { lessonIds = await fetchLessonIds(semester.id); }
  178. catch (e) { log.warn("获取 " + semester.name + " 课程失败: " + e.message); }
  179. if (lessonIds.length) return { semester, lessonIds };
  180. if (!(await ui.alert("无课程数据", "「" + semester.name + "」没有课程数据,请选择其他学期。", "重新选择"))) return null;
  181. }
  182. }
  183. async function runImportFlow() {
  184. try {
  185. if (!(await ui.alert("西安建筑科技大学课表导入", "请确保已登录教务系统(swjw.xauat.edu.cn)。\n本适配将自动获取学期与课程数据。", "开始导入"))) {
  186. ui.toast("导入已取消"); return;
  187. }
  188. ui.toast("正在获取学期列表...");
  189. const chosen = await chooseSemester(await getSemesters());
  190. if (!chosen) { ui.toast("导入已取消"); return; }
  191. // 课程详情与学期信息无依赖,并行请求
  192. ui.toast("正在获取课程数据...");
  193. const [datum, semesterInfo] = await Promise.all([
  194. fetchScheduleDatum(chosen.lessonIds),
  195. fetchSemesterInfo(chosen.semester.id),
  196. ]);
  197. const startDate = deriveSemesterStartDate(datum.scheduleList) || semesterInfo.startDate;
  198. if (!startDate) throw new Error("无法确定开学日期,请重试");
  199. log.info("推算开学日期: " + startDate);
  200. const courses = convertCourses(datum.lessonList, datum.scheduleList, startDate);
  201. if (!courses.length) {
  202. await ui.alert("无课程数据", "未能转换出有效课程。", "确定");
  203. return;
  204. }
  205. // 优先用课程实际最大周次,回退到日期差计算
  206. const totalWeeks = clampWeeks(getMaxCourseWeek(courses) || calculateTotalWeeks(startDate, semesterInfo.endDate));
  207. await saveConfig({ semesterStartDate: startDate, semesterTotalWeeks: totalWeeks });
  208. await saveCourses(courses);
  209. ui.toast("课表导入完成!");
  210. ui.done();
  211. } catch (e) {
  212. log.error("导入异常: " + (e.stack || e.message));
  213. await ui.alert("导入失败", e.message || "未知错误,请重试", "确定");
  214. }
  215. }
  216. runImportFlow();
  217. })();