swjtu_yhxt.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. // 西南交通大学新教务系统课表导入脚本
  2. // 适配页面:https://yhxt.swjtu.edu.cn/study/teach/course/stu-course-list
  3. (() => {
  4. const SWJTU_SCHEDULE_API =
  5. "https://yhxt.swjtu.edu.cn/yethan/common/course-schedule/student-course-schedule";
  6. const SWJTU_SEMESTER_TOTAL_WEEKS = 19;
  7. const MAX_CLASS_TIME_FIELDS = 40;
  8. const SWJTU_TIME_SLOTS = [
  9. { number: 1, startTime: "08:00", endTime: "08:45" },
  10. { number: 2, startTime: "08:50", endTime: "09:35" },
  11. { number: 3, startTime: "09:50", endTime: "10:35" },
  12. { number: 4, startTime: "10:40", endTime: "11:25" },
  13. { number: 5, startTime: "11:30", endTime: "12:15" },
  14. { number: 6, startTime: "14:00", endTime: "14:45" },
  15. { number: 7, startTime: "14:50", endTime: "15:35" },
  16. { number: 8, startTime: "15:40", endTime: "16:25" },
  17. { number: 9, startTime: "16:40", endTime: "17:25" },
  18. { number: 10, startTime: "17:30", endTime: "18:15" },
  19. { number: 11, startTime: "19:30", endTime: "20:15" },
  20. { number: 12, startTime: "20:20", endTime: "21:05" },
  21. { number: 13, startTime: "21:10", endTime: "21:55" },
  22. ];
  23. const SWJTU_COURSE_CONFIG = {
  24. semesterStartDate: null,
  25. semesterTotalWeeks: SWJTU_SEMESTER_TOTAL_WEEKS,
  26. defaultClassDuration: 45,
  27. firstDayOfWeek: 1,
  28. };
  29. const DAY_MAP = {
  30. 一: 1,
  31. 二: 2,
  32. 三: 3,
  33. 四: 4,
  34. 五: 5,
  35. 六: 6,
  36. 日: 7,
  37. 天: 7,
  38. };
  39. function normalizeText(text) {
  40. return String(text || "")
  41. .replace(/\u00a0/g, " ")
  42. .replace(/ /gi, " ")
  43. .replace(/[\t\r\n]+/g, " ")
  44. .replace(/ +/g, " ")
  45. .trim();
  46. }
  47. function cleanValue(value) {
  48. const text = normalizeText(value);
  49. if (!text || text.toLowerCase() === "null" || text === "无") return "";
  50. return text;
  51. }
  52. function getBridge() {
  53. return {
  54. bridge: window.AndroidBridge || window.shiguangBridge,
  55. promise: window.AndroidBridgePromise || window.shiguangBridgePromise,
  56. };
  57. }
  58. function showToast(message) {
  59. const { bridge } = getBridge();
  60. if (bridge?.showToast) {
  61. bridge.showToast(message);
  62. } else {
  63. console.log(message);
  64. }
  65. }
  66. async function showAlert(title, message) {
  67. const { promise } = getBridge();
  68. if (promise?.showAlert) {
  69. await promise.showAlert(title, message, "确定");
  70. } else {
  71. console.warn(`${title}: ${message}`);
  72. }
  73. }
  74. function getCookieValue(name) {
  75. const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  76. const match = document.cookie.match(
  77. new RegExp(`(?:^|;\\s*)${escapedName}=([^;]*)`),
  78. );
  79. return match ? decodeURIComponent(match[1]) : "";
  80. }
  81. function getStorageValue(name) {
  82. try {
  83. return localStorage.getItem(name) || sessionStorage.getItem(name) || "";
  84. } catch (error) {
  85. console.warn("读取本地登录态失败:", error);
  86. return "";
  87. }
  88. }
  89. function getYToken() {
  90. const candidates = [
  91. getCookieValue("ytoken"),
  92. getStorageValue("ytoken"),
  93. getStorageValue("YToken"),
  94. getStorageValue("token"),
  95. getStorageValue("TOKEN"),
  96. ];
  97. return candidates.map(cleanValue).find(Boolean) || "";
  98. }
  99. async function fetchSemesterSchedule() {
  100. const ytoken = getYToken();
  101. if (!ytoken) {
  102. throw new Error("未找到 ytoken,请确认已登录西南交通大学新教务系统。");
  103. }
  104. const response = await fetch(SWJTU_SCHEDULE_API, {
  105. method: "GET",
  106. credentials: "include",
  107. headers: {
  108. Accept: "application/json, text/plain, */*",
  109. "Content-Type": "application/json",
  110. ytoken,
  111. },
  112. });
  113. if (!response.ok) {
  114. throw new Error(`课表接口请求失败:HTTP ${response.status}`);
  115. }
  116. const payload = await response.json();
  117. if (payload.code !== "00000") {
  118. throw new Error(payload.message || `课表接口返回异常:${payload.code}`);
  119. }
  120. if (Array.isArray(payload.data)) return payload.data;
  121. if (Array.isArray(payload.data?.list)) return payload.data.list;
  122. throw new Error("课表接口返回结构异常:data 不是课程数组。");
  123. }
  124. function parseWeeks(weekText) {
  125. const weeks = [];
  126. const text = normalizeText(weekText)
  127. .replace(/第/g, "")
  128. .replace(/[[【]/g, "(")
  129. .replace(/[\]】]/g, ")");
  130. const parity = text.includes("单") ? "单" : text.includes("双") ? "双" : "";
  131. const cleanedText = text
  132. .replace(/[单双]/g, "")
  133. .replace(/[()()]/g, "")
  134. .replace(/周/g, "");
  135. cleanedText.split(/[,,、;]/).forEach((segment) => {
  136. const match = normalizeText(segment).match(
  137. /^(\d+)(?:\s*[-~—至]\s*(\d+))?$/,
  138. );
  139. if (!match) return;
  140. const start = Number(match[1]);
  141. const end = match[2] ? Number(match[2]) : start;
  142. if (!Number.isFinite(start) || !Number.isFinite(end) || start > end)
  143. return;
  144. for (let week = start; week <= end; week++) {
  145. if (parity === "单" && week % 2 !== 1) continue;
  146. if (parity === "双" && week % 2 !== 0) continue;
  147. if (!weeks.includes(week)) weeks.push(week);
  148. }
  149. });
  150. return weeks.sort((a, b) => a - b);
  151. }
  152. function parseClassTime(classTime) {
  153. const text = cleanValue(classTime);
  154. if (!text) return [];
  155. const results = [];
  156. const pattern =
  157. /((?:第?\d+(?:\s*[-~—至]\s*\d+)?(?:\s*[,,、]\s*第?\d+(?:\s*[-~—至]\s*\d+)?)*\s*周(?:\s*[((]?\s*[单双]\s*[))]?\s*周?)?))\s*(?:星期|周)([一二三四五六日天])\s*第?\s*(\d+)(?:\s*[-~—至]\s*(\d+))?\s*节/g;
  158. let match = pattern.exec(text);
  159. while (match !== null) {
  160. const weeks = parseWeeks(match[1]);
  161. const day = DAY_MAP[match[2]];
  162. const startSection = Number(match[3]);
  163. const endSection = match[4] ? Number(match[4]) : startSection;
  164. if (
  165. weeks.length > 0 &&
  166. day &&
  167. Number.isFinite(startSection) &&
  168. Number.isFinite(endSection) &&
  169. startSection <= endSection
  170. ) {
  171. results.push({ weeks, day, startSection, endSection });
  172. }
  173. match = pattern.exec(text);
  174. }
  175. return results;
  176. }
  177. function buildTeacher(item) {
  178. const names = [item.staffName, item.staffNameOther]
  179. .flatMap((value) => cleanValue(value).split(/[,,、;;]/))
  180. .map(cleanValue)
  181. .filter(Boolean);
  182. return Array.from(new Set(names)).join(",") || "未指定";
  183. }
  184. function buildCourses(apiCourses) {
  185. const courses = [];
  186. const skipped = [];
  187. apiCourses.forEach((item) => {
  188. const name = cleanValue(item.courseName || item.name || item.kcmc);
  189. if (!name) return;
  190. const teacher = buildTeacher(item);
  191. let hasSpecificSchedule = false;
  192. let hasAnySchedule = false;
  193. for (let index = 1; index <= MAX_CLASS_TIME_FIELDS; index++) {
  194. const classTime = cleanValue(item[`classTime${index}`]);
  195. const classPlace = cleanValue(item[`classPlace${index}`]);
  196. if (!classTime) continue;
  197. hasAnySchedule = true;
  198. const parsedTimes = parseClassTime(classTime);
  199. if (parsedTimes.length === 0) continue;
  200. hasSpecificSchedule = true;
  201. parsedTimes.forEach((time) => {
  202. courses.push({
  203. name,
  204. teacher,
  205. position: classPlace || cleanValue(item.campusName) || "未指定",
  206. day: time.day,
  207. startSection: time.startSection,
  208. endSection: time.endSection,
  209. weeks: time.weeks,
  210. });
  211. });
  212. }
  213. if (hasAnySchedule && !hasSpecificSchedule) {
  214. skipped.push(name);
  215. }
  216. });
  217. return { courses, skipped };
  218. }
  219. async function saveImportResult(courses, skipped) {
  220. const { bridge, promise } = getBridge();
  221. if (!promise?.saveImportedCourses) {
  222. throw new Error(
  223. "未找到保存课表的 Bridge,请在拾光 App 或测试插件中执行。",
  224. );
  225. }
  226. await promise.saveCourseConfig(JSON.stringify(SWJTU_COURSE_CONFIG));
  227. await promise.savePresetTimeSlots(JSON.stringify(SWJTU_TIME_SLOTS));
  228. await promise.saveImportedCourses(JSON.stringify(courses));
  229. if (skipped.length > 0) {
  230. showToast(
  231. `成功导入 ${courses.length} 条课程,另有 ${skipped.length} 门无具体节次课程已跳过`,
  232. );
  233. } else {
  234. showToast(`成功导入 ${courses.length} 条课程`);
  235. }
  236. bridge?.notifyTaskCompletion?.();
  237. }
  238. async function importSwjtuSchedule() {
  239. try {
  240. showToast("正在通过西南交通大学新教务接口获取课表...");
  241. const apiCourses = await fetchSemesterSchedule();
  242. const { courses, skipped } = buildCourses(apiCourses);
  243. if (courses.length === 0) {
  244. throw new Error(
  245. `接口返回 ${apiCourses.length} 门课程,但没有解析到带星期和节次的上课安排。`,
  246. );
  247. }
  248. await saveImportResult(courses, skipped);
  249. return true;
  250. } catch (error) {
  251. console.error("SWJTU 新教务课表导入失败:", error);
  252. await showAlert("导入失败", `解析或保存课表失败:${error.message}`);
  253. return false;
  254. }
  255. }
  256. void importSwjtuSchedule();
  257. })();